From 1ca683154f1e5ba4584e15b1c573d508a402e38c Mon Sep 17 00:00:00 2001 From: karthikmudunuri <102793643+karthikmudunuri@users.noreply.github.com> Date: Mon, 18 May 2026 17:48:53 +0530 Subject: [PATCH] feat(slidewise): add jsonDeck prop for AI-generated decks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a top-level `jsonDeck?: Deck | string` prop on `SlidewiseEditor` and `Slidewise.Root` so hosts can feed model-generated decks directly into the editor without manual `JSON.parse` / `migrate()` glue. Takes precedence over `deck` when both are passed; `deck` is now optional and one of the two must be supplied. Also exports `resolveJsonDeck(input)` — the same parse + migrate helper the editor uses internally — for hosts that want to validate AI output before mounting, or for tools that emit `Deck` JSON outside of React. The schema LLMs target is the existing public `Deck` type. --- .changeset/json-deck-prop.md | 29 +++++++++++++ packages/slidewise/src/SlidewiseEditor.tsx | 21 ++++++++- .../slidewise/src/compound/SlidewiseRoot.tsx | 43 ++++++++++++++++--- packages/slidewise/src/index.ts | 1 + .../src/lib/schema/__tests__/json.test.ts | 39 +++++++++++++++++ packages/slidewise/src/lib/schema/json.ts | 18 ++++++++ 6 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 .changeset/json-deck-prop.md create mode 100644 packages/slidewise/src/lib/schema/__tests__/json.test.ts create mode 100644 packages/slidewise/src/lib/schema/json.ts diff --git a/.changeset/json-deck-prop.md b/.changeset/json-deck-prop.md new file mode 100644 index 0000000..85f488d --- /dev/null +++ b/.changeset/json-deck-prop.md @@ -0,0 +1,29 @@ +--- +"@textcortex/slidewise": minor +--- + +Add `jsonDeck` prop and expose `resolveJsonDeck` — the AI-facing entry point for feeding model-generated decks into the editor. + +**`SlidewiseEditor` / `Slidewise.Root`** + +- New top-level `jsonDeck?: Deck | string` prop. Pass either a parsed `Deck` object or a JSON string and Slidewise will `JSON.parse` (when needed) and run the value through `migrate()` before mounting — no manual normalisation required. Takes precedence over `deck` when both are provided. +- `deck` is now optional; one of `deck` or `jsonDeck` must be supplied. Existing callers passing only `deck` are unaffected. + +**Why** + +This is the contract LLMs target when generating slides. The exported `Deck` TypeScript type is the JSON schema: hosts can prompt their model to emit a `Deck`-shaped object (or stringified JSON) and pipe it straight into `` without writing glue. + +**New export: `resolveJsonDeck(input: Deck | string): Deck`** + +Same parse + migrate helper Slidewise uses internally. Use it to validate AI output before passing it to the editor, or when building tools that emit `Deck` JSON outside of React. + +```tsx +import { SlidewiseEditor, resolveJsonDeck } from "@textcortex/slidewise"; + +// Pass JSON directly: + + +// Or validate first: +const deck = resolveJsonDeck(aiGeneratedJsonString); + +``` diff --git a/packages/slidewise/src/SlidewiseEditor.tsx b/packages/slidewise/src/SlidewiseEditor.tsx index 199baf6..92b7f7d 100644 --- a/packages/slidewise/src/SlidewiseEditor.tsx +++ b/packages/slidewise/src/SlidewiseEditor.tsx @@ -30,8 +30,25 @@ export interface SlidewiseEditorProps { * `onChange` — that would loop. Hold the deck in a stable ref, and * only pass a new one when you intentionally want to reset the editor * (e.g. discard changes, load a different file). + * + * One of `deck` or `jsonDeck` is required. + */ + deck?: Deck; + /** + * Deck supplied as JSON — either a `Deck` object or a JSON string. This + * is the AI-facing entry point: feed model output directly without + * manually calling `JSON.parse` or `migrate()`. The value is parsed (if + * a string) and run through `migrate()` so older schema versions are + * upgraded transparently. + * + * Takes precedence over `deck` when both are provided. Pass a stable + * reference (or stable string) on subsequent renders — changing it + * resets the editor's internal state, same as swapping `deck`. + * + * The JSON shape is the public `Deck` type exported from this package. + * Use it as the schema your LLM targets when generating new decks. */ - deck: Deck; + jsonDeck?: Deck | string; /** Fires after every committed mutation; receives the updated deck. */ onChange?: (deck: Deck) => void; /** Fires when the user clicks "Save" in the top bar. */ @@ -159,6 +176,7 @@ export const SlidewiseEditor = forwardRef< >(function SlidewiseEditor( { deck, + jsonDeck, onChange, onSave, onExport, @@ -199,6 +217,7 @@ export const SlidewiseEditor = forwardRef< const rootProps: SlidewiseRootProps = { deck, + jsonDeck, onChange, onSave, onExport, diff --git a/packages/slidewise/src/compound/SlidewiseRoot.tsx b/packages/slidewise/src/compound/SlidewiseRoot.tsx index 795627d..061b484 100644 --- a/packages/slidewise/src/compound/SlidewiseRoot.tsx +++ b/packages/slidewise/src/compound/SlidewiseRoot.tsx @@ -3,6 +3,7 @@ import { useEffect, useId, useImperativeHandle, + useMemo, useRef, useState, type CSSProperties, @@ -15,6 +16,7 @@ import { useEditorStore, } from "@/lib/StoreProvider"; import { collectFontFamilies, ensureGoogleFontsLoaded } from "@/lib/fonts"; +import { resolveJsonDeck } from "@/lib/schema/json"; import type { Deck } from "@/lib/types"; import { GridView } from "@/components/editor/GridView"; import { PlayMode } from "@/components/editor/PlayMode"; @@ -39,8 +41,22 @@ export interface SlidewiseRootProps { * Deck to load on mount. Pass a new reference only when you intend to * reset the editor's state (e.g. discard changes, load a different file) * — passing a new reference on every `onChange` would loop. + * + * One of `deck` or `jsonDeck` is required. */ - deck: Deck; + deck?: Deck; + /** + * Deck supplied as JSON — either a `Deck` object or a JSON string. This is + * the AI-facing entry point: feed model output directly without manually + * calling `JSON.parse` or `migrate()`. The input is parsed (if a string) + * and run through `migrate()` to upgrade older schema versions. + * + * Takes precedence over `deck` when both are provided. Like `deck`, the + * value should only change when you intentionally want to reset the + * editor's state — pass a stable reference (or stable string) on subsequent + * renders to avoid re-loading on every commit. + */ + jsonDeck?: Deck | string; /** Fires after every committed mutation. */ onChange?: (deck: Deck) => void; /** Fires when the user invokes save (top bar button or imperative API). */ @@ -222,15 +238,30 @@ export interface SlidewiseRootHandle { * which is just `` rendering the standard layout. */ export const Root = forwardRef>( - function SlidewiseRoot(props, ref) { + function SlidewiseRoot({ deck, jsonDeck, ...rest }, ref) { + const resolvedDeck = useMemo( + () => resolveInputDeck(deck, jsonDeck), + [deck, jsonDeck] + ); return ( - - + + ); } ); +function resolveInputDeck( + deck: Deck | undefined, + jsonDeck: Deck | string | undefined +): Deck { + if (jsonDeck !== undefined) return resolveJsonDeck(jsonDeck); + if (deck !== undefined) return deck; + throw new Error( + "Slidewise: requires either a `deck` or `jsonDeck` prop." + ); +} + function RootInner({ deck, onChange, @@ -258,7 +289,9 @@ function RootInner({ style, children, forwardedRef, -}: PropsWithChildren & { +}: PropsWithChildren> & { + /** Resolved deck — always provided by the outer `Root`. */ + deck: Deck; forwardedRef: Ref; }) { const store = useEditorStore(); diff --git a/packages/slidewise/src/index.ts b/packages/slidewise/src/index.ts index 0a20f51..3ea03a4 100644 --- a/packages/slidewise/src/index.ts +++ b/packages/slidewise/src/index.ts @@ -92,6 +92,7 @@ export { parsePptx, serializeDeck } from "./lib/pptx"; export type { ParseDiagnostics, ParseResult } from "./lib/pptx/types"; export { migrate, CURRENT_DECK_VERSION } from "./lib/schema/migrate"; +export { resolveJsonDeck } from "./lib/schema/json"; export type { Deck, diff --git a/packages/slidewise/src/lib/schema/__tests__/json.test.ts b/packages/slidewise/src/lib/schema/__tests__/json.test.ts new file mode 100644 index 0000000..fe416ef --- /dev/null +++ b/packages/slidewise/src/lib/schema/__tests__/json.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from "vitest"; +import { resolveJsonDeck } from "../json"; +import { CURRENT_DECK_VERSION } from "../migrate"; + +describe("schema/json.resolveJsonDeck", () => { + it("accepts a parsed Deck object and stamps the current version", () => { + const out = resolveJsonDeck({ + title: "From AI", + slides: [{ id: "s1", background: "#FFFFFF", elements: [] }], + } as never); + expect(out.version).toBe(CURRENT_DECK_VERSION); + expect(out.title).toBe("From AI"); + expect(out.slides).toHaveLength(1); + }); + + it("accepts a JSON string", () => { + const json = JSON.stringify({ + version: CURRENT_DECK_VERSION, + title: "Stringified", + slides: [{ id: "s1", background: "#000", elements: [] }], + }); + const out = resolveJsonDeck(json); + expect(out.title).toBe("Stringified"); + expect(out.slides[0].id).toBe("s1"); + }); + + it("throws on malformed JSON strings", () => { + expect(() => resolveJsonDeck("{ not valid")).toThrow(); + }); + + it("rejects decks from a newer schema", () => { + const json = JSON.stringify({ + version: CURRENT_DECK_VERSION + 1, + title: "Future", + slides: [], + }); + expect(() => resolveJsonDeck(json)).toThrow(/newer than this build/); + }); +}); diff --git a/packages/slidewise/src/lib/schema/json.ts b/packages/slidewise/src/lib/schema/json.ts new file mode 100644 index 0000000..54375ac --- /dev/null +++ b/packages/slidewise/src/lib/schema/json.ts @@ -0,0 +1,18 @@ +import type { Deck } from "@/lib/types"; +import { migrate } from "./migrate"; + +/** + * Normalise a host-supplied deck JSON — either a parsed `Deck` object or a + * JSON string — into a current-schema `Deck`. Runs the input through + * `migrate()` so older `version` stamps are upgraded and the basic shape is + * validated. This is the entry point hosts use when feeding AI-generated + * decks to ``. + * + * Throws if the string isn't valid JSON, or if the resulting object is + * missing the basic `Deck` shape / its `version` is newer than this build. + */ +export function resolveJsonDeck(input: Deck | string): Deck { + const parsed: unknown = + typeof input === "string" ? (JSON.parse(input) as unknown) : input; + return migrate(parsed); +}