forked from Tadrisino/MathApp_frontend
- the parents section is gone: no parent login, invite codes or "I'm a parent" button; "گزارشِ والدین" in the profile shows the week against the previous one (praise line, six tiles, minutes-per-day and right/wrong-per-day charts, a table of the numbers) - birth date as day / month / year selects (Jalali, leap Esfand handled) instead of age; adults (19+) are asked which grade's math they want to learn - user ID under the name, copyable - "پرداختها و فاکتورها": every order with invoice number, discount, amount and reference code - tutor: when the free daily quota runs out, a button to the subscription page instead of "retry" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
3.2 KiB
TypeScript
91 lines
3.2 KiB
TypeScript
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<AskState>) {
|
|
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<ChatMessage, 'localId'>): number {
|
|
const localId = nextLocalId++;
|
|
emit({ messages: [...state.messages, { ...message, localId }] });
|
|
return localId;
|
|
},
|
|
update(localId: number, patch: Partial<ChatMessage>) {
|
|
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<ChatMessage, 'localId'>[]) {
|
|
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);
|
|
}
|