feat(ai): assistant panel, AI settings and studio generator UI
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a57222ecc3
commit
08d3e3f642
15 files changed
+973
-7
No files matched your search
@@ -0,0 +1,77 @@
|
||||
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>;
|
||||
}
|
||||
Reference in new issue
Block a user