SQLite + sqlite-vec replaces JSON memory files with semantic search, follow-up injection, privacy levels, and lifecycle management. Six prompt styles (quick/standard/creative/roleplayer/game-master/storyteller) with per-style Claude model tiering (Haiku/Sonnet/Opus), temperature control, and section stripping. Characters can set default style and per-style overrides. Dream character import and GAZE character linking in the dashboard editor with auto-populated fields, cover image resolution, and preset assignment. Bridge: session isolation (conversation_id / 12h satellite buckets), model routing refactor, PUT/DELETE support, memory REST endpoints. Dashboard: mobile-responsive sidebar, retry button, style picker in chat, follow-up banner, memory lifecycle/privacy UI, cloud model options in editor. Wyoming TTS: upgraded to v1.8.0 for HA 1.7.2 compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
93 lines
2.9 KiB
JavaScript
93 lines
2.9 KiB
JavaScript
const MAX_RETRIES = 3
|
|
const RETRY_DELAY_MS = 2000
|
|
|
|
async function fetchWithRetry(url, options, retries = MAX_RETRIES) {
|
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
try {
|
|
const res = await fetch(url, options)
|
|
if (res.status === 502 && attempt < retries) {
|
|
// Bridge unreachable — wait and retry
|
|
await new Promise(r => setTimeout(r, RETRY_DELAY_MS * attempt))
|
|
continue
|
|
}
|
|
return res
|
|
} catch (err) {
|
|
if (attempt >= retries) throw err
|
|
await new Promise(r => setTimeout(r, RETRY_DELAY_MS * attempt))
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function sendMessage(text, characterId = null, promptStyle = null, conversationId = null) {
|
|
const payload = { message: text, agent: 'main' }
|
|
if (characterId) payload.character_id = characterId
|
|
if (promptStyle) payload.prompt_style = promptStyle
|
|
if (conversationId) payload.conversation_id = conversationId
|
|
const res = await fetchWithRetry('/api/agent/message', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ error: 'Request failed' }))
|
|
throw new Error(err.error || `HTTP ${res.status}`)
|
|
}
|
|
const data = await res.json()
|
|
return { response: data.response, model: data.model || null, prompt_style: data.prompt_style || null }
|
|
}
|
|
|
|
export async function getPromptStyles() {
|
|
const res = await fetch('/api/prompt-styles')
|
|
if (!res.ok) return []
|
|
return await res.json()
|
|
}
|
|
|
|
export async function getActiveStyle() {
|
|
const res = await fetch('/api/prompt-style')
|
|
if (!res.ok) return { style: 'standard' }
|
|
return await res.json()
|
|
}
|
|
|
|
export async function setActiveStyle(style) {
|
|
const res = await fetch('/api/prompt-style', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ style }),
|
|
})
|
|
if (!res.ok) throw new Error('Failed to set prompt style')
|
|
return await res.json()
|
|
}
|
|
|
|
export async function synthesize(text, voice, engine = 'kokoro', model = null) {
|
|
const payload = { text, voice, engine }
|
|
if (model) payload.model = model
|
|
const res = await fetch('/api/tts', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
if (!res.ok) throw new Error('TTS failed')
|
|
return await res.arrayBuffer()
|
|
}
|
|
|
|
export async function transcribe(wavBlob) {
|
|
const res = await fetch('/api/stt', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'audio/wav' },
|
|
body: wavBlob,
|
|
})
|
|
if (!res.ok) throw new Error('STT failed')
|
|
const data = await res.json()
|
|
return data.text
|
|
}
|
|
|
|
export async function healthCheck() {
|
|
try {
|
|
const res = await fetch('/api/health?url=' + encodeURIComponent('http://localhost:8081/'), { signal: AbortSignal.timeout(5000) })
|
|
const data = await res.json()
|
|
return data.status === 'online'
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|