Trae Assistant commited on
Commit
7fb55c3
·
0 Parent(s):

Initial commit of Urban Flow Agent

Browse files
Files changed (5) hide show
  1. Dockerfile +13 -0
  2. README.md +61 -0
  3. app.py +115 -0
  4. requirements.txt +5 -0
  5. templates/index.html +327 -0
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Ensure instance directory exists for SQLite (if used) or logs
11
+ RUN mkdir -p instance && chmod 777 instance
12
+
13
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
README.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Urban Flow Agent
3
+ emoji: 🚦
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ pinned: false
8
+ app_port: 7860
9
+ short_description: 城市脉动智能体 - 智慧交通与出行优化 SaaS
10
+ ---
11
+
12
+ # 城市脉动智能体 (Urban Flow Agent)
13
+
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
+
29
+ ## 快速开始
30
+
31
+ ### 本地运行
32
+ 1. 克隆项目
33
+ ```bash
34
+ git clone https://huggingface.co/spaces/<your-username>/urban-flow-agent
35
+ cd urban-flow-agent
36
+ ```
37
+
38
+ 2. 安装依赖
39
+ ```bash
40
+ pip install -r requirements.txt
41
+ ```
42
+
43
+ 3. 运行应用
44
+ ```bash
45
+ python app.py
46
+ ```
47
+ 访问: http://localhost:7860
48
+
49
+ ### Docker 运行
50
+ ```bash
51
+ 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
app.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import random
4
+ 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)
11
+
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),
28
+ "avg_speed": round(random.uniform(15, 60), 1),
29
+ "active_incidents": random.randint(0, 5),
30
+ "district_status": []
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('/')
42
+ def index():
43
+ return render_template('index.html')
44
+
45
+ @app.route('/api/traffic-data', methods=['GET'])
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 = {
71
+ "model": "Qwen/Qwen2.5-7B-Instruct",
72
+ "messages": [
73
+ {"role": "system", "content": system_prompt},
74
+ {"role": "user", "content": user_message}
75
+ ],
76
+ "temperature": 0.7,
77
+ "max_tokens": 512
78
+ }
79
+
80
+ headers = {
81
+ "Authorization": f"Bearer {SILICONFLOW_API_KEY}",
82
+ "Content-Type": "application/json"
83
+ }
84
+
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)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ flask
2
+ flask-cors
3
+ requests
4
+ gunicorn
5
+ python-dotenv
templates/index.html ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Urban Flow Agent - 城市脉动</title>
7
+ <!-- Vue 3 -->
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <!-- Tailwind CSS -->
10
+ <script src="https://cdn.tailwindcss.com"></script>
11
+ <!-- ECharts -->
12
+ <script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
13
+ <!-- Markdown-it -->
14
+ <script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
15
+ <!-- Font Awesome -->
16
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
17
+
18
+ <style>
19
+ [v-cloak] { display: none; }
20
+ .markdown-body ul { list-style-type: disc; margin-left: 1.5em; }
21
+ .markdown-body ol { list-style-type: decimal; margin-left: 1.5em; }
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>
107
+ </div>
108
+ </div>
109
+ </div>
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>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ </main>
174
+ </div>
175
+
176
+ <script>
177
+ const { createApp, ref, onMounted, nextTick } = Vue;
178
+
179
+ createApp({
180
+ delimiters: ['${', '}'],
181
+ setup() {
182
+ const trafficData = ref({
183
+ congestion_index: 0,
184
+ avg_speed: 0,
185
+ active_incidents: 0,
186
+ district_status: []
187
+ });
188
+ const chatHistory = ref([]);
189
+ const userInput = ref('');
190
+ const isThinking = ref(false);
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
+
197
+ // Polling for data
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;
207
+ }
208
+ };
209
+
210
+ const updateChart = (value) => {
211
+ if (!chartInstance) return;
212
+
213
+ const option = {
214
+ series: [{
215
+ type: 'gauge',
216
+ startAngle: 180,
217
+ endAngle: 0,
218
+ min: 0,
219
+ max: 10,
220
+ splitNumber: 5,
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 },
228
+ anchor: { show: false },
229
+ title: { show: false },
230
+ detail: { valueAnimation: true, width: '60%', lineHeight: 40, borderRadius: 8, offsetCenter: [0, '-15%'], fontSize: 30, fontWeight: 'bolder', formatter: '{value}', color: 'auto' },
231
+ data: [{ value: value }]
232
+ }]
233
+ };
234
+ chartInstance.setOption(option);
235
+ };
236
+
237
+ const sendMessage = async () => {
238
+ const text = userInput.value.trim();
239
+ if (!text || isThinking.value) return;
240
+
241
+ // Add user message
242
+ chatHistory.value.push({
243
+ role: 'user',
244
+ content: text,
245
+ time: new Date().toLocaleTimeString()
246
+ });
247
+ userInput.value = '';
248
+ isThinking.value = true;
249
+ scrollToBottom();
250
+
251
+ try {
252
+ const res = await fetch('/api/chat', {
253
+ method: 'POST',
254
+ headers: { 'Content-Type': 'application/json' },
255
+ body: JSON.stringify({
256
+ message: text,
257
+ context: trafficData.value
258
+ })
259
+ });
260
+ const data = await res.json();
261
+
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;
275
+ scrollToBottom();
276
+ }
277
+ };
278
+
279
+ const quickAsk = (text) => {
280
+ userInput.value = text;
281
+ sendMessage();
282
+ };
283
+
284
+ const clearChat = () => {
285
+ chatHistory.value = [];
286
+ };
287
+
288
+ const scrollToBottom = async () => {
289
+ await nextTick();
290
+ if (chatContainer.value) {
291
+ chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
292
+ }
293
+ };
294
+
295
+ const renderMarkdown = (text) => {
296
+ return md.render(text);
297
+ };
298
+
299
+ onMounted(() => {
300
+ // Init Chart
301
+ if (gaugeChart.value) {
302
+ chartInstance = echarts.init(gaugeChart.value);
303
+ window.addEventListener('resize', () => chartInstance.resize());
304
+ }
305
+
306
+ fetchTrafficData();
307
+ setInterval(fetchTrafficData, 5000); // Poll every 5s
308
+ });
309
+
310
+ return {
311
+ trafficData,
312
+ chatHistory,
313
+ userInput,
314
+ isThinking,
315
+ apiStatus,
316
+ chatContainer,
317
+ gaugeChart,
318
+ sendMessage,
319
+ quickAsk,
320
+ clearChat,
321
+ renderMarkdown
322
+ };
323
+ }
324
+ }).mount('#app');
325
+ </script>
326
+ </body>
327
+ </html>