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,188 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, computed } from 'vue'
|
|
3
|
+
import { NModal, NForm, NFormItem, NInput, NButton, NSelect, NInputNumber, useMessage } from 'naive-ui'
|
|
4
|
+
import { useJobsStore } from '@/stores/jobs'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
jobId: string | null
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const emit = defineEmits<{
|
|
11
|
+
close: []
|
|
12
|
+
saved: []
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const jobsStore = useJobsStore()
|
|
16
|
+
const message = useMessage()
|
|
17
|
+
|
|
18
|
+
const showModal = ref(true)
|
|
19
|
+
const loading = ref(false)
|
|
20
|
+
|
|
21
|
+
const formData = ref({
|
|
22
|
+
name: '',
|
|
23
|
+
schedule: '',
|
|
24
|
+
prompt: '',
|
|
25
|
+
deliver: 'origin',
|
|
26
|
+
repeat_times: null as number | null,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const presetValue = ref<string | null>(null)
|
|
30
|
+
|
|
31
|
+
const isEdit = computed(() => !!props.jobId)
|
|
32
|
+
|
|
33
|
+
const schedulePresets = [
|
|
34
|
+
{ label: 'Every minute', value: '* * * * *' },
|
|
35
|
+
{ label: 'Every 5 minutes', value: '*/5 * * * *' },
|
|
36
|
+
{ label: 'Every hour', value: '0 * * * *' },
|
|
37
|
+
{ label: 'Every day at 00:00', value: '0 0 * * *' },
|
|
38
|
+
{ label: 'Every day at 09:00', value: '0 9 * * *' },
|
|
39
|
+
{ label: 'Every Monday at 09:00', value: '0 9 * * 1' },
|
|
40
|
+
{ label: 'Every month 1st at 09:00', value: '0 9 1 * *' },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const targetOptions = [
|
|
44
|
+
{ label: 'Origin', value: 'origin' },
|
|
45
|
+
{ label: 'Local', value: 'local' },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
onMounted(async () => {
|
|
49
|
+
if (props.jobId) {
|
|
50
|
+
try {
|
|
51
|
+
const { getJob } = await import('@/api/jobs')
|
|
52
|
+
const job = await getJob(props.jobId)
|
|
53
|
+
formData.value = {
|
|
54
|
+
name: job.name,
|
|
55
|
+
schedule: typeof job.schedule === 'string' ? job.schedule : (job.schedule?.expr || job.schedule_display || ''),
|
|
56
|
+
prompt: job.prompt,
|
|
57
|
+
deliver: job.deliver || 'origin',
|
|
58
|
+
repeat_times: typeof job.repeat === 'number' ? job.repeat : (typeof job.repeat === 'object' ? job.repeat.times : null),
|
|
59
|
+
}
|
|
60
|
+
} catch (e: any) {
|
|
61
|
+
message.error('Failed to load job: ' + e.message)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
async function handleSave() {
|
|
67
|
+
if (!formData.value.name.trim()) {
|
|
68
|
+
message.warning('Name is required')
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (!formData.value.schedule.trim()) {
|
|
72
|
+
message.warning('Schedule is required')
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
loading.value = true
|
|
77
|
+
try {
|
|
78
|
+
const payload = {
|
|
79
|
+
name: formData.value.name,
|
|
80
|
+
schedule: formData.value.schedule,
|
|
81
|
+
prompt: formData.value.prompt,
|
|
82
|
+
deliver: formData.value.deliver,
|
|
83
|
+
repeat: formData.value.repeat_times ?? undefined,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isEdit.value) {
|
|
87
|
+
await jobsStore.updateJob(props.jobId!, payload)
|
|
88
|
+
message.success('Job updated')
|
|
89
|
+
} else {
|
|
90
|
+
await jobsStore.createJob(payload)
|
|
91
|
+
message.success('Job created')
|
|
92
|
+
}
|
|
93
|
+
emit('saved')
|
|
94
|
+
} catch (e: any) {
|
|
95
|
+
message.error(e.message)
|
|
96
|
+
} finally {
|
|
97
|
+
loading.value = false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function handleClose() {
|
|
102
|
+
showModal.value = false
|
|
103
|
+
setTimeout(() => emit('close'), 200)
|
|
104
|
+
}
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
<template>
|
|
108
|
+
<NModal
|
|
109
|
+
v-model:show="showModal"
|
|
110
|
+
preset="card"
|
|
111
|
+
:title="isEdit ? 'Edit Job' : 'Create Job'"
|
|
112
|
+
:style="{ width: '520px' }"
|
|
113
|
+
:mask-closable="!loading"
|
|
114
|
+
@after-leave="emit('close')"
|
|
115
|
+
>
|
|
116
|
+
<NForm label-placement="top">
|
|
117
|
+
<NFormItem label="Name" required>
|
|
118
|
+
<NInput
|
|
119
|
+
v-model:value="formData.name"
|
|
120
|
+
placeholder="Job name"
|
|
121
|
+
maxlength="200"
|
|
122
|
+
show-count
|
|
123
|
+
/>
|
|
124
|
+
</NFormItem>
|
|
125
|
+
|
|
126
|
+
<NFormItem label="Schedule (Cron Expression)" required>
|
|
127
|
+
<NInput
|
|
128
|
+
v-model:value="formData.schedule"
|
|
129
|
+
placeholder="e.g. 0 9 * * *"
|
|
130
|
+
/>
|
|
131
|
+
</NFormItem>
|
|
132
|
+
|
|
133
|
+
<NFormItem label="Quick Presets">
|
|
134
|
+
<NSelect
|
|
135
|
+
v-model:value="presetValue"
|
|
136
|
+
:options="schedulePresets"
|
|
137
|
+
placeholder="Select a preset..."
|
|
138
|
+
@update:value="v => formData.schedule = v"
|
|
139
|
+
/>
|
|
140
|
+
</NFormItem>
|
|
141
|
+
|
|
142
|
+
<NFormItem label="Prompt" required>
|
|
143
|
+
<NInput
|
|
144
|
+
v-model:value="formData.prompt"
|
|
145
|
+
type="textarea"
|
|
146
|
+
placeholder="The prompt to execute"
|
|
147
|
+
:rows="4"
|
|
148
|
+
maxlength="5000"
|
|
149
|
+
show-count
|
|
150
|
+
/>
|
|
151
|
+
</NFormItem>
|
|
152
|
+
|
|
153
|
+
<NFormItem label="Deliver Target">
|
|
154
|
+
<NSelect
|
|
155
|
+
v-model:value="formData.deliver"
|
|
156
|
+
:options="targetOptions"
|
|
157
|
+
/>
|
|
158
|
+
</NFormItem>
|
|
159
|
+
|
|
160
|
+
<NFormItem label="Repeat Count (optional)">
|
|
161
|
+
<NInputNumber
|
|
162
|
+
v-model:value="formData.repeat_times"
|
|
163
|
+
:min="1"
|
|
164
|
+
placeholder="Leave empty for infinite"
|
|
165
|
+
clearable
|
|
166
|
+
style="width: 100%"
|
|
167
|
+
/>
|
|
168
|
+
</NFormItem>
|
|
169
|
+
</NForm>
|
|
170
|
+
|
|
171
|
+
<template #footer>
|
|
172
|
+
<div class="modal-footer">
|
|
173
|
+
<NButton @click="handleClose">Cancel</NButton>
|
|
174
|
+
<NButton type="primary" :loading="loading" @click="handleSave">
|
|
175
|
+
{{ isEdit ? 'Update' : 'Create' }}
|
|
176
|
+
</NButton>
|
|
177
|
+
</div>
|
|
178
|
+
</template>
|
|
179
|
+
</NModal>
|
|
180
|
+
</template>
|
|
181
|
+
|
|
182
|
+
<style scoped lang="scss">
|
|
183
|
+
.modal-footer {
|
|
184
|
+
display: flex;
|
|
185
|
+
justify-content: flex-end;
|
|
186
|
+
gap: 8px;
|
|
187
|
+
}
|
|
188
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import JobCard from './JobCard.vue'
|
|
3
|
+
import { useJobsStore } from '@/stores/jobs'
|
|
4
|
+
|
|
5
|
+
const emit = defineEmits<{
|
|
6
|
+
edit: [jobId: string]
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const jobsStore = useJobsStore()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div v-if="jobsStore.jobs.length === 0" class="empty-state">
|
|
14
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" class="empty-icon">
|
|
15
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
|
16
|
+
<line x1="16" y1="2" x2="16" y2="6"/>
|
|
17
|
+
<line x1="8" y1="2" x2="8" y2="6"/>
|
|
18
|
+
<line x1="3" y1="10" x2="21" y2="10"/>
|
|
19
|
+
</svg>
|
|
20
|
+
<p>No scheduled jobs yet. Create one to get started.</p>
|
|
21
|
+
</div>
|
|
22
|
+
<div v-else class="jobs-grid">
|
|
23
|
+
<JobCard
|
|
24
|
+
v-for="job in jobsStore.jobs"
|
|
25
|
+
:key="job.id"
|
|
26
|
+
:job="job"
|
|
27
|
+
@edit="emit('edit', job.id)"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<style scoped lang="scss">
|
|
33
|
+
@use '@/styles/variables' as *;
|
|
34
|
+
|
|
35
|
+
.empty-state {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
height: 100%;
|
|
41
|
+
color: $text-muted;
|
|
42
|
+
gap: 12px;
|
|
43
|
+
|
|
44
|
+
.empty-icon {
|
|
45
|
+
opacity: 0.3;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
p {
|
|
49
|
+
font-size: 14px;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.jobs-grid {
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
56
|
+
gap: 14px;
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
4
|
+
import { useAppStore } from '@/stores/app'
|
|
5
|
+
|
|
6
|
+
const route = useRoute()
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
const appStore = useAppStore()
|
|
9
|
+
|
|
10
|
+
const selectedKey = computed(() => route.name as string)
|
|
11
|
+
|
|
12
|
+
function handleNav(key: string) {
|
|
13
|
+
router.push({ name: key })
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<aside class="sidebar">
|
|
19
|
+
<div class="sidebar-logo" @click="router.push('/')">
|
|
20
|
+
<img src="/assets/logo.png" alt="Hermes" class="logo-img" />
|
|
21
|
+
<span class="logo-text">Hermes</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<nav class="sidebar-nav">
|
|
25
|
+
<button
|
|
26
|
+
class="nav-item"
|
|
27
|
+
:class="{ active: selectedKey === 'chat' }"
|
|
28
|
+
@click="handleNav('chat')"
|
|
29
|
+
>
|
|
30
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
31
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
32
|
+
</svg>
|
|
33
|
+
<span>Chat</span>
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
<button
|
|
37
|
+
class="nav-item"
|
|
38
|
+
:class="{ active: selectedKey === 'jobs' }"
|
|
39
|
+
@click="handleNav('jobs')"
|
|
40
|
+
>
|
|
41
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
42
|
+
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
|
43
|
+
<line x1="16" y1="2" x2="16" y2="6" />
|
|
44
|
+
<line x1="8" y1="2" x2="8" y2="6" />
|
|
45
|
+
<line x1="3" y1="10" x2="21" y2="10" />
|
|
46
|
+
</svg>
|
|
47
|
+
<span>Jobs</span>
|
|
48
|
+
</button>
|
|
49
|
+
</nav>
|
|
50
|
+
|
|
51
|
+
<div class="sidebar-footer">
|
|
52
|
+
<div class="status-indicator" :class="{ connected: appStore.connected, disconnected: !appStore.connected }">
|
|
53
|
+
<span class="status-dot"></span>
|
|
54
|
+
<span class="status-text">{{ appStore.connected ? 'Connected' : 'Disconnected' }}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="version-info">Hermes {{ appStore.serverVersion || 'v0.1.0' }}</div>
|
|
57
|
+
</div>
|
|
58
|
+
</aside>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<style scoped lang="scss">
|
|
62
|
+
@use '@/styles/variables' as *;
|
|
63
|
+
|
|
64
|
+
.sidebar {
|
|
65
|
+
width: $sidebar-width;
|
|
66
|
+
height: 100vh;
|
|
67
|
+
background-color: $bg-sidebar;
|
|
68
|
+
border-right: 1px solid $border-color;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
padding: 20px 12px;
|
|
72
|
+
flex-shrink: 0;
|
|
73
|
+
transition: width $transition-normal;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.logo-img {
|
|
77
|
+
width: 28px;
|
|
78
|
+
height: 28px;
|
|
79
|
+
border-radius: 50%;
|
|
80
|
+
flex-shrink: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.sidebar-logo {
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 10px;
|
|
87
|
+
padding: 4px 12px 20px;
|
|
88
|
+
color: $text-primary;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
|
|
91
|
+
.logo-text {
|
|
92
|
+
font-size: 18px;
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
letter-spacing: 0.5px;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.sidebar-nav {
|
|
99
|
+
flex: 1;
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: 4px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.nav-item {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 10px;
|
|
109
|
+
padding: 10px 12px;
|
|
110
|
+
border: none;
|
|
111
|
+
background: none;
|
|
112
|
+
color: $text-secondary;
|
|
113
|
+
font-size: 14px;
|
|
114
|
+
border-radius: $radius-sm;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
transition: all $transition-fast;
|
|
117
|
+
width: 100%;
|
|
118
|
+
text-align: left;
|
|
119
|
+
|
|
120
|
+
&:hover {
|
|
121
|
+
background-color: rgba($accent-primary, 0.06);
|
|
122
|
+
color: $text-primary;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
&.active {
|
|
126
|
+
background-color: rgba($accent-primary, 0.12);
|
|
127
|
+
color: $accent-primary;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.sidebar-footer {
|
|
132
|
+
padding-top: 16px;
|
|
133
|
+
border-top: 1px solid $border-color;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.status-indicator {
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 8px;
|
|
140
|
+
padding: 8px 12px;
|
|
141
|
+
font-size: 12px;
|
|
142
|
+
|
|
143
|
+
.status-dot {
|
|
144
|
+
width: 8px;
|
|
145
|
+
height: 8px;
|
|
146
|
+
border-radius: 50%;
|
|
147
|
+
flex-shrink: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
&.connected .status-dot {
|
|
151
|
+
background-color: $success;
|
|
152
|
+
box-shadow: 0 0 6px rgba($success, 0.5);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
&.disconnected .status-dot {
|
|
156
|
+
background-color: $error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.status-text {
|
|
160
|
+
color: $text-secondary;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.version-info {
|
|
165
|
+
padding: 4px 12px;
|
|
166
|
+
font-size: 11px;
|
|
167
|
+
color: $text-muted;
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { onMounted, onUnmounted } from 'vue'
|
|
2
|
+
import { useRouter } from 'vue-router'
|
|
3
|
+
import { useChatStore } from '@/stores/chat'
|
|
4
|
+
|
|
5
|
+
export function useKeyboard() {
|
|
6
|
+
const router = useRouter()
|
|
7
|
+
const chatStore = useChatStore()
|
|
8
|
+
|
|
9
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
10
|
+
const mod = e.ctrlKey || e.metaKey
|
|
11
|
+
|
|
12
|
+
if (mod && e.key === 'n') {
|
|
13
|
+
e.preventDefault()
|
|
14
|
+
chatStore.newChat()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (mod && e.key === 'j') {
|
|
18
|
+
e.preventDefault()
|
|
19
|
+
router.push({ name: 'jobs' })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (e.key === 'Escape') {
|
|
23
|
+
// Close any open modals — naive-ui handles this internally
|
|
24
|
+
const modal = document.querySelector('.n-modal-mask')
|
|
25
|
+
if (modal) {
|
|
26
|
+
const closeBtn = modal.querySelector('.n-base-close') as HTMLElement
|
|
27
|
+
closeBtn?.click()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onMounted(() => {
|
|
33
|
+
window.addEventListener('keydown', handleKeydown)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
onUnmounted(() => {
|
|
37
|
+
window.removeEventListener('keydown', handleKeydown)
|
|
38
|
+
})
|
|
39
|
+
}
|
package/src/env.d.ts
ADDED
package/src/main.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createRouter, createWebHashHistory } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
const router = createRouter({
|
|
4
|
+
history: createWebHashHistory(),
|
|
5
|
+
routes: [
|
|
6
|
+
{
|
|
7
|
+
path: '/',
|
|
8
|
+
name: 'chat',
|
|
9
|
+
component: () => import('@/views/ChatView.vue'),
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
path: '/jobs',
|
|
13
|
+
name: 'jobs',
|
|
14
|
+
component: () => import('@/views/JobsView.vue'),
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
path: '/settings',
|
|
18
|
+
name: 'settings',
|
|
19
|
+
redirect: '/',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export default router
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { checkHealth, fetchModels } from '@/api/system'
|
|
4
|
+
import type { Model } from '@/api/system'
|
|
5
|
+
|
|
6
|
+
export const useAppStore = defineStore('app', () => {
|
|
7
|
+
const connected = ref(false)
|
|
8
|
+
const serverVersion = ref('')
|
|
9
|
+
const models = ref<Model[]>([])
|
|
10
|
+
const healthPollTimer = ref<ReturnType<typeof setInterval>>()
|
|
11
|
+
|
|
12
|
+
// Settings
|
|
13
|
+
const streamEnabled = ref(true)
|
|
14
|
+
const sessionPersistence = ref(true)
|
|
15
|
+
const maxTokens = ref(4096)
|
|
16
|
+
const selectedModel = ref('hermes-agent')
|
|
17
|
+
|
|
18
|
+
async function checkConnection() {
|
|
19
|
+
try {
|
|
20
|
+
const res = await checkHealth()
|
|
21
|
+
connected.value = true
|
|
22
|
+
if (res.version) serverVersion.value = res.version
|
|
23
|
+
} catch {
|
|
24
|
+
connected.value = false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function loadModels() {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetchModels()
|
|
31
|
+
models.value = res.data || []
|
|
32
|
+
if (models.value.length > 0 && !models.value.find(m => m.id === selectedModel.value)) {
|
|
33
|
+
selectedModel.value = models.value[0].id
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function startHealthPolling(interval = 30000) {
|
|
41
|
+
stopHealthPolling()
|
|
42
|
+
checkConnection()
|
|
43
|
+
healthPollTimer.value = setInterval(checkConnection, interval)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stopHealthPolling() {
|
|
47
|
+
if (healthPollTimer.value) {
|
|
48
|
+
clearInterval(healthPollTimer.value)
|
|
49
|
+
healthPollTimer.value = undefined
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
connected,
|
|
55
|
+
serverVersion,
|
|
56
|
+
models,
|
|
57
|
+
streamEnabled,
|
|
58
|
+
sessionPersistence,
|
|
59
|
+
maxTokens,
|
|
60
|
+
selectedModel,
|
|
61
|
+
checkConnection,
|
|
62
|
+
loadModels,
|
|
63
|
+
startHealthPolling,
|
|
64
|
+
stopHealthPolling,
|
|
65
|
+
}
|
|
66
|
+
})
|