mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
763 lines
22 KiB
TypeScript
763 lines
22 KiB
TypeScript
import { defineStore, storeToRefs } from 'pinia'
|
|
import { computed, reactive, ref, watch } from 'vue'
|
|
import { useRetryingStream } from '@/composables/useRetryingStream'
|
|
import { useUserStore } from '@/store/user'
|
|
import { useChatSelectionStore } from '@/store/chat-selection'
|
|
import { shouldRefreshFromMessageCreated, upsertById } from './chat-list.utils'
|
|
import {
|
|
createSession,
|
|
deleteSession as requestDeleteSession,
|
|
fetchSessions,
|
|
type Bot,
|
|
type SessionSummary,
|
|
type MessageStreamEvent,
|
|
type ChatAttachment,
|
|
type ChatWebSocket,
|
|
type UIAttachment,
|
|
type UIAttachmentsMessage,
|
|
type UIMessage,
|
|
type UIReasoningMessage,
|
|
type UITextMessage,
|
|
type UIToolMessage,
|
|
type UITurn,
|
|
type UIUserTurn,
|
|
type UIStreamEvent,
|
|
fetchBots,
|
|
fetchMessagesUI,
|
|
sendLocalChannelMessage,
|
|
streamMessageEvents,
|
|
connectWebSocket,
|
|
} from '@/composables/api/useChat'
|
|
|
|
export type TextBlock = UITextMessage
|
|
export type ThinkingBlock = UIReasoningMessage
|
|
export type AttachmentItem = UIAttachment
|
|
export type AttachmentBlock = UIAttachmentsMessage
|
|
|
|
export interface ToolCallBlock extends UIToolMessage {
|
|
toolCallId: string
|
|
toolName: string
|
|
result: unknown | null
|
|
done: boolean
|
|
}
|
|
|
|
export type ContentBlock = TextBlock | ThinkingBlock | ToolCallBlock | AttachmentBlock
|
|
|
|
export interface ChatUserTurn {
|
|
id: string
|
|
role: 'user'
|
|
text: string
|
|
attachments: AttachmentItem[]
|
|
timestamp: string
|
|
platform?: string
|
|
senderDisplayName?: string
|
|
senderAvatarUrl?: string
|
|
senderUserId?: string
|
|
streaming: boolean
|
|
isSelf: boolean
|
|
}
|
|
|
|
export interface ChatAssistantTurn {
|
|
id: string
|
|
role: 'assistant'
|
|
messages: ContentBlock[]
|
|
timestamp: string
|
|
platform?: string
|
|
streaming: boolean
|
|
}
|
|
|
|
export type ChatMessage = ChatUserTurn | ChatAssistantTurn
|
|
|
|
interface PendingAssistantStream {
|
|
assistantTurn: ChatAssistantTurn
|
|
done: boolean
|
|
resolve: () => void
|
|
reject: (err: Error) => void
|
|
}
|
|
|
|
export const useChatStore = defineStore('chat', () => {
|
|
const selectionStore = useChatSelectionStore()
|
|
const { currentBotId, sessionId } = storeToRefs(selectionStore)
|
|
|
|
const messages = reactive<ChatMessage[]>([])
|
|
const streamingSessionId = ref<string | null>(null)
|
|
const streaming = computed(() => streamingSessionId.value !== null && streamingSessionId.value === sessionId.value)
|
|
const sessions = ref<SessionSummary[]>([])
|
|
const loading = ref(false)
|
|
const loadingChats = ref(false)
|
|
const loadingOlder = ref(false)
|
|
const hasMoreOlder = ref(true)
|
|
const initializing = ref(false)
|
|
const bots = ref<Bot[]>([])
|
|
const overrideModelId = ref<string>('')
|
|
const overrideReasoningEffort = ref<string>('')
|
|
|
|
let abortFn: (() => void) | null = null
|
|
let messageEventsSince = ''
|
|
let pendingAssistantStream: PendingAssistantStream | null = null
|
|
let activeWs: ChatWebSocket | null = null
|
|
let refreshTimer: ReturnType<typeof setTimeout> | null = null
|
|
let refreshPromise: Promise<void> | null = null
|
|
const messageEventsStream = useRetryingStream()
|
|
|
|
const activeSession = computed(() =>
|
|
sessions.value.find((s) => s.id === sessionId.value) ?? null,
|
|
)
|
|
|
|
const activeChatReadOnly = computed(() => {
|
|
const session = activeSession.value
|
|
if (!session) return false
|
|
const type = session.type ?? 'chat'
|
|
if (type === 'heartbeat' || type === 'schedule' || type === 'subagent') return true
|
|
const ct = (session.channel_type ?? '').trim().toLowerCase()
|
|
if (ct && ct !== 'local') return true
|
|
return false
|
|
})
|
|
|
|
watch(currentBotId, (newId) => {
|
|
if (newId) {
|
|
void initialize()
|
|
} else {
|
|
stopMessageEvents()
|
|
stopWebSocket()
|
|
rejectPendingAssistantStream(new Error('Bot stream stopped'))
|
|
messageEventsSince = ''
|
|
sessions.value = []
|
|
sessionId.value = null
|
|
replaceMessages([])
|
|
}
|
|
})
|
|
|
|
const nextId = () => `${Date.now()}-${Math.floor(Math.random() * 1000)}`
|
|
|
|
const isPendingBot = (bot: Bot | null | undefined) =>
|
|
bot?.status === 'creating' || bot?.status === 'deleting'
|
|
|
|
function normalizeTimestamp(value?: string): string {
|
|
const raw = (value ?? '').trim()
|
|
if (!raw) return new Date().toISOString()
|
|
const parsed = new Date(raw)
|
|
return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString()
|
|
}
|
|
|
|
function resolveIsSelf(turn: UIUserTurn): boolean {
|
|
const platform = (turn.platform ?? '').trim().toLowerCase()
|
|
if (!platform || platform === 'local') return true
|
|
const senderUserId = (turn.sender_user_id ?? '').trim()
|
|
if (!senderUserId) return false
|
|
const userStore = useUserStore()
|
|
const currentUserId = (userStore.userInfo.id ?? '').trim()
|
|
if (!currentUserId) return false
|
|
return senderUserId === currentUserId
|
|
}
|
|
|
|
function normalizeAttachment(att: UIAttachment): AttachmentItem {
|
|
return { ...att }
|
|
}
|
|
|
|
function normalizeUIMessage(msg: UIMessage): ContentBlock {
|
|
switch (msg.type) {
|
|
case 'tool':
|
|
return {
|
|
...msg,
|
|
toolCallId: msg.tool_call_id,
|
|
toolName: msg.name,
|
|
result: msg.output ?? null,
|
|
done: !msg.running,
|
|
progress: msg.progress ? [...msg.progress] : undefined,
|
|
}
|
|
case 'attachments':
|
|
return {
|
|
...msg,
|
|
attachments: msg.attachments.map(normalizeAttachment),
|
|
}
|
|
default:
|
|
return { ...msg }
|
|
}
|
|
}
|
|
|
|
function normalizeTurn(turn: UITurn): ChatMessage {
|
|
if (turn.role === 'user') {
|
|
return {
|
|
id: String(turn.id ?? nextId()),
|
|
role: 'user',
|
|
text: turn.text ?? '',
|
|
attachments: (turn.attachments ?? []).map(normalizeAttachment),
|
|
timestamp: normalizeTimestamp(turn.timestamp),
|
|
platform: (turn.platform ?? '').trim() || undefined,
|
|
senderDisplayName: (turn.sender_display_name ?? '').trim() || undefined,
|
|
senderAvatarUrl: (turn.sender_avatar_url ?? '').trim() || undefined,
|
|
senderUserId: (turn.sender_user_id ?? '').trim() || undefined,
|
|
streaming: false,
|
|
isSelf: resolveIsSelf(turn),
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: String(turn.id ?? nextId()),
|
|
role: 'assistant',
|
|
messages: (turn.messages ?? []).map(normalizeUIMessage),
|
|
timestamp: normalizeTimestamp(turn.timestamp),
|
|
platform: (turn.platform ?? '').trim() || undefined,
|
|
streaming: false,
|
|
}
|
|
}
|
|
|
|
function updateSince(value?: string) {
|
|
const next = (value ?? '').trim()
|
|
if (!next) return
|
|
if (!messageEventsSince) {
|
|
messageEventsSince = next
|
|
return
|
|
}
|
|
const currentTs = Date.parse(messageEventsSince)
|
|
const nextTs = Date.parse(next)
|
|
if (!Number.isNaN(nextTs) && (Number.isNaN(currentTs) || nextTs > currentTs)) {
|
|
messageEventsSince = next
|
|
}
|
|
}
|
|
|
|
function updateSinceFromMessages(items: ChatMessage[]) {
|
|
messageEventsSince = ''
|
|
for (const item of items) {
|
|
updateSince(item.timestamp)
|
|
}
|
|
}
|
|
|
|
function replaceMessages(items: UITurn[]) {
|
|
const normalized = items.map(normalizeTurn)
|
|
messages.splice(0, messages.length, ...normalized)
|
|
updateSinceFromMessages(normalized)
|
|
}
|
|
|
|
function createCompletionForAssistantTurn(assistantTurn: ChatAssistantTurn): Promise<void> {
|
|
return new Promise<void>((resolve, reject) => {
|
|
pendingAssistantStream = {
|
|
assistantTurn,
|
|
done: false,
|
|
resolve,
|
|
reject,
|
|
}
|
|
})
|
|
}
|
|
|
|
function createOptimisticAssistantTurn(): ChatAssistantTurn {
|
|
return {
|
|
id: nextId(),
|
|
role: 'assistant',
|
|
messages: [],
|
|
timestamp: new Date().toISOString(),
|
|
streaming: true,
|
|
}
|
|
}
|
|
|
|
function createOptimisticUserTurn(text: string, attachments?: ChatAttachment[]): ChatUserTurn {
|
|
return {
|
|
id: nextId(),
|
|
role: 'user',
|
|
text,
|
|
attachments: (attachments ?? []).map((attachment) => ({
|
|
type: attachment.type,
|
|
url: attachment.base64,
|
|
base64: attachment.base64,
|
|
name: attachment.name ?? '',
|
|
mime: attachment.mime ?? '',
|
|
})),
|
|
timestamp: new Date().toISOString(),
|
|
streaming: false,
|
|
isSelf: true,
|
|
}
|
|
}
|
|
|
|
function resolvePendingAssistantStream() {
|
|
if (!pendingAssistantStream || pendingAssistantStream.done) return
|
|
const session = pendingAssistantStream
|
|
session.assistantTurn.streaming = false
|
|
session.done = true
|
|
pendingAssistantStream = null
|
|
session.resolve()
|
|
}
|
|
|
|
function rejectPendingAssistantStream(err: Error) {
|
|
if (!pendingAssistantStream || pendingAssistantStream.done) return
|
|
const session = pendingAssistantStream
|
|
session.assistantTurn.streaming = false
|
|
session.done = true
|
|
pendingAssistantStream = null
|
|
session.reject(err)
|
|
}
|
|
|
|
function ensureDiscussStream(): PendingAssistantStream {
|
|
if (pendingAssistantStream && !pendingAssistantStream.done) {
|
|
return pendingAssistantStream
|
|
}
|
|
messages.push(createOptimisticAssistantTurn())
|
|
const assistantTurn = messages[messages.length - 1] as ChatAssistantTurn
|
|
void createCompletionForAssistantTurn(assistantTurn).catch(() => {})
|
|
return pendingAssistantStream!
|
|
}
|
|
|
|
function upsertAssistantUIMessage(turn: ChatAssistantTurn, message: UIMessage) {
|
|
const normalized = normalizeUIMessage(message)
|
|
turn.messages = upsertById(turn.messages, normalized)
|
|
}
|
|
|
|
function nextAssistantMessageId(turn: ChatAssistantTurn): number {
|
|
return turn.messages.reduce((maxId, message) => Math.max(maxId, message.id), -1) + 1
|
|
}
|
|
|
|
function appendAssistantError(session: PendingAssistantStream, errorMessage: string) {
|
|
const text = errorMessage.trim()
|
|
if (!text) return
|
|
|
|
for (let index = session.assistantTurn.messages.length - 1; index >= 0; index -= 1) {
|
|
const current = session.assistantTurn.messages[index]
|
|
if (current?.type === 'text') {
|
|
session.assistantTurn.messages[index] = {
|
|
...current,
|
|
content: `${current.content}\n\n**Error:** ${text}`.trim(),
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
session.assistantTurn.messages.push({
|
|
id: nextAssistantMessageId(session.assistantTurn),
|
|
type: 'text',
|
|
content: `**Error:** ${text}`,
|
|
})
|
|
}
|
|
|
|
function handleWSStreamEvent(event: UIStreamEvent) {
|
|
switch (event.type) {
|
|
case 'start':
|
|
ensureDiscussStream()
|
|
break
|
|
case 'message':
|
|
upsertAssistantUIMessage(ensureDiscussStream().assistantTurn, event.data)
|
|
break
|
|
case 'end':
|
|
resolvePendingAssistantStream()
|
|
break
|
|
case 'error': {
|
|
const session = ensureDiscussStream()
|
|
appendAssistantError(session, event.message || 'stream error')
|
|
rejectPendingAssistantStream(new Error(event.message || 'stream error'))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
function stopMessageEvents() {
|
|
messageEventsStream.stop()
|
|
}
|
|
|
|
function stopWebSocket() {
|
|
if (activeWs) {
|
|
activeWs.close()
|
|
activeWs = null
|
|
}
|
|
}
|
|
|
|
function startWebSocket(targetBotId: string) {
|
|
const bid = targetBotId.trim()
|
|
stopWebSocket()
|
|
if (!bid) return
|
|
activeWs = connectWebSocket(bid, handleWSStreamEvent)
|
|
}
|
|
|
|
function ensureWebSocket(targetBotId: string): ChatWebSocket | null {
|
|
const bid = targetBotId.trim()
|
|
if (!bid) return null
|
|
if (!activeWs) {
|
|
startWebSocket(bid)
|
|
}
|
|
return activeWs
|
|
}
|
|
|
|
async function refreshCurrentSession(targetBotId?: string, targetSessionId?: string) {
|
|
const bid = (targetBotId ?? currentBotId.value ?? '').trim()
|
|
const sid = (targetSessionId ?? sessionId.value ?? '').trim()
|
|
if (!bid || !sid) return
|
|
|
|
if (refreshPromise) {
|
|
await refreshPromise
|
|
return
|
|
}
|
|
|
|
refreshPromise = (async () => {
|
|
const turns = await fetchMessagesUI(bid, sid, { limit: PAGE_SIZE })
|
|
if (currentBotId.value !== bid || sessionId.value !== sid) return
|
|
replaceMessages(turns)
|
|
touchSession(sid)
|
|
const streamStillActive = streamingSessionId.value === sid && pendingAssistantStream && !pendingAssistantStream.done
|
|
if (!streamStillActive && pendingAssistantStream) {
|
|
pendingAssistantStream.assistantTurn.streaming = false
|
|
pendingAssistantStream = null
|
|
}
|
|
if (!streamStillActive) {
|
|
streamingSessionId.value = null
|
|
}
|
|
})().finally(() => {
|
|
refreshPromise = null
|
|
})
|
|
|
|
await refreshPromise
|
|
}
|
|
|
|
function scheduleRefreshCurrentSession(expectedSessionId?: string, delay = 100) {
|
|
const sid = (sessionId.value ?? '').trim()
|
|
if (!sid) return
|
|
if (expectedSessionId?.trim() && expectedSessionId.trim() !== sid) return
|
|
if (refreshTimer) return
|
|
|
|
refreshTimer = setTimeout(() => {
|
|
refreshTimer = null
|
|
void refreshCurrentSession()
|
|
}, delay)
|
|
}
|
|
|
|
function handleStreamEvent(targetBotId: string, event: MessageStreamEvent) {
|
|
const eventType = (event.type ?? '').toLowerCase()
|
|
const eBotId = (event.bot_id ?? '').trim()
|
|
if (eBotId && eBotId !== targetBotId) return
|
|
|
|
if (eventType === 'message_created') {
|
|
const raw = event.message
|
|
if (!raw) return
|
|
updateSince(raw.created_at)
|
|
if (shouldRefreshFromMessageCreated(targetBotId, sessionId.value, streamingSessionId.value, event)) {
|
|
scheduleRefreshCurrentSession((raw.session_id ?? '').trim())
|
|
}
|
|
return
|
|
}
|
|
|
|
if (eventType === 'session_title_updated') {
|
|
const sid = (event.session_id ?? '').trim()
|
|
const title = (event.title ?? '').trim()
|
|
if (!sid || !title) return
|
|
const target = sessions.value.find((session) => session.id === sid)
|
|
if (target) target.title = title
|
|
}
|
|
}
|
|
|
|
function startMessageEvents(targetBotId: string) {
|
|
const bid = targetBotId.trim()
|
|
stopMessageEvents()
|
|
if (!bid) return
|
|
|
|
messageEventsStream.start(async (signal) => {
|
|
await streamMessageEvents(
|
|
bid,
|
|
signal,
|
|
(event) => handleStreamEvent(bid, event),
|
|
messageEventsSince || undefined,
|
|
)
|
|
})
|
|
}
|
|
|
|
function abort() {
|
|
if (activeWs?.connected) {
|
|
activeWs.abort()
|
|
}
|
|
abortFn?.()
|
|
abortFn = null
|
|
for (const message of messages) {
|
|
if (message.role === 'assistant' && message.streaming) {
|
|
message.streaming = false
|
|
}
|
|
}
|
|
streamingSessionId.value = null
|
|
}
|
|
|
|
async function ensureBot(): Promise<string | null> {
|
|
try {
|
|
const list = await fetchBots()
|
|
bots.value = list
|
|
if (!list.length) {
|
|
currentBotId.value = null
|
|
return null
|
|
}
|
|
if (currentBotId.value) {
|
|
const found = list.find(bot => bot.id === currentBotId.value)
|
|
if (found && !isPendingBot(found)) return currentBotId.value
|
|
}
|
|
const ready = list.find(bot => !isPendingBot(bot))
|
|
currentBotId.value = ready ? ready.id : list[0]!.id
|
|
return currentBotId.value
|
|
} catch (error) {
|
|
console.error('Failed to fetch bots:', error)
|
|
return currentBotId.value
|
|
}
|
|
}
|
|
|
|
const PAGE_SIZE = 30
|
|
|
|
async function loadMessages(botId: string, sid: string) {
|
|
const turns = await fetchMessagesUI(botId, sid, { limit: PAGE_SIZE })
|
|
replaceMessages(turns)
|
|
hasMoreOlder.value = turns.length > 0
|
|
}
|
|
|
|
async function loadOlderMessages(): Promise<number> {
|
|
const bid = currentBotId.value ?? ''
|
|
const sid = sessionId.value ?? ''
|
|
if (!bid || !sid || loadingOlder.value || !hasMoreOlder.value) return 0
|
|
const first = messages[0]
|
|
if (!first?.timestamp) return 0
|
|
|
|
loadingOlder.value = true
|
|
try {
|
|
const turns = await fetchMessagesUI(bid, sid, {
|
|
limit: PAGE_SIZE,
|
|
before: first.timestamp,
|
|
})
|
|
const existingIds = new Set(messages.map(message => message.id))
|
|
const older = turns
|
|
.map(normalizeTurn)
|
|
.filter(turn => !existingIds.has(turn.id))
|
|
if (older.length === 0) {
|
|
hasMoreOlder.value = false
|
|
return 0
|
|
}
|
|
messages.unshift(...older)
|
|
return older.length
|
|
} finally {
|
|
loadingOlder.value = false
|
|
}
|
|
}
|
|
|
|
function touchSession(targetSessionId: string) {
|
|
const index = sessions.value.findIndex(session => session.id === targetSessionId)
|
|
if (index < 0) return
|
|
const [target] = sessions.value.splice(index, 1)
|
|
if (!target) return
|
|
target.updated_at = new Date().toISOString()
|
|
sessions.value.unshift(target)
|
|
}
|
|
|
|
async function ensureActiveSession() {
|
|
if (sessionId.value) return
|
|
const bid = currentBotId.value ?? await ensureBot()
|
|
if (!bid) throw new Error('Bot not ready')
|
|
const created = await createSession(bid)
|
|
sessions.value = [created, ...sessions.value.filter(session => session.id !== created.id)]
|
|
sessionId.value = created.id
|
|
replaceMessages([])
|
|
}
|
|
|
|
async function initialize() {
|
|
if (initializing.value) return
|
|
initializing.value = true
|
|
loadingChats.value = true
|
|
stopMessageEvents()
|
|
stopWebSocket()
|
|
try {
|
|
const bid = await ensureBot()
|
|
if (!bid) {
|
|
messageEventsSince = ''
|
|
sessions.value = []
|
|
sessionId.value = null
|
|
replaceMessages([])
|
|
return
|
|
}
|
|
|
|
const visible = await fetchSessions(bid)
|
|
sessions.value = visible
|
|
if (!visible.length) {
|
|
messageEventsSince = ''
|
|
sessionId.value = null
|
|
replaceMessages([])
|
|
} else {
|
|
const activeSessionId = sessionId.value && visible.some(session => session.id === sessionId.value)
|
|
? sessionId.value
|
|
: visible[0]!.id
|
|
sessionId.value = activeSessionId
|
|
await loadMessages(bid, activeSessionId)
|
|
}
|
|
|
|
startWebSocket(bid)
|
|
startMessageEvents(bid)
|
|
} finally {
|
|
loadingChats.value = false
|
|
initializing.value = false
|
|
}
|
|
}
|
|
|
|
async function selectBot(targetBotId: string) {
|
|
if (currentBotId.value === targetBotId) return
|
|
abort()
|
|
currentBotId.value = targetBotId
|
|
sessionId.value = null
|
|
await initialize()
|
|
}
|
|
|
|
async function selectSession(targetSessionId: string) {
|
|
const sid = targetSessionId.trim()
|
|
if (!sid || sid === sessionId.value) return
|
|
sessionId.value = sid
|
|
loadingChats.value = true
|
|
try {
|
|
const bid = currentBotId.value ?? ''
|
|
if (!bid) throw new Error('Bot not selected')
|
|
await loadMessages(bid, sid)
|
|
} finally {
|
|
loadingChats.value = false
|
|
}
|
|
}
|
|
|
|
async function createNewSession() {
|
|
const bid = await ensureBot()
|
|
if (!bid) return
|
|
sessionId.value = null
|
|
replaceMessages([])
|
|
}
|
|
|
|
async function removeSession(targetSessionId: string) {
|
|
const delId = targetSessionId.trim()
|
|
if (!delId) return
|
|
loadingChats.value = true
|
|
try {
|
|
const bid = currentBotId.value ?? ''
|
|
if (!bid) throw new Error('Bot not selected')
|
|
await requestDeleteSession(bid, delId)
|
|
const remaining = sessions.value.filter(session => session.id !== delId)
|
|
sessions.value = remaining
|
|
if (sessionId.value !== delId) return
|
|
if (!remaining.length) {
|
|
sessionId.value = null
|
|
replaceMessages([])
|
|
return
|
|
}
|
|
sessionId.value = remaining[0]!.id
|
|
await loadMessages(bid, remaining[0]!.id)
|
|
} finally {
|
|
loadingChats.value = false
|
|
}
|
|
}
|
|
|
|
async function sendMessage(text: string, attachments?: ChatAttachment[]) {
|
|
const trimmed = text.trim()
|
|
if ((!trimmed && !attachments?.length) || streaming.value || !currentBotId.value) return
|
|
|
|
loading.value = true
|
|
let assistantTurn: ChatAssistantTurn | null = null
|
|
|
|
try {
|
|
await ensureActiveSession()
|
|
|
|
const bid = currentBotId.value!
|
|
const sid = sessionId.value!
|
|
streamingSessionId.value = sid
|
|
|
|
messages.push(createOptimisticUserTurn(trimmed, attachments))
|
|
messages.push(createOptimisticAssistantTurn())
|
|
assistantTurn = messages[messages.length - 1] as ChatAssistantTurn
|
|
|
|
const modelId = overrideModelId.value || undefined
|
|
const effort = overrideReasoningEffort.value
|
|
const reasoningEffort = effort && effort !== 'off' ? effort : undefined
|
|
|
|
const ws = ensureWebSocket(bid)
|
|
if (ws) {
|
|
const completion = createCompletionForAssistantTurn(assistantTurn)
|
|
abortFn = () => {
|
|
const abortError = new Error('aborted')
|
|
abortError.name = 'AbortError'
|
|
ws.abort()
|
|
rejectPendingAssistantStream(abortError)
|
|
}
|
|
ws.send({
|
|
type: 'message',
|
|
text: trimmed,
|
|
session_id: sid,
|
|
attachments,
|
|
model_id: modelId,
|
|
reasoning_effort: reasoningEffort,
|
|
})
|
|
await completion
|
|
await refreshCurrentSession(bid, sid)
|
|
} else {
|
|
void createCompletionForAssistantTurn(assistantTurn).catch(() => {})
|
|
await sendLocalChannelMessage(bid, trimmed, attachments, { modelId, reasoningEffort })
|
|
await refreshCurrentSession(bid, sid)
|
|
}
|
|
|
|
assistantTurn.streaming = false
|
|
streamingSessionId.value = null
|
|
loading.value = false
|
|
abortFn = null
|
|
touchSession(sid)
|
|
} catch (error) {
|
|
const isAbort = error instanceof Error && error.name === 'AbortError'
|
|
const reason = error instanceof Error ? error.message : 'Unknown error'
|
|
if (!isAbort && assistantTurn) {
|
|
assistantTurn.messages = [{
|
|
id: 0,
|
|
type: 'text',
|
|
content: `Failed to send message: ${reason}`,
|
|
}]
|
|
assistantTurn.streaming = false
|
|
} else if (!isAbort) {
|
|
messages.push({
|
|
id: nextId(),
|
|
role: 'assistant',
|
|
messages: [{
|
|
id: 0,
|
|
type: 'text',
|
|
content: `Failed to send message: ${reason}`,
|
|
}],
|
|
timestamp: new Date().toISOString(),
|
|
streaming: false,
|
|
})
|
|
}
|
|
pendingAssistantStream = null
|
|
streamingSessionId.value = null
|
|
loading.value = false
|
|
abortFn = null
|
|
}
|
|
}
|
|
|
|
function clearMessages() {
|
|
abort()
|
|
replaceMessages([])
|
|
}
|
|
|
|
const chats = sessions
|
|
const chatId = sessionId
|
|
|
|
return {
|
|
messages,
|
|
streaming,
|
|
sessions,
|
|
chats,
|
|
chatId,
|
|
sessionId,
|
|
currentBotId,
|
|
bots,
|
|
activeSession,
|
|
activeChatReadOnly,
|
|
loading,
|
|
loadingChats,
|
|
loadingOlder,
|
|
hasMoreOlder,
|
|
initializing,
|
|
overrideModelId,
|
|
overrideReasoningEffort,
|
|
initialize,
|
|
selectBot,
|
|
selectSession,
|
|
selectChat: selectSession,
|
|
createNewSession,
|
|
createNewChat: createNewSession,
|
|
removeSession,
|
|
removeChat: removeSession,
|
|
deleteChat: removeSession,
|
|
sendMessage,
|
|
clearMessages,
|
|
loadOlderMessages,
|
|
abort,
|
|
}
|
|
})
|
|
|