import type { Dispatch, SetStateAction } from 'react'; import { useCallback, useMemo, useState, useEffect, } from 'react'; import { useForm, type UseFormReturn } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { UncontrolledTooltip, Dropdown, DropdownToggle, DropdownMenu, DropdownItem, } from 'reactstrap'; import { apiv3Post } from '~/client/util/apiv3-client'; import { SseMessageSchema, type SseMessage } from '~/features/openai/interfaces/knowledge-assistant/sse-schemas'; import { handleIfSuccessfullyParsed } from '~/features/openai/utils/handle-if-successfully-parsed'; import type { MessageLog, MessageWithCustomMetaData } from '../../interfaces/message'; import type { IThreadRelationHasId } from '../../interfaces/thread-relation'; import { ThreadType } from '../../interfaces/thread-relation'; import { AiAssistantChatInitialView } from '../components/AiAssistant/AiAssistantSidebar/AiAssistantChatInitialView'; import { MessageCard, type MessageCardRole } from '../components/AiAssistant/AiAssistantSidebar/MessageCard'; import { useAiAssistantSidebar } from '../stores/ai-assistant'; import { useSWRMUTxMessages } from '../stores/message'; import { useSWRMUTxThreads } from '../stores/thread'; interface CreateThread { (aiAssistantId: string, initialUserMessage: string): Promise; } interface PostMessage { (aiAssistantId: string, threadId: string, formData: FormData): Promise; } interface ProcessMessage { (data: unknown, handler: { onMessage: (data: SseMessage) => void} ): void; } interface GenerateMessageCard { (role: MessageCardRole, children: string): JSX.Element; } export interface FormData { input: string summaryMode?: boolean extendedThinkingMode?: boolean } interface GenerateModeSwitchesDropdown { (isGenerating: boolean): JSX.Element } type UseKnowledgeAssistant = () => { createThread: CreateThread postMessage: PostMessage processMessage: ProcessMessage form: UseFormReturn resetForm: () => void // Views initialView: JSX.Element generateMessageCard: GenerateMessageCard generateModeSwitchesDropdown: GenerateModeSwitchesDropdown headerIcon: JSX.Element headerText: JSX.Element placeHolder: string } export const useKnowledgeAssistant: UseKnowledgeAssistant = () => { // Hooks const { data: aiAssistantSidebarData } = useAiAssistantSidebar(); const { aiAssistantData } = aiAssistantSidebarData ?? {}; const { threadData } = aiAssistantSidebarData ?? {}; const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistantData?._id); const { t } = useTranslation(); const form = useForm({ defaultValues: { input: '', summaryMode: true, extendedThinkingMode: false, }, }); // States const [currentThreadTitle, setCurrentThreadId] = useState(threadData?.title); // Functions const resetForm = useCallback(() => { const summaryMode = form.getValues('summaryMode'); const extendedThinkingMode = form.getValues('extendedThinkingMode'); form.reset({ input: '', summaryMode, extendedThinkingMode }); }, [form]); const createThread: CreateThread = useCallback(async(aiAssistantId, initialUserMessage) => { const response = await apiv3Post('/openai/thread', { type: ThreadType.KNOWLEDGE, aiAssistantId, initialUserMessage, }); const thread = response.data; setCurrentThreadId(thread.title); // No need to await because data is not used mutateThreadData(); return thread; }, [mutateThreadData]); const postMessage: PostMessage = useCallback(async(aiAssistantId, threadId, formData) => { const response = await fetch('/_api/v3/openai/message', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ aiAssistantId, threadId, userMessage: formData.input, summaryMode: form.getValues('summaryMode'), extendedThinkingMode: form.getValues('extendedThinkingMode'), }), }); return response; }, [form]); const processMessage: ProcessMessage = useCallback((data, handler) => { handleIfSuccessfullyParsed(data, SseMessageSchema, (data: SseMessage) => { handler.onMessage(data); }); }, []); // Views const headerIcon = useMemo(() => { return ai_assistant; }, []); const headerText = useMemo(() => { return <>{currentThreadTitle ?? aiAssistantData?.name}; }, [aiAssistantData?.name, currentThreadTitle]); const placeHolder = useMemo(() => { return 'sidebar_ai_assistant.knowledge_assistant_placeholder' }, []); const initialView = useMemo(() => { if (aiAssistantSidebarData?.aiAssistantData == null) { return <>; } return ( ); }, [aiAssistantSidebarData?.aiAssistantData]); const generateMessageCard: GenerateMessageCard = useCallback((role, children) => { return ( {children} ); }, []); const [dropdownOpen, setDropdownOpen] = useState(false); const toggleDropdown = useCallback(() => { setDropdownOpen(prevState => !prevState); }, []); const generateModeSwitchesDropdown: GenerateModeSwitchesDropdown = useCallback((isGenerating) => { return ( tune
help {t('sidebar_ai_assistant.summary_mode_help')}
help {t('sidebar_ai_assistant.extended_thinking_mode_help')}
); }, [dropdownOpen, toggleDropdown, form, t]); return { createThread, postMessage, processMessage, form, resetForm, // Views initialView, generateMessageCard, generateModeSwitchesDropdown, headerIcon, headerText, placeHolder, }; }; // Helper function to transform API message data to MessageLog[] const transformApiMessagesToLogs = ( apiMessageData: MessageWithCustomMetaData | null | undefined, ): MessageLog[] => { if (apiMessageData?.data == null || !Array.isArray(apiMessageData.data)) { return []; } // Define a type for the items in apiMessageData.data for clarity type ApiMessageItem = (typeof apiMessageData.data)[number]; return apiMessageData.data .slice() // Create a shallow copy before reversing .reverse() .filter((message: ApiMessageItem) => message.metadata?.shouldHideMessage !== 'true') .map((message: ApiMessageItem): MessageLog => { // Extract the first text content block, if any let messageTextContent = ''; const textContentBlock = message.content?.find(contentBlock => contentBlock.type === 'text'); if (textContentBlock != null && textContentBlock.type === 'text') { messageTextContent = textContentBlock.text.value; } return { id: message.id, // Use the actual message ID from OpenAI content: messageTextContent, isUserMessage: message.role === 'user', }; }); }; export const useFetchAndSetMessageDataEffect = ( setMessageLogs: Dispatch>, threadId?: string, ): void => { const { data: aiAssistantSidebarData } = useAiAssistantSidebar(); const { trigger: mutateMessageData } = useSWRMUTxMessages( aiAssistantSidebarData?.aiAssistantData?._id, threadId, ); useEffect(() => { if (aiAssistantSidebarData?.isEditorAssistant) { return; } if (threadId == null) { setMessageLogs([]); return; // Early return if no threadId } const fetchAndSetLogs = async() => { try { // Assuming mutateMessageData() returns a Promise const rawApiMessageData: MessageWithCustomMetaData | null | undefined = await mutateMessageData(); const fetchedLogs = transformApiMessagesToLogs(rawApiMessageData); setMessageLogs((currentLogs) => { // Preserve current logs if they represent a single, user-submitted message // AND the newly fetched logs are empty (common for new threads). const shouldPreserveCurrentMessage = currentLogs.length === 1 && currentLogs[0].isUserMessage && fetchedLogs.length === 0; // Update with fetched logs, or preserve current if applicable return shouldPreserveCurrentMessage ? currentLogs : fetchedLogs; }); } catch (error) { // console.error('Failed to fetch or process message data:', error); // Optional: for debugging setMessageLogs([]); // Clear logs on error to avoid inconsistent state } }; fetchAndSetLogs(); }, [threadId, mutateMessageData, setMessageLogs]); // Dependencies };