+ )
+}
diff --git a/lib/render/components/shared.ts b/lib/render/components/shared.ts
new file mode 100644
index 000000000..562699cef
--- /dev/null
+++ b/lib/render/components/shared.ts
@@ -0,0 +1,10 @@
+import { catalog } from '../catalog'
+
+export type CatalogType = typeof catalog
+
+export const stackGap = {
+ xs: 'gap-1',
+ sm: 'gap-2',
+ md: 'gap-3',
+ lg: 'gap-4'
+} as const
diff --git a/lib/render/components/stack.tsx b/lib/render/components/stack.tsx
new file mode 100644
index 000000000..ce4023e58
--- /dev/null
+++ b/lib/render/components/stack.tsx
@@ -0,0 +1,25 @@
+'use client'
+
+import type { ComponentFn } from '@json-render/react'
+
+import { cn } from '@/lib/utils'
+
+import { type CatalogType, stackGap } from './shared'
+
+export const Stack: ComponentFn = ({
+ props,
+ children
+}) => {
+ const { direction = 'vertical', gap = 'md' } = props
+ return (
+
+ {children}
+
+ )
+}
diff --git a/lib/render/prompt.ts b/lib/render/prompt.ts
index 4b37675de..4dcdd57b7 100644
--- a/lib/render/prompt.ts
+++ b/lib/render/prompt.ts
@@ -33,11 +33,64 @@ AVAILABLE ACTIONS:
- submitQuery: { query: string } - Submit a follow-up query
SPEC RULES:
-1. Always wrap JSONL patches in a single \`\`\`spec fence at the END of your response.
-2. The \`\`\`spec block must contain ONLY JSONL patches — no commentary inside.
+1. The related questions \`\`\`spec fence must appear at the END of your response.
+2. Every \`\`\`spec block must contain ONLY JSONL patches — no commentary inside.
3. Keep each JSON object on a single line.
4. The "text" prop and "query" param must be identical for each question.
-5. Do NOT open more than one \`\`\`spec block in a single answer.
+5. Emit exactly ONE related questions spec block per answer (image spec blocks are separate and may appear inline).
6. Do NOT include follow-up suggestions or questions in your markdown text. Only use the spec block for them.
`
}
+
+/**
+ * Returns the prompt section that instructs the LLM to optionally embed
+ * inline image groups as ```spec fenced blocks within the response body.
+ */
+export function getImageSpecPrompt(): string {
+ return `
+INLINE IMAGE EMBEDDING:
+When images would meaningfully enhance the answer (e.g. visual subjects, people, places, products, events),
+embed one or more inline image groups anywhere in the markdown body using \`\`\`spec fenced code blocks.
+Actively include images whenever they help the reader's understanding of the answer — visual context often
+communicates faster and more clearly than prose.
+
+Use this when:
+- The search tool results contain relevant images (check the "images" array in tool output).
+- A picture helps the reader understand the topic (subjects, places, products, events, diagrams, comparisons).
+
+Skip images only for purely abstract or text-only topics where no image would add value.
+
+AVAILABLE COMPONENTS FOR IMAGES:
+- Grid: { columns: 1 | 2 | 3 | 4, gap?: "xs" | "sm" | "md" | "lg" } - A fixed-column container that reserves cell widths upfront so streaming images don't reflow.
+- Image: { src: string, sourceUrl?: string, title?: string, description?: string, aspectRatio?: "1:1" | "16:9" | "4:3" }
+
+IMAGE SPEC RULES:
+1. Only use image URLs taken verbatim from the search tool's "images" array — NEVER fabricate or guess URLs.
+2. Map tool output fields to Image props as follows, copying values EXACTLY without rewording:
+ - image.url → "src"
+ - image.sourceUrl → "sourceUrl" (omit if not present in the tool output — do NOT invent)
+ - image.title → "title" (omit if not present)
+ - image.description → "description" (omit if not present)
+3. The "aspectRatio" field SHOULD reflect the natural orientation of the subject: "1:1" for square (logos, portraits), "16:9" for wide (landscapes, scenes), "4:3" for standard photos. Images within the same Grid should generally use the SAME aspectRatio so they render at identical heights.
+4. Always wrap image groups in a Grid. Set "columns" to the exact number of Image children (1–4). For 1 image use columns=1, for 2 use columns=2, etc. Choose the number of images based on the situation — the variety and relevance of available images and how much visual context genuinely helps the answer.
+5. You MAY emit multiple \`\`\`spec image blocks, each placed at the position in the markdown where they are contextually relevant (e.g. right after the heading or paragraph they illustrate).
+6. Image spec blocks are separate from the mandatory related-questions spec block at the end.
+7. Each image spec block must contain ONLY JSONL patches — no commentary inside.
+
+Example (inline image group embedded in markdown body):
+
+## Mount Fuji
+
+Mount Fuji is Japan's tallest peak.
+
+\`\`\`spec
+{"op":"add","path":"/root","value":"grid"}
+{"op":"add","path":"/elements/grid","value":{"type":"Grid","props":{"columns":3,"gap":"sm"},"children":["img1","img2","img3"]}}
+{"op":"add","path":"/elements/img1","value":{"type":"Image","props":{"src":"https://cdn.example.com/fuji-1.jpg","sourceUrl":"https://en.wikipedia.org/wiki/Mount_Fuji","title":"Mount Fuji - Wikipedia","description":"Snow-capped peak at sunrise","aspectRatio":"4:3"},"children":[]}}
+{"op":"add","path":"/elements/img2","value":{"type":"Image","props":{"src":"https://cdn.example.com/fuji-2.jpg","sourceUrl":"https://travel.example.com/mount-fuji","title":"Mount Fuji Travel Guide","aspectRatio":"4:3"},"children":[]}}
+{"op":"add","path":"/elements/img3","value":{"type":"Image","props":{"src":"https://cdn.example.com/fuji-3.jpg","title":"Cherry blossoms in spring","aspectRatio":"4:3"},"children":[]}}
+\`\`\`
+
+It rises 3,776 meters above sea level...
+`
+}
diff --git a/lib/render/registry.tsx b/lib/render/registry.tsx
index bf6b6d605..c8233528f 100644
--- a/lib/render/registry.tsx
+++ b/lib/render/registry.tsx
@@ -1,77 +1,21 @@
'use client'
-import type { ComponentFn } from '@json-render/react'
import { defineRegistry } from '@json-render/react'
-import { ArrowRight, Repeat2 } from 'lucide-react'
-
-import { cn } from '@/lib/utils'
+import { Grid } from './components/grid'
+import { Image } from './components/image'
+import { QuestionButton } from './components/question-button'
+import { SectionHeader } from './components/section-header'
+import { Stack } from './components/stack'
import { catalog } from './catalog'
-type CatalogType = typeof catalog
-
-const iconMap = {
- related: Repeat2
-} as const
-
-const SectionHeader: ComponentFn = ({
- props
-}) => {
- const Icon = props.icon ? iconMap[props.icon] : null
- return (
-