mdx-artifacts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/README.zh-CN.md +129 -0
- package/agents/AGENTS.snippet.md +13 -0
- package/artifact-docs/examples/commentable-feedback.mdx +126 -0
- package/artifact-docs/examples/decision-matrix.mdx +80 -0
- package/artifact-docs/examples/layout-composition.mdx +216 -0
- package/artifact-docs/examples/streamlit-style-mixed.mdx +183 -0
- package/dist/lib/cli/artifact-state.d.ts +27 -0
- package/dist/lib/cli/artifact-state.js +115 -0
- package/dist/lib/cli/build.d.ts +1 -0
- package/dist/lib/cli/build.js +25 -0
- package/dist/lib/cli/components.d.ts +3 -0
- package/dist/lib/cli/components.js +58 -0
- package/dist/lib/cli/config.d.ts +2 -0
- package/dist/lib/cli/config.js +36 -0
- package/dist/lib/cli/dev.d.ts +1 -0
- package/dist/lib/cli/dev.js +24 -0
- package/dist/lib/cli/index.d.ts +2 -0
- package/dist/lib/cli/index.js +69 -0
- package/dist/lib/cli/review.d.ts +33 -0
- package/dist/lib/cli/review.js +390 -0
- package/dist/lib/cli/scaffold.d.ts +1 -0
- package/dist/lib/cli/scaffold.js +56 -0
- package/dist/lib/cli/types.d.ts +7 -0
- package/dist/lib/cli/types.js +1 -0
- package/dist/lib/cli/validate.d.ts +6 -0
- package/dist/lib/cli/validate.js +79 -0
- package/dist/lib/cli/vite-artifact.d.ts +13 -0
- package/dist/lib/cli/vite-artifact.js +213 -0
- package/dist/lib/react/components/AnnotatedCode.d.ts +19 -0
- package/dist/lib/react/components/AnnotatedCode.js +30 -0
- package/dist/lib/react/components/ArtifactState.d.ts +68 -0
- package/dist/lib/react/components/ArtifactState.js +286 -0
- package/dist/lib/react/components/Callout.d.ts +9 -0
- package/dist/lib/react/components/Callout.js +21 -0
- package/dist/lib/react/components/CodeBlock.d.ts +10 -0
- package/dist/lib/react/components/CodeBlock.js +28 -0
- package/dist/lib/react/components/Comments.d.ts +53 -0
- package/dist/lib/react/components/Comments.js +613 -0
- package/dist/lib/react/components/ComparisonSet.d.ts +24 -0
- package/dist/lib/react/components/ComparisonSet.js +30 -0
- package/dist/lib/react/components/DecisionMatrix.d.ts +16 -0
- package/dist/lib/react/components/DecisionMatrix.js +27 -0
- package/dist/lib/react/components/DiffBlock.d.ts +15 -0
- package/dist/lib/react/components/DiffBlock.js +24 -0
- package/dist/lib/react/components/ExportPanel.d.ts +7 -0
- package/dist/lib/react/components/ExportPanel.js +84 -0
- package/dist/lib/react/components/InlineText.d.ts +9 -0
- package/dist/lib/react/components/InlineText.js +18 -0
- package/dist/lib/react/components/Layout.d.ts +44 -0
- package/dist/lib/react/components/Layout.js +28 -0
- package/dist/lib/react/components/MarkdownBody.d.ts +7 -0
- package/dist/lib/react/components/MarkdownBody.js +36 -0
- package/dist/lib/react/components/OptionGrid.d.ts +13 -0
- package/dist/lib/react/components/OptionGrid.js +21 -0
- package/dist/lib/react/components/Section.d.ts +7 -0
- package/dist/lib/react/components/Section.js +41 -0
- package/dist/lib/react/components/SeverityBadge.d.ts +7 -0
- package/dist/lib/react/components/SeverityBadge.js +14 -0
- package/dist/lib/react/index.d.ts +33 -0
- package/dist/lib/react/index.js +16 -0
- package/dist/lib/react/registry.d.ts +24 -0
- package/dist/lib/react/registry.js +1002 -0
- package/dist/lib/react/styles.css +1417 -0
- package/docs/component-protocol.md +273 -0
- package/docs/component-taxonomy.md +273 -0
- package/docs/design.md +239 -0
- package/docs/design.zh-CN.md +217 -0
- package/docs/naming.md +123 -0
- package/docs/testing.md +138 -0
- package/package.json +90 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InlineConfig, ViteDevServer } from "vite";
|
|
2
|
+
import { type ArtifactRoute } from "./artifact-state";
|
|
3
|
+
import type { ArtifactKitConfig } from "./types";
|
|
4
|
+
export type ArtifactProject = {
|
|
5
|
+
artifact: ArtifactRoute;
|
|
6
|
+
tmpDir: string;
|
|
7
|
+
distDir: string;
|
|
8
|
+
config: InlineConfig;
|
|
9
|
+
cleanup: () => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
export declare function createArtifactProject(projectRoot: string, mdxPath: string, config: Required<ArtifactKitConfig>): Promise<ArtifactProject>;
|
|
12
|
+
export declare function startDevServer(project: ArtifactProject): Promise<ViteDevServer>;
|
|
13
|
+
export declare function buildArtifact(project: ArtifactProject): Promise<string>;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import mdx from "@mdx-js/rollup";
|
|
2
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createServer, build as viteBuild } from "vite";
|
|
9
|
+
import { createArtifactMeta, createArtifactRoute, readArtifactState, writeArtifactState } from "./artifact-state.js";
|
|
10
|
+
const packageCliDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const defaultStylesPath = path.resolve(packageCliDir, "../react/styles.css");
|
|
12
|
+
export async function createArtifactProject(projectRoot, mdxPath, config) {
|
|
13
|
+
const tmpDir = path.join(projectRoot, ".artifact-kit", "tmp", randomUUID());
|
|
14
|
+
const artifact = createArtifactRoute(projectRoot, mdxPath, config.docsDir);
|
|
15
|
+
const srcDir = path.join(tmpDir, "src");
|
|
16
|
+
const distDir = path.join(tmpDir, "dist");
|
|
17
|
+
const entryPath = path.join(srcDir, "entry.tsx");
|
|
18
|
+
const mdxImport = toRelativeImport(entryPath, mdxPath);
|
|
19
|
+
const styleImports = createStyleImports(projectRoot, entryPath, config);
|
|
20
|
+
const reactEntryImport = toRelativeImport(entryPath, await resolveReactEntryPath());
|
|
21
|
+
await mkdir(srcDir, { recursive: true });
|
|
22
|
+
await writeFile(path.join(tmpDir, "index.html"), `<!doctype html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="UTF-8" />
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
27
|
+
<title>MDX Artifact</title>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div id="root"></div>
|
|
31
|
+
<script type="module" src="/src/entry.tsx"></script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
`);
|
|
35
|
+
await writeFile(entryPath, `import React from "react";
|
|
36
|
+
import { createRoot } from "react-dom/client";
|
|
37
|
+
import { ArtifactStateProvider, CommentLayer } from "${reactEntryImport}";
|
|
38
|
+
import Doc from "${mdxImport}";
|
|
39
|
+
${styleImports}
|
|
40
|
+
|
|
41
|
+
function App() {
|
|
42
|
+
return (
|
|
43
|
+
<main className="ak-shell">
|
|
44
|
+
<article className="ak-document">
|
|
45
|
+
<ArtifactStateProvider>
|
|
46
|
+
<CommentLayer>
|
|
47
|
+
<Doc />
|
|
48
|
+
</CommentLayer>
|
|
49
|
+
</ArtifactStateProvider>
|
|
50
|
+
</article>
|
|
51
|
+
</main>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
createRoot(document.getElementById("root")!).render(<App />);
|
|
56
|
+
`);
|
|
57
|
+
const viteConfig = {
|
|
58
|
+
root: tmpDir,
|
|
59
|
+
logLevel: "warn",
|
|
60
|
+
plugins: [artifactStatePlugin(projectRoot, artifact), react(), mdx(), tailwindcss()],
|
|
61
|
+
server: {
|
|
62
|
+
port: config.port,
|
|
63
|
+
fs: {
|
|
64
|
+
allow: [projectRoot]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
build: {
|
|
68
|
+
outDir: distDir,
|
|
69
|
+
emptyOutDir: true,
|
|
70
|
+
cssCodeSplit: false,
|
|
71
|
+
assetsInlineLimit: Number.MAX_SAFE_INTEGER,
|
|
72
|
+
rollupOptions: {
|
|
73
|
+
input: path.join(tmpDir, "index.html")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
artifact,
|
|
79
|
+
tmpDir,
|
|
80
|
+
distDir,
|
|
81
|
+
config: viteConfig,
|
|
82
|
+
cleanup: () => rm(tmpDir, { recursive: true, force: true })
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function artifactStatePlugin(projectRoot, artifact) {
|
|
86
|
+
return {
|
|
87
|
+
name: "artifact-kit-state",
|
|
88
|
+
configureServer(server) {
|
|
89
|
+
server.middlewares.use(async (request, response, next) => {
|
|
90
|
+
const requestUrl = new URL(request.url ?? "/", "http://localhost");
|
|
91
|
+
try {
|
|
92
|
+
if (requestUrl.pathname === "/__artifact/meta") {
|
|
93
|
+
await handleArtifactMeta(projectRoot, artifact, request, response);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (requestUrl.pathname === "/__artifact/state") {
|
|
97
|
+
await handleArtifactState(projectRoot, artifact, request, response);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
sendJson(response, 500, {
|
|
103
|
+
error: error instanceof Error ? error.message : String(error)
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
next();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function handleArtifactMeta(projectRoot, artifact, request, response) {
|
|
113
|
+
if (request.method !== "GET") {
|
|
114
|
+
sendJson(response, 405, { error: "Method not allowed." });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
sendJson(response, 200, await createArtifactMeta(projectRoot, artifact));
|
|
118
|
+
}
|
|
119
|
+
async function handleArtifactState(projectRoot, artifact, request, response) {
|
|
120
|
+
if (request.method === "GET") {
|
|
121
|
+
sendJson(response, 200, await readArtifactState(artifact));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (request.method === "POST") {
|
|
125
|
+
let value;
|
|
126
|
+
try {
|
|
127
|
+
value = JSON.parse(await readRequestBody(request));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
sendJson(response, 400, { error: "Request body must be valid JSON." });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const state = await writeArtifactState(projectRoot, artifact, value);
|
|
134
|
+
sendJson(response, 200, { ok: true, state });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
sendJson(response, 405, { error: "Method not allowed." });
|
|
138
|
+
}
|
|
139
|
+
function readRequestBody(request) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
let body = "";
|
|
142
|
+
request.setEncoding("utf8");
|
|
143
|
+
request.on("data", (chunk) => {
|
|
144
|
+
body += chunk;
|
|
145
|
+
});
|
|
146
|
+
request.on("end", () => resolve(body));
|
|
147
|
+
request.on("error", reject);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function sendJson(response, statusCode, value) {
|
|
151
|
+
response.statusCode = statusCode;
|
|
152
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
153
|
+
response.end(JSON.stringify(value, null, 2));
|
|
154
|
+
}
|
|
155
|
+
async function resolveReactEntryPath() {
|
|
156
|
+
const builtEntryPath = path.resolve(packageCliDir, "../react/index.js");
|
|
157
|
+
const sourceEntryPath = path.resolve(packageCliDir, "../react/index.ts");
|
|
158
|
+
try {
|
|
159
|
+
await access(builtEntryPath);
|
|
160
|
+
return builtEntryPath;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return sourceEntryPath;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export async function startDevServer(project) {
|
|
167
|
+
const server = await createServer(project.config);
|
|
168
|
+
await server.listen();
|
|
169
|
+
return server;
|
|
170
|
+
}
|
|
171
|
+
export async function buildArtifact(project) {
|
|
172
|
+
await viteBuild(project.config);
|
|
173
|
+
return inlineBuildAssets(project.distDir);
|
|
174
|
+
}
|
|
175
|
+
async function inlineBuildAssets(distDir) {
|
|
176
|
+
const htmlPath = path.join(distDir, "index.html");
|
|
177
|
+
let html = await readFile(htmlPath, "utf8");
|
|
178
|
+
html = await replaceAsync(html, /<link rel="stylesheet" crossorigin href="([^"]+)">/g, async (_match, href) => {
|
|
179
|
+
const css = await readFile(assetPath(distDir, href), "utf8");
|
|
180
|
+
return `<style>${css}</style>`;
|
|
181
|
+
});
|
|
182
|
+
html = await replaceAsync(html, /<script type="module" crossorigin src="([^"]+)"><\/script>/g, async (_match, src) => {
|
|
183
|
+
const js = await readFile(assetPath(distDir, src), "utf8");
|
|
184
|
+
return `<script type="module">${escapeScriptContent(js)}</script>`;
|
|
185
|
+
});
|
|
186
|
+
return html;
|
|
187
|
+
}
|
|
188
|
+
function assetPath(distDir, value) {
|
|
189
|
+
return path.join(distDir, value.replace(/^\//, ""));
|
|
190
|
+
}
|
|
191
|
+
function toRelativeImport(fromFile, targetFile) {
|
|
192
|
+
const relative = path.relative(path.dirname(fromFile), targetFile).replaceAll(path.sep, "/");
|
|
193
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
194
|
+
}
|
|
195
|
+
function createStyleImports(projectRoot, entryPath, config) {
|
|
196
|
+
const styles = [
|
|
197
|
+
...(config.includeDefaultStyles ? [defaultStylesPath] : []),
|
|
198
|
+
...config.styles.map((stylePath) => path.resolve(projectRoot, stylePath))
|
|
199
|
+
];
|
|
200
|
+
return styles.map((stylePath) => `import "${toRelativeImport(entryPath, stylePath)}";`).join("\n");
|
|
201
|
+
}
|
|
202
|
+
async function replaceAsync(source, pattern, replacer) {
|
|
203
|
+
const matches = Array.from(source.matchAll(pattern));
|
|
204
|
+
let output = source;
|
|
205
|
+
for (const match of matches) {
|
|
206
|
+
const replacement = await replacer(match[0], ...match.slice(1));
|
|
207
|
+
output = output.replace(match[0], () => replacement);
|
|
208
|
+
}
|
|
209
|
+
return output;
|
|
210
|
+
}
|
|
211
|
+
function escapeScriptContent(source) {
|
|
212
|
+
return source.replace(/<\/script/gi, "<\\/script");
|
|
213
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type SeverityLevel } from "./SeverityBadge";
|
|
2
|
+
export type CodeAnnotation = {
|
|
3
|
+
id?: string;
|
|
4
|
+
line: number;
|
|
5
|
+
body: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
severity?: SeverityLevel;
|
|
8
|
+
};
|
|
9
|
+
export type AnnotatedCodeProps = {
|
|
10
|
+
id?: string;
|
|
11
|
+
code: string;
|
|
12
|
+
annotations: CodeAnnotation[];
|
|
13
|
+
language?: string;
|
|
14
|
+
filename?: string;
|
|
15
|
+
showLineNumbers?: boolean;
|
|
16
|
+
highlightLines?: number[];
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function AnnotatedCode({ id, code, annotations, language, filename, showLineNumbers, highlightLines, className }: AnnotatedCodeProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CodeBlock } from "./CodeBlock.js";
|
|
3
|
+
import { CommentTarget } from "./Comments.js";
|
|
4
|
+
import { InlineText } from "./InlineText.js";
|
|
5
|
+
import { MarkdownBody } from "./MarkdownBody.js";
|
|
6
|
+
import { SeverityBadge } from "./SeverityBadge.js";
|
|
7
|
+
export function AnnotatedCode({ id, code, annotations, language, filename, showLineNumbers = true, highlightLines = [], className }) {
|
|
8
|
+
const annotationLines = annotations.map((annotation) => annotation.line);
|
|
9
|
+
const highlighted = Array.from(new Set([...highlightLines, ...annotationLines])).sort((a, b) => a - b);
|
|
10
|
+
const title = filename ?? (language ? `${language} annotated code` : "Annotated code");
|
|
11
|
+
const targetId = id ?? `annotated-code:${slugify(filename ?? language ?? code)}`;
|
|
12
|
+
return (_jsx(CommentTarget, { className: classNames("ak-comment-target-section", className), description: "AnnotatedCode component", targetId: targetId, title: title, children: _jsxs("section", { className: "ak-annotated-code", children: [_jsx(CodeBlock, { code: code, filename: filename, highlightLines: highlighted, id: id ? `${id}.code` : undefined, language: language, showLineNumbers: showLineNumbers }), annotations.length > 0 ? (_jsx("div", { className: "ak-annotation-list", children: annotations.map((annotation) => {
|
|
13
|
+
const annotationTargetId = id
|
|
14
|
+
? `${id}.${annotation.id ?? `line-${annotation.line}`}`
|
|
15
|
+
: `annotation:${slugify(title)}:${annotation.line}:${slugify(annotation.title ?? annotation.body)}`;
|
|
16
|
+
return (_jsx(CommentTarget, { className: "ak-comment-target-section", description: `Annotation for line ${annotation.line}`, targetId: annotationTargetId, title: annotation.title ?? `Line ${annotation.line}`, children: _jsxs("article", { className: "ak-annotation", "data-line": annotation.line, children: [_jsxs("div", { className: "ak-annotation-header", children: [_jsxs("span", { className: "ak-annotation-line", children: ["Line ", annotation.line] }), _jsx(SeverityBadge, { level: annotation.severity ?? "info" })] }), annotation.title ? _jsx(InlineText, { as: "h3", text: annotation.title, variant: "subtitle" }) : null, _jsx(MarkdownBody, { body: annotation.body, variant: "compact" })] }) }, `${annotation.line}-${annotation.id ?? annotation.title ?? annotation.body}`));
|
|
17
|
+
}) })) : null] }) }));
|
|
18
|
+
}
|
|
19
|
+
function classNames(...values) {
|
|
20
|
+
return values.filter(Boolean).join(" ");
|
|
21
|
+
}
|
|
22
|
+
function slugify(value) {
|
|
23
|
+
const slug = value
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[`*_~[\]()]/g, "")
|
|
26
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
27
|
+
.replace(/^-+|-+$/g, "")
|
|
28
|
+
.slice(0, 64);
|
|
29
|
+
return slug || "item";
|
|
30
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type ArtifactStateProviderProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
};
|
|
5
|
+
export type ArtifactStateMeta = {
|
|
6
|
+
sourcePath: string;
|
|
7
|
+
statePath: string;
|
|
8
|
+
writable: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type ArtifactStateValue = {
|
|
11
|
+
version: number;
|
|
12
|
+
source: string;
|
|
13
|
+
threads: ArtifactStateThread[];
|
|
14
|
+
interactions: Record<string, unknown>;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
export type ArtifactStateThread = {
|
|
18
|
+
id: string;
|
|
19
|
+
anchorId: string;
|
|
20
|
+
status: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
messages: ArtifactStateMessage[];
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
export type ArtifactStateMessage = {
|
|
27
|
+
id: string;
|
|
28
|
+
role: "user" | "assistant";
|
|
29
|
+
body: string;
|
|
30
|
+
createdAt?: string;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
export type ArtifactStateComment = {
|
|
34
|
+
id: string;
|
|
35
|
+
blockId: string;
|
|
36
|
+
blockTitle: string;
|
|
37
|
+
blockDescription?: string;
|
|
38
|
+
comment: string;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
};
|
|
41
|
+
export type ArtifactStateReviewThread = {
|
|
42
|
+
id: string;
|
|
43
|
+
blockId: string;
|
|
44
|
+
blockTitle: string;
|
|
45
|
+
blockDescription?: string;
|
|
46
|
+
status: string;
|
|
47
|
+
messages: ArtifactStateMessage[];
|
|
48
|
+
};
|
|
49
|
+
export type ArtifactStateStatus = "loading" | "static" | "ready" | "saving" | "saved" | "error";
|
|
50
|
+
type ArtifactStateContextValue = {
|
|
51
|
+
state?: ArtifactStateValue;
|
|
52
|
+
actions: {
|
|
53
|
+
saveComments: (comments: ArtifactStateComment[]) => Promise<void>;
|
|
54
|
+
saveThreads: (threads: ArtifactStateReviewThread[]) => Promise<void>;
|
|
55
|
+
saveState: (state: unknown) => Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
meta: {
|
|
58
|
+
daemon?: ArtifactStateMeta;
|
|
59
|
+
status: ArtifactStateStatus;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export declare function ArtifactStateProvider({ children }: ArtifactStateProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
63
|
+
export declare function useOptionalArtifactState(): ArtifactStateContextValue | null;
|
|
64
|
+
export declare function createArtifactStateFromComments(comments: ArtifactStateComment[], currentState: unknown, sourcePath: string): ArtifactStateValue;
|
|
65
|
+
export declare function createArtifactStateFromThreads(threads: ArtifactStateReviewThread[], currentState: unknown, sourcePath: string): ArtifactStateValue;
|
|
66
|
+
export declare function createArtifactCommentsFromState(value: unknown): ArtifactStateComment[];
|
|
67
|
+
export declare function createArtifactThreadsFromState(value: unknown): ArtifactStateReviewThread[];
|
|
68
|
+
export {};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
const ArtifactStateContext = createContext(null);
|
|
4
|
+
export function ArtifactStateProvider({ children }) {
|
|
5
|
+
const [daemonMeta, setDaemonMeta] = useState();
|
|
6
|
+
const [state, setState] = useState();
|
|
7
|
+
const [status, setStatus] = useState("loading");
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
let active = true;
|
|
10
|
+
async function loadArtifactState() {
|
|
11
|
+
if (typeof fetch !== "function") {
|
|
12
|
+
setStatus("static");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const metaResponse = await fetch("/__artifact/meta", { headers: { accept: "application/json" } });
|
|
17
|
+
if (!metaResponse.ok) {
|
|
18
|
+
setStatus("static");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const nextMeta = await metaResponse.json();
|
|
22
|
+
if (!isArtifactStateMeta(nextMeta) || !nextMeta.writable) {
|
|
23
|
+
setStatus("static");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const stateResponse = await fetch("/__artifact/state", { headers: { accept: "application/json" } });
|
|
27
|
+
const nextState = stateResponse.ok ? normalizeArtifactState(await stateResponse.json(), nextMeta.sourcePath) : createEmptyArtifactState(nextMeta.sourcePath);
|
|
28
|
+
if (active) {
|
|
29
|
+
setDaemonMeta(nextMeta);
|
|
30
|
+
setState(nextState);
|
|
31
|
+
setStatus("ready");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
if (active) {
|
|
36
|
+
setStatus("static");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
loadArtifactState();
|
|
41
|
+
return () => {
|
|
42
|
+
active = false;
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!daemonMeta || typeof window === "undefined" || typeof fetch !== "function") {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const interval = window.setInterval(async () => {
|
|
50
|
+
if (status === "saving") {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch("/__artifact/state", { headers: { accept: "application/json" } });
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const nextState = normalizeArtifactState(await response.json(), daemonMeta.sourcePath);
|
|
59
|
+
setState((current) => (sameArtifactState(current, nextState) ? current : nextState));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Keep the current in-page state when the local daemon is temporarily unavailable.
|
|
63
|
+
}
|
|
64
|
+
}, 2000);
|
|
65
|
+
return () => window.clearInterval(interval);
|
|
66
|
+
}, [daemonMeta, status]);
|
|
67
|
+
const value = useMemo(() => ({
|
|
68
|
+
state,
|
|
69
|
+
actions: {
|
|
70
|
+
async saveComments(comments) {
|
|
71
|
+
if (!daemonMeta) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const nextState = createArtifactStateFromComments(comments, state, daemonMeta.sourcePath);
|
|
75
|
+
await saveArtifactState(nextState, daemonMeta, setStatus, setState);
|
|
76
|
+
},
|
|
77
|
+
async saveThreads(threads) {
|
|
78
|
+
if (!daemonMeta) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const nextState = createArtifactStateFromThreads(threads, state, daemonMeta.sourcePath);
|
|
82
|
+
await saveArtifactState(nextState, daemonMeta, setStatus, setState);
|
|
83
|
+
},
|
|
84
|
+
async saveState(nextState) {
|
|
85
|
+
if (!daemonMeta) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await saveArtifactState(normalizeArtifactState(nextState, daemonMeta.sourcePath), daemonMeta, setStatus, setState);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
meta: {
|
|
92
|
+
daemon: daemonMeta,
|
|
93
|
+
status
|
|
94
|
+
}
|
|
95
|
+
}), [daemonMeta, state, status]);
|
|
96
|
+
return _jsx(ArtifactStateContext.Provider, { value: value, children: children });
|
|
97
|
+
}
|
|
98
|
+
export function useOptionalArtifactState() {
|
|
99
|
+
return useContext(ArtifactStateContext);
|
|
100
|
+
}
|
|
101
|
+
export function createArtifactStateFromComments(comments, currentState, sourcePath) {
|
|
102
|
+
return createArtifactStateFromThreads(comments.map(createReviewThreadFromComment), currentState, sourcePath);
|
|
103
|
+
}
|
|
104
|
+
export function createArtifactStateFromThreads(threads, currentState, sourcePath) {
|
|
105
|
+
const baseState = normalizeArtifactState(currentState, sourcePath);
|
|
106
|
+
const reviewThreads = threads.map((thread) => createStateThreadFromReviewThread(thread, baseState.threads));
|
|
107
|
+
const threadAnchorIds = new Set(reviewThreads.map((thread) => thread.anchorId));
|
|
108
|
+
const retainedThreads = baseState.threads.filter((thread) => !threadAnchorIds.has(thread.anchorId));
|
|
109
|
+
return {
|
|
110
|
+
...baseState,
|
|
111
|
+
source: sourcePath,
|
|
112
|
+
threads: [...retainedThreads, ...reviewThreads]
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function createArtifactCommentsFromState(value) {
|
|
116
|
+
return createArtifactThreadsFromState(value).flatMap((thread) => {
|
|
117
|
+
const userMessage = thread.messages.find((message) => message.role === "user" && message.body.trim().length > 0);
|
|
118
|
+
if (!userMessage) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
id: userMessage.id,
|
|
124
|
+
blockId: thread.blockId,
|
|
125
|
+
blockTitle: thread.blockTitle,
|
|
126
|
+
blockDescription: thread.blockDescription,
|
|
127
|
+
comment: userMessage.body.trim(),
|
|
128
|
+
createdAt: userMessage.createdAt ?? ""
|
|
129
|
+
}
|
|
130
|
+
];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export function createArtifactThreadsFromState(value) {
|
|
134
|
+
if (!isRecord(value) || !Array.isArray(value.threads)) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
return value.threads.flatMap((thread) => createArtifactThreadFromStateThread(thread));
|
|
138
|
+
}
|
|
139
|
+
async function saveArtifactState(nextState, daemonMeta, setStatus, setState) {
|
|
140
|
+
setStatus("saving");
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch("/__artifact/state", {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "content-type": "application/json" },
|
|
145
|
+
body: JSON.stringify(nextState)
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error("Failed to save artifact state.");
|
|
149
|
+
}
|
|
150
|
+
const payload = await response.json();
|
|
151
|
+
const savedState = isRecord(payload) && "state" in payload ? payload.state : nextState;
|
|
152
|
+
setState(normalizeArtifactState(savedState, daemonMeta.sourcePath));
|
|
153
|
+
setStatus("saved");
|
|
154
|
+
window.setTimeout(() => setStatus("ready"), 1200);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
setStatus("error");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function normalizeArtifactState(value, sourcePath) {
|
|
161
|
+
if (!isRecord(value)) {
|
|
162
|
+
return createEmptyArtifactState(sourcePath);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
...value,
|
|
166
|
+
version: typeof value.version === "number" ? value.version : 1,
|
|
167
|
+
source: sourcePath,
|
|
168
|
+
threads: normalizeThreads(value.threads),
|
|
169
|
+
interactions: isRecord(value.interactions) ? value.interactions : {}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function sameArtifactState(first, second) {
|
|
173
|
+
return JSON.stringify(first) === JSON.stringify(second);
|
|
174
|
+
}
|
|
175
|
+
function createEmptyArtifactState(sourcePath) {
|
|
176
|
+
return {
|
|
177
|
+
version: 1,
|
|
178
|
+
source: sourcePath,
|
|
179
|
+
threads: [],
|
|
180
|
+
interactions: {}
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function createReviewThreadFromComment(comment) {
|
|
184
|
+
return {
|
|
185
|
+
id: `thread-${comment.id}`,
|
|
186
|
+
blockId: comment.blockId,
|
|
187
|
+
blockTitle: comment.blockTitle,
|
|
188
|
+
blockDescription: comment.blockDescription,
|
|
189
|
+
status: "open",
|
|
190
|
+
messages: [
|
|
191
|
+
{
|
|
192
|
+
id: comment.id,
|
|
193
|
+
role: "user",
|
|
194
|
+
body: comment.comment,
|
|
195
|
+
createdAt: comment.createdAt
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function createStateThreadFromReviewThread(thread, currentThreads) {
|
|
201
|
+
const currentThread = currentThreads.find((item) => item.anchorId === thread.blockId);
|
|
202
|
+
const currentMessages = currentThread?.messages ?? [];
|
|
203
|
+
const messages = thread.messages.map((message) => {
|
|
204
|
+
const currentMessage = currentMessages.find((item) => item.id === message.id) ??
|
|
205
|
+
(message.role === "user" ? currentMessages.find((item) => item.role === "user") : undefined);
|
|
206
|
+
return {
|
|
207
|
+
...currentMessage,
|
|
208
|
+
id: currentMessage?.id ?? message.id,
|
|
209
|
+
role: message.role,
|
|
210
|
+
body: message.body,
|
|
211
|
+
createdAt: currentMessage?.createdAt ?? message.createdAt
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
const messageIds = new Set(messages.map((message) => message.id));
|
|
215
|
+
const retainedAssistantMessages = currentMessages.filter((message) => message.role === "assistant" && !messageIds.has(message.id));
|
|
216
|
+
return {
|
|
217
|
+
...currentThread,
|
|
218
|
+
id: resolveThreadId(currentThread?.id, thread.blockId),
|
|
219
|
+
anchorId: thread.blockId,
|
|
220
|
+
status: thread.status,
|
|
221
|
+
title: thread.blockTitle,
|
|
222
|
+
description: thread.blockDescription,
|
|
223
|
+
messages: [...messages, ...retainedAssistantMessages]
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function createArtifactThreadFromStateThread(thread) {
|
|
227
|
+
if (!isRecord(thread) || typeof thread.anchorId !== "string" || !Array.isArray(thread.messages)) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const typedThread = thread;
|
|
231
|
+
const messages = thread.messages.filter(isStateMessage);
|
|
232
|
+
if (messages.length === 0) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
return [
|
|
236
|
+
{
|
|
237
|
+
id: typedThread.id,
|
|
238
|
+
blockId: typedThread.anchorId,
|
|
239
|
+
blockTitle: typeof typedThread.title === "string" ? typedThread.title : typedThread.anchorId,
|
|
240
|
+
blockDescription: typeof typedThread.description === "string" ? typedThread.description : undefined,
|
|
241
|
+
status: typeof typedThread.status === "string" ? typedThread.status : "open",
|
|
242
|
+
messages
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
function normalizeThreads(value) {
|
|
247
|
+
if (!Array.isArray(value)) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
return value.filter(isArtifactStateThread);
|
|
251
|
+
}
|
|
252
|
+
function isArtifactStateThread(value) {
|
|
253
|
+
if (!isRecord(value)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
return typeof value.id === "string" && typeof value.anchorId === "string" && Array.isArray(value.messages);
|
|
257
|
+
}
|
|
258
|
+
function isStateMessage(value) {
|
|
259
|
+
return (isRecord(value) &&
|
|
260
|
+
typeof value.id === "string" &&
|
|
261
|
+
(value.role === "user" || value.role === "assistant") &&
|
|
262
|
+
typeof value.body === "string" &&
|
|
263
|
+
value.body.trim().length > 0);
|
|
264
|
+
}
|
|
265
|
+
function isArtifactStateMeta(value) {
|
|
266
|
+
return (isRecord(value) &&
|
|
267
|
+
typeof value.sourcePath === "string" &&
|
|
268
|
+
typeof value.statePath === "string" &&
|
|
269
|
+
typeof value.writable === "boolean");
|
|
270
|
+
}
|
|
271
|
+
function resolveThreadId(currentId, anchorId) {
|
|
272
|
+
if (currentId && currentId.startsWith("thr") && currentId.length <= 32) {
|
|
273
|
+
return currentId;
|
|
274
|
+
}
|
|
275
|
+
return `thr_${compactId(anchorId)}`;
|
|
276
|
+
}
|
|
277
|
+
function compactId(value) {
|
|
278
|
+
let hash = 5381;
|
|
279
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
280
|
+
hash = (hash * 33) ^ value.charCodeAt(index);
|
|
281
|
+
}
|
|
282
|
+
return (hash >>> 0).toString(36);
|
|
283
|
+
}
|
|
284
|
+
function isRecord(value) {
|
|
285
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
286
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type CalloutTone = "info" | "success" | "warning" | "danger";
|
|
2
|
+
export type CalloutProps = {
|
|
3
|
+
id?: string;
|
|
4
|
+
body: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
tone?: CalloutTone;
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function Callout({ id, body, title, tone, className }: CalloutProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CommentTarget } from "./Comments.js";
|
|
3
|
+
import { InlineText } from "./InlineText.js";
|
|
4
|
+
import { MarkdownBody } from "./MarkdownBody.js";
|
|
5
|
+
export function Callout({ id, body, title, tone = "info", className }) {
|
|
6
|
+
const displayTitle = title ?? `${tone} callout`;
|
|
7
|
+
const targetId = id ?? `callout:${slugify(displayTitle)}:${slugify(body)}`;
|
|
8
|
+
return (_jsx(CommentTarget, { className: classNames("ak-comment-target-section", className), description: "Callout component", targetId: targetId, title: displayTitle, children: _jsxs("aside", { className: classNames("ak-callout", `ak-callout-${tone}`), children: [title ? _jsx(InlineText, { as: "h3", text: title, variant: "subtitle" }) : null, _jsx(MarkdownBody, { body: body, variant: "compact" })] }) }));
|
|
9
|
+
}
|
|
10
|
+
function classNames(...values) {
|
|
11
|
+
return values.filter(Boolean).join(" ");
|
|
12
|
+
}
|
|
13
|
+
function slugify(value) {
|
|
14
|
+
const slug = value
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[`*_~[\]()]/g, "")
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "")
|
|
19
|
+
.slice(0, 64);
|
|
20
|
+
return slug || "item";
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CodeBlockProps = {
|
|
2
|
+
id?: string;
|
|
3
|
+
code: string;
|
|
4
|
+
language?: string;
|
|
5
|
+
filename?: string;
|
|
6
|
+
showLineNumbers?: boolean;
|
|
7
|
+
highlightLines?: number[];
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function CodeBlock({ id, code, language, filename, showLineNumbers, highlightLines, className }: CodeBlockProps): import("react/jsx-runtime").JSX.Element;
|