The selected companion everywhere, each with a small trait; next-APK notes
- state/companion.ts: one store for the chosen companion (also in localStorage so splash/login show it).
BrandMark (the orange «π» tile) now shows the companion's symbol (Σ Δ π ω) in its colour; screen-header
mascots, loaders and empty/error states default to it; the assistant is «از {name} بپرس» with its face,
colour and wording; the shop, lesson and quiz «ask» buttons name it; the active tab takes its tint.
- Traits, all client-side and light: Σ sigma keeps a running tally of correct answers in the quiz header;
Δ delta shows a flame «×N» chip and an extra buzz for answers in a row; π pi gives a short solving tip
after a wrong answer; ω omega throws gold confetti at the end of every island whatever the score.
Each companion's «correct» sound is pitched differently; confetti uses its colours. Characters screen
lists each trait.
- android/README.md: what MainActivity.kt has that the installed APK lacks (vibrate bridge, media gesture,
in-app gateway + paysInApp), what to check on a phone after building, signing owner, Bazaar policy note.
Checked in headless Chrome with Sigma, Delta and Omega selected (home, assistant, shop, characters, quiz).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,14 @@ import { MathText } from '@/lib/MathText';
|
||||
import { friendlyError } from '@/lib/errors';
|
||||
import { toFa } from '@/lib/format';
|
||||
import { useGateHold } from '@/lib/useAsync';
|
||||
import { iAm, useCompanion } from '@/state/companion';
|
||||
|
||||
const STARTERS = ['این فصل دربارهی چیه؟', 'یه مثالِ ساده برام حل کن', 'چطور برای امتحان تمرین کنم؟'];
|
||||
|
||||
/** «از پی بپرس»: دستیارِ ریاضی که از روی کتابِ پایهی کاربر جواب میدهد. */
|
||||
/** «از {همراه} بپرس»: دستیارِ ریاضی که از روی کتابِ پایهی کاربر جواب میدهد؛ چهره و نامش همراهِ انتخابیِ کاربر است. */
|
||||
export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number; questionId?: string; prompt?: string }) {
|
||||
const { profile } = useAppData();
|
||||
const companion = useCompanion();
|
||||
const chat = useAskState();
|
||||
const [input, setInput] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -132,11 +134,11 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
|
||||
return (
|
||||
<Screen>
|
||||
<ScreenHeader
|
||||
title="از پی بپرس"
|
||||
title={`از ${companion.name} بپرس`}
|
||||
subtitle={chat.lessonId ? 'از روی کتابِ همین درس جواب میدم' : 'دستیارِ ریاضی، از روی کتابِ خودت'}
|
||||
color="#9B7BFF"
|
||||
color={companion.color}
|
||||
back={lessonId !== undefined || prompt !== undefined}
|
||||
mascot={{ char: 'pi', mood: busy ? 'excited' : 'idle', size: 60 }}
|
||||
mascot={{ mood: busy ? 'excited' : 'idle', size: 60 }}
|
||||
>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{quota !== null && <QuotaPills free={quota.free} credits={quota.credits} />}
|
||||
@@ -155,7 +157,7 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
|
||||
|
||||
<div className="flex flex-1 flex-col gap-3 overflow-y-auto px-4 py-4">
|
||||
<Bubble role="assistant">
|
||||
سلام {profile.name || 'دوستِ من'}! من پیام 🙂 هر سوالی از ریاضیِ کتابت داری بپرس؛ قدمبهقدم با هم حلش میکنیم.
|
||||
سلام {profile.name || 'دوستِ من'}! {`من ${iAm(companion.name)}`} 🙂 هر سوالی از ریاضیِ کتابت داری بپرس؛ قدمبهقدم با هم حلش میکنیم.
|
||||
</Bubble>
|
||||
|
||||
{empty && chat.historyLoaded && (
|
||||
@@ -216,11 +218,12 @@ export function AskScreen({ lessonId, questionId, prompt }: { lessonId?: number;
|
||||
|
||||
function Bubble({ role, children, extra }: { role: 'user' | 'assistant'; children: React.ReactNode; extra?: React.ReactNode }) {
|
||||
const mine = role === 'user';
|
||||
const companion = useCompanion().char;
|
||||
return (
|
||||
<div className={`flex gap-2 ${mine ? 'flex-row-reverse' : ''}`}>
|
||||
{!mine && (
|
||||
<span className="flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-full bg-[#f5f1ff]">
|
||||
<SeyarMascot char="pi" mood="happy" size={32} />
|
||||
<SeyarMascot char={companion} mood="happy" size={32} />
|
||||
</span>
|
||||
)}
|
||||
<div
|
||||
@@ -242,8 +245,8 @@ function Bubble({ role, children, extra }: { role: 'user' | 'assistant'; childre
|
||||
* سرور وصل نیستند.
|
||||
*/
|
||||
const THINKING_STEPS: [number, string][] = [
|
||||
[0, 'پی داره تو کتابت میگرده'],
|
||||
[4, 'پی داره حلش میکنه'],
|
||||
[0, '{name} داره تو کتابت میگرده'],
|
||||
[4, '{name} داره حلش میکنه'],
|
||||
[15, 'داره قدمها رو مرتب مینویسه'],
|
||||
[28, 'جوابِ کامل یه کم طول میکشه، الان میرسه'],
|
||||
];
|
||||
@@ -255,11 +258,12 @@ function Thinking() {
|
||||
const timer = setInterval(() => setSeconds((Date.now() - started) / 1000), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
const text = [...THINKING_STEPS].reverse().find(([at]) => seconds >= at)![1];
|
||||
const companion = useCompanion();
|
||||
const text = [...THINKING_STEPS].reverse().find(([at]) => seconds >= at)![1].replace('{name}', companion.name);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-8 w-8 flex-shrink-0 items-center justify-center overflow-hidden rounded-full bg-[#f5f1ff]">
|
||||
<SeyarMascot char="pi" mood="idle" size={32} />
|
||||
<SeyarMascot char={companion.char} mood="idle" size={32} />
|
||||
</span>
|
||||
<div className="flex items-center gap-2 rounded-2xl border border-line bg-white px-4 py-3 text-xs text-muted" aria-live="polite">
|
||||
{text}
|
||||
|
||||
Reference in New Issue
Block a user