You've already forked agentic-coding-workflow
feat(web): chat pinia store + useChatStream (fetch+ReadableStream SSE)
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
chatApi,
|
||||
type ChatMessage,
|
||||
type Conversation,
|
||||
type ListConversationsParams,
|
||||
} from '@/api/chat'
|
||||
import {
|
||||
useChatStream,
|
||||
type ChatStreamEvent,
|
||||
type ChatStreamHandle,
|
||||
} from '@/composables/useChatStream'
|
||||
|
||||
interface UsagePayload {
|
||||
PromptTokens?: number
|
||||
CompletionTokens?: number
|
||||
ThinkingTokens?: number
|
||||
}
|
||||
|
||||
interface TextDeltaPayload {
|
||||
text?: string
|
||||
}
|
||||
|
||||
interface ErrorPayload {
|
||||
code?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
interface MetaPayload {
|
||||
id?: number
|
||||
role?: string
|
||||
status?: ChatMessage['status']
|
||||
}
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const conversations = ref<Conversation[]>([])
|
||||
const currentConversation = ref<Conversation | null>(null)
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const streamingMessageId = ref<number | null>(null)
|
||||
const streamHandle = ref<ChatStreamHandle | null>(null)
|
||||
|
||||
const isStreaming = computed(() => streamingMessageId.value !== null)
|
||||
|
||||
async function loadConversations(filter: ListConversationsParams = {}) {
|
||||
conversations.value = await chatApi.listConversations(filter)
|
||||
}
|
||||
|
||||
async function openConversation(id: string) {
|
||||
detachStream()
|
||||
const detail = await chatApi.getConversation(id)
|
||||
currentConversation.value = detail.conversation
|
||||
messages.value = detail.messages
|
||||
const pending = detail.messages.find((m) => m.status === 'pending')
|
||||
if (pending) {
|
||||
streamingMessageId.value = pending.id
|
||||
attachStream(`/api/v1/conversations/${id}/stream`, pending.id - 1)
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(content: string, attachmentIds: string[] = []) {
|
||||
if (!currentConversation.value) return
|
||||
const conv = currentConversation.value
|
||||
const resp = await chatApi.sendMessage(conv.id, {
|
||||
content,
|
||||
attachment_ids: attachmentIds,
|
||||
})
|
||||
const now = new Date().toISOString()
|
||||
messages.value.push({
|
||||
id: resp.user_message_id,
|
||||
conversation_id: conv.id,
|
||||
role: 'user',
|
||||
content,
|
||||
status: 'ok',
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
messages.value.push({
|
||||
id: resp.assistant_message_id,
|
||||
conversation_id: conv.id,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
status: 'pending',
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
})
|
||||
streamingMessageId.value = resp.assistant_message_id
|
||||
attachStream(resp.stream_url, resp.user_message_id)
|
||||
}
|
||||
|
||||
async function cancelMessage() {
|
||||
const id = streamingMessageId.value
|
||||
if (id == null) return
|
||||
await chatApi.cancelMessage(id)
|
||||
detachStream()
|
||||
}
|
||||
|
||||
async function retryMessage(messageId: number) {
|
||||
if (!currentConversation.value) return
|
||||
const resp = await chatApi.retryMessage(messageId)
|
||||
const idx = messages.value.findIndex((m) => m.id === messageId)
|
||||
if (idx >= 0) {
|
||||
const old = messages.value[idx]
|
||||
messages.value[idx] = {
|
||||
...old,
|
||||
id: resp.assistant_message_id,
|
||||
content: '',
|
||||
thinking: null,
|
||||
status: 'pending',
|
||||
error_message: null,
|
||||
}
|
||||
}
|
||||
streamingMessageId.value = resp.assistant_message_id
|
||||
attachStream(resp.stream_url, resp.assistant_message_id - 1)
|
||||
}
|
||||
|
||||
async function deleteConversation(id: string) {
|
||||
await chatApi.deleteConversation(id)
|
||||
conversations.value = conversations.value.filter((c) => c.id !== id)
|
||||
if (currentConversation.value?.id === id) {
|
||||
currentConversation.value = null
|
||||
messages.value = []
|
||||
detachStream()
|
||||
}
|
||||
}
|
||||
|
||||
function attachStream(streamUrl: string, sinceMessageID: number) {
|
||||
detachStream()
|
||||
const stream = useChatStream()
|
||||
streamHandle.value = stream.start(streamUrl, sinceMessageID, {
|
||||
onEvent: (ev) => applyStreamEvent(ev),
|
||||
onClose: () => {
|
||||
streamHandle.value = null
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error('chat stream error', err)
|
||||
const id = streamingMessageId.value
|
||||
if (id != null) {
|
||||
const msg = messages.value.find((m) => m.id === id)
|
||||
if (msg && msg.status === 'pending') {
|
||||
msg.status = 'error'
|
||||
msg.error_message = err.message
|
||||
}
|
||||
}
|
||||
streamingMessageId.value = null
|
||||
streamHandle.value = null
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function detachStream() {
|
||||
streamHandle.value?.stop()
|
||||
streamHandle.value = null
|
||||
}
|
||||
|
||||
function applyStreamEvent(ev: ChatStreamEvent) {
|
||||
const targetId = streamingMessageId.value
|
||||
if (targetId == null) return
|
||||
const msg = messages.value.find((m) => m.id === targetId)
|
||||
if (!msg) return
|
||||
switch (ev.event) {
|
||||
case 'message_meta': {
|
||||
const d = ev.data as MetaPayload
|
||||
if (d.status) msg.status = d.status
|
||||
break
|
||||
}
|
||||
case 'text_delta': {
|
||||
const d = ev.data as TextDeltaPayload
|
||||
if (d.text) msg.content += d.text
|
||||
break
|
||||
}
|
||||
case 'thinking_delta': {
|
||||
const d = ev.data as TextDeltaPayload
|
||||
if (d.text) msg.thinking = (msg.thinking ?? '') + d.text
|
||||
break
|
||||
}
|
||||
case 'usage': {
|
||||
const u = ev.data as UsagePayload
|
||||
msg.prompt_tokens = u.PromptTokens ?? msg.prompt_tokens
|
||||
msg.completion_tokens = u.CompletionTokens ?? msg.completion_tokens
|
||||
msg.thinking_tokens = u.ThinkingTokens ?? msg.thinking_tokens
|
||||
break
|
||||
}
|
||||
case 'done':
|
||||
msg.status = 'ok'
|
||||
streamingMessageId.value = null
|
||||
break
|
||||
case 'error': {
|
||||
const d = ev.data as ErrorPayload
|
||||
msg.status = 'error'
|
||||
msg.error_message = d.message ?? d.code ?? 'stream error'
|
||||
streamingMessageId.value = null
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
conversations,
|
||||
currentConversation,
|
||||
messages,
|
||||
streamingMessageId,
|
||||
isStreaming,
|
||||
loadConversations,
|
||||
openConversation,
|
||||
sendMessage,
|
||||
cancelMessage,
|
||||
retryMessage,
|
||||
deleteConversation,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user