import { useSyncExternalStore } from 'react'; import type { Diagram, UsedChunk } from '@/api'; import { KEYS, store } from '@/lib/storage'; /** * گفتگوی دستیار، بیرون از کامپوننت تا با عوض کردنِ تب پاک نشود. * شناسه‌ی گفتگو در localStorage می‌ماند و بعد از بستنِ اپ تاریخچه از سرور برمی‌گردد. */ export interface ChatMessage { localId: number; role: 'user' | 'assistant'; text: string; messageId?: string; usedChunks?: UsedChunk[]; followUps?: string[]; /** شکلِ پاسخ؛ اپ آن را خودش به SVG تبدیل می‌کند (components/Diagram). */ diagram?: Diagram | null; feedback?: 'up' | 'down'; /** پیامِ خطا به‌جای پاسخ؛ با «دوباره» همان سوال دوباره فرستاده می‌شود. */ failedQuestion?: string; /** سهمیه‌ی رایگانِ امروز تمام شده؛ به‌جای «دوباره»، راهِ اشتراک نشان داده می‌شود. */ quotaReached?: boolean; } interface AskState { messages: ChatMessage[]; conversationId: string | null; /** درسی که گفتگو درباره‌ی آن است؛ عوض شدنش یعنی گفتگوی تازه. */ lessonId: number | null; historyLoaded: boolean; } let state: AskState = { messages: [], conversationId: store.get(KEYS.conversationId), lessonId: null, historyLoaded: false, }; let nextLocalId = 1; const listeners = new Set<() => void>(); function emit(next: Partial) { state = { ...state, ...next }; if ('conversationId' in next) { if (state.conversationId) store.set(KEYS.conversationId, state.conversationId); else store.remove(KEYS.conversationId); } listeners.forEach((l) => l()); } export const askStore = { get: () => state, subscribe(listener: () => void) { listeners.add(listener); return () => { listeners.delete(listener); }; }, add(message: Omit): number { const localId = nextLocalId++; emit({ messages: [...state.messages, { ...message, localId }] }); return localId; }, update(localId: number, patch: Partial) { emit({ messages: state.messages.map((m) => (m.localId === localId ? { ...m, ...patch } : m)) }); }, remove(localId: number) { emit({ messages: state.messages.filter((m) => m.localId !== localId) }); }, setConversation(conversationId: string | null, lessonId: number | null = state.lessonId) { emit({ conversationId, lessonId }); }, setHistory(messages: Omit[]) { emit({ messages: messages.map((m) => ({ ...m, localId: nextLocalId++ })), historyLoaded: true }); }, markHistoryLoaded() { emit({ historyLoaded: true }); }, reset(lessonId: number | null = null) { emit({ messages: [], conversationId: null, lessonId, historyLoaded: true }); }, /** بعد از خروج از حساب، گفتگوی کاربرِ قبلی نباید بماند. */ clear() { emit({ messages: [], conversationId: null, lessonId: null, historyLoaded: false }); }, }; export function useAskState(): AskState { return useSyncExternalStore(askStore.subscribe, askStore.get); }