Add runtime service links for lab endpoints
This commit is contained in:
@@ -9,6 +9,14 @@ import { Objective5Chat } from "~/components/labs/Objective5Chat";
|
||||
import { QuantizationGridExplorer } from "~/components/labs/QuantizationGridExplorer";
|
||||
import { QuantizationExplorer } from "~/components/labs/QuantizationExplorer";
|
||||
import { TokenizerPlaygroundEmbed } from "~/components/labs/TokenizerPlaygroundEmbed";
|
||||
import {
|
||||
fetchCoursewareRuntimeConfig,
|
||||
isCoursewareServiceId,
|
||||
normalizeCoursewareRuntimeConfig,
|
||||
resolveCoursewareServiceAddress,
|
||||
resolveCoursewareServiceUrl,
|
||||
type ResolvedCoursewareRuntimeConfig,
|
||||
} from "~/lib/courseware-runtime";
|
||||
|
||||
type LabContentProps = {
|
||||
className: string;
|
||||
@@ -43,6 +51,8 @@ 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>";
|
||||
const serviceTokenPattern =
|
||||
/\{\{service-(url|address):([a-z0-9-]+)(?::([^}]+))?\}\}/g;
|
||||
|
||||
function looksLikeCliCommand(commandText: string, className: string) {
|
||||
if (cliLanguagePattern.test(className)) return true;
|
||||
@@ -258,9 +268,150 @@ function enhanceHarnessSelectors(root: HTMLElement) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveServiceTokenValue(
|
||||
runtimeConfig: ResolvedCoursewareRuntimeConfig,
|
||||
tokenType: string,
|
||||
serviceId: string,
|
||||
pathSuffix?: string,
|
||||
) {
|
||||
if (!isCoursewareServiceId(serviceId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tokenType === "url") {
|
||||
return resolveCoursewareServiceUrl(runtimeConfig, serviceId, pathSuffix);
|
||||
}
|
||||
|
||||
if (tokenType === "address") {
|
||||
return resolveCoursewareServiceAddress(runtimeConfig, serviceId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function replaceServiceTokens(
|
||||
root: HTMLElement,
|
||||
runtimeConfig: ResolvedCoursewareRuntimeConfig,
|
||||
) {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode(node) {
|
||||
if (!(node instanceof Text)) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
|
||||
if (!node.nodeValue?.includes("{{service-")) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
|
||||
const parent = node.parentElement;
|
||||
if (!parent) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
|
||||
if (parent.closest("[data-widget-enhanced='true']")) {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
},
|
||||
});
|
||||
|
||||
const textNodes: Text[] = [];
|
||||
let currentNode = walker.nextNode();
|
||||
while (currentNode) {
|
||||
if (currentNode instanceof Text) {
|
||||
textNodes.push(currentNode);
|
||||
}
|
||||
currentNode = walker.nextNode();
|
||||
}
|
||||
|
||||
for (const textNode of textNodes) {
|
||||
const parent = textNode.parentElement;
|
||||
const nodeValue = textNode.nodeValue;
|
||||
if (!parent || !nodeValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const allowLinks = !parent.closest("code, pre, a");
|
||||
const nextTextValue = nodeValue.replace(
|
||||
serviceTokenPattern,
|
||||
(fullMatch, tokenType: string, serviceId: string, pathSuffix?: string) => {
|
||||
const replacement = resolveServiceTokenValue(
|
||||
runtimeConfig,
|
||||
tokenType,
|
||||
serviceId,
|
||||
pathSuffix,
|
||||
);
|
||||
return replacement ?? fullMatch;
|
||||
},
|
||||
);
|
||||
|
||||
if (!allowLinks) {
|
||||
if (nextTextValue !== nodeValue) {
|
||||
textNode.nodeValue = nextTextValue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
serviceTokenPattern.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let didReplace = false;
|
||||
const fragment = document.createDocumentFragment();
|
||||
let match = serviceTokenPattern.exec(nodeValue);
|
||||
|
||||
while (match) {
|
||||
const [fullMatch, tokenType, serviceId, pathSuffix] = match;
|
||||
const replacement = resolveServiceTokenValue(
|
||||
runtimeConfig,
|
||||
tokenType,
|
||||
serviceId,
|
||||
pathSuffix,
|
||||
);
|
||||
|
||||
if (replacement === null) {
|
||||
match = serviceTokenPattern.exec(nodeValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
didReplace = true;
|
||||
if (match.index > lastIndex) {
|
||||
fragment.append(nodeValue.slice(lastIndex, match.index));
|
||||
}
|
||||
|
||||
if (tokenType === "url") {
|
||||
const link = document.createElement("a");
|
||||
link.href = replacement;
|
||||
link.rel = "noreferrer";
|
||||
link.target = "_blank";
|
||||
link.textContent = replacement;
|
||||
fragment.append(link);
|
||||
} else {
|
||||
fragment.append(replacement);
|
||||
}
|
||||
|
||||
lastIndex = match.index + fullMatch.length;
|
||||
match = serviceTokenPattern.exec(nodeValue);
|
||||
}
|
||||
|
||||
if (!didReplace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastIndex < nodeValue.length) {
|
||||
fragment.append(nodeValue.slice(lastIndex));
|
||||
}
|
||||
|
||||
textNode.replaceWith(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
export function LabContent({ className, html }: LabContentProps) {
|
||||
const containerRef = useRef<HTMLElement>(null);
|
||||
const [zoomedImage, setZoomedImage] = useState<ZoomedImageState | null>(null);
|
||||
const [runtimeConfig, setRuntimeConfig] = useState(() =>
|
||||
normalizeCoursewareRuntimeConfig(),
|
||||
);
|
||||
const [isRuntimeResolved, setIsRuntimeResolved] = useState(false);
|
||||
|
||||
const renderedContent = html
|
||||
.split(
|
||||
@@ -314,9 +465,33 @@ export function LabContent({ className, html }: LabContentProps) {
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
void fetchCoursewareRuntimeConfig()
|
||||
.then((nextRuntimeConfig) => {
|
||||
if (isCancelled) return;
|
||||
setRuntimeConfig(nextRuntimeConfig);
|
||||
})
|
||||
.catch(() => {
|
||||
if (isCancelled) return;
|
||||
setRuntimeConfig(normalizeCoursewareRuntimeConfig());
|
||||
})
|
||||
.finally(() => {
|
||||
if (isCancelled) return;
|
||||
setIsRuntimeResolved(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const root = containerRef.current;
|
||||
if (!root) return;
|
||||
if (!root || !isRuntimeResolved) return;
|
||||
|
||||
replaceServiceTokens(root, runtimeConfig);
|
||||
|
||||
const preBlocks = root.querySelectorAll<HTMLPreElement>("pre");
|
||||
for (const pre of preBlocks) {
|
||||
@@ -388,7 +563,7 @@ export function LabContent({ className, html }: LabContentProps) {
|
||||
cleanupHarnessSelectors();
|
||||
root.removeEventListener("click", handleRootClick);
|
||||
};
|
||||
}, [html]);
|
||||
}, [html, isRuntimeResolved, runtimeConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!zoomedImage) return;
|
||||
|
||||
Reference in New Issue
Block a user