Trae Assistant commited on
Commit
888384b
·
1 Parent(s): c2b307b

Enhance app with file upload, error handling, and localization

Browse files
Files changed (3) hide show
  1. .gitattributes +5 -0
  2. app.py +69 -1
  3. templates/index.html +70 -2
.gitattributes CHANGED
@@ -1 +1,6 @@
1
  *.db filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
1
  *.db filter=lfs diff=lfs merge=lfs -text
2
+ *.edf filter=lfs diff=lfs merge=lfs -text
3
+ *.bdf filter=lfs diff=lfs merge=lfs -text
4
+ *.h5 filter=lfs diff=lfs merge=lfs -text
5
+ *.pth filter=lfs diff=lfs merge=lfs -text
6
+ *.bin filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -4,24 +4,42 @@ import sqlite3
4
  import requests
5
  import random
6
  import time
 
7
  from flask import Flask, render_template, request, jsonify, g
8
  from flask_cors import CORS
 
9
 
10
  app = Flask(__name__)
11
  CORS(app)
12
 
13
  # Configuration
14
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
 
15
  app.config['DATABASE'] = os.path.join(app.instance_path, 'synapse.db')
16
  SILICONFLOW_API_KEY = "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi"
17
  SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
18
 
19
- # Ensure instance folder exists
20
  try:
21
  os.makedirs(app.instance_path)
 
22
  except OSError:
23
  pass
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  # Database Setup
26
  def get_db():
27
  db = getattr(g, '_database', None)
@@ -49,16 +67,66 @@ def init_db():
49
  raw_metrics TEXT
50
  )
51
  ''')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  db.commit()
53
 
54
  init_db()
55
 
 
 
 
 
 
 
56
  # --- Routes ---
57
 
58
  @app.route('/')
59
  def index():
60
  return render_template('index.html')
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  @app.route('/api/mock/signal')
63
  def mock_signal():
64
  """Generate mock EEG signal data for visualization."""
 
4
  import requests
5
  import random
6
  import time
7
+ import traceback
8
  from flask import Flask, render_template, request, jsonify, g
9
  from flask_cors import CORS
10
+ from werkzeug.utils import secure_filename
11
 
12
  app = Flask(__name__)
13
  CORS(app)
14
 
15
  # Configuration
16
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
17
+ app.config['UPLOAD_FOLDER'] = os.path.join(app.instance_path, 'uploads')
18
  app.config['DATABASE'] = os.path.join(app.instance_path, 'synapse.db')
19
  SILICONFLOW_API_KEY = "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi"
20
  SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
21
 
22
+ # Ensure instance and upload folders exist
23
  try:
24
  os.makedirs(app.instance_path)
25
+ os.makedirs(app.config['UPLOAD_FOLDER'])
26
  except OSError:
27
  pass
28
 
29
+ # Error Handlers
30
+ @app.errorhandler(404)
31
+ def page_not_found(e):
32
+ return render_template('index.html'), 404
33
+
34
+ @app.errorhandler(500)
35
+ def internal_server_error(e):
36
+ traceback.print_exc()
37
+ return jsonify(error="Internal Server Error", message=str(e)), 500
38
+
39
+ @app.errorhandler(413)
40
+ def request_entity_too_large(e):
41
+ return jsonify(error="File too large", message="File exceeds the maximum allowed size of 16MB"), 413
42
+
43
  # Database Setup
44
  def get_db():
45
  db = getattr(g, '_database', None)
 
67
  raw_metrics TEXT
68
  )
69
  ''')
70
+
71
+ # Check if empty and add default data
72
+ cur = db.execute('SELECT count(*) FROM history')
73
+ if cur.fetchone()[0] == 0:
74
+ default_analysis = {
75
+ "summary": "**演示数据报告**: 这是一个自动生成的示例报告。\n\n受试者 **DEMO-USER** 在 **专注训练** 中表现优秀。Alpha波与Beta波的交替出现表明受试者能够自如地在放松和专注状态间切换。",
76
+ "cognitive_state": "Flow State (心流状态)",
77
+ "recommendations": ["继续保持当前的训练频率", "尝试增加训练时长至30分钟", "记录训练后的主观感受"],
78
+ "radar_chart": {"专注": 85, "放松": 70, "反应速度": 80, "记忆负荷": 65, "情绪稳定性": 88}
79
+ }
80
+ db.execute(
81
+ 'INSERT INTO history (timestamp, subject_id, session_type, analysis_result, raw_metrics) VALUES (?, ?, ?, ?, ?)',
82
+ (time.strftime('%Y-%m-%d %H:%M:%S'), 'DEMO-USER', 'Focus Training', json.dumps(default_analysis), "demo_data")
83
+ )
84
+ print("Default data initialized.")
85
+
86
  db.commit()
87
 
88
  init_db()
89
 
90
+ # --- Helpers ---
91
+ ALLOWED_EXTENSIONS = {'txt', 'csv', 'json', 'edf', 'bdf'}
92
+
93
+ def allowed_file(filename):
94
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
95
+
96
  # --- Routes ---
97
 
98
  @app.route('/')
99
  def index():
100
  return render_template('index.html')
101
 
102
+ @app.route('/api/upload', methods=['POST'])
103
+ def upload_file():
104
+ if 'file' not in request.files:
105
+ return jsonify({'error': 'No file part'}), 400
106
+ file = request.files['file']
107
+ if file.filename == '':
108
+ return jsonify({'error': 'No selected file'}), 400
109
+ if file and allowed_file(file.filename):
110
+ filename = secure_filename(file.filename)
111
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
112
+ file.save(filepath)
113
+
114
+ # Mock processing of the file to extract metrics
115
+ # In a real app, we would parse EDF/CSV here
116
+ file_size = os.path.getsize(filepath)
117
+
118
+ return jsonify({
119
+ 'status': 'success',
120
+ 'message': f'File {filename} uploaded successfully ({file_size} bytes)',
121
+ 'extracted_metrics': {
122
+ 'alpha': random.uniform(8, 12),
123
+ 'beta': random.uniform(15, 25),
124
+ 'theta': random.uniform(4, 7),
125
+ 'delta': random.uniform(1, 3)
126
+ }
127
+ })
128
+ return jsonify({'error': 'File type not allowed'}), 400
129
+
130
  @app.route('/api/mock/signal')
131
  def mock_signal():
132
  """Generate mock EEG signal data for visualization."""
templates/index.html CHANGED
@@ -152,6 +152,19 @@
152
  <option>Motor Imagery (运动想象)</option>
153
  </select>
154
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  <button @click="analyzeSession" :disabled="isAnalyzing" class="w-full mt-auto bg-brand-600 hover:bg-brand-700 text-white py-2.5 rounded-lg font-medium transition-all shadow-lg shadow-brand-500/30 disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2">
156
  <i v-if="isAnalyzing" class="fa-solid fa-circle-notch animate-spin"></i>
157
  ${ isAnalyzing ? '正在分析数据...' : '生成分析报告' }
@@ -283,6 +296,8 @@
283
  const metrics = ref({ alpha: 0, beta: 0, theta: 0, delta: 0 });
284
  const lastAnalysis = ref(null);
285
  const historyList = ref([]);
 
 
286
 
287
  // Chart Refs
288
  const signalChart = ref(null);
@@ -304,6 +319,58 @@
304
  return marked.parse(lastAnalysis.value.summary);
305
  });
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  const initCharts = () => {
308
  if (signalChart.value) {
309
  signalChartInst = echarts.init(signalChart.value);
@@ -490,8 +557,9 @@
490
  currentView, mobileMenuOpen, isRecording, isAnalyzing,
491
  subjectId, sessionType, metrics, lastAnalysis, historyList,
492
  signalChart, spectrumChart, radarChart,
493
- parsedSummary,
494
- toggleRecording, analyzeSession, fetchHistory, loadAnalysis
 
495
  };
496
  }
497
  }).mount('#app');
 
152
  <option>Motor Imagery (运动想象)</option>
153
  </select>
154
  </div>
155
+
156
+ <!-- File Upload Section -->
157
+ <div>
158
+ <label class="block text-sm font-medium text-slate-700 mb-1">数据源 (可选)</label>
159
+ <div class="flex gap-2">
160
+ <button @click="triggerUpload" class="flex-1 px-3 py-2 border border-dashed border-slate-300 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-brand-600 transition-colors text-sm flex items-center justify-center gap-2">
161
+ <i class="fa-solid fa-cloud-arrow-up"></i>
162
+ ${ uploadStatus || '上传 EEG/CSV 文件' }
163
+ </button>
164
+ <input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" accept=".csv,.txt,.edf,.bdf,.json">
165
+ </div>
166
+ </div>
167
+
168
  <button @click="analyzeSession" :disabled="isAnalyzing" class="w-full mt-auto bg-brand-600 hover:bg-brand-700 text-white py-2.5 rounded-lg font-medium transition-all shadow-lg shadow-brand-500/30 disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2">
169
  <i v-if="isAnalyzing" class="fa-solid fa-circle-notch animate-spin"></i>
170
  ${ isAnalyzing ? '正在分析数据...' : '生成分析报告' }
 
296
  const metrics = ref({ alpha: 0, beta: 0, theta: 0, delta: 0 });
297
  const lastAnalysis = ref(null);
298
  const historyList = ref([]);
299
+ const uploadStatus = ref('');
300
+ const fileInput = ref(null);
301
 
302
  // Chart Refs
303
  const signalChart = ref(null);
 
319
  return marked.parse(lastAnalysis.value.summary);
320
  });
321
 
322
+ const triggerUpload = () => {
323
+ fileInput.value.click();
324
+ };
325
+
326
+ const handleFileUpload = async (event) => {
327
+ const file = event.target.files[0];
328
+ if (!file) return;
329
+
330
+ uploadStatus.value = '上传中...';
331
+ const formData = new FormData();
332
+ formData.append('file', file);
333
+
334
+ try {
335
+ const res = await fetch('/api/upload', {
336
+ method: 'POST',
337
+ body: formData
338
+ });
339
+ const data = await res.json();
340
+
341
+ if (res.ok) {
342
+ uploadStatus.value = '上传成功';
343
+ // Use extracted metrics to update dashboard
344
+ if (data.extracted_metrics) {
345
+ metrics.value = data.extracted_metrics;
346
+ // Also update spectrum chart
347
+ if (spectrumChartInst) {
348
+ spectrumChartInst.setOption({
349
+ series: [{
350
+ data: [
351
+ data.extracted_metrics.alpha,
352
+ data.extracted_metrics.beta,
353
+ data.extracted_metrics.theta,
354
+ data.extracted_metrics.delta
355
+ ]
356
+ }]
357
+ });
358
+ }
359
+ }
360
+ setTimeout(() => { uploadStatus.value = ''; }, 3000);
361
+ } else {
362
+ uploadStatus.value = '上传失败';
363
+ alert(data.error || '上传失败');
364
+ }
365
+ } catch (e) {
366
+ console.error(e);
367
+ uploadStatus.value = '出错';
368
+ alert('上传过程中发生错误');
369
+ }
370
+ // Reset input
371
+ event.target.value = '';
372
+ };
373
+
374
  const initCharts = () => {
375
  if (signalChart.value) {
376
  signalChartInst = echarts.init(signalChart.value);
 
557
  currentView, mobileMenuOpen, isRecording, isAnalyzing,
558
  subjectId, sessionType, metrics, lastAnalysis, historyList,
559
  signalChart, spectrumChart, radarChart,
560
+ parsedSummary, uploadStatus, fileInput,
561
+ toggleRecording, analyzeSession, fetchHistory, loadAnalysis,
562
+ triggerUpload, handleFileUpload
563
  };
564
  }
565
  }).mount('#app');