Spaces:
Sleeping
Sleeping
Trae Assistant commited on
Commit ·
9990299
1
Parent(s): dc6ced4
feat: complete project setup with chinese localization, datasets support, and mock mode
Browse files- .gitignore +5 -0
- Dockerfile +16 -0
- app.py +330 -0
- requirements.txt +5 -0
- 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 |
+
© 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>
|