question packs page, free/purchased counters in the tutor, packs from the no-hearts page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 11:41:43 +03:30
parent 44871894e0
commit b860c77b91
7 changed files with 147 additions and 90 deletions

View File

@@ -21,7 +21,11 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
const chat = useAskState();
const [input, setInput] = useState('');
const [busy, setBusy] = useState(false);
const [quota, setQuota] = useState<number | null>(null);
// «X سوالِ رایگانِ امروز + Y سوالِ خریده‌شده»؛ پیش از اولین سوال از سرور، بعد از هر جواب از خودِ جواب.
const [quota, setQuota] = useState<{ free: number; credits: number } | null>(null);
useEffect(() => {
api.ai.quota().then((q) => setQuota({ free: q.remainingQuota, credits: q.credits })).catch(() => {});
}, []);
const listEnd = useRef<HTMLDivElement>(null);
const autoSent = useRef(false);
@@ -91,7 +95,7 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
diagram: done.diagram ?? null,
});
askStore.setConversation(done.conversationId);
setQuota(done.remainingQuota);
setQuota((q) => ({ free: done.remainingQuota, credits: done.credits ?? q?.credits ?? 0 }));
} catch (err) {
// سقفِ روزانه (نه throttleِ چندثانیه‌ای): «دوباره» تا فردا بی‌فایده است.
const quotaReached = err instanceof ApiError && err.isRateLimited && err.detail.includes('سوال');
@@ -99,7 +103,7 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
text: friendlyError(err, 'جوابی نرسید'),
...(quotaReached ? { quotaReached: true } : { failedQuestion: question }),
});
if (quotaReached) setQuota(0);
if (quotaReached) setQuota({ free: 0, credits: 0 });
} finally {
setBusy(false);
}
@@ -194,7 +198,7 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
<Send size={16} className="-scale-x-100" />
</button>
</div>
{quota !== null && <p className="mt-1.5 text-center text-[10px] text-muted">امروز {toFa(quota)} سوالِ دیگه می‌تونی بپرسی</p>}
{quota !== null && <QuotaLine free={quota.free} credits={quota.credits} />}
</form>
</Screen>
);
@@ -293,7 +297,7 @@ function Message({
<div className="flex flex-wrap items-center gap-2 pr-10">
{message.quotaReached ? (
<button type="button" onClick={() => nav.switchTab('subscription')} className="rounded-full bg-violet px-3 py-1 text-xs font-bold text-white">
دیدنِ اشتراک‌ها
خریدِ بسته‌ی سوال
</button>
) : message.failedQuestion ? (
<button type="button" disabled={busy} onClick={() => onRetry(message)} className="flex items-center gap-1 rounded-full bg-white px-3 py-1 text-xs font-bold text-coral ring-1 ring-line disabled:opacity-50">
@@ -331,3 +335,22 @@ function Message({
</div>
);
}
/** شمارنده‌ی زیرِ جعبه‌ی پرسش: رایگانِ امروز و اعتبارِ خریده‌شده، هر دو جدا و روشن. */
function QuotaLine({ free, credits }: { free: number; credits: number }) {
const nav = useNav();
return (
<div className="mt-1.5 flex items-center justify-center gap-2 text-[11px]">
<span className={`rounded-full px-2.5 py-0.5 font-bold ${free > 0 ? 'bg-[#EAF8F0] text-[#2E7D5B]' : 'bg-soft text-muted'}`}>
رایگانِ امروز: {toFa(free)}
</span>
<button
type="button"
onClick={() => nav.switchTab('subscription')}
className={`rounded-full px-2.5 py-0.5 font-bold ${credits > 0 ? 'bg-[#F5F1FF] text-violet' : 'bg-soft text-muted'}`}
>
سوالِ خریده‌شده: {toFa(credits)}{credits === 0 && ' · خرید'}
</button>
</div>
);
}