378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
"use client";
|
||
|
||
import { Fragment, useEffect, useRef, useState } from "react";
|
||
import { Lab1ConfidenceChat } from "~/components/labs/Lab1ConfidenceChat";
|
||
import { Lab1NetronPanel } from "~/components/labs/Lab1NetronPanel";
|
||
import { Lab3TerminalFrame } from "~/components/labs/Lab3TerminalFrame";
|
||
import { Objective5Chat } from "~/components/labs/Objective5Chat";
|
||
import { QuantizationGridExplorer } from "~/components/labs/QuantizationGridExplorer";
|
||
import { QuantizationExplorer } from "~/components/labs/QuantizationExplorer";
|
||
import { TokenizerPlaygroundEmbed } from "~/components/labs/TokenizerPlaygroundEmbed";
|
||
|
||
type LabContentProps = {
|
||
className: string;
|
||
html: string;
|
||
};
|
||
|
||
const cliLanguagePattern =
|
||
/\b(language-(bash|sh|shell|zsh|console|terminal)|bash|shell|zsh)\b/i;
|
||
const cliCommandPattern =
|
||
/(^|\n)\s*(\$|sudo\s|git\s|python3?\s|pip\s|npm\s|pnpm\s|yarn\s|llama-|ollama\s|curl\s|wget\s|apt\s|cd\s|ls\s|cat\s|cp\s|mv\s|chmod\s|make\s)/i;
|
||
const promptLanguagePattern =
|
||
/\b(language-(text|plaintext|md|markdown)|text|plaintext|markdown)\b/i;
|
||
const promptSignalPattern =
|
||
/\b(you are|guidelines|follow these|example|when provided|system prompt|tasked with)\b/i;
|
||
|
||
type ParsedSetting = {
|
||
key: string;
|
||
value: string;
|
||
};
|
||
|
||
type ZoomedImageState = {
|
||
src: string;
|
||
alt: string;
|
||
};
|
||
|
||
const quantizationExplorerToken = "<div data-quantization-explorer></div>";
|
||
const quantizationGridExplorerToken =
|
||
"<div data-quantization-grid-explorer></div>";
|
||
const objective5ChatToken = "<div data-objective5-chat></div>";
|
||
const lab3TerminalToken = "<div data-lab3-terminal></div>";
|
||
const lab1ConfidenceToken = "<div data-lab1-confidence></div>";
|
||
const lab1NetronToken = "<div data-lab1-netron-panel></div>";
|
||
const tokenizerPlaygroundToken = "<div data-tokenizer-playground></div>";
|
||
|
||
function looksLikeCliCommand(commandText: string, className: string) {
|
||
if (cliLanguagePattern.test(className)) return true;
|
||
return (
|
||
cliCommandPattern.test(commandText) || /--[a-z0-9-]+/i.test(commandText)
|
||
);
|
||
}
|
||
|
||
function looksLikePromptTextBlock(text: string, className: string) {
|
||
if (looksLikeCliCommand(text, className)) return false;
|
||
|
||
const normalizedText = text.trim();
|
||
if (!normalizedText) return false;
|
||
|
||
const lineCount = normalizedText.split("\n").length;
|
||
if (promptLanguagePattern.test(className) && normalizedText.length > 80)
|
||
return true;
|
||
if (lineCount >= 4 && promptSignalPattern.test(normalizedText)) return true;
|
||
if (lineCount >= 6 && /(^|\n)\s*[*-]\s+/.test(normalizedText)) return true;
|
||
return false;
|
||
}
|
||
|
||
function escapeRegex(value: string) {
|
||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
}
|
||
|
||
function escapeHtml(value: string) {
|
||
return value
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
function parseSettingListItem(item: HTMLLIElement): ParsedSetting | null {
|
||
const keyElement = item.querySelector("code");
|
||
if (!keyElement) return null;
|
||
|
||
const key = (keyElement.textContent ?? "").replace(/\s+/g, " ").trim();
|
||
if (!key || key.length > 40) return null;
|
||
|
||
const text = (item.textContent ?? "").replace(/\s+/g, " ").trim();
|
||
const match = new RegExp(
|
||
`^${escapeRegex(key)}\\s*(?:-|–|—|:|=)\\s*(.+)$`,
|
||
).exec(text);
|
||
if (!match) return null;
|
||
|
||
const value = (match[1] ?? "").replace(/\s+/g, " ").trim();
|
||
if (!value || value.length > 36) return null;
|
||
if (/[.;]/.test(value) && value.length > 16) return null;
|
||
|
||
return { key, value };
|
||
}
|
||
|
||
function enhanceSettingsLists(root: HTMLElement) {
|
||
const lists = root.querySelectorAll<HTMLUListElement>("ul");
|
||
|
||
for (const list of lists) {
|
||
if (list.dataset.settingsEnhanced === "true") continue;
|
||
|
||
const items = Array.from(list.children).filter(
|
||
(node): node is HTMLLIElement => {
|
||
return node.tagName === "LI";
|
||
},
|
||
);
|
||
if (items.length < 2) continue;
|
||
|
||
const parsedItems = items.map((item) => parseSettingListItem(item));
|
||
if (parsedItems.some((parsedItem) => parsedItem === null)) continue;
|
||
|
||
const settings = parsedItems as ParsedSetting[];
|
||
const compactValueCount = settings.filter(
|
||
(setting) => setting.value.length <= 20,
|
||
).length;
|
||
if (compactValueCount < Math.max(2, Math.ceil(settings.length * 0.66)))
|
||
continue;
|
||
|
||
list.dataset.settingsEnhanced = "true";
|
||
list.classList.add("lab-settings-list");
|
||
|
||
for (let i = 0; i < items.length; i++) {
|
||
const item = items[i];
|
||
const setting = settings[i];
|
||
if (!item || !setting) continue;
|
||
|
||
item.classList.add("lab-settings-item");
|
||
item.innerHTML =
|
||
`<span class="lab-setting-key">${escapeHtml(setting.key)}</span>` +
|
||
`<span class="lab-setting-value">${escapeHtml(setting.value)}</span>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
function ensureCopyButton(pre: HTMLPreElement, label: string) {
|
||
if (pre.dataset.copyEnhanced === "true") return;
|
||
|
||
pre.dataset.copyEnhanced = "true";
|
||
const copyButton = document.createElement("button");
|
||
copyButton.type = "button";
|
||
copyButton.className = "lab-copy-button";
|
||
copyButton.textContent = label;
|
||
copyButton.dataset.defaultLabel = label;
|
||
copyButton.setAttribute("aria-label", "Copy block to clipboard");
|
||
pre.appendChild(copyButton);
|
||
}
|
||
|
||
async function copyTextToClipboard(text: string) {
|
||
if (window.isSecureContext && navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(text);
|
||
return;
|
||
}
|
||
|
||
const activeElement =
|
||
document.activeElement instanceof HTMLElement
|
||
? document.activeElement
|
||
: null;
|
||
const selection = document.getSelection();
|
||
const previousRanges =
|
||
selection && selection.rangeCount > 0
|
||
? Array.from({ length: selection.rangeCount }, (_, index) =>
|
||
selection.getRangeAt(index).cloneRange(),
|
||
)
|
||
: [];
|
||
|
||
const textarea = document.createElement("textarea");
|
||
textarea.value = text;
|
||
textarea.setAttribute("readonly", "");
|
||
textarea.setAttribute("aria-hidden", "true");
|
||
textarea.style.position = "fixed";
|
||
textarea.style.top = "0";
|
||
textarea.style.left = "-9999px";
|
||
textarea.style.opacity = "0";
|
||
textarea.style.pointerEvents = "none";
|
||
|
||
document.body.appendChild(textarea);
|
||
textarea.focus();
|
||
textarea.select();
|
||
textarea.setSelectionRange(0, textarea.value.length);
|
||
|
||
try {
|
||
const didCopy = document.execCommand("copy");
|
||
if (!didCopy) {
|
||
throw new Error("Copy command was rejected");
|
||
}
|
||
} finally {
|
||
document.body.removeChild(textarea);
|
||
if (selection) {
|
||
selection.removeAllRanges();
|
||
for (const range of previousRanges) {
|
||
selection.addRange(range);
|
||
}
|
||
}
|
||
activeElement?.focus();
|
||
}
|
||
}
|
||
|
||
export function LabContent({ className, html }: LabContentProps) {
|
||
const containerRef = useRef<HTMLElement>(null);
|
||
const [zoomedImage, setZoomedImage] = useState<ZoomedImageState | null>(null);
|
||
|
||
const renderedContent = html
|
||
.split(
|
||
new RegExp(
|
||
`(${escapeRegex(quantizationExplorerToken)}|${escapeRegex(quantizationGridExplorerToken)}|${escapeRegex(objective5ChatToken)}|${escapeRegex(lab3TerminalToken)}|${escapeRegex(lab1ConfidenceToken)}|${escapeRegex(lab1NetronToken)}|${escapeRegex(tokenizerPlaygroundToken)})`,
|
||
"g",
|
||
),
|
||
)
|
||
.filter(Boolean)
|
||
.map((part, index) => {
|
||
if (part === quantizationExplorerToken) {
|
||
return <QuantizationExplorer key={`quantization-explorer-${index}`} />;
|
||
}
|
||
|
||
if (part === quantizationGridExplorerToken) {
|
||
return (
|
||
<QuantizationGridExplorer
|
||
key={`quantization-grid-explorer-${index}`}
|
||
/>
|
||
);
|
||
}
|
||
|
||
if (part === objective5ChatToken) {
|
||
return <Objective5Chat key={`objective5-chat-${index}`} />;
|
||
}
|
||
|
||
if (part === lab3TerminalToken) {
|
||
return <Lab3TerminalFrame key={`lab3-terminal-${index}`} />;
|
||
}
|
||
|
||
if (part === lab1ConfidenceToken) {
|
||
return <Lab1ConfidenceChat key={`lab1-confidence-${index}`} />;
|
||
}
|
||
|
||
if (part === lab1NetronToken) {
|
||
return <Lab1NetronPanel key={`lab1-netron-${index}`} />;
|
||
}
|
||
|
||
if (part === tokenizerPlaygroundToken) {
|
||
return <TokenizerPlaygroundEmbed key={`tokenizer-playground-${index}`} />;
|
||
}
|
||
|
||
return (
|
||
<Fragment key={`html-segment-${index}`}>
|
||
<div dangerouslySetInnerHTML={{ __html: part }} />
|
||
</Fragment>
|
||
);
|
||
});
|
||
|
||
useEffect(() => {
|
||
const root = containerRef.current;
|
||
if (!root) return;
|
||
|
||
const preBlocks = root.querySelectorAll<HTMLPreElement>("pre");
|
||
for (const pre of preBlocks) {
|
||
const code = pre.querySelector<HTMLElement>("code");
|
||
if (!code) continue;
|
||
|
||
const blockText = code.textContent ?? "";
|
||
if (looksLikeCliCommand(blockText, code.className)) {
|
||
pre.classList.add("lab-cli-shell");
|
||
ensureCopyButton(pre, "Copy");
|
||
continue;
|
||
}
|
||
|
||
if (looksLikePromptTextBlock(blockText, code.className)) {
|
||
pre.classList.add("lab-prompt-card");
|
||
ensureCopyButton(pre, "Copy Text");
|
||
}
|
||
}
|
||
|
||
enhanceSettingsLists(root);
|
||
|
||
const handleRootClick = (event: Event) => {
|
||
const target = event.target as HTMLElement;
|
||
const button = target.closest<HTMLButtonElement>(
|
||
"button.lab-copy-button",
|
||
);
|
||
if (button) {
|
||
const pre = button.closest("pre");
|
||
const code = pre?.querySelector("code");
|
||
const commandText = code?.textContent?.trimEnd();
|
||
if (!commandText) return;
|
||
const defaultLabel = button.dataset.defaultLabel ?? "Copy";
|
||
|
||
void copyTextToClipboard(commandText)
|
||
.then(() => {
|
||
button.textContent = "Copied";
|
||
button.classList.add("is-copied");
|
||
window.setTimeout(() => {
|
||
button.textContent = defaultLabel;
|
||
button.classList.remove("is-copied");
|
||
}, 1200);
|
||
})
|
||
.catch(() => {
|
||
button.textContent = "Failed";
|
||
window.setTimeout(() => {
|
||
button.textContent = defaultLabel;
|
||
}, 1200);
|
||
});
|
||
return;
|
||
}
|
||
|
||
const image = target.closest<HTMLImageElement>("img");
|
||
if (!image || !root.contains(image)) return;
|
||
|
||
const src = image.getAttribute("src");
|
||
if (!src) return;
|
||
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
setZoomedImage({
|
||
src,
|
||
alt: image.getAttribute("alt") ?? "",
|
||
});
|
||
};
|
||
|
||
root.addEventListener("click", handleRootClick);
|
||
return () => {
|
||
root.removeEventListener("click", handleRootClick);
|
||
};
|
||
}, [html]);
|
||
|
||
useEffect(() => {
|
||
if (!zoomedImage) return;
|
||
|
||
const previousOverflow = document.body.style.overflow;
|
||
document.body.style.overflow = "hidden";
|
||
|
||
const activeElement = document.activeElement;
|
||
const previousFocusedElement =
|
||
activeElement instanceof HTMLElement ? activeElement : null;
|
||
|
||
const handleEscape = (event: KeyboardEvent) => {
|
||
if (event.key === "Escape") {
|
||
setZoomedImage(null);
|
||
}
|
||
};
|
||
|
||
window.addEventListener("keydown", handleEscape);
|
||
return () => {
|
||
window.removeEventListener("keydown", handleEscape);
|
||
document.body.style.overflow = previousOverflow;
|
||
previousFocusedElement?.focus();
|
||
};
|
||
}, [zoomedImage]);
|
||
|
||
return (
|
||
<>
|
||
<article ref={containerRef} className={className}>
|
||
{renderedContent}
|
||
</article>
|
||
{zoomedImage ? (
|
||
<div
|
||
className="lab-image-modal"
|
||
role="presentation"
|
||
onClick={() => setZoomedImage(null)}
|
||
>
|
||
<div
|
||
className="lab-image-modal__surface"
|
||
onClick={(event) => event.stopPropagation()}
|
||
>
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img
|
||
className="lab-image-modal__image"
|
||
src={zoomedImage.src}
|
||
alt={zoomedImage.alt}
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</>
|
||
);
|
||
}
|