draw every kind of textbook figure, lock math text left-to-right, student-first family screen

- src/diagram: plane (constructed points, graphs with computed roots, vertex,
  extrema, tangent, asymptotes, piecewise limits, regions, rotations and
  reflections, line from ax + by = c), number line with intervals and stacked
  fractions, solids with hidden edges, bar/line/pie charts, sign tables, Venn,
  arrow mappings, probability trees, fraction bars and mixed numbers, grids,
  groups, clock. Pure-math text is bidi-overridden: Persian digits used to turn
  3 × 4 = 12 into 12 = 4 × 3 even with dir=ltr.
- MathText locks math runs inside Persian sentences in chat bubbles and figure
  captions, so -1 no longer shows as 1-.
- Family screen: a student sees only the invite-code form; the parent form is
  behind a link.
- Dev-only diagram gallery at ?gallery (not in the production build).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 23:24:50 +03:30
parent 355bae7035
commit ec4c844531
18 changed files with 2165 additions and 419 deletions

58
src/diagram/index.tsx Normal file
View File

@@ -0,0 +1,58 @@
import { Component, memo, type ReactNode } from 'react';
import type { DiagramSpec } from './types';
import { Plane } from './plane';
import { NumberLine } from './numberline';
import { Solid } from './solid';
import { Chart } from './charts';
import { Mapping, Tree, Venn } from './sets';
import { Clock, Fraction, Grid, Groups, Table } from './misc';
import { PAPER } from './svg';
import { MathText } from '@/lib/MathText';
export type { DiagramSpec } from './types';
/**
* شکلِ جوابِ دستیار. توصیف از بک‌اند می‌آید (rag/services/diagram.py، اعتبارسنجی‌شده)
* و اینجا فقط به عنصرِ React/SVG تبدیل می‌شود؛ هیچ HTMLی از مدل وارد صفحه نمی‌شود.
*
* هر خطایی در رسم (شکلی که کد پیش‌بینی نکرده) فقط همین شکل را پنهان می‌کند؛
* متنِ جواب سرِ جایش می‌ماند.
*/
function Body({ spec }: { spec: DiagramSpec }): ReactNode {
switch (spec.kind) {
case 'plane': return <Plane spec={spec} />;
case 'numberline': return <NumberLine spec={spec} />;
case 'solid': return <Solid spec={spec} />;
case 'bar': case 'linechart': case 'pie': return <Chart spec={spec} />;
case 'venn': return <Venn spec={spec} />;
case 'mapping': return <Mapping spec={spec} />;
case 'tree': return <Tree spec={spec} />;
case 'fraction': return <Fraction spec={spec} />;
case 'grid': return <Grid spec={spec} />;
case 'groups': return <Groups spec={spec} />;
case 'clock': return <Clock spec={spec} />;
case 'table': return <Table spec={spec} />;
default: return null;
}
}
class Guard extends Component<{ children: ReactNode }, { broken: boolean }> {
state = { broken: false };
static getDerivedStateFromError() { return { broken: true }; }
render() { return this.state.broken ? null : this.props.children; }
}
function DiagramBase({ spec }: { spec: DiagramSpec }) {
return (
<Guard>
{/* dir=ltr: محور و مختصات چپ‌به‌راست‌اند؛ در زمینه‌ی rtl «−۱» به «۱−» برمی‌گشت.
برچسب‌های فارسی با unicode-bidi: plaintext جهتِ خودشان را می‌گیرند. */}
<figure dir="ltr" className="mt-2 overflow-hidden rounded-2xl border border-line p-2" style={{ backgroundColor: PAPER }}>
<Body spec={spec} />
{spec.title && <figcaption dir="rtl" className="mt-1 text-center text-[11px] font-bold text-muted"><MathText text={spec.title} /></figcaption>}
</figure>
</Guard>
);
}
export const Diagram = memo(DiagramBase);