DuckVizBeta
Guides

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:

RoutePurpose
POST /api/generate-deck-slidesParallel slide generation (SSE)
POST /api/modify-deck-slidesAI rewrite of a single slide
POST /api/query-from-promptNL → 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 toggle
  • 1.2.2 — clickable TOC bookmarks in PDF + DOCX
  • 1.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 since 1.1.0

See also: Embedding Dashboards, Next.js Integration.