File size: 12,114 Bytes
9990299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
import os
import json
import sqlite3
import requests
import random
import time
from flask import Flask, render_template, request, jsonify, g, send_from_directory
from werkzeug.utils import secure_filename
from werkzeug.exceptions import HTTPException

app = Flask(__name__)

# Config
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB Limit
app.config['UPLOAD_FOLDER'] = os.path.join(app.instance_path, 'uploads')
DB_PATH = os.path.join(app.instance_path, 'material_mind.db')

# API Configuration (SiliconFlow)
SILICONFLOW_API_KEY = os.environ.get("SILICONFLOW_API_KEY", "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi")
SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"

# Ensure directories exist
try:
    os.makedirs(app.instance_path, exist_ok=True)
    os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
except OSError:
    pass

# Database Helpers
def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DB_PATH)
        db.row_factory = sqlite3.Row
    return db

@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

def init_db():
    with app.app_context():
        db = get_db()
        # Experiments Table
        db.execute('''
            CREATE TABLE IF NOT EXISTS experiments (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                composition TEXT NOT NULL, -- JSON string
                properties TEXT, -- JSON string
                notes TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        # Datasets Table (New)
        db.execute('''
            CREATE TABLE IF NOT EXISTS datasets (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                filename TEXT NOT NULL,
                filepath TEXT NOT NULL,
                description TEXT,
                uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        db.commit()

# Initialize DB
init_db()

# --- Global Error Handlers ---
@app.errorhandler(404)
def page_not_found(e):
    return render_template('index.html'), 200 # SPA fallback or error page

@app.errorhandler(500)
def internal_server_error(e):
    return jsonify(error="Internal Server Error", message=str(e)), 500

@app.errorhandler(413)
def request_entity_too_large(e):
    return jsonify(error="File too large", message="File exceeds 16MB limit"), 413

# --- Routes ---

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/experiments', methods=['GET'])
def get_experiments():
    db = get_db()
    cur = db.execute('SELECT * FROM experiments ORDER BY created_at DESC')
    rows = cur.fetchall()
    experiments = []
    for row in rows:
        experiments.append({
            'id': row['id'],
            'title': row['title'],
            'composition': json.loads(row['composition']),
            'properties': json.loads(row['properties']) if row['properties'] else {},
            'notes': row['notes'],
            'created_at': row['created_at']
        })
    return jsonify(experiments)

@app.route('/api/experiments', methods=['POST'])
def create_experiment():
    data = request.json
    title = data.get('title', 'Untitled Experiment')
    composition = json.dumps(data.get('composition', {}))
    properties = json.dumps(data.get('properties', {}))
    notes = data.get('notes', '')
    
    db = get_db()
    cur = db.execute(
        'INSERT INTO experiments (title, composition, properties, notes) VALUES (?, ?, ?, ?)',
        (title, composition, properties, notes)
    )
    db.commit()
    return jsonify({'id': cur.lastrowid, 'status': 'success'})

@app.route('/api/experiments/<int:experiment_id>', methods=['DELETE'])
def delete_experiment(experiment_id):
    db = get_db()
    db.execute('DELETE FROM experiments WHERE id = ?', (experiment_id,))
    db.commit()
    return jsonify({'status': 'success'})

# --- Dataset/File Upload Routes ---
@app.route('/api/datasets', methods=['GET'])
def get_datasets():
    db = get_db()
    cur = db.execute('SELECT * FROM datasets ORDER BY uploaded_at DESC')
    rows = cur.fetchall()
    datasets = []
    for row in rows:
        datasets.append({
            'id': row['id'],
            'filename': row['filename'],
            'description': row['description'],
            'uploaded_at': row['uploaded_at']
        })
    return jsonify(datasets)

@app.route('/api/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'No file part'}), 400
    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': 'No selected file'}), 400
    
    if file:
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        
        description = request.form.get('description', 'Uploaded Dataset')
        
        db = get_db()
        db.execute('INSERT INTO datasets (filename, filepath, description) VALUES (?, ?, ?)',
                   (filename, filepath, description))
        db.commit()
        
        return jsonify({'status': 'success', 'filename': filename})

# --- Simulation Logic ---
@app.route('/api/simulate', methods=['POST'])
def simulate_properties():
    data = request.json
    composition = data.get('composition', {})
    
    # Base Properties
    base_strength = 200
    base_ductility = 50
    base_cost = 10
    
    # Contribution factors (Mock Database)
    factors = {
        'Fe': {'strength': 2.0, 'ductility': 1.0, 'cost': 1.0, 'corrosion': 1.0},
        'C':  {'strength': 12.0, 'ductility': -6.0, 'cost': 2.0, 'corrosion': -2.0},
        'Ni': {'strength': 4.0, 'ductility': 3.0, 'cost': 15.0, 'corrosion': 8.0},
        'Cr': {'strength': 5.0, 'ductility': 0.5, 'cost': 12.0, 'corrosion': 10.0},
        'Ti': {'strength': 9.0, 'ductility': -2.0, 'cost': 25.0, 'corrosion': 6.0},
        'Al': {'strength': 2.5, 'ductility': 0.0, 'cost': 5.0, 'corrosion': 4.0},
        'Cu': {'strength': 1.5, 'ductility': 2.0, 'cost': 8.0, 'corrosion': 3.0},
        'Mn': {'strength': 3.0, 'ductility': 1.5, 'cost': 3.0, 'corrosion': 1.0},
    }
    
    total_strength = base_strength
    total_ductility = base_ductility
    total_cost = base_cost
    total_corrosion = 50 # Base score
    
    # Normalize composition
    total_percent = sum(float(v) for v in composition.values())
    if total_percent == 0: total_percent = 1
    
    for elem, amount in composition.items():
        try:
            amount = float(amount)
        except ValueError:
            amount = 0
            
        f = factors.get(elem, {'strength': 1, 'ductility': 0, 'cost': 1, 'corrosion': 0})
        
        # Contribution Model
        total_strength += f['strength'] * amount * 0.6
        total_ductility += f['ductility'] * amount * 0.4
        total_cost += f['cost'] * amount * 0.1
        total_corrosion += f['corrosion'] * amount * 0.5
        
    # Apply some non-linear interactions (Mocking complex physics)
    # E.g., Cr + Ni synergy for corrosion
    cr = float(composition.get('Cr', 0))
    ni = float(composition.get('Ni', 0))
    if cr > 10 and ni > 5:
        total_corrosion *= 1.2 # Synergy bonus
        
    # Constraints
    total_strength = max(50, round(total_strength, 1))
    total_ductility = max(0.1, round(total_ductility, 1))
    total_cost = max(1, round(total_cost, 1))
    total_corrosion = min(100, max(0, round(total_corrosion, 1)))
    
    return jsonify({
        'tensile_strength': total_strength,
        'ductility': total_ductility,
        'cost_index': total_cost,
        'corrosion_resistance': total_corrosion,
        'melting_point': 1500 - (float(composition.get('C', 0)) * 50) + (float(composition.get('W', 0)) * 20) # Fake
    })

# --- Chat & AI Logic ---
@app.route('/api/chat', methods=['POST'])
def chat():
    data = request.json
    user_message = data.get('message', '')
    history = data.get('history', [])
    
    # System Prompt with specific instruction to return JSON for charts if needed
    system_prompt = {
        "role": "system", 
        "content": (
            "你是智材灵动(Material Mind)的AI助手,一位资深的材料科学家。"
            "请用专业、严谨但易懂的中文回答。"
            "如果你需要展示数据趋势或图表,请在回复的最后附加一个JSON代码块,"
            "格式为: ```json:chart { \"type\": \"bar|line|pie\", \"data\": { ... }, \"title\": \"...\" } ```。"
            "例如展示钢材强度对比:```json:chart { \"type\": \"bar\", \"title\": \"不同合金强度对比\", \"labels\": [\"合金A\", \"合金B\"], \"datasets\": [{ \"label\": \"强度(MPa)\", \"data\": [450, 600] }] } ```"
        )
    }
    
    messages = [system_prompt] + history + [{"role": "user", "content": user_message}]
    
    headers = {
        "Authorization": f"Bearer {SILICONFLOW_API_KEY}",
        "Content-Type": "application/json"
    }
    
    payload = {
        "model": "Qwen/Qwen2.5-7B-Instruct",
        "messages": messages,
        "stream": False,
        "max_tokens": 1024
    }
    
    try:
        response = requests.post(SILICONFLOW_API_URL, json=payload, headers=headers, timeout=30)
        response.raise_for_status()
        result = response.json()
        ai_content = result['choices'][0]['message']['content']
        return jsonify({'response': ai_content})
    except Exception as e:
        print(f"API Error: {e}")
        return mock_chat_response(user_message)

def mock_chat_response(message):
    """
    Mock Fallback that returns rich content (Markdown + JSON Charts)
    """
    time.sleep(1) # Simulate network delay
    
    base_response = "**Mock Mode (云端连接中断)**: 正在使用本地应急知识库。\n\n"
    
    if "强度" in message or "strength" in message:
        return jsonify({'response': base_response + 
            "关于材料强度,我们通常关注屈服强度和抗拉强度。添加碳(C)通常能显著提高钢的强度,但会降低延展性。\n\n"
            "以下是常见合金元素的强化效果对比:\n"
            "```json:chart\n"
            "{\n"
            "  \"type\": \"bar\",\n"
            "  \"title\": \"合金元素强化效果 (Mock Data)\",\n"
            "  \"labels\": [\"碳 (C)\", \"锰 (Mn)\", \"硅 (Si)\", \"铬 (Cr)\"],\n"
            "  \"datasets\": [{\n"
            "    \"label\": \"强化系数\",\n"
            "    \"data\": [12, 4, 3, 2],\n"
            "    \"backgroundColor\": [\"#ef4444\", \"#3b82f6\", \"#10b981\", \"#f59e0b\"]\n"
            "  }]\n"
            "}\n"
            "```"
        })
    elif "腐蚀" in message or "corrosion" in message:
        return jsonify({'response': base_response +
            "提高耐腐蚀性的关键是形成致密的氧化膜。铬(Cr)是实现这一点的关键元素(如不锈钢需含Cr > 10.5%)。\n\n"
            "不锈钢耐腐蚀性随Cr含量变化趋势:\n"
            "```json:chart\n"
            "{\n"
            "  \"type\": \"line\",\n"
            "  \"title\": \"Cr含量与耐腐蚀性\",\n"
            "  \"labels\": [\"0%\", \"5%\", \"10%\", \"15%\", \"20%\"],\n"
            "  \"datasets\": [{\n"
            "    \"label\": \"耐腐蚀指数\",\n"
            "    \"data\": [10, 25, 80, 95, 98],\n"
            "    \"borderColor\": \"#3b82f6\",\n"
            "    \"fill\": true\n"
            "  }]\n"
            "}\n"
            "```"
        })
    else:
        return jsonify({'response': base_response + 
            f"收到您的问题:“{message}”。\n"
            "作为一个材料科学助手,我可以帮您设计配方、预测性能或分析实验数据。\n"
            "尝试问我:“如何提高强度?”或者“不锈钢的配方是什么?”"
        })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=7860, debug=True)