File size: 3,473 Bytes
dd4a03a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | import { createContext, useCallback, useContext, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import { getSessionMessages } from '../api/client.ts'
import type { SourceInfo } from '../api/types.ts'
export type MessageStatus = 'pending' | 'streaming' | 'done' | 'error'
export interface ChatMessage {
id: string
role: 'user' | 'assistant'
content: string
sources: SourceInfo[] | null
status: MessageStatus
createdAt: string
}
interface SessionContextValue {
sessionId: string
messages: ChatMessage[]
isLoadingHistory: boolean
loadHistory: () => Promise<void>
appendMessage: (message: ChatMessage) => void
updateLastMessage: (updater: (message: ChatMessage) => ChatMessage) => void
switchSession: (newSessionId: string) => void
setMessages: (messages: ChatMessage[]) => void
}
const SESSION_KEY = 'studyson:session_id'
const SessionContext = createContext<SessionContextValue | undefined>(undefined)
function getOrCreateSessionId(): string {
let id = localStorage.getItem(SESSION_KEY)
if (!id) {
id = crypto.randomUUID()
localStorage.setItem(SESSION_KEY, id)
}
return id
}
function makeMessageId(): string {
return crypto.randomUUID ? crypto.randomUUID() : `m_${Date.now()}_${Math.random().toString(36).slice(2)}`
}
export function SessionProvider({ children }: { children: ReactNode }) {
const [sessionId, setSessionId] = useState<string>(getOrCreateSessionId)
const [messages, setMessages] = useState<ChatMessage[]>([])
const [isLoadingHistory, setIsLoadingHistory] = useState(false)
const loadHistory = useCallback(async () => {
setIsLoadingHistory(true)
try {
const currentId = localStorage.getItem(SESSION_KEY) ?? sessionId
const res = await getSessionMessages(currentId)
const hydrated: ChatMessage[] = res.messages.map((m) => ({
id: makeMessageId(),
role: m.role,
content: m.content,
sources: m.sources,
status: 'done',
createdAt: m.created_at,
}))
setMessages(hydrated)
} catch {
// Leave messages as-is on failure; UI surfaces errors via toast at call sites.
} finally {
setIsLoadingHistory(false)
}
}, [sessionId])
const appendMessage = useCallback((message: ChatMessage) => {
setMessages((prev) => [...prev, message])
}, [])
const updateLastMessage = useCallback((updater: (message: ChatMessage) => ChatMessage) => {
setMessages((prev) => {
if (prev.length === 0) return prev
const next = prev.slice()
const last = next[next.length - 1]
if (!last) return prev
next[next.length - 1] = updater(last)
return next
})
}, [])
const switchSession = useCallback((newSessionId: string) => {
localStorage.setItem(SESSION_KEY, newSessionId)
setSessionId(newSessionId)
setMessages([])
}, [])
const value = useMemo(
() => ({
sessionId,
messages,
isLoadingHistory,
loadHistory,
appendMessage,
updateLastMessage,
switchSession,
setMessages,
}),
[sessionId, messages, isLoadingHistory, loadHistory, appendMessage, updateLastMessage, switchSession],
)
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>
}
export function useSession(): SessionContextValue {
const ctx = useContext(SessionContext)
if (!ctx) {
throw new Error('useSession must be used within a SessionProvider')
}
return ctx
}
|