2026-04-12 23:59:18 +08:00
|
|
|
import { startRun, streamRunEvents, type ChatMessage, type RunEvent } from '@/api/chat'
|
|
|
|
|
import { deleteSession as deleteSessionApi, fetchSession, fetchSessions, type HermesMessage, type SessionSummary } from '@/api/sessions'
|
2026-04-11 15:59:14 +08:00
|
|
|
import { defineStore } from 'pinia'
|
|
|
|
|
import { ref } from 'vue'
|
2026-04-12 23:23:50 +08:00
|
|
|
import { useAppStore } from './app'
|
2026-04-11 15:59:14 +08:00
|
|
|
|
2026-04-11 18:54:46 +08:00
|
|
|
export interface Attachment {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
type: string
|
|
|
|
|
size: number
|
|
|
|
|
url: string
|
|
|
|
|
file?: File
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 15:59:14 +08:00
|
|
|
export interface Message {
|
|
|
|
|
id: string
|
|
|
|
|
role: 'user' | 'assistant' | 'system' | 'tool'
|
|
|
|
|
content: string
|
|
|
|
|
timestamp: number
|
|
|
|
|
toolName?: string
|
|
|
|
|
toolPreview?: string
|
2026-04-12 23:59:18 +08:00
|
|
|
toolArgs?: string
|
|
|
|
|
toolResult?: string
|
2026-04-11 15:59:14 +08:00
|
|
|
toolStatus?: 'running' | 'done' | 'error'
|
|
|
|
|
isStreaming?: boolean
|
2026-04-11 18:54:46 +08:00
|
|
|
attachments?: Attachment[]
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
export interface Session {
|
2026-04-11 15:59:14 +08:00
|
|
|
id: string
|
|
|
|
|
title: string
|
2026-04-12 23:59:18 +08:00
|
|
|
source?: string
|
2026-04-11 15:59:14 +08:00
|
|
|
messages: Message[]
|
|
|
|
|
createdAt: number
|
|
|
|
|
updatedAt: number
|
2026-04-11 21:33:04 +08:00
|
|
|
model?: string
|
2026-04-12 23:23:50 +08:00
|
|
|
provider?: string
|
2026-04-11 21:33:04 +08:00
|
|
|
messageCount?: number
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function uid(): string {
|
|
|
|
|
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 18:54:46 +08:00
|
|
|
async function uploadFiles(attachments: Attachment[]): Promise<{ name: string; path: string }[]> {
|
|
|
|
|
if (attachments.length === 0) return []
|
|
|
|
|
const formData = new FormData()
|
|
|
|
|
for (const att of attachments) {
|
|
|
|
|
if (att.file) formData.append('file', att.file, att.name)
|
|
|
|
|
}
|
2026-04-11 21:33:04 +08:00
|
|
|
const res = await fetch('/upload', { method: 'POST', body: formData })
|
2026-04-11 18:54:46 +08:00
|
|
|
if (!res.ok) throw new Error(`Upload failed: ${res.status}`)
|
|
|
|
|
const data = await res.json() as { files: { name: string; path: string }[] }
|
|
|
|
|
return data.files
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
function mapHermesMessages(msgs: HermesMessage[]): Message[] {
|
2026-04-12 23:59:18 +08:00
|
|
|
// Build lookups from assistant messages with tool_calls
|
2026-04-11 21:33:04 +08:00
|
|
|
const toolNameMap = new Map<string, string>()
|
2026-04-12 23:59:18 +08:00
|
|
|
const toolArgsMap = new Map<string, string>()
|
2026-04-11 21:33:04 +08:00
|
|
|
for (const msg of msgs) {
|
|
|
|
|
if (msg.role === 'assistant' && msg.tool_calls) {
|
|
|
|
|
for (const tc of msg.tool_calls) {
|
2026-04-12 23:59:18 +08:00
|
|
|
if (tc.id) {
|
|
|
|
|
if (tc.function?.name) toolNameMap.set(tc.id, tc.function.name)
|
|
|
|
|
if (tc.function?.arguments) toolArgsMap.set(tc.id, tc.function.arguments)
|
2026-04-11 21:33:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
const result: Message[] = []
|
|
|
|
|
for (const msg of msgs) {
|
|
|
|
|
// Skip assistant messages that only contain tool_calls (no meaningful content)
|
|
|
|
|
if (msg.role === 'assistant' && msg.tool_calls?.length && !msg.content?.trim()) {
|
|
|
|
|
// Emit a tool.started message for each tool call
|
|
|
|
|
for (const tc of msg.tool_calls) {
|
|
|
|
|
result.push({
|
|
|
|
|
id: String(msg.id) + '_' + tc.id,
|
|
|
|
|
role: 'tool',
|
|
|
|
|
content: '',
|
|
|
|
|
timestamp: Math.round(msg.timestamp * 1000),
|
|
|
|
|
toolName: tc.function?.name || 'Tool',
|
2026-04-12 23:59:18 +08:00
|
|
|
toolArgs: tc.function?.arguments || undefined,
|
2026-04-11 21:33:04 +08:00
|
|
|
toolStatus: 'done',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tool result messages
|
|
|
|
|
if (msg.role === 'tool') {
|
2026-04-12 23:59:18 +08:00
|
|
|
const tcId = msg.tool_call_id || ''
|
|
|
|
|
const toolName = msg.tool_name || toolNameMap.get(tcId) || 'Tool'
|
|
|
|
|
const toolArgs = toolArgsMap.get(tcId) || undefined
|
2026-04-11 21:33:04 +08:00
|
|
|
// Extract a short preview from the content
|
|
|
|
|
let preview = ''
|
|
|
|
|
if (msg.content) {
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(msg.content)
|
|
|
|
|
preview = parsed.url || parsed.title || parsed.preview || parsed.summary || ''
|
|
|
|
|
} catch {
|
|
|
|
|
preview = msg.content.slice(0, 80)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-12 23:59:18 +08:00
|
|
|
// Find and remove the matching placeholder from tool_calls above
|
|
|
|
|
const placeholderIdx = result.findIndex(
|
|
|
|
|
m => m.role === 'tool' && m.toolName === toolName && !m.toolResult && m.id.includes('_' + tcId)
|
|
|
|
|
)
|
|
|
|
|
if (placeholderIdx !== -1) {
|
|
|
|
|
result.splice(placeholderIdx, 1)
|
|
|
|
|
}
|
2026-04-11 21:33:04 +08:00
|
|
|
result.push({
|
|
|
|
|
id: String(msg.id),
|
|
|
|
|
role: 'tool',
|
|
|
|
|
content: '',
|
|
|
|
|
timestamp: Math.round(msg.timestamp * 1000),
|
|
|
|
|
toolName,
|
2026-04-12 23:59:18 +08:00
|
|
|
toolArgs,
|
2026-04-13 00:52:34 +08:00
|
|
|
toolPreview: typeof preview === 'string' ? preview.slice(0, 100) || undefined : undefined,
|
2026-04-12 23:59:18 +08:00
|
|
|
toolResult: msg.content || undefined,
|
2026-04-11 21:33:04 +08:00
|
|
|
toolStatus: 'done',
|
|
|
|
|
})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normal user/assistant messages
|
|
|
|
|
result.push({
|
|
|
|
|
id: String(msg.id),
|
|
|
|
|
role: msg.role,
|
|
|
|
|
content: msg.content || '',
|
|
|
|
|
timestamp: Math.round(msg.timestamp * 1000),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return result
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
function mapHermesSession(s: SessionSummary): Session {
|
|
|
|
|
return {
|
|
|
|
|
id: s.id,
|
|
|
|
|
title: s.title || 'New Chat',
|
2026-04-12 23:59:18 +08:00
|
|
|
source: s.source || undefined,
|
2026-04-11 21:33:04 +08:00
|
|
|
messages: [],
|
|
|
|
|
createdAt: Math.round(s.started_at * 1000),
|
|
|
|
|
updatedAt: Math.round((s.ended_at || s.started_at) * 1000),
|
|
|
|
|
model: s.model,
|
2026-04-12 23:23:50 +08:00
|
|
|
provider: (s as any).billing_provider || '',
|
2026-04-11 21:33:04 +08:00
|
|
|
messageCount: s.message_count,
|
|
|
|
|
}
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const useChatStore = defineStore('chat', () => {
|
2026-04-11 21:33:04 +08:00
|
|
|
const sessions = ref<Session[]>([])
|
|
|
|
|
const activeSessionId = ref<string | null>(null)
|
2026-04-11 15:59:14 +08:00
|
|
|
const isStreaming = ref(false)
|
|
|
|
|
const abortController = ref<AbortController | null>(null)
|
2026-04-11 21:33:04 +08:00
|
|
|
const isLoadingSessions = ref(false)
|
|
|
|
|
const isLoadingMessages = ref(false)
|
2026-04-11 15:59:14 +08:00
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
const activeSession = ref<Session | null>(null)
|
|
|
|
|
const messages = ref<Message[]>([])
|
2026-04-11 15:59:14 +08:00
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
async function loadSessions() {
|
|
|
|
|
isLoadingSessions.value = true
|
|
|
|
|
try {
|
2026-04-12 23:59:18 +08:00
|
|
|
const list = await fetchSessions()
|
2026-04-11 21:33:04 +08:00
|
|
|
sessions.value = list.map(mapHermesSession)
|
|
|
|
|
// Auto-select the most recent session
|
|
|
|
|
if (!activeSessionId.value && sessions.value.length > 0) {
|
|
|
|
|
await switchSession(sessions.value[0].id)
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to load sessions:', err)
|
|
|
|
|
} finally {
|
|
|
|
|
isLoadingSessions.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-11 15:59:14 +08:00
|
|
|
|
2026-04-13 00:52:34 +08:00
|
|
|
|
2026-04-11 15:59:14 +08:00
|
|
|
function createSession(): Session {
|
|
|
|
|
const session: Session = {
|
|
|
|
|
id: uid(),
|
|
|
|
|
title: 'New Chat',
|
2026-04-13 00:52:34 +08:00
|
|
|
source: 'api_server',
|
2026-04-11 15:59:14 +08:00
|
|
|
messages: [],
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
updatedAt: Date.now(),
|
|
|
|
|
}
|
|
|
|
|
sessions.value.unshift(session)
|
|
|
|
|
return session
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
async function switchSession(sessionId: string) {
|
2026-04-11 15:59:14 +08:00
|
|
|
activeSessionId.value = sessionId
|
|
|
|
|
activeSession.value = sessions.value.find(s => s.id === sessionId) || null
|
2026-04-11 21:33:04 +08:00
|
|
|
|
|
|
|
|
// If session has no messages loaded, fetch from API
|
|
|
|
|
if (activeSession.value && activeSession.value.messages.length === 0) {
|
|
|
|
|
isLoadingMessages.value = true
|
|
|
|
|
try {
|
|
|
|
|
const detail = await fetchSession(sessionId)
|
|
|
|
|
if (detail && detail.messages) {
|
|
|
|
|
const mapped = mapHermesMessages(detail.messages)
|
|
|
|
|
activeSession.value.messages = mapped
|
2026-04-12 23:23:50 +08:00
|
|
|
// Update title: use Hermes title, or fallback to first user message
|
2026-04-11 21:33:04 +08:00
|
|
|
if (detail.title) {
|
|
|
|
|
activeSession.value.title = detail.title
|
2026-04-12 23:23:50 +08:00
|
|
|
} 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 ? '...' : '')
|
|
|
|
|
}
|
2026-04-11 21:33:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to load session messages:', err)
|
|
|
|
|
} finally {
|
|
|
|
|
isLoadingMessages.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 15:59:14 +08:00
|
|
|
messages.value = activeSession.value ? [...activeSession.value.messages] : []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function newChat() {
|
|
|
|
|
if (isStreaming.value) return
|
|
|
|
|
const session = createSession()
|
2026-04-12 23:23:50 +08:00
|
|
|
// Inherit current global model
|
|
|
|
|
const appStore = useAppStore()
|
|
|
|
|
session.model = appStore.selectedModel || undefined
|
2026-04-11 15:59:14 +08:00
|
|
|
switchSession(session.id)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:23:50 +08:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
async function deleteSession(sessionId: string) {
|
|
|
|
|
await deleteSessionApi(sessionId)
|
2026-04-11 15:59:14 +08:00
|
|
|
sessions.value = sessions.value.filter(s => s.id !== sessionId)
|
|
|
|
|
if (activeSessionId.value === sessionId) {
|
|
|
|
|
if (sessions.value.length > 0) {
|
2026-04-11 21:33:04 +08:00
|
|
|
await switchSession(sessions.value[0].id)
|
2026-04-11 15:59:14 +08:00
|
|
|
} else {
|
|
|
|
|
const session = createSession()
|
|
|
|
|
switchSession(session.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
function addMessage(msg: Message) {
|
|
|
|
|
messages.value.push(msg)
|
2026-04-11 18:54:46 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
function updateMessage(id: string, update: Partial<Message>) {
|
|
|
|
|
const idx = messages.value.findIndex(m => m.id === id)
|
|
|
|
|
if (idx !== -1) {
|
|
|
|
|
messages.value[idx] = { ...messages.value[idx], ...update }
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-11 15:59:14 +08:00
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
function updateSessionTitle() {
|
|
|
|
|
if (!activeSession.value) return
|
2026-04-11 15:59:14 +08:00
|
|
|
if (activeSession.value.title === 'New Chat') {
|
|
|
|
|
const firstUser = messages.value.find(m => m.role === 'user')
|
|
|
|
|
if (firstUser) {
|
2026-04-11 18:54:46 +08:00
|
|
|
const title = firstUser.attachments?.length
|
|
|
|
|
? firstUser.attachments.map(a => a.name).join(', ')
|
|
|
|
|
: firstUser.content
|
|
|
|
|
activeSession.value.title = title.slice(0, 40) + (title.length > 40 ? '...' : '')
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-11 21:33:04 +08:00
|
|
|
activeSession.value.updatedAt = Date.now()
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 18:54:46 +08:00
|
|
|
async function sendMessage(content: string, attachments?: Attachment[]) {
|
|
|
|
|
if ((!content.trim() && !(attachments && attachments.length > 0)) || isStreaming.value) return
|
2026-04-11 15:59:14 +08:00
|
|
|
|
|
|
|
|
if (!activeSession.value) {
|
|
|
|
|
const session = createSession()
|
|
|
|
|
switchSession(session.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const userMsg: Message = {
|
|
|
|
|
id: uid(),
|
|
|
|
|
role: 'user',
|
|
|
|
|
content: content.trim(),
|
|
|
|
|
timestamp: Date.now(),
|
2026-04-11 18:54:46 +08:00
|
|
|
attachments: attachments && attachments.length > 0 ? attachments : undefined,
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
addMessage(userMsg)
|
2026-04-11 21:33:04 +08:00
|
|
|
updateSessionTitle()
|
2026-04-11 15:59:14 +08:00
|
|
|
|
|
|
|
|
isStreaming.value = true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Build conversation history from past messages
|
|
|
|
|
const history: ChatMessage[] = messages.value
|
|
|
|
|
.filter(m => (m.role === 'user' || m.role === 'assistant') && m.content.trim())
|
|
|
|
|
.map(m => ({ role: m.role as 'user' | 'assistant' | 'system', content: m.content }))
|
|
|
|
|
|
2026-04-11 18:54:46 +08:00
|
|
|
// Upload attachments and build input with file paths
|
|
|
|
|
let inputText = content.trim()
|
|
|
|
|
if (attachments && attachments.length > 0) {
|
|
|
|
|
const uploaded = await uploadFiles(attachments)
|
|
|
|
|
const pathParts = uploaded.map(f => `[File: ${f.name}](${f.path})`)
|
|
|
|
|
inputText = inputText ? inputText + '\n\n' + pathParts.join('\n') : pathParts.join('\n')
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 23:23:50 +08:00
|
|
|
const appStore = useAppStore()
|
|
|
|
|
// Use session-level model if set, otherwise fall back to global
|
|
|
|
|
const sessionModel = activeSession.value?.model || appStore.selectedModel
|
2026-04-11 15:59:14 +08:00
|
|
|
const run = await startRun({
|
2026-04-11 18:54:46 +08:00
|
|
|
input: inputText,
|
2026-04-11 15:59:14 +08:00
|
|
|
conversation_history: history,
|
|
|
|
|
session_id: activeSession.value?.id,
|
2026-04-12 23:23:50 +08:00
|
|
|
model: sessionModel || undefined,
|
2026-04-11 15:59:14 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const runId = (run as any).run_id || (run as any).id
|
|
|
|
|
if (!runId) {
|
|
|
|
|
addMessage({
|
|
|
|
|
id: uid(),
|
|
|
|
|
role: 'system',
|
|
|
|
|
content: `Error: startRun returned no run ID. Response: ${JSON.stringify(run)}`,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
})
|
|
|
|
|
isStreaming.value = false
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Listen to SSE events
|
|
|
|
|
abortController.value = streamRunEvents(
|
|
|
|
|
runId,
|
|
|
|
|
// onEvent
|
|
|
|
|
(evt: RunEvent) => {
|
|
|
|
|
switch (evt.event) {
|
|
|
|
|
case 'run.started':
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
case 'message.delta': {
|
|
|
|
|
const last = messages.value[messages.value.length - 1]
|
|
|
|
|
if (last?.role === 'assistant' && last.isStreaming) {
|
|
|
|
|
last.content += evt.delta || ''
|
|
|
|
|
} else {
|
|
|
|
|
addMessage({
|
|
|
|
|
id: uid(),
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
content: evt.delta || '',
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
isStreaming: true,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'tool.started': {
|
|
|
|
|
const last = messages.value[messages.value.length - 1]
|
|
|
|
|
if (last?.isStreaming) {
|
|
|
|
|
updateMessage(last.id, { isStreaming: false })
|
|
|
|
|
}
|
|
|
|
|
addMessage({
|
|
|
|
|
id: uid(),
|
|
|
|
|
role: 'tool',
|
|
|
|
|
content: '',
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
toolName: evt.tool || evt.name,
|
|
|
|
|
toolPreview: evt.preview,
|
|
|
|
|
toolStatus: 'running',
|
|
|
|
|
})
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'tool.completed': {
|
|
|
|
|
const toolMsgs = messages.value.filter(
|
|
|
|
|
m => m.role === 'tool' && m.toolStatus === 'running',
|
|
|
|
|
)
|
|
|
|
|
if (toolMsgs.length > 0) {
|
|
|
|
|
const last = toolMsgs[toolMsgs.length - 1]
|
|
|
|
|
updateMessage(last.id, { toolStatus: 'done' })
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'run.completed':
|
|
|
|
|
const lastMsg = messages.value[messages.value.length - 1]
|
|
|
|
|
if (lastMsg?.isStreaming) {
|
|
|
|
|
updateMessage(lastMsg.id, { isStreaming: false })
|
|
|
|
|
}
|
|
|
|
|
isStreaming.value = false
|
|
|
|
|
abortController.value = null
|
2026-04-11 21:33:04 +08:00
|
|
|
updateSessionTitle()
|
2026-04-11 15:59:14 +08:00
|
|
|
break
|
|
|
|
|
|
|
|
|
|
case 'run.failed':
|
|
|
|
|
const lastErr = messages.value[messages.value.length - 1]
|
|
|
|
|
if (lastErr?.isStreaming) {
|
|
|
|
|
updateMessage(lastErr.id, {
|
|
|
|
|
isStreaming: false,
|
|
|
|
|
content: evt.error ? `Error: ${evt.error}` : 'Run failed',
|
|
|
|
|
role: 'system',
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
addMessage({
|
|
|
|
|
id: uid(),
|
|
|
|
|
role: 'system',
|
|
|
|
|
content: evt.error ? `Error: ${evt.error}` : 'Run failed',
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
messages.value.forEach((m, i) => {
|
|
|
|
|
if (m.role === 'tool' && m.toolStatus === 'running') {
|
|
|
|
|
messages.value[i] = { ...m, toolStatus: 'error' }
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
isStreaming.value = false
|
|
|
|
|
abortController.value = null
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
// onDone
|
|
|
|
|
() => {
|
|
|
|
|
const last = messages.value[messages.value.length - 1]
|
|
|
|
|
if (last?.isStreaming) {
|
|
|
|
|
updateMessage(last.id, { isStreaming: false })
|
|
|
|
|
}
|
|
|
|
|
isStreaming.value = false
|
|
|
|
|
abortController.value = null
|
2026-04-11 21:33:04 +08:00
|
|
|
updateSessionTitle()
|
2026-04-11 15:59:14 +08:00
|
|
|
},
|
|
|
|
|
// onError
|
|
|
|
|
(err) => {
|
|
|
|
|
const last = messages.value[messages.value.length - 1]
|
|
|
|
|
if (last?.isStreaming) {
|
|
|
|
|
updateMessage(last.id, {
|
|
|
|
|
isStreaming: false,
|
|
|
|
|
content: `Error: ${err.message}`,
|
|
|
|
|
role: 'system',
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
addMessage({
|
|
|
|
|
id: uid(),
|
|
|
|
|
role: 'system',
|
|
|
|
|
content: `Error: ${err.message}`,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
isStreaming.value = false
|
|
|
|
|
abortController.value = null
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
addMessage({
|
|
|
|
|
id: uid(),
|
|
|
|
|
role: 'system',
|
|
|
|
|
content: `Error: ${err.message}`,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
})
|
|
|
|
|
isStreaming.value = false
|
|
|
|
|
abortController.value = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stopStreaming() {
|
|
|
|
|
abortController.value?.abort()
|
|
|
|
|
isStreaming.value = false
|
|
|
|
|
const lastMsg = messages.value[messages.value.length - 1]
|
|
|
|
|
if (lastMsg?.isStreaming) {
|
|
|
|
|
updateMessage(lastMsg.id, { isStreaming: false })
|
|
|
|
|
}
|
|
|
|
|
abortController.value = null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 21:33:04 +08:00
|
|
|
// Load sessions on init
|
|
|
|
|
loadSessions()
|
2026-04-11 15:59:14 +08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
sessions,
|
|
|
|
|
activeSessionId,
|
|
|
|
|
activeSession,
|
|
|
|
|
messages,
|
|
|
|
|
isStreaming,
|
2026-04-11 21:33:04 +08:00
|
|
|
isLoadingSessions,
|
|
|
|
|
isLoadingMessages,
|
2026-04-11 15:59:14 +08:00
|
|
|
newChat,
|
|
|
|
|
switchSession,
|
2026-04-12 23:23:50 +08:00
|
|
|
switchSessionModel,
|
2026-04-11 15:59:14 +08:00
|
|
|
deleteSession,
|
|
|
|
|
sendMessage,
|
|
|
|
|
stopStreaming,
|
2026-04-11 21:33:04 +08:00
|
|
|
loadSessions,
|
2026-04-11 15:59:14 +08:00
|
|
|
}
|
|
|
|
|
})
|