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,93 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue'
|
|
3
|
+
import { NButton, NSpin } from 'naive-ui'
|
|
4
|
+
import JobsPanel from '@/components/jobs/JobsPanel.vue'
|
|
5
|
+
import JobFormModal from '@/components/jobs/JobFormModal.vue'
|
|
6
|
+
import { useJobsStore } from '@/stores/jobs'
|
|
7
|
+
|
|
8
|
+
const jobsStore = useJobsStore()
|
|
9
|
+
const showModal = ref(false)
|
|
10
|
+
const editingJob = ref<string | null>(null)
|
|
11
|
+
|
|
12
|
+
onMounted(() => {
|
|
13
|
+
jobsStore.fetchJobs()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function openCreateModal() {
|
|
17
|
+
editingJob.value = null
|
|
18
|
+
showModal.value = true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function openEditModal(jobId: string) {
|
|
22
|
+
editingJob.value = jobId
|
|
23
|
+
showModal.value = true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function handleModalClose() {
|
|
27
|
+
showModal.value = false
|
|
28
|
+
editingJob.value = null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function handleSave() {
|
|
32
|
+
await jobsStore.fetchJobs()
|
|
33
|
+
handleModalClose()
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div class="jobs-view">
|
|
39
|
+
<header class="jobs-header">
|
|
40
|
+
<h2 class="header-title">Scheduled Jobs</h2>
|
|
41
|
+
<NButton type="primary" @click="openCreateModal">
|
|
42
|
+
<template #icon>
|
|
43
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
44
|
+
</template>
|
|
45
|
+
Create Job
|
|
46
|
+
</NButton>
|
|
47
|
+
</header>
|
|
48
|
+
|
|
49
|
+
<div class="jobs-content">
|
|
50
|
+
<NSpin :show="jobsStore.loading && jobsStore.jobs.length === 0">
|
|
51
|
+
<JobsPanel @edit="openEditModal" />
|
|
52
|
+
</NSpin>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<JobFormModal
|
|
56
|
+
v-if="showModal"
|
|
57
|
+
:job-id="editingJob"
|
|
58
|
+
@close="handleModalClose"
|
|
59
|
+
@saved="handleSave"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<style scoped lang="scss">
|
|
65
|
+
@use '@/styles/variables' as *;
|
|
66
|
+
|
|
67
|
+
.jobs-view {
|
|
68
|
+
height: 100vh;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.jobs-header {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: space-between;
|
|
77
|
+
padding: 12px 20px;
|
|
78
|
+
border-bottom: 1px solid $border-color;
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.header-title {
|
|
83
|
+
font-size: 16px;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
color: $text-primary;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.jobs-content {
|
|
89
|
+
flex: 1;
|
|
90
|
+
overflow-y: auto;
|
|
91
|
+
padding: 20px;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
NButton, NInput, NSwitch, NSlider, NSelect, NDataTable, useMessage,
|
|
5
|
+
} from 'naive-ui'
|
|
6
|
+
import { useAppStore } from '@/stores/app'
|
|
7
|
+
import { setServerUrl, setApiKey, getBaseUrlValue } from '@/api/client'
|
|
8
|
+
|
|
9
|
+
const appStore = useAppStore()
|
|
10
|
+
const message = useMessage()
|
|
11
|
+
|
|
12
|
+
const serverUrl = ref(getBaseUrlValue())
|
|
13
|
+
const apiKey = ref(localStorage.getItem('hermes_api_key') || '')
|
|
14
|
+
const testingConnection = ref(false)
|
|
15
|
+
|
|
16
|
+
const modelOptions = computed(() =>
|
|
17
|
+
appStore.models.map(m => ({ label: m.id, value: m.id })),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
async function handleTestConnection() {
|
|
21
|
+
testingConnection.value = true
|
|
22
|
+
setServerUrl(serverUrl.value)
|
|
23
|
+
if (apiKey.value) setApiKey(apiKey.value)
|
|
24
|
+
try {
|
|
25
|
+
await appStore.checkConnection()
|
|
26
|
+
if (appStore.connected) {
|
|
27
|
+
message.success('Connected successfully')
|
|
28
|
+
} else {
|
|
29
|
+
message.error('Connection failed')
|
|
30
|
+
}
|
|
31
|
+
} catch (e: any) {
|
|
32
|
+
message.error(e.message)
|
|
33
|
+
} finally {
|
|
34
|
+
testingConnection.value = false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleSaveApiKey() {
|
|
39
|
+
setApiKey(apiKey.value)
|
|
40
|
+
message.success('API key saved')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const endpointColumns = [
|
|
44
|
+
{ title: 'Method', key: 'method', width: 80 },
|
|
45
|
+
{ title: 'Endpoint', key: 'endpoint' },
|
|
46
|
+
{ title: 'Description', key: 'description' },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
const endpoints = [
|
|
50
|
+
{ method: 'GET', endpoint: '/health', description: 'Health Check' },
|
|
51
|
+
{ method: 'GET', endpoint: '/v1/health', description: 'Health Check (v1)' },
|
|
52
|
+
{ method: 'GET', endpoint: '/v1/models', description: 'Model List' },
|
|
53
|
+
{ method: 'POST', endpoint: '/v1/chat/completions', description: 'Chat Completions (OpenAI compatible)' },
|
|
54
|
+
{ method: 'POST', endpoint: '/v1/responses', description: 'Create Response (stateful)' },
|
|
55
|
+
{ method: 'GET', endpoint: '/v1/responses/{id}', description: 'Get Stored Response' },
|
|
56
|
+
{ method: 'DELETE', endpoint: '/v1/responses/{id}', description: 'Delete Response' },
|
|
57
|
+
{ method: 'POST', endpoint: '/v1/runs', description: 'Start Async Run' },
|
|
58
|
+
{ method: 'GET', endpoint: '/v1/runs/{id}/events', description: 'SSE Event Stream' },
|
|
59
|
+
{ method: 'GET', endpoint: '/api/jobs', description: 'List Jobs' },
|
|
60
|
+
{ method: 'POST', endpoint: '/api/jobs', description: 'Create Job' },
|
|
61
|
+
{ method: 'GET', endpoint: '/api/jobs/{id}', description: 'Get Job Detail' },
|
|
62
|
+
{ method: 'PATCH', endpoint: '/api/jobs/{id}', description: 'Update Job' },
|
|
63
|
+
{ method: 'DELETE', endpoint: '/api/jobs/{id}', description: 'Delete Job' },
|
|
64
|
+
{ method: 'POST', endpoint: '/api/jobs/{id}/pause', description: 'Pause Job' },
|
|
65
|
+
{ method: 'POST', endpoint: '/api/jobs/{id}/resume', description: 'Resume Job' },
|
|
66
|
+
{ method: 'POST', endpoint: '/api/jobs/{id}/run', description: 'Trigger Job Now' },
|
|
67
|
+
]
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div class="settings-view">
|
|
72
|
+
<header class="settings-header">
|
|
73
|
+
<h2 class="header-title">Settings</h2>
|
|
74
|
+
</header>
|
|
75
|
+
|
|
76
|
+
<div class="settings-content">
|
|
77
|
+
<!-- API Configuration -->
|
|
78
|
+
<section class="settings-section">
|
|
79
|
+
<h3 class="section-title">API Configuration</h3>
|
|
80
|
+
<div class="form-group">
|
|
81
|
+
<label class="form-label">Server URL</label>
|
|
82
|
+
<NInput v-model:value="serverUrl" placeholder="http://127.0.0.1:8642" />
|
|
83
|
+
</div>
|
|
84
|
+
<div class="form-group">
|
|
85
|
+
<label class="form-label">API Key (optional)</label>
|
|
86
|
+
<div class="input-with-action">
|
|
87
|
+
<NInput v-model:value="apiKey" type="password" show-password-on="click" placeholder="Enter API key" />
|
|
88
|
+
<NButton size="small" @click="handleSaveApiKey">Save</NButton>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="form-group">
|
|
92
|
+
<div class="connection-status">
|
|
93
|
+
<span class="status-dot" :class="{ on: appStore.connected, off: !appStore.connected }"></span>
|
|
94
|
+
<span>{{ appStore.connected ? 'Connected' : 'Disconnected' }}</span>
|
|
95
|
+
<span v-if="appStore.serverVersion" class="version">v{{ appStore.serverVersion }}</span>
|
|
96
|
+
</div>
|
|
97
|
+
<NButton type="primary" size="small" :loading="testingConnection" @click="handleTestConnection">
|
|
98
|
+
Test Connection
|
|
99
|
+
</NButton>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
|
|
103
|
+
<!-- Chat Settings -->
|
|
104
|
+
<section class="settings-section">
|
|
105
|
+
<h3 class="section-title">Chat Settings</h3>
|
|
106
|
+
<div class="form-group">
|
|
107
|
+
<label class="form-label">Default Model</label>
|
|
108
|
+
<NSelect
|
|
109
|
+
v-model:value="appStore.selectedModel"
|
|
110
|
+
:options="modelOptions"
|
|
111
|
+
placeholder="Select model"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="form-group">
|
|
115
|
+
<label class="form-label">Stream Responses</label>
|
|
116
|
+
<NSwitch v-model:value="appStore.streamEnabled" />
|
|
117
|
+
</div>
|
|
118
|
+
<div class="form-group">
|
|
119
|
+
<label class="form-label">Session Persistence</label>
|
|
120
|
+
<NSwitch v-model:value="appStore.sessionPersistence" />
|
|
121
|
+
</div>
|
|
122
|
+
<div class="form-group">
|
|
123
|
+
<label class="form-label">Max Tokens: {{ appStore.maxTokens }}</label>
|
|
124
|
+
<NSlider v-model:value="appStore.maxTokens" :min="256" :max="32768" :step="256" />
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
|
|
128
|
+
<!-- About -->
|
|
129
|
+
<section class="settings-section">
|
|
130
|
+
<h3 class="section-title">About</h3>
|
|
131
|
+
<p class="about-text">
|
|
132
|
+
Hermes Agent Web UI
|
|
133
|
+
<br />Version 0.1.0
|
|
134
|
+
</p>
|
|
135
|
+
<div class="endpoint-table">
|
|
136
|
+
<NDataTable
|
|
137
|
+
:columns="endpointColumns"
|
|
138
|
+
:data="endpoints"
|
|
139
|
+
:bordered="false"
|
|
140
|
+
size="small"
|
|
141
|
+
:row-props="() => ({ style: 'cursor: default;' })"
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
</section>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
|
|
149
|
+
<style scoped lang="scss">
|
|
150
|
+
@use '@/styles/variables' as *;
|
|
151
|
+
|
|
152
|
+
.settings-view {
|
|
153
|
+
height: 100vh;
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.settings-header {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
padding: 12px 20px;
|
|
162
|
+
border-bottom: 1px solid $border-color;
|
|
163
|
+
flex-shrink: 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.header-title {
|
|
167
|
+
font-size: 16px;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
color: $text-primary;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.settings-content {
|
|
173
|
+
flex: 1;
|
|
174
|
+
overflow-y: auto;
|
|
175
|
+
padding: 20px;
|
|
176
|
+
max-width: 640px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.settings-section {
|
|
180
|
+
margin-bottom: 28px;
|
|
181
|
+
|
|
182
|
+
.section-title {
|
|
183
|
+
font-size: 13px;
|
|
184
|
+
font-weight: 600;
|
|
185
|
+
color: $text-secondary;
|
|
186
|
+
text-transform: uppercase;
|
|
187
|
+
letter-spacing: 0.5px;
|
|
188
|
+
margin-bottom: 14px;
|
|
189
|
+
padding-bottom: 8px;
|
|
190
|
+
border-bottom: 1px solid $border-light;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.form-group {
|
|
195
|
+
margin-bottom: 14px;
|
|
196
|
+
|
|
197
|
+
.form-label {
|
|
198
|
+
display: block;
|
|
199
|
+
font-size: 13px;
|
|
200
|
+
color: $text-secondary;
|
|
201
|
+
margin-bottom: 6px;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.input-with-action {
|
|
206
|
+
display: flex;
|
|
207
|
+
gap: 8px;
|
|
208
|
+
align-items: center;
|
|
209
|
+
|
|
210
|
+
.n-input {
|
|
211
|
+
flex: 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.connection-status {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
gap: 8px;
|
|
219
|
+
font-size: 13px;
|
|
220
|
+
color: $text-secondary;
|
|
221
|
+
margin-bottom: 10px;
|
|
222
|
+
|
|
223
|
+
.status-dot {
|
|
224
|
+
width: 8px;
|
|
225
|
+
height: 8px;
|
|
226
|
+
border-radius: 50%;
|
|
227
|
+
|
|
228
|
+
&.on {
|
|
229
|
+
background-color: $success;
|
|
230
|
+
box-shadow: 0 0 6px rgba($success, 0.5);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
&.off {
|
|
234
|
+
background-color: $error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.version {
|
|
239
|
+
color: $text-muted;
|
|
240
|
+
font-size: 12px;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.about-text {
|
|
245
|
+
font-size: 13px;
|
|
246
|
+
color: $text-secondary;
|
|
247
|
+
line-height: 1.6;
|
|
248
|
+
margin-bottom: 14px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.endpoint-table {
|
|
252
|
+
:deep(.n-data-table) {
|
|
253
|
+
--n-td-color: transparent;
|
|
254
|
+
--n-th-color: rgba($accent-primary, 0.04);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
5
|
+
"types": ["vite/client"],
|
|
6
|
+
"ignoreDeprecations": "6.0",
|
|
7
|
+
"baseUrl": ".",
|
|
8
|
+
"paths": {
|
|
9
|
+
"@/*": ["src/*"]
|
|
10
|
+
},
|
|
11
|
+
"noUnusedLocals": true,
|
|
12
|
+
"noUnusedParameters": true,
|
|
13
|
+
"erasableSyntaxOnly": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
|
17
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"erasableSyntaxOnly": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["vite.config.ts"]
|
|
24
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import type { ProxyOptions } from 'vite'
|
|
4
|
+
import { resolve } from 'path'
|
|
5
|
+
|
|
6
|
+
function createProxyConfig(): ProxyOptions {
|
|
7
|
+
return {
|
|
8
|
+
target: 'http://127.0.0.1:8642',
|
|
9
|
+
changeOrigin: true,
|
|
10
|
+
configure: (proxy) => {
|
|
11
|
+
proxy.on('proxyReq', (proxyReq) => {
|
|
12
|
+
proxyReq.removeHeader('origin')
|
|
13
|
+
proxyReq.removeHeader('referer')
|
|
14
|
+
})
|
|
15
|
+
// Disable response buffering for SSE streaming
|
|
16
|
+
proxy.on('proxyRes', (proxyRes) => {
|
|
17
|
+
proxyRes.headers['cache-control'] = 'no-cache'
|
|
18
|
+
proxyRes.headers['x-accel-buffering'] = 'no'
|
|
19
|
+
})
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
plugins: [vue()],
|
|
26
|
+
resolve: {
|
|
27
|
+
alias: {
|
|
28
|
+
'@': resolve(__dirname, 'src'),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
server: {
|
|
32
|
+
compress: false,
|
|
33
|
+
proxy: {
|
|
34
|
+
'/api': createProxyConfig(),
|
|
35
|
+
'/v1': createProxyConfig(),
|
|
36
|
+
'/health': createProxyConfig(),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
})
|