diff --git a/asserts/ARCHITECTURE.md b/asserts/ARCHITECTURE.md
index 55b9601..e9dfc60 100644
--- a/asserts/ARCHITECTURE.md
+++ b/asserts/ARCHITECTURE.md
@@ -22,6 +22,7 @@ luxx/
│ ├── auth.py # 认证
│ ├── conversations.py # 会话管理
│ ├── messages.py # 消息处理
+│ ├── providers.py # LLM 提供商管理
│ └── tools.py # 工具管理
├── services/ # 服务层
│ ├── chat.py # 聊天服务
@@ -101,15 +102,63 @@ erDiagram
string id PK
string conversation_id FK
string role
- longtext content
+ longtext content "JSON 格式"
int token_count
datetime created_at
}
USER ||--o{ CONVERSATION : "has"
CONVERSATION ||--o{ MESSAGE : "has"
+
+ USER ||--o{ LLM_PROVIDER : "configures"
+
+ LLM_PROVIDER {
+ int id PK
+ int user_id FK
+ string name
+ string provider_type
+ string base_url
+ string api_key
+ string default_model
+ boolean is_default
+ boolean enabled
+ datetime created_at
+ datetime updated_at
+ }
```
+### Message Content JSON 结构
+
+`content` 字段统一使用 JSON 格式存储:
+
+**User 消息:**
+
+```json
+{
+ "text": "用户输入的文本内容",
+ "attachments": [
+ {"name": "utils.py", "extension": "py", "content": "..."}
+ ]
+}
+```
+
+**Assistant 消息:**
+
+```json
+{
+ "text": "AI 回复的文本内容",
+ "tool_calls": [...],
+ "steps": [
+ {"id": "step-0", "index": 0, "type": "thinking", "content": "..."},
+ {"id": "step-1", "index": 1, "type": "text", "content": "..."},
+ {"id": "step-2", "index": 2, "type": "tool_call", "id_ref": "call_xxx", "name": "...", "arguments": "..."},
+ {"id": "step-3", "index": 3, "type": "tool_result", "id_ref": "call_xxx", "name": "...", "content": "..."}
+ ]
+}
+```
+
+`steps` 字段是**渲染顺序的唯一数据源**,按 `index` 顺序排列。thinking、text、tool_call、tool_result 可以在多轮迭代中穿插出现。
+
### 5. 工具系统
```mermaid
@@ -191,6 +240,9 @@ LLM API 客户端:
| `/conversations` | GET/POST | 会话列表/创建 |
| `/conversations/{id}` | GET/DELETE | 会话详情/删除 |
| `/messages/stream` | POST | 流式消息发送 |
+| `/providers` | GET/POST | LLM 提供商列表/创建 |
+| `/providers/{id}` | GET/PUT/DELETE | 提供商详情/更新/删除 |
+| `/providers/{id}/test` | POST | 测试提供商连接 |
| `/tools` | GET | 可用工具列表 |
## 数据流
@@ -227,12 +279,29 @@ sequenceDiagram
| 事件 | 说明 |
|------|------|
-| `text` | 文本内容增量 |
-| `tool_call` | 工具调用请求 |
-| `tool_result` | 工具执行结果 |
+| `process_step` | 结构化步骤(thinking/text/tool_call/tool_result),携带 `id`、`index` 确保渲染顺序 |
| `done` | 响应完成 |
| `error` | 错误信息 |
+### process_step 事件格式
+
+```json
+{"type": "process_step", "step": {"id": "step-0", "index": 0, "type": "thinking", "content": "..."}}
+{"type": "process_step", "step": {"id": "step-1", "index": 1, "type": "text", "content": "回复文本..."}}
+{"type": "process_step", "step": {"id": "step-2", "index": 2, "type": "tool_call", "id_ref": "call_abc", "name": "web_search", "arguments": "{\"query\": \"...\"}"}}
+{"type": "process_step", "step": {"id": "step-3", "index": 3, "type": "tool_result", "id_ref": "call_abc", "name": "web_search", "content": "{\"success\": true, ...}"}}
+```
+
+| 字段 | 说明 |
+|------|------|
+| `id` | 步骤唯一标识(格式 `step-{index}`) |
+| `index` | 步骤序号,确保按正确顺序显示 |
+| `type` | 步骤类型:`thinking` / `text` / `tool_call` / `tool_result` |
+| `id_ref` | 工具调用引用 ID(仅 tool_call/tool_result) |
+| `name` | 工具名称(仅 tool_call/tool_result) |
+| `arguments` | 工具调用参数 JSON 字符串(仅 tool_call) |
+| `content` | 内容(thinking 的思考内容、text 的文本、tool_result 的返回结果) |
+
## 配置示例
### config.yaml
diff --git a/config.yaml b/config.yaml
index a59ab84..f13c4e2 100644
--- a/config.yaml
+++ b/config.yaml
@@ -1,7 +1,7 @@
# 配置文件
app:
secret_key: ${APP_SECRET_KEY}
- debug: true
+ debug: false
host: 0.0.0.0
port: 8000
diff --git a/dashboard/index.html b/dashboard/index.html
index 2412468..efa808b 100644
--- a/dashboard/index.html
+++ b/dashboard/index.html
@@ -4,7 +4,11 @@
-
dashboard
+ Luxx Dashboard
+
+
+
+
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
index 602e7a0..365c9d5 100644
--- a/dashboard/package-lock.json
+++ b/dashboard/package-lock.json
@@ -9,6 +9,9 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.15.0",
+ "katex": "^0.16.11",
+ "marked": "^15.0.0",
+ "marked-highlight": "^2.2.1",
"pinia": "^3.0.4",
"vue": "^3.5.32",
"vue-router": "^4.6.4"
@@ -586,6 +589,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/copy-anything": {
"version": "4.0.5",
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz",
@@ -887,6 +899,22 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
+ "node_modules/katex": {
+ "version": "0.16.45",
+ "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.45.tgz",
+ "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -1157,6 +1185,28 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/marked": {
+ "version": "15.0.12",
+ "resolved": "https://registry.npmmirror.com/marked/-/marked-15.0.12.tgz",
+ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/marked-highlight": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmmirror.com/marked-highlight/-/marked-highlight-2.2.4.tgz",
+ "integrity": "sha512-PZxisNMJDduSjc0q6uvjsnqqHCXc9s0eyzxDO9sB1eNGJnd/H1/Fu+z6g/liC1dfJdFW4SftMwMlLvsBhUPrqQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "marked": ">=4 <19"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
diff --git a/dashboard/package.json b/dashboard/package.json
index bc505fd..6e61846 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -10,6 +10,9 @@
},
"dependencies": {
"axios": "^1.15.0",
+ "katex": "^0.16.11",
+ "marked": "^15.0.0",
+ "marked-highlight": "^2.2.1",
"pinia": "^3.0.4",
"vue": "^3.5.32",
"vue-router": "^4.6.4"
diff --git a/dashboard/src/components/MessageBubble.vue b/dashboard/src/components/MessageBubble.vue
new file mode 100644
index 0000000..34addad
--- /dev/null
+++ b/dashboard/src/components/MessageBubble.vue
@@ -0,0 +1,137 @@
+
+
+
user
+
Luxx
+
+
+
+
+ {{ file.extension }}
+ {{ file.name }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/components/ProcessBlock.vue b/dashboard/src/components/ProcessBlock.vue
new file mode 100644
index 0000000..ef26c63
--- /dev/null
+++ b/dashboard/src/components/ProcessBlock.vue
@@ -0,0 +1,373 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AI 正在输入...
+
+
+
+
+
+
+
diff --git a/dashboard/src/services/api.js b/dashboard/src/services/api.js
index ccc9289..9d20737 100644
--- a/dashboard/src/services/api.js
+++ b/dashboard/src/services/api.js
@@ -36,101 +36,133 @@ api.interceptors.response.use(
}
)
+/**
+ * SSE 流式请求处理器
+ * @param {string} url - API URL (不含 baseURL 前缀)
+ * @param {object} body - 请求体
+ * @param {object} callbacks - 事件回调: { onProcessStep, onDone, onError }
+ * @returns {{ abort: () => void }}
+ */
+export function createSSEStream(url, body, { onProcessStep, onDone, onError }) {
+ const token = localStorage.getItem('access_token')
+ const controller = new AbortController()
+
+ const promise = (async () => {
+ try {
+ const res = await fetch(`/api${url}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ },
+ body: JSON.stringify(body),
+ signal: controller.signal
+ })
+
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({}))
+ throw new Error(err.message || `HTTP ${res.status}`)
+ }
+
+ const reader = res.body.getReader()
+ const decoder = new TextDecoder()
+ let buffer = ''
+ let completed = false
+
+ while (true) {
+ const { done, value } = await reader.read()
+ if (done) break
+
+ buffer += decoder.decode(value, { stream: true })
+ const lines = buffer.split('\n')
+ buffer = lines.pop() || ''
+
+ let currentEvent = ''
+ for (const line of lines) {
+ if (line.startsWith('event: ')) {
+ currentEvent = line.slice(7).trim()
+ } else if (line.startsWith('data: ')) {
+ const data = JSON.parse(line.slice(6))
+ if (currentEvent === 'process_step' && onProcessStep) {
+ onProcessStep(data)
+ } else if (currentEvent === 'done' && onDone) {
+ completed = true
+ onDone(data)
+ } else if (currentEvent === 'error' && onError) {
+ onError(data.content)
+ }
+ }
+ }
+ }
+
+ if (!completed && onError) {
+ onError('stream ended unexpectedly')
+ }
+ } catch (e) {
+ if (e.name !== 'AbortError' && onError) {
+ onError(e.message)
+ }
+ }
+ })()
+
+ promise.abort = () => controller.abort()
+ return promise
+}
+
// ============ 认证接口 ============
export const authAPI = {
- // 用户登录
login: (data) => api.post('/auth/login', data),
-
- // 用户注册
register: (data) => api.post('/auth/register', data),
-
- // 用户登出
logout: () => api.post('/auth/logout'),
-
- // 获取当前用户信息
getMe: () => api.get('/auth/me')
}
// ============ 会话接口 ============
export const conversationsAPI = {
- // 获取会话列表
list: (params) => api.get('/conversations/', { params }),
-
- // 创建会话
create: (data) => api.post('/conversations/', data),
-
- // 获取会话详情
get: (id) => api.get(`/conversations/${id}`),
-
- // 更新会话
update: (id, data) => api.put(`/conversations/${id}`, data),
-
- // 删除会话
delete: (id) => api.delete(`/conversations/${id}`)
}
// ============ 消息接口 ============
export const messagesAPI = {
- // 获取消息列表
list: (conversationId, params) => api.get('/messages/', { params: { conversation_id: conversationId, ...params } }),
-
- // 发送消息(非流式)
send: (data) => api.post('/messages/', data),
- // 发送消息(流式)- 使用原生 fetch 避免 axios 拦截
- sendStream: (data) => {
- const token = localStorage.getItem('access_token')
- return fetch('/api/messages/stream', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${token}`
- },
- body: JSON.stringify(data)
- })
+ // 发送消息(流式)
+ sendStream: (data, callbacks) => {
+ return createSSEStream('/messages/stream', {
+ conversation_id: data.conversation_id,
+ content: data.content,
+ tools_enabled: callbacks.toolsEnabled !== false
+ }, callbacks)
},
- // 删除消息
delete: (id) => api.delete(`/messages/${id}`)
}
// ============ 工具接口 ============
export const toolsAPI = {
- // 获取工具列表
list: (params) => api.get('/tools/', { params }),
-
- // 获取工具详情
get: (name) => api.get(`/tools/${name}`),
-
- // 执行工具
execute: (name, data) => api.post(`/tools/${name}/execute`, data)
}
// ============ LLM Provider 接口 ============
export const providersAPI = {
- // 获取提供商列表
list: () => api.get('/providers/'),
-
- // 创建提供商
create: (data) => api.post('/providers/', data),
-
- // 获取提供商详情
get: (id) => api.get(`/providers/${id}`),
-
- // 更新提供商
update: (id, data) => api.put(`/providers/${id}`, data),
-
- // 删除提供商
delete: (id) => api.delete(`/providers/${id}`),
-
- // 测试连接
test: (id) => api.post(`/providers/${id}/test`)
}
-// 默认导出
-export default api
\ No newline at end of file
+export default api
diff --git a/dashboard/src/style.css b/dashboard/src/style.css
index 9ac0253..9d7ae49 100644
--- a/dashboard/src/style.css
+++ b/dashboard/src/style.css
@@ -1,294 +1,455 @@
+/* ============ Global Reset & Base ============ */
:root {
- --text: #6b6375;
- --text-h: #08060d;
- --bg: #fff;
- --border: #e5e4e7;
- --code-bg: #f4f3ec;
- --accent: #2563eb;
- --accent-bg: rgba(37, 99, 235, 0.1);
- --accent-border: rgba(37, 99, 235, 0.5);
- --social-bg: rgba(244, 243, 236, 0.5);
- --shadow:
- rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+ /* 背景色 */
+ --bg-primary: #ffffff;
+ --bg-secondary: #f8fafc;
+ --bg-tertiary: #f0f4f8;
+ --bg-hover: rgba(37, 99, 235, 0.06);
+ --bg-active: rgba(37, 99, 235, 0.12);
+ --bg-input: #f8fafc;
+ --bg-code: #f1f5f9;
+ --bg-thinking: #f1f5f9;
- --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
- --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
- --mono: ui-monospace, Consolas, monospace;
+ /* 文字颜色 */
+ --text-primary: #1e293b;
+ --text-secondary: #64748b;
+ --text-tertiary: #94a3b8;
+
+ /* 边框颜色 */
+ --border-light: rgba(0, 0, 0, 0.06);
+ --border-medium: rgba(0, 0, 0, 0.08);
+ --border-input: rgba(0, 0, 0, 0.08);
+
+ /* 主题色 */
+ --accent-primary: #2563eb;
+ --accent-primary-hover: #3b82f6;
+ --accent-primary-light: rgba(37, 99, 235, 0.08);
+ --accent-primary-medium: rgba(37, 99, 235, 0.15);
+
+ /* 工具调用颜色 */
+ --tool-color: #5478FF;
+ --tool-color-hover: #3d5ce0;
+ --tool-bg: rgba(84, 120, 255, 0.18);
+ --tool-bg-hover: rgba(84, 120, 255, 0.28);
+ --tool-border: rgba(84, 120, 255, 0.22);
+
+ /* 附件颜色 */
+ --attachment-color: #ca8a04;
+ --attachment-color-hover: #a16207;
+ --attachment-bg: rgba(202, 138, 4, 0.15);
+
+ /* 状态颜色 */
+ --success-color: #059669;
+ --success-bg: rgba(16, 185, 129, 0.1);
+ --danger-color: #ef4444;
+ --danger-bg: rgba(239, 68, 68, 0.08);
+
+ /* 滚动条颜色 */
+ --scrollbar-thumb: rgba(0, 0, 0, 0.08);
+ --scrollbar-thumb-sidebar: rgba(0, 0, 0, 0.1);
+
+ /* 遮罩背景 */
+ --overlay-bg: rgba(0, 0, 0, 0.3);
+ --avatar-gradient: linear-gradient(135deg, #3b82f6, #60a5fa);
+
+ /* 兼容旧变量 */
+ --text: var(--text-primary);
+ --text-h: var(--text-primary);
+ --bg: var(--bg-primary);
+ --border: var(--border-light);
+ --accent: var(--accent-primary);
+ --accent-bg: var(--accent-primary-light);
+ --accent-border: var(--accent-primary-medium);
+ --social-bg: rgba(244, 243, 236, 0.5);
+
+ --sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ --heading: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ --mono: ui-monospace, Consolas, 'JetBrains Mono', monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
- color: var(--text);
- background: var(--bg);
+ color: var(--text-primary);
+ background: var(--bg-primary);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-
- @media (max-width: 1024px) {
- font-size: 16px;
- }
}
-@media (prefers-color-scheme: dark) {
- :root {
- --text: #9ca3af;
- --text-h: #f3f4f6;
- --bg: #16171d;
- --border: #2e303a;
- --code-bg: #1f2028;
- --accent: #60a5fa;
- --accent-bg: rgba(96, 165, 250, 0.15);
- --accent-border: rgba(96, 165, 250, 0.5);
- --social-bg: rgba(47, 48, 58, 0.5);
- --shadow:
- rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
- }
+[data-theme="dark"] {
+ --bg-primary: #1a1a1a;
+ --bg-secondary: #141414;
+ --bg-tertiary: #0a0a0a;
+ --bg-hover: rgba(255, 255, 255, 0.08);
+ --bg-active: rgba(255, 255, 255, 0.12);
+ --bg-input: #141414;
+ --bg-code: #141414;
+ --bg-thinking: #141414;
- #social .button-icon {
- filter: invert(1) brightness(2);
- }
+ --text-primary: #f0f0f0;
+ --text-secondary: #a0a0a0;
+ --text-tertiary: #606060;
+
+ --border-light: rgba(255, 255, 255, 0.08);
+ --border-medium: rgba(255, 255, 255, 0.12);
+ --border-input: rgba(255, 255, 255, 0.1);
+
+ --accent-primary: #3b82f6;
+ --accent-primary-hover: #60a5fa;
+ --accent-primary-light: rgba(59, 130, 246, 0.15);
+ --accent-primary-medium: rgba(59, 130, 246, 0.25);
+
+ --tool-color: #5478FF;
+ --tool-color-hover: #7a96ff;
+ --tool-bg: rgba(84, 120, 255, 0.28);
+ --tool-bg-hover: rgba(84, 120, 255, 0.40);
+ --tool-border: rgba(84, 120, 255, 0.32);
+
+ --attachment-color: #facc15;
+ --attachment-color-hover: #fde047;
+ --attachment-bg: rgba(250, 204, 21, 0.22);
+
+ --success-color: #34d399;
+ --success-bg: rgba(52, 211, 153, 0.15);
+ --danger-color: #f87171;
+ --danger-bg: rgba(248, 113, 113, 0.15);
+
+ --scrollbar-thumb: rgba(255, 255, 255, 0.1);
+ --scrollbar-thumb-sidebar: rgba(255, 255, 255, 0.15);
+
+ --overlay-bg: rgba(0, 0, 0, 0.6);
+ --avatar-gradient: linear-gradient(135deg, #3b82f6, #60a5fa);
+
+ /* 兼容旧变量 */
+ --text: var(--text-primary);
+ --text-h: var(--text-primary);
+ --bg: var(--bg-primary);
+ --border: var(--border-light);
+ --accent: var(--accent-primary);
+ --accent-bg: var(--accent-primary-light);
+ --accent-border: var(--accent-primary-medium);
+ --social-bg: rgba(47, 48, 58, 0.5);
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html, body {
+ height: 100%;
+ overflow: hidden;
}
body {
margin: 0;
}
-h1,
-h2 {
- font-family: var(--heading);
- font-weight: 500;
- color: var(--text-h);
-}
-
-h1 {
- font-size: 56px;
- letter-spacing: -1.68px;
- margin: 32px 0;
- @media (max-width: 1024px) {
- font-size: 36px;
- margin: 20px 0;
- }
-}
-h2 {
- font-size: 24px;
- line-height: 118%;
- letter-spacing: -0.24px;
- margin: 0 0 8px;
- @media (max-width: 1024px) {
- font-size: 20px;
- }
-}
-p {
- margin: 0;
-}
-
-code,
-.counter {
- font-family: var(--mono);
- display: inline-flex;
- border-radius: 4px;
- color: var(--text-h);
-}
-
-code {
- font-size: 15px;
- line-height: 135%;
- padding: 4px 8px;
- background: var(--code-bg);
-}
-
-.counter {
- font-size: 16px;
- padding: 5px 10px;
- border-radius: 5px;
- color: var(--accent);
- background: var(--accent-bg);
- border: 2px solid transparent;
- transition: border-color 0.3s;
- margin-bottom: 24px;
-
- &:hover {
- border-color: var(--accent-border);
- }
- &:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
- }
-}
-
-.hero {
- position: relative;
-
- .base,
- .framework,
- .vite {
- inset-inline: 0;
- margin: 0 auto;
- }
-
- .base {
- width: 170px;
- position: relative;
- z-index: 0;
- }
-
- .framework,
- .vite {
- position: absolute;
- }
-
- .framework {
- z-index: 1;
- top: 34px;
- height: 28px;
- transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
- scale(1.4);
- }
-
- .vite {
- z-index: 0;
- top: 107px;
- height: 26px;
- width: auto;
- transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
- scale(0.8);
- }
-}
-
#app {
- width: 100%;
- max-width: 100%;
- margin: 0 auto;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
+ height: 100%;
}
-#center {
- display: flex;
- flex-direction: column;
- gap: 25px;
- place-content: center;
- place-items: center;
- flex-grow: 1;
-
- @media (max-width: 1024px) {
- padding: 32px 20px 24px;
- gap: 18px;
- }
+/* ============ Scrollbar ============ */
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
}
-#next-steps {
- display: flex;
- border-top: 1px solid var(--border);
- text-align: left;
-
- & > div {
- flex: 1 1 0;
- padding: 32px;
- @media (max-width: 1024px) {
- padding: 24px 20px;
- }
- }
-
- .icon {
- margin-bottom: 16px;
- width: 22px;
- height: 22px;
- }
-
- @media (max-width: 1024px) {
- flex-direction: column;
- text-align: center;
- }
+::-webkit-scrollbar-track {
+ background: transparent;
}
-#docs {
- border-right: 1px solid var(--border);
-
- @media (max-width: 1024px) {
- border-right: none;
- border-bottom: 1px solid var(--border);
- }
+::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb);
+ border-radius: 3px;
}
-#next-steps ul {
- list-style: none;
+::-webkit-scrollbar-thumb:hover {
+ background: var(--border-medium);
+}
+
+/* ============ Ghost Button ============ */
+.ghost-btn {
+ background: none;
+ border: none;
+ color: var(--text-tertiary);
+ cursor: pointer;
+ padding: 4px 6px;
+ border-radius: 4px;
+ font-size: 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.15s;
+}
+
+.ghost-btn:hover {
+ background: var(--bg-hover);
+ color: var(--text-secondary);
+}
+
+.ghost-btn.danger:hover {
+ background: var(--danger-bg);
+ color: var(--danger-color);
+}
+
+.ghost-btn.success:hover {
+ background: var(--success-bg);
+ color: var(--success-color);
+}
+
+.ghost-btn.accent:hover {
+ background: var(--accent-primary-light);
+ color: var(--accent-primary);
+}
+
+/* ============ Markdown Content ============ */
+.md-content {
+ font-size: 15px;
+ line-height: 1.7;
+ color: var(--text-primary);
+ word-break: break-word;
+}
+
+.md-content h1,
+.md-content h2,
+.md-content h3 {
+ margin: 0.8em 0 0.4em;
+ font-weight: 600;
+}
+
+.md-content h1 { font-size: 1.4em; }
+.md-content h2 { font-size: 1.2em; }
+.md-content h3 { font-size: 1.1em; }
+
+.md-content p {
+ margin: 0.4em 0;
+}
+
+.md-content code {
+ background: var(--bg-code);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-family: var(--mono);
+ font-size: 0.9em;
+}
+
+.md-content pre {
+ background: var(--bg-code);
+ padding: 12px;
+ border-radius: 8px;
+ overflow-x: auto;
+ margin: 0.5em 0;
+}
+
+.md-content pre code {
+ background: transparent;
padding: 0;
- display: flex;
- gap: 8px;
- margin: 32px 0 0;
-
- .logo {
- height: 18px;
- }
-
- a {
- color: var(--text-h);
- font-size: 16px;
- border-radius: 6px;
- background: var(--social-bg);
- display: flex;
- padding: 6px 12px;
- align-items: center;
- gap: 8px;
- text-decoration: none;
- transition: box-shadow 0.3s;
-
- &:hover {
- box-shadow: var(--shadow);
- }
- .button-icon {
- height: 18px;
- width: 18px;
- }
- }
-
- @media (max-width: 1024px) {
- margin-top: 20px;
- flex-wrap: wrap;
- justify-content: center;
-
- li {
- flex: 1 1 calc(50% - 8px);
- }
-
- a {
- width: 100%;
- justify-content: center;
- box-sizing: border-box;
- }
- }
}
-#spacer {
- height: 88px;
- border-top: 1px solid var(--border);
- @media (max-width: 1024px) {
- height: 48px;
- }
+.md-content blockquote {
+ border-left: 3px solid var(--border-medium);
+ padding-left: 12px;
+ margin: 0.5em 0;
+ color: var(--text-secondary);
}
-.ticks {
- position: relative;
+.md-content ul,
+.md-content ol {
+ padding-left: 20px;
+ margin: 0.5em 0;
+}
+
+.md-content a {
+ color: var(--accent-primary);
+ text-decoration: none;
+}
+
+.md-content a:hover {
+ text-decoration: underline;
+}
+
+.md-content img {
+ max-width: 100%;
+ border-radius: 8px;
+}
+
+.md-content table {
+ border-collapse: collapse;
width: 100%;
-
- &::before,
- &::after {
- content: '';
- position: absolute;
- top: -4.5px;
- border: 5px solid transparent;
- }
-
- &::before {
- left: 0;
- border-left-color: var(--border);
- }
- &::after {
- right: 0;
- border-right-color: var(--border);
- }
+ margin: 0.5em 0;
+}
+
+.md-content th,
+.md-content td {
+ border: 1px solid var(--border-light);
+ padding: 8px 12px;
+ text-align: left;
+}
+
+.md-content th {
+ background: var(--bg-secondary);
+}
+
+/* ============ Message Bubble Shared ============ */
+.message-bubble {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 16px;
+ width: 100%;
+}
+
+.message-bubble.user {
+ flex-direction: row-reverse;
+}
+
+.message-container {
+ display: flex;
+ flex-direction: column;
+ min-width: 200px;
+ width: 100%;
+}
+
+.message-bubble.user .message-container {
+ align-items: flex-end;
+ width: fit-content;
+ max-width: 85%;
+}
+
+.message-bubble.assistant .message-container {
+ align-items: flex-start;
+ flex: 1 1 auto;
+ width: 100%;
+ min-width: 0;
+}
+
+.message-bubble.assistant .message-body {
+ width: 100%;
+}
+
+.avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: -0.3px;
+ flex-shrink: 0;
+}
+
+.user .avatar {
+ background: linear-gradient(135deg, #2563eb, #3b82f6);
+ color: white;
+ font-size: 12px;
+}
+
+.assistant .avatar {
+ background: var(--avatar-gradient);
+ color: white;
+ font-size: 12px;
+}
+
+.message-body {
+ flex: 1;
+ min-width: 0;
+ padding: 16px;
+ border: 1px solid var(--border-light);
+ border-radius: 12px;
+ background: var(--bg-primary);
+ transition: background 0.2s, border-color 0.2s;
+}
+
+/* ============ App Layout ============ */
+.app {
+ height: 100vh;
+ display: flex;
+ overflow: hidden;
+}
+
+.main-panel {
+ flex: 1 1 0;
+ min-width: 0;
+ overflow: hidden;
+ transition: all 0.2s;
+}
+
+/* ============ Transitions ============ */
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.2s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
+/* ============ Modal Overlay ============ */
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: var(--overlay-bg);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 100;
+}
+
+.modal-content {
+ background: var(--bg-primary);
+ border-radius: 12px;
+ max-width: 600px;
+ width: 90%;
+ max-height: 80vh;
+ overflow: auto;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+/* ============ Form ============ */
+.form-group {
+ margin-bottom: 16px;
+}
+
+.form-group label {
+ display: block;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+}
+
+.form-group input,
+.form-group textarea,
+.form-group select {
+ width: 100%;
+ padding: 10px 12px;
+ background: var(--bg-input);
+ border: 1px solid var(--border-input);
+ border-radius: 8px;
+ color: var(--text-primary);
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.form-group input:focus,
+.form-group textarea:focus,
+.form-group select:focus {
+ border-color: var(--accent-primary);
+}
+
+.form-group textarea {
+ resize: vertical;
+ min-height: 80px;
+}
+
+.form-group .hint {
+ font-size: 12px;
+ color: var(--text-tertiary);
+ margin-top: 4px;
}
diff --git a/dashboard/src/utils/markdown.js b/dashboard/src/utils/markdown.js
new file mode 100644
index 0000000..998ed8c
--- /dev/null
+++ b/dashboard/src/utils/markdown.js
@@ -0,0 +1,62 @@
+import { marked } from 'marked'
+import katex from 'katex'
+
+function renderMath(text, displayMode) {
+ try {
+ return katex.renderToString(text, {
+ displayMode,
+ throwOnError: false,
+ strict: false,
+ })
+ } catch {
+ return text
+ }
+}
+
+// marked extension for inline math $...$
+const mathExtension = {
+ name: 'math',
+ level: 'inline',
+ start(src) {
+ const idx = src.search(/(?${renderMath(token.text, true)}`
+ },
+}
+
+marked.use({
+ extensions: [blockMathExtension, mathExtension],
+ breaks: true,
+ gfm: true
+})
+
+export function renderMarkdown(text) {
+ return marked.parse(text)
+}
diff --git a/dashboard/src/views/ConversationDetailView.vue b/dashboard/src/views/ConversationDetailView.vue
index 37fc613..d03e546 100644
--- a/dashboard/src/views/ConversationDetailView.vue
+++ b/dashboard/src/views/ConversationDetailView.vue
@@ -1,55 +1,131 @@
-
-
-
-
加载中...
-
-
开始对话吧!
+
+
+
+
+
+
Chat
+
选择一个对话开始,或创建新对话
+
+
+
+