Initial commit
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function LabContent({ className, html }: LabContentProps) {
|
||||
const containerRef = useRef<HTMLElement>(null);
|
||||
const [zoomedImage, setZoomedImage] = useState<ZoomedImageState | null>(null);
|
||||
|
||||
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 navigator.clipboard.writeText(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}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
{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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user