feat: add model selector, skills/memory pages, and config management
- Add model selector in sidebar that discovers models from auth.json credential pool - Add per-session model display (badge in chat header and session list) - Add skills browser page and memory editor page - Add BFF routes for skills, memory, and config model management - Model switching updates config.yaml provider field to bypass env auto-detection - Refactor Settings page, simplify ChatInput with file upload - Add attachment upload support via BFF /upload endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+21
-11
@@ -1,19 +1,18 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { checkHealth, fetchModels } from '@/api/system'
|
||||
import type { Model } from '@/api/system'
|
||||
import { checkHealth, fetchAvailableModels, updateDefaultModel, type AvailableModelGroup } from '@/api/system'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const connected = ref(false)
|
||||
const serverVersion = ref('')
|
||||
const models = ref<Model[]>([])
|
||||
const modelGroups = ref<AvailableModelGroup[]>([])
|
||||
const selectedModel = ref('')
|
||||
const healthPollTimer = ref<ReturnType<typeof setInterval>>()
|
||||
|
||||
// Settings
|
||||
const streamEnabled = ref(true)
|
||||
const sessionPersistence = ref(true)
|
||||
const maxTokens = ref(4096)
|
||||
const selectedModel = ref('hermes-agent')
|
||||
|
||||
async function checkConnection() {
|
||||
try {
|
||||
@@ -27,16 +26,26 @@ export const useAppStore = defineStore('app', () => {
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const res = await fetchModels()
|
||||
models.value = res.data || []
|
||||
if (models.value.length > 0 && !models.value.find(m => m.id === selectedModel.value)) {
|
||||
selectedModel.value = models.value[0].id
|
||||
}
|
||||
const res = await fetchAvailableModels()
|
||||
modelGroups.value = res.groups
|
||||
selectedModel.value = res.default
|
||||
} 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
|
||||
} catch (err: any) {
|
||||
console.error('Failed to switch model:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function startHealthPolling(interval = 30000) {
|
||||
stopHealthPolling()
|
||||
checkConnection()
|
||||
@@ -53,13 +62,14 @@ export const useAppStore = defineStore('app', () => {
|
||||
return {
|
||||
connected,
|
||||
serverVersion,
|
||||
models,
|
||||
modelGroups,
|
||||
selectedModel,
|
||||
streamEnabled,
|
||||
sessionPersistence,
|
||||
maxTokens,
|
||||
selectedModel,
|
||||
checkConnection,
|
||||
loadModels,
|
||||
switchModel,
|
||||
startHealthPolling,
|
||||
stopHealthPolling,
|
||||
}
|
||||
|
||||
+45
-1
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { startRun, streamRunEvents, type ChatMessage, type RunEvent } from '@/api/chat'
|
||||
import { fetchSessions, fetchSession, deleteSession as deleteSessionApi, type SessionSummary, type HermesMessage } from '@/api/sessions'
|
||||
import { useAppStore } from './app'
|
||||
|
||||
export interface Attachment {
|
||||
id: string
|
||||
@@ -31,6 +32,7 @@ export interface Session {
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
model?: string
|
||||
provider?: string
|
||||
messageCount?: number
|
||||
}
|
||||
|
||||
@@ -125,6 +127,7 @@ function mapHermesSession(s: SessionSummary): Session {
|
||||
createdAt: Math.round(s.started_at * 1000),
|
||||
updatedAt: Math.round((s.ended_at || s.started_at) * 1000),
|
||||
model: s.model,
|
||||
provider: (s as any).billing_provider || '',
|
||||
messageCount: s.message_count,
|
||||
}
|
||||
}
|
||||
@@ -145,6 +148,22 @@ export const useChatStore = defineStore('chat', () => {
|
||||
try {
|
||||
const list = await fetchSessions('api_server')
|
||||
sessions.value = list.map(mapHermesSession)
|
||||
// Backfill titles from first user message for sessions with null title
|
||||
const nullTitleSessions = sessions.value.filter(s => s.title === 'New Chat')
|
||||
if (nullTitleSessions.length > 0) {
|
||||
await Promise.allSettled(
|
||||
nullTitleSessions.map(async (s) => {
|
||||
const detail = await fetchSession(s.id)
|
||||
if (detail?.messages) {
|
||||
const firstUser = detail.messages.find(m => m.role === 'user')
|
||||
if (firstUser) {
|
||||
const t = firstUser.content.slice(0, 40)
|
||||
s.title = t + (firstUser.content.length > 40 ? '...' : '')
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
// Auto-select the most recent session
|
||||
if (!activeSessionId.value && sessions.value.length > 0) {
|
||||
await switchSession(sessions.value[0].id)
|
||||
@@ -180,9 +199,15 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (detail && detail.messages) {
|
||||
const mapped = mapHermesMessages(detail.messages)
|
||||
activeSession.value.messages = mapped
|
||||
// Update title from Hermes data
|
||||
// Update title: use Hermes title, or fallback to first user message
|
||||
if (detail.title) {
|
||||
activeSession.value.title = detail.title
|
||||
} else {
|
||||
const firstUser = mapped.find(m => m.role === 'user')
|
||||
if (firstUser) {
|
||||
const t = firstUser.content.slice(0, 40)
|
||||
activeSession.value.title = t + (firstUser.content.length > 40 ? '...' : '')
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -198,9 +223,23 @@ export const useChatStore = defineStore('chat', () => {
|
||||
function newChat() {
|
||||
if (isStreaming.value) return
|
||||
const session = createSession()
|
||||
// Inherit current global model
|
||||
const appStore = useAppStore()
|
||||
session.model = appStore.selectedModel || undefined
|
||||
switchSession(session.id)
|
||||
}
|
||||
|
||||
async function switchSessionModel(modelId: string, provider?: string) {
|
||||
if (!activeSession.value) return
|
||||
activeSession.value.model = modelId
|
||||
activeSession.value.provider = provider || ''
|
||||
// If provider changed, update global config too (Hermes requires it)
|
||||
if (provider) {
|
||||
const { useAppStore } = await import('./app')
|
||||
await useAppStore().switchModel(modelId, provider)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession(sessionId: string) {
|
||||
await deleteSessionApi(sessionId)
|
||||
sessions.value = sessions.value.filter(s => s.id !== sessionId)
|
||||
@@ -273,10 +312,14 @@ export const useChatStore = defineStore('chat', () => {
|
||||
inputText = inputText ? inputText + '\n\n' + pathParts.join('\n') : pathParts.join('\n')
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
// Use session-level model if set, otherwise fall back to global
|
||||
const sessionModel = activeSession.value?.model || appStore.selectedModel
|
||||
const run = await startRun({
|
||||
input: inputText,
|
||||
conversation_history: history,
|
||||
session_id: activeSession.value?.id,
|
||||
model: sessionModel || undefined,
|
||||
})
|
||||
|
||||
const runId = (run as any).run_id || (run as any).id
|
||||
@@ -446,6 +489,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
isLoadingMessages,
|
||||
newChat,
|
||||
switchSession,
|
||||
switchSessionModel,
|
||||
deleteSession,
|
||||
sendMessage,
|
||||
stopStreaming,
|
||||
|
||||
Reference in New Issue
Block a user