slidev-addon-agent 0.0.1
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 +54 -0
- package/agent/constants.ts +119 -0
- package/agent/deck-context.ts +67 -0
- package/agent/index.ts +201 -0
- package/agent/middleware.ts +163 -0
- package/agent/skills/slidev/README.md +61 -0
- package/agent/skills/slidev/SKILL.md +189 -0
- package/agent/skills/slidev/references/animation-click-marker.md +37 -0
- package/agent/skills/slidev/references/animation-drawing.md +68 -0
- package/agent/skills/slidev/references/animation-rough-marker.md +53 -0
- package/agent/skills/slidev/references/api-slide-hooks.md +37 -0
- package/agent/skills/slidev/references/build-og-image.md +36 -0
- package/agent/skills/slidev/references/build-pdf.md +40 -0
- package/agent/skills/slidev/references/build-remote-assets.md +34 -0
- package/agent/skills/slidev/references/build-seo-meta.md +43 -0
- package/agent/skills/slidev/references/code-groups.md +64 -0
- package/agent/skills/slidev/references/code-import-snippet.md +55 -0
- package/agent/skills/slidev/references/code-line-highlighting.md +50 -0
- package/agent/skills/slidev/references/code-line-numbers.md +46 -0
- package/agent/skills/slidev/references/code-magic-move.md +57 -0
- package/agent/skills/slidev/references/code-max-height.md +37 -0
- package/agent/skills/slidev/references/code-twoslash.md +42 -0
- package/agent/skills/slidev/references/core-animations.md +196 -0
- package/agent/skills/slidev/references/core-cli.md +140 -0
- package/agent/skills/slidev/references/core-components.md +197 -0
- package/agent/skills/slidev/references/core-exporting.md +148 -0
- package/agent/skills/slidev/references/core-frontmatter.md +195 -0
- package/agent/skills/slidev/references/core-global-context.md +155 -0
- package/agent/skills/slidev/references/core-headmatter.md +188 -0
- package/agent/skills/slidev/references/core-hosting.md +152 -0
- package/agent/skills/slidev/references/core-layouts.md +286 -0
- package/agent/skills/slidev/references/core-syntax.md +155 -0
- package/agent/skills/slidev/references/diagram-latex.md +55 -0
- package/agent/skills/slidev/references/diagram-mermaid.md +44 -0
- package/agent/skills/slidev/references/diagram-plantuml.md +45 -0
- package/agent/skills/slidev/references/editor-monaco-run.md +44 -0
- package/agent/skills/slidev/references/editor-monaco-write.md +24 -0
- package/agent/skills/slidev/references/editor-monaco.md +50 -0
- package/agent/skills/slidev/references/editor-prettier.md +40 -0
- package/agent/skills/slidev/references/editor-side.md +23 -0
- package/agent/skills/slidev/references/editor-vscode.md +55 -0
- package/agent/skills/slidev/references/layout-canvas-size.md +25 -0
- package/agent/skills/slidev/references/layout-draggable.md +57 -0
- package/agent/skills/slidev/references/layout-global-layers.md +50 -0
- package/agent/skills/slidev/references/layout-slots.md +75 -0
- package/agent/skills/slidev/references/layout-transform.md +33 -0
- package/agent/skills/slidev/references/layout-zoom.md +39 -0
- package/agent/skills/slidev/references/presenter-notes-ruby.md +35 -0
- package/agent/skills/slidev/references/presenter-recording.md +30 -0
- package/agent/skills/slidev/references/presenter-remote.md +40 -0
- package/agent/skills/slidev/references/presenter-timer.md +34 -0
- package/agent/skills/slidev/references/style-direction.md +34 -0
- package/agent/skills/slidev/references/style-icons.md +46 -0
- package/agent/skills/slidev/references/style-scoped.md +50 -0
- package/agent/skills/slidev/references/syntax-block-frontmatter.md +39 -0
- package/agent/skills/slidev/references/syntax-frontmatter-merging.md +49 -0
- package/agent/skills/slidev/references/syntax-importing-slides.md +60 -0
- package/agent/skills/slidev/references/syntax-mdc.md +51 -0
- package/agent/skills/slidev/references/tool-eject-theme.md +27 -0
- package/agent/tools/export-tool.ts +216 -0
- package/agent/tools/review-tool.ts +136 -0
- package/app/index.ts +124 -0
- package/components/MessageItem.vue +231 -0
- package/components/SlidevAgentNavButton.vue +48 -0
- package/components/SlidevAgentSidebar.vue +766 -0
- package/components/SubagentCard.vue +184 -0
- package/components/TypingDots.vue +62 -0
- package/dist/agent/constants.js +117 -0
- package/dist/agent/deck-context.js +47 -0
- package/dist/agent/index.js +167 -0
- package/dist/agent/middleware.js +134 -0
- package/dist/agent/slide-preview-tool.js +257 -0
- package/dist/agent/tools/export-tool.js +167 -0
- package/dist/agent/tools/review-tool.js +111 -0
- package/dist/app/index.js +101 -0
- package/dist/bin/slidev-agent.js +155 -0
- package/dist/lib/bridge.js +151 -0
- package/dist/lib/env.js +17 -0
- package/dist/lib/headless-tools.js +10 -0
- package/dist/lib/langgraph-init.js +59 -0
- package/dist/lib/review-tool.js +98 -0
- package/lib/bridge.ts +212 -0
- package/lib/config.ts +79 -0
- package/lib/env.ts +38 -0
- package/lib/headless-tool-impl.ts +26 -0
- package/lib/headless-tools.ts +11 -0
- package/lib/langgraph-init.ts +79 -0
- package/lib/messages.ts +169 -0
- package/lib/render-chat-markdown.ts +19 -0
- package/lib/sidebar.ts +573 -0
- package/lib/state.ts +44 -0
- package/package.json +65 -0
- package/public/deepagents.svg +12 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { mkdir, readFile, readdir, realpath } from "node:fs/promises"
|
|
3
|
+
import { spawn } from "node:child_process"
|
|
4
|
+
import { createRequire } from "node:module"
|
|
5
|
+
import { pathToFileURL } from "node:url"
|
|
6
|
+
|
|
7
|
+
import { tool } from "langchain"
|
|
8
|
+
import { z } from "zod"
|
|
9
|
+
|
|
10
|
+
import { resolveDeckExecutionContext } from "../deck-context.js"
|
|
11
|
+
|
|
12
|
+
const slidevExportScreenshotSchema = z.object({
|
|
13
|
+
slideIndex: z.number().int().positive().describe("1-based slide index to export as PNG."),
|
|
14
|
+
outputDir: z.string().optional().describe("Optional output directory. Prefer omitting this unless you need a custom project-local path."),
|
|
15
|
+
timeout: z.number().int().positive().optional().describe("Optional Slidev export timeout in milliseconds. Defaults to 60000."),
|
|
16
|
+
wait: z.number().int().nonnegative().optional().describe("Optional Slidev export wait in milliseconds. Defaults to 500."),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function isWithinRoot(rootDir: string, candidatePath: string) {
|
|
20
|
+
return candidatePath === rootDir || candidatePath.startsWith(`${rootDir}${path.sep}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function resolveOutputDir(rootDir: string, deckHostDir: string, slideIndex: number, outputDir?: string) {
|
|
24
|
+
const fallbackOutputDir = path.join(deckHostDir, ".slidev-agent-artifacts", `verify-${slideIndex}`)
|
|
25
|
+
const requestedOutputDir = outputDir?.trim() || fallbackOutputDir
|
|
26
|
+
const resolvedOutputDir = path.isAbsolute(requestedOutputDir)
|
|
27
|
+
? path.resolve(requestedOutputDir)
|
|
28
|
+
: path.resolve(deckHostDir, requestedOutputDir)
|
|
29
|
+
|
|
30
|
+
const [realRoot, realOutputDirParent] = await Promise.all([
|
|
31
|
+
realpath(rootDir).catch(() => rootDir),
|
|
32
|
+
realpath(path.dirname(resolvedOutputDir)).catch(() => path.dirname(resolvedOutputDir)),
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
if (!isWithinRoot(realRoot, realOutputDirParent) && !isWithinRoot(rootDir, resolvedOutputDir)) {
|
|
36
|
+
throw new Error(`slidev_export_screenshot outputDir must stay within the project root (${rootDir}).`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return resolvedOutputDir
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getPnpmCommand() {
|
|
43
|
+
return process.platform === "win32" ? "pnpm.cmd" : "pnpm"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type SlidevCliModule = {
|
|
47
|
+
resolveOptions?: (
|
|
48
|
+
options: { entry: string, theme?: string },
|
|
49
|
+
mode: string,
|
|
50
|
+
) => Promise<{ data?: { slides?: unknown[] } }>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ANSI_ESCAPE_PATTERN = /\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g
|
|
54
|
+
|
|
55
|
+
async function resolveDeckSlideCount(deckHostDir: string, entryPath: string) {
|
|
56
|
+
const requireFromDeck = createRequire(path.join(deckHostDir, "package.json"))
|
|
57
|
+
const slidevCliPackageJsonPath = requireFromDeck.resolve("@slidev/cli/package.json")
|
|
58
|
+
const slidevCliPackageJson = JSON.parse(
|
|
59
|
+
await readFile(slidevCliPackageJsonPath, "utf8"),
|
|
60
|
+
) as { module?: string, main?: string }
|
|
61
|
+
const slidevCliEntry = path.resolve(
|
|
62
|
+
path.dirname(slidevCliPackageJsonPath),
|
|
63
|
+
slidevCliPackageJson.module || slidevCliPackageJson.main || "dist/index.mjs",
|
|
64
|
+
)
|
|
65
|
+
const slidevCli = await import(pathToFileURL(slidevCliEntry).href) as SlidevCliModule
|
|
66
|
+
const resolved = await slidevCli.resolveOptions?.({ entry: entryPath }, "export")
|
|
67
|
+
return Array.isArray(resolved?.data?.slides) ? resolved.data.slides.length : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stripAnsi(output: string) {
|
|
71
|
+
return output.replace(ANSI_ESCAPE_PATTERN, "")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractLikelyBuildError(output: string) {
|
|
75
|
+
const cleanedOutput = stripAnsi(output)
|
|
76
|
+
const lines = cleanedOutput.split(/\r?\n/)
|
|
77
|
+
const errorStartIndex = lines.findIndex(line =>
|
|
78
|
+
/\[vite\].*(Pre-transform error|Internal server error)/.test(line)
|
|
79
|
+
|| /Plugin:\s+vite:vue/.test(line)
|
|
80
|
+
|| /Failed to resolve import/i.test(line)
|
|
81
|
+
|| /Invalid end tag\./.test(line),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (errorStartIndex === -1)
|
|
85
|
+
return null
|
|
86
|
+
|
|
87
|
+
const errorLines: string[] = []
|
|
88
|
+
for (let index = errorStartIndex; index < lines.length; index += 1) {
|
|
89
|
+
const line = lines[index]
|
|
90
|
+
|
|
91
|
+
if (index > errorStartIndex && !line.trim())
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if (/^\s*✓\s+exported to\b/.test(line))
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
errorLines.push(line)
|
|
98
|
+
|
|
99
|
+
if (errorLines.length >= 30)
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return errorLines.join("\n").trim() || null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function runCommand(cwd: string, args: string[]) {
|
|
107
|
+
return new Promise<{ exitCode: number, output: string }>((resolve, reject) => {
|
|
108
|
+
const child = spawn(getPnpmCommand(), args, {
|
|
109
|
+
cwd,
|
|
110
|
+
env: process.env,
|
|
111
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
let output = ""
|
|
115
|
+
|
|
116
|
+
child.stdout.on("data", (chunk) => {
|
|
117
|
+
output += chunk.toString()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
child.stderr.on("data", (chunk) => {
|
|
121
|
+
output += chunk.toString()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
child.on("error", reject)
|
|
125
|
+
child.on("close", (exitCode) => {
|
|
126
|
+
resolve({
|
|
127
|
+
exitCode: exitCode ?? 1,
|
|
128
|
+
output,
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createSlidevExportScreenshotTool(rootDir: string) {
|
|
135
|
+
return tool(async ({ slideIndex, outputDir, timeout, wait }) => {
|
|
136
|
+
const deckContext = resolveDeckExecutionContext(path.resolve(rootDir))
|
|
137
|
+
const resolvedOutputDir = await resolveOutputDir(deckContext.rootDir, deckContext.deckHostDir, slideIndex, outputDir)
|
|
138
|
+
const totalSlides = await resolveDeckSlideCount(deckContext.deckHostDir, deckContext.entryPath).catch(() => null)
|
|
139
|
+
|
|
140
|
+
if (totalSlides !== null && slideIndex > totalSlides) {
|
|
141
|
+
throw new Error([
|
|
142
|
+
`slidev_export_screenshot requested slide ${slideIndex}, but the resolved deck only has ${totalSlides} slides.`,
|
|
143
|
+
`Resolved deck entry: ${path.relative(deckContext.rootDir, deckContext.entryPath) || path.basename(deckContext.entryPath)}`,
|
|
144
|
+
"This usually means the slide is not imported into the final deck yet or the 1-based slide index was counted incorrectly.",
|
|
145
|
+
].join("\n"))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await mkdir(resolvedOutputDir, { recursive: true })
|
|
149
|
+
|
|
150
|
+
const args = [
|
|
151
|
+
"exec",
|
|
152
|
+
"slidev-agent",
|
|
153
|
+
"export",
|
|
154
|
+
"--format",
|
|
155
|
+
"png",
|
|
156
|
+
"--range",
|
|
157
|
+
String(slideIndex),
|
|
158
|
+
"--per-slide",
|
|
159
|
+
"--output",
|
|
160
|
+
resolvedOutputDir,
|
|
161
|
+
"--timeout",
|
|
162
|
+
String(timeout ?? 60000),
|
|
163
|
+
"--wait",
|
|
164
|
+
String(wait ?? 500),
|
|
165
|
+
"--scale",
|
|
166
|
+
"1",
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
const result = await runCommand(deckContext.deckHostDir, args)
|
|
170
|
+
if (result.exitCode !== 0) {
|
|
171
|
+
throw new Error([
|
|
172
|
+
`slidev_export_screenshot failed with exit code ${result.exitCode}.`,
|
|
173
|
+
`Deck directory: ${deckContext.deckHostDir}`,
|
|
174
|
+
`Command: ${getPnpmCommand()} ${args.join(" ")}`,
|
|
175
|
+
result.output.trim(),
|
|
176
|
+
].filter(Boolean).join("\n"))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const imageFiles = (await readdir(resolvedOutputDir))
|
|
180
|
+
.filter(fileName => fileName.toLowerCase().endsWith(".png"))
|
|
181
|
+
.sort((a, b) => a.localeCompare(b))
|
|
182
|
+
|
|
183
|
+
if (imageFiles.length === 0) {
|
|
184
|
+
const buildError = extractLikelyBuildError(result.output)
|
|
185
|
+
if (buildError) {
|
|
186
|
+
throw new Error([
|
|
187
|
+
"slidev_export_screenshot hit a Slidev/Vite build error and produced no PNG files.",
|
|
188
|
+
`Deck directory: ${deckContext.deckHostDir}`,
|
|
189
|
+
`Command: ${getPnpmCommand()} ${args.join(" ")}`,
|
|
190
|
+
buildError,
|
|
191
|
+
].join("\n"))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new Error([
|
|
195
|
+
`slidev_export_screenshot succeeded but produced no PNG files in ${resolvedOutputDir}.`,
|
|
196
|
+
totalSlides !== null ? `Resolved deck slide count: ${totalSlides}. Requested slide: ${slideIndex}.` : "",
|
|
197
|
+
"This usually means the requested 1-based slide index does not exist in the final rendered deck yet.",
|
|
198
|
+
].filter(Boolean).join("\n"))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const imagePaths = imageFiles.map(fileName => path.join(resolvedOutputDir, fileName))
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
slideIndex,
|
|
205
|
+
deckDir: deckContext.deckHostDir,
|
|
206
|
+
outputDir: resolvedOutputDir,
|
|
207
|
+
imagePaths,
|
|
208
|
+
primaryImagePath: imagePaths[0],
|
|
209
|
+
command: `${getPnpmCommand()} ${args.join(" ")}`,
|
|
210
|
+
}
|
|
211
|
+
}, {
|
|
212
|
+
name: "slidev_export_screenshot",
|
|
213
|
+
description: "Export one Slidev slide to PNG from the correct deck directory and return the generated image path.",
|
|
214
|
+
schema: slidevExportScreenshotSchema,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import { readFile, realpath, stat } from "node:fs/promises"
|
|
4
|
+
|
|
5
|
+
import { initChatModel, tool } from "langchain"
|
|
6
|
+
import { z } from "zod"
|
|
7
|
+
|
|
8
|
+
import { model } from "../../lib/env.js"
|
|
9
|
+
|
|
10
|
+
const slidevReviewScreenshotSchema = z.object({
|
|
11
|
+
imagePath: z.string().min(1).describe("Path to the exported PNG screenshot file. Use a path inside the project root, for example `.slidev-agent-artifacts/verify-5/1.png`."),
|
|
12
|
+
slideIndex: z.number().int().positive().optional().describe("Optional 1-based slide index represented by the screenshot."),
|
|
13
|
+
focus: z.string().optional().describe("Optional short note about what to scrutinize most closely."),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const slidevReviewScreenshotResultSchema = z.object({
|
|
17
|
+
pass: z.boolean().describe("Whether the slide looks presentation-ready in the screenshot."),
|
|
18
|
+
summary: z.string().describe("Short visual verdict."),
|
|
19
|
+
issues: z.array(z.string()).describe("Concrete visual problems found in the screenshot."),
|
|
20
|
+
suggestions: z.array(z.string()).describe("Targeted improvements to address the issues."),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function isWithinRoot(rootDir: string, candidatePath: string) {
|
|
24
|
+
return candidatePath === rootDir || candidatePath.startsWith(`${rootDir}${path.sep}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeImagePathInput(imagePath: string) {
|
|
28
|
+
const trimmed = imagePath.trim()
|
|
29
|
+
if (!trimmed)
|
|
30
|
+
return trimmed
|
|
31
|
+
|
|
32
|
+
if (trimmed.startsWith("file://"))
|
|
33
|
+
return fileURLToPath(trimmed)
|
|
34
|
+
|
|
35
|
+
return trimmed
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function resolveImagePath(rootDir: string, imagePath: string) {
|
|
39
|
+
const absoluteRoot = path.resolve(rootDir)
|
|
40
|
+
const normalizedImagePath = normalizeImagePathInput(imagePath)
|
|
41
|
+
const resolved = path.isAbsolute(normalizedImagePath)
|
|
42
|
+
? path.resolve(normalizedImagePath)
|
|
43
|
+
: path.resolve(absoluteRoot, normalizedImagePath)
|
|
44
|
+
|
|
45
|
+
if (resolved === absoluteRoot)
|
|
46
|
+
throw new Error("Screenshot path must point to an image file, not the project root.")
|
|
47
|
+
|
|
48
|
+
if (isWithinRoot(absoluteRoot, resolved))
|
|
49
|
+
return resolved
|
|
50
|
+
|
|
51
|
+
const [realRoot, realResolved] = await Promise.all([
|
|
52
|
+
realpath(absoluteRoot).catch(() => absoluteRoot),
|
|
53
|
+
realpath(resolved).catch(() => resolved),
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
if (!isWithinRoot(realRoot, realResolved)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Screenshot path must stay within the project root (${absoluteRoot}). Export the PNG into a project-local folder such as ".slidev-agent-artifacts/verify-<slideIndex>" and pass that PNG file path to slidev_review_screenshot.`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return realResolved
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isPngBuffer(fileBuffer: Buffer) {
|
|
66
|
+
return fileBuffer.length >= 8
|
|
67
|
+
&& fileBuffer[0] === 0x89
|
|
68
|
+
&& fileBuffer[1] === 0x50
|
|
69
|
+
&& fileBuffer[2] === 0x4E
|
|
70
|
+
&& fileBuffer[3] === 0x47
|
|
71
|
+
&& fileBuffer[4] === 0x0D
|
|
72
|
+
&& fileBuffer[5] === 0x0A
|
|
73
|
+
&& fileBuffer[6] === 0x1A
|
|
74
|
+
&& fileBuffer[7] === 0x0A
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createSlidevReviewScreenshotTool(rootDir: string) {
|
|
78
|
+
return tool(async ({ imagePath, slideIndex, focus }) => {
|
|
79
|
+
const resolvedImagePath = await resolveImagePath(rootDir, imagePath)
|
|
80
|
+
const fileInfo = await stat(resolvedImagePath).catch(() => null)
|
|
81
|
+
if (!fileInfo?.isFile())
|
|
82
|
+
throw new Error(`Screenshot not found: ${imagePath}`)
|
|
83
|
+
|
|
84
|
+
const imageBuffer = await readFile(resolvedImagePath)
|
|
85
|
+
if (path.extname(resolvedImagePath).toLowerCase() !== ".png") {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`slidev_review_screenshot only accepts PNG files. Got: ${path.extname(resolvedImagePath) || "unknown"}`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!isPngBuffer(imageBuffer))
|
|
92
|
+
throw new Error(`slidev_review_screenshot expected a real PNG file at: ${imagePath}`)
|
|
93
|
+
|
|
94
|
+
const imageData = imageBuffer.toString("base64")
|
|
95
|
+
const reviewModel = await initChatModel(model, {
|
|
96
|
+
temperature: 0,
|
|
97
|
+
})
|
|
98
|
+
const structuredReviewer = reviewModel.withStructuredOutput(slidevReviewScreenshotResultSchema)
|
|
99
|
+
|
|
100
|
+
const review = await structuredReviewer.invoke([
|
|
101
|
+
{
|
|
102
|
+
role: "user",
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: [
|
|
107
|
+
`Review this rendered Slidev slide screenshot${slideIndex ? ` for slide ${slideIndex}` : ""}.`,
|
|
108
|
+
"Judge only what is visible in the image. Do not guess about hidden or source markdown content.",
|
|
109
|
+
"Mark the slide as failing if you notice clipped or off-screen content, overflow, cramped layout, collisions, unreadable or tiny text, broken wrapping, poor contrast, or obvious alignment problems.",
|
|
110
|
+
"Only pass the slide if it looks presentation-ready.",
|
|
111
|
+
focus ? `Pay extra attention to: ${focus}` : "",
|
|
112
|
+
"Return concise structured output.",
|
|
113
|
+
].filter(Boolean).join("\n"),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: "image",
|
|
117
|
+
source_type: "base64",
|
|
118
|
+
data: imageData,
|
|
119
|
+
mimeType: "image/png",
|
|
120
|
+
mime_type: "image/png",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
...review,
|
|
128
|
+
imagePath: path.relative(rootDir, resolvedImagePath) || path.basename(resolvedImagePath),
|
|
129
|
+
slideIndex: slideIndex || null,
|
|
130
|
+
}
|
|
131
|
+
}, {
|
|
132
|
+
name: "slidev_review_screenshot",
|
|
133
|
+
description: "Review an exported PNG slide screenshot with a multimodal model and report visual issues such as clipping, overflow, collisions, poor contrast, and unreadable text.",
|
|
134
|
+
schema: slidevReviewScreenshotSchema,
|
|
135
|
+
})
|
|
136
|
+
}
|
package/app/index.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { readFile, stat } from "node:fs/promises"
|
|
3
|
+
|
|
4
|
+
import { Hono } from "hono"
|
|
5
|
+
import { env } from "../lib/env.js"
|
|
6
|
+
|
|
7
|
+
function resolveRootDir() {
|
|
8
|
+
return path.resolve(env(process.env, "SLIDEV_AGENT_ROOT_DIR") || process.cwd())
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeBasePath(basePath: string) {
|
|
12
|
+
if (!basePath.startsWith("/"))
|
|
13
|
+
return `/${basePath}`
|
|
14
|
+
|
|
15
|
+
return basePath.replace(/\/+$/, "")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getMimeType(filePath: string) {
|
|
19
|
+
const extension = path.extname(filePath).toLowerCase()
|
|
20
|
+
switch (extension) {
|
|
21
|
+
case ".html":
|
|
22
|
+
return "text/html; charset=utf-8"
|
|
23
|
+
case ".js":
|
|
24
|
+
return "application/javascript; charset=utf-8"
|
|
25
|
+
case ".mjs":
|
|
26
|
+
return "application/javascript; charset=utf-8"
|
|
27
|
+
case ".css":
|
|
28
|
+
return "text/css; charset=utf-8"
|
|
29
|
+
case ".json":
|
|
30
|
+
return "application/json; charset=utf-8"
|
|
31
|
+
case ".svg":
|
|
32
|
+
return "image/svg+xml"
|
|
33
|
+
case ".png":
|
|
34
|
+
return "image/png"
|
|
35
|
+
case ".jpg":
|
|
36
|
+
case ".jpeg":
|
|
37
|
+
return "image/jpeg"
|
|
38
|
+
case ".gif":
|
|
39
|
+
return "image/gif"
|
|
40
|
+
case ".webp":
|
|
41
|
+
return "image/webp"
|
|
42
|
+
case ".woff":
|
|
43
|
+
return "font/woff"
|
|
44
|
+
case ".woff2":
|
|
45
|
+
return "font/woff2"
|
|
46
|
+
default:
|
|
47
|
+
return "application/octet-stream"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function exists(filePath: string) {
|
|
52
|
+
try {
|
|
53
|
+
await stat(filePath)
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function proxyToDevServer(requestUrl: URL, assetPath: string) {
|
|
62
|
+
const devServerUrl = new URL(env(process.env, "SLIDEV_AGENT_DEV_URL", "http://localhost:3030"))
|
|
63
|
+
const targetUrl = new URL(assetPath || "/", devServerUrl)
|
|
64
|
+
targetUrl.search = requestUrl.search
|
|
65
|
+
|
|
66
|
+
const response = await fetch(targetUrl)
|
|
67
|
+
const body = await response.arrayBuffer()
|
|
68
|
+
|
|
69
|
+
return new Response(body, {
|
|
70
|
+
status: response.status,
|
|
71
|
+
headers: response.headers,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const app = new Hono()
|
|
76
|
+
|
|
77
|
+
const basePath = normalizeBasePath(env(process.env, "SLIDEV_AGENT_APP_BASE_PATH", "/slidev-agent"))
|
|
78
|
+
|
|
79
|
+
app.get("/", c => c.redirect(`${basePath}/`, 302))
|
|
80
|
+
app.get(basePath, c => c.redirect(`${basePath}/`, 302))
|
|
81
|
+
|
|
82
|
+
app.get(`${basePath}/*`, async (c) => {
|
|
83
|
+
const requestUrl = new URL(c.req.url)
|
|
84
|
+
const assetPath = requestUrl.pathname.slice(basePath.length) || "/"
|
|
85
|
+
const distRoot = path.join(resolveRootDir(), "dist")
|
|
86
|
+
|
|
87
|
+
if (await exists(path.join(distRoot, "index.html"))) {
|
|
88
|
+
const candidatePath = path.normalize(path.join(distRoot, assetPath))
|
|
89
|
+
const safeRoot = `${distRoot}${path.sep}`
|
|
90
|
+
const safeCandidate = candidatePath === distRoot || candidatePath.startsWith(safeRoot)
|
|
91
|
+
|
|
92
|
+
if (!safeCandidate)
|
|
93
|
+
return c.text("Invalid asset path", 400)
|
|
94
|
+
|
|
95
|
+
const isSpaRoute = !path.extname(assetPath)
|
|
96
|
+
const filePath = await exists(candidatePath)
|
|
97
|
+
? candidatePath
|
|
98
|
+
: isSpaRoute
|
|
99
|
+
? path.join(distRoot, "index.html")
|
|
100
|
+
: ""
|
|
101
|
+
|
|
102
|
+
if (!filePath)
|
|
103
|
+
return c.text("Asset not found", 404)
|
|
104
|
+
|
|
105
|
+
const content = await readFile(filePath)
|
|
106
|
+
return new Response(content, {
|
|
107
|
+
headers: {
|
|
108
|
+
"content-type": getMimeType(filePath),
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
return await proxyToDevServer(requestUrl, assetPath)
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return c.text(
|
|
118
|
+
`Slidev app is not available yet. Start \`slidev-agent dev\` or build the deck so LangGraph can serve it from ${basePath}/.`,
|
|
119
|
+
503,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
export { app }
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SidebarMessage } from "../lib/sidebar"
|
|
3
|
+
import { renderChatMarkdown } from "../lib/render-chat-markdown"
|
|
4
|
+
import SubagentCard from "./SubagentCard.vue"
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
message: SidebarMessage
|
|
8
|
+
}>()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div
|
|
13
|
+
class="slidev-agent-message"
|
|
14
|
+
:class="`slidev-agent-message--${message.role}`"
|
|
15
|
+
>
|
|
16
|
+
<template v-if="message.kind === 'tool'">
|
|
17
|
+
<div class="slidev-agent-tool">
|
|
18
|
+
<span class="slidev-agent-tool__dot" />
|
|
19
|
+
<span class="slidev-agent-tool__headline">{{ message.toolHeadline }}</span>
|
|
20
|
+
<span v-if="message.argsSummary" class="slidev-agent-tool__args">
|
|
21
|
+
{{ message.argsSummary }}
|
|
22
|
+
</span>
|
|
23
|
+
<span class="slidev-agent-tool__summary">
|
|
24
|
+
{{ message.resultSummary }}
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<template v-else>
|
|
30
|
+
<div class="slidev-agent-message__label">
|
|
31
|
+
{{ message.label }}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
v-if="message.content"
|
|
36
|
+
class="slidev-agent-message__body"
|
|
37
|
+
v-html="renderChatMarkdown(message.content)"
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<div
|
|
41
|
+
v-if="message.subagents.length > 0"
|
|
42
|
+
class="slidev-agent-message__subagents"
|
|
43
|
+
>
|
|
44
|
+
<SubagentCard
|
|
45
|
+
v-for="subagent in message.subagents"
|
|
46
|
+
:key="subagent.id"
|
|
47
|
+
:subagent="subagent"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<style scoped>
|
|
55
|
+
.slidev-agent-message {
|
|
56
|
+
padding: 0.75rem;
|
|
57
|
+
border-radius: 0.9rem;
|
|
58
|
+
background: rgba(30, 41, 59, 0.85);
|
|
59
|
+
min-width: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.slidev-agent-message--human {
|
|
63
|
+
background: rgba(37, 99, 235, 0.2);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.slidev-agent-message--tool {
|
|
67
|
+
padding: 0;
|
|
68
|
+
border-radius: 0;
|
|
69
|
+
background: transparent;
|
|
70
|
+
border: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.slidev-agent-message--subagent {
|
|
74
|
+
padding: 0.55rem 0.6rem;
|
|
75
|
+
background: rgba(15, 23, 42, 0.72);
|
|
76
|
+
border: 1px solid rgba(148, 163, 184, 0.16);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.slidev-agent-message__label {
|
|
80
|
+
margin-bottom: 0.35rem;
|
|
81
|
+
font-size: 0.72rem;
|
|
82
|
+
text-transform: uppercase;
|
|
83
|
+
letter-spacing: 0.06em;
|
|
84
|
+
color: #93c5fd;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.slidev-agent-message__body {
|
|
88
|
+
line-height: 1.45;
|
|
89
|
+
font-size: 0.92rem;
|
|
90
|
+
word-break: break-word;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.slidev-agent-message__body :deep(p) {
|
|
94
|
+
margin: 0 0 0.5em;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.slidev-agent-message__body :deep(p:last-child) {
|
|
98
|
+
margin-bottom: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.slidev-agent-message__body :deep(ul),
|
|
102
|
+
.slidev-agent-message__body :deep(ol) {
|
|
103
|
+
margin: 0.35em 0 0.5em;
|
|
104
|
+
padding-left: 1.25rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.slidev-agent-message__body :deep(li + li) {
|
|
108
|
+
margin-top: 0.25em;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.slidev-agent-message__body :deep(a) {
|
|
112
|
+
color: #93c5fd;
|
|
113
|
+
text-decoration: underline;
|
|
114
|
+
text-underline-offset: 0.12em;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.slidev-agent-message__body :deep(code) {
|
|
118
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
119
|
+
font-size: 0.88em;
|
|
120
|
+
padding: 0.12em 0.35em;
|
|
121
|
+
border-radius: 0.35rem;
|
|
122
|
+
background: rgba(15, 23, 42, 0.85);
|
|
123
|
+
color: #e2e8f0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.slidev-agent-message__body :deep(pre) {
|
|
127
|
+
margin: 0.5em 0;
|
|
128
|
+
padding: 0.55rem 0.65rem;
|
|
129
|
+
border-radius: 0.5rem;
|
|
130
|
+
background: rgba(15, 23, 42, 0.92);
|
|
131
|
+
overflow-x: auto;
|
|
132
|
+
font-size: 0.82rem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.slidev-agent-message__body :deep(pre code) {
|
|
136
|
+
padding: 0;
|
|
137
|
+
background: transparent;
|
|
138
|
+
font-size: inherit;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.slidev-agent-message__body :deep(blockquote) {
|
|
142
|
+
margin: 0.4em 0;
|
|
143
|
+
padding-left: 0.75rem;
|
|
144
|
+
border-left: 3px solid rgba(148, 163, 184, 0.45);
|
|
145
|
+
color: #cbd5e1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.slidev-agent-message__body :deep(h1),
|
|
149
|
+
.slidev-agent-message__body :deep(h2),
|
|
150
|
+
.slidev-agent-message__body :deep(h3) {
|
|
151
|
+
margin: 0.5em 0 0.35em;
|
|
152
|
+
font-weight: 600;
|
|
153
|
+
line-height: 1.25;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.slidev-agent-message__body :deep(h1) {
|
|
157
|
+
font-size: 1.05em;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.slidev-agent-message__body :deep(h2) {
|
|
161
|
+
font-size: 1em;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.slidev-agent-message__body :deep(h3) {
|
|
165
|
+
font-size: 0.95em;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.slidev-agent-message__body :deep(table) {
|
|
169
|
+
width: 100%;
|
|
170
|
+
border-collapse: collapse;
|
|
171
|
+
margin: 0.45em 0;
|
|
172
|
+
font-size: 0.88em;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.slidev-agent-message__body :deep(th),
|
|
176
|
+
.slidev-agent-message__body :deep(td) {
|
|
177
|
+
border: 1px solid rgba(148, 163, 184, 0.25);
|
|
178
|
+
padding: 0.3rem 0.45rem;
|
|
179
|
+
text-align: left;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.slidev-agent-message__body :deep(hr) {
|
|
183
|
+
margin: 0.6em 0;
|
|
184
|
+
border: 0;
|
|
185
|
+
border-top: 1px solid rgba(148, 163, 184, 0.25);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.slidev-agent-message__subagents {
|
|
189
|
+
margin-top: 0.5rem;
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-direction: column;
|
|
192
|
+
gap: 0.35rem;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.slidev-agent-tool {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
flex-wrap: wrap;
|
|
199
|
+
gap: 0.35rem;
|
|
200
|
+
padding: 0 0.1rem;
|
|
201
|
+
font-size: 0.74rem;
|
|
202
|
+
line-height: 1.4;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.slidev-agent-tool__dot {
|
|
206
|
+
width: 0.38rem;
|
|
207
|
+
height: 0.38rem;
|
|
208
|
+
border-radius: 999px;
|
|
209
|
+
background: rgba(96, 165, 250, 0.85);
|
|
210
|
+
flex: 0 0 auto;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.slidev-agent-tool__headline {
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
color: #dbeafe;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.slidev-agent-tool__args {
|
|
219
|
+
color: #cbd5e1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.slidev-agent-tool__summary {
|
|
223
|
+
color: #93c5fd;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.slidev-agent-tool__summary::before {
|
|
227
|
+
content: "•";
|
|
228
|
+
margin-right: 0.3rem;
|
|
229
|
+
color: rgba(148, 163, 184, 0.6);
|
|
230
|
+
}
|
|
231
|
+
</style>
|