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.
- package/README.md +434 -0
- package/assets/logo.png +0 -0
- package/bin/hermes-web-ui.mjs +24 -0
- package/index.html +13 -0
- package/package.json +44 -0
- package/public/favicon.svg +1 -0
- package/public/icons.svg +24 -0
- package/src/App.vue +54 -0
- package/src/api/chat.ts +87 -0
- package/src/api/client.ts +44 -0
- package/src/api/jobs.ts +100 -0
- package/src/api/system.ts +25 -0
- package/src/assets/hero.png +0 -0
- package/src/assets/vite.svg +1 -0
- package/src/components/chat/ChatInput.vue +123 -0
- package/src/components/chat/ChatPanel.vue +289 -0
- package/src/components/chat/MarkdownRenderer.vue +187 -0
- package/src/components/chat/MessageItem.vue +189 -0
- package/src/components/chat/MessageList.vue +94 -0
- package/src/components/jobs/JobCard.vue +244 -0
- package/src/components/jobs/JobFormModal.vue +188 -0
- package/src/components/jobs/JobsPanel.vue +58 -0
- package/src/components/layout/AppSidebar.vue +169 -0
- package/src/composables/useKeyboard.ts +39 -0
- package/src/env.d.ts +7 -0
- package/src/main.ts +10 -0
- package/src/router/index.ts +24 -0
- package/src/stores/app.ts +66 -0
- package/src/stores/chat.ts +344 -0
- package/src/stores/jobs.ts +72 -0
- package/src/styles/global.scss +60 -0
- package/src/styles/theme.ts +71 -0
- package/src/styles/variables.scss +56 -0
- package/src/views/ChatView.vue +25 -0
- package/src/views/JobsView.vue +93 -0
- package/src/views/SettingsView.vue +257 -0
- package/tsconfig.app.json +17 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { startRun, streamRunEvents, type ChatMessage, type RunEvent } from '@/api/chat'
|
|
4
|
+
import { useAppStore } from './app'
|
|
5
|
+
|
|
6
|
+
export interface Message {
|
|
7
|
+
id: string
|
|
8
|
+
role: 'user' | 'assistant' | 'system' | 'tool'
|
|
9
|
+
content: string
|
|
10
|
+
timestamp: number
|
|
11
|
+
toolName?: string
|
|
12
|
+
toolPreview?: string
|
|
13
|
+
toolStatus?: 'running' | 'done' | 'error'
|
|
14
|
+
isStreaming?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Session {
|
|
18
|
+
id: string
|
|
19
|
+
title: string
|
|
20
|
+
messages: Message[]
|
|
21
|
+
createdAt: number
|
|
22
|
+
updatedAt: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function uid(): string {
|
|
26
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SESSIONS_KEY = 'hermes_chat_sessions'
|
|
30
|
+
const ACTIVE_SESSION_KEY = 'hermes_active_session'
|
|
31
|
+
|
|
32
|
+
function loadSessions(): Session[] {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(localStorage.getItem(SESSIONS_KEY) || '[]')
|
|
35
|
+
} catch {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function saveSessions(sessions: Session[]) {
|
|
41
|
+
localStorage.setItem(SESSIONS_KEY, JSON.stringify(sessions))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadActiveSessionId(): string | null {
|
|
45
|
+
return localStorage.getItem(ACTIVE_SESSION_KEY)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const useChatStore = defineStore('chat', () => {
|
|
49
|
+
const appStore = useAppStore()
|
|
50
|
+
const sessions = ref<Session[]>(loadSessions())
|
|
51
|
+
const activeSessionId = ref<string | null>(loadActiveSessionId())
|
|
52
|
+
const isStreaming = ref(false)
|
|
53
|
+
const abortController = ref<AbortController | null>(null)
|
|
54
|
+
|
|
55
|
+
const activeSession = ref<Session | null>(
|
|
56
|
+
sessions.value.find(s => s.id === activeSessionId.value) || null,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const messages = ref<Message[]>(activeSession.value?.messages || [])
|
|
60
|
+
|
|
61
|
+
function createSession(): Session {
|
|
62
|
+
const session: Session = {
|
|
63
|
+
id: uid(),
|
|
64
|
+
title: 'New Chat',
|
|
65
|
+
messages: [],
|
|
66
|
+
createdAt: Date.now(),
|
|
67
|
+
updatedAt: Date.now(),
|
|
68
|
+
}
|
|
69
|
+
sessions.value.unshift(session)
|
|
70
|
+
saveSessions(sessions.value)
|
|
71
|
+
return session
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function switchSession(sessionId: string) {
|
|
75
|
+
activeSessionId.value = sessionId
|
|
76
|
+
localStorage.setItem(ACTIVE_SESSION_KEY, sessionId)
|
|
77
|
+
activeSession.value = sessions.value.find(s => s.id === sessionId) || null
|
|
78
|
+
messages.value = activeSession.value ? [...activeSession.value.messages] : []
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function newChat() {
|
|
82
|
+
if (isStreaming.value) return
|
|
83
|
+
const session = createSession()
|
|
84
|
+
switchSession(session.id)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function deleteSession(sessionId: string) {
|
|
88
|
+
sessions.value = sessions.value.filter(s => s.id !== sessionId)
|
|
89
|
+
saveSessions(sessions.value)
|
|
90
|
+
if (activeSessionId.value === sessionId) {
|
|
91
|
+
if (sessions.value.length > 0) {
|
|
92
|
+
switchSession(sessions.value[0].id)
|
|
93
|
+
} else {
|
|
94
|
+
const session = createSession()
|
|
95
|
+
switchSession(session.id)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function persistMessages() {
|
|
101
|
+
if (!activeSession.value || !appStore.sessionPersistence) return
|
|
102
|
+
activeSession.value.messages = [...messages.value]
|
|
103
|
+
activeSession.value.updatedAt = Date.now()
|
|
104
|
+
|
|
105
|
+
if (activeSession.value.title === 'New Chat') {
|
|
106
|
+
const firstUser = messages.value.find(m => m.role === 'user')
|
|
107
|
+
if (firstUser) {
|
|
108
|
+
activeSession.value.title = firstUser.content.slice(0, 40) + (firstUser.content.length > 40 ? '...' : '')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const idx = sessions.value.findIndex(s => s.id === activeSession.value!.id)
|
|
113
|
+
if (idx !== -1) sessions.value[idx] = activeSession.value
|
|
114
|
+
saveSessions(sessions.value)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function addMessage(msg: Message) {
|
|
118
|
+
messages.value.push(msg)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function updateMessage(id: string, update: Partial<Message>) {
|
|
122
|
+
const idx = messages.value.findIndex(m => m.id === id)
|
|
123
|
+
if (idx !== -1) {
|
|
124
|
+
messages.value[idx] = { ...messages.value[idx], ...update }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function sendMessage(content: string) {
|
|
129
|
+
if (!content.trim() || isStreaming.value) return
|
|
130
|
+
|
|
131
|
+
if (!activeSession.value) {
|
|
132
|
+
const session = createSession()
|
|
133
|
+
switchSession(session.id)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const userMsg: Message = {
|
|
137
|
+
id: uid(),
|
|
138
|
+
role: 'user',
|
|
139
|
+
content: content.trim(),
|
|
140
|
+
timestamp: Date.now(),
|
|
141
|
+
}
|
|
142
|
+
addMessage(userMsg)
|
|
143
|
+
persistMessages()
|
|
144
|
+
|
|
145
|
+
isStreaming.value = true
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
// Build conversation history from past messages
|
|
149
|
+
const history: ChatMessage[] = messages.value
|
|
150
|
+
.filter(m => (m.role === 'user' || m.role === 'assistant') && m.content.trim())
|
|
151
|
+
.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content }))
|
|
152
|
+
|
|
153
|
+
const run = await startRun({
|
|
154
|
+
input: content.trim(),
|
|
155
|
+
conversation_history: history,
|
|
156
|
+
session_id: activeSession.value?.id,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const runId = (run as any).run_id || (run as any).id
|
|
160
|
+
if (!runId) {
|
|
161
|
+
addMessage({
|
|
162
|
+
id: uid(),
|
|
163
|
+
role: 'system',
|
|
164
|
+
content: `Error: startRun returned no run ID. Response: ${JSON.stringify(run)}`,
|
|
165
|
+
timestamp: Date.now(),
|
|
166
|
+
})
|
|
167
|
+
isStreaming.value = false
|
|
168
|
+
persistMessages()
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Listen to SSE events
|
|
173
|
+
abortController.value = streamRunEvents(
|
|
174
|
+
runId,
|
|
175
|
+
// onEvent
|
|
176
|
+
(evt: RunEvent) => {
|
|
177
|
+
switch (evt.event) {
|
|
178
|
+
case 'run.started':
|
|
179
|
+
// run started, nothing to render yet
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
case 'message.delta': {
|
|
183
|
+
// Find or create the assistant message
|
|
184
|
+
const last = messages.value[messages.value.length - 1]
|
|
185
|
+
if (last?.role === 'assistant' && last.isStreaming) {
|
|
186
|
+
last.content += evt.delta || ''
|
|
187
|
+
} else {
|
|
188
|
+
addMessage({
|
|
189
|
+
id: uid(),
|
|
190
|
+
role: 'assistant',
|
|
191
|
+
content: evt.delta || '',
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
isStreaming: true,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
break
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case 'tool.started': {
|
|
200
|
+
// Close any streaming assistant message first
|
|
201
|
+
const last = messages.value[messages.value.length - 1]
|
|
202
|
+
if (last?.isStreaming) {
|
|
203
|
+
updateMessage(last.id, { isStreaming: false })
|
|
204
|
+
}
|
|
205
|
+
// Add tool message
|
|
206
|
+
addMessage({
|
|
207
|
+
id: uid(),
|
|
208
|
+
role: 'tool',
|
|
209
|
+
content: '',
|
|
210
|
+
timestamp: Date.now(),
|
|
211
|
+
toolName: evt.tool || evt.name,
|
|
212
|
+
toolPreview: evt.preview,
|
|
213
|
+
toolStatus: 'running',
|
|
214
|
+
})
|
|
215
|
+
break
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
case 'tool.completed': {
|
|
219
|
+
// Find the running tool message and mark done
|
|
220
|
+
const toolMsgs = messages.value.filter(
|
|
221
|
+
m => m.role === 'tool' && m.toolStatus === 'running',
|
|
222
|
+
)
|
|
223
|
+
if (toolMsgs.length > 0) {
|
|
224
|
+
const last = toolMsgs[toolMsgs.length - 1]
|
|
225
|
+
updateMessage(last.id, { toolStatus: 'done' })
|
|
226
|
+
}
|
|
227
|
+
break
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case 'run.completed':
|
|
231
|
+
// Close any streaming message
|
|
232
|
+
const lastMsg = messages.value[messages.value.length - 1]
|
|
233
|
+
if (lastMsg?.isStreaming) {
|
|
234
|
+
updateMessage(lastMsg.id, { isStreaming: false })
|
|
235
|
+
}
|
|
236
|
+
isStreaming.value = false
|
|
237
|
+
abortController.value = null
|
|
238
|
+
persistMessages()
|
|
239
|
+
break
|
|
240
|
+
|
|
241
|
+
case 'run.failed':
|
|
242
|
+
// Mark error
|
|
243
|
+
const lastErr = messages.value[messages.value.length - 1]
|
|
244
|
+
if (lastErr?.isStreaming) {
|
|
245
|
+
updateMessage(lastErr.id, {
|
|
246
|
+
isStreaming: false,
|
|
247
|
+
content: evt.error ? `Error: ${evt.error}` : 'Run failed',
|
|
248
|
+
role: 'system',
|
|
249
|
+
})
|
|
250
|
+
} else {
|
|
251
|
+
addMessage({
|
|
252
|
+
id: uid(),
|
|
253
|
+
role: 'system',
|
|
254
|
+
content: evt.error ? `Error: ${evt.error}` : 'Run failed',
|
|
255
|
+
timestamp: Date.now(),
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
// Mark any running tools as error
|
|
259
|
+
messages.value.forEach((m, i) => {
|
|
260
|
+
if (m.role === 'tool' && m.toolStatus === 'running') {
|
|
261
|
+
messages.value[i] = { ...m, toolStatus: 'error' }
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
isStreaming.value = false
|
|
265
|
+
abortController.value = null
|
|
266
|
+
persistMessages()
|
|
267
|
+
break
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
// onDone
|
|
271
|
+
() => {
|
|
272
|
+
const last = messages.value[messages.value.length - 1]
|
|
273
|
+
if (last?.isStreaming) {
|
|
274
|
+
updateMessage(last.id, { isStreaming: false })
|
|
275
|
+
}
|
|
276
|
+
isStreaming.value = false
|
|
277
|
+
abortController.value = null
|
|
278
|
+
persistMessages()
|
|
279
|
+
},
|
|
280
|
+
// onError
|
|
281
|
+
(err) => {
|
|
282
|
+
const last = messages.value[messages.value.length - 1]
|
|
283
|
+
if (last?.isStreaming) {
|
|
284
|
+
updateMessage(last.id, {
|
|
285
|
+
isStreaming: false,
|
|
286
|
+
content: `Error: ${err.message}`,
|
|
287
|
+
role: 'system',
|
|
288
|
+
})
|
|
289
|
+
} else {
|
|
290
|
+
addMessage({
|
|
291
|
+
id: uid(),
|
|
292
|
+
role: 'system',
|
|
293
|
+
content: `Error: ${err.message}`,
|
|
294
|
+
timestamp: Date.now(),
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
isStreaming.value = false
|
|
298
|
+
abortController.value = null
|
|
299
|
+
persistMessages()
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
} catch (err: any) {
|
|
303
|
+
addMessage({
|
|
304
|
+
id: uid(),
|
|
305
|
+
role: 'system',
|
|
306
|
+
content: `Error: ${err.message}`,
|
|
307
|
+
timestamp: Date.now(),
|
|
308
|
+
})
|
|
309
|
+
isStreaming.value = false
|
|
310
|
+
abortController.value = null
|
|
311
|
+
persistMessages()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function stopStreaming() {
|
|
316
|
+
abortController.value?.abort()
|
|
317
|
+
isStreaming.value = false
|
|
318
|
+
const lastMsg = messages.value[messages.value.length - 1]
|
|
319
|
+
if (lastMsg?.isStreaming) {
|
|
320
|
+
updateMessage(lastMsg.id, { isStreaming: false })
|
|
321
|
+
}
|
|
322
|
+
abortController.value = null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (sessions.value.length === 0) {
|
|
326
|
+
const session = createSession()
|
|
327
|
+
switchSession(session.id)
|
|
328
|
+
} else if (!activeSession.value) {
|
|
329
|
+
switchSession(sessions.value[0].id)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
sessions,
|
|
334
|
+
activeSessionId,
|
|
335
|
+
activeSession,
|
|
336
|
+
messages,
|
|
337
|
+
isStreaming,
|
|
338
|
+
newChat,
|
|
339
|
+
switchSession,
|
|
340
|
+
deleteSession,
|
|
341
|
+
sendMessage,
|
|
342
|
+
stopStreaming,
|
|
343
|
+
}
|
|
344
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import * as jobsApi from '@/api/jobs'
|
|
4
|
+
import type { Job, CreateJobRequest, UpdateJobRequest } from '@/api/jobs'
|
|
5
|
+
|
|
6
|
+
function matchId(job: Job, id: string): boolean {
|
|
7
|
+
return job.job_id === id || job.id === id
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useJobsStore = defineStore('jobs', () => {
|
|
11
|
+
const jobs = ref<Job[]>([])
|
|
12
|
+
const loading = ref(false)
|
|
13
|
+
|
|
14
|
+
async function fetchJobs() {
|
|
15
|
+
loading.value = true
|
|
16
|
+
try {
|
|
17
|
+
jobs.value = await jobsApi.listJobs()
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('Failed to fetch jobs:', err)
|
|
20
|
+
} finally {
|
|
21
|
+
loading.value = false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function createJob(data: CreateJobRequest): Promise<Job> {
|
|
26
|
+
const job = await jobsApi.createJob(data)
|
|
27
|
+
jobs.value.unshift(job)
|
|
28
|
+
return job
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function updateJob(jobId: string, data: UpdateJobRequest): Promise<Job> {
|
|
32
|
+
const job = await jobsApi.updateJob(jobId, data)
|
|
33
|
+
const idx = jobs.value.findIndex(j => matchId(j, jobId))
|
|
34
|
+
if (idx !== -1) jobs.value[idx] = job
|
|
35
|
+
return job
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function deleteJob(jobId: string) {
|
|
39
|
+
await jobsApi.deleteJob(jobId)
|
|
40
|
+
jobs.value = jobs.value.filter(j => !matchId(j, jobId))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function pauseJob(jobId: string) {
|
|
44
|
+
const job = await jobsApi.pauseJob(jobId)
|
|
45
|
+
const idx = jobs.value.findIndex(j => matchId(j, jobId))
|
|
46
|
+
if (idx !== -1) jobs.value[idx] = job
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function resumeJob(jobId: string) {
|
|
50
|
+
const job = await jobsApi.resumeJob(jobId)
|
|
51
|
+
const idx = jobs.value.findIndex(j => matchId(j, jobId))
|
|
52
|
+
if (idx !== -1) jobs.value[idx] = job
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function runJob(jobId: string) {
|
|
56
|
+
const job = await jobsApi.runJob(jobId)
|
|
57
|
+
const idx = jobs.value.findIndex(j => matchId(j, jobId))
|
|
58
|
+
if (idx !== -1) jobs.value[idx] = job
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
jobs,
|
|
63
|
+
loading,
|
|
64
|
+
fetchJobs,
|
|
65
|
+
createJob,
|
|
66
|
+
updateJob,
|
|
67
|
+
deleteJob,
|
|
68
|
+
pauseJob,
|
|
69
|
+
resumeJob,
|
|
70
|
+
runJob,
|
|
71
|
+
}
|
|
72
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
@use 'variables' as *;
|
|
2
|
+
|
|
3
|
+
*,
|
|
4
|
+
*::before,
|
|
5
|
+
*::after {
|
|
6
|
+
margin: 0;
|
|
7
|
+
padding: 0;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
html, body, #app {
|
|
12
|
+
height: 100%;
|
|
13
|
+
width: 100%;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
font-family: $font-ui;
|
|
19
|
+
background-color: $bg-primary;
|
|
20
|
+
color: $text-primary;
|
|
21
|
+
font-size: 14px;
|
|
22
|
+
line-height: 1.6;
|
|
23
|
+
-webkit-font-smoothing: antialiased;
|
|
24
|
+
-moz-osx-font-smoothing: grayscale;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
code, pre, .mono {
|
|
28
|
+
font-family: $font-code;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
a {
|
|
32
|
+
color: $accent-primary;
|
|
33
|
+
text-decoration: none;
|
|
34
|
+
|
|
35
|
+
&:hover {
|
|
36
|
+
color: $accent-hover;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
::-webkit-scrollbar {
|
|
41
|
+
width: 6px;
|
|
42
|
+
height: 6px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
::-webkit-scrollbar-track {
|
|
46
|
+
background: transparent;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
::-webkit-scrollbar-thumb {
|
|
50
|
+
background: $border-color;
|
|
51
|
+
border-radius: 3px;
|
|
52
|
+
|
|
53
|
+
&:hover {
|
|
54
|
+
background: $text-muted;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
::selection {
|
|
59
|
+
background: rgba($accent-primary, 0.3);
|
|
60
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { GlobalThemeOverrides } from 'naive-ui'
|
|
2
|
+
|
|
3
|
+
export const themeOverrides: GlobalThemeOverrides = {
|
|
4
|
+
common: {
|
|
5
|
+
primaryColor: '#333333',
|
|
6
|
+
primaryColorHover: '#1a1a1a',
|
|
7
|
+
primaryColorPressed: '#000000',
|
|
8
|
+
primaryColorSuppl: '#333333',
|
|
9
|
+
bodyColor: '#fafafa',
|
|
10
|
+
cardColor: '#ffffff',
|
|
11
|
+
modalColor: '#ffffff',
|
|
12
|
+
popoverColor: '#ffffff',
|
|
13
|
+
tableColor: '#ffffff',
|
|
14
|
+
inputColor: '#ffffff',
|
|
15
|
+
actionColor: '#f0f0f0',
|
|
16
|
+
textColorBase: '#1a1a1a',
|
|
17
|
+
textColor1: '#1a1a1a',
|
|
18
|
+
textColor2: '#666666',
|
|
19
|
+
textColor3: '#999999',
|
|
20
|
+
dividerColor: '#e0e0e0',
|
|
21
|
+
borderColor: '#e0e0e0',
|
|
22
|
+
hoverColor: 'rgba(0, 0, 0, 0.04)',
|
|
23
|
+
borderRadius: '8px',
|
|
24
|
+
borderRadiusSmall: '6px',
|
|
25
|
+
fontSize: '14px',
|
|
26
|
+
fontSizeMedium: '14px',
|
|
27
|
+
heightMedium: '36px',
|
|
28
|
+
fontFamily: 'Inter, system-ui, -apple-system, sans-serif',
|
|
29
|
+
fontFamilyMono: 'JetBrains Mono, Fira Code, Consolas, monospace',
|
|
30
|
+
},
|
|
31
|
+
Layout: {
|
|
32
|
+
color: '#fafafa',
|
|
33
|
+
siderColor: '#f5f5f5',
|
|
34
|
+
headerColor: '#fafafa',
|
|
35
|
+
},
|
|
36
|
+
Menu: {
|
|
37
|
+
itemTextColorActive: '#1a1a1a',
|
|
38
|
+
itemTextColorActiveHover: '#1a1a1a',
|
|
39
|
+
itemTextColorChildActive: '#1a1a1a',
|
|
40
|
+
itemIconColorActive: '#1a1a1a',
|
|
41
|
+
itemIconColorActiveHover: '#000000',
|
|
42
|
+
itemColorActive: 'rgba(0, 0, 0, 0.06)',
|
|
43
|
+
itemColorActiveHover: 'rgba(0, 0, 0, 0.1)',
|
|
44
|
+
arrowColorActive: '#1a1a1a',
|
|
45
|
+
},
|
|
46
|
+
Button: {
|
|
47
|
+
textColorPrimary: '#ffffff',
|
|
48
|
+
colorPrimary: '#333333',
|
|
49
|
+
colorHoverPrimary: '#1a1a1a',
|
|
50
|
+
colorPressedPrimary: '#000000',
|
|
51
|
+
},
|
|
52
|
+
Input: {
|
|
53
|
+
color: '#ffffff',
|
|
54
|
+
colorFocus: '#ffffff',
|
|
55
|
+
border: '1px solid #e0e0e0',
|
|
56
|
+
borderHover: '1px solid #999999',
|
|
57
|
+
borderFocus: '1px solid #333333',
|
|
58
|
+
placeholderColor: '#999999',
|
|
59
|
+
caretColor: '#1a1a1a',
|
|
60
|
+
},
|
|
61
|
+
Card: {
|
|
62
|
+
color: '#ffffff',
|
|
63
|
+
borderColor: '#e0e0e0',
|
|
64
|
+
},
|
|
65
|
+
Modal: {
|
|
66
|
+
color: '#ffffff',
|
|
67
|
+
},
|
|
68
|
+
Tag: {
|
|
69
|
+
borderRadius: '6px',
|
|
70
|
+
},
|
|
71
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// 黑白水墨 — Pure Ink
|
|
2
|
+
// 纯黑白灰,无彩色
|
|
3
|
+
|
|
4
|
+
// Backgrounds
|
|
5
|
+
$bg-primary: #fafafa;
|
|
6
|
+
$bg-secondary: #f0f0f0;
|
|
7
|
+
$bg-sidebar: #f5f5f5;
|
|
8
|
+
$bg-card: #ffffff;
|
|
9
|
+
$bg-card-hover: #fafafa;
|
|
10
|
+
$bg-input: #ffffff;
|
|
11
|
+
|
|
12
|
+
// Borders
|
|
13
|
+
$border-color: #e0e0e0;
|
|
14
|
+
$border-light: #ebebeb;
|
|
15
|
+
|
|
16
|
+
// Accent
|
|
17
|
+
$accent-primary: #333333;
|
|
18
|
+
$accent-hover: #1a1a1a;
|
|
19
|
+
$accent-muted: #888888;
|
|
20
|
+
|
|
21
|
+
// Text
|
|
22
|
+
$text-primary: #1a1a1a;
|
|
23
|
+
$text-secondary: #666666;
|
|
24
|
+
$text-muted: #999999;
|
|
25
|
+
|
|
26
|
+
// Status
|
|
27
|
+
$success: #2e7d32;
|
|
28
|
+
$error: #c62828;
|
|
29
|
+
$warning: #f57f17;
|
|
30
|
+
$info: $accent-primary;
|
|
31
|
+
|
|
32
|
+
// Message bubbles
|
|
33
|
+
$msg-user-bg: #e8e8e8;
|
|
34
|
+
$msg-assistant-bg: #f5f5f5;
|
|
35
|
+
$msg-system-border: #bdbdbd;
|
|
36
|
+
|
|
37
|
+
// Code
|
|
38
|
+
$code-bg: #f4f4f4;
|
|
39
|
+
|
|
40
|
+
// Typography
|
|
41
|
+
$font-ui: 'Inter', system-ui, -apple-system, sans-serif;
|
|
42
|
+
$font-code: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
|
43
|
+
|
|
44
|
+
// Layout
|
|
45
|
+
$sidebar-width: 240px;
|
|
46
|
+
$sidebar-collapsed-width: 64px;
|
|
47
|
+
$header-height: 56px;
|
|
48
|
+
|
|
49
|
+
// Radius
|
|
50
|
+
$radius-sm: 6px;
|
|
51
|
+
$radius-md: 10px;
|
|
52
|
+
$radius-lg: 14px;
|
|
53
|
+
|
|
54
|
+
// Transition
|
|
55
|
+
$transition-fast: 0.15s ease;
|
|
56
|
+
$transition-normal: 0.25s ease;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted } from 'vue'
|
|
3
|
+
import ChatPanel from '@/components/chat/ChatPanel.vue'
|
|
4
|
+
import { useAppStore } from '@/stores/app'
|
|
5
|
+
|
|
6
|
+
const appStore = useAppStore()
|
|
7
|
+
|
|
8
|
+
onMounted(() => {
|
|
9
|
+
appStore.loadModels()
|
|
10
|
+
})
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div class="chat-view">
|
|
15
|
+
<ChatPanel />
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<style scoped lang="scss">
|
|
20
|
+
.chat-view {
|
|
21
|
+
height: 100vh;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
}
|
|
25
|
+
</style>
|