import { useCallback, useMemo, useRef, useState } from 'react'; import type { Dispatch, SetStateAction } from 'react'; import type { ChatMessage } from '../../src/elements/chat-types'; export type { ChatMessage } from '../../src/elements/chat-types'; import type { AttachmentData } from '../../src/components/attachment-types'; import { appendMessage, updateMessage, removeMessage, addSuggestion, removeSuggestion, createAssistantStream, onStreamSettled, type AssistantStream, type SetMessages, } from '../../src/state'; export interface UseKaiChatOptions { /** Seed messages, read once at mount and copied. Later changes are ignored — drive updates through the returned ops. */ initialMessages?: ChatMessage[]; /** Seed suggestions, read once at mount and copied. Later changes are ignored. */ initialSuggestions?: string[]; onSubmit?: (detail: { value: string; attachments: AttachmentData[] }) => void | Promise; } /** Owns chat state in React and exposes ergonomic, fully-typed controlled operations. */ export interface KaiChatController { messages: ChatMessage[]; setMessages: Dispatch>; suggestions: string[]; setSuggestions: Dispatch>; loading: boolean; append: (msg: ChatMessage) => void; update: (id: string, patch: Partial | ((m: ChatMessage) => ChatMessage)) => void; remove: (id: string) => void; addSuggestion: (s: string) => void; removeSuggestion: (s: string) => void; clearSuggestions: () => void; streamAssistant: (init?: Partial) => AssistantStream; /** Spread onto `` — wires messages, loading, suggestions, and kai-submit. */ bind: { messages: ChatMessage[]; loading: boolean; suggestions: string[]; onSubmit: (event: CustomEvent<{ value: string; attachments: AttachmentData[] }>) => void; }; } export function useKaiChat(options: UseKaiChatOptions = {}): KaiChatController { // Copy the seed arrays so a caller mutating the array it passed in can't reach our state. const [messages, setMessages] = useState(() => [...(options.initialMessages ?? [])]); const [suggestions, setSuggestions] = useState(() => [...(options.initialSuggestions ?? [])]); const [loading, setLoading] = useState(false); // Keep onSubmit current without re-binding the listener identity. const onSubmitRef = useRef(options.onSubmit); onSubmitRef.current = options.onSubmit; const set: SetMessages = useCallback((updater) => setMessages(updater), []); const append = useCallback((msg: ChatMessage) => set((prev) => appendMessage(prev, msg)), [set]); const update = useCallback( (id: string, patch: Partial | ((m: ChatMessage) => ChatMessage)) => set((prev) => updateMessage(prev, id, patch)), [set], ); const remove = useCallback((id: string) => set((prev) => removeMessage(prev, id)), [set]); const addSug = useCallback((s: string) => setSuggestions((prev) => addSuggestion(prev, s)), []); const removeSug = useCallback((s: string) => setSuggestions((prev) => removeSuggestion(prev, s)), []); const clearSuggestions = useCallback(() => setSuggestions([]), []); const streamAssistant = useCallback((init?: Partial): AssistantStream => { setLoading(true); return onStreamSettled(createAssistantStream(set, init), () => setLoading(false)); }, [set]); const onSubmit = useCallback( (event: CustomEvent<{ value: string; attachments: AttachmentData[] }>) => { void onSubmitRef.current?.(event.detail); }, [], ); const bind = useMemo( () => ({ messages, loading, suggestions, onSubmit }), [messages, loading, suggestions, onSubmit], ); return { messages, setMessages, suggestions, setSuggestions, loading, append, update, remove, addSuggestion: addSug, removeSuggestion: removeSug, clearSuggestions, streamAssistant, bind, }; }