Skip to content

Commit a3212ef

Browse files
breadonceejerelvelarde
authored andcommitted
feat(templates): improve apply flow with ID-based lookup and chat submission
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.
1 parent fd91f3d commit a3212ef

3 files changed

Lines changed: 80 additions & 27 deletions

File tree

apps/agent/main.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,20 @@
5151
5252
## UI Templates
5353
54-
Users can save generated UIs as reusable templates and apply them later:
55-
56-
- When a user asks to save a widget as a template, call `save_template` with the
57-
widget's HTML, a short name, description, and a description of the data shape.
58-
- When a user asks to apply a template, first call `list_templates` to find the
59-
right one, then call `apply_template` to get its HTML. Adapt the HTML with the
60-
user's new data and render via `widgetRenderer`.
61-
- When a user asks to see their templates, call `list_templates`.
62-
- When a user asks to delete a template, call `delete_template`.
54+
Users can save generated UIs as reusable templates and apply them later.
55+
You have backend tools: `save_template`, `list_templates`, `apply_template`, `delete_template`.
56+
57+
**When a user asks to apply/recreate a template with new data:**
58+
The message includes the template name and ID in the format: "template name" (template-id)
59+
1. Call `apply_template(template_id="...")` with the ID from the message
60+
2. Take the returned HTML and COPY IT EXACTLY, only replacing the data values
61+
(names, numbers, dates, labels, amounts) to match the user's new data
62+
3. Render the modified HTML using `widgetRenderer`
63+
64+
CRITICAL: Do NOT rewrite or generate HTML from scratch. Take the original HTML string,
65+
find-and-replace ONLY the data values, and pass the result to widgetRenderer.
66+
This preserves the exact layout and styling of the original template.
67+
For bar/pie chart templates, use `barChart` or `pieChart` component instead.
6368
""",
6469
)
6570

apps/agent/src/templates.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,24 +77,46 @@ def list_templates(runtime: ToolRuntime):
7777

7878

7979
@tool
80-
def apply_template(template_id: str, runtime: ToolRuntime):
80+
def apply_template(name: str = "", template_id: str = "", runtime: ToolRuntime = None):
8181
"""
8282
Retrieve a saved template's HTML so you can adapt it with new data.
83-
After calling this, modify the HTML to fit the user's new data and render it via widgetRenderer.
83+
After calling this, generate a NEW widget in the same style and render via widgetRenderer.
84+
85+
You can look up by name or ID. If both are provided, ID takes priority.
86+
When multiple templates share the same name, returns the most recently created one.
8487
8588
Args:
86-
template_id: The ID of the template to apply
89+
name: The name of the template to apply (e.g. "Invoice")
90+
template_id: The ID of the template to apply (optional)
8791
"""
8892
templates = runtime.state.get("templates", [])
89-
for t in templates:
90-
if t["id"] == template_id:
93+
94+
# Look up by ID first
95+
if template_id:
96+
for t in templates:
97+
if t["id"] == template_id:
98+
return {
99+
"name": t["name"],
100+
"description": t["description"],
101+
"html": t["html"],
102+
"data_description": t.get("data_description", ""),
103+
}
104+
return {"error": f"Template with id '{template_id}' not found"}
105+
106+
# Look up by name (most recent match)
107+
if name:
108+
matches = [t for t in templates if t["name"].lower() == name.lower()]
109+
if matches:
110+
t = max(matches, key=lambda x: x.get("created_at", ""))
91111
return {
92112
"name": t["name"],
93113
"description": t["description"],
94114
"html": t["html"],
95-
"data_description": t["data_description"],
115+
"data_description": t.get("data_description", ""),
96116
}
97-
return {"error": f"Template with id '{template_id}' not found"}
117+
return {"error": f"No template named '{name}' found"}
118+
119+
return {"error": "Provide either a name or template_id"}
98120

99121

100122
@tool

apps/app/src/components/template-library/index.tsx

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useState, useCallback } from "react";
44
import { useAgent } from "@copilotkit/react-core/v2";
55
import { TemplateCard } from "./template-card";
66

7+
/** Submit a message through the CopilotChat textarea so it goes through the full rendering pipeline */
8+
function submitChatPrompt(text: string) {
9+
const textarea = document.querySelector<HTMLTextAreaElement>(
10+
'[data-testid="copilot-chat-textarea"]'
11+
);
12+
if (!textarea) return;
13+
14+
const setter = Object.getOwnPropertyDescriptor(
15+
window.HTMLTextAreaElement.prototype, "value"
16+
)?.set;
17+
setter?.call(textarea, text);
18+
textarea.dispatchEvent(new Event("input", { bubbles: true }));
19+
20+
setTimeout(() => {
21+
const form = textarea.closest("form");
22+
if (form) form.requestSubmit();
23+
}, 150);
24+
}
25+
726
interface TemplateLibraryProps {
827
open: boolean;
928
onClose: () => void;
10-
onSendPrompt: (text: string) => void;
1129
}
1230

1331
interface Template {
@@ -16,10 +34,11 @@ interface Template {
1634
description: string;
1735
html: string;
1836
data_description: string;
37+
component_type?: string;
1938
version: number;
2039
}
2140

22-
export function TemplateLibrary({ open, onClose, onSendPrompt }: TemplateLibraryProps) {
41+
export function TemplateLibrary({ open, onClose }: TemplateLibraryProps) {
2342
const { agent } = useAgent();
2443
const templates: Template[] = agent.state?.templates || [];
2544

@@ -35,21 +54,26 @@ export function TemplateLibrary({ open, onClose, onSendPrompt }: TemplateLibrary
3554
}
3655
};
3756

38-
const handleApplyConfirm = () => {
57+
const handleApplyConfirm = useCallback(() => {
3958
if (!applyingTemplate) return;
4059
const dataDesc = applyData.trim();
4160
if (!dataDesc) return;
4261

43-
// Send the template HTML directly in the prompt so the agent doesn't
44-
// need to look it up from state (frontend setState may not sync to backend)
45-
onSendPrompt(
46-
`Use this saved template called "${applyingTemplate.name}" as a base layout and adapt it for the following new data: ${dataDesc}\n\n` +
47-
`Here is the template HTML to adapt:\n\n${applyingTemplate.html}`
48-
);
62+
const isChart =
63+
applyingTemplate.component_type === "barChart" ||
64+
applyingTemplate.component_type === "pieChart";
65+
66+
const chartType = applyingTemplate.component_type === "pieChart" ? "pie chart" : "bar chart";
67+
const message = isChart
68+
? `Apply ${chartType} template "${applyingTemplate.name}" (${applyingTemplate.id}) with: ${dataDesc}`
69+
: `Apply template "${applyingTemplate.name}" (${applyingTemplate.id}) with: ${dataDesc}`;
70+
71+
submitChatPrompt(message);
72+
4973
setApplyingTemplate(null);
5074
setApplyData("");
5175
onClose();
52-
};
76+
}, [applyingTemplate, applyData, onClose]);
5377

5478
const handleApplyCancel = () => {
5579
setApplyingTemplate(null);
@@ -153,6 +177,8 @@ export function TemplateLibrary({ open, onClose, onSendPrompt }: TemplateLibrary
153177
name={t.name}
154178
description={t.description}
155179
html={t.html}
180+
componentType={(t as unknown as Record<string, unknown>).component_type as string | undefined}
181+
componentData={(t as unknown as Record<string, unknown>).component_data as Record<string, unknown> | undefined}
156182
dataDescription={t.data_description}
157183
version={t.version}
158184
onApply={handleApplyClick}

0 commit comments

Comments
 (0)