Trae Assistant commited on
Commit
9990299
·
1 Parent(s): dc6ced4

feat: complete project setup with chinese localization, datasets support, and mock mode

Browse files
Files changed (5) hide show
  1. .gitignore +5 -0
  2. Dockerfile +16 -0
  3. app.py +330 -0
  4. requirements.txt +5 -0
  5. templates/index.html +711 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ instance/
4
+ .env
5
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ # Create instance directory for SQLite
11
+ RUN mkdir -p instance
12
+ RUN chmod 777 instance
13
+
14
+ EXPOSE 7860
15
+
16
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import sqlite3
4
+ import requests
5
+ import random
6
+ import time
7
+ from flask import Flask, render_template, request, jsonify, g, send_from_directory
8
+ from werkzeug.utils import secure_filename
9
+ from werkzeug.exceptions import HTTPException
10
+
11
+ app = Flask(__name__)
12
+
13
+ # Config
14
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB Limit
15
+ app.config['UPLOAD_FOLDER'] = os.path.join(app.instance_path, 'uploads')
16
+ DB_PATH = os.path.join(app.instance_path, 'material_mind.db')
17
+
18
+ # API Configuration (SiliconFlow)
19
+ SILICONFLOW_API_KEY = os.environ.get("SILICONFLOW_API_KEY", "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi")
20
+ SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
21
+
22
+ # Ensure directories exist
23
+ try:
24
+ os.makedirs(app.instance_path, exist_ok=True)
25
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
26
+ except OSError:
27
+ pass
28
+
29
+ # Database Helpers
30
+ def get_db():
31
+ db = getattr(g, '_database', None)
32
+ if db is None:
33
+ db = g._database = sqlite3.connect(DB_PATH)
34
+ db.row_factory = sqlite3.Row
35
+ return db
36
+
37
+ @app.teardown_appcontext
38
+ def close_connection(exception):
39
+ db = getattr(g, '_database', None)
40
+ if db is not None:
41
+ db.close()
42
+
43
+ def init_db():
44
+ with app.app_context():
45
+ db = get_db()
46
+ # Experiments Table
47
+ db.execute('''
48
+ CREATE TABLE IF NOT EXISTS experiments (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ title TEXT NOT NULL,
51
+ composition TEXT NOT NULL, -- JSON string
52
+ properties TEXT, -- JSON string
53
+ notes TEXT,
54
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
55
+ )
56
+ ''')
57
+ # Datasets Table (New)
58
+ db.execute('''
59
+ CREATE TABLE IF NOT EXISTS datasets (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ filename TEXT NOT NULL,
62
+ filepath TEXT NOT NULL,
63
+ description TEXT,
64
+ uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
65
+ )
66
+ ''')
67
+ db.commit()
68
+
69
+ # Initialize DB
70
+ init_db()
71
+
72
+ # --- Global Error Handlers ---
73
+ @app.errorhandler(404)
74
+ def page_not_found(e):
75
+ return render_template('index.html'), 200 # SPA fallback or error page
76
+
77
+ @app.errorhandler(500)
78
+ def internal_server_error(e):
79
+ return jsonify(error="Internal Server Error", message=str(e)), 500
80
+
81
+ @app.errorhandler(413)
82
+ def request_entity_too_large(e):
83
+ return jsonify(error="File too large", message="File exceeds 16MB limit"), 413
84
+
85
+ # --- Routes ---
86
+
87
+ @app.route('/')
88
+ def index():
89
+ return render_template('index.html')
90
+
91
+ @app.route('/api/experiments', methods=['GET'])
92
+ def get_experiments():
93
+ db = get_db()
94
+ cur = db.execute('SELECT * FROM experiments ORDER BY created_at DESC')
95
+ rows = cur.fetchall()
96
+ experiments = []
97
+ for row in rows:
98
+ experiments.append({
99
+ 'id': row['id'],
100
+ 'title': row['title'],
101
+ 'composition': json.loads(row['composition']),
102
+ 'properties': json.loads(row['properties']) if row['properties'] else {},
103
+ 'notes': row['notes'],
104
+ 'created_at': row['created_at']
105
+ })
106
+ return jsonify(experiments)
107
+
108
+ @app.route('/api/experiments', methods=['POST'])
109
+ def create_experiment():
110
+ data = request.json
111
+ title = data.get('title', 'Untitled Experiment')
112
+ composition = json.dumps(data.get('composition', {}))
113
+ properties = json.dumps(data.get('properties', {}))
114
+ notes = data.get('notes', '')
115
+
116
+ db = get_db()
117
+ cur = db.execute(
118
+ 'INSERT INTO experiments (title, composition, properties, notes) VALUES (?, ?, ?, ?)',
119
+ (title, composition, properties, notes)
120
+ )
121
+ db.commit()
122
+ return jsonify({'id': cur.lastrowid, 'status': 'success'})
123
+
124
+ @app.route('/api/experiments/<int:experiment_id>', methods=['DELETE'])
125
+ def delete_experiment(experiment_id):
126
+ db = get_db()
127
+ db.execute('DELETE FROM experiments WHERE id = ?', (experiment_id,))
128
+ db.commit()
129
+ return jsonify({'status': 'success'})
130
+
131
+ # --- Dataset/File Upload Routes ---
132
+ @app.route('/api/datasets', methods=['GET'])
133
+ def get_datasets():
134
+ db = get_db()
135
+ cur = db.execute('SELECT * FROM datasets ORDER BY uploaded_at DESC')
136
+ rows = cur.fetchall()
137
+ datasets = []
138
+ for row in rows:
139
+ datasets.append({
140
+ 'id': row['id'],
141
+ 'filename': row['filename'],
142
+ 'description': row['description'],
143
+ 'uploaded_at': row['uploaded_at']
144
+ })
145
+ return jsonify(datasets)
146
+
147
+ @app.route('/api/upload', methods=['POST'])
148
+ def upload_file():
149
+ if 'file' not in request.files:
150
+ return jsonify({'error': 'No file part'}), 400
151
+ file = request.files['file']
152
+ if file.filename == '':
153
+ return jsonify({'error': 'No selected file'}), 400
154
+
155
+ if file:
156
+ filename = secure_filename(file.filename)
157
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
158
+ file.save(filepath)
159
+
160
+ description = request.form.get('description', 'Uploaded Dataset')
161
+
162
+ db = get_db()
163
+ db.execute('INSERT INTO datasets (filename, filepath, description) VALUES (?, ?, ?)',
164
+ (filename, filepath, description))
165
+ db.commit()
166
+
167
+ return jsonify({'status': 'success', 'filename': filename})
168
+
169
+ # --- Simulation Logic ---
170
+ @app.route('/api/simulate', methods=['POST'])
171
+ def simulate_properties():
172
+ data = request.json
173
+ composition = data.get('composition', {})
174
+
175
+ # Base Properties
176
+ base_strength = 200
177
+ base_ductility = 50
178
+ base_cost = 10
179
+
180
+ # Contribution factors (Mock Database)
181
+ factors = {
182
+ 'Fe': {'strength': 2.0, 'ductility': 1.0, 'cost': 1.0, 'corrosion': 1.0},
183
+ 'C': {'strength': 12.0, 'ductility': -6.0, 'cost': 2.0, 'corrosion': -2.0},
184
+ 'Ni': {'strength': 4.0, 'ductility': 3.0, 'cost': 15.0, 'corrosion': 8.0},
185
+ 'Cr': {'strength': 5.0, 'ductility': 0.5, 'cost': 12.0, 'corrosion': 10.0},
186
+ 'Ti': {'strength': 9.0, 'ductility': -2.0, 'cost': 25.0, 'corrosion': 6.0},
187
+ 'Al': {'strength': 2.5, 'ductility': 0.0, 'cost': 5.0, 'corrosion': 4.0},
188
+ 'Cu': {'strength': 1.5, 'ductility': 2.0, 'cost': 8.0, 'corrosion': 3.0},
189
+ 'Mn': {'strength': 3.0, 'ductility': 1.5, 'cost': 3.0, 'corrosion': 1.0},
190
+ }
191
+
192
+ total_strength = base_strength
193
+ total_ductility = base_ductility
194
+ total_cost = base_cost
195
+ total_corrosion = 50 # Base score
196
+
197
+ # Normalize composition
198
+ total_percent = sum(float(v) for v in composition.values())
199
+ if total_percent == 0: total_percent = 1
200
+
201
+ for elem, amount in composition.items():
202
+ try:
203
+ amount = float(amount)
204
+ except ValueError:
205
+ amount = 0
206
+
207
+ f = factors.get(elem, {'strength': 1, 'ductility': 0, 'cost': 1, 'corrosion': 0})
208
+
209
+ # Contribution Model
210
+ total_strength += f['strength'] * amount * 0.6
211
+ total_ductility += f['ductility'] * amount * 0.4
212
+ total_cost += f['cost'] * amount * 0.1
213
+ total_corrosion += f['corrosion'] * amount * 0.5
214
+
215
+ # Apply some non-linear interactions (Mocking complex physics)
216
+ # E.g., Cr + Ni synergy for corrosion
217
+ cr = float(composition.get('Cr', 0))
218
+ ni = float(composition.get('Ni', 0))
219
+ if cr > 10 and ni > 5:
220
+ total_corrosion *= 1.2 # Synergy bonus
221
+
222
+ # Constraints
223
+ total_strength = max(50, round(total_strength, 1))
224
+ total_ductility = max(0.1, round(total_ductility, 1))
225
+ total_cost = max(1, round(total_cost, 1))
226
+ total_corrosion = min(100, max(0, round(total_corrosion, 1)))
227
+
228
+ return jsonify({
229
+ 'tensile_strength': total_strength,
230
+ 'ductility': total_ductility,
231
+ 'cost_index': total_cost,
232
+ 'corrosion_resistance': total_corrosion,
233
+ 'melting_point': 1500 - (float(composition.get('C', 0)) * 50) + (float(composition.get('W', 0)) * 20) # Fake
234
+ })
235
+
236
+ # --- Chat & AI Logic ---
237
+ @app.route('/api/chat', methods=['POST'])
238
+ def chat():
239
+ data = request.json
240
+ user_message = data.get('message', '')
241
+ history = data.get('history', [])
242
+
243
+ # System Prompt with specific instruction to return JSON for charts if needed
244
+ system_prompt = {
245
+ "role": "system",
246
+ "content": (
247
+ "你是智材灵动(Material Mind)的AI助手,一位资深的材料科学家。"
248
+ "请用专业、严谨但易懂的中文回答。"
249
+ "如果你需要展示数据趋势或图表,请在回复的最后附加一个JSON代码块,"
250
+ "格式为: ```json:chart { \"type\": \"bar|line|pie\", \"data\": { ... }, \"title\": \"...\" } ```。"
251
+ "例如展示钢材强度对比:```json:chart { \"type\": \"bar\", \"title\": \"不同合金强度对比\", \"labels\": [\"合金A\", \"合金B\"], \"datasets\": [{ \"label\": \"强度(MPa)\", \"data\": [450, 600] }] } ```"
252
+ )
253
+ }
254
+
255
+ messages = [system_prompt] + history + [{"role": "user", "content": user_message}]
256
+
257
+ headers = {
258
+ "Authorization": f"Bearer {SILICONFLOW_API_KEY}",
259
+ "Content-Type": "application/json"
260
+ }
261
+
262
+ payload = {
263
+ "model": "Qwen/Qwen2.5-7B-Instruct",
264
+ "messages": messages,
265
+ "stream": False,
266
+ "max_tokens": 1024
267
+ }
268
+
269
+ try:
270
+ response = requests.post(SILICONFLOW_API_URL, json=payload, headers=headers, timeout=30)
271
+ response.raise_for_status()
272
+ result = response.json()
273
+ ai_content = result['choices'][0]['message']['content']
274
+ return jsonify({'response': ai_content})
275
+ except Exception as e:
276
+ print(f"API Error: {e}")
277
+ return mock_chat_response(user_message)
278
+
279
+ def mock_chat_response(message):
280
+ """
281
+ Mock Fallback that returns rich content (Markdown + JSON Charts)
282
+ """
283
+ time.sleep(1) # Simulate network delay
284
+
285
+ base_response = "**Mock Mode (云端连接中断)**: 正在使用本地应急知识库。\n\n"
286
+
287
+ if "强度" in message or "strength" in message:
288
+ return jsonify({'response': base_response +
289
+ "关于材料强度,我们通常关注屈服强度和抗拉强度。添加碳(C)通常能显著提高钢的强度,但会降低延展性。\n\n"
290
+ "以下是常见合金元素的强化效果对比:\n"
291
+ "```json:chart\n"
292
+ "{\n"
293
+ " \"type\": \"bar\",\n"
294
+ " \"title\": \"合金元素强化效果 (Mock Data)\",\n"
295
+ " \"labels\": [\"碳 (C)\", \"锰 (Mn)\", \"硅 (Si)\", \"铬 (Cr)\"],\n"
296
+ " \"datasets\": [{\n"
297
+ " \"label\": \"强化系数\",\n"
298
+ " \"data\": [12, 4, 3, 2],\n"
299
+ " \"backgroundColor\": [\"#ef4444\", \"#3b82f6\", \"#10b981\", \"#f59e0b\"]\n"
300
+ " }]\n"
301
+ "}\n"
302
+ "```"
303
+ })
304
+ elif "腐蚀" in message or "corrosion" in message:
305
+ return jsonify({'response': base_response +
306
+ "提高耐腐蚀性的关键是形成致密的氧化膜。铬(Cr)是实现这一点的关键元素(如不锈钢需含Cr > 10.5%)。\n\n"
307
+ "不锈钢耐腐蚀性随Cr含量变化趋势:\n"
308
+ "```json:chart\n"
309
+ "{\n"
310
+ " \"type\": \"line\",\n"
311
+ " \"title\": \"Cr含量与耐腐蚀性\",\n"
312
+ " \"labels\": [\"0%\", \"5%\", \"10%\", \"15%\", \"20%\"],\n"
313
+ " \"datasets\": [{\n"
314
+ " \"label\": \"耐腐蚀指数\",\n"
315
+ " \"data\": [10, 25, 80, 95, 98],\n"
316
+ " \"borderColor\": \"#3b82f6\",\n"
317
+ " \"fill\": true\n"
318
+ " }]\n"
319
+ "}\n"
320
+ "```"
321
+ })
322
+ else:
323
+ return jsonify({'response': base_response +
324
+ f"收到您的问题:“{message}”。\n"
325
+ "作为一个材料科学助手,我可以帮您设计配方、预测性能或分析实验数据。\n"
326
+ "尝试问我:“如何提高强度?”或者“不锈钢的配方是什么?”"
327
+ })
328
+
329
+ if __name__ == '__main__':
330
+ app.run(host='0.0.0.0', port=7860, debug=True)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask==3.0.0
2
+ flask-cors==4.0.0
3
+ requests==2.31.0
4
+ python-dotenv==1.0.0
5
+ gunicorn==21.2.0
templates/index.html ADDED
@@ -0,0 +1,711 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>智材灵动 - AI材料研发平台</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
11
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
12
+ <style>
13
+ [v-cloak] { display: none; }
14
+ .markdown-body h1 { font-size: 1.5em; font-weight: bold; margin-bottom: 0.5em; }
15
+ .markdown-body h2 { font-size: 1.3em; font-weight: bold; margin-bottom: 0.5em; }
16
+ .markdown-body p { margin-bottom: 1em; }
17
+ .markdown-body ul { list-style-type: disc; margin-left: 1.5em; margin-bottom: 1em; }
18
+ .markdown-body code { background-color: #f3f4f6; padding: 0.2em 0.4em; border-radius: 0.25em; font-family: monospace; }
19
+ .markdown-body pre { background-color: #1f2937; color: #f3f4f6; padding: 1em; border-radius: 0.5em; overflow-x: auto; margin-bottom: 1em; }
20
+ .chart-container { width: 100%; height: 300px; margin-top: 1rem; margin-bottom: 1rem; }
21
+
22
+ /* Custom Scrollbar */
23
+ ::-webkit-scrollbar { width: 6px; }
24
+ ::-webkit-scrollbar-track { background: #f1f1f1; }
25
+ ::-webkit-scrollbar-thumb { background: #c7c7cc; border-radius: 3px; }
26
+ ::-webkit-scrollbar-thumb:hover { background: #a1a1aa; }
27
+ </style>
28
+ </head>
29
+ <body class="bg-slate-50 text-slate-800 h-screen flex overflow-hidden">
30
+ <div id="app" v-cloak class="flex w-full h-full">
31
+ <!-- Sidebar -->
32
+ <aside class="w-64 bg-slate-900 text-white flex flex-col hidden md:flex shadow-2xl z-20">
33
+ <div class="p-6 border-b border-slate-800 flex items-center gap-3">
34
+ <div class="w-10 h-10 rounded-full bg-gradient-to-br from-teal-400 to-indigo-600 flex items-center justify-center shadow-lg">
35
+ <i class="fa-solid fa-atom text-white text-xl"></i>
36
+ </div>
37
+ <div>
38
+ <h1 class="text-xl font-bold tracking-tight">智材灵动</h1>
39
+ <p class="text-xs text-slate-400 font-mono">Material Mind v2.0</p>
40
+ </div>
41
+ </div>
42
+ <nav class="flex-1 p-4 space-y-2">
43
+ <button @click="currentTab = 'dashboard'" :class="{'bg-slate-800 text-teal-400 border-l-4 border-teal-400': currentTab === 'dashboard', 'text-slate-400 hover:bg-slate-800 hover:text-white': currentTab !== 'dashboard'}" class="w-full flex items-center gap-3 px-4 py-3 rounded-r-lg transition-all duration-200 group">
44
+ <i class="fa-solid fa-chart-pie w-5 group-hover:scale-110 transition-transform"></i> 创新雷达
45
+ </button>
46
+ <button @click="currentTab = 'composer'" :class="{'bg-slate-800 text-teal-400 border-l-4 border-teal-400': currentTab === 'composer', 'text-slate-400 hover:bg-slate-800 hover:text-white': currentTab !== 'composer'}" class="w-full flex items-center gap-3 px-4 py-3 rounded-r-lg transition-all duration-200 group">
47
+ <i class="fa-solid fa-flask w-5 group-hover:scale-110 transition-transform"></i> 配方合成器
48
+ </button>
49
+ <button @click="currentTab = 'chat'" :class="{'bg-slate-800 text-teal-400 border-l-4 border-teal-400': currentTab === 'chat', 'text-slate-400 hover:bg-slate-800 hover:text-white': currentTab !== 'chat'}" class="w-full flex items-center gap-3 px-4 py-3 rounded-r-lg transition-all duration-200 group">
50
+ <i class="fa-solid fa-robot w-5 group-hover:scale-110 transition-transform"></i> 实验助手
51
+ </button>
52
+ <button @click="currentTab = 'experiments'" :class="{'bg-slate-800 text-teal-400 border-l-4 border-teal-400': currentTab === 'experiments', 'text-slate-400 hover:bg-slate-800 hover:text-white': currentTab !== 'experiments'}" class="w-full flex items-center gap-3 px-4 py-3 rounded-r-lg transition-all duration-200 group">
53
+ <i class="fa-solid fa-database w-5 group-hover:scale-110 transition-transform"></i> 资产库
54
+ </button>
55
+ <button @click="currentTab = 'datasets'" :class="{'bg-slate-800 text-teal-400 border-l-4 border-teal-400': currentTab === 'datasets', 'text-slate-400 hover:bg-slate-800 hover:text-white': currentTab !== 'datasets'}" class="w-full flex items-center gap-3 px-4 py-3 rounded-r-lg transition-all duration-200 group">
56
+ <i class="fa-solid fa-file-upload w-5 group-hover:scale-110 transition-transform"></i> 数据集
57
+ </button>
58
+ </nav>
59
+ <div class="p-4 border-t border-slate-800 text-xs text-slate-500 text-center">
60
+ &copy; 2026 Material Mind Lab<br>Running on Hugging Face
61
+ </div>
62
+ </aside>
63
+
64
+ <!-- Mobile Header -->
65
+ <div class="md:hidden fixed top-0 left-0 right-0 bg-slate-900 text-white z-50 px-4 py-3 flex justify-between items-center shadow-md">
66
+ <div class="flex items-center gap-2">
67
+ <i class="fa-solid fa-atom text-teal-400"></i>
68
+ <span class="font-bold">智材灵动</span>
69
+ </div>
70
+ <button @click="mobileMenuOpen = !mobileMenuOpen" class="text-white">
71
+ <i class="fa-solid fa-bars text-xl"></i>
72
+ </button>
73
+ </div>
74
+
75
+ <!-- Mobile Menu -->
76
+ <div v-if="mobileMenuOpen" class="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden" @click="mobileMenuOpen = false"></div>
77
+ <div v-if="mobileMenuOpen" class="fixed right-0 top-0 bottom-0 w-64 bg-slate-900 z-50 p-4 transform transition-transform duration-300">
78
+ <nav class="space-y-4 mt-10">
79
+ <button @click="currentTab = 'dashboard'; mobileMenuOpen = false" class="w-full text-left text-white p-2 border-b border-slate-700">创新雷达</button>
80
+ <button @click="currentTab = 'composer'; mobileMenuOpen = false" class="w-full text-left text-white p-2 border-b border-slate-700">配方合成器</button>
81
+ <button @click="currentTab = 'chat'; mobileMenuOpen = false" class="w-full text-left text-white p-2 border-b border-slate-700">实验助手</button>
82
+ <button @click="currentTab = 'experiments'; mobileMenuOpen = false" class="w-full text-left text-white p-2 border-b border-slate-700">资产库</button>
83
+ <button @click="currentTab = 'datasets'; mobileMenuOpen = false" class="w-full text-left text-white p-2 border-b border-slate-700">数据集</button>
84
+ </nav>
85
+ </div>
86
+
87
+ <!-- Main Content -->
88
+ <main class="flex-1 overflow-y-auto p-4 md:p-8 pt-16 md:pt-8 bg-slate-50 relative">
89
+
90
+ <!-- Toast Notification -->
91
+ <div v-if="toast.show" class="fixed top-4 right-4 z-50 px-6 py-4 rounded-lg shadow-lg flex items-center gap-3 animate-bounce-in" :class="{'bg-green-500 text-white': toast.type === 'success', 'bg-red-500 text-white': toast.type === 'error'}">
92
+ <i class="fa-solid" :class="{'fa-check-circle': toast.type === 'success', 'fa-exclamation-circle': toast.type === 'error'}"></i>
93
+ <span class="font-medium">${ toast.message }</span>
94
+ </div>
95
+
96
+ <!-- Dashboard View -->
97
+ <div v-if="currentTab === 'dashboard'" class="space-y-6 max-w-6xl mx-auto">
98
+ <header class="flex justify-between items-end border-b border-slate-200 pb-4">
99
+ <div>
100
+ <h2 class="text-3xl font-bold text-slate-800">实验室概览</h2>
101
+ <p class="text-slate-500 mt-1">欢迎回来,今日实验室运行状态良好</p>
102
+ </div>
103
+ <div class="text-sm text-slate-400">
104
+ <i class="fa-regular fa-calendar mr-1"></i> ${ new Date().toLocaleDateString() }
105
+ </div>
106
+ </header>
107
+
108
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
109
+ <div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition">
110
+ <div class="flex justify-between items-start mb-4">
111
+ <div class="p-3 bg-indigo-50 rounded-lg text-indigo-600">
112
+ <i class="fa-solid fa-flask text-xl"></i>
113
+ </div>
114
+ <span class="text-xs font-bold px-2 py-1 bg-green-100 text-green-700 rounded-full">+12%</span>
115
+ </div>
116
+ <div class="text-slate-500 text-sm mb-1">已归档实验</div>
117
+ <div class="text-3xl font-bold text-slate-800">${ experiments.length || 5 }</div>
118
+ </div>
119
+ <div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition">
120
+ <div class="flex justify-between items-start mb-4">
121
+ <div class="p-3 bg-teal-50 rounded-lg text-teal-600">
122
+ <i class="fa-solid fa-bolt text-xl"></i>
123
+ </div>
124
+ </div>
125
+ <div class="text-slate-500 text-sm mb-1">今日模拟次数</div>
126
+ <div class="text-3xl font-bold text-slate-800">${ simulationCount }</div>
127
+ </div>
128
+ <div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 hover:shadow-md transition">
129
+ <div class="flex justify-between items-start mb-4">
130
+ <div class="p-3 bg-purple-50 rounded-lg text-purple-600">
131
+ <i class="fa-solid fa-comments text-xl"></i>
132
+ </div>
133
+ </div>
134
+ <div class="text-slate-500 text-sm mb-1">AI 咨询量</div>
135
+ <div class="text-3xl font-bold text-slate-800">${ Math.floor(chatHistory.length / 2) }</div>
136
+ </div>
137
+ </div>
138
+
139
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
140
+ <div class="lg:col-span-2 bg-white p-6 rounded-xl shadow-sm border border-slate-100">
141
+ <h3 class="font-bold mb-4 text-slate-700 flex items-center gap-2">
142
+ <i class="fa-solid fa-chart-line text-indigo-500"></i> 近期材料性能趋势
143
+ </h3>
144
+ <div id="trendChart" class="w-full h-80"></div>
145
+ </div>
146
+ <div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
147
+ <h3 class="font-bold mb-4 text-slate-700 flex items-center gap-2">
148
+ <i class="fa-solid fa-radar text-teal-500"></i> 多维雷达分析
149
+ </h3>
150
+ <div id="radarChart" class="w-full h-80"></div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <!-- Composer View -->
156
+ <div v-if="currentTab === 'composer'" class="space-y-6 max-w-6xl mx-auto">
157
+ <div class="flex justify-between items-center">
158
+ <h2 class="text-2xl font-bold text-slate-800">材料配方合成器</h2>
159
+ <button @click="resetComposition" class="text-sm text-slate-500 hover:text-indigo-600 bg-white px-3 py-1 rounded border border-slate-200 shadow-sm"><i class="fa-solid fa-rotate-right"></i> 重置</button>
160
+ </div>
161
+
162
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
163
+ <!-- Controls -->
164
+ <div class="bg-white p-6 rounded-xl shadow-sm border border-slate-100">
165
+ <h3 class="font-bold mb-4 text-slate-700 border-b pb-2">元素配比 (%)</h3>
166
+ <div class="space-y-5">
167
+ <div v-for="(value, elem) in composition" :key="elem" class="group">
168
+ <div class="flex justify-between mb-2">
169
+ <label class="font-medium text-slate-700 flex items-center gap-2">
170
+ <span class="w-6 h-6 rounded bg-slate-100 flex items-center justify-center text-xs font-bold text-slate-600">${ elem }</span>
171
+ ${ getElementName(elem) }
172
+ </label>
173
+ <span class="text-sm text-indigo-600 font-bold bg-indigo-50 px-2 rounded">${ value }%</span>
174
+ </div>
175
+ <input type="range" v-model.number="composition[elem]" min="0" max="100" class="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-indigo-600 group-hover:bg-slate-300 transition">
176
+ </div>
177
+ </div>
178
+ <div class="mt-8 pt-4 border-t border-slate-100 flex justify-between items-center">
179
+ <div class="text-sm text-slate-500 font-medium">
180
+ 总计: <span :class="{'text-red-500': totalPercent > 100, 'text-green-500': totalPercent <= 100}" class="text-lg font-bold">${ totalPercent }%</span>
181
+ </div>
182
+ <button @click="simulate" :disabled="isSimulating" class="bg-indigo-600 text-white px-8 py-2.5 rounded-lg hover:bg-indigo-700 transition shadow-lg shadow-indigo-200 disabled:opacity-50 disabled:shadow-none flex items-center gap-2">
183
+ <i class="fa-solid fa-flask" :class="{'fa-spin': isSimulating}"></i>
184
+ ${ isSimulating ? '模拟计算中...' : '开始模拟' }
185
+ </button>
186
+ </div>
187
+ </div>
188
+
189
+ <!-- Results -->
190
+ <div class="space-y-6">
191
+ <div v-if="simulationResult" class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 animate-fade-in">
192
+ <h3 class="font-bold mb-4 text-slate-700 border-b pb-2">预测性能指标</h3>
193
+ <div class="grid grid-cols-2 gap-4">
194
+ <div class="bg-indigo-50 p-4 rounded-xl border border-indigo-100">
195
+ <div class="text-xs text-indigo-500 uppercase font-bold tracking-wider">抗拉强度 (MPa)</div>
196
+ <div class="text-2xl font-bold text-indigo-800 mt-1">${ simulationResult.tensile_strength }</div>
197
+ <div class="w-full bg-indigo-200 h-1.5 rounded-full mt-2 overflow-hidden">
198
+ <div class="bg-indigo-500 h-full" :style="{width: Math.min(simulationResult.tensile_strength / 10, 100) + '%'}"></div>
199
+ </div>
200
+ </div>
201
+ <div class="bg-teal-50 p-4 rounded-xl border border-teal-100">
202
+ <div class="text-xs text-teal-500 uppercase font-bold tracking-wider">延展性 (%)</div>
203
+ <div class="text-2xl font-bold text-teal-800 mt-1">${ simulationResult.ductility }</div>
204
+ <div class="w-full bg-teal-200 h-1.5 rounded-full mt-2 overflow-hidden">
205
+ <div class="bg-teal-500 h-full" :style="{width: Math.min(simulationResult.ductility, 100) + '%'}"></div>
206
+ </div>
207
+ </div>
208
+ <div class="bg-purple-50 p-4 rounded-xl border border-purple-100">
209
+ <div class="text-xs text-purple-500 uppercase font-bold tracking-wider">成本指数</div>
210
+ <div class="text-2xl font-bold text-purple-800 mt-1">${ simulationResult.cost_index }</div>
211
+ </div>
212
+ <div class="bg-orange-50 p-4 rounded-xl border border-orange-100">
213
+ <div class="text-xs text-orange-500 uppercase font-bold tracking-wider">耐腐蚀评分</div>
214
+ <div class="text-2xl font-bold text-orange-800 mt-1">${ simulationResult.corrosion_resistance }</div>
215
+ </div>
216
+ </div>
217
+
218
+ <div class="mt-6">
219
+ <label class="block text-sm font-medium text-slate-700 mb-2">实验备注</label>
220
+ <textarea v-model="newExperimentNote" placeholder="输入实验备注..." class="w-full border border-slate-300 rounded-lg p-3 text-sm h-24 focus:ring-2 focus:ring-indigo-500 outline-none transition"></textarea>
221
+ <button @click="saveExperiment" class="mt-4 w-full border border-indigo-600 text-indigo-600 py-2.5 rounded-lg hover:bg-indigo-50 transition font-medium flex justify-center items-center gap-2">
222
+ <i class="fa-solid fa-save"></i> 保存到资产库
223
+ </button>
224
+ </div>
225
+ </div>
226
+
227
+ <div v-else class="bg-slate-100 border-2 border-dashed border-slate-300 rounded-xl h-64 flex flex-col items-center justify-center text-slate-400">
228
+ <div class="w-16 h-16 bg-slate-200 rounded-full flex items-center justify-center mb-4">
229
+ <i class="fa-solid fa-chart-simple text-2xl text-slate-400"></i>
230
+ </div>
231
+ <p class="font-medium">调整左侧配方并点击“开始模拟”</p>
232
+ <p class="text-sm mt-1">AI 模型将为您预测材料性能</p>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ <!-- Chat View -->
239
+ <div v-if="currentTab === 'chat'" class="flex flex-col h-[calc(100vh-6rem)] max-w-5xl mx-auto">
240
+ <div class="flex-1 overflow-y-auto space-y-6 p-4" id="chat-container">
241
+ <div v-if="chatHistory.length === 0" class="text-center text-slate-400 mt-20">
242
+ <div class="w-24 h-24 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-6">
243
+ <i class="fa-solid fa-robot text-4xl text-indigo-500"></i>
244
+ </div>
245
+ <h3 class="text-lg font-bold text-slate-700 mb-2">我是您的实验智能助手</h3>
246
+ <p class="mb-6">我可以帮您分析配方、预测性能或解答材料科学问题。</p>
247
+ <div class="flex flex-wrap justify-center gap-2 max-w-lg mx-auto">
248
+ <button @click="userInput='如何提高钢材的耐腐蚀性?'; sendMessage()" class="bg-white border border-slate-200 px-4 py-2 rounded-full text-sm hover:bg-slate-50 hover:border-indigo-300 transition">如何提高耐腐蚀性?</button>
249
+ <button @click="userInput='解释一下马氏体相变'; sendMessage()" class="bg-white border border-slate-200 px-4 py-2 rounded-full text-sm hover:bg-slate-50 hover:border-indigo-300 transition">解释马氏体相变</button>
250
+ <button @click="userInput='生成一个高强度低成本的配方'; sendMessage()" class="bg-white border border-slate-200 px-4 py-2 rounded-full text-sm hover:bg-slate-50 hover:border-indigo-300 transition">推荐高强度配方</button>
251
+ </div>
252
+ </div>
253
+
254
+ <div v-for="(msg, index) in chatHistory" :key="index" :class="{'flex justify-end': msg.role === 'user', 'flex justify-start': msg.role === 'assistant'}">
255
+ <div class="flex items-start gap-3 max-w-[85%]">
256
+ <div v-if="msg.role === 'assistant'" class="w-8 h-8 rounded-full bg-indigo-600 flex-shrink-0 flex items-center justify-center mt-1">
257
+ <i class="fa-solid fa-robot text-white text-xs"></i>
258
+ </div>
259
+
260
+ <div :class="{'bg-indigo-600 text-white': msg.role === 'user', 'bg-white border border-slate-200 text-slate-800 shadow-sm': msg.role === 'assistant'}" class="rounded-2xl px-5 py-4">
261
+ <div v-if="msg.role === 'assistant'" class="markdown-body text-sm">
262
+ <div v-html="renderMarkdown(msg.content)"></div>
263
+ </div>
264
+ <div v-else class="text-sm">${ msg.content }</div>
265
+ </div>
266
+
267
+ <div v-if="msg.role === 'user'" class="w-8 h-8 rounded-full bg-slate-200 flex-shrink-0 flex items-center justify-center mt-1">
268
+ <i class="fa-solid fa-user text-slate-500 text-xs"></i>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <div v-if="isChatting" class="flex justify-start">
274
+ <div class="flex items-center gap-3">
275
+ <div class="w-8 h-8 rounded-full bg-indigo-600 flex-shrink-0 flex items-center justify-center">
276
+ <i class="fa-solid fa-robot text-white text-xs"></i>
277
+ </div>
278
+ <div class="bg-slate-100 rounded-2xl px-5 py-3 text-slate-500 text-sm flex items-center gap-2">
279
+ <span class="flex space-x-1">
280
+ <span class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0s"></span>
281
+ <span class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></span>
282
+ <span class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0.4s"></span>
283
+ </span>
284
+ <span>AI 正在思考...</span>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+
290
+ <div class="p-4 pt-2">
291
+ <div class="bg-white border border-slate-300 rounded-xl p-2 shadow-sm flex items-end gap-2 focus-within:ring-2 focus-within:ring-indigo-500 focus-within:border-indigo-500 transition">
292
+ <textarea v-model="userInput" @keydown.enter.prevent="sendMessage" placeholder="输入您的问题..." class="flex-1 bg-transparent border-none outline-none text-slate-700 resize-none max-h-32 p-2" rows="1"></textarea>
293
+ <button @click="sendMessage" :disabled="!userInput.trim() || isChatting" class="bg-indigo-600 text-white w-10 h-10 rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:bg-slate-300 flex items-center justify-center transition flex-shrink-0">
294
+ <i class="fa-solid fa-paper-plane"></i>
295
+ </button>
296
+ </div>
297
+ <div class="text-center text-xs text-slate-400 mt-2">AI 生成内容仅供参考,请以实验数据为准</div>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- Experiments View -->
302
+ <div v-if="currentTab === 'experiments'" class="space-y-6 max-w-6xl mx-auto">
303
+ <div class="flex justify-between items-center">
304
+ <h2 class="text-2xl font-bold text-slate-800">研发资产库</h2>
305
+ <div class="flex gap-2">
306
+ <div class="relative">
307
+ <input type="text" placeholder="搜索实验..." class="pl-9 pr-4 py-2 rounded-lg border border-slate-200 text-sm focus:ring-2 focus:ring-indigo-500 outline-none">
308
+ <i class="fa-solid fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 text-xs"></i>
309
+ </div>
310
+ </div>
311
+ </div>
312
+
313
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
314
+ <div v-for="exp in experiments" :key="exp.id" class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden hover:shadow-lg hover:-translate-y-1 transition duration-300 group">
315
+ <div class="p-4 border-b border-slate-50 flex justify-between items-start bg-slate-50 group-hover:bg-indigo-50 transition">
316
+ <div>
317
+ <h3 class="font-bold text-slate-800 line-clamp-1" :title="exp.title">${ exp.title }</h3>
318
+ <p class="text-xs text-slate-500">${ new Date(exp.created_at).toLocaleString() }</p>
319
+ </div>
320
+ <button @click="deleteExperiment(exp.id)" class="text-slate-300 hover:text-red-500 transition">
321
+ <i class="fa-solid fa-trash"></i>
322
+ </button>
323
+ </div>
324
+ <div class="p-4 space-y-4">
325
+ <div>
326
+ <div class="text-xs text-slate-400 uppercase font-bold mb-2">主要成分</div>
327
+ <div class="flex flex-wrap gap-1">
328
+ <span v-for="(v, k) in exp.composition" v-if="v > 0" :key="k" class="text-xs bg-slate-100 text-slate-600 px-2 py-1 rounded font-mono">
329
+ ${ k }:${ v }%
330
+ </span>
331
+ </div>
332
+ </div>
333
+ <div v-if="exp.properties">
334
+ <div class="text-xs text-slate-400 uppercase font-bold mb-2">关键性能</div>
335
+ <div class="grid grid-cols-2 gap-2 text-sm">
336
+ <div class="bg-indigo-50 rounded p-2 text-center">
337
+ <div class="text-xs text-indigo-400">强度</div>
338
+ <div class="font-bold text-indigo-700">${ exp.properties.tensile_strength }</div>
339
+ </div>
340
+ <div class="bg-teal-50 rounded p-2 text-center">
341
+ <div class="text-xs text-teal-400">延展</div>
342
+ <div class="font-bold text-teal-700">${ exp.properties.ductility }</div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ <div v-if="exp.notes" class="text-sm text-slate-500 italic bg-slate-50 p-2 rounded border border-slate-100 line-clamp-2">
347
+ <i class="fa-solid fa-quote-left text-slate-300 mr-1"></i>${ exp.notes }
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ <div v-if="experiments.length === 0" class="col-span-full text-center py-20 text-slate-400">
353
+ <div class="w-20 h-20 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
354
+ <i class="fa-solid fa-flask text-3xl text-slate-300"></i>
355
+ </div>
356
+ <p class="text-lg">暂无实验记录</p>
357
+ <p class="text-sm">前往“配方合成器”开始您的第一次实验</p>
358
+ </div>
359
+ </div>
360
+ </div>
361
+
362
+ <!-- Datasets View -->
363
+ <div v-if="currentTab === 'datasets'" class="space-y-6 max-w-6xl mx-auto">
364
+ <div class="flex justify-between items-center">
365
+ <h2 class="text-2xl font-bold text-slate-800">数据集管理</h2>
366
+ <button @click="triggerUpload" class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition flex items-center gap-2">
367
+ <i class="fa-solid fa-cloud-arrow-up"></i> 上传数据
368
+ </button>
369
+ <!-- Hidden File Input -->
370
+ <input type="file" ref="fileInput" @change="handleFileUpload" class="hidden">
371
+ </div>
372
+
373
+ <div class="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
374
+ <table class="w-full text-sm text-left text-slate-500">
375
+ <thead class="text-xs text-slate-700 uppercase bg-slate-50">
376
+ <tr>
377
+ <th scope="col" class="px-6 py-3">文件名</th>
378
+ <th scope="col" class="px-6 py-3">描述</th>
379
+ <th scope="col" class="px-6 py-3">上传时间</th>
380
+ <th scope="col" class="px-6 py-3 text-right">操作</th>
381
+ </tr>
382
+ </thead>
383
+ <tbody>
384
+ <tr v-for="ds in datasets" :key="ds.id" class="bg-white border-b hover:bg-slate-50">
385
+ <td class="px-6 py-4 font-medium text-slate-900 flex items-center gap-2">
386
+ <i class="fa-solid fa-file-csv text-green-500 text-lg"></i>
387
+ ${ ds.filename }
388
+ </td>
389
+ <td class="px-6 py-4">${ ds.description }</td>
390
+ <td class="px-6 py-4">${ new Date(ds.uploaded_at).toLocaleString() }</td>
391
+ <td class="px-6 py-4 text-right">
392
+ <a href="#" class="font-medium text-indigo-600 hover:underline">下载</a>
393
+ </td>
394
+ </tr>
395
+ <tr v-if="datasets.length === 0">
396
+ <td colspan="4" class="px-6 py-10 text-center text-slate-400">
397
+ 暂无上传的数据集
398
+ </td>
399
+ </tr>
400
+ </tbody>
401
+ </table>
402
+ </div>
403
+ </div>
404
+
405
+ </main>
406
+ </div>
407
+
408
+ <script>
409
+ const { createApp, ref, computed, onMounted, nextTick } = Vue;
410
+
411
+ createApp({
412
+ delimiters: ['${', '}'],
413
+ setup() {
414
+ const currentTab = ref('dashboard');
415
+ const mobileMenuOpen = ref(false);
416
+ const isSimulating = ref(false);
417
+ const simulationResult = ref(null);
418
+ const newExperimentNote = ref('');
419
+ const simulationCount = ref(0);
420
+ const fileInput = ref(null);
421
+
422
+ // Toast
423
+ const toast = ref({ show: false, message: '', type: 'success' });
424
+ const showToast = (msg, type='success') => {
425
+ toast.value = { show: true, message: msg, type };
426
+ setTimeout(() => toast.value.show = false, 3000);
427
+ };
428
+
429
+ // Chat
430
+ const userInput = ref('');
431
+ const isChatting = ref(false);
432
+ const chatHistory = ref([]);
433
+
434
+ // Data
435
+ const experiments = ref([]);
436
+ const datasets = ref([]);
437
+ const composition = ref({
438
+ 'Fe': 80, 'C': 5, 'Ni': 5, 'Cr': 5, 'Ti': 0, 'Al': 5, 'Cu': 0, 'Mn': 0
439
+ });
440
+
441
+ const totalPercent = computed(() => {
442
+ return Object.values(composition.value).reduce((a, b) => a + b, 0);
443
+ });
444
+
445
+ const getElementName = (symbol) => {
446
+ const names = {'Fe': '铁', 'C': '碳', 'Ni': '镍', 'Cr': '铬', 'Ti': '钛', 'Al': '铝', 'Cu': '铜', 'Mn': '锰'};
447
+ return names[symbol] || symbol;
448
+ };
449
+
450
+ // Charts
451
+ let radarChart = null;
452
+ let trendChart = null;
453
+
454
+ const updateCharts = () => {
455
+ nextTick(() => {
456
+ if (currentTab.value === 'dashboard') {
457
+ initDashboardCharts();
458
+ }
459
+ });
460
+ };
461
+
462
+ const initDashboardCharts = () => {
463
+ const chartDom = document.getElementById('radarChart');
464
+ if (chartDom) {
465
+ if (radarChart) radarChart.dispose();
466
+ radarChart = echarts.init(chartDom);
467
+ const option = {
468
+ radar: {
469
+ indicator: [
470
+ { name: '强度', max: 500 },
471
+ { name: '延展性', max: 100 },
472
+ { name: '成本', max: 100 },
473
+ { name: '耐腐蚀', max: 100 },
474
+ { name: '熔点', max: 2000 }
475
+ ]
476
+ },
477
+ series: [{
478
+ name: '性能分布',
479
+ type: 'radar',
480
+ data: [
481
+ {
482
+ value: [350, 40, 20, 60, 1500],
483
+ name: '当前平均',
484
+ itemStyle: { color: '#6366f1' },
485
+ areaStyle: { color: 'rgba(99, 102, 241, 0.2)' }
486
+ }
487
+ ]
488
+ }]
489
+ };
490
+ radarChart.setOption(option);
491
+ }
492
+
493
+ const trendDom = document.getElementById('trendChart');
494
+ if (trendDom) {
495
+ if (trendChart) trendChart.dispose();
496
+ trendChart = echarts.init(trendDom);
497
+ // Mock Trend Data based on experiments
498
+ const data = experiments.value.slice(0, 10).map(e => e.properties?.tensile_strength || 0);
499
+ const dates = experiments.value.slice(0, 10).map(e => new Date(e.created_at).toLocaleDateString());
500
+
501
+ const option = {
502
+ tooltip: { trigger: 'axis' },
503
+ xAxis: { type: 'category', data: dates.length ? dates : ['Mon', 'Tue', 'Wed'] },
504
+ yAxis: { type: 'value' },
505
+ series: [{
506
+ data: data.length ? data : [150, 230, 224],
507
+ type: 'line',
508
+ smooth: true,
509
+ itemStyle: { color: '#14b8a6' },
510
+ areaStyle: { color: 'rgba(20, 184, 166, 0.1)' }
511
+ }]
512
+ };
513
+ trendChart.setOption(option);
514
+ }
515
+ };
516
+
517
+ // API Calls
518
+ const fetchExperiments = async () => {
519
+ try {
520
+ const res = await fetch('/api/experiments');
521
+ experiments.value = await res.json();
522
+ updateCharts();
523
+ } catch (e) {
524
+ console.error(e);
525
+ }
526
+ };
527
+
528
+ const fetchDatasets = async () => {
529
+ try {
530
+ const res = await fetch('/api/datasets');
531
+ datasets.value = await res.json();
532
+ } catch (e) {
533
+ console.error(e);
534
+ }
535
+ };
536
+
537
+ const simulate = async () => {
538
+ isSimulating.value = true;
539
+ try {
540
+ const res = await fetch('/api/simulate', {
541
+ method: 'POST',
542
+ headers: {'Content-Type': 'application/json'},
543
+ body: JSON.stringify({ composition: composition.value })
544
+ });
545
+ simulationResult.value = await res.json();
546
+ simulationCount.value++;
547
+ showToast('模拟计算完成', 'success');
548
+ } catch (e) {
549
+ showToast('模拟失败', 'error');
550
+ } finally {
551
+ isSimulating.value = false;
552
+ }
553
+ };
554
+
555
+ const saveExperiment = async () => {
556
+ if (!simulationResult.value) return;
557
+ const title = `Exp #${experiments.value.length + 1} - ${new Date().toLocaleTimeString()}`;
558
+ try {
559
+ await fetch('/api/experiments', {
560
+ method: 'POST',
561
+ headers: {'Content-Type': 'application/json'},
562
+ body: JSON.stringify({
563
+ title,
564
+ composition: composition.value,
565
+ properties: simulationResult.value,
566
+ notes: newExperimentNote.value
567
+ })
568
+ });
569
+ newExperimentNote.value = '';
570
+ simulationResult.value = null;
571
+ showToast('已保存到资产库', 'success');
572
+ fetchExperiments();
573
+ } catch (e) {
574
+ showToast('保存失败', 'error');
575
+ }
576
+ };
577
+
578
+ const deleteExperiment = async (id) => {
579
+ if (!confirm('确定删除此实验记录吗?')) return;
580
+ try {
581
+ await fetch(`/api/experiments/${id}`, { method: 'DELETE' });
582
+ experiments.value = experiments.value.filter(e => e.id !== id);
583
+ showToast('删除成功', 'success');
584
+ } catch (e) {
585
+ showToast('删除失败', 'error');
586
+ }
587
+ };
588
+
589
+ // File Upload
590
+ const triggerUpload = () => {
591
+ fileInput.value.click();
592
+ };
593
+
594
+ const handleFileUpload = async (event) => {
595
+ const file = event.target.files[0];
596
+ if (!file) return;
597
+
598
+ const formData = new FormData();
599
+ formData.append('file', file);
600
+ formData.append('description', '用户上传数据集');
601
+
602
+ try {
603
+ showToast('正在上传...', 'info');
604
+ const res = await fetch('/api/upload', {
605
+ method: 'POST',
606
+ body: formData
607
+ });
608
+ if (!res.ok) throw new Error('Upload failed');
609
+ showToast('文件上传成功', 'success');
610
+ fetchDatasets();
611
+ } catch (e) {
612
+ showToast('上传失败: ' + e.message, 'error');
613
+ }
614
+ event.target.value = ''; // Reset
615
+ };
616
+
617
+ // Chat Logic with Chart Rendering
618
+ const sendMessage = async () => {
619
+ if (!userInput.value.trim()) return;
620
+ const msg = userInput.value;
621
+ chatHistory.value.push({ role: 'user', content: msg });
622
+ userInput.value = '';
623
+ isChatting.value = true;
624
+
625
+ try {
626
+ const res = await fetch('/api/chat', {
627
+ method: 'POST',
628
+ headers: {'Content-Type': 'application/json'},
629
+ body: JSON.stringify({
630
+ message: msg,
631
+ history: chatHistory.value.slice(-6) // Context window
632
+ })
633
+ });
634
+ const data = await res.json();
635
+ chatHistory.value.push({ role: 'assistant', content: data.response });
636
+ nextTick(() => renderChatCharts());
637
+ } catch (e) {
638
+ chatHistory.value.push({ role: 'assistant', content: '抱歉,连接出现问题。' });
639
+ } finally {
640
+ isChatting.value = false;
641
+ // Scroll to bottom
642
+ const container = document.getElementById('chat-container');
643
+ if (container) container.scrollTop = container.scrollHeight;
644
+ }
645
+ };
646
+
647
+ const renderMarkdown = (text) => {
648
+ // Extract JSON charts first to avoid marked parsing them as code blocks badly
649
+ // Actually, we can just let marked parse them, then we find the code blocks in DOM
650
+ return marked.parse(text);
651
+ };
652
+
653
+ const renderChatCharts = () => {
654
+ const codes = document.querySelectorAll('.markdown-body code');
655
+ codes.forEach(code => {
656
+ const content = code.textContent.trim();
657
+ if (content.startsWith('json:chart')) {
658
+ try {
659
+ const jsonStr = content.replace('json:chart', '').trim();
660
+ const chartData = JSON.parse(jsonStr);
661
+
662
+ // Create container
663
+ const container = document.createElement('div');
664
+ container.className = 'chart-container';
665
+ code.parentNode.replaceWith(container); // Replace pre/code block
666
+
667
+ const chart = echarts.init(container);
668
+ const option = {
669
+ title: { text: chartData.title, left: 'center' },
670
+ tooltip: { trigger: 'axis' },
671
+ legend: { bottom: 0 },
672
+ xAxis: { type: 'category', data: chartData.labels },
673
+ yAxis: { type: 'value' },
674
+ series: chartData.datasets.map(ds => ({
675
+ type: chartData.type || 'bar',
676
+ ...ds
677
+ }))
678
+ };
679
+ chart.setOption(option);
680
+ } catch (e) {
681
+ console.error('Chart parsing error', e);
682
+ }
683
+ }
684
+ });
685
+ };
686
+
687
+ onMounted(() => {
688
+ fetchExperiments();
689
+ fetchDatasets();
690
+ // Mock initial data if empty
691
+ if (experiments.value.length === 0) {
692
+ experiments.value = [
693
+ {id: 1, title: '高强钢示例', composition: {Fe:90, C:10}, properties: {tensile_strength: 300, ductility: 10}, created_at: new Date().toISOString(), notes: '系统默认示例'}
694
+ ];
695
+ }
696
+ updateCharts();
697
+ });
698
+
699
+ return {
700
+ currentTab, mobileMenuOpen,
701
+ composition, totalPercent, isSimulating, simulationResult, simulate,
702
+ saveExperiment, newExperimentNote, experiments, deleteExperiment,
703
+ userInput, isChatting, chatHistory, sendMessage, renderMarkdown,
704
+ simulationCount, getElementName,
705
+ datasets, fileInput, triggerUpload, handleFileUpload, toast
706
+ };
707
+ }
708
+ }).mount('#app');
709
+ </script>
710
+ </body>
711
+ </html>