Skip to content

Commit a1c84e0

Browse files
committed
fix(templates): reliable apply via pending_template and non-overlapping badge
- apply_template tool now auto-reads pending_template from state before falling back to name/ID args, so the agent doesn't need to parse state - Seed templates merged client-side in TemplateLibrary (removed broken agent-state seeding hook that ran before session was established) - Template name badge moved above widget content to avoid overlapping - Badge detects source template via pending_template ref + exact HTML match
1 parent bfd3844 commit a1c84e0

File tree

5 files changed

+77
-40
lines changed

5 files changed

+77
-40
lines changed

apps/agent/src/templates.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,21 @@ def apply_template(name: str = "", template_id: str = "", runtime: ToolRuntime =
8484
Retrieve a saved template's HTML so you can adapt it with new data.
8585
After calling this, generate a NEW widget in the same style and render via widgetRenderer.
8686
87-
You can look up by name or ID. If both are provided, ID takes priority.
88-
When multiple templates share the same name, returns the most recently created one.
87+
This tool automatically checks for a pending_template in state (set by the
88+
frontend when the user picks a template from the library). If pending_template
89+
is present, it takes priority over name/template_id arguments.
8990
9091
Args:
91-
name: The name of the template to apply (e.g. "Invoice")
92-
template_id: The ID of the template to apply (optional)
92+
name: The name of the template to apply (fallback if no pending_template)
93+
template_id: The ID of the template to apply (fallback if no pending_template)
9394
"""
9495
templates = runtime.state.get("templates", [])
9596

97+
# Check pending_template from frontend first — this is the most reliable source
98+
pending = runtime.state.get("pending_template")
99+
if pending and pending.get("id"):
100+
template_id = pending["id"]
101+
96102
# Look up by ID first
97103
if template_id:
98104
for t in templates:

apps/app/src/app/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useEffect, useState } from "react";
44
import { ExampleLayout } from "@/components/example-layout";
55
import { useGenerativeUIExamples, useExampleSuggestions } from "@/hooks";
6-
import { useSeedTemplates } from "@/hooks/use-seed-templates";
76
import { ExplainerCardsPortal } from "@/components/explainer-cards";
87
import { TemplateLibrary } from "@/components/template-library";
98
import { TemplateChip } from "@/components/template-library/template-chip";
@@ -13,7 +12,6 @@ import { CopilotChat } from "@copilotkit/react-core/v2";
1312
export default function HomePage() {
1413
useGenerativeUIExamples();
1514
useExampleSuggestions();
16-
useSeedTemplates();
1715

1816
const [templateDrawerOpen, setTemplateDrawerOpen] = useState(false);
1917

apps/app/src/components/generative-ui/save-template-overlay.tsx

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use client";
22

3-
import { useState, useCallback, type ReactNode } from "react";
3+
import { useState, useCallback, useMemo, useRef, type ReactNode } from "react";
44
import { useAgent } from "@copilotkit/react-core/v2";
5+
import { SEED_TEMPLATES } from "@/components/template-library/seed-templates";
56

67
type SaveState = "idle" | "input" | "saving" | "saved";
78

@@ -34,6 +35,37 @@ export function SaveTemplateOverlay({
3435
const [saveState, setSaveState] = useState<SaveState>("idle");
3536
const [templateName, setTemplateName] = useState("");
3637

38+
// Capture pending_template at mount time — it may be cleared by the agent later
39+
const pending = agent.state?.pending_template as { id: string; name: string } | null | undefined;
40+
const sourceRef = useRef<{ id: string; name: string } | null>(null);
41+
if (pending?.id && !sourceRef.current) {
42+
sourceRef.current = pending;
43+
}
44+
45+
// Check if this content matches an existing template:
46+
// 1. Exact HTML match (seed templates rendered as-is)
47+
// 2. Source template captured from pending_template (applied templates with modified data)
48+
const matchedTemplate = useMemo(() => {
49+
// First check source template from apply flow
50+
if (sourceRef.current) {
51+
const allTemplates = [
52+
...SEED_TEMPLATES,
53+
...((agent.state?.templates as { id: string; name: string }[]) || []),
54+
];
55+
const source = allTemplates.find((t) => t.id === sourceRef.current!.id);
56+
if (source) return source;
57+
}
58+
// Then check exact HTML match
59+
if (!html) return null;
60+
const normalise = (s: string) => s.replace(/\s+/g, " ").trim();
61+
const norm = normalise(html);
62+
const allTemplates = [
63+
...SEED_TEMPLATES,
64+
...((agent.state?.templates as { id: string; name: string; html: string }[]) || []),
65+
];
66+
return allTemplates.find((t) => t.html && normalise(t.html) === norm) ?? null;
67+
}, [html, agent.state?.templates]);
68+
3769
const handleSave = useCallback(() => {
3870
const name = templateName.trim() || title || "Untitled Template";
3971
setSaveState("saving");
@@ -161,8 +193,8 @@ export function SaveTemplateOverlay({
161193
</div>
162194
)}
163195

164-
{/* Idle bookmark button */}
165-
{saveState === "idle" && (
196+
{/* Idle: show save button (badge moved outside this container) */}
197+
{saveState === "idle" && !matchedTemplate && (
166198
<button
167199
onClick={() => setSaveState("input")}
168200
className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium shadow-md transition-all duration-150 hover:scale-105"
@@ -181,6 +213,25 @@ export function SaveTemplateOverlay({
181213
)}
182214
</div>
183215

216+
{/* Template name badge — shown above widget when matched */}
217+
{saveState === "idle" && matchedTemplate && ready && (
218+
<div className="flex justify-end mb-1">
219+
<div
220+
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium"
221+
style={{
222+
background: "linear-gradient(135deg, rgba(99,102,241,0.10), rgba(16,185,129,0.10))",
223+
border: "1px solid rgba(99,102,241,0.20)",
224+
color: "var(--text-primary, #1a1a1a)",
225+
}}
226+
>
227+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.55 }}>
228+
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
229+
</svg>
230+
{matchedTemplate.name}
231+
</div>
232+
</div>
233+
)}
234+
184235
{children}
185236
</div>
186237
);

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useAgent } from "@copilotkit/react-core/v2";
44
import { TemplateCard } from "./template-card";
5+
import { SEED_TEMPLATES } from "./seed-templates";
56

67
interface TemplateLibraryProps {
78
open: boolean;
@@ -21,15 +22,25 @@ interface Template {
2122

2223
export function TemplateLibrary({ open, onClose }: TemplateLibraryProps) {
2324
const { agent } = useAgent();
24-
const templates: Template[] = agent.state?.templates || [];
25+
const agentTemplates: Template[] = agent.state?.templates || [];
26+
// Merge seed templates with user-saved ones
27+
const templates: Template[] = [
28+
...SEED_TEMPLATES.filter((s) => !agentTemplates.some((t) => t.id === s.id)),
29+
...agentTemplates,
30+
];
2531

2632
const handleApplyClick = (id: string) => {
2733
const template = templates.find((t) => t.id === id);
2834
if (!template) return;
2935

30-
// Attach template as a chip in the chat input — user types their prompt naturally
36+
// Ensure template is in agent state so the backend can retrieve it via apply_template
37+
const stateTemplates = agentTemplates.some((t) => t.id === id)
38+
? agentTemplates
39+
: [...agentTemplates, template];
40+
3141
agent.setState({
3242
...agent.state,
43+
templates: stateTemplates,
3344
pending_template: { id: template.id, name: template.name },
3445
});
3546
onClose();

apps/app/src/hooks/use-seed-templates.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

0 commit comments

Comments
 (0)