Trae Assistant commited on
Commit
2a3ee2e
·
1 Parent(s): 7fb55c3

Enhancement: Add file upload, localize to Chinese, improve UI

Browse files
Files changed (4) hide show
  1. .gitattributes +35 -0
  2. README.md +13 -10
  3. app.py +82 -21
  4. templates/index.html +204 -73
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ *.saved_model filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -14,15 +14,18 @@ short_description: 城市脉动智能体 - 智慧交通与出行优化 SaaS
14
  ## 项目简介
15
  **Urban Flow Agent** 是一个专注于智慧交通与城市出行优化的 SaaS 平台。它利用 AI 智能体技术,结合实时数据分析,为城市管理者和物流运营商提供交通流量监控、拥堵预警、信号灯优化建议以及突发事件应急响应方案。
16
 
 
 
17
  ## 核心功能
18
- 1. **交通全景雷达 (Traffic Radar)**: 实时监控城市交通流量、平均车速和拥堵指数(通过 ECharts 可视化展示)。
19
- 2. **智能事件侦测 (Incident Scout)**: 利用 AI 分析模拟的交通数据,识别潜在交通事故或异常拥堵
20
- 3. **信号优化专家 (Signal Optima)**: 基于当前流量,由 AI 智能体生成交通信号灯配时优化方案
21
- 4. **出行智囊团 (Mobility AI)**: 集成 SiliconFlow 大模型,提供自然语言交互的交通咨询和决策支持
 
22
 
23
  ## 技术栈
24
- - **后端**: Python Flask
25
- - **前端**: Vue.js 3 + Tailwind CSS + ECharts
26
  - **AI 模型**: SiliconFlow API (Qwen/Qwen2.5-7B-Instruct)
27
  - **部署**: Docker / Hugging Face Spaces
28
 
@@ -52,10 +55,10 @@ docker build -t urban-flow-agent .
52
  docker run -p 7860:7860 urban-flow-agent
53
  ```
54
 
55
- ## 商业价值
56
- - **政府/交管**: 提升城市交通效率,减少拥堵,降低碳排放
57
- - **物流公司**: 优化配送路线,规避拥堵区域,降低运输成本
58
- - **车联网**: 自动驾驶车辆提供宏观路况数据支持
59
 
60
  ## 许可证
61
  MIT License
 
14
  ## 项目简介
15
  **Urban Flow Agent** 是一个专注于智慧交通与城市出行优化的 SaaS 平台。它利用 AI 智能体技术,结合实时数据分析,为城市管理者和物流运营商提供交通流量监控、拥堵预警、信号灯优化建议以及突发事件应急响应方案。
16
 
17
+ 本项目已实现**逻辑闭环**,支持**数据上传 -> 实时分析 -> AI 决策 -> 报告生成**的全流程操作。
18
+
19
  ## 核心功能
20
+ 1. **交通全景雷达 (Traffic Radar)**: 实时监控城市交通流量、平均车速和拥堵指数(ECharts 可视化)。
21
+ 2. **数据闭环管理 (Data Loop)**: 支持上传本地交通数据文件(JSON/CSV)系统自动解析并更新大屏指标,实现从数据源到决策端无缝衔接
22
+ 3. **智能事件侦测 (Incident Scout)**: 利用 AI 分析交通数据,识别潜在的交通事故或异常拥堵风险
23
+ 4. **信号优化专家 (Signal Optima)**: 基于当前流量,由 AI 智能体生成具体的交通信号灯配时优化方案(精确到秒)
24
+ 5. **出行智囊团 (Mobility AI)**: 集成 SiliconFlow 大模型,提供自然语言交互的交通咨询,支持中文对话。
25
 
26
  ## 技术栈
27
+ - **后端**: Python Flask (支持文件上传、CORS、Mock 模式)
28
+ - **前端**: Vue.js 3 + Tailwind CSS + ECharts (响应式布局、中文界面)
29
  - **AI 模型**: SiliconFlow API (Qwen/Qwen2.5-7B-Instruct)
30
  - **部署**: Docker / Hugging Face Spaces
31
 
 
55
  docker run -p 7860:7860 urban-flow-agent
56
  ```
57
 
58
+ ## 功能演示
59
+ - **上传数据**: 点击右上角"上传数据"按钮,导入您的交通监测日志
60
+ - **智能问答**: 在下方对话框输入"当前哪里最堵?"或"如何优化市中心交通?"
61
+ - **Mock 模式**: 当 API 额度耗尽或网络不佳时,系统自动切换至本地模拟专家模式,保证演示不中断
62
 
63
  ## 许可证
64
  MIT License
app.py CHANGED
@@ -5,6 +5,7 @@ import time
5
  import requests
6
  from flask import Flask, render_template, request, jsonify, send_from_directory
7
  from flask_cors import CORS
 
8
 
9
  app = Flask(__name__)
10
  CORS(app)
@@ -12,16 +13,23 @@ CORS(app)
12
  # Configuration
13
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
14
  app.config['JSON_AS_ASCII'] = False
 
 
 
 
15
 
16
  # API Key Configuration
17
  # In production, use environment variables: os.environ.get("SILICONFLOW_API_KEY")
18
  SILICONFLOW_API_KEY = os.environ.get("SILICONFLOW_API_KEY", "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi")
19
  SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
20
 
 
 
 
21
  # Mock Data Generator
22
  def generate_traffic_data():
23
- """Generate mock traffic data for the dashboard."""
24
- districts = ["Downtown", "North District", "Tech Park", "Harbor Area", "West End"]
25
  data = {
26
  "timestamp": time.strftime("%H:%M:%S"),
27
  "congestion_index": round(random.uniform(1.0, 9.9), 1),
@@ -31,11 +39,23 @@ def generate_traffic_data():
31
  }
32
 
33
  for district in districts:
 
 
 
 
 
 
 
34
  data["district_status"].append({
35
  "name": district,
36
  "flow": random.randint(500, 3000), # vehicles per hour
37
- "status": random.choice(["Smooth", "Moderate", "Congested", "Heavy Congestion"])
 
38
  })
 
 
 
 
39
  return data
40
 
41
  @app.route('/')
@@ -46,25 +66,58 @@ def index():
46
  def get_traffic_data():
47
  return jsonify(generate_traffic_data())
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  @app.route('/api/chat', methods=['POST'])
50
  def chat():
51
  data = request.json
52
  user_message = data.get('message', '')
53
- context_data = data.get('context', {})
 
54
 
55
  if not user_message:
56
  return jsonify({"error": "No message provided"}), 400
57
 
58
- # System Prompt with Context
59
- system_prompt = f"""You are 'Urban Flow', an AI expert in Smart City Traffic Management and Urban Mobility.
60
- Current Traffic Context:
61
- - Congestion Index: {context_data.get('congestion_index', 'N/A')}
62
- - Average Speed: {context_data.get('avg_speed', 'N/A')} km/h
63
- - Active Incidents: {context_data.get('active_incidents', 'N/A')}
64
 
65
- Your role is to analyze traffic situations, suggest signal optimization strategies, and provide mobility advice.
66
- Be professional, concise, and data-driven. Use Markdown for formatting.
67
- If asked about 'optimizing signals', suggest specific adjustments (e.g., 'Increase Green Time at North Main St by 15s').
68
  """
69
 
70
  payload = {
@@ -85,31 +138,39 @@ def chat():
85
  try:
86
  # Mock Mode Fallback logic (Network resilience)
87
  try:
88
- response = requests.post(SILICONFLOW_API_URL, json=payload, headers=headers, timeout=10)
89
  response.raise_for_status()
90
  ai_response = response.json()['choices'][0]['message']['content']
91
  except Exception as e:
92
  print(f"API Error: {e}. Switching to Mock Mode.")
93
  # Fallback Mock Response
94
  time.sleep(1) # Simulate delay
95
- ai_response = f"""**[Mock Mode Active]** Network unavailable.
96
 
97
- Based on the current congestion index of **{context_data.get('congestion_index', 'N/A')}**, here is my analysis:
98
 
99
- 1. **Signal Optimization**: Suggest extending green light duration in **Downtown** sector by 20%.
100
- 2. **Route Advisory**: Recommend diverting heavy freight traffic to the Outer Ring Road.
101
- 3. **Incident Response**: Deploy drone units to monitor the West End intersection.
102
 
103
- *Note: Connect to the internet and verify API key for live AI analysis.*"""
104
 
105
  return jsonify({"response": ai_response})
106
 
107
  except Exception as e:
108
  return jsonify({"error": str(e)}), 500
109
 
 
 
 
 
110
  @app.errorhandler(404)
111
  def page_not_found(e):
112
- return jsonify({"error": "Endpoint not found"}), 404
 
 
 
 
113
 
114
  if __name__ == '__main__':
115
  app.run(host='0.0.0.0', port=7860, debug=True)
 
5
  import requests
6
  from flask import Flask, render_template, request, jsonify, send_from_directory
7
  from flask_cors import CORS
8
+ from werkzeug.utils import secure_filename
9
 
10
  app = Flask(__name__)
11
  CORS(app)
 
13
  # Configuration
14
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB
15
  app.config['JSON_AS_ASCII'] = False
16
+ app.config['UPLOAD_FOLDER'] = 'uploads'
17
+
18
+ # Ensure upload folder exists
19
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
20
 
21
  # API Key Configuration
22
  # In production, use environment variables: os.environ.get("SILICONFLOW_API_KEY")
23
  SILICONFLOW_API_KEY = os.environ.get("SILICONFLOW_API_KEY", "sk-vimuseiptfbomzegyuvmebjzooncsqbyjtlddrfodzcdskgi")
24
  SILICONFLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
25
 
26
+ # Global context to store uploaded data or latest traffic state
27
+ traffic_context = {}
28
+
29
  # Mock Data Generator
30
  def generate_traffic_data():
31
+ """Generate mock traffic data for the dashboard (Chinese)."""
32
+ districts = ["市中心", "北区", "科技园", "港口区", "西区"]
33
  data = {
34
  "timestamp": time.strftime("%H:%M:%S"),
35
  "congestion_index": round(random.uniform(1.0, 9.9), 1),
 
39
  }
40
 
41
  for district in districts:
42
+ status_map = {
43
+ "Smooth": "畅通",
44
+ "Moderate": "缓行",
45
+ "Congested": "拥堵",
46
+ "Heavy Congestion": "严重拥堵"
47
+ }
48
+ raw_status = random.choice(list(status_map.keys()))
49
  data["district_status"].append({
50
  "name": district,
51
  "flow": random.randint(500, 3000), # vehicles per hour
52
+ "status": raw_status,
53
+ "status_cn": status_map[raw_status]
54
  })
55
+
56
+ # Update global context
57
+ global traffic_context
58
+ traffic_context = data
59
  return data
60
 
61
  @app.route('/')
 
66
  def get_traffic_data():
67
  return jsonify(generate_traffic_data())
68
 
69
+ @app.route('/api/upload', methods=['POST'])
70
+ def upload_file():
71
+ if 'file' not in request.files:
72
+ return jsonify({"error": "没有文件部分"}), 400
73
+ file = request.files['file']
74
+ if file.filename == '':
75
+ return jsonify({"error": "未选择文件"}), 400
76
+
77
+ if file:
78
+ filename = secure_filename(file.filename)
79
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
80
+ try:
81
+ file.save(filepath)
82
+ except Exception as e:
83
+ return jsonify({"error": f"保存文件失败: {str(e)}"}), 500
84
+
85
+ # Simple processing logic
86
+ msg = f"文件 {filename} 上传成功。"
87
+ if filename.lower().endswith('.json'):
88
+ try:
89
+ with open(filepath, 'r', encoding='utf-8') as f:
90
+ data = json.load(f)
91
+ # Try to merge into context if structure matches
92
+ if 'congestion_index' in data:
93
+ global traffic_context
94
+ traffic_context.update(data)
95
+ msg += " 交通数据已更新。"
96
+ except Exception as e:
97
+ msg += f" 但解析JSON失败: {str(e)}"
98
+
99
+ return jsonify({"message": msg, "filename": filename})
100
+
101
  @app.route('/api/chat', methods=['POST'])
102
  def chat():
103
  data = request.json
104
  user_message = data.get('message', '')
105
+ # Use global context if available, else client provided context
106
+ context_data = traffic_context if traffic_context else data.get('context', {})
107
 
108
  if not user_message:
109
  return jsonify({"error": "No message provided"}), 400
110
 
111
+ # System Prompt with Context (Chinese)
112
+ system_prompt = f"""你是一个名为'Urban Flow'的智慧城市交通管理AI专家。
113
+ 当前交通状况:
114
+ - 拥堵指数: {context_data.get('congestion_index', 'N/A')}
115
+ - 平均车速: {context_data.get('avg_speed', 'N/A')} km/h
116
+ - 活跃事件: {context_data.get('active_incidents', 'N/A')}
117
 
118
+ 你的角色是分析交通状况,提出信号优化策略和出行建议。
119
+ 请保持专业、简洁,并用数据说话。使用Markdown格式回复。
120
+ 如果被问及'优化信号灯',请提出具体的调整建议(例如:'建议将市中心主干道的绿灯时间延长15秒')。
121
  """
122
 
123
  payload = {
 
138
  try:
139
  # Mock Mode Fallback logic (Network resilience)
140
  try:
141
+ response = requests.post(SILICONFLOW_API_URL, json=payload, headers=headers, timeout=5)
142
  response.raise_for_status()
143
  ai_response = response.json()['choices'][0]['message']['content']
144
  except Exception as e:
145
  print(f"API Error: {e}. Switching to Mock Mode.")
146
  # Fallback Mock Response
147
  time.sleep(1) # Simulate delay
148
+ ai_response = f"""**[模拟模式已激活]** 网络连接不可用。
149
 
150
+ 根据当前拥堵指数 **{context_data.get('congestion_index', 'N/A')}**,我的分析如下:
151
 
152
+ 1. **信号优化**: 建议延长 **市中心** 区域绿灯时长 20%
153
+ 2. **路线建议**: 建议重型货车绕行外环路。
154
+ 3. **事件响应**: 派遣无人机巡查西区路口。
155
 
156
+ *注意: 请检查网络连接和API密钥以获取实时AI分析。*"""
157
 
158
  return jsonify({"response": ai_response})
159
 
160
  except Exception as e:
161
  return jsonify({"error": str(e)}), 500
162
 
163
+ @app.errorhandler(413)
164
+ def request_entity_too_large(error):
165
+ return jsonify({"error": "文件太大,超过限制 (16MB)"}), 413
166
+
167
  @app.errorhandler(404)
168
  def page_not_found(e):
169
+ return jsonify({"error": "未找到该资源 (404)"}), 404
170
+
171
+ @app.errorhandler(500)
172
+ def internal_server_error(e):
173
+ return jsonify({"error": "服务器内部错误 (500)"}), 500
174
 
175
  if __name__ == '__main__':
176
  app.run(host='0.0.0.0', port=7860, debug=True)
templates/index.html CHANGED
@@ -22,85 +22,122 @@
22
  .markdown-body p { margin-bottom: 0.5em; }
23
  .markdown-body strong { font-weight: bold; }
24
  .echart-container { width: 100%; height: 300px; }
 
 
 
 
 
25
  </style>
26
  </head>
27
  <body class="bg-gray-50 text-gray-900 font-sans">
28
- <div id="app" v-cloak class="min-h-screen flex flex-col max-w-5xl mx-auto shadow-xl bg-white">
29
 
30
  <!-- Header -->
31
  <header class="bg-white border-b border-gray-200 p-4 sticky top-0 z-50 flex items-center justify-between">
32
  <div class="flex items-center space-x-3">
33
- <div class="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center text-white text-xl">
34
  <i class="fa-solid fa-traffic-light"></i>
35
  </div>
36
  <div>
37
  <h1 class="text-xl font-bold tracking-tight text-gray-900">Urban Flow Agent</h1>
38
- <p class="text-xs text-gray-500">城市脉动 · 智慧交通 SaaS</p>
39
  </div>
40
  </div>
41
- <div class="flex items-center space-x-2">
42
- <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
43
- :class="apiStatus ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'">
44
- ${ apiStatus ? 'System Online' : 'Offline' }
 
 
 
 
 
 
 
 
 
 
45
  </span>
46
  </div>
47
  </header>
48
 
49
  <!-- Main Content -->
50
- <main class="flex-1 p-4 space-y-6 overflow-y-auto">
51
 
52
  <!-- Key Metrics Grid -->
53
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
54
- <div class="bg-blue-50 p-4 rounded-xl border border-blue-100">
55
- <div class="text-blue-500 text-sm font-medium mb-1">拥堵指数</div>
56
- <div class="text-2xl font-bold text-gray-900">${ trafficData.congestion_index }</div>
57
- <div class="text-xs text-blue-400 mt-1">
58
- <i class="fa-solid" :class="trafficData.congestion_index > 5 ? 'fa-arrow-up' : 'fa-arrow-down'"></i>
59
- 实时更新
 
 
 
 
60
  </div>
61
  </div>
62
- <div class="bg-green-50 p-4 rounded-xl border border-green-100">
63
- <div class="text-green-500 text-sm font-medium mb-1">平均车速</div>
64
- <div class="text-2xl font-bold text-gray-900">${ trafficData.avg_speed } <span class="text-sm font-normal text-gray-500">km/h</span></div>
 
 
65
  </div>
66
- <div class="bg-orange-50 p-4 rounded-xl border border-orange-100">
67
- <div class="text-orange-500 text-sm font-medium mb-1">活跃事件</div>
68
- <div class="text-2xl font-bold text-gray-900">${ trafficData.active_incidents }</div>
 
 
69
  </div>
70
- <div class="bg-purple-50 p-4 rounded-xl border border-purple-100">
71
- <div class="text-purple-500 text-sm font-medium mb-1">系统状态</div>
72
- <div class="text-lg font-bold text-gray-900">Monitoring</div>
 
 
 
73
  </div>
74
  </div>
75
 
76
  <!-- Visualization Section -->
77
- <div class="grid md:grid-cols-2 gap-6">
78
- <!-- Traffic Gauge -->
79
- <div class="bg-white rounded-xl border border-gray-200 p-4 shadow-sm">
80
  <h3 class="text-sm font-bold text-gray-700 mb-4 flex items-center">
81
- <i class="fa-solid fa-gauge-high mr-2 text-blue-500"></i> 实时拥堵仪表盘
82
  </h3>
83
- <div ref="gaugeChart" class="echart-container"></div>
84
  </div>
85
 
86
- <!-- District Status List -->
87
- <div class="bg-white rounded-xl border border-gray-200 p-4 shadow-sm">
88
  <h3 class="text-sm font-bold text-gray-700 mb-4 flex items-center">
89
  <i class="fa-solid fa-map-location-dot mr-2 text-blue-500"></i> 区域路况详情
90
  </h3>
91
- <div class="space-y-3">
92
  <div v-for="district in trafficData.district_status" :key="district.name"
93
- class="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition">
94
- <div>
95
- <div class="font-medium text-gray-900">${ district.name }</div>
96
- <div class="text-xs text-gray-500">流量: ${ district.flow } veh/h</div>
 
 
 
 
 
 
 
 
 
 
 
97
  </div>
98
- <span class="px-2 py-1 text-xs rounded-md font-medium"
99
  :class="{
100
- 'bg-green-100 text-green-700': district.status === 'Smooth',
101
- 'bg-yellow-100 text-yellow-700': district.status === 'Moderate',
102
- 'bg-orange-100 text-orange-700': district.status === 'Congested',
103
- 'bg-red-100 text-red-700': district.status === 'Heavy Congestion'
104
  }">
105
  ${ district.status_cn || district.status }
106
  </span>
@@ -110,59 +147,80 @@
110
  </div>
111
 
112
  <!-- AI Assistant Section -->
113
- <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden flex flex-col h-[500px]">
114
  <div class="bg-gray-50 p-4 border-b border-gray-200 flex justify-between items-center">
115
  <h3 class="text-sm font-bold text-gray-700 flex items-center">
116
  <i class="fa-solid fa-robot mr-2 text-blue-600"></i> 交通指挥 AI 助手
117
  </h3>
118
- <button @click="clearChat" class="text-xs text-gray-500 hover:text-red-500">
119
- <i class="fa-solid fa-trash"></i> 清空
120
- </button>
 
 
121
  </div>
122
 
123
  <!-- Chat Messages -->
124
  <div class="flex-1 overflow-y-auto p-4 space-y-4 bg-white" ref="chatContainer">
125
- <div v-if="chatHistory.length === 0" class="text-center text-gray-400 py-10">
126
- <i class="fa-solid fa-comments text-4xl mb-3 opacity-20"></i>
127
- <p class="text-sm">请下达指令,例如:"分析当前拥堵原因" "生成信号灯优化方案"</p>
128
- <div class="mt-4 flex flex-wrap justify-center gap-2">
129
- <button @click="quickAsk('分析当前全区交通状况并给出建议')" class="px-3 py-1 bg-blue-50 text-blue-600 text-xs rounded-full hover:bg-blue-100">分析路况</button>
130
- <button @click="quickAsk('为拥堵最严重的区域生成信号灯优化方案')" class="px-3 py-1 bg-blue-50 text-blue-600 text-xs rounded-full hover:bg-blue-100">信号优化</button>
131
- <button @click="quickAsk('检测是否有潜在交通事故风险')" class="px-3 py-1 bg-blue-50 text-blue-600 text-xs rounded-full hover:bg-blue-100">安全审计</button>
 
 
 
 
 
 
 
 
132
  </div>
133
  </div>
134
 
135
  <div v-for="(msg, index) in chatHistory" :key="index"
136
- class="flex flex-col"
137
  :class="msg.role === 'user' ? 'items-end' : 'items-start'">
138
- <div class="max-w-[85%] rounded-2xl px-4 py-3 text-sm shadow-sm"
139
- :class="msg.role === 'user' ? 'bg-blue-600 text-white rounded-br-none' : 'bg-gray-100 text-gray-800 rounded-bl-none'">
140
- <div v-if="msg.role === 'assistant'" class="markdown-body" v-html="renderMarkdown(msg.content)"></div>
141
- <div v-else>${ msg.content }</div>
 
 
 
 
142
  </div>
143
- <div class="text-[10px] text-gray-400 mt-1 px-1">
144
- ${ msg.role === 'user' ? 'Operator' : 'AI Agent' } ${ msg.time }
 
 
145
  </div>
146
  </div>
147
 
148
  <div v-if="isThinking" class="flex items-start">
149
- <div class="bg-gray-100 rounded-2xl rounded-bl-none px-4 py-3 flex items-center space-x-2">
150
- <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
151
- <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-75"></div>
152
- <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-150"></div>
 
 
 
 
 
153
  </div>
154
  </div>
155
  </div>
156
 
157
  <!-- Input Area -->
158
- <div class="p-4 border-t border-gray-200 bg-gray-50">
159
- <div class="flex space-x-2">
160
  <input v-model="userInput" @keyup.enter="sendMessage"
161
- type="text" placeholder="输入指令..."
162
- class="flex-1 bg-white border border-gray-300 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
163
  :disabled="isThinking">
164
  <button @click="sendMessage"
165
- class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
166
  :disabled="!userInput.trim() || isThinking">
167
  <i class="fa-solid fa-paper-plane"></i>
168
  </button>
@@ -171,6 +229,21 @@
171
  </div>
172
 
173
  </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </div>
175
 
176
  <script>
@@ -191,6 +264,10 @@
191
  const apiStatus = ref(true);
192
  const chatContainer = ref(null);
193
  const gaugeChart = ref(null);
 
 
 
 
194
  let chartInstance = null;
195
  const md = window.markdownit();
196
 
@@ -198,9 +275,12 @@
198
  const fetchTrafficData = async () => {
199
  try {
200
  const res = await fetch('/api/traffic-data');
 
201
  const data = await res.json();
202
  trafficData.value = data;
203
  updateChart(data.congestion_index);
 
 
204
  } catch (e) {
205
  console.error("Data fetch error", e);
206
  apiStatus.value = false;
@@ -221,7 +301,7 @@
221
  itemStyle: { color: '#58D9F9' },
222
  progress: { show: true, width: 30 },
223
  pointer: { icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z', length: '12%', width: 20, offsetCenter: [0, '-60%'], itemStyle: { color: 'auto' } },
224
- axisLine: { lineStyle: { width: 30, color: [[0.3, '#67e0e3'], [0.7, '#37a2da'], [1, '#fd666d']] } },
225
  axisTick: { distance: -45, splitNumber: 5, lineStyle: { width: 2, color: '#999' } },
226
  splitLine: { distance: -52, length: 14, lineStyle: { width: 3, color: '#999' } },
227
  axisLabel: { distance: -20, color: '#999', fontSize: 14 },
@@ -242,7 +322,7 @@
242
  chatHistory.value.push({
243
  role: 'user',
244
  content: text,
245
- time: new Date().toLocaleTimeString()
246
  });
247
  userInput.value = '';
248
  isThinking.value = true;
@@ -262,13 +342,13 @@
262
  chatHistory.value.push({
263
  role: 'assistant',
264
  content: data.response || "No response received.",
265
- time: new Date().toLocaleTimeString()
266
  });
267
  } catch (e) {
268
  chatHistory.value.push({
269
  role: 'assistant',
270
- content: "**Error:** Failed to connect to AI service.",
271
- time: new Date().toLocaleTimeString()
272
  });
273
  } finally {
274
  isThinking.value = false;
@@ -296,6 +376,52 @@
296
  return md.render(text);
297
  };
298
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  onMounted(() => {
300
  // Init Chart
301
  if (gaugeChart.value) {
@@ -315,10 +441,15 @@
315
  apiStatus,
316
  chatContainer,
317
  gaugeChart,
 
 
 
318
  sendMessage,
319
  quickAsk,
320
  clearChat,
321
- renderMarkdown
 
 
322
  };
323
  }
324
  }).mount('#app');
 
22
  .markdown-body p { margin-bottom: 0.5em; }
23
  .markdown-body strong { font-weight: bold; }
24
  .echart-container { width: 100%; height: 300px; }
25
+ /* Custom Scrollbar */
26
+ ::-webkit-scrollbar { width: 8px; }
27
+ ::-webkit-scrollbar-track { background: #f1f1f1; }
28
+ ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
29
+ ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
30
  </style>
31
  </head>
32
  <body class="bg-gray-50 text-gray-900 font-sans">
33
+ <div id="app" v-cloak class="min-h-screen flex flex-col max-w-6xl mx-auto shadow-xl bg-white">
34
 
35
  <!-- Header -->
36
  <header class="bg-white border-b border-gray-200 p-4 sticky top-0 z-50 flex items-center justify-between">
37
  <div class="flex items-center space-x-3">
38
+ <div class="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center text-white text-xl shadow-lg">
39
  <i class="fa-solid fa-traffic-light"></i>
40
  </div>
41
  <div>
42
  <h1 class="text-xl font-bold tracking-tight text-gray-900">Urban Flow Agent</h1>
43
+ <p class="text-xs text-gray-500">城市脉动 · 智慧交通 SaaS 平台</p>
44
  </div>
45
  </div>
46
+ <div class="flex items-center space-x-3">
47
+ <!-- Upload Button -->
48
+ <div class="relative">
49
+ <input type="file" ref="fileInput" @change="uploadFile" class="hidden" accept=".json,.csv,.txt,.pdf">
50
+ <button @click="triggerUpload" class="flex items-center space-x-2 px-3 py-1.5 bg-gray-100 hover:bg-gray-200 rounded-md text-sm transition">
51
+ <i class="fa-solid fa-cloud-upload"></i>
52
+ <span>上传数据</span>
53
+ </button>
54
+ </div>
55
+
56
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border"
57
+ :class="apiStatus ? 'bg-green-50 text-green-700 border-green-200' : 'bg-red-50 text-red-700 border-red-200'">
58
+ <span class="w-2 h-2 rounded-full mr-1.5" :class="apiStatus ? 'bg-green-500' : 'bg-red-500'"></span>
59
+ ${ apiStatus ? '系统在线' : '离线' }
60
  </span>
61
  </div>
62
  </header>
63
 
64
  <!-- Main Content -->
65
+ <main class="flex-1 p-6 space-y-6 overflow-y-auto">
66
 
67
  <!-- Key Metrics Grid -->
68
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
69
+ <div class="bg-gradient-to-br from-blue-50 to-white p-5 rounded-xl border border-blue-100 shadow-sm hover:shadow-md transition">
70
+ <div class="text-blue-600 text-sm font-medium mb-1 flex items-center">
71
+ <i class="fa-solid fa-gauge-high mr-2"></i> 拥堵指数
72
+ </div>
73
+ <div class="text-3xl font-bold text-gray-900">${ trafficData.congestion_index }</div>
74
+ <div class="text-xs text-blue-400 mt-2 flex items-center">
75
+ <span :class="trafficData.congestion_index > 5 ? 'text-red-500' : 'text-green-500'">
76
+ <i class="fa-solid" :class="trafficData.congestion_index > 5 ? 'fa-arrow-trend-up' : 'fa-arrow-trend-down'"></i>
77
+ ${ trafficData.congestion_index > 5 ? '上升趋势' : '保持平稳' }
78
+ </span>
79
  </div>
80
  </div>
81
+ <div class="bg-gradient-to-br from-green-50 to-white p-5 rounded-xl border border-green-100 shadow-sm hover:shadow-md transition">
82
+ <div class="text-green-600 text-sm font-medium mb-1 flex items-center">
83
+ <i class="fa-solid fa-road mr-2"></i> 平均车速
84
+ </div>
85
+ <div class="text-3xl font-bold text-gray-900">${ trafficData.avg_speed } <span class="text-sm font-normal text-gray-500">km/h</span></div>
86
  </div>
87
+ <div class="bg-gradient-to-br from-orange-50 to-white p-5 rounded-xl border border-orange-100 shadow-sm hover:shadow-md transition">
88
+ <div class="text-orange-600 text-sm font-medium mb-1 flex items-center">
89
+ <i class="fa-solid fa-triangle-exclamation mr-2"></i> 活跃事件
90
+ </div>
91
+ <div class="text-3xl font-bold text-gray-900">${ trafficData.active_incidents }</div>
92
  </div>
93
+ <div class="bg-gradient-to-br from-purple-50 to-white p-5 rounded-xl border border-purple-100 shadow-sm hover:shadow-md transition">
94
+ <div class="text-purple-600 text-sm font-medium mb-1 flex items-center">
95
+ <i class="fa-solid fa-server mr-2"></i> 系统状态
96
+ </div>
97
+ <div class="text-lg font-bold text-gray-900 mt-1">实时监控中</div>
98
+ <div class="text-xs text-gray-400 mt-1">更新于: ${ currentTime }</div>
99
  </div>
100
  </div>
101
 
102
  <!-- Visualization Section -->
103
+ <div class="grid md:grid-cols-12 gap-6">
104
+ <!-- Traffic Gauge (4 cols) -->
105
+ <div class="md:col-span-4 bg-white rounded-xl border border-gray-200 p-4 shadow-sm">
106
  <h3 class="text-sm font-bold text-gray-700 mb-4 flex items-center">
107
+ <i class="fa-solid fa-chart-pie mr-2 text-blue-500"></i> 实时拥堵仪表盘
108
  </h3>
109
+ <div ref="gaugeChart" class="echart-container" style="height: 250px;"></div>
110
  </div>
111
 
112
+ <!-- District Status List (8 cols) -->
113
+ <div class="md:col-span-8 bg-white rounded-xl border border-gray-200 p-4 shadow-sm flex flex-col">
114
  <h3 class="text-sm font-bold text-gray-700 mb-4 flex items-center">
115
  <i class="fa-solid fa-map-location-dot mr-2 text-blue-500"></i> 区域路况详情
116
  </h3>
117
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3 overflow-y-auto max-h-[250px] pr-2">
118
  <div v-for="district in trafficData.district_status" :key="district.name"
119
+ class="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-100 hover:border-blue-200 hover:shadow-sm transition">
120
+ <div class="flex items-center space-x-3">
121
+ <div class="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold"
122
+ :class="{
123
+ 'bg-green-100 text-green-700': district.status === 'Smooth',
124
+ 'bg-yellow-100 text-yellow-700': district.status === 'Moderate',
125
+ 'bg-orange-100 text-orange-700': district.status === 'Congested',
126
+ 'bg-red-100 text-red-700': district.status === 'Heavy Congestion'
127
+ }">
128
+ ${ district.name.charAt(0) }
129
+ </div>
130
+ <div>
131
+ <div class="font-medium text-gray-900 text-sm">${ district.name }</div>
132
+ <div class="text-xs text-gray-500">流量: ${ district.flow } 车/小时</div>
133
+ </div>
134
  </div>
135
+ <span class="px-2 py-1 text-xs rounded-md font-medium border"
136
  :class="{
137
+ 'bg-green-50 text-green-700 border-green-200': district.status === 'Smooth',
138
+ 'bg-yellow-50 text-yellow-700 border-yellow-200': district.status === 'Moderate',
139
+ 'bg-orange-50 text-orange-700 border-orange-200': district.status === 'Congested',
140
+ 'bg-red-50 text-red-700 border-red-200': district.status === 'Heavy Congestion'
141
  }">
142
  ${ district.status_cn || district.status }
143
  </span>
 
147
  </div>
148
 
149
  <!-- AI Assistant Section -->
150
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden flex flex-col h-[600px]">
151
  <div class="bg-gray-50 p-4 border-b border-gray-200 flex justify-between items-center">
152
  <h3 class="text-sm font-bold text-gray-700 flex items-center">
153
  <i class="fa-solid fa-robot mr-2 text-blue-600"></i> 交通指挥 AI 助手
154
  </h3>
155
+ <div class="flex space-x-2">
156
+ <button @click="clearChat" class="text-xs text-gray-500 hover:text-red-500 transition px-2 py-1 rounded hover:bg-red-50">
157
+ <i class="fa-solid fa-trash mr-1"></i> 清空
158
+ </button>
159
+ </div>
160
  </div>
161
 
162
  <!-- Chat Messages -->
163
  <div class="flex-1 overflow-y-auto p-4 space-y-4 bg-white" ref="chatContainer">
164
+ <div v-if="chatHistory.length === 0" class="text-center text-gray-400 py-20 flex flex-col items-center">
165
+ <div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
166
+ <i class="fa-solid fa-comments text-2xl text-gray-300"></i>
167
+ </div>
168
+ <p class="text-sm text-gray-500">请下达指令,例如:"分析当前拥堵原因" 或 "生成信号灯优化方案"</p>
169
+ <div class="mt-6 flex flex-wrap justify-center gap-2 max-w-md">
170
+ <button @click="quickAsk('分析当前全区交通状况并给出建议')" class="px-3 py-1.5 bg-white border border-gray-200 text-gray-600 text-xs rounded-full hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition shadow-sm">
171
+ <i class="fa-solid fa-magnifying-glass mr-1"></i> 分析路况
172
+ </button>
173
+ <button @click="quickAsk('为拥堵最严重的区域生成信号灯优化方案')" class="px-3 py-1.5 bg-white border border-gray-200 text-gray-600 text-xs rounded-full hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition shadow-sm">
174
+ <i class="fa-solid fa-traffic-light mr-1"></i> 信号优化
175
+ </button>
176
+ <button @click="quickAsk('检测是否有潜在交通事故风险')" class="px-3 py-1.5 bg-white border border-gray-200 text-gray-600 text-xs rounded-full hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition shadow-sm">
177
+ <i class="fa-solid fa-shield-halved mr-1"></i> 安全审计
178
+ </button>
179
  </div>
180
  </div>
181
 
182
  <div v-for="(msg, index) in chatHistory" :key="index"
183
+ class="flex flex-col group"
184
  :class="msg.role === 'user' ? 'items-end' : 'items-start'">
185
+ <div class="flex items-center mb-1 space-x-2" :class="msg.role === 'user' ? 'flex-row-reverse space-x-reverse' : 'flex-row'">
186
+ <div class="w-6 h-6 rounded-full flex items-center justify-center text-[10px]"
187
+ :class="msg.role === 'user' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600'">
188
+ <i class="fa-solid" :class="msg.role === 'user' ? 'fa-user' : 'fa-robot'"></i>
189
+ </div>
190
+ <div class="text-[10px] text-gray-400">
191
+ ${ msg.role === 'user' ? '操作员' : 'AI 助手' } • ${ msg.time }
192
+ </div>
193
  </div>
194
+ <div class="max-w-[85%] rounded-2xl px-4 py-3 text-sm shadow-sm border"
195
+ :class="msg.role === 'user' ? 'bg-blue-600 text-white rounded-tr-none border-blue-600' : 'bg-white text-gray-800 rounded-tl-none border-gray-100'">
196
+ <div v-if="msg.role === 'assistant'" class="markdown-body prose prose-sm max-w-none" v-html="renderMarkdown(msg.content)"></div>
197
+ <div v-else>${ msg.content }</div>
198
  </div>
199
  </div>
200
 
201
  <div v-if="isThinking" class="flex items-start">
202
+ <div class="flex items-center mb-1 space-x-2">
203
+ <div class="w-6 h-6 rounded-full bg-gray-100 text-gray-600 flex items-center justify-center text-[10px]">
204
+ <i class="fa-solid fa-robot"></i>
205
+ </div>
206
+ </div>
207
+ <div class="ml-2 bg-white border border-gray-100 rounded-2xl rounded-tl-none px-4 py-3 flex items-center space-x-2 shadow-sm">
208
+ <div class="w-2 h-2 bg-blue-400 rounded-full animate-bounce"></div>
209
+ <div class="w-2 h-2 bg-blue-400 rounded-full animate-bounce delay-75"></div>
210
+ <div class="w-2 h-2 bg-blue-400 rounded-full animate-bounce delay-150"></div>
211
  </div>
212
  </div>
213
  </div>
214
 
215
  <!-- Input Area -->
216
+ <div class="p-4 border-t border-gray-200 bg-white">
217
+ <div class="flex space-x-2 relative">
218
  <input v-model="userInput" @keyup.enter="sendMessage"
219
+ type="text" placeholder="输入指令..."
220
+ class="flex-1 bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
221
  :disabled="isThinking">
222
  <button @click="sendMessage"
223
+ class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-xl transition shadow-sm disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
224
  :disabled="!userInput.trim() || isThinking">
225
  <i class="fa-solid fa-paper-plane"></i>
226
  </button>
 
229
  </div>
230
 
231
  </main>
232
+
233
+ <!-- Toast Notification -->
234
+ <div class="fixed bottom-4 right-4 z-50 transition-all duration-300" v-if="notification.show">
235
+ <div class="bg-white border-l-4 shadow-lg rounded-r px-4 py-3 flex items-center"
236
+ :class="notification.type === 'error' ? 'border-red-500' : 'border-green-500'">
237
+ <div class="text-xl mr-3" :class="notification.type === 'error' ? 'text-red-500' : 'text-green-500'">
238
+ <i class="fa-solid" :class="notification.type === 'error' ? 'fa-circle-exclamation' : 'fa-circle-check'"></i>
239
+ </div>
240
+ <div>
241
+ <p class="font-bold text-sm text-gray-900">${ notification.title }</p>
242
+ <p class="text-xs text-gray-600">${ notification.message }</p>
243
+ </div>
244
+ </div>
245
+ </div>
246
+
247
  </div>
248
 
249
  <script>
 
264
  const apiStatus = ref(true);
265
  const chatContainer = ref(null);
266
  const gaugeChart = ref(null);
267
+ const fileInput = ref(null);
268
+ const currentTime = ref('');
269
+ const notification = ref({ show: false, type: 'info', title: '', message: '' });
270
+
271
  let chartInstance = null;
272
  const md = window.markdownit();
273
 
 
275
  const fetchTrafficData = async () => {
276
  try {
277
  const res = await fetch('/api/traffic-data');
278
+ if (!res.ok) throw new Error("API Error");
279
  const data = await res.json();
280
  trafficData.value = data;
281
  updateChart(data.congestion_index);
282
+ currentTime.value = new Date().toLocaleTimeString('zh-CN');
283
+ apiStatus.value = true;
284
  } catch (e) {
285
  console.error("Data fetch error", e);
286
  apiStatus.value = false;
 
301
  itemStyle: { color: '#58D9F9' },
302
  progress: { show: true, width: 30 },
303
  pointer: { icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z', length: '12%', width: 20, offsetCenter: [0, '-60%'], itemStyle: { color: 'auto' } },
304
+ axisLine: { lineStyle: { width: 30, color: [[0.3, '#10B981'], [0.7, '#F59E0B'], [1, '#EF4444']] } },
305
  axisTick: { distance: -45, splitNumber: 5, lineStyle: { width: 2, color: '#999' } },
306
  splitLine: { distance: -52, length: 14, lineStyle: { width: 3, color: '#999' } },
307
  axisLabel: { distance: -20, color: '#999', fontSize: 14 },
 
322
  chatHistory.value.push({
323
  role: 'user',
324
  content: text,
325
+ time: new Date().toLocaleTimeString('zh-CN')
326
  });
327
  userInput.value = '';
328
  isThinking.value = true;
 
342
  chatHistory.value.push({
343
  role: 'assistant',
344
  content: data.response || "No response received.",
345
+ time: new Date().toLocaleTimeString('zh-CN')
346
  });
347
  } catch (e) {
348
  chatHistory.value.push({
349
  role: 'assistant',
350
+ content: "**错误:** 无法连接到 AI 服务。",
351
+ time: new Date().toLocaleTimeString('zh-CN')
352
  });
353
  } finally {
354
  isThinking.value = false;
 
376
  return md.render(text);
377
  };
378
 
379
+ // File Upload Logic
380
+ const triggerUpload = () => {
381
+ if (fileInput.value) {
382
+ fileInput.value.click();
383
+ } else {
384
+ showNotification('error', '错误', '找不到文件输入组件');
385
+ }
386
+ };
387
+
388
+ const uploadFile = async (event) => {
389
+ const file = event.target.files[0];
390
+ if (!file) return;
391
+
392
+ const formData = new FormData();
393
+ formData.append('file', file);
394
+
395
+ try {
396
+ showNotification('info', '上传中', `正在上传 ${file.name}...`);
397
+ const res = await fetch('/api/upload', {
398
+ method: 'POST',
399
+ body: formData
400
+ });
401
+ const data = await res.json();
402
+
403
+ if (res.ok) {
404
+ showNotification('success', '上传成功', data.message);
405
+ // Refresh data immediately
406
+ fetchTrafficData();
407
+ } else {
408
+ showNotification('error', '上传失败', data.error || '未知错误');
409
+ }
410
+ } catch (e) {
411
+ showNotification('error', '上传错误', e.message);
412
+ } finally {
413
+ // Reset input
414
+ event.target.value = '';
415
+ }
416
+ };
417
+
418
+ const showNotification = (type, title, message) => {
419
+ notification.value = { show: true, type, title, message };
420
+ setTimeout(() => {
421
+ notification.value.show = false;
422
+ }, 3000);
423
+ };
424
+
425
  onMounted(() => {
426
  // Init Chart
427
  if (gaugeChart.value) {
 
441
  apiStatus,
442
  chatContainer,
443
  gaugeChart,
444
+ fileInput,
445
+ currentTime,
446
+ notification,
447
  sendMessage,
448
  quickAsk,
449
  clearChat,
450
+ renderMarkdown,
451
+ triggerUpload,
452
+ uploadFile
453
  };
454
  }
455
  }).mount('#app');