| import React, { createContext, useContext, useEffect, useState } from 'react'; |
| import { |
| APIMessage, |
| CanvasData, |
| Conversation, |
| Message, |
| PendingMessage, |
| ViewingChat, |
| } from './types'; |
| import StorageUtils from './storage'; |
| import { |
| filterThoughtFromMsgs, |
| normalizeMsgsForAPI, |
| getSSEStreamAsync, |
| } from './misc'; |
| import { BASE_URL, CONFIG_DEFAULT, isDev } from '../Config'; |
| import { matchPath, useLocation, useNavigate } from 'react-router'; |
|
|
| interface AppContextValue { |
| |
| viewingChat: ViewingChat | null; |
| pendingMessages: Record<Conversation['id'], PendingMessage>; |
| isGenerating: (convId: string) => boolean; |
| sendMessage: ( |
| convId: string | null, |
| leafNodeId: Message['id'] | null, |
| content: string, |
| extra: Message['extra'], |
| onChunk: CallbackGeneratedChunk |
| ) => Promise<boolean>; |
| stopGenerating: (convId: string) => void; |
| replaceMessageAndGenerate: ( |
| convId: string, |
| parentNodeId: Message['id'], |
| content: string | null, |
| extra: Message['extra'], |
| onChunk: CallbackGeneratedChunk |
| ) => Promise<void>; |
|
|
| |
| canvasData: CanvasData | null; |
| setCanvasData: (data: CanvasData | null) => void; |
|
|
| |
| config: typeof CONFIG_DEFAULT; |
| saveConfig: (config: typeof CONFIG_DEFAULT) => void; |
| showSettings: boolean; |
| setShowSettings: (show: boolean) => void; |
| } |
|
|
| |
| export type CallbackGeneratedChunk = (currLeafNodeId?: Message['id']) => void; |
|
|
| |
| const AppContext = createContext<AppContextValue>({} as any); |
|
|
| const getViewingChat = async (convId: string): Promise<ViewingChat | null> => { |
| const conv = await StorageUtils.getOneConversation(convId); |
| if (!conv) return null; |
| return { |
| conv: conv, |
| |
| messages: await StorageUtils.getMessages(convId), |
| }; |
| }; |
|
|
| export const AppContextProvider = ({ |
| children, |
| }: { |
| children: React.ReactElement; |
| }) => { |
| const { pathname } = useLocation(); |
| const navigate = useNavigate(); |
| const params = matchPath('/chat/:convId', pathname); |
| const convId = params?.params?.convId; |
|
|
| const [viewingChat, setViewingChat] = useState<ViewingChat | null>(null); |
| const [pendingMessages, setPendingMessages] = useState< |
| Record<Conversation['id'], PendingMessage> |
| >({}); |
| const [aborts, setAborts] = useState< |
| Record<Conversation['id'], AbortController> |
| >({}); |
| const [config, setConfig] = useState(StorageUtils.getConfig()); |
| const [canvasData, setCanvasData] = useState<CanvasData | null>(null); |
| const [showSettings, setShowSettings] = useState(false); |
|
|
| |
| useEffect(() => { |
| |
| setCanvasData(null); |
| const handleConversationChange = async (changedConvId: string) => { |
| if (changedConvId !== convId) return; |
| setViewingChat(await getViewingChat(changedConvId)); |
| }; |
| StorageUtils.onConversationChanged(handleConversationChange); |
| getViewingChat(convId ?? '').then(setViewingChat); |
| return () => { |
| StorageUtils.offConversationChanged(handleConversationChange); |
| }; |
| }, [convId]); |
|
|
| const setPending = (convId: string, pendingMsg: PendingMessage | null) => { |
| |
| if (!pendingMsg) { |
| setPendingMessages((prev) => { |
| const newState = { ...prev }; |
| delete newState[convId]; |
| return newState; |
| }); |
| } else { |
| setPendingMessages((prev) => ({ ...prev, [convId]: pendingMsg })); |
| } |
| }; |
|
|
| const setAbort = (convId: string, controller: AbortController | null) => { |
| if (!controller) { |
| setAborts((prev) => { |
| const newState = { ...prev }; |
| delete newState[convId]; |
| return newState; |
| }); |
| } else { |
| setAborts((prev) => ({ ...prev, [convId]: controller })); |
| } |
| }; |
|
|
| |
| |
|
|
| const isGenerating = (convId: string) => !!pendingMessages[convId]; |
|
|
| const generateMessage = async ( |
| convId: string, |
| leafNodeId: Message['id'], |
| onChunk: CallbackGeneratedChunk |
| ) => { |
| if (isGenerating(convId)) return; |
|
|
| const config = StorageUtils.getConfig(); |
| const currConversation = await StorageUtils.getOneConversation(convId); |
| if (!currConversation) { |
| throw new Error('Current conversation is not found'); |
| } |
|
|
| const currMessages = StorageUtils.filterByLeafNodeId( |
| await StorageUtils.getMessages(convId), |
| leafNodeId, |
| false |
| ); |
| const abortController = new AbortController(); |
| setAbort(convId, abortController); |
|
|
| if (!currMessages) { |
| throw new Error('Current messages are not found'); |
| } |
|
|
| const pendingId = Date.now() + 1; |
| let pendingMsg: PendingMessage = { |
| id: pendingId, |
| convId, |
| type: 'text', |
| timestamp: pendingId, |
| role: 'assistant', |
| content: null, |
| parent: leafNodeId, |
| children: [], |
| }; |
| setPending(convId, pendingMsg); |
|
|
| try { |
| |
| let messages: APIMessage[] = [ |
| ...(config.systemMessage.length === 0 |
| ? [] |
| : [{ role: 'system', content: config.systemMessage } as APIMessage]), |
| ...normalizeMsgsForAPI(currMessages), |
| ]; |
| if (config.excludeThoughtOnReq) { |
| messages = filterThoughtFromMsgs(messages); |
| } |
| if (isDev) console.log({ messages }); |
|
|
| |
| const params = { |
| messages, |
| stream: true, |
| cache_prompt: true, |
| samplers: config.samplers, |
| temperature: config.temperature, |
| dynatemp_range: config.dynatemp_range, |
| dynatemp_exponent: config.dynatemp_exponent, |
| top_k: config.top_k, |
| top_p: config.top_p, |
| min_p: config.min_p, |
| typical_p: config.typical_p, |
| xtc_probability: config.xtc_probability, |
| xtc_threshold: config.xtc_threshold, |
| repeat_last_n: config.repeat_last_n, |
| repeat_penalty: config.repeat_penalty, |
| presence_penalty: config.presence_penalty, |
| frequency_penalty: config.frequency_penalty, |
| dry_multiplier: config.dry_multiplier, |
| dry_base: config.dry_base, |
| dry_allowed_length: config.dry_allowed_length, |
| dry_penalty_last_n: config.dry_penalty_last_n, |
| max_tokens: config.max_tokens, |
| timings_per_token: !!config.showTokensPerSecond, |
| ...(config.custom.length ? JSON.parse(config.custom) : {}), |
| }; |
|
|
| |
| const fetchResponse = await fetch(`${BASE_URL}/v1/chat/completions`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| ...(config.apiKey |
| ? { Authorization: `Bearer ${config.apiKey}` } |
| : {}), |
| }, |
| body: JSON.stringify(params), |
| signal: abortController.signal, |
| }); |
| if (fetchResponse.status !== 200) { |
| const body = await fetchResponse.json(); |
| throw new Error(body?.error?.message || 'Unknown error'); |
| } |
| const chunks = getSSEStreamAsync(fetchResponse); |
| for await (const chunk of chunks) { |
| |
| if (chunk.error) { |
| throw new Error(chunk.error?.message || 'Unknown error'); |
| } |
| const addedContent = chunk.choices[0].delta.content; |
| const lastContent = pendingMsg.content || ''; |
| if (addedContent) { |
| pendingMsg = { |
| ...pendingMsg, |
| content: lastContent + addedContent, |
| }; |
| } |
| const timings = chunk.timings; |
| if (timings && config.showTokensPerSecond) { |
| |
| pendingMsg.timings = { |
| prompt_n: timings.prompt_n, |
| prompt_ms: timings.prompt_ms, |
| predicted_n: timings.predicted_n, |
| predicted_ms: timings.predicted_ms, |
| }; |
| } |
| setPending(convId, pendingMsg); |
| onChunk(); |
| } |
| } catch (err) { |
| setPending(convId, null); |
| if ((err as Error).name === 'AbortError') { |
| |
| |
| } else { |
| console.error(err); |
| |
| alert((err as any)?.message ?? 'Unknown error'); |
| throw err; |
| } |
| } |
|
|
| if (pendingMsg.content !== null) { |
| await StorageUtils.appendMsg(pendingMsg as Message, leafNodeId); |
| } |
| setPending(convId, null); |
| onChunk(pendingId); |
| }; |
|
|
| const sendMessage = async ( |
| convId: string | null, |
| leafNodeId: Message['id'] | null, |
| content: string, |
| extra: Message['extra'], |
| onChunk: CallbackGeneratedChunk |
| ): Promise<boolean> => { |
| if (isGenerating(convId ?? '') || content.trim().length === 0) return false; |
|
|
| if (convId === null || convId.length === 0 || leafNodeId === null) { |
| const conv = await StorageUtils.createConversation( |
| content.substring(0, 256) |
| ); |
| convId = conv.id; |
| leafNodeId = conv.currNode; |
| |
| navigate(`/chat/${convId}`); |
| } |
|
|
| const now = Date.now(); |
| const currMsgId = now; |
| StorageUtils.appendMsg( |
| { |
| id: currMsgId, |
| timestamp: now, |
| type: 'text', |
| convId, |
| role: 'user', |
| content, |
| extra, |
| parent: leafNodeId, |
| children: [], |
| }, |
| leafNodeId |
| ); |
| onChunk(currMsgId); |
|
|
| try { |
| await generateMessage(convId, currMsgId, onChunk); |
| return true; |
| } catch (_) { |
| |
| } |
| return false; |
| }; |
|
|
| const stopGenerating = (convId: string) => { |
| setPending(convId, null); |
| aborts[convId]?.abort(); |
| }; |
|
|
| |
| const replaceMessageAndGenerate = async ( |
| convId: string, |
| parentNodeId: Message['id'], |
| content: string | null, |
| extra: Message['extra'], |
| onChunk: CallbackGeneratedChunk |
| ) => { |
| if (isGenerating(convId)) return; |
|
|
| if (content !== null) { |
| const now = Date.now(); |
| const currMsgId = now; |
| StorageUtils.appendMsg( |
| { |
| id: currMsgId, |
| timestamp: now, |
| type: 'text', |
| convId, |
| role: 'user', |
| content, |
| extra, |
| parent: parentNodeId, |
| children: [], |
| }, |
| parentNodeId |
| ); |
| parentNodeId = currMsgId; |
| } |
| onChunk(parentNodeId); |
|
|
| await generateMessage(convId, parentNodeId, onChunk); |
| }; |
|
|
| const saveConfig = (config: typeof CONFIG_DEFAULT) => { |
| StorageUtils.setConfig(config); |
| setConfig(config); |
| }; |
|
|
| return ( |
| <AppContext.Provider |
| value={{ |
| isGenerating, |
| viewingChat, |
| pendingMessages, |
| sendMessage, |
| stopGenerating, |
| replaceMessageAndGenerate, |
| canvasData, |
| setCanvasData, |
| config, |
| saveConfig, |
| showSettings, |
| setShowSettings, |
| }} |
| > |
| {children} |
| </AppContext.Provider> |
| ); |
| }; |
|
|
| export const useAppContext = () => useContext(AppContext); |
|
|