Deck Integration
Mount <DeckBuilder /> in your own app, wire SSE, and export PPTX.
This is the host-side companion to Deck Mode. Embed <DeckBuilder /> from @duckviz/report, point it at your dashboard config, and get the same slide-generation + presenter + PPTX export as the hosted product.
Prerequisite: you have @duckviz/dashboard already working. Deck reuses the same dashboard config shape.
Minimal example
import { DuckvizDBProvider } from "@duckviz/db";
import { DeckBuilder } from "@duckviz/report";
import type { BuilderDashboardConfig } from "@duckviz/report";
const config: BuilderDashboardConfig = {
name: "Q4 Review",
widgets: [
{
id: "revenue",
type: "bar",
title: "Revenue by Region",
description: "Top 10 regions",
dataKey: "SELECT region AS category, SUM(amount) AS value FROM t_sales GROUP BY region ORDER BY value DESC LIMIT 10",
},
{
id: "trend",
type: "line",
title: "Monthly Trend",
description: "Revenue over time",
dataKey: "SELECT month AS category, total AS value FROM t_monthly",
},
],
};
export function ReviewDeck({ onBack, salesRows }: Props) {
const [fullscreen, setFullscreen] = useState(false);
return (
<DuckvizDBProvider persistence>
<DeckBuilder
config={config}
datasets={[{ name: "Sales", data: salesRows, tableName: "t_sales" }]}
onBack={onBack}
onPresenting={(p) => setFullscreen(p)}
/>
</DuckvizDBProvider>
);
}DeckBuilder handles SSE wiring, slide state, editor UI, presenter, and PPTX export internally. You own the shell (routing, chrome, fullscreen toggle).
Hosting the API routes
Deck slide generation calls three server endpoints:
| Route | Purpose |
|---|---|
POST /api/generate-deck-slides | Parallel slide generation (SSE) |
POST /api/modify-deck-slides | AI rewrite of a single slide |
POST /api/query-from-prompt | NL → SQL for "add a slide about X" |
Use @duckviz/sdk to proxy these from your Next.js app. The SDK's Next adapter wires all three — see Server Proxy Setup.
Custom path rewrite
If you serve the routes from a non-default prefix (e.g. /api/duckviz/…), pass a customFetch:
const customFetch: FetchFn = (input, init) => {
if (typeof input === "string" && input.startsWith("/api/")) {
input = input.replace(/^\/api\//, "/api/duckviz/");
}
return fetch(input, init);
};
<DeckBuilder config={config} customFetch={customFetch} /* ... */ />Template variables
Chart slides can declare placeholders like {{growth_pct | percent}} that the LLM fills from each widget's SQL result. The server-side processTemplate (from @duckviz/report/server) resolves them before the slide ships to the client.
You don't normally call processTemplate yourself — the slide endpoint does. But if you're building a custom server handler, the pipeline is:
import {
parse,
evaluate,
processTemplate,
formatCurrency,
formatPercent,
DECK_SYSTEM_PROMPT,
buildChartSlidePrompt,
} from "@duckviz/report/server";
// 1. Run the widget's SQL locally (your DuckDB)
const rows = await runSql(widget.duckdbQuery);
// 2. Let the LLM emit a slide with placeholders
const prompt = buildChartSlidePrompt({ widget, rows, systemPromptHint });
const slide = await llm.complete({ system: DECK_SYSTEM_PROMPT, prompt });
// 3. Resolve placeholders server-side
const filled = processTemplate(slide.text, slide.variables, { rows });
return filled;Full spec in TEMPLATE_SYNTAX_REFERENCE — paste it into the LLM's system prompt to get syntactically valid output every time.
Power-user composition
If the default builder chrome doesn't fit (e.g. you want your own slide rail, custom toolbars, or a different modal flow), drop to the lower-level pieces:
import {
ReportProvider,
createReportCallbacks,
DeckView,
DeckRightPanel,
DeckPresenter,
useDeckGeneration,
exportToPptx,
} from "@duckviz/report";createReportCallbacks({ customFetch, runQuery }) returns the SSE callback bundle ReportProvider expects. The useDeckGeneration() hook gives you { phase, slides, start, modify, reset } — you drive the state machine, the package drives the stream.
PPTX export in a custom host
exportToPptx(deck, options) is a pure function — call it from any button:
import { exportToPptx } from "@duckviz/report";
async function handleExport() {
await exportToPptx(structuredDeck, {
fileName: "q4-review.pptx",
branding: { companyName: "Acme", logoDataUrl },
});
}Chart SVGs rasterize at export time via captureSvgElement — text stays native PowerPoint (selectable/editable).
Presenter embed
<DeckPresenter deck={...} /> is the fullscreen presenter as a composable component. Common iframe pattern for a sandboxed viewer (e.g. a reader-only share page):
<iframe
src="/deck/abc123/present"
sandbox="allow-same-origin allow-scripts"
allowFullScreen
/>Inside the iframe page, render the presenter without chrome:
export default function Page({ params }: { params: { id: string } }) {
const deck = useLoadDeck(params.id);
if (!deck) return <Loader />;
return <DeckPresenter deck={deck} onExit={() => window.close()} />;
}Version notes
@duckviz/report@1.2.0— pre-export dialog with A4 preview, cover page, TOC toggle1.2.2— clickable TOC bookmarks in PDF + DOCX1.2.3— data-table widgets export as real tables (not images) in PDF/DOCX- Deck generation has been in the package since
1.0.0; server-side template engine since1.1.0
See also: Embedding Dashboards, Next.js Integration.