Files
STCourses/packages/feature-ai/src/web/components.tsx
T

78 lines
3.1 KiB
TypeScript

import { Fragment, type ReactNode } from "react";
import "./ai.css";
/** Shown whenever the learner has switched the course's mentor rules off. */
export function MentorBanner({ on, available }: { on: boolean; available: boolean }) {
if (on || !available) return null;
return <p className="ai-banner" role="status">Mentor mode off: I'll give full answers</p>;
}
export function Field({ label, hint, children }: { label: string; hint?: string; children: ReactNode }) {
return (
<label className="ai-field">
<span className="ai-label">{label}</span>
{children}
{hint ? <span className="ai-hint">{hint}</span> : null}
</label>
);
}
export function PageHead({ chap, title, children }: { chap?: ReactNode; title: string; children?: ReactNode }) {
return (
<header className="ai-head">
{chap ? <p className="ai-chap">{chap}</p> : null}
<h1 className="ai-title">{title}</h1>
{children ? <div className="ai-lede">{children}</div> : null}
</header>
);
}
function inline(text: string, key: string): ReactNode[] {
const out: ReactNode[] = [];
const re = /(`[^`]+`|\*\*[^*]+\*\*)/g;
let last = 0;
let m: RegExpExecArray | null;
let i = 0;
while ((m = re.exec(text))) {
if (m.index > last) out.push(text.slice(last, m.index));
const tok = m[0];
out.push(tok.startsWith("`") ? <code key={`${key}-${i++}`}>{tok.slice(1, -1)}</code> : <strong key={`${key}-${i++}`}>{tok.slice(2, -2)}</strong>);
last = m.index + tok.length;
}
if (last < text.length) out.push(text.slice(last));
return out;
}
/** A small, safe Markdown subset for assistant answers: paragraphs, lists, headings, fenced code, `code`, **bold**. */
export function Markdown({ text }: { text: string }) {
const blocks: ReactNode[] = [];
const lines = text.replace(/\r\n/g, "\n").split("\n");
let i = 0;
let k = 0;
while (i < lines.length) {
const line = lines[i]!;
if (/^\s*```/.test(line)) {
const code: string[] = [];
i++;
while (i < lines.length && !/^\s*```/.test(lines[i]!)) code.push(lines[i++]!);
i++;
blocks.push(<pre className="code" key={k++}>{code.join("\n")}</pre>);
} else if (/^\s*([-*]|\d+\.)\s+/.test(line)) {
const ordered = /^\s*\d+\./.test(line);
const items: string[] = [];
while (i < lines.length && /^\s*([-*]|\d+\.)\s+/.test(lines[i]!)) items.push(lines[i++]!.replace(/^\s*([-*]|\d+\.)\s+/, ""));
const kids = items.map((it, j) => <li key={j}>{inline(it, `${k}-${j}`)}</li>);
blocks.push(ordered ? <ol key={k++}>{kids}</ol> : <ul key={k++}>{kids}</ul>);
} else if (/^#{1,6}\s/.test(line)) {
blocks.push(<p className="ai-md-h" key={k++}>{inline(line.replace(/^#+\s*/, ""), String(k))}</p>);
i++;
} else if (!line.trim()) i++;
else {
const para: string[] = [];
while (i < lines.length && lines[i]!.trim() && !/^\s*(```|[-*]\s|\d+\.\s|#{1,6}\s)/.test(lines[i]!)) para.push(lines[i++]!);
blocks.push(<p key={k++}>{para.map((p, j) => <Fragment key={j}>{j ? <br /> : null}{inline(p, `${k}-${j}`)}</Fragment>)}</p>);
}
}
return <div className="ai-md">{blocks}</div>;
}