Skip to content

Commit ee73ee2

Browse files
aaronbrezelclaude
andauthored
feat: document summarization recipe redesign for journalism use (#88)
* feat: add conditional block syntax to interpolateTemplate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add same-key block test; docs: note nesting unsupported Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: update document-summarization recipe for journalism use * test: integration tests for recipe template rendering * docs: add design and implementation plan for document summarization recipe redesign * feat: remove redundant User Prompt column from document-summarization recipe * feat: apply field-label styling and (optional) markers to recipe inputs * feat: use field-group wrapper for recipe inputs to get section dividers * feat: add intro field to RecipeDefinition for panel-level description * fix: remove redundant optional field guidance from recipe intro * tweaking helper text * fix: remove BLUF label prefix from system prompt structure guideline --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 88e2fdf commit ee73ee2

10 files changed

Lines changed: 499 additions & 32 deletions

__tests__/panels/recipe.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,16 @@ async function flush() {
6565
describe("rendering", () => {
6666
it("renders one input field per RecipeInput", () => {
6767
const { container } = mount();
68-
expect(container.querySelectorAll(".recipe-input-field")).toHaveLength(2);
68+
expect(container.querySelectorAll(".field-group")).toHaveLength(2);
6969
});
7070

7171
it("renders the label for each input", () => {
7272
const { container } = mount();
73-
const labels = Array.from(container.querySelectorAll(".recipe-input-label")).map(
73+
const labels = Array.from(container.querySelectorAll(".field-label")).map(
7474
(el) => el.textContent,
7575
);
76-
expect(labels).toContain("Drive Folder");
77-
expect(labels).toContain("What are you looking for?");
76+
expect(labels).toContain("Drive Folder *");
77+
expect(labels).toContain("What are you looking for? (optional)");
7878
});
7979

8080
it("renders placeholder on the input element", () => {

__tests__/recipes.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Integration tests for src/client/recipes.ts
3+
*
4+
* Exercises each recipe's template strings through interpolateTemplate to catch
5+
* typos in placeholder/block syntax (e.g. mismatched closing tags) that would
6+
* silently pass unit tests on either module in isolation.
7+
*/
8+
9+
import { RECIPES } from "../src/client/recipes";
10+
import { interpolateTemplate } from "../src/server/utils";
11+
import type { RecipeColumn } from "../src/client/types";
12+
13+
function getTemplateCol(cols: RecipeColumn[], role: RecipeColumn["role"]): string | null {
14+
const col = cols.find((c) => c.role === role);
15+
if (!col || col.fillStrategy.kind !== "template") return null;
16+
return col.fillStrategy.template;
17+
}
18+
19+
describe("document-summarization recipe", () => {
20+
const recipe = RECIPES.find((r) => r.id === "document-summarization");
21+
22+
it("is registered in RECIPES", () => {
23+
expect(recipe).toBeDefined();
24+
});
25+
26+
describe("system-prompt template", () => {
27+
const template = recipe ? getTemplateCol(recipe.prepTemplate, "system-prompt") : null;
28+
29+
it("uses a template fill strategy", () => {
30+
expect(template).not.toBeNull();
31+
});
32+
33+
it("renders cleanly with no optional inputs", () => {
34+
const result = interpolateTemplate(template!, { folder: "", docType: "", focus: "" });
35+
expect(result).not.toContain("{{");
36+
expect(result).toContain("Briefing Assistant");
37+
expect(result).not.toContain("Document type");
38+
expect(result).not.toContain("Area of interest");
39+
});
40+
41+
it("renders docType when provided", () => {
42+
const result = interpolateTemplate(template!, {
43+
folder: "",
44+
docType: "court filing",
45+
focus: "",
46+
});
47+
expect(result).toContain("Document type: court filing");
48+
expect(result).not.toContain("Area of interest");
49+
expect(result).not.toContain("{{");
50+
});
51+
52+
it("renders focus when provided", () => {
53+
const result = interpolateTemplate(template!, {
54+
folder: "",
55+
docType: "",
56+
focus: "financial fraud",
57+
});
58+
expect(result).not.toContain("Document type");
59+
expect(result).toContain("Area of interest: financial fraud");
60+
expect(result).not.toContain("{{");
61+
});
62+
63+
it("renders both optional inputs when provided", () => {
64+
const result = interpolateTemplate(template!, {
65+
folder: "",
66+
docType: "FOIA response",
67+
focus: "conflicts of interest",
68+
});
69+
expect(result).toContain("Document type: FOIA response");
70+
expect(result).toContain("Area of interest: conflicts of interest");
71+
expect(result).not.toContain("{{");
72+
});
73+
});
74+
});

__tests__/utils.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,7 @@ describe("interpolateTemplate", () => {
437437
});
438438

439439
it("replaces multiple placeholders in one string", () => {
440-
expect(
441-
interpolateTemplate("{{a}} and {{b}}", { a: "foo", b: "bar" }),
442-
).toBe("foo and bar");
440+
expect(interpolateTemplate("{{a}} and {{b}}", { a: "foo", b: "bar" })).toBe("foo and bar");
443441
});
444442

445443
it("replaces the same placeholder multiple times", () => {
@@ -453,4 +451,50 @@ describe("interpolateTemplate", () => {
453451
it("returns the string unchanged when no placeholders present", () => {
454452
expect(interpolateTemplate("no placeholders", { x: "y" })).toBe("no placeholders");
455453
});
454+
455+
it("includes block content when the key has a non-empty value", () => {
456+
expect(interpolateTemplate("{{#focus}}Focus: {{focus}}{{/focus}}", { focus: "fraud" })).toBe(
457+
"Focus: fraud",
458+
);
459+
});
460+
461+
it("omits block content when the key is empty string", () => {
462+
expect(interpolateTemplate("{{#focus}}Focus: {{focus}}{{/focus}}", { focus: "" })).toBe("");
463+
});
464+
465+
it("omits block content when the key is missing from inputValues", () => {
466+
expect(interpolateTemplate("{{#focus}}Focus: {{focus}}{{/focus}}", {})).toBe("");
467+
});
468+
469+
it("handles multiple conditional blocks independently", () => {
470+
expect(
471+
interpolateTemplate(
472+
"{{#docType}}Type: {{docType}}{{/docType}}\n{{#focus}}Focus: {{focus}}{{/focus}}",
473+
{ docType: "filing", focus: "" },
474+
),
475+
).toBe("Type: filing\n");
476+
});
477+
478+
it("handles conditional blocks alongside simple placeholders", () => {
479+
expect(
480+
interpolateTemplate("Hello {{name}}{{#extra}} ({{extra}}){{/extra}}", {
481+
name: "world",
482+
extra: "detail",
483+
}),
484+
).toBe("Hello world (detail)");
485+
});
486+
487+
it("handles conditional blocks with multiline content", () => {
488+
expect(interpolateTemplate("before\n{{#key}}line1\nline2\n{{/key}}after", { key: "yes" })).toBe(
489+
"before\nline1\nline2\nafter",
490+
);
491+
});
492+
493+
it("handles two conditional blocks with the same key", () => {
494+
expect(interpolateTemplate("{{#x}}first{{/x}}{{#x}}second{{/x}}", { x: "yes" })).toBe(
495+
"firstsecond",
496+
);
497+
498+
expect(interpolateTemplate("{{#x}}first{{/x}}{{#x}}second{{/x}}", { x: "" })).toBe("");
499+
});
456500
});
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Document Summarization Recipe Redesign — Implementation Plan
2+
3+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4+
5+
**Goal:** Update the document-summarization recipe for journalism use — two new optional inputs (document type, area of interest), a journalist-oriented BLUF system prompt using conditional template blocks, and the infrastructure to support those blocks.
6+
7+
**Architecture:** Add a two-pass conditional block syntax (`{{#key}}...{{/key}}`) to `interpolateTemplate`, then update the recipe definition to use the new inputs and template. No new files; three existing files modified.
8+
9+
**Tech Stack:** TypeScript, Jest, Rollup (no new dependencies)
10+
11+
---
12+
13+
### Task 1: Extend `interpolateTemplate` with conditional block support
14+
15+
**Files:**
16+
- Modify: `src/server/utils.ts:145-151`
17+
- Test: `__tests__/utils.test.ts:434-456`
18+
19+
**Step 1: Write the failing tests**
20+
21+
Add the following cases to the `describe("interpolateTemplate", ...)` block in `__tests__/utils.test.ts`, just before the closing `});` on line 456:
22+
23+
```typescript
24+
it("includes block content when the key has a non-empty value", () => {
25+
expect(
26+
interpolateTemplate("{{#focus}}Focus: {{focus}}{{/focus}}", { focus: "fraud" }),
27+
).toBe("Focus: fraud");
28+
});
29+
30+
it("omits block content when the key is empty string", () => {
31+
expect(
32+
interpolateTemplate("{{#focus}}Focus: {{focus}}{{/focus}}", { focus: "" }),
33+
).toBe("");
34+
});
35+
36+
it("omits block content when the key is missing from inputValues", () => {
37+
expect(
38+
interpolateTemplate("{{#focus}}Focus: {{focus}}{{/focus}}", {}),
39+
).toBe("");
40+
});
41+
42+
it("handles multiple conditional blocks independently", () => {
43+
expect(
44+
interpolateTemplate(
45+
"{{#docType}}Type: {{docType}}{{/docType}}\n{{#focus}}Focus: {{focus}}{{/focus}}",
46+
{ docType: "filing", focus: "" },
47+
),
48+
).toBe("Type: filing\n");
49+
});
50+
51+
it("handles conditional blocks alongside simple placeholders", () => {
52+
expect(
53+
interpolateTemplate("Hello {{name}}{{#extra}} ({{extra}}){{/extra}}", {
54+
name: "world",
55+
extra: "detail",
56+
}),
57+
).toBe("Hello world (detail)");
58+
});
59+
60+
it("handles conditional blocks with multiline content", () => {
61+
expect(
62+
interpolateTemplate("before\n{{#key}}line1\nline2\n{{/key}}after", { key: "yes" }),
63+
).toBe("before\nline1\nline2\nafter");
64+
});
65+
```
66+
67+
**Step 2: Run tests to verify they fail**
68+
69+
```bash
70+
npx jest __tests__/utils.test.ts -t "interpolateTemplate" --no-coverage
71+
```
72+
73+
Expected: 6 new tests FAIL with errors like `"Focus: fraud" !== ""`
74+
75+
**Step 3: Implement two-pass interpolation**
76+
77+
Replace the body of `interpolateTemplate` in `src/server/utils.ts` (lines 145–151):
78+
79+
```typescript
80+
/**
81+
* Replace {{inputId}} placeholders and {{#key}}...{{/key}} conditional blocks
82+
* in a template string with values from a map.
83+
*
84+
* Conditional blocks: content is included only when the named key has a non-empty value.
85+
* Simple placeholders: unknown keys are replaced with an empty string.
86+
*/
87+
export function interpolateTemplate(template: string, inputValues: Record<string, string>): string {
88+
// Pass 1: conditional blocks — include content only if value is non-empty
89+
const withBlocks = template.replace(
90+
/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g,
91+
(_, id: string, content: string) => ((inputValues[id] ?? "") ? content : ""),
92+
);
93+
// Pass 2: simple interpolations
94+
return withBlocks.replace(/\{\{(\w+)\}\}/g, (_, id: string) => inputValues[id] ?? "");
95+
}
96+
```
97+
98+
**Step 4: Run tests to verify they pass**
99+
100+
```bash
101+
npx jest __tests__/utils.test.ts -t "interpolateTemplate" --no-coverage
102+
```
103+
104+
Expected: all 11 tests PASS (5 existing + 6 new)
105+
106+
**Step 5: Run the full test suite**
107+
108+
```bash
109+
npm test
110+
```
111+
112+
Expected: all tests PASS
113+
114+
**Step 6: Commit**
115+
116+
```bash
117+
git add src/server/utils.ts __tests__/utils.test.ts
118+
git commit -m "feat: add conditional block syntax to interpolateTemplate"
119+
```
120+
121+
---
122+
123+
### Task 2: Update the document-summarization recipe
124+
125+
**Files:**
126+
- Modify: `src/client/recipes.ts`
127+
128+
**Step 1: Replace the recipe definition**
129+
130+
Replace the entire contents of `src/client/recipes.ts` with:
131+
132+
```typescript
133+
import type { RecipeDefinition } from "./types";
134+
135+
export const RECIPES: RecipeDefinition[] = [
136+
{
137+
id: "document-summarization",
138+
name: "Document Summarization",
139+
icon: "📄",
140+
description: "Summarize files in a Google Drive folder",
141+
inputs: [
142+
{
143+
id: "folder",
144+
label: "Drive Folder",
145+
required: true,
146+
helperText: "Make sure you have access to this folder",
147+
placeholder: "Paste Google Drive folder URL",
148+
},
149+
{
150+
id: "docType",
151+
label: "Document Type",
152+
required: false,
153+
placeholder: "e.g. court filing, FOIA response, annual report",
154+
},
155+
{
156+
id: "focus",
157+
label: "Area of Interest",
158+
required: false,
159+
placeholder: "e.g. financial fraud, conflicts of interest",
160+
},
161+
],
162+
prepTemplate: [
163+
{
164+
colTitle: "Drive Link",
165+
fillStrategy: { kind: "list-drive-folder", inputId: "folder" },
166+
role: "file-prompt",
167+
},
168+
{
169+
colTitle: "System Prompt",
170+
fillStrategy: {
171+
kind: "template",
172+
template:
173+
"Role: You are a specialized Briefing Assistant. Your goal is to distill complex documents into ultra-concise, scannable summaries.\n\n" +
174+
"Tone: Objective, professional, and dense with information but sparse with \"fluff\" words.\n\n" +
175+
"Guidelines:\n" +
176+
" - Prioritize Utility: Focus on information that helps a user decide: \"Do I need to open the full file?\"\n" +
177+
" - Structure: Always start with a 1-sentence \"Bottom Line Up Front\" (BLUF). Follow with 3-5 high-impact bullet points.\n" +
178+
" - Constraint: Keep the entire output under 150 words.\n" +
179+
"{{#docType}} - Document type: {{docType}}\n{{/docType}}" +
180+
"{{#focus}} - Area of interest: {{focus}} — prioritize this above all else.\n{{/focus}}",
181+
},
182+
role: "system-prompt",
183+
},
184+
{
185+
colTitle: "User Prompt",
186+
fillStrategy: {
187+
kind: "fill-value",
188+
value: "Summarize the attached document.",
189+
},
190+
role: "text-prompt",
191+
},
192+
{
193+
colTitle: "AI_Summarization",
194+
fillStrategy: { kind: "create-empty" },
195+
role: "output",
196+
},
197+
],
198+
},
199+
];
200+
```
201+
202+
**Step 2: Type-check**
203+
204+
```bash
205+
npm run typecheck
206+
```
207+
208+
Expected: no errors
209+
210+
**Step 3: Build**
211+
212+
```bash
213+
npm run build
214+
```
215+
216+
Expected: clean build, no errors
217+
218+
**Step 4: Commit**
219+
220+
```bash
221+
git add src/client/recipes.ts
222+
git commit -m "feat: update document-summarization recipe for journalism use"
223+
```

0 commit comments

Comments
 (0)