2026-04-11 15:59:14 +08:00
|
|
|
<script setup lang="ts">
|
2026-04-12 23:23:50 +08:00
|
|
|
import { ref } from 'vue'
|
2026-04-11 15:59:14 +08:00
|
|
|
import {
|
2026-04-12 23:23:50 +08:00
|
|
|
NButton, NSwitch, NSlider, NDataTable, useMessage,
|
2026-04-11 15:59:14 +08:00
|
|
|
} from 'naive-ui'
|
|
|
|
|
import { useAppStore } from '@/stores/app'
|
|
|
|
|
|
|
|
|
|
const appStore = useAppStore()
|
|
|
|
|
const message = useMessage()
|
|
|
|
|
|
|
|
|
|
const testingConnection = ref(false)
|
|
|
|
|
|
|
|
|
|
async function handleTestConnection() {
|
|
|
|
|
testingConnection.value = true
|
|
|
|
|
try {
|
|
|
|
|
await appStore.checkConnection()
|
|
|
|
|
if (appStore.connected) {
|
|
|
|
|
message.success('Connected successfully')
|
|
|
|
|
} else {
|
|
|
|
|
message.error('Connection failed')
|
|
|
|
|
}
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
message.error(e.message)
|
|
|
|
|
} finally {
|
|
|
|
|
testingConnection.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:23:50 +08:00
|
|
|
const providerColumns = [
|
|
|
|
|
{ title: 'Provider', key: 'provider' },
|
|
|
|
|
{ title: 'Models', key: 'models' },
|
|
|
|
|
{ title: 'Base URL', key: 'base_url' },
|
2026-04-11 15:59:14 +08:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const endpoints = [
|
|
|
|
|
{ method: 'GET', endpoint: '/health', description: 'Health Check' },
|
|
|
|
|
{ method: 'POST', endpoint: '/v1/runs', description: 'Start Async Run' },
|
|
|
|
|
{ method: 'GET', endpoint: '/v1/runs/{id}/events', description: 'SSE Event Stream' },
|
|
|
|
|
{ method: 'GET', endpoint: '/api/jobs', description: 'List Jobs' },
|
|
|
|
|
{ method: 'POST', endpoint: '/api/jobs', description: 'Create Job' },
|
|
|
|
|
{ method: 'POST', endpoint: '/api/jobs/{id}/run', description: 'Trigger Job Now' },
|
|
|
|
|
]
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="settings-view">
|
|
|
|
|
<header class="settings-header">
|
|
|
|
|
<h2 class="header-title">Settings</h2>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="settings-content">
|
|
|
|
|
<!-- API Configuration -->
|
|
|
|
|
<section class="settings-section">
|
|
|
|
|
<h3 class="section-title">API Configuration</h3>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<div class="connection-status">
|
|
|
|
|
<span class="status-dot" :class="{ on: appStore.connected, off: !appStore.connected }"></span>
|
|
|
|
|
<span>{{ appStore.connected ? 'Connected' : 'Disconnected' }}</span>
|
|
|
|
|
<span v-if="appStore.serverVersion" class="version">v{{ appStore.serverVersion }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<NButton type="primary" size="small" :loading="testingConnection" @click="handleTestConnection">
|
|
|
|
|
Test Connection
|
|
|
|
|
</NButton>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
2026-04-12 23:23:50 +08:00
|
|
|
<!-- Model Management -->
|
2026-04-11 15:59:14 +08:00
|
|
|
<section class="settings-section">
|
2026-04-12 23:23:50 +08:00
|
|
|
<h3 class="section-title">Model Management</h3>
|
2026-04-11 15:59:14 +08:00
|
|
|
<div class="form-group">
|
2026-04-12 23:23:50 +08:00
|
|
|
<label class="form-label">Current Model</label>
|
|
|
|
|
<div class="current-model">{{ appStore.selectedModel || 'Not set' }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="appStore.modelGroups.length > 0" class="form-group">
|
|
|
|
|
<label class="form-label">Available Models</label>
|
|
|
|
|
<p class="form-hint">Models are discovered from ~/.hermes/auth.json credential pool. Use the sidebar selector to switch.</p>
|
|
|
|
|
<NDataTable
|
|
|
|
|
:columns="providerColumns"
|
|
|
|
|
:data="appStore.modelGroups.map(g => ({
|
|
|
|
|
provider: g.label,
|
|
|
|
|
models: g.models.join(', '),
|
|
|
|
|
base_url: g.base_url,
|
|
|
|
|
}))"
|
|
|
|
|
:bordered="false"
|
|
|
|
|
size="small"
|
|
|
|
|
:row-props="() => ({ style: 'cursor: default;' })"
|
2026-04-11 15:59:14 +08:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-04-12 23:23:50 +08:00
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<!-- Chat Settings -->
|
|
|
|
|
<section class="settings-section">
|
|
|
|
|
<h3 class="section-title">Chat Settings</h3>
|
2026-04-11 15:59:14 +08:00
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Stream Responses</label>
|
|
|
|
|
<NSwitch v-model:value="appStore.streamEnabled" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Session Persistence</label>
|
|
|
|
|
<NSwitch v-model:value="appStore.sessionPersistence" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Max Tokens: {{ appStore.maxTokens }}</label>
|
|
|
|
|
<NSlider v-model:value="appStore.maxTokens" :min="256" :max="32768" :step="256" />
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<!-- About -->
|
|
|
|
|
<section class="settings-section">
|
|
|
|
|
<h3 class="section-title">About</h3>
|
|
|
|
|
<p class="about-text">
|
|
|
|
|
Hermes Agent Web UI
|
2026-04-12 23:23:50 +08:00
|
|
|
<br />Version 0.1.3
|
2026-04-11 15:59:14 +08:00
|
|
|
</p>
|
|
|
|
|
<div class="endpoint-table">
|
|
|
|
|
<NDataTable
|
2026-04-12 23:23:50 +08:00
|
|
|
:columns="[{ title: 'Method', key: 'method', width: 80 }, { title: 'Endpoint', key: 'endpoint' }, { title: 'Description', key: 'description' }]"
|
2026-04-11 15:59:14 +08:00
|
|
|
:data="endpoints"
|
|
|
|
|
:bordered="false"
|
|
|
|
|
size="small"
|
|
|
|
|
:row-props="() => ({ style: 'cursor: default;' })"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
@use '@/styles/variables' as *;
|
|
|
|
|
|
|
|
|
|
.settings-view {
|
|
|
|
|
height: 100vh;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
border-bottom: 1px solid $border-color;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: $text-primary;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
max-width: 640px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.settings-section {
|
|
|
|
|
margin-bottom: 28px;
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
border-bottom: 1px solid $border-light;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.form-group {
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
|
|
|
|
.form-label {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:23:50 +08:00
|
|
|
.form-hint {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
margin-bottom: 10px;
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.connection-status {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
|
|
|
|
.status-dot {
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
|
|
|
|
&.on {
|
|
|
|
|
background-color: $success;
|
|
|
|
|
box-shadow: 0 0 6px rgba($success, 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.off {
|
|
|
|
|
background-color: $error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.version {
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:23:50 +08:00
|
|
|
.current-model {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: $text-primary;
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
background: $bg-secondary;
|
|
|
|
|
border-radius: $radius-sm;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-text {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
font-style: italic;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 15:59:14 +08:00
|
|
|
.about-text {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: $text-secondary;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.endpoint-table {
|
|
|
|
|
:deep(.n-data-table) {
|
|
|
|
|
--n-td-color: transparent;
|
|
|
|
|
--n-th-color: rgba($accent-primary, 0.04);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|