Skip to content

feat: UI template system for saving and reusing generated widgets#20

Open
breadoncee wants to merge 11 commits intoCopilotKit:mainfrom
breadoncee:feat/ui-template-system
Open

feat: UI template system for saving and reusing generated widgets#20
breadoncee wants to merge 11 commits intoCopilotKit:mainfrom
breadoncee:feat/ui-template-system

Conversation

@breadoncee
Copy link

@breadoncee breadoncee commented Mar 23, 2026

opengenui_templates.mp4

Summary

  • Save any generated UI as a template — bar charts, pie charts, and widget renderer outputs all get a "Save as Template" button via a shared overlay component
  • Template library drawer with mini chart previews (inline SVG for bar/pie charts, scaled iframe for HTML widgets)
  • Apply templates with new data — submits through CopilotChat for proper component rendering, agent copies original HTML and swaps data values
  • Agent tools for template CRUD: save_template, list_templates, apply_template (supports name + ID lookup), delete_template
  • Progressive streaming for widget previews with loading indicators

Changes

Frontend

  • New SaveTemplateOverlay shared component wrapping all generative UI components
  • Template cards with inline SVG mini-previews for charts, iframe previews for widgets
  • Apply flow submits through chat textarea with template ID for reliable lookup
  • Removed dead postMessage relay from page component

Agent

  • Template tools with state-based CRUD operations
  • apply_template supports both name and ID-based lookup
  • System prompt instructs agent to copy-and-adapt original HTML when applying templates

Test plan

  • Generate an invoice widget → save as template → apply with new data → verify new invoice renders
  • Generate a bar chart → save → verify mini chart preview in drawer → apply with new data
  • Generate a pie chart → save → verify mini pie preview → apply with new data
  • Delete templates from drawer → verify removal
  • Verify save button only appears after content finishes rendering

- Add template tools (save, list, apply, delete) in agent backend
- Extend AgentState with templates field
- Add "Save as Template" button overlay on widget renderer with
  animated save flow (input → saving spinner → checkmark confirmation)
- Add template library drawer panel (toggle from header)
- Templates saved directly to agent state via setState (no chat round-trip)
- Template deletion from drawer updates state directly
- Apply template sends chat prompt for agent to adapt HTML with new data

Closes CopilotKit#16
- Remove misleading system prompt instruction about "save-as-template"
  messages (saves go directly via agent.setState, not through chat)
- Replace fragile DOM querySelector hack for sending prompts with
  agent.addMessage() + agent.runAgent() API calls
Replace the hide-everything-then-reveal approach with progressive
streaming that shows content building up as the LLM streams HTML
tokens. The iframe shell loads once and content updates are sent
via postMessage instead of full srcdoc reloads.

- Loading phrases shown above the widget during streaming
- Save Template button only appears after streaming settles
- Debounced htmlSettled detection (800ms of no changes)
- Smooth fade-out transitions for streaming indicator
- Apply button now prompts user to describe their new data before
  sending to the agent (textarea with placeholder example)
- Template HTML is passed directly in the prompt so the agent doesn't
  need to look it up from backend state (fixes state sync issue where
  frontend setState wasn't visible to agent tools)
- Fix template card callback signatures
…mponent

Move save-as-template UI from widget-renderer into a reusable
SaveTemplateOverlay component. Wrap bar chart, pie chart, and widget
renderer with it so all generated UIs can be saved as templates.
Uses useAgent() directly instead of window.postMessage relay.
Render inline SVG bar/pie chart previews in template cards using the
saved component data. Widget renderer templates keep the iframe preview.
Templates without HTML show a placeholder icon.
…mission

Submit template apply through CopilotChat textarea so components render
properly. Include template ID in the message for reliable lookup.
Update apply_template tool to support both name and ID-based lookup.
Improve agent prompt to copy original HTML and replace data values.
Remove saveTemplate callback, postMessage handler, and unused useAgent
import from page.tsx. Template saving now happens directly in the
SaveTemplateOverlay component via useAgent().
Copy link
Collaborator

@GeneralJerel GeneralJerel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: UI Template System

Thanks for this contribution — the template save/apply concept is a great addition and the UX polish (streaming previews, mini chart SVGs, animated save flow) is solid work. A few things need attention before this can merge, mostly around security and CI.


Must fix (blocking)

1. XSS via postMessage in iframe bridgewidget-renderer.tsx:351-370

The update-content message listener inside BRIDGE_JS sets innerHTML and re-executes <script> tags without checking e.origin or e.source. Since the iframe uses sandbox="allow-scripts allow-same-origin", any window that can obtain a reference to the iframe (another frame on the page, a browser extension, window.open) can inject and execute arbitrary JS in the parent's origin context.

Suggested fixes (pick one or combine):

  • Check e.source === window.parent and validate e.origin
  • Strip <script> tags from streamed HTML before setting innerHTML
  • Remove allow-same-origin from the sandbox if same-origin access isn't required

2. Lint errors (CI gate) — 5 errors, 2 warnings

These will fail CI and must be resolved:

  • widget-renderer.tsx:549setShowStreamingIndicator(true) called directly in a useEffect body. Derive this value instead (e.g., a ref + timeout, or compute it from existing state).
  • template-card.tsx:68cumulative += d.value mutates a variable during render inside .map(). Pre-compute the cumulative angles with a reduce() pass instead.

3. Template schema mismatch between frontend and backend

SaveTemplateOverlay saves templates directly via agent.setState() with component_type and component_data fields that don't exist in the Python UITemplate TypedDict. Backend save_template produces templates without these fields. This means:

  • The library has to use as unknown as Record<string, unknown> casts to access the extra fields (visible at index.tsx:200-201)
  • Backend tools like list_templates may behave differently depending on which path saved the template

Recommend: unify on one schema. Either add component_type/component_data to the backend UITemplate and always save via the backend tool, or define a single shared shape both paths produce.


Should fix (non-blocking, but worth addressing)

4. submitChatPrompt DOM hacktemplate-library/index.tsx:32-48

Using Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set to programmatically drive a React-controlled textarea is fragile — it'll break silently if CopilotKit renames the data-testid, changes the textarea implementation, or upgrades React. Check if CopilotKit exposes a programmatic sendMessage() API on the agent/chat hook. If not, at minimum add a fallback when the textarea isn't found (e.g., a console warning or toast).

5. Template preview iframe sandboxtemplate-card.tsx:141

The preview iframe uses sandbox="allow-same-origin allow-scripts". This combination allows the iframe content to script its way out of the sandbox (it can remove its own sandbox attribute). Since these render user-saved HTML, consider dropping allow-same-origin (the preview just needs to display HTML/CSS — scripts running in previews seem unnecessary).

6. Script re-execution during streamingwidget-renderer.tsx:356-366

The update-content handler re-runs all <script> tags on every streaming update, not just new ones. For widgets with stateful scripts (e.g., Three.js, D3 transitions), this causes duplicate initialization, compounding event listeners, and memory leaks. Consider marking executed scripts (data attribute) and only running new ones, or deferring script execution until streaming settles.

7. apply_template runtime defaulttemplates.py:80

runtime: ToolRuntime = None — the other tools don't default runtime and shouldn't need to. If this is ever called without LangGraph injecting the runtime, every .state access raises AttributeError. Remove the = None default to match the other tool signatures.


Nits

  • assembleDocument() at widget-renderer.tsx:388 is now a trivial pass-through to assembleShell() — can be inlined or removed.
  • No delete confirmation on templates — one misclick permanently removes a template. A brief "undo" toast would be a nice UX safety net.

Testing

No tests were added for the new components or backend tools. Not blocking for this PR, but flagging it — the streaming postMessage flow and the template save/apply round-trip are the kind of stateful interactions that tend to regress.


Happy to help work through any of these. The core feature is well-designed — just needs the security edges cleaned up. 🙏

- Prevent XSS via postMessage: validate e.source, strip scripts before
  innerHTML, only execute new scripts via data-exec tracking
- Remove allow-same-origin from widget and preview iframe sandboxes
- Fix all lint errors: refactor useEffect setState to adjust-state-during-render
  pattern, replace cumulative mutation with reduce() pre-computation
- Unify template schema: add component_type/component_data to backend
  UITemplate, remove unsafe casts in frontend
- Add console.warn fallback to submitChatPrompt when textarea not found
- Remove apply_template runtime=None default to match other tool signatures
- Remove trivial assembleDocument() pass-through
Python requires that parameters with defaults precede those without.
Since name and template_id have defaults, runtime must too.
Load the iframe shell empty and stream all content via postMessage
after the iframe loads. Previously, partial streaming fragments
(e.g. an unclosed <style> tag) inserted into the shell would consume
the bridge script, preventing all subsequent postMessage updates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants