78 lines
3.1 KiB
TypeScript
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>;
|
|
}
|