1909 lines
81 KiB
HTML
1909 lines
81 KiB
HTML
<!doctype html>
|
||
<html lang="en-US">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="referrer" content="no-referrer">
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; style-src 'unsafe-inline' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; img-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; font-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; media-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; worker-src blob:; connect-src blob: data:; frame-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'">
|
||
<title>writing-workspace-framework.html</title>
|
||
<style>:root{color-scheme:light dark;background:light-dark(rgb(255 255 255), rgb(24 24 24))}html,body{margin:0}body{box-sizing:border-box;padding:1rem;background:inherit}iframe{display:block;width:100%;max-width:736px;height:calc(100vh - 2rem);margin:0 auto;border:0}</style>
|
||
</head>
|
||
<body>
|
||
<iframe id="codex-visualization" sandbox="allow-scripts" scrolling="no" referrerpolicy="no-referrer" title="writing-workspace-framework.html" data-srcdoc="<!doctype html>
|
||
<html lang="en-US">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="referrer" content="no-referrer">
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; style-src 'unsafe-inline' blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; img-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; font-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; media-src blob: data: https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://esm.sh https://fonts.bunny.net https://fonts.googleapis.com https://fonts.gstatic.com https://unpkg.com; worker-src blob:; connect-src blob: data:; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
|
||
<title>writing-workspace-framework.html</title>
|
||
</head>
|
||
<body>
|
||
<script>(() => {
|
||
const root = document.documentElement;
|
||
const mediaQuery = globalThis.matchMedia("(prefers-color-scheme: dark)");
|
||
const apply = Reflect.apply;
|
||
const isArray = Array.isArray;
|
||
const parentWindow = globalThis.parent;
|
||
/**
|
||
* @param {object} prototype
|
||
* @param {string} property
|
||
*/
|
||
const getPrototypeGetter = (prototype, property) => {
|
||
// oxlint-disable-next-line typescript/unbound-method -- invoked through captured Reflect.apply
|
||
const getter = Object.getOwnPropertyDescriptor(prototype, property)?.get;
|
||
if (getter == null) {
|
||
throw new Error(`Missing ${property} getter`);
|
||
}
|
||
return getter;
|
||
};
|
||
const getMessageData = getPrototypeGetter(MessageEvent.prototype, "data");
|
||
const getMessagePorts = getPrototypeGetter(MessageEvent.prototype, "ports");
|
||
const getMessageSource = getPrototypeGetter(MessageEvent.prototype, "source");
|
||
// oxlint-disable-next-line typescript/unbound-method -- captured before the untrusted fragment runs
|
||
const addHostMessageListener = MessagePort.prototype.addEventListener;
|
||
// oxlint-disable-next-line typescript/unbound-method -- captured before the untrusted fragment runs
|
||
const postHostMessage = MessagePort.prototype.postMessage;
|
||
// oxlint-disable-next-line typescript/unbound-method -- captured before the untrusted fragment runs
|
||
const startHostPort = MessagePort.prototype.start;
|
||
// oxlint-disable-next-line typescript/unbound-method -- captured before the untrusted fragment runs
|
||
const stopMessagePropagation = Event.prototype.stopImmediatePropagation;
|
||
/** @type {MessagePort | null} */
|
||
let hostPort = null;
|
||
const postToHost = (type, payload) => {
|
||
if (hostPort != null) {
|
||
apply(postHostMessage, hostPort, [{ type, ...payload }]);
|
||
}
|
||
};
|
||
const openExternal = ({ href }) => {
|
||
if (globalThis.navigator.userActivation?.isActive === true) {
|
||
postToHost("open-external", { href });
|
||
}
|
||
};
|
||
const sendFollowUpMessage = ({ prompt, title }) => {
|
||
if (globalThis.navigator.userActivation?.isActive === true) {
|
||
postToHost("follow-up", { prompt, title });
|
||
}
|
||
return Promise.resolve();
|
||
};
|
||
const syncTheme = () => {
|
||
const theme = mediaQuery.matches ? "dark" : "light";
|
||
root.dataset.theme = theme;
|
||
globalThis.openai = {
|
||
...globalThis.openai,
|
||
openExternal,
|
||
sendFollowUpMessage,
|
||
theme,
|
||
visualizationStyleVariables: {},
|
||
visualizationTheme: theme,
|
||
};
|
||
globalThis.dispatchEvent(
|
||
new CustomEvent("openai:set_globals", {
|
||
detail: { globals: globalThis.openai },
|
||
}),
|
||
);
|
||
};
|
||
const sendHeight = () => {
|
||
postToHost("height", {
|
||
height: Math.ceil(
|
||
Math.max(
|
||
document.body.scrollHeight,
|
||
document.body.getBoundingClientRect().height,
|
||
),
|
||
),
|
||
});
|
||
};
|
||
|
||
globalThis.addEventListener(
|
||
"message",
|
||
(event) => {
|
||
const data = apply(getMessageData, event, []);
|
||
const ports = apply(getMessagePorts, event, []);
|
||
const source = apply(getMessageSource, event, []);
|
||
if (
|
||
source !== parentWindow ||
|
||
typeof data !== "object" ||
|
||
data == null ||
|
||
data.type !== "codex-visualization-initialize" ||
|
||
!isArray(ports) ||
|
||
ports.length !== 1
|
||
) {
|
||
return;
|
||
}
|
||
apply(stopMessagePropagation, event, []);
|
||
const nextHostPort = ports[0];
|
||
try {
|
||
apply(addHostMessageListener, nextHostPort, [
|
||
"message",
|
||
(event) => {
|
||
const data = apply(getMessageData, event, []);
|
||
if (typeof data === "object" && data?.type === "measure") {
|
||
sendHeight();
|
||
}
|
||
},
|
||
]);
|
||
apply(startHostPort, nextHostPort, []);
|
||
} catch {
|
||
return;
|
||
}
|
||
hostPort = nextHostPort;
|
||
sendHeight();
|
||
},
|
||
{ capture: true },
|
||
);
|
||
document.currentScript?.remove();
|
||
mediaQuery.addEventListener("change", syncTheme);
|
||
globalThis.addEventListener("click", (event) => {
|
||
if (event.defaultPrevented) {
|
||
return;
|
||
}
|
||
let element = null;
|
||
if (event.target instanceof globalThis.Element) {
|
||
element = event.target;
|
||
} else if (event.target instanceof globalThis.Node) {
|
||
element = event.target.parentElement;
|
||
}
|
||
const link = element?.closest("a[href]");
|
||
const href = link?.getAttribute("href");
|
||
if (href == null) {
|
||
return;
|
||
}
|
||
if (href.startsWith("#")) {
|
||
let id;
|
||
try {
|
||
id = decodeURIComponent(href.slice(1));
|
||
} catch {
|
||
return;
|
||
}
|
||
const target = id.length === 0 ? root : document.getElementById(id);
|
||
if (
|
||
target == null ||
|
||
globalThis.navigator.userActivation?.isActive !== true
|
||
) {
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
postToHost("scroll-to", {
|
||
top: target.getBoundingClientRect().top + globalThis.scrollY,
|
||
});
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
openExternal({ href });
|
||
});
|
||
new ResizeObserver(sendHeight).observe(document.body);
|
||
syncTheme();
|
||
})();
|
||
</script>
|
||
<style>:root {
|
||
color-scheme: light dark;
|
||
background-color: var(--background) !important;
|
||
|
||
/* Agent-facing contract; keep in sync with SKILL.md. */
|
||
--background: light-dark(rgb(255 255 255), rgb(24 24 24));
|
||
--foreground: light-dark(rgb(26 28 31), rgb(255 255 255));
|
||
--card: color-mix(in oklab, var(--foreground) 5%, transparent);
|
||
--card-foreground: var(--foreground);
|
||
--popover: light-dark(rgb(255 255 255), rgb(45 45 45));
|
||
--popover-foreground: var(--foreground);
|
||
--primary: light-dark(rgb(51 156 255), rgb(131 195 255));
|
||
--primary-foreground: light-dark(rgb(255 255 255), rgb(13 13 13));
|
||
--secondary: light-dark(rgb(255 255 255 / 96%), rgb(54 54 54 / 96%));
|
||
--secondary-foreground: var(--foreground);
|
||
--muted: color-mix(in srgb, var(--foreground) 10%, transparent);
|
||
--muted-foreground: light-dark(
|
||
rgb(26 28 31 / 49.4%),
|
||
rgb(255 255 255 / 49.8%)
|
||
);
|
||
--accent: light-dark(rgb(229 242 255), rgb(13 39 63));
|
||
--accent-foreground: var(--primary);
|
||
--destructive: light-dark(rgb(226 85 7), rgb(255 133 73));
|
||
--border: light-dark(rgb(26 28 31 / 8%), rgb(255 255 255 / 8.2%));
|
||
--input: light-dark(
|
||
rgb(26 28 31 / 11.8%),
|
||
color-mix(in oklab, rgb(0 0 0) 10%, transparent)
|
||
);
|
||
--ring: light-dark(rgb(51 156 255), rgb(131 195 255 / 76%));
|
||
--font-size-base: 14px;
|
||
--viz-series-1: var(--primary);
|
||
--viz-series-2: light-dark(rgb(243 136 59), rgb(245 154 86));
|
||
--viz-series-3: light-dark(rgb(93 201 119), rgb(116 213 139));
|
||
--viz-series-4: light-dark(rgb(235 119 177), rgb(240 143 192));
|
||
--viz-series-5: light-dark(rgb(155 121 236), rgb(170 145 239));
|
||
--viz-series-6: light-dark(rgb(58 185 177), rgb(90 203 194));
|
||
|
||
/* Internal implementation variables; not part of the agent contract. */
|
||
--font-sans: -apple-system, system-ui, "Segoe UI", sans-serif;
|
||
--font-mono:
|
||
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono",
|
||
monospace;
|
||
--font-size-normal: max(11px, var(--font-size-base));
|
||
--font-size-tooltip: calc(var(--font-size-base) - 1px);
|
||
--font-size-small: max(11px, calc(var(--font-size-base) - 2px));
|
||
--font-size-h1: calc(var(--font-size-normal) * 1.7142857143);
|
||
--font-size-h2: calc(var(--font-size-normal) * 1.4285714286);
|
||
--font-size-h3: calc(var(--font-size-normal) * 1.2857142857);
|
||
--font-weight-normal: 430;
|
||
--font-weight-medium: 500;
|
||
--line-height-normal: calc(var(--font-size-normal) * 1.5);
|
||
--line-height-tooltip: calc(var(--font-size-tooltip) * 1.4285714286);
|
||
--line-height-small: calc(var(--font-size-small) + 4px);
|
||
--radius: 12.5px;
|
||
--radius-sm: calc(var(--radius) * 0.6);
|
||
--radius-md: calc(var(--radius) * 0.8);
|
||
--radius-lg: var(--radius);
|
||
--radius-2xl: calc(var(--radius) * 1.6);
|
||
--radius-full: 9999px;
|
||
--shadow-sm: 0 1px 2px -1px rgb(0 0 0 / 8%);
|
||
--checkmark-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 17 17'%3E%3Cpath d='M12.8961 3.64101C13.1297 3.41418 13.4984 3.37523 13.7779 3.56581C14.0571 3.75635 14.1554 4.11331 14.0299 4.41347L13.9615 4.53847L7.71151 13.7045C7.59411 13.8767 7.4063 13.9877 7.19881 14.0072C6.99136 14.0267 6.78564 13.9533 6.63826 13.806L2.88826 10.056L2.79842 9.9457C2.6192 9.67407 2.64927 9.30496 2.88826 9.06581C3.12738 8.82669 3.49647 8.79676 3.76815 8.97597L3.8785 9.06581L7.03084 12.2182L12.8053 3.74941L12.8961 3.64101Z'/%3E%3C/svg%3E");
|
||
|
||
/* Legacy aliases; not part of the current agent contract. */
|
||
--viz-bg: transparent;
|
||
--viz-panel: var(--card);
|
||
--viz-border: var(--border);
|
||
--viz-text: var(--foreground);
|
||
--viz-muted: var(--muted-foreground);
|
||
--viz-accent: var(--primary);
|
||
--viz-accent-text: var(--primary-foreground);
|
||
--viz-accent-bg: var(--accent);
|
||
--viz-font-size: var(--font-size-base);
|
||
--viz-warning: var(--destructive);
|
||
--color-background-primary: var(--background);
|
||
--color-text-primary: var(--foreground);
|
||
--color-border-secondary: var(--border);
|
||
}
|
||
|
||
:root[data-theme="light"] {
|
||
color-scheme: light;
|
||
}
|
||
|
||
:root[data-theme="dark"] {
|
||
color-scheme: dark;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
html > body {
|
||
margin: 0;
|
||
padding: 5px;
|
||
color: var(--foreground);
|
||
background: transparent !important;
|
||
font-family: var(--font-sans);
|
||
font-size: var(--font-size-normal);
|
||
font-weight: var(--font-weight-normal);
|
||
line-height: var(--line-height-normal);
|
||
}
|
||
|
||
a {
|
||
color: color-mix(in srgb, var(--viz-accent) 80%, var(--viz-text) 20%);
|
||
cursor: pointer;
|
||
font-weight: var(--viz-font-weight-medium);
|
||
text-decoration: none;
|
||
text-underline-offset: 2px;
|
||
}
|
||
|
||
a:is(:hover, :focus-visible) {
|
||
text-decoration-line: underline;
|
||
text-decoration-style: dashed;
|
||
text-decoration-thickness: 0.5px;
|
||
}
|
||
|
||
h1,
|
||
h2,
|
||
h3,
|
||
h4,
|
||
h5,
|
||
h6,
|
||
p {
|
||
margin-block: 0;
|
||
}
|
||
|
||
h1 {
|
||
font-size: var(--font-size-h1);
|
||
font-weight: var(--font-weight-medium);
|
||
line-height: 1.25;
|
||
}
|
||
|
||
h2 {
|
||
font-size: var(--font-size-h2);
|
||
font-weight: var(--font-weight-medium);
|
||
line-height: 1.25;
|
||
}
|
||
|
||
h3,
|
||
h4,
|
||
h5,
|
||
h6 {
|
||
font-size: var(--font-size-h3);
|
||
font-weight: var(--font-weight-medium);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
b,
|
||
strong,
|
||
th {
|
||
font-weight: var(--font-weight-medium);
|
||
}
|
||
|
||
code:not(pre code) {
|
||
display: inline;
|
||
padding: 1px 6px;
|
||
border-radius: var(--radius-sm);
|
||
corner-shape: superellipse(1.5);
|
||
background: var(--muted);
|
||
font-family: var(--font-mono);
|
||
font-size: 0.92em;
|
||
overflow-wrap: anywhere;
|
||
-webkit-box-decoration-break: clone;
|
||
box-decoration-break: clone;
|
||
word-break: break-word;
|
||
}
|
||
|
||
#widget {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
width: 100%;
|
||
padding: 0;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.card {
|
||
min-width: 0;
|
||
padding: 12px;
|
||
overflow: hidden;
|
||
overflow-wrap: break-word;
|
||
border-radius: var(--radius-2xl);
|
||
corner-shape: superellipse(1.5);
|
||
color: var(--card-foreground);
|
||
background: var(--card);
|
||
}
|
||
|
||
#widget > :not(.card) {
|
||
width: 100% !important;
|
||
max-width: none !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
border: 0 !important;
|
||
border-radius: 0 !important;
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
.tooltip {
|
||
position: fixed;
|
||
z-index: 50;
|
||
top: 0;
|
||
left: 0;
|
||
width: max-content;
|
||
max-width: min(
|
||
20rem,
|
||
var(--tooltip-available-width, calc(100vw - 10px)),
|
||
calc(100vw - 10px)
|
||
);
|
||
max-height: min(
|
||
var(--tooltip-available-height, calc(100vh - 10px)),
|
||
calc(100vh - 10px)
|
||
);
|
||
padding: 4px 8px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-lg);
|
||
corner-shape: superellipse(1.5);
|
||
color: var(--popover-foreground);
|
||
background: var(--popover);
|
||
box-shadow: none;
|
||
font-size: var(--font-size-tooltip);
|
||
line-height: var(--line-height-tooltip);
|
||
overflow-wrap: break-word;
|
||
white-space: normal;
|
||
pointer-events: none;
|
||
user-select: none;
|
||
}
|
||
|
||
.viz-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(max(180px, 24%), 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.viz-stat {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.viz-stat-value {
|
||
font-size: var(--font-size-h2);
|
||
font-weight: var(--font-weight-medium);
|
||
line-height: 1.25;
|
||
}
|
||
|
||
.viz-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.viz-badge {
|
||
padding: 3px 8px;
|
||
border-radius: var(--radius-full);
|
||
color: var(--accent-foreground);
|
||
background: var(--accent);
|
||
font-weight: var(--font-weight-medium);
|
||
}
|
||
|
||
small,
|
||
.text-small,
|
||
.viz-badge {
|
||
font-size: var(--font-size-small);
|
||
line-height: var(--line-height-small);
|
||
}
|
||
|
||
.text-muted {
|
||
color: var(--muted-foreground);
|
||
}
|
||
|
||
.sr-only {
|
||
position: absolute;
|
||
width: 1px;
|
||
height: 1px;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
clip: rect(0, 0, 0, 0);
|
||
white-space: nowrap;
|
||
border: 0;
|
||
}
|
||
|
||
.viz-controls {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.viz-controls > .form-label {
|
||
display: grid;
|
||
min-width: min(100%, 260px);
|
||
flex: 1 1 280px;
|
||
grid-template-columns: minmax(0, 1fr) auto;
|
||
align-items: baseline;
|
||
gap: 2px 12px;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.viz-controls > .form-label > :is(.form-control, .form-range, .form-select) {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.btn,
|
||
.form-check-input,
|
||
.form-control,
|
||
.form-range,
|
||
.form-select {
|
||
font: inherit;
|
||
}
|
||
|
||
.btn {
|
||
appearance: button;
|
||
display: inline-flex;
|
||
inline-size: fit-content;
|
||
max-inline-size: 100%;
|
||
min-height: 28px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
margin: 0;
|
||
padding: 0 8px;
|
||
-webkit-app-region: no-drag;
|
||
border: 1px solid var(--input);
|
||
border-radius: var(--radius-lg);
|
||
corner-shape: superellipse(1.5);
|
||
color: var(--secondary-foreground);
|
||
background: var(--secondary);
|
||
cursor: var(--cursor-interaction, pointer);
|
||
text-align: center;
|
||
text-decoration: none;
|
||
white-space: nowrap;
|
||
user-select: none;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
.btn:is(.btn-block, .viz-tile) {
|
||
inline-size: 100%;
|
||
}
|
||
|
||
.btn.viz-tile {
|
||
min-width: 0;
|
||
overflow-wrap: anywhere;
|
||
white-space: normal;
|
||
}
|
||
|
||
a.btn {
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.btn:not(:disabled):hover {
|
||
background: color-mix(in srgb, var(--foreground) 6%, var(--secondary));
|
||
}
|
||
|
||
.btn-primary {
|
||
border-color: transparent;
|
||
color: var(--primary-foreground);
|
||
background: var(--foreground);
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
.btn-primary .text-muted {
|
||
color: color-mix(in srgb, var(--primary-foreground) 50%, transparent);
|
||
}
|
||
|
||
.btn-primary:not(:disabled):hover {
|
||
background: color-mix(in srgb, var(--foreground) 80%, transparent);
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
.btn-ghost {
|
||
border-color: transparent;
|
||
color: var(--muted-foreground);
|
||
background: transparent;
|
||
}
|
||
|
||
.btn-ghost:not(:disabled):hover {
|
||
color: var(--foreground);
|
||
background: color-mix(in srgb, var(--foreground) 6%, var(--secondary));
|
||
}
|
||
|
||
.btn:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.4;
|
||
}
|
||
|
||
[data-lucide] {
|
||
stroke-width: 1.6;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
color: var(--foreground);
|
||
}
|
||
|
||
.form-control {
|
||
display: block;
|
||
width: 100%;
|
||
min-height: 28px;
|
||
padding: 0 8px;
|
||
outline: none;
|
||
border: 1px solid var(--input);
|
||
border-radius: var(--radius-lg);
|
||
corner-shape: superellipse(1.5);
|
||
color: var(--foreground);
|
||
background: var(--secondary);
|
||
}
|
||
|
||
.form-control::placeholder {
|
||
color: var(--muted-foreground);
|
||
}
|
||
|
||
.form-control[type="file"] {
|
||
padding: 0;
|
||
overflow: hidden;
|
||
cursor: var(--cursor-interaction, pointer);
|
||
}
|
||
|
||
.form-control[type="file"]::file-selector-button {
|
||
min-height: 26px;
|
||
margin-right: 8px;
|
||
padding: 0 8px;
|
||
border: 0;
|
||
border-right: 1px solid var(--input);
|
||
color: var(--secondary-foreground);
|
||
background: var(--secondary);
|
||
cursor: inherit;
|
||
font: inherit;
|
||
}
|
||
|
||
.form-control[type="file"]:not(:disabled):hover::file-selector-button {
|
||
background: color-mix(in srgb, var(--foreground) 6%, var(--secondary));
|
||
}
|
||
|
||
.form-control-color[type="color"] {
|
||
width: 40px;
|
||
height: 28px;
|
||
padding: 3px;
|
||
cursor: var(--cursor-interaction, pointer);
|
||
}
|
||
|
||
.form-control-color[type="color"]::-webkit-color-swatch-wrapper {
|
||
padding: 0;
|
||
}
|
||
|
||
.form-control-color[type="color"]::-webkit-color-swatch {
|
||
border: 0;
|
||
border-radius: calc(var(--radius-lg) - 4px);
|
||
corner-shape: superellipse(1.5);
|
||
}
|
||
|
||
textarea.form-control {
|
||
height: auto;
|
||
min-height: 72px;
|
||
padding: 8px 10px;
|
||
resize: vertical;
|
||
}
|
||
|
||
.form-control:focus-visible {
|
||
border-color: var(--ring);
|
||
box-shadow: inset 0 0 0 1px var(--ring);
|
||
}
|
||
|
||
.form-control:disabled,
|
||
.form-select:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.4;
|
||
}
|
||
|
||
.form-select {
|
||
appearance: none;
|
||
display: block;
|
||
width: 100%;
|
||
min-height: 28px;
|
||
margin: 0;
|
||
padding: 0 32px 0 8px;
|
||
outline: none;
|
||
border: 1px solid var(--input);
|
||
border-radius: var(--radius-lg);
|
||
corner-shape: superellipse(1.5);
|
||
color: var(--foreground);
|
||
background-color: var(--secondary);
|
||
background-image:
|
||
linear-gradient(45deg, transparent 50%, var(--muted-foreground) 50%),
|
||
linear-gradient(135deg, var(--muted-foreground) 50%, transparent 50%);
|
||
background-position:
|
||
calc(100% - 14px) 50%,
|
||
calc(100% - 10px) 50%;
|
||
background-repeat: no-repeat;
|
||
background-size: 4px 4px;
|
||
cursor: var(--cursor-interaction, default);
|
||
}
|
||
|
||
.form-select:not(:disabled):hover {
|
||
background-color: color-mix(in srgb, var(--foreground) 6%, var(--secondary));
|
||
}
|
||
|
||
.form-select:focus-visible {
|
||
border-color: var(--ring);
|
||
box-shadow: inset 0 0 0 1px var(--ring);
|
||
}
|
||
|
||
.form-check {
|
||
display: flex;
|
||
min-height: 20px;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.form-check-input {
|
||
appearance: none;
|
||
width: 14px;
|
||
height: 14px;
|
||
flex: 0 0 auto;
|
||
margin: 0;
|
||
border: 1px solid var(--input);
|
||
color: var(--primary-foreground);
|
||
background-color: transparent;
|
||
cursor: var(--cursor-interaction, default);
|
||
transition:
|
||
background-color 150ms,
|
||
border-color 150ms,
|
||
box-shadow 150ms;
|
||
}
|
||
|
||
.form-check:not(.form-switch) .form-check-input[type="checkbox"] {
|
||
border-color: var(--input);
|
||
border-radius: var(--radius-sm);
|
||
corner-shape: superellipse(1.5);
|
||
background-color: var(--secondary);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.form-check:not(.form-switch)
|
||
.form-check-input:not(:disabled):not(:checked):hover {
|
||
background-color: var(--card);
|
||
}
|
||
|
||
.form-check:not(.form-switch) .form-check-input[type="checkbox"]:checked {
|
||
border-color: var(--primary);
|
||
background-color: var(--primary);
|
||
}
|
||
|
||
.form-check:not(.form-switch)
|
||
.form-check-input[type="checkbox"]:checked::before {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: var(--primary-foreground);
|
||
content: "";
|
||
mask: var(--checkmark-image) center / 12px 12px no-repeat;
|
||
}
|
||
|
||
.form-check-input[type="radio"] {
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: var(--radius-full);
|
||
}
|
||
|
||
.form-check-input[type="radio"]:checked {
|
||
border: 2px solid var(--primary);
|
||
background:
|
||
radial-gradient(circle, var(--primary-foreground) 0 2.5px, transparent 3px),
|
||
var(--primary);
|
||
}
|
||
|
||
.form-check:not(.form-switch) .form-check-input:focus-visible {
|
||
outline: 2px solid var(--ring);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.form-check:not(.form-switch) .form-check-input:disabled {
|
||
cursor: not-allowed;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.form-check:not(.form-switch) .form-check-input:disabled + .form-check-label {
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.form-switch .form-check-input:disabled,
|
||
.form-switch .form-check-input:disabled + .form-check-label {
|
||
cursor: not-allowed;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.form-check-label {
|
||
color: var(--foreground);
|
||
cursor: var(--cursor-interaction, default);
|
||
}
|
||
|
||
.form-switch .form-check-input {
|
||
position: relative;
|
||
width: 32px;
|
||
height: 20px;
|
||
border: 0;
|
||
border-radius: var(--radius-full);
|
||
background: var(--muted);
|
||
box-shadow: none;
|
||
transition: background-color 200ms cubic-bezier(0, 0, 0.2, 1);
|
||
}
|
||
|
||
.form-switch .form-check-input::before {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
width: 16px;
|
||
height: 16px;
|
||
box-sizing: border-box;
|
||
border: 1px solid light-dark(var(--primary-foreground), var(--foreground));
|
||
border-radius: var(--radius-full);
|
||
background: light-dark(var(--primary-foreground), var(--foreground));
|
||
box-shadow: var(--shadow-sm);
|
||
content: "";
|
||
transform: translate(2px, -50%);
|
||
transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
|
||
}
|
||
|
||
.form-switch .form-check-input:checked {
|
||
background: var(--primary);
|
||
}
|
||
|
||
.form-switch .form-check-input:checked::before {
|
||
transform: translate(14px, -50%);
|
||
}
|
||
|
||
.form-switch .form-check-input:focus-visible {
|
||
box-shadow: 0 0 0 2px var(--ring);
|
||
}
|
||
|
||
.form-range {
|
||
appearance: none;
|
||
display: block;
|
||
width: 100%;
|
||
height: 28px;
|
||
flex: 1;
|
||
margin: 0;
|
||
padding: 0;
|
||
outline: none;
|
||
border: 0;
|
||
accent-color: var(--primary);
|
||
background: linear-gradient(
|
||
color-mix(in srgb, var(--foreground) 7%, transparent),
|
||
color-mix(in srgb, var(--foreground) 7%, transparent)
|
||
)
|
||
center / 100% 2px no-repeat;
|
||
}
|
||
|
||
.form-range::-webkit-slider-runnable-track {
|
||
height: 28px;
|
||
background: transparent;
|
||
}
|
||
|
||
.form-range::-webkit-slider-thumb {
|
||
appearance: none;
|
||
width: 20px;
|
||
height: 20px;
|
||
margin-top: 4px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-full);
|
||
background: light-dark(var(--primary-foreground), var(--foreground));
|
||
}
|
||
|
||
.form-range:focus-visible::-webkit-slider-thumb {
|
||
border-color: var(--ring);
|
||
box-shadow: inset 0 0 0 1px var(--ring);
|
||
}
|
||
|
||
.form-range::-moz-range-track {
|
||
height: 28px;
|
||
background: transparent;
|
||
}
|
||
|
||
.form-range::-moz-range-thumb {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-full);
|
||
background: light-dark(var(--primary-foreground), var(--foreground));
|
||
}
|
||
|
||
.form-range:focus-visible::-moz-range-thumb {
|
||
border-color: var(--ring);
|
||
box-shadow: inset 0 0 0 1px var(--ring);
|
||
}
|
||
|
||
.form-range:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.4;
|
||
}
|
||
|
||
.btn:not(.btn-primary, .viz-tile):is(
|
||
[aria-pressed="true"],
|
||
[aria-selected="true"],
|
||
.is-selected
|
||
) {
|
||
border-color: var(--primary);
|
||
color: var(--primary-foreground);
|
||
background: var(--primary);
|
||
}
|
||
|
||
.btn.viz-tile:is([aria-pressed="true"], [aria-selected="true"], .is-selected) {
|
||
border-color: var(--primary);
|
||
box-shadow: inset 0 0 0 1px var(--primary);
|
||
}
|
||
|
||
.btn:focus-visible {
|
||
outline: 2px solid var(--ring);
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
svg {
|
||
display: block;
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
#widget > svg {
|
||
width: 100%;
|
||
}
|
||
|
||
.text-warning,
|
||
.text-destructive {
|
||
color: var(--destructive);
|
||
}
|
||
|
||
html>body{padding:0}</style>
|
||
<script>
|
||
(() => {
|
||
const root = document.documentElement;
|
||
const initialGlobals =
|
||
globalThis.__codexVisualizationInitialGlobals ?? globalThis.openai;
|
||
delete globalThis.__codexVisualizationInitialGlobals;
|
||
let appliedProperties = [];
|
||
|
||
const applyHostStyleVariables = (styleVariables) => {
|
||
if (styleVariables == null || typeof styleVariables !== "object") {
|
||
return;
|
||
}
|
||
for (const property of appliedProperties) {
|
||
root.style.removeProperty(property);
|
||
}
|
||
appliedProperties = [];
|
||
for (const [property, value] of Object.entries(styleVariables)) {
|
||
if (property.startsWith("--") && typeof value === "string") {
|
||
root.style.setProperty(property, value);
|
||
appliedProperties.push(property);
|
||
}
|
||
}
|
||
};
|
||
|
||
const syncHostStyles = (globals) => {
|
||
applyHostStyleVariables(globals?.visualizationStyleVariables);
|
||
};
|
||
|
||
syncHostStyles(initialGlobals);
|
||
globalThis.addEventListener("openai:set_globals", (event) => {
|
||
syncHostStyles(event.detail?.globals);
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<div id="thinkloom-framework">
|
||
<style>
|
||
#thinkloom-framework {
|
||
color: var(--foreground);
|
||
background: transparent;
|
||
width: 100%;
|
||
}
|
||
#thinkloom-framework * { box-sizing: border-box; }
|
||
#thinkloom-framework .app-shell {
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
background: var(--background);
|
||
}
|
||
#thinkloom-framework .topbar,
|
||
#thinkloom-framework .phasebar,
|
||
#thinkloom-framework .section-head,
|
||
#thinkloom-framework .composer-actions,
|
||
#thinkloom-framework .idea-head,
|
||
#thinkloom-framework .preview-actions,
|
||
#thinkloom-framework .utility-panel-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
#thinkloom-framework .topbar {
|
||
justify-content: space-between;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
#thinkloom-framework .brand-group,
|
||
#thinkloom-framework .status-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
#thinkloom-framework .brand-mark {
|
||
width: 30px;
|
||
height: 30px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 8px;
|
||
color: var(--primary-foreground);
|
||
background: var(--primary);
|
||
}
|
||
#thinkloom-framework .project-name { font-weight: 500; }
|
||
#thinkloom-framework .phasebar {
|
||
justify-content: space-between;
|
||
padding: 9px 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: color-mix(in srgb, var(--muted) 45%, transparent);
|
||
}
|
||
#thinkloom-framework .phase-tabs,
|
||
#thinkloom-framework .utility-tabs {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
}
|
||
#thinkloom-framework .workspace { min-height: 510px; }
|
||
#thinkloom-framework .screen { display: none; }
|
||
#thinkloom-framework .screen.is-active { display: block; }
|
||
#thinkloom-framework .section-head {
|
||
justify-content: space-between;
|
||
min-height: 51px;
|
||
padding: 9px 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
#thinkloom-framework .section-head h2,
|
||
#thinkloom-framework .pane-title,
|
||
#thinkloom-framework .rail-title { margin: 0; font-weight: 500; }
|
||
#thinkloom-framework .ideation-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.55fr) minmax(260px, .85fr);
|
||
}
|
||
#thinkloom-framework .draft-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(180px, .65fr) minmax(360px, 1.7fr) minmax(230px, .8fr);
|
||
}
|
||
#thinkloom-framework .final-grid {
|
||
display: grid;
|
||
grid-template-columns: minmax(170px, .55fr) minmax(380px, 1.7fr) minmax(210px, .7fr);
|
||
}
|
||
#thinkloom-framework .pane,
|
||
#thinkloom-framework .rail {
|
||
min-width: 0;
|
||
padding: 14px;
|
||
}
|
||
#thinkloom-framework .pane + .pane,
|
||
#thinkloom-framework .pane + .rail,
|
||
#thinkloom-framework .rail + .pane {
|
||
border-left: 1px solid var(--border);
|
||
}
|
||
#thinkloom-framework .pane-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
}
|
||
#thinkloom-framework .conversation {
|
||
display: grid;
|
||
gap: 12px;
|
||
min-height: 285px;
|
||
align-content: start;
|
||
}
|
||
#thinkloom-framework .turn {
|
||
max-width: 86%;
|
||
padding: 10px 12px;
|
||
border-radius: 10px;
|
||
background: var(--muted);
|
||
color: var(--foreground);
|
||
}
|
||
#thinkloom-framework .turn.user {
|
||
justify-self: end;
|
||
background: var(--primary);
|
||
color: var(--primary-foreground);
|
||
}
|
||
#thinkloom-framework .turn-label {
|
||
display: block;
|
||
margin-bottom: 4px;
|
||
font-weight: 500;
|
||
}
|
||
#thinkloom-framework .composer {
|
||
display: grid;
|
||
gap: 8px;
|
||
margin-top: 14px;
|
||
}
|
||
#thinkloom-framework textarea.form-control { resize: vertical; }
|
||
#thinkloom-framework .composer-actions { justify-content: space-between; }
|
||
#thinkloom-framework .recording {
|
||
display: none;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 10px;
|
||
color: var(--destructive);
|
||
border-left: 3px solid var(--destructive);
|
||
background: color-mix(in srgb, var(--destructive) 8%, transparent);
|
||
}
|
||
#thinkloom-framework .recording.is-active { display: flex; }
|
||
#thinkloom-framework .idea-list,
|
||
#thinkloom-framework .rail-list,
|
||
#thinkloom-framework .assistant-stack {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
#thinkloom-framework .idea-item {
|
||
padding: 12px 0;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
#thinkloom-framework .idea-item:first-child { border-top: 0; padding-top: 0; }
|
||
#thinkloom-framework .idea-head { justify-content: space-between; }
|
||
#thinkloom-framework .idea-title { font-weight: 500; }
|
||
#thinkloom-framework .idea-summary { margin: 8px 0; }
|
||
#thinkloom-framework .idea-meta {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
#thinkloom-framework .idea-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
margin-top: 10px;
|
||
}
|
||
#thinkloom-framework .selection-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: flex-start;
|
||
padding: 10px 0;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
#thinkloom-framework .selection-row:first-of-type { border-top: 0; }
|
||
#thinkloom-framework .selection-row input { margin-top: 3px; }
|
||
#thinkloom-framework .editor-paper {
|
||
min-height: 400px;
|
||
padding: 24px clamp(16px, 6vw, 54px);
|
||
outline: none;
|
||
background: color-mix(in srgb, var(--card) 58%, var(--background));
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
}
|
||
#thinkloom-framework .editor-paper h3 { margin: 0 0 16px; font-weight: 500; }
|
||
#thinkloom-framework .editor-paper p { margin: 0 0 14px; }
|
||
#thinkloom-framework .ai-insert {
|
||
background: color-mix(in srgb, var(--viz-series-1) 18%, transparent);
|
||
border-left: 3px solid var(--viz-series-1);
|
||
padding-left: 10px;
|
||
}
|
||
#thinkloom-framework .assistant-note {
|
||
padding: 10px;
|
||
background: var(--muted);
|
||
border-radius: 8px;
|
||
}
|
||
#thinkloom-framework .preview {
|
||
display: none;
|
||
padding: 12px 0 0;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
#thinkloom-framework .preview.is-active { display: block; }
|
||
#thinkloom-framework .preview-copy {
|
||
padding: 10px;
|
||
border-left: 3px solid var(--viz-series-1);
|
||
background: color-mix(in srgb, var(--viz-series-1) 10%, transparent);
|
||
}
|
||
#thinkloom-framework .preview-actions { margin-top: 10px; }
|
||
#thinkloom-framework .rail-list .btn { justify-content: flex-start; }
|
||
#thinkloom-framework .release-list {
|
||
margin: 0;
|
||
padding-left: 20px;
|
||
display: grid;
|
||
gap: 7px;
|
||
}
|
||
#thinkloom-framework .compare-strip {
|
||
display: none;
|
||
margin-bottom: 12px;
|
||
padding: 10px;
|
||
border-left: 3px solid var(--viz-series-2);
|
||
background: color-mix(in srgb, var(--viz-series-2) 10%, transparent);
|
||
}
|
||
#thinkloom-framework .compare-strip.is-active { display: block; }
|
||
#thinkloom-framework .utility-panel {
|
||
display: none;
|
||
padding: 14px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--popover);
|
||
color: var(--popover-foreground);
|
||
}
|
||
#thinkloom-framework .utility-panel.is-active { display: block; }
|
||
#thinkloom-framework .utility-panel-head { justify-content: space-between; }
|
||
#thinkloom-framework .timeline {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(110px, 1fr));
|
||
gap: 12px;
|
||
margin-top: 12px;
|
||
}
|
||
#thinkloom-framework .timeline-step {
|
||
padding-left: 10px;
|
||
border-left: 2px solid var(--border);
|
||
}
|
||
#thinkloom-framework .timeline-step.is-current { border-left-color: var(--viz-series-1); }
|
||
#thinkloom-framework .live-note { min-height: 18px; }
|
||
#thinkloom-framework button i,
|
||
#thinkloom-framework .brand-mark i { vertical-align: middle; }
|
||
@media (max-width: 850px) {
|
||
#thinkloom-framework .draft-grid,
|
||
#thinkloom-framework .final-grid { grid-template-columns: 1fr; }
|
||
#thinkloom-framework .pane + .pane,
|
||
#thinkloom-framework .pane + .rail,
|
||
#thinkloom-framework .rail + .pane {
|
||
border-left: 0;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
#thinkloom-framework .draft-grid .editor-pane,
|
||
#thinkloom-framework .final-grid .editor-pane { order: -1; }
|
||
}
|
||
@media (max-width: 620px) {
|
||
#thinkloom-framework .ideation-grid { grid-template-columns: 1fr; }
|
||
#thinkloom-framework .pane + .pane { border-left: 0; border-top: 1px solid var(--border); }
|
||
#thinkloom-framework .utility-tabs .btn span { display: none; }
|
||
#thinkloom-framework .timeline { grid-template-columns: 1fr 1fr; }
|
||
#thinkloom-framework .topbar { align-items: flex-start; }
|
||
}
|
||
</style>
|
||
|
||
<section class="app-shell" aria-label="Conversation-first writing application mockup">
|
||
<header class="topbar">
|
||
<div class="brand-group">
|
||
<span class="brand-mark"><i data-lucide="feather" aria-hidden="true"></i></span>
|
||
<div>
|
||
<div class="project-name">The Shape of Attention</div>
|
||
<div class="text-small text-muted">Essay project · 1,284 words</div>
|
||
</div>
|
||
</div>
|
||
<div class="status-group">
|
||
<span class="viz-badge"><i data-lucide="hard-drive" aria-hidden="true"></i> Local</span>
|
||
<button class="btn btn-ghost" type="button" data-utility="provenance">
|
||
<i data-lucide="shield-check" aria-hidden="true"></i> History recorded
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<nav class="phasebar" aria-label="Application navigation">
|
||
<div class="phase-tabs" aria-label="Writing phases">
|
||
<button class="btn" type="button" data-phase="ideation" aria-pressed="true">Ideation</button>
|
||
<button class="btn" type="button" data-phase="drafting" aria-pressed="false">Drafting</button>
|
||
<button class="btn" type="button" data-phase="finalization" aria-pressed="false">Finalization</button>
|
||
</div>
|
||
<div class="utility-tabs" aria-label="Project tools">
|
||
<button class="btn btn-ghost" type="button" data-utility="project"><i data-lucide="folder" aria-hidden="true"></i><span> Project</span></button>
|
||
<button class="btn btn-ghost" type="button" data-utility="history"><i data-lucide="history" aria-hidden="true"></i><span> History</span></button>
|
||
<button class="btn btn-ghost" type="button" data-utility="provenance"><i data-lucide="git-fork" aria-hidden="true"></i><span> Provenance</span></button>
|
||
<button class="btn btn-ghost" type="button" data-utility="export"><i data-lucide="package-open" aria-hidden="true"></i><span> Export</span></button>
|
||
<button class="btn btn-ghost" type="button" data-utility="settings"><i data-lucide="settings" aria-hidden="true"></i><span> Settings</span></button>
|
||
</div>
|
||
</nav>
|
||
|
||
<aside class="utility-panel" aria-live="polite">
|
||
<div class="utility-panel-head">
|
||
<div>
|
||
<div class="project-name" data-utility-title>Project history</div>
|
||
<div class="text-small text-muted" data-utility-copy>Meaningful versions and creative contributions stay inspectable.</div>
|
||
</div>
|
||
<button class="btn btn-ghost" type="button" data-close-utility aria-label="Close panel"><i data-lucide="x" aria-hidden="true"></i></button>
|
||
</div>
|
||
<div class="timeline" data-utility-body>
|
||
<div class="timeline-step"><div class="text-small text-muted">9:14 AM</div><div>Idea accepted</div></div>
|
||
<div class="timeline-step"><div class="text-small text-muted">9:32 AM</div><div>Draft checkpoint</div></div>
|
||
<div class="timeline-step"><div class="text-small text-muted">10:08 AM</div><div>Passage revised</div></div>
|
||
<div class="timeline-step is-current"><div class="text-small text-muted">Now</div><div>Working version</div></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="workspace">
|
||
<section class="screen is-active" data-screen="ideation" aria-label="Ideation workspace">
|
||
<div class="section-head">
|
||
<div>
|
||
<h2>Explore the idea</h2>
|
||
<div class="text-small text-muted">Reflect, question, and save only what matters.</div>
|
||
</div>
|
||
<div class="viz-row">
|
||
<label class="form-label">Challenge
|
||
<select class="form-select" aria-label="Challenge level">
|
||
<option>Gentle</option><option selected>Balanced</option><option>Rigorous</option>
|
||
</select>
|
||
</label>
|
||
<label class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" id="spoken-replies">
|
||
<span class="form-check-label">Spoken replies</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="ideation-grid">
|
||
<section class="pane" aria-label="Conversation">
|
||
<div class="pane-header">
|
||
<div class="pane-title">Conversation</div>
|
||
<span class="text-small text-muted">Session 3 · autosaved</span>
|
||
</div>
|
||
<div class="conversation" aria-live="polite" data-conversation>
|
||
<div class="turn">
|
||
<span class="turn-label text-small">Assistant</span>
|
||
You seem less concerned with distraction itself than with what constant interruption does to the shape of a thought. What feels lost when attention is repeatedly broken?
|
||
</div>
|
||
<div class="turn user">
|
||
<span class="turn-label text-small">You</span>
|
||
Continuity. A thought can become surprising only if I stay with it long enough to move beyond the obvious first answer.
|
||
</div>
|
||
<div class="turn">
|
||
<span class="turn-label text-small">Assistant</span>
|
||
So sustained attention is not merely concentration; it is the condition that lets an idea become less predictable. Is that a claim you want to develop?
|
||
</div>
|
||
</div>
|
||
<div class="recording" data-recording role="status"><i data-lucide="audio-waveform" aria-hidden="true"></i><span>Listening… audio stays in memory and will not be retained.</span></div>
|
||
<div class="composer">
|
||
<label class="sr-only" for="idea-message">Reply</label>
|
||
<textarea id="idea-message" class="form-control" rows="3" placeholder="Respond, or ask to save something as an idea…"></textarea>
|
||
<div class="composer-actions">
|
||
<div class="viz-row">
|
||
<button class="btn" type="button" data-mic aria-pressed="false"><i data-lucide="mic" aria-hidden="true"></i> Push to talk</button>
|
||
<button class="btn btn-ghost" type="button"><i data-lucide="bookmark-plus" aria-hidden="true"></i> Mark as idea</button>
|
||
</div>
|
||
<button class="btn btn-primary" type="button" data-send>Send <i data-lucide="arrow-up" aria-hidden="true"></i></button>
|
||
</div>
|
||
<div class="text-small text-muted live-note" data-idea-note aria-live="polite"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<aside class="pane" aria-label="Suggested ideas">
|
||
<div class="pane-header">
|
||
<div class="pane-title">Suggested ideas</div>
|
||
<span class="viz-badge" data-idea-count>2 awaiting review</span>
|
||
</div>
|
||
<div class="idea-list">
|
||
<article class="idea-item" data-idea-card>
|
||
<div class="idea-head"><span class="idea-title">Attention enables surprise</span><span class="viz-badge" data-status>Suggested</span></div>
|
||
<p class="idea-summary">A thought becomes original only after sustained attention carries it past the first obvious answer.</p>
|
||
<div class="idea-meta text-small text-muted"><span>Assistant suggestion</span><span>·</span><span>2 source turns</span><span>·</span><span>#attention</span></div>
|
||
<div class="idea-actions">
|
||
<button class="btn btn-primary" type="button" data-idea-action="accept"><i data-lucide="check" aria-hidden="true"></i> Accept</button>
|
||
<button class="btn" type="button" data-idea-action="reject">Reject</button>
|
||
<button class="btn btn-ghost" type="button">Reframe</button>
|
||
<button class="btn btn-ghost" type="button" aria-label="More idea actions"><i data-lucide="ellipsis" aria-hidden="true"></i></button>
|
||
</div>
|
||
</article>
|
||
<article class="idea-item" data-idea-card>
|
||
<div class="idea-head"><span class="idea-title">Interruption changes thought</span><span class="viz-badge" data-status>Suggested</span></div>
|
||
<p class="idea-summary">The cost of interruption is not just lost time; it changes which ideas are able to emerge at all.</p>
|
||
<div class="idea-meta text-small text-muted"><span>Mixed origin</span><span>·</span><span>3 source turns</span><span>·</span><span>#distraction</span></div>
|
||
<div class="idea-actions">
|
||
<button class="btn btn-primary" type="button" data-idea-action="accept"><i data-lucide="check" aria-hidden="true"></i> Accept</button>
|
||
<button class="btn" type="button" data-idea-action="reject">Reject</button>
|
||
<button class="btn btn-ghost" type="button">Expand</button>
|
||
<button class="btn btn-ghost" type="button" aria-label="More idea actions"><i data-lucide="ellipsis" aria-hidden="true"></i></button>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="screen" data-screen="drafting" aria-label="Drafting workspace">
|
||
<div class="section-head">
|
||
<div><h2>Shape the draft</h2><div class="text-small text-muted">Select ideas, preview prose, then decide exactly what enters the manuscript.</div></div>
|
||
<span class="viz-badge"><i data-lucide="save" aria-hidden="true"></i> Saved just now</span>
|
||
</div>
|
||
<div class="draft-grid">
|
||
<aside class="pane" aria-label="Drafting ideas">
|
||
<div class="pane-header"><div class="pane-title">Ideas</div><span class="text-small text-muted" data-selected-count>2 selected</span></div>
|
||
<label class="selection-row"><input type="checkbox" checked data-draft-idea><span><span class="idea-title">Attention enables surprise</span><br><span class="text-small text-muted">Accepted · #attention</span></span></label>
|
||
<label class="selection-row"><input type="checkbox" checked data-draft-idea><span><span class="idea-title">Interruption changes thought</span><br><span class="text-small text-muted">Accepted · #distraction</span></span></label>
|
||
<label class="selection-row"><input type="checkbox" data-draft-idea><span><span class="idea-title">Boredom as threshold</span><br><span class="text-small text-muted">Accepted · #patience</span></span></label>
|
||
<button class="btn btn-ghost btn-block" type="button"><i data-lucide="layout-grid" aria-hidden="true"></i> Open full idea board</button>
|
||
</aside>
|
||
|
||
<section class="pane editor-pane" aria-label="Manuscript editor">
|
||
<div class="pane-header"><div class="pane-title">Manuscript</div><span class="text-small text-muted">Introduction</span></div>
|
||
<article class="editor-paper" contenteditable="true" aria-label="Editable manuscript" data-editor>
|
||
<h3>The Shape of Attention</h3>
|
||
<p>We often describe attention as if it were a spotlight: point it at the right object, hold it steady, and the important thing becomes clear. But that metaphor misses what attention changes over time.</p>
|
||
<p>Staying with a question is not passive. The first answer is usually inherited—available because it is familiar. Only after that answer has been exhausted can a stranger, more personal thought appear.</p>
|
||
<p>The real cost of interruption may therefore be larger than the minutes it takes to recover. Interruption decides which thoughts are allowed to mature.</p>
|
||
</article>
|
||
</section>
|
||
|
||
<aside class="pane" aria-label="Drafting assistant">
|
||
<div class="pane-header"><div class="pane-title">Assistant</div><span class="viz-badge">Balanced</span></div>
|
||
<div class="assistant-stack">
|
||
<div class="assistant-note">I can connect the two selected ideas without changing the opening’s reflective tone.</div>
|
||
<label class="form-label">Relationship
|
||
<select class="form-select" data-operation><option>Synthesize</option><option>Compare</option><option>Contrast</option><option>Sequence</option><option>Separate sections</option></select>
|
||
</label>
|
||
<label class="form-label">Placement
|
||
<select class="form-select"><option>At cursor</option><option>Replace selection</option><option>Append to section</option><option>Create new section</option></select>
|
||
</label>
|
||
<button class="btn btn-primary btn-block" type="button" data-generate><i data-lucide="sparkles" aria-hidden="true"></i> Preview passage</button>
|
||
<div class="preview" data-preview aria-live="polite">
|
||
<div class="text-small text-muted">Preview · not yet in manuscript</div>
|
||
<p class="preview-copy">Continuity gives a thought time to become unfamiliar to its author. Repeated interruption does more than delay that transformation; it quietly prevents it.</p>
|
||
<div class="preview-actions">
|
||
<button class="btn btn-primary" type="button" data-insert>Insert at cursor</button>
|
||
<button class="btn" type="button">Regenerate</button>
|
||
<button class="btn btn-ghost" type="button" data-discard>Discard</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="screen" data-screen="finalization" aria-label="Finalization workspace">
|
||
<div class="section-head">
|
||
<div><h2>Refine and release</h2><div class="text-small text-muted">Polish with reversible changes, compare versions, and create a release checkpoint.</div></div>
|
||
<button class="btn" type="button" data-compare><i data-lucide="columns-2" aria-hidden="true"></i> Compare versions</button>
|
||
</div>
|
||
<div class="final-grid">
|
||
<aside class="rail" aria-label="Editorial actions">
|
||
<div class="pane-header"><div class="rail-title">Editorial actions</div></div>
|
||
<div class="rail-list">
|
||
<button class="btn" type="button" data-edit-action="Clarity"><i data-lucide="scan-text" aria-hidden="true"></i> Clarity</button>
|
||
<button class="btn" type="button" data-edit-action="Shorten"><i data-lucide="minimize-2" aria-hidden="true"></i> Shorten</button>
|
||
<button class="btn" type="button" data-edit-action="Transitions"><i data-lucide="move-right" aria-hidden="true"></i> Transitions</button>
|
||
<button class="btn" type="button" data-edit-action="Repetition"><i data-lucide="repeat-2" aria-hidden="true"></i> Repetition</button>
|
||
<button class="btn" type="button" data-edit-action="Consistency"><i data-lucide="list-checks" aria-hidden="true"></i> Consistency</button>
|
||
<button class="btn" type="button" data-edit-action="User voice"><i data-lucide="audio-lines" aria-hidden="true"></i> User voice</button>
|
||
<button class="btn" type="button" data-edit-action="Proofread"><i data-lucide="spell-check" aria-hidden="true"></i> Proofread</button>
|
||
</div>
|
||
</aside>
|
||
<section class="pane editor-pane" aria-label="Final manuscript">
|
||
<div class="compare-strip" data-compare-strip role="status">Comparing <strong>Working version</strong> with <strong>Checkpoint · Jul 14</strong>. Added text is marked in the manuscript.</div>
|
||
<div class="pane-header"><div class="pane-title">The Shape of Attention</div><span class="text-small text-muted">1,284 words · working version</span></div>
|
||
<article class="editor-paper" contenteditable="true" aria-label="Editable final manuscript">
|
||
<h3>The Shape of Attention</h3>
|
||
<p>We often describe attention as if it were a spotlight: point it at the right object, hold it steady, and the important thing becomes clear. But that metaphor misses what attention changes over time.</p>
|
||
<p>Staying with a question is not passive. The first answer is usually inherited—available because it is familiar. Only after that answer has been exhausted can a stranger, more personal thought appear.</p>
|
||
<p class="ai-insert">Continuity gives a thought time to become unfamiliar to its author. Repeated interruption does more than delay that transformation; it quietly prevents it.</p>
|
||
<p>The real cost of interruption may therefore be larger than the minutes it takes to recover. Interruption decides which thoughts are allowed to mature.</p>
|
||
</article>
|
||
<div class="text-small text-muted live-note" data-edit-note aria-live="polite"></div>
|
||
</section>
|
||
<aside class="rail" aria-label="Release">
|
||
<div class="pane-header"><div class="rail-title">Release</div><span class="viz-badge">Draft</span></div>
|
||
<ol class="release-list">
|
||
<li>Review manuscript</li>
|
||
<li>Create checkpoint</li>
|
||
<li>Generate hashes</li>
|
||
<li>Choose publication files</li>
|
||
</ol>
|
||
<div class="assistant-note" style="margin-top:14px">Creative-process history will remain available without making legal claims about authorship.</div>
|
||
<button class="btn btn-primary btn-block" style="margin-top:14px" type="button" data-finalize><i data-lucide="badge-check" aria-hidden="true"></i> Finalize release</button>
|
||
<div class="text-small text-muted live-note" data-release-note aria-live="polite"></div>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</section>
|
||
|
||
<script>
|
||
(() => {
|
||
const root = document.getElementById('thinkloom-framework');
|
||
if (!root) return;
|
||
|
||
const phaseButtons = [...root.querySelectorAll('[data-phase]')];
|
||
const screens = [...root.querySelectorAll('[data-screen]')];
|
||
phaseButtons.forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
const phase = button.dataset.phase;
|
||
phaseButtons.forEach((peer) => peer.setAttribute('aria-pressed', String(peer === button)));
|
||
screens.forEach((screen) => screen.classList.toggle('is-active', screen.dataset.screen === phase));
|
||
});
|
||
});
|
||
|
||
const panel = root.querySelector('.utility-panel');
|
||
const utilityTitle = root.querySelector('[data-utility-title]');
|
||
const utilityCopy = root.querySelector('[data-utility-copy]');
|
||
const utilityBody = root.querySelector('[data-utility-body]');
|
||
const utilityContent = {
|
||
project: ['Project', 'Self-contained files, recovery status, and publication details.', '<div class="timeline-step is-current"><div class="text-small text-muted">Folder</div><div>The Shape of Attention</div></div><div class="timeline-step"><div class="text-small text-muted">Recovery</div><div>Snapshots verified</div></div><div class="timeline-step"><div class="text-small text-muted">Provider</div><div>Ollama · local</div></div><div class="timeline-step"><div class="text-small text-muted">Phase</div><div>Ideation</div></div>'],
|
||
history: ['Project history', 'Restore or compare meaningful checkpoints without exposing Git terminology.', '<div class="timeline-step"><div class="text-small text-muted">Jul 12</div><div>Project created</div></div><div class="timeline-step"><div class="text-small text-muted">Jul 14</div><div>Opening drafted</div></div><div class="timeline-step"><div class="text-small text-muted">Jul 15</div><div>Structure revised</div></div><div class="timeline-step is-current"><div class="text-small text-muted">Now</div><div>Working version</div></div>'],
|
||
provenance: ['Creative process', 'Ideas, model outputs, selections, and edits are linked in a verifiable history.', '<div class="timeline-step"><div class="text-small text-muted">9:14 AM</div><div>Idea accepted</div></div><div class="timeline-step"><div class="text-small text-muted">9:32 AM</div><div>Passage generated</div></div><div class="timeline-step"><div class="text-small text-muted">9:34 AM</div><div>Range accepted</div></div><div class="timeline-step is-current"><div class="text-small text-muted">10:08 AM</div><div>User revision</div></div>'],
|
||
export: ['Export', 'Create publication files, a restorable project backup, or a sanitized creative-process package.', '<div class="timeline-step"><div class="text-small text-muted">Publication</div><div>Markdown · HTML · PDF · text</div></div><div class="timeline-step"><div class="text-small text-muted">Backup</div><div>Complete project ZIP</div></div><div class="timeline-step"><div class="text-small text-muted">Evidence</div><div>Creative-process ZIP</div></div><div class="timeline-step"><div class="text-small text-muted">Privacy</div><div>Sanitization options</div></div>'],
|
||
settings: ['Settings', 'Provider, voice, style profile, accessibility, and backup preferences.', '<div class="timeline-step"><div class="text-small text-muted">Model</div><div>Local · llama3.2</div></div><div class="timeline-step"><div class="text-small text-muted">Voice</div><div>Push to talk</div></div><div class="timeline-step"><div class="text-small text-muted">Style</div><div>Developing profile</div></div><div class="timeline-step"><div class="text-small text-muted">Backups</div><div>Daily snapshots</div></div>']
|
||
};
|
||
root.querySelectorAll('[data-utility]').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
const content = utilityContent[button.dataset.utility] || utilityContent.history;
|
||
utilityTitle.textContent = content[0];
|
||
utilityCopy.textContent = content[1];
|
||
utilityBody.innerHTML = content[2];
|
||
panel.classList.add('is-active');
|
||
});
|
||
});
|
||
root.querySelector('[data-close-utility]').addEventListener('click', () => panel.classList.remove('is-active'));
|
||
|
||
const recording = root.querySelector('[data-recording]');
|
||
const mic = root.querySelector('[data-mic]');
|
||
mic.addEventListener('click', () => {
|
||
const active = mic.getAttribute('aria-pressed') !== 'true';
|
||
mic.setAttribute('aria-pressed', String(active));
|
||
mic.innerHTML = active ? '<i data-lucide="square" aria-hidden="true"></i> Stop recording' : '<i data-lucide="mic" aria-hidden="true"></i> Push to talk';
|
||
recording.classList.toggle('is-active', active);
|
||
if (window.lucide) lucide.createIcons({ attrs: { width: 16, height: 16 } });
|
||
});
|
||
|
||
root.querySelector('[data-send]').addEventListener('click', () => {
|
||
const field = root.querySelector('#idea-message');
|
||
const value = field.value.trim();
|
||
if (!value) {
|
||
root.querySelector('[data-idea-note]').textContent = 'Write or record a thought first.';
|
||
return;
|
||
}
|
||
const turn = document.createElement('div');
|
||
turn.className = 'turn user';
|
||
const label = document.createElement('span');
|
||
label.className = 'turn-label text-small';
|
||
label.textContent = 'You';
|
||
turn.append(label, document.createTextNode(value));
|
||
root.querySelector('[data-conversation]').appendChild(turn);
|
||
field.value = '';
|
||
root.querySelector('[data-idea-note]').textContent = 'Turn saved to this session.';
|
||
});
|
||
|
||
const updateAwaiting = () => {
|
||
const awaiting = [...root.querySelectorAll('[data-idea-card] [data-status]')].filter((status) => status.textContent === 'Suggested').length;
|
||
root.querySelector('[data-idea-count]').textContent = awaiting ? `${awaiting} awaiting review` : 'All reviewed';
|
||
};
|
||
root.querySelectorAll('[data-idea-action]').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
const card = button.closest('[data-idea-card]');
|
||
const status = card.querySelector('[data-status]');
|
||
const accepted = button.dataset.ideaAction === 'accept';
|
||
status.textContent = accepted ? 'Accepted' : 'Rejected';
|
||
card.querySelectorAll('[data-idea-action]').forEach((action) => action.disabled = true);
|
||
updateAwaiting();
|
||
});
|
||
});
|
||
|
||
const updateSelection = () => {
|
||
const count = [...root.querySelectorAll('[data-draft-idea]')].filter((box) => box.checked).length;
|
||
root.querySelector('[data-selected-count]').textContent = `${count} selected`;
|
||
};
|
||
root.querySelectorAll('[data-draft-idea]').forEach((box) => box.addEventListener('change', updateSelection));
|
||
|
||
const preview = root.querySelector('[data-preview]');
|
||
root.querySelector('[data-generate]').addEventListener('click', () => preview.classList.add('is-active'));
|
||
root.querySelector('[data-discard]').addEventListener('click', () => preview.classList.remove('is-active'));
|
||
root.querySelector('[data-insert]').addEventListener('click', () => {
|
||
const paragraph = document.createElement('p');
|
||
paragraph.className = 'ai-insert';
|
||
paragraph.textContent = 'Continuity gives a thought time to become unfamiliar to its author. Repeated interruption does more than delay that transformation; it quietly prevents it.';
|
||
root.querySelector('[data-editor]').appendChild(paragraph);
|
||
preview.classList.remove('is-active');
|
||
});
|
||
|
||
root.querySelector('[data-compare]').addEventListener('click', (event) => {
|
||
const strip = root.querySelector('[data-compare-strip]');
|
||
strip.classList.toggle('is-active');
|
||
event.currentTarget.setAttribute('aria-pressed', String(strip.classList.contains('is-active')));
|
||
});
|
||
root.querySelectorAll('[data-edit-action]').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
root.querySelector('[data-edit-note]').textContent = `${button.dataset.editAction} review is ready as a preview; the manuscript has not changed.`;
|
||
});
|
||
});
|
||
root.querySelector('[data-finalize]').addEventListener('click', () => {
|
||
root.querySelector('[data-release-note]').textContent = 'Release review opened: publication files and privacy choices are ready to confirm.';
|
||
});
|
||
})();
|
||
</script>
|
||
</div>
|
||
|
||
|
||
<style id="codex-visualization-document-scrollbar">html,body{scrollbar-width:none!important}html::-webkit-scrollbar,body::-webkit-scrollbar{display:none!important}</style>
|
||
|
||
<script src="https://unpkg.com/@floating-ui/[email protected]/dist/floating-ui.core.umd.min.js"></script>
|
||
<script src="https://unpkg.com/@floating-ui/[email protected]/dist/floating-ui.dom.umd.min.js"></script>
|
||
<script>
|
||
(() => {
|
||
if (window.FloatingUIDOM == null) {
|
||
return;
|
||
}
|
||
const { autoUpdate, computePosition, flip, offset, shift, size } =
|
||
window.FloatingUIDOM;
|
||
const tooltipPlacements = new Set(["top", "right", "bottom", "left"]);
|
||
const tooltipId = `codex-visualization-tooltip-${
|
||
window.crypto?.randomUUID?.() ?? Date.now().toString(36)
|
||
}`;
|
||
const tooltipGap = 5;
|
||
const hoverDelay = 700;
|
||
const skipDelay = 300;
|
||
let activeTrigger = null;
|
||
let pendingTrigger = null;
|
||
let tooltip = null;
|
||
let openTimeout = null;
|
||
let autoUpdateCleanup = null;
|
||
let positionRequest = 0;
|
||
let skipDelayUntil = 0;
|
||
|
||
const getTrigger = (target) =>
|
||
target?.nodeType === Node.ELEMENT_NODE
|
||
? target.closest("[data-tooltip]")
|
||
: null;
|
||
const containsTarget = (trigger, target) =>
|
||
target?.nodeType != null && trigger.contains(target);
|
||
const getContent = (trigger) =>
|
||
trigger.getAttribute("data-tooltip")?.trim() ?? "";
|
||
const getPlacement = (trigger) => {
|
||
const placement = trigger.getAttribute("data-tooltip-placement");
|
||
return tooltipPlacements.has(placement) ? placement : "top";
|
||
};
|
||
const getTooltip = () => {
|
||
if (tooltip == null) {
|
||
tooltip = document.createElement("div");
|
||
tooltip.id = tooltipId;
|
||
tooltip.className = "tooltip";
|
||
tooltip.setAttribute("role", "tooltip");
|
||
}
|
||
return tooltip;
|
||
};
|
||
const setDescribedBy = (trigger, described) => {
|
||
const ids = new Set(
|
||
(trigger.getAttribute("aria-describedby") ?? "")
|
||
.split(/\s+/)
|
||
.filter(Boolean),
|
||
);
|
||
if (described) {
|
||
ids.add(tooltipId);
|
||
} else {
|
||
ids.delete(tooltipId);
|
||
}
|
||
if (ids.size === 0) {
|
||
trigger.removeAttribute("aria-describedby");
|
||
} else {
|
||
trigger.setAttribute("aria-describedby", [...ids].join(" "));
|
||
}
|
||
};
|
||
const clearOpenTimeout = () => {
|
||
if (openTimeout != null) {
|
||
window.clearTimeout(openTimeout);
|
||
openTimeout = null;
|
||
}
|
||
pendingTrigger = null;
|
||
};
|
||
const closeTooltip = (activateSkipDelay = true) => {
|
||
clearOpenTimeout();
|
||
positionRequest += 1;
|
||
autoUpdateCleanup?.();
|
||
autoUpdateCleanup = null;
|
||
if (activeTrigger == null) {
|
||
return;
|
||
}
|
||
setDescribedBy(activeTrigger, false);
|
||
activeTrigger = null;
|
||
tooltip?.remove();
|
||
if (activateSkipDelay) {
|
||
skipDelayUntil = Date.now() + skipDelay;
|
||
}
|
||
};
|
||
const updatePosition = (trigger, floating) => {
|
||
const request = ++positionRequest;
|
||
void computePosition(trigger, floating, {
|
||
middleware: [
|
||
offset(tooltipGap),
|
||
flip({ padding: tooltipGap }),
|
||
shift({ padding: tooltipGap }),
|
||
size({
|
||
padding: tooltipGap,
|
||
apply({ availableHeight, availableWidth, elements }) {
|
||
elements.floating.style.setProperty(
|
||
"--tooltip-available-width",
|
||
`${Math.max(0, availableWidth)}px`,
|
||
);
|
||
elements.floating.style.setProperty(
|
||
"--tooltip-available-height",
|
||
`${Math.max(0, availableHeight)}px`,
|
||
);
|
||
},
|
||
}),
|
||
],
|
||
placement: getPlacement(trigger),
|
||
strategy: "fixed",
|
||
})
|
||
.then(({ x, y }) => {
|
||
if (activeTrigger !== trigger || request !== positionRequest) {
|
||
return;
|
||
}
|
||
const scale = window.devicePixelRatio || 1;
|
||
floating.style.transform = `translate(${Math.round(x * scale) / scale}px, ${Math.round(y * scale) / scale}px)`;
|
||
floating.style.visibility = "visible";
|
||
})
|
||
.catch(() => {
|
||
if (activeTrigger === trigger && request === positionRequest) {
|
||
closeTooltip(false);
|
||
}
|
||
});
|
||
};
|
||
const openTooltip = (trigger) => {
|
||
const content = getContent(trigger);
|
||
if (!trigger.isConnected || content.length === 0) {
|
||
return;
|
||
}
|
||
clearOpenTimeout();
|
||
if (activeTrigger === trigger) {
|
||
return;
|
||
}
|
||
closeTooltip();
|
||
const floating = getTooltip();
|
||
floating.textContent = content;
|
||
floating.style.visibility = "hidden";
|
||
floating.style.transform = "translate(0, 0)";
|
||
document.body.appendChild(floating);
|
||
activeTrigger = trigger;
|
||
setDescribedBy(trigger, true);
|
||
autoUpdateCleanup = autoUpdate(trigger, floating, () => {
|
||
updatePosition(trigger, floating);
|
||
});
|
||
};
|
||
const requestOpen = (trigger, immediate = false) => {
|
||
if (
|
||
getContent(trigger).length === 0 ||
|
||
activeTrigger === trigger ||
|
||
pendingTrigger === trigger
|
||
) {
|
||
return;
|
||
}
|
||
clearOpenTimeout();
|
||
pendingTrigger = trigger;
|
||
const delay =
|
||
immediate || activeTrigger != null || Date.now() < skipDelayUntil
|
||
? 0
|
||
: hoverDelay;
|
||
if (delay === 0) {
|
||
openTooltip(trigger);
|
||
return;
|
||
}
|
||
openTimeout = window.setTimeout(() => {
|
||
openTimeout = null;
|
||
pendingTrigger = null;
|
||
openTooltip(trigger);
|
||
}, delay);
|
||
};
|
||
const handlePointerOpen = (event) => {
|
||
if (event.pointerType === "touch") {
|
||
return;
|
||
}
|
||
const trigger = getTrigger(event.target);
|
||
if (trigger == null || containsTarget(trigger, event.relatedTarget)) {
|
||
return;
|
||
}
|
||
requestOpen(trigger);
|
||
};
|
||
const handleLeave = (event) => {
|
||
const trigger = getTrigger(event.target);
|
||
if (trigger == null || containsTarget(trigger, event.relatedTarget)) {
|
||
return;
|
||
}
|
||
if (activeTrigger === trigger || pendingTrigger === trigger) {
|
||
closeTooltip();
|
||
}
|
||
};
|
||
const handleFocusIn = (event) => {
|
||
const trigger = getTrigger(event.target);
|
||
if (trigger?.matches(":focus-visible")) {
|
||
requestOpen(trigger, true);
|
||
}
|
||
};
|
||
const handleKeyDown = (event) => {
|
||
if (event.key === "Escape") {
|
||
closeTooltip();
|
||
}
|
||
};
|
||
const handleContextMenu = (event) => {
|
||
const trigger = getTrigger(event.target);
|
||
if (activeTrigger === trigger || pendingTrigger === trigger) {
|
||
closeTooltip();
|
||
}
|
||
};
|
||
const destroy = () => {
|
||
closeTooltip(false);
|
||
document.removeEventListener("pointerover", handlePointerOpen);
|
||
document.removeEventListener("pointerout", handleLeave);
|
||
document.removeEventListener("focusin", handleFocusIn);
|
||
document.removeEventListener("focusout", handleLeave);
|
||
document.removeEventListener("keydown", handleKeyDown);
|
||
document.removeEventListener("contextmenu", handleContextMenu);
|
||
tooltip = null;
|
||
};
|
||
|
||
document.addEventListener("pointerover", handlePointerOpen);
|
||
document.addEventListener("pointerout", handleLeave);
|
||
document.addEventListener("focusin", handleFocusIn);
|
||
document.addEventListener("focusout", handleLeave);
|
||
document.addEventListener("keydown", handleKeyDown);
|
||
document.addEventListener("contextmenu", handleContextMenu);
|
||
window.addEventListener("pagehide", destroy, { once: true });
|
||
})();
|
||
</script>
|
||
<script id="codex-visualization-lucide" async src="https://unpkg.com/[email protected]/dist/umd/lucide.js"></script>
|
||
<script>
|
||
(() => {
|
||
const initialize = () => {
|
||
globalThis.lucide?.createIcons({ attrs: { width: 16, height: 16 } });
|
||
};
|
||
if (globalThis.lucide != null) {
|
||
initialize();
|
||
return;
|
||
}
|
||
document
|
||
.getElementById("codex-visualization-lucide")
|
||
?.addEventListener("load", initialize, { once: true });
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|
||
"></iframe>
|
||
<script>(() => {
|
||
const frame = document.getElementById("codex-visualization");
|
||
if (!(frame instanceof HTMLIFrameElement)) {
|
||
return;
|
||
}
|
||
let viewportHeight = globalThis.innerHeight;
|
||
|
||
const channel = new MessageChannel();
|
||
channel.port1.addEventListener("message", (event) => {
|
||
const data = event.data;
|
||
if (typeof data !== "object" || data == null) {
|
||
return;
|
||
}
|
||
if (data.type === "height") {
|
||
const height = data.height;
|
||
if (
|
||
typeof height === "number" &&
|
||
Number.isFinite(height) &&
|
||
height >= 0 &&
|
||
height <= 10_000
|
||
) {
|
||
frame.style.height = Math.ceil(height) + "px";
|
||
}
|
||
return;
|
||
}
|
||
if (data.type === "scroll-to") {
|
||
const top = data.top;
|
||
if (
|
||
typeof top === "number" &&
|
||
Number.isFinite(top) &&
|
||
top >= 0 &&
|
||
globalThis.navigator.userActivation?.isActive === true
|
||
) {
|
||
globalThis.scrollTo({
|
||
top: Math.ceil(
|
||
globalThis.scrollY + frame.getBoundingClientRect().top + top,
|
||
),
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
if (data.type === "open-external") {
|
||
const rawHref = data.href;
|
||
if (typeof rawHref !== "string") {
|
||
return;
|
||
}
|
||
let href;
|
||
try {
|
||
const url = new URL(rawHref, document.baseURI);
|
||
if (url.protocol !== "https:") {
|
||
return;
|
||
}
|
||
href = url.href;
|
||
} catch {
|
||
return;
|
||
}
|
||
if (globalThis.navigator.userActivation?.isActive === true) {
|
||
const anchor = document.createElement("a");
|
||
anchor.href = href;
|
||
anchor.rel = "noopener noreferrer";
|
||
anchor.target = "_blank";
|
||
document.body.appendChild(anchor);
|
||
anchor.click();
|
||
anchor.remove();
|
||
}
|
||
return;
|
||
}
|
||
if (data.type === "follow-up") {
|
||
const prompt = data.prompt;
|
||
if (
|
||
typeof prompt !== "string" ||
|
||
globalThis.navigator.userActivation?.isActive !== true
|
||
) {
|
||
return;
|
||
}
|
||
const title = data.title;
|
||
globalThis.prompt(typeof title === "string" ? title : prompt, prompt);
|
||
}
|
||
});
|
||
channel.port1.start();
|
||
const srcdoc = frame.dataset.srcdoc;
|
||
if (srcdoc == null) {
|
||
return;
|
||
}
|
||
globalThis.addEventListener("resize", () => {
|
||
const height = Number.parseFloat(frame.style.height);
|
||
if (!Number.isFinite(height) || height <= viewportHeight) {
|
||
frame.style.removeProperty("height");
|
||
}
|
||
viewportHeight = globalThis.innerHeight;
|
||
channel.port1.postMessage({ type: "measure" });
|
||
});
|
||
frame.addEventListener(
|
||
"load",
|
||
() => {
|
||
frame.contentWindow?.postMessage(
|
||
{ type: "codex-visualization-initialize" },
|
||
"*",
|
||
[channel.port2],
|
||
);
|
||
},
|
||
{ once: true },
|
||
);
|
||
frame.srcdoc = srcdoc;
|
||
delete frame.dataset.srcdoc;
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|