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:
ekko
2026-04-12 23:23:50 +08:00
co-authored by Claude Opus 4.6
parent ee9f56dfbd
commit 5887462f7d
21 changed files with 1941 additions and 106 deletions
+1
View File
@@ -10,6 +10,7 @@ export interface StartRunRequest {
instructions?: string
conversation_history?: ChatMessage[]
session_id?: string
model?: string
}
export interface StartRunResponse {
+55
View File
@@ -0,0 +1,55 @@
import { request } from './client'
export interface SkillInfo {
name: string
description: string
}
export interface SkillCategory {
name: string
description: string
skills: SkillInfo[]
}
export interface SkillListResponse {
categories: SkillCategory[]
}
export interface SkillFileEntry {
path: string
name: string
isDir: boolean
}
export interface MemoryData {
memory: string
user: string
memory_mtime: number | null
user_mtime: number | null
}
export async function fetchSkills(): Promise<SkillCategory[]> {
const res = await request<SkillListResponse>('/api/skills')
return res.categories
}
export async function fetchSkillContent(skillPath: string): Promise<string> {
const res = await request<{ content: string }>(`/api/skills/${skillPath}`)
return res.content
}
export async function fetchSkillFiles(category: string, skill: string): Promise<SkillFileEntry[]> {
const res = await request<{ files: SkillFileEntry[] }>(`/api/skills/${category}/${skill}/files`)
return res.files
}
export async function fetchMemory(): Promise<MemoryData> {
return request<MemoryData>('/api/memory')
}
export async function saveMemory(section: 'memory' | 'user', content: string): Promise<void> {
await request('/api/memory', {
method: 'POST',
body: JSON.stringify({ section, content }),
})
}
+68
View File
@@ -16,6 +16,41 @@ export interface ModelsResponse {
data: Model[]
}
// Config-based model types
export interface ModelInfo {
id: string
label: string
}
export interface ModelGroup {
provider: string
models: ModelInfo[]
}
export interface ConfigModelsResponse {
default: string
groups: ModelGroup[]
}
export interface AvailableModelGroup {
provider: string // credential pool key (e.g. "zai", "custom:subrouter.ai")
label: string // display name (e.g. "zai", "subrouter.ai")
base_url: string
models: string[]
}
export interface AvailableModelsResponse {
default: string
groups: AvailableModelGroup[]
}
export interface CustomProvider {
name: string
base_url: string
api_key: string
model: string
}
export async function checkHealth(): Promise<HealthResponse> {
return request<HealthResponse>('/health')
}
@@ -23,3 +58,36 @@ export async function checkHealth(): Promise<HealthResponse> {
export async function fetchModels(): Promise<ModelsResponse> {
return request<ModelsResponse>('/v1/models')
}
export async function fetchConfigModels(): Promise<ConfigModelsResponse> {
return request<ConfigModelsResponse>('/api/config/models')
}
export async function fetchAvailableModels(): Promise<AvailableModelsResponse> {
return request<AvailableModelsResponse>('/api/available-models')
}
export async function updateDefaultModel(data: {
default: string
provider?: string
base_url?: string
api_key?: string
}): Promise<void> {
await request('/api/config/model', {
method: 'PUT',
body: JSON.stringify(data),
})
}
export async function addCustomProvider(data: CustomProvider): Promise<void> {
await request('/api/config/providers', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function removeCustomProvider(name: string): Promise<void> {
await request(`/api/config/providers/${encodeURIComponent(name)}`, {
method: 'DELETE',
})
}