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
@@ -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,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -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
+ })