Files
Hermes-ui/packages/client/src/stores/hermes/app.ts
T

180 lines
5.7 KiB
TypeScript
Raw Normal View History

2026-04-11 15:59:14 +08:00
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { checkHealth, fetchAvailableModels, updateDefaultModel, updateModelVisibility, triggerUpdate, type AvailableModelGroup, type AvailableModelsResponse, type ModelVisibility, type ModelVisibilityRule } from '@/api/hermes/system'
const WEB_UI_VERSION = __APP_VERSION__
2026-04-11 15:59:14 +08:00
const SIDEBAR_COLLAPSED_KEY = 'hermes_sidebar_collapsed'
2026-04-11 15:59:14 +08:00
export const useAppStore = defineStore('app', () => {
2026-04-15 09:12:54 +08:00
const sidebarOpen = ref(false)
// Desktop-only collapsed state (icon-rail mode). Persisted to localStorage.
const sidebarCollapsed = ref(localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1')
2026-04-15 09:12:54 +08:00
2026-04-11 15:59:14 +08:00
const connected = ref(false)
const serverVersion = ref(WEB_UI_VERSION)
const latestVersion = ref('')
const updateAvailable = ref(false)
const updating = ref(false)
const modelGroups = ref<AvailableModelGroup[]>([])
const selectedModel = ref('')
const selectedProvider = ref('')
const customModels = ref<Record<string, string[]>>({})
const modelVisibility = ref<ModelVisibility>({})
2026-04-11 15:59:14 +08:00
const healthPollTimer = ref<ReturnType<typeof setInterval>>()
const nodeVersion = ref('')
2026-04-11 15:59:14 +08:00
// Settings
const streamEnabled = ref(true)
const sessionPersistence = ref(true)
const maxTokens = ref(4096)
async function doUpdate(): Promise<boolean> {
updating.value = true
try {
const res = await triggerUpdate()
if (res.success) {
updateAvailable.value = false
await checkConnection()
}
return res.success
} catch (err) {
console.error('Failed to update Hermes Web UI:', err)
return false
} finally {
updating.value = false
}
}
2026-04-11 15:59:14 +08:00
async function checkConnection() {
try {
const res = await checkHealth()
connected.value = res.status === 'ok'
if (res.webui_version) serverVersion.value = res.webui_version
if (res.webui_latest) latestVersion.value = res.webui_latest
updateAvailable.value = !!res.webui_update_available
if (res.node_version) nodeVersion.value = res.node_version
2026-04-11 15:59:14 +08:00
} catch {
connected.value = false
}
}
function applyAvailableModelsResponse(res: AvailableModelsResponse) {
modelGroups.value = res.groups
modelVisibility.value = res.model_visibility || {}
const defaultGroup = res.groups.find(g => g.provider === (res.default_provider || '') && g.models.includes(res.default))
const inferredGroup = res.groups.find(g => g.models.includes(res.default))
const fallbackGroup = res.groups.find(g => g.models.length > 0)
const selectedGroup = defaultGroup || inferredGroup || fallbackGroup
selectedModel.value = selectedGroup ? (defaultGroup || inferredGroup ? res.default : selectedGroup.models[0]) : ''
selectedProvider.value = selectedGroup?.provider || ''
}
2026-04-11 15:59:14 +08:00
async function loadModels() {
try {
const res = await fetchAvailableModels()
applyAvailableModelsResponse(res)
2026-04-11 15:59:14 +08:00
} catch {
// ignore
}
}
async function switchModel(modelId: string, providerOverride?: string) {
try {
// Find the group containing this model to get provider info
const group = modelGroups.value.find(g => g.models.includes(modelId))
const provider = providerOverride || group?.provider || ''
await updateDefaultModel({ default: modelId, provider })
selectedModel.value = modelId
selectedProvider.value = provider || ''
// Track as custom if not already in the server-fetched list
if (provider && !modelGroups.value.find(g => g.provider === provider)?.models.includes(modelId)) {
if (!customModels.value[provider]) customModels.value[provider] = []
if (!customModels.value[provider].includes(modelId)) {
customModels.value[provider] = [...customModels.value[provider], modelId]
}
}
} catch (err: any) {
console.error('Failed to switch model:', err)
}
}
function getProviderVisibility(provider: string): ModelVisibilityRule {
return modelVisibility.value[provider] || { mode: 'all', models: [] }
}
function isModelVisible(provider: string, model: string): boolean {
const rule = getProviderVisibility(provider)
return rule.mode !== 'include' || rule.models.includes(model)
}
async function setModelVisibility(provider: string, rule: ModelVisibilityRule) {
const res = await updateModelVisibility({ provider, mode: rule.mode, models: rule.models })
modelVisibility.value = res.model_visibility || {}
await loadModels()
}
2026-04-11 15:59:14 +08:00
function startHealthPolling(interval = 30000) {
stopHealthPolling()
checkConnection()
healthPollTimer.value = setInterval(checkConnection, interval)
}
function stopHealthPolling() {
if (healthPollTimer.value) {
clearInterval(healthPollTimer.value)
healthPollTimer.value = undefined
}
}
2026-04-15 09:12:54 +08:00
function toggleSidebar() {
sidebarOpen.value = !sidebarOpen.value
}
function closeSidebar() {
sidebarOpen.value = false
}
function toggleSidebarCollapsed() {
sidebarCollapsed.value = !sidebarCollapsed.value
try {
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, sidebarCollapsed.value ? '1' : '0')
} catch {
// ignore quota errors — fallback to in-memory only
}
}
2026-04-11 15:59:14 +08:00
return {
2026-04-15 09:12:54 +08:00
sidebarOpen,
sidebarCollapsed,
2026-04-15 09:12:54 +08:00
toggleSidebar,
closeSidebar,
toggleSidebarCollapsed,
2026-04-11 15:59:14 +08:00
connected,
serverVersion,
latestVersion,
nodeVersion,
updateAvailable,
updating,
doUpdate,
modelGroups,
customModels,
modelVisibility,
selectedModel,
selectedProvider,
2026-04-11 15:59:14 +08:00
streamEnabled,
sessionPersistence,
maxTokens,
checkConnection,
loadModels,
applyAvailableModelsResponse,
switchModel,
getProviderVisibility,
isModelVisible,
setModelVisibility,
2026-04-11 15:59:14 +08:00
startHealthPolling,
stopHealthPolling,
}
})