156 lines
3.7 KiB
TypeScript
156 lines
3.7 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
import { api } from '@/services/api'
|
|
|
|
export interface ChatSource {
|
|
type: 'TEXT' | 'FIGURE'
|
|
bookId?: string
|
|
bookTitle: string
|
|
page: number | null
|
|
refLabel?: string
|
|
// TEXT-specific
|
|
chunkText?: string
|
|
// FIGURE-specific
|
|
figureId?: string
|
|
label?: string
|
|
caption?: string
|
|
figureType?: string
|
|
imageUrl?: string
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
id: string
|
|
role: 'USER' | 'ASSISTANT'
|
|
content: string
|
|
sources: ChatSource[]
|
|
createdAt: string
|
|
}
|
|
|
|
export interface ChatSession {
|
|
sessionId: string
|
|
topicId: string | null
|
|
createdAt: string
|
|
}
|
|
|
|
export const useChatStore = defineStore('chat', () => {
|
|
const session = ref<ChatSession | null>(null)
|
|
const messages = ref<ChatMessage[]>([])
|
|
const loading = ref(false)
|
|
const sending = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
async function createSession(topicId?: string): Promise<boolean> {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const body = topicId ? { topicId } : {}
|
|
const response = await api.post<ChatSession>('/chat/sessions', body)
|
|
session.value = response.data
|
|
messages.value = []
|
|
return true
|
|
} catch (err: any) {
|
|
error.value = err.message
|
|
return false
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadMessages(): Promise<void> {
|
|
if (!session.value) return
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const response = await api.get<ChatMessage[]>(
|
|
`/chat/sessions/${session.value.sessionId}/messages`
|
|
)
|
|
messages.value = response.data
|
|
} catch (err: any) {
|
|
error.value = err.message
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function sendMessage(content: string): Promise<boolean> {
|
|
if (!session.value) return false
|
|
sending.value = true
|
|
error.value = null
|
|
|
|
// Optimistically add user message
|
|
const tempUserMsg: ChatMessage = {
|
|
id: crypto.randomUUID(),
|
|
role: 'USER',
|
|
content,
|
|
sources: [],
|
|
createdAt: new Date().toISOString()
|
|
}
|
|
messages.value.push(tempUserMsg)
|
|
|
|
try {
|
|
const response = await api.post<ChatMessage>(
|
|
`/chat/sessions/${session.value.sessionId}/messages`,
|
|
{ content }
|
|
)
|
|
// Replace temp message & add assistant response
|
|
messages.value = messages.value.filter((m) => m.id !== tempUserMsg.id)
|
|
// Reload full conversation to stay in sync
|
|
await loadMessages()
|
|
return true
|
|
} catch (err: any) {
|
|
// Remove optimistic message on failure
|
|
messages.value = messages.value.filter((m) => m.id !== tempUserMsg.id)
|
|
error.value = err.message
|
|
return false
|
|
} finally {
|
|
sending.value = false
|
|
}
|
|
}
|
|
|
|
function leaveSession(): void {
|
|
session.value = null
|
|
messages.value = []
|
|
error.value = null
|
|
}
|
|
|
|
async function fetchSessionsByTopic(topicId: string): Promise<ChatSession[]> {
|
|
try {
|
|
const response = await api.get<ChatSession[]>('/chat/sessions', { params: { topicId } })
|
|
return response.data
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
async function deleteSession(): Promise<boolean> {
|
|
if (!session.value) return false
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
await api.delete(`/chat/sessions/${session.value.sessionId}`)
|
|
session.value = null
|
|
messages.value = []
|
|
return true
|
|
} catch (err: any) {
|
|
error.value = err.message
|
|
return false
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
session,
|
|
messages,
|
|
loading,
|
|
sending,
|
|
error,
|
|
createSession,
|
|
loadMessages,
|
|
sendMessage,
|
|
leaveSession,
|
|
fetchSessionsByTopic,
|
|
deleteSession
|
|
}
|
|
})
|