hermes-web-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +434 -0
  2. package/assets/logo.png +0 -0
  3. package/bin/hermes-web-ui.mjs +24 -0
  4. package/index.html +13 -0
  5. package/package.json +44 -0
  6. package/public/favicon.svg +1 -0
  7. package/public/icons.svg +24 -0
  8. package/src/App.vue +54 -0
  9. package/src/api/chat.ts +87 -0
  10. package/src/api/client.ts +44 -0
  11. package/src/api/jobs.ts +100 -0
  12. package/src/api/system.ts +25 -0
  13. package/src/assets/hero.png +0 -0
  14. package/src/assets/vite.svg +1 -0
  15. package/src/components/chat/ChatInput.vue +123 -0
  16. package/src/components/chat/ChatPanel.vue +289 -0
  17. package/src/components/chat/MarkdownRenderer.vue +187 -0
  18. package/src/components/chat/MessageItem.vue +189 -0
  19. package/src/components/chat/MessageList.vue +94 -0
  20. package/src/components/jobs/JobCard.vue +244 -0
  21. package/src/components/jobs/JobFormModal.vue +188 -0
  22. package/src/components/jobs/JobsPanel.vue +58 -0
  23. package/src/components/layout/AppSidebar.vue +169 -0
  24. package/src/composables/useKeyboard.ts +39 -0
  25. package/src/env.d.ts +7 -0
  26. package/src/main.ts +10 -0
  27. package/src/router/index.ts +24 -0
  28. package/src/stores/app.ts +66 -0
  29. package/src/stores/chat.ts +344 -0
  30. package/src/stores/jobs.ts +72 -0
  31. package/src/styles/global.scss +60 -0
  32. package/src/styles/theme.ts +71 -0
  33. package/src/styles/variables.scss +56 -0
  34. package/src/views/ChatView.vue +25 -0
  35. package/src/views/JobsView.vue +93 -0
  36. package/src/views/SettingsView.vue +257 -0
  37. package/tsconfig.app.json +17 -0
  38. package/tsconfig.json +7 -0
  39. package/tsconfig.node.json +24 -0
  40. package/vite.config.ts +39 -0
package/README.md ADDED
@@ -0,0 +1,434 @@
1
+ # Hermes UI
2
+
3
+ Hermes Agent 的 Web 管理面板,用于对话交互和定时任务管理。
4
+
5
+ ## 技术栈
6
+
7
+ - **Vue 3** — Composition API + `<script setup>`
8
+ - **TypeScript**
9
+ - **Vite** — 构建工具
10
+ - **Naive UI** — 组件库
11
+ - **Pinia** — 状态管理
12
+ - **Vue Router** — 路由(Hash 模式)
13
+ - **SCSS** — 样式预处理
14
+ - **markdown-it** + **highlight.js** — Markdown 渲染与代码高亮
15
+
16
+ ## 快速开始
17
+
18
+ ### 1. 配置 API Server
19
+
20
+ 编辑 `~/.hermes/config.yaml`,启用 API Server:
21
+
22
+ ```yaml
23
+ platforms:
24
+ api_server:
25
+ enabled: true
26
+ host: "127.0.0.1"
27
+ port: 8642
28
+ key: ""
29
+ cors_origins: "*"
30
+ ```
31
+
32
+ 重启 Gateway 使配置生效:
33
+
34
+ ```bash
35
+ hermes gateway restart
36
+ ```
37
+
38
+ ### 2. 安装并启动
39
+
40
+ ```bash
41
+ # 全局安装
42
+ npm install -g hermes-web-ui
43
+
44
+ # 启动 Web 面板(默认 http://localhost:8648)
45
+ hermes-web-ui start
46
+ ```
47
+
48
+ ### 开发模式
49
+
50
+ ```bash
51
+ # 克隆项目后
52
+ npm install
53
+ npm run dev
54
+ ```
55
+
56
+ ## 项目结构
57
+
58
+ ```
59
+ src/
60
+ ├── api/
61
+ │ ├── client.ts # HTTP 请求封装(fetch + Bearer Auth)
62
+ │ ├── chat.ts # 对话 API(startRun + SSE 事件流)
63
+ │ ├── jobs.ts # 定时任务 CRUD
64
+ │ └── system.ts # 健康检查、模型列表
65
+ ├── stores/
66
+ │ ├── app.ts # 全局状态(连接状态、版本、模型)
67
+ │ ├── chat.ts # 对话状态(消息、会话、流式输出)
68
+ │ └── jobs.ts # 任务状态(列表、CRUD 操作)
69
+ ├── components/
70
+ │ ├── layout/
71
+ │ │ └── AppSidebar.vue # 侧边栏导航
72
+ │ ├── chat/
73
+ │ │ ├── ChatPanel.vue # 对话面板(会话列表 + 聊天区域)
74
+ │ │ ├── MessageList.vue # 消息列表(自动滚动、加载动画)
75
+ │ │ ├── MessageItem.vue # 单条消息(用户/AI/工具/系统)
76
+ │ │ ├── ChatInput.vue # 输入框(Ctrl+Enter 发送)
77
+ │ │ └── MarkdownRenderer.vue # Markdown 渲染(代码高亮、复制)
78
+ │ └── jobs/
79
+ │ ├── JobsPanel.vue # 任务面板
80
+ │ ├── JobCard.vue # 任务卡片
81
+ │ └── JobFormModal.vue # 创建/编辑任务弹窗
82
+ ├── views/
83
+ │ ├── ChatView.vue # 对话页
84
+ │ └── JobsView.vue # 任务页
85
+ ├── router/
86
+ │ └── index.ts # 路由配置
87
+ ├── styles/
88
+ │ ├── variables.scss # SCSS 设计变量
89
+ │ ├── global.scss # 全局样式
90
+ │ └── theme.ts # Naive UI 主题覆盖
91
+ ├── composables/
92
+ │ └── useKeyboard.ts # 键盘快捷键
93
+ └── main.ts # 应用入口
94
+ ```
95
+
96
+ ## 功能特性
97
+
98
+ ### 对话(Chat)
99
+
100
+ - 基于 `/v1/runs` + `/v1/runs/{id}/events` 的异步 Run + SSE 事件流
101
+ - 实时流式输出,工具调用进度可视化
102
+ - 多会话管理,会话历史持久化到 localStorage
103
+ - Markdown 渲染,代码块语法高亮与一键复制
104
+
105
+ ### 定时任务(Jobs)
106
+
107
+ - 任务列表查看(含暂停/禁用任务)
108
+ - 创建、编辑、删除任务
109
+ - 暂停/恢复任务
110
+ - 立即触发任务执行
111
+ - Cron 表达式快速预设
112
+
113
+ ### 其他
114
+
115
+ - 连接状态实时检测(30s 轮询)
116
+ - 纯黑白主题
117
+ - 键盘快捷键支持
118
+
119
+ ---
120
+
121
+ ## API 接口文档
122
+
123
+ Base URL: `http://127.0.0.1:8642`
124
+
125
+ ### 认证
126
+
127
+ 除 `/health` 外,所有接口支持 Bearer Token 认证(如果服务端配置了 `key`):
128
+
129
+ ```
130
+ Authorization: Bearer <your-api-key>
131
+ ```
132
+
133
+ 未配置 key 时所有请求放行。
134
+
135
+ ### 通用错误格式
136
+
137
+ ```json
138
+ {
139
+ "error": {
140
+ "message": "错误描述",
141
+ "type": "invalid_request_error",
142
+ "param": null,
143
+ "code": "invalid_api_key"
144
+ }
145
+ }
146
+ ```
147
+
148
+ | 状态码 | 说明 |
149
+ |--------|------|
150
+ | 200 | 成功 |
151
+ | 400 | 请求参数错误 |
152
+ | 401 | API Key 无效 |
153
+ | 404 | 资源不存在 |
154
+ | 413 | 请求体过大(上限 1MB) |
155
+ | 429 | 并发超限(最大 10 个 Run) |
156
+ | 500 | 服务器内部错误 |
157
+
158
+ ---
159
+
160
+ ### 1. 健康检查
161
+
162
+ **GET** `/health` 或 `/v1/health`
163
+
164
+ 无需认证。
165
+
166
+ ```json
167
+ {"status": "ok", "platform": "hermes-agent"}
168
+ ```
169
+
170
+ ---
171
+
172
+ ### 2. 模型列表
173
+
174
+ **GET** `/v1/models`
175
+
176
+ ```json
177
+ {
178
+ "object": "list",
179
+ "data": [
180
+ {
181
+ "id": "hermes-agent",
182
+ "object": "model",
183
+ "created": 1744348800,
184
+ "owned_by": "hermes"
185
+ }
186
+ ]
187
+ }
188
+ ```
189
+
190
+ ---
191
+
192
+ ### 3. Chat Completions(OpenAI 兼容)
193
+
194
+ **POST** `/v1/chat/completions`
195
+
196
+ | 字段 | 类型 | 必填 | 说明 |
197
+ |------|------|------|------|
198
+ | messages | array | Y | 消息数组,格式同 OpenAI |
199
+ | stream | boolean | N | 是否流式返回,默认 false |
200
+ | model | string | N | 模型名,默认 "hermes-agent" |
201
+
202
+ 可选 Header: `X-Hermes-Session-Id` 指定会话 ID。
203
+
204
+ **stream=false 响应:**
205
+ ```json
206
+ {
207
+ "id": "chatcmpl-xxxxx",
208
+ "object": "chat.completion",
209
+ "created": 1744348800,
210
+ "model": "hermes-agent",
211
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": "回复内容"}, "finish_reason": "stop"}],
212
+ "usage": {"prompt_tokens": 100, "completion_tokens": 50, "total_tokens": 150}
213
+ }
214
+ ```
215
+
216
+ **stream=true 响应:** SSE 流(`Content-Type: text/event-stream`)
217
+ ```
218
+ data: {"id":"chatcmpl-xxx","choices":[{"delta":{"content":"你"},"index":0}]}
219
+ data: [DONE]
220
+ ```
221
+
222
+ ---
223
+
224
+ ### 4. Responses(有状态链式对话)
225
+
226
+ **POST** `/v1/responses`
227
+
228
+ | 字段 | 类型 | 必填 | 说明 |
229
+ |------|------|------|------|
230
+ | input | string / array | Y | 用户输入 |
231
+ | instructions | string | N | 系统指令 |
232
+ | previous_response_id | string | N | 链式对话的上一次响应 ID |
233
+ | conversation | string | N | 会话名称,自动链式到最新响应 |
234
+ | conversation_history | array | N | 显式传入对话历史 |
235
+ | store | boolean | N | 是否存储响应,默认 true |
236
+ | truncation | string | N | 设为 "auto" 自动截断历史到 100 条 |
237
+ | model | string | N | 模型名 |
238
+
239
+ > `conversation` 和 `previous_response_id` 互斥。
240
+
241
+ 可选 Header: `Idempotency-Key` 幂等键。
242
+
243
+ ```json
244
+ {
245
+ "id": "resp_xxx",
246
+ "object": "response",
247
+ "status": "completed",
248
+ "created_at": 1744348800,
249
+ "output": [{"type": "message", "role": "assistant", "content": "回复内容"}],
250
+ "usage": {"input_tokens": 100, "output_tokens": 50, "total_tokens": 150}
251
+ }
252
+ ```
253
+
254
+ ---
255
+
256
+ ### 5. 获取 / 删除存储的响应
257
+
258
+ **GET** `/v1/responses/{response_id}` — 获取存储的响应
259
+
260
+ **DELETE** `/v1/responses/{response_id}` — 删除存储的响应
261
+
262
+ ```json
263
+ {"id": "resp_xxx", "object": "response", "deleted": true}
264
+ ```
265
+
266
+ ---
267
+
268
+ ### 6. 启动异步 Run
269
+
270
+ **POST** `/v1/runs`
271
+
272
+ | 字段 | 类型 | 必填 | 说明 |
273
+ |------|------|------|------|
274
+ | input | string / array | Y | 用户输入 |
275
+ | instructions | string | N | 系统指令 |
276
+ | previous_response_id | string | N | 链式对话 ID |
277
+ | conversation_history | array | N | 对话历史 |
278
+ | session_id | string | N | 会话 ID,默认使用 run_id |
279
+
280
+ ```json
281
+ {"run_id": "run_xxx", "status": "started"}
282
+ ```
283
+
284
+ ---
285
+
286
+ ### 7. SSE 事件流
287
+
288
+ **GET** `/v1/runs/{run_id}/events`
289
+
290
+ `Content-Type: text/event-stream`
291
+
292
+ **事件类型:**
293
+
294
+ | 事件 | 说明 |
295
+ |------|------|
296
+ | `run.started` | Run 开始 |
297
+ | `message.delta` | 消息内容片段(字段 `delta`) |
298
+ | `tool.started` | 工具调用开始(字段 `tool`、`preview`) |
299
+ | `tool.completed` | 工具调用完成(字段 `tool`、`duration`) |
300
+ | `run.completed` | Run 完成(字段 `output`、`usage`) |
301
+ | `run.failed` | Run 失败(字段 `error`) |
302
+
303
+ 示例:
304
+ ```
305
+ data: {"event":"message.delta","run_id":"run_xxx","delta":"你好","timestamp":...}
306
+ data: {"event":"tool.started","run_id":"run_xxx","tool":"browser_navigate","preview":"https://...","timestamp":...}
307
+ data: {"event":"tool.completed","run_id":"run_xxx","tool":"browser_navigate","duration":3.8,"timestamp":...}
308
+ data: {"event":"run.completed","run_id":"run_xxx","output":"完整回复","usage":{"input_tokens":100,"output_tokens":50,"total_tokens":150}}
309
+ ```
310
+
311
+ ---
312
+
313
+ ### 8. 定时任务
314
+
315
+ #### 列出任务
316
+
317
+ **GET** `/api/jobs?include_disabled=true`
318
+
319
+ ```json
320
+ {
321
+ "jobs": [
322
+ {
323
+ "job_id": "61a5eb0baeb9",
324
+ "name": "任务名",
325
+ "schedule": "0 9 * * *",
326
+ "repeat": "forever",
327
+ "deliver": "origin",
328
+ "next_run_at": "2026-04-12T09:00:00+08:00",
329
+ "last_run_at": "2026-04-11T09:04:25+08:00",
330
+ "last_status": "ok",
331
+ "enabled": true,
332
+ "state": "scheduled",
333
+ "prompt_preview": "...",
334
+ "skills": []
335
+ }
336
+ ]
337
+ }
338
+ ```
339
+
340
+ #### 创建任务
341
+
342
+ **POST** `/api/jobs`
343
+
344
+ | 字段 | 类型 | 必填 | 说明 |
345
+ |------|------|------|------|
346
+ | name | string | Y | 任务名称(最大 200 字符) |
347
+ | schedule | string | Y | Cron 表达式 |
348
+ | prompt | string | N | 任务 prompt |
349
+ | deliver | string | N | 投递目标(origin / local / telegram / discord) |
350
+ | skills | array | N | skill 名称数组 |
351
+ | repeat | integer | N | 重复次数,不传表示永久 |
352
+
353
+ 响应包裹在 `{"job": {...}}` 中。
354
+
355
+ #### 查看任务详情
356
+
357
+ **GET** `/api/jobs/{job_id}`
358
+
359
+ #### 更新任务
360
+
361
+ **PATCH** `/api/jobs/{job_id}`
362
+
363
+ 可更新字段:`name`、`schedule`、`prompt`、`deliver`、`skills`、`repeat`、`enabled`
364
+
365
+ #### 删除任务
366
+
367
+ **DELETE** `/api/jobs/{job_id}`
368
+
369
+ ```json
370
+ {"ok": true}
371
+ ```
372
+
373
+ #### 暂停任务
374
+
375
+ **POST** `/api/jobs/{job_id}/pause`
376
+
377
+ ```json
378
+ {"job": {"job_id": "xxx", "enabled": false, "state": "paused", ...}}
379
+ ```
380
+
381
+ #### 恢复任务
382
+
383
+ **POST** `/api/jobs/{job_id}/resume`
384
+
385
+ ```json
386
+ {"job": {"job_id": "xxx", "enabled": true, "state": "scheduled", ...}}
387
+ ```
388
+
389
+ #### 立即触发任务
390
+
391
+ **POST** `/api/jobs/{job_id}/run`
392
+
393
+ ```json
394
+ {"job": {"job_id": "xxx", "state": "scheduled", ...}}
395
+ ```
396
+
397
+ ---
398
+
399
+ ## 快速测试
400
+
401
+ ```bash
402
+ # 健康检查
403
+ curl http://127.0.0.1:8642/health
404
+
405
+ # 模型列表
406
+ curl http://127.0.0.1:8642/v1/models
407
+
408
+ # Chat Completions
409
+ curl -X POST http://127.0.0.1:8642/v1/chat/completions \
410
+ -H "Content-Type: application/json" \
411
+ -d '{"messages":[{"role":"user","content":"你好"}]}'
412
+
413
+ # 启动异步 Run
414
+ curl -X POST http://127.0.0.1:8642/v1/runs \
415
+ -H "Content-Type: application/json" \
416
+ -d '{"input":"你好"}'
417
+
418
+ # 监听 Run 事件流
419
+ curl http://127.0.0.1:8642/v1/runs/{run_id}/events
420
+
421
+ # 列出任务(含已暂停)
422
+ curl "http://127.0.0.1:8642/api/jobs?include_disabled=true"
423
+
424
+ # 创建任务
425
+ curl -X POST http://127.0.0.1:8642/api/jobs \
426
+ -H "Content-Type: application/json" \
427
+ -d '{"name":"测试任务","schedule":"0 9 * * *","prompt":"执行测试"}'
428
+
429
+ # 暂停 / 恢复 / 触发 / 删除
430
+ curl -X POST http://127.0.0.1:8642/api/jobs/{job_id}/pause
431
+ curl -X POST http://127.0.0.1:8642/api/jobs/{job_id}/resume
432
+ curl -X POST http://127.0.0.1:8642/api/jobs/{job_id}/run
433
+ curl -X DELETE http://127.0.0.1:8642/api/jobs/{job_id}
434
+ ```
Binary file
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process'
3
+ import { resolve, dirname } from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+ const projectRoot = resolve(__dirname, '..')
8
+
9
+ const args = process.argv.slice(2)
10
+ const command = args[0]
11
+
12
+ if (!command || command === 'start' || command === 'dev') {
13
+ const viteBin = resolve(projectRoot, 'node_modules/.bin/vite')
14
+ spawn(viteBin, ['--host', '--port', '8648'], { stdio: 'inherit', cwd: projectRoot })
15
+ } else if (command === 'build') {
16
+ const viteBin = resolve(projectRoot, 'node_modules/.bin/vite')
17
+ spawn(viteBin, ['build'], { stdio: 'inherit', cwd: projectRoot })
18
+ } else {
19
+ console.log(`Usage: hermes-web-ui [command]`)
20
+ console.log()
21
+ console.log('Commands:')
22
+ console.log(' start Start dev server (default)')
23
+ console.log(' build Build for production')
24
+ }
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Hermes</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "hermes-web-ui",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "hermes-web-ui": "./bin/hermes-web-ui.mjs"
7
+ },
8
+ "scripts": {
9
+ "start": "vite --host --port 8648",
10
+ "dev": "vite --host",
11
+ "build": "vue-tsc -b && vite build",
12
+ "preview": "vite preview"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "index.html",
17
+ "public/",
18
+ "assets/",
19
+ "src/",
20
+ "vite.config.ts",
21
+ "tsconfig.json",
22
+ "tsconfig.app.json",
23
+ "tsconfig.node.json",
24
+ "package.json"
25
+ ],
26
+ "dependencies": {
27
+ "highlight.js": "^11.11.1",
28
+ "markdown-it": "^14.1.1",
29
+ "naive-ui": "^2.44.1",
30
+ "pinia": "^3.0.4",
31
+ "vue": "^3.5.32",
32
+ "vue-router": "^4.6.4"
33
+ },
34
+ "devDependencies": {
35
+ "@types/markdown-it": "^14.1.2",
36
+ "@types/node": "^24.12.2",
37
+ "@vitejs/plugin-vue": "^6.0.5",
38
+ "@vue/tsconfig": "^0.9.1",
39
+ "sass": "^1.99.0",
40
+ "typescript": "~6.0.2",
41
+ "vite": "^8.0.4",
42
+ "vue-tsc": "^3.2.6"
43
+ }
44
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg">
2
+ <symbol id="bluesky-icon" viewBox="0 0 16 17">
3
+ <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
4
+ <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
5
+ </symbol>
6
+ <symbol id="discord-icon" viewBox="0 0 20 19">
7
+ <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
8
+ </symbol>
9
+ <symbol id="documentation-icon" viewBox="0 0 21 20">
10
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
11
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
12
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
13
+ </symbol>
14
+ <symbol id="github-icon" viewBox="0 0 19 19">
15
+ <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
16
+ </symbol>
17
+ <symbol id="social-icon" viewBox="0 0 20 20">
18
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
19
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
20
+ </symbol>
21
+ <symbol id="x-icon" viewBox="0 0 19 19">
22
+ <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
23
+ </symbol>
24
+ </svg>
package/src/App.vue ADDED
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ import { onMounted, onUnmounted } from 'vue'
3
+ import { NConfigProvider, NMessageProvider, NDialogProvider, NNotificationProvider } from 'naive-ui'
4
+ import { themeOverrides } from '@/styles/theme'
5
+ import AppSidebar from '@/components/layout/AppSidebar.vue'
6
+ import { useKeyboard } from '@/composables/useKeyboard'
7
+ import { useAppStore } from '@/stores/app'
8
+
9
+ const appStore = useAppStore()
10
+
11
+ onMounted(() => {
12
+ appStore.startHealthPolling()
13
+ })
14
+
15
+ onUnmounted(() => {
16
+ appStore.stopHealthPolling()
17
+ })
18
+
19
+ useKeyboard()
20
+ </script>
21
+
22
+ <template>
23
+ <NConfigProvider :theme-overrides="themeOverrides">
24
+ <NMessageProvider>
25
+ <NDialogProvider>
26
+ <NNotificationProvider>
27
+ <div class="app-layout">
28
+ <AppSidebar />
29
+ <main class="app-main">
30
+ <router-view />
31
+ </main>
32
+ </div>
33
+ </NNotificationProvider>
34
+ </NDialogProvider>
35
+ </NMessageProvider>
36
+ </NConfigProvider>
37
+ </template>
38
+
39
+ <style scoped lang="scss">
40
+ @use '@/styles/variables' as *;
41
+
42
+ .app-layout {
43
+ display: flex;
44
+ height: 100vh;
45
+ width: 100vw;
46
+ overflow: hidden;
47
+ }
48
+
49
+ .app-main {
50
+ flex: 1;
51
+ overflow: hidden;
52
+ background-color: $bg-primary;
53
+ }
54
+ </style>