@terreno/blocks 57.12.0 → 57.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +13 -1
- package/dist/interactive.d.ts +26 -0
- package/dist/interactive.js +62 -0
- package/dist/limits.d.ts +26 -0
- package/dist/limits.js +26 -0
- package/dist/lint.d.ts +18 -1
- package/dist/lint.js +186 -26
- package/dist/plainText.d.ts +17 -0
- package/dist/plainText.js +56 -0
- package/dist/prompt.d.ts +10 -1
- package/dist/prompt.js +73 -3
- package/dist/schema.d.ts +113 -2
- package/dist/schema.js +121 -5
- package/dist/validate.js +26 -2
- package/package.json +1 -1
- package/src/__snapshots__/prompt.test.ts.snap +530 -0
- package/src/errors.ts +4 -0
- package/src/fixtures/golden/sunday-roast.yaml +174 -0
- package/src/fixtures/invalid/card-eyebrow-empty.yaml +7 -0
- package/src/fixtures/invalid/card-eyebrow-too-long.yaml +7 -0
- package/src/fixtures/invalid/checklist-duplicate-item.yaml +9 -0
- package/src/fixtures/invalid/checklist-item-id-too-long.yaml +7 -0
- package/src/fixtures/invalid/checklist-reserved-id.yaml +16 -0
- package/src/fixtures/invalid/copy-missing-text-and-target.yaml +10 -0
- package/src/fixtures/invalid/copy-target.yaml +14 -0
- package/src/fixtures/invalid/gallery-caption-too-long.yaml +9 -0
- package/src/fixtures/invalid/gallery-image-host.yaml +10 -0
- package/src/fixtures/invalid/gallery-too-few.yaml +6 -0
- package/src/fixtures/invalid/list-image-host.yaml +12 -0
- package/src/fixtures/invalid/list-missing-title.yaml +6 -0
- package/src/fixtures/invalid/list-too-many.yaml +17 -0
- package/src/fixtures/invalid/stepper-decimals.yaml +14 -0
- package/src/fixtures/invalid/stepper-out-of-range.yaml +10 -0
- package/src/fixtures/invalid/stepper-reserved-id.yaml +19 -0
- package/src/fixtures/valid/card-eyebrow.yaml +8 -0
- package/src/fixtures/valid/checklist.yaml +29 -0
- package/src/fixtures/valid/copy.yaml +85 -0
- package/src/fixtures/valid/gallery.yaml +22 -0
- package/src/fixtures/valid/list.yaml +24 -0
- package/src/fixtures/valid/stepper.yaml +35 -0
- package/src/golden.test.ts +234 -0
- package/src/index.test.ts +9 -0
- package/src/index.ts +24 -0
- package/src/interactive.test.ts +141 -0
- package/src/interactive.ts +75 -0
- package/src/limits.ts +26 -0
- package/src/lint.test.ts +672 -0
- package/src/lint.ts +264 -33
- package/src/parse.test.ts +40 -0
- package/src/plainText.test.ts +158 -0
- package/src/plainText.ts +81 -0
- package/src/prompt.test.ts +325 -2
- package/src/prompt.ts +87 -2
- package/src/schema.ts +259 -6
- package/src/validate.ts +26 -2
package/src/prompt.test.ts
CHANGED
|
@@ -3,9 +3,49 @@ import {describe, expect, it} from "bun:test";
|
|
|
3
3
|
import {BLOCK_LIMITS} from "./limits";
|
|
4
4
|
import {blocksPromptSection} from "./prompt";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* The option sets the prompt took before rich blocks existed. Their snapshots were written by
|
|
8
|
+
* the pre-rich-blocks prompt, so `richBlocks: false` must keep reproducing them byte for byte.
|
|
9
|
+
*/
|
|
10
|
+
const LEGACY_OPTION_SETS = {
|
|
11
|
+
allowHtml: {allowHtml: true},
|
|
12
|
+
everything: {
|
|
13
|
+
allowHtml: true,
|
|
14
|
+
hostActions: ["approve", "scaleStepper"],
|
|
15
|
+
imageHosts: ["cdn.example.com"],
|
|
16
|
+
},
|
|
17
|
+
hostActions: {hostActions: ["exportDataset"]},
|
|
18
|
+
imageHosts: {imageHosts: ["cdn.example.com"]},
|
|
19
|
+
none: {},
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
const STEPPER_LINE_START = "stepper requires";
|
|
23
|
+
const CHECKLIST_LINE_START = "checklist requires";
|
|
24
|
+
const GALLERY_LINE_START = "gallery requires";
|
|
25
|
+
const LIST_LINE_START = "list requires";
|
|
26
|
+
const COPY_LINE_START = "action.kind may also be copy";
|
|
27
|
+
|
|
28
|
+
/** Every option that changes the rich prompt, so the full on-case snapshot catches any drift. */
|
|
29
|
+
const FULL_RICH_OPTIONS = {
|
|
30
|
+
checklistActions: ["toggleChecklist"],
|
|
31
|
+
hostActions: ["approve", "scaleStepper", "toggleChecklist"],
|
|
32
|
+
imageHosts: ["images.example.com"],
|
|
33
|
+
stepperActions: ["scaleStepper"],
|
|
34
|
+
} as const;
|
|
35
|
+
/** Matches the list block by word, so checklist lines do not count. */
|
|
36
|
+
const LIST_WORD = /\blist\b/;
|
|
37
|
+
|
|
38
|
+
const linesAdded = (section: string, base: string): string[] => {
|
|
39
|
+
const baseLines = new Set(base.split("\n"));
|
|
40
|
+
return section.split("\n").filter((line) => !baseLines.has(line));
|
|
41
|
+
};
|
|
42
|
+
|
|
6
43
|
describe("blocksPromptSection", () => {
|
|
7
44
|
it("embeds the reply rule, the ref rule, both examples, and every limit", () => {
|
|
8
|
-
const section = blocksPromptSection({
|
|
45
|
+
const section = blocksPromptSection({
|
|
46
|
+
hostActions: ["exportDataset", "scaleStepper"],
|
|
47
|
+
stepperActions: ["scaleStepper"],
|
|
48
|
+
});
|
|
9
49
|
expect(section).toContain(
|
|
10
50
|
"Your entire reply is one document; no prose outside it; no text before tool calls."
|
|
11
51
|
);
|
|
@@ -13,7 +53,7 @@ describe("blocksPromptSection", () => {
|
|
|
13
53
|
expect(section).toContain("exportDataset");
|
|
14
54
|
expect(section).toContain("Show weekly signups for this quarter");
|
|
15
55
|
expect(section).toContain("I could not find any signups for that range.");
|
|
16
|
-
for (const value of Object.
|
|
56
|
+
for (const [, value] of Object.entries(BLOCK_LIMITS)) {
|
|
17
57
|
expect(section).toContain(String(value));
|
|
18
58
|
}
|
|
19
59
|
});
|
|
@@ -21,4 +61,287 @@ describe("blocksPromptSection", () => {
|
|
|
21
61
|
it("says no callbacks are registered when the host passes none", () => {
|
|
22
62
|
expect(blocksPromptSection()).toContain("No callback names are registered.");
|
|
23
63
|
});
|
|
64
|
+
|
|
65
|
+
describe("with richBlocks false", () => {
|
|
66
|
+
for (const [name, options] of Object.entries(LEGACY_OPTION_SETS)) {
|
|
67
|
+
it(`matches the pre-rich-blocks prompt for ${name}, even with stepper and checklist actions`, () => {
|
|
68
|
+
const section = blocksPromptSection({
|
|
69
|
+
...options,
|
|
70
|
+
checklistActions: ["toggleChecklist"],
|
|
71
|
+
richBlocks: false,
|
|
72
|
+
stepperActions: ["scaleStepper"],
|
|
73
|
+
});
|
|
74
|
+
expect(section).toMatchSnapshot();
|
|
75
|
+
expect(section).not.toContain("stepper");
|
|
76
|
+
expect(section).not.toContain("checklist");
|
|
77
|
+
expect(section).not.toContain("gallery");
|
|
78
|
+
expect(section).not.toMatch(LIST_WORD);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("the stepper line", () => {
|
|
84
|
+
it("is on by default when the host registers a stepper action", () => {
|
|
85
|
+
const section = blocksPromptSection({
|
|
86
|
+
hostActions: ["scaleStepper"],
|
|
87
|
+
stepperActions: ["scaleStepper"],
|
|
88
|
+
});
|
|
89
|
+
expect(section).toContain(STEPPER_LINE_START);
|
|
90
|
+
expect(section).toBe(
|
|
91
|
+
blocksPromptSection({
|
|
92
|
+
hostActions: ["scaleStepper"],
|
|
93
|
+
richBlocks: true,
|
|
94
|
+
stepperActions: ["scaleStepper"],
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("never mentions stepper without a stepper action", () => {
|
|
100
|
+
expect(blocksPromptSection({hostActions: ["scaleStepper"]})).not.toContain("stepper");
|
|
101
|
+
expect(
|
|
102
|
+
blocksPromptSection({hostActions: ["scaleStepper"], stepperActions: []})
|
|
103
|
+
).not.toContain("stepper");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("lists stepper as a block type and names every stepper action as its callback", () => {
|
|
107
|
+
const section = blocksPromptSection({
|
|
108
|
+
hostActions: ["approve", "scaleStepper", "scalePortions"],
|
|
109
|
+
stepperActions: ["scaleStepper", "scalePortions"],
|
|
110
|
+
});
|
|
111
|
+
const typesLine = section.split("\n").find((line) => line.startsWith("Block types:"));
|
|
112
|
+
expect(typesLine).toContain(", stepper");
|
|
113
|
+
expect(section).toContain(
|
|
114
|
+
"A stepper callback name must be one of: scaleStepper, scalePortions."
|
|
115
|
+
);
|
|
116
|
+
expect(section).toContain(
|
|
117
|
+
"A callback name must be one of: approve, scaleStepper, scalePortions."
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("prints every stepper limit from BLOCK_LIMITS", () => {
|
|
122
|
+
const withStepper = blocksPromptSection({stepperActions: ["scaleStepper"]});
|
|
123
|
+
const without = blocksPromptSection();
|
|
124
|
+
const added = withStepper
|
|
125
|
+
.split("\n")
|
|
126
|
+
.filter((line) => !without.split("\n").includes(line))
|
|
127
|
+
.join("\n");
|
|
128
|
+
const stepperLimits = Object.entries(BLOCK_LIMITS).filter(([key]) =>
|
|
129
|
+
key.startsWith("stepper")
|
|
130
|
+
);
|
|
131
|
+
expect(stepperLimits.length).toBeGreaterThan(0);
|
|
132
|
+
for (const [, value] of stepperLimits) {
|
|
133
|
+
expect(added).toContain(String(value));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("the checklist line", () => {
|
|
139
|
+
it("is on by default and lists checklist as a block type", () => {
|
|
140
|
+
const section = blocksPromptSection();
|
|
141
|
+
expect(section).toContain(CHECKLIST_LINE_START);
|
|
142
|
+
expect(section).toBe(blocksPromptSection({richBlocks: true}));
|
|
143
|
+
const typesLine = section.split("\n").find((line) => line.startsWith("Block types:"));
|
|
144
|
+
expect(typesLine).toContain(", checklist");
|
|
145
|
+
const allowHtmlTypes = blocksPromptSection({allowHtml: true})
|
|
146
|
+
.split("\n")
|
|
147
|
+
.find((line) => line.startsWith("Block types:"));
|
|
148
|
+
expect(allowHtmlTypes).toContain(", checklist");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("tells the model to leave callback out when no checklist action is registered", () => {
|
|
152
|
+
const section = blocksPromptSection({hostActions: ["toggleChecklist"]});
|
|
153
|
+
const added = linesAdded(
|
|
154
|
+
section,
|
|
155
|
+
blocksPromptSection({hostActions: ["toggleChecklist"], richBlocks: false})
|
|
156
|
+
);
|
|
157
|
+
const callbackLine = added.find(
|
|
158
|
+
(line) => line.includes("checklist") && line.includes("callback")
|
|
159
|
+
);
|
|
160
|
+
expect(callbackLine).toContain("Leave callback out of a checklist");
|
|
161
|
+
expect(added.join("\n")).not.toContain("toggleChecklist");
|
|
162
|
+
expect(blocksPromptSection({checklistActions: []})).toBe(blocksPromptSection());
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("names every checklist action as the callback to set when the host registers them", () => {
|
|
166
|
+
const section = blocksPromptSection({
|
|
167
|
+
checklistActions: ["toggleChecklist", "recordProgress"],
|
|
168
|
+
hostActions: ["approve", "toggleChecklist", "recordProgress"],
|
|
169
|
+
});
|
|
170
|
+
expect(section).toContain(
|
|
171
|
+
"Set a checklist callback name to one of: toggleChecklist, recordProgress."
|
|
172
|
+
);
|
|
173
|
+
expect(section).not.toContain("Leave callback out of a checklist");
|
|
174
|
+
expect(section).not.toContain("stepper");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("prints every checklist limit from BLOCK_LIMITS in the lines it adds", () => {
|
|
178
|
+
const added = linesAdded(
|
|
179
|
+
blocksPromptSection(),
|
|
180
|
+
blocksPromptSection({richBlocks: false})
|
|
181
|
+
).join("\n");
|
|
182
|
+
const checklistLimits = Object.entries(BLOCK_LIMITS).filter(([key]) =>
|
|
183
|
+
key.startsWith("checklist")
|
|
184
|
+
);
|
|
185
|
+
expect(checklistLimits.length).toBeGreaterThan(0);
|
|
186
|
+
for (const [, value] of checklistLimits) {
|
|
187
|
+
expect(added).toContain(String(value));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("the gallery line", () => {
|
|
193
|
+
it("is on by default and lists gallery as a block type", () => {
|
|
194
|
+
const section = blocksPromptSection();
|
|
195
|
+
expect(section).toContain(GALLERY_LINE_START);
|
|
196
|
+
for (const allowHtml of [false, true]) {
|
|
197
|
+
const typesLine = blocksPromptSection({allowHtml})
|
|
198
|
+
.split("\n")
|
|
199
|
+
.find((line) => line.startsWith("Block types:"));
|
|
200
|
+
expect(typesLine).toContain(", gallery");
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("sends gallery srcs to the image rules and keeps the https rule tied to imageHosts", () => {
|
|
205
|
+
const withoutHosts = linesAdded(
|
|
206
|
+
blocksPromptSection(),
|
|
207
|
+
blocksPromptSection({richBlocks: false})
|
|
208
|
+
)
|
|
209
|
+
.filter((line) => line.includes("gallery"))
|
|
210
|
+
.join("\n");
|
|
211
|
+
expect(withoutHosts).toContain("image src rules");
|
|
212
|
+
expect(withoutHosts).not.toContain("https");
|
|
213
|
+
const withHosts = blocksPromptSection({imageHosts: ["cdn.example.com"]});
|
|
214
|
+
expect(withHosts).toContain(
|
|
215
|
+
"An https image src must use one of these hosts: cdn.example.com."
|
|
216
|
+
);
|
|
217
|
+
expect(blocksPromptSection()).toContain(
|
|
218
|
+
"Do not emit an https image src. Use a data:image URL or a file: ref."
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("prints every gallery limit from BLOCK_LIMITS in the gallery lines", () => {
|
|
223
|
+
const galleryLines = linesAdded(
|
|
224
|
+
blocksPromptSection(),
|
|
225
|
+
blocksPromptSection({richBlocks: false})
|
|
226
|
+
)
|
|
227
|
+
.filter((line) => line.includes("gallery"))
|
|
228
|
+
.join("\n");
|
|
229
|
+
const galleryLimits = Object.entries(BLOCK_LIMITS).filter(([key]) =>
|
|
230
|
+
key.startsWith("gallery")
|
|
231
|
+
);
|
|
232
|
+
expect(galleryLimits.length).toBe(4);
|
|
233
|
+
for (const [, value] of galleryLimits) {
|
|
234
|
+
expect(galleryLines).toContain(String(value));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("the list line", () => {
|
|
240
|
+
const listLines = (): string =>
|
|
241
|
+
linesAdded(blocksPromptSection(), blocksPromptSection({richBlocks: false}))
|
|
242
|
+
.filter((line) => LIST_WORD.test(line))
|
|
243
|
+
.join("\n");
|
|
244
|
+
|
|
245
|
+
it("is on by default and lists list as a block type", () => {
|
|
246
|
+
expect(blocksPromptSection()).toContain(LIST_LINE_START);
|
|
247
|
+
for (const allowHtml of [false, true]) {
|
|
248
|
+
const typesLine = blocksPromptSection({allowHtml})
|
|
249
|
+
.split("\n")
|
|
250
|
+
.find((line) => line.startsWith("Block types:"));
|
|
251
|
+
expect(typesLine).toMatch(/, gallery, list[.,]/);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("names the list block only in its own lines, never as a generic word", () => {
|
|
256
|
+
const section = blocksPromptSection({
|
|
257
|
+
checklistActions: ["toggleChecklist"],
|
|
258
|
+
stepperActions: ["scaleStepper"],
|
|
259
|
+
});
|
|
260
|
+
const mentions = section.split("\n").filter((line) => LIST_WORD.test(line));
|
|
261
|
+
expect(mentions.length).toBe(4);
|
|
262
|
+
expect(mentions[0]).toStartWith("Block types:");
|
|
263
|
+
expect(mentions[1]).toStartWith(LIST_LINE_START);
|
|
264
|
+
expect(mentions[2]).toStartWith("A list item");
|
|
265
|
+
expect(mentions[3]).toStartWith(COPY_LINE_START);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("sends item image srcs to the image rules and keeps the https rule tied to imageHosts", () => {
|
|
269
|
+
const lines = listLines();
|
|
270
|
+
expect(lines).toContain("image src rules");
|
|
271
|
+
expect(lines).toContain("plain text");
|
|
272
|
+
expect(lines).not.toContain("https");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("prints every list limit from BLOCK_LIMITS and the image block's alt limit", () => {
|
|
276
|
+
const lines = listLines();
|
|
277
|
+
const listLimits = Object.entries(BLOCK_LIMITS).filter(([key]) => key.startsWith("list"));
|
|
278
|
+
expect(listLimits.length).toBe(5);
|
|
279
|
+
for (const [, value] of listLimits) {
|
|
280
|
+
expect(lines).toContain(String(value));
|
|
281
|
+
}
|
|
282
|
+
expect(lines).toContain(`image alt ${BLOCK_LIMITS.headingTextMaxLength}`);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("the card eyebrow line", () => {
|
|
287
|
+
const eyebrowLines = (): string[] =>
|
|
288
|
+
linesAdded(blocksPromptSection(), blocksPromptSection({richBlocks: false})).filter((line) =>
|
|
289
|
+
line.includes("eyebrow")
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
it("is offered by default with its limit from BLOCK_LIMITS", () => {
|
|
293
|
+
const lines = eyebrowLines();
|
|
294
|
+
expect(lines.length).toBeGreaterThan(0);
|
|
295
|
+
expect(lines.join("\n")).toContain(`at most ${BLOCK_LIMITS.cardEyebrowMaxLength}`);
|
|
296
|
+
expect(BLOCK_LIMITS.cardEyebrowMaxLength).toBe(60);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("is absent when richBlocks is false", () => {
|
|
300
|
+
expect(blocksPromptSection({richBlocks: false})).not.toContain("eyebrow");
|
|
301
|
+
expect(blocksPromptSection({allowHtml: true, richBlocks: false})).not.toContain("eyebrow");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("the copy line", () => {
|
|
306
|
+
const copyLines = (options: Parameters<typeof blocksPromptSection>[0] = {}): string[] =>
|
|
307
|
+
linesAdded(
|
|
308
|
+
blocksPromptSection(options),
|
|
309
|
+
blocksPromptSection({...options, richBlocks: false})
|
|
310
|
+
).filter((line) => line.includes("copy"));
|
|
311
|
+
|
|
312
|
+
it("is one line, on by default, with the copy text limit from BLOCK_LIMITS", () => {
|
|
313
|
+
const lines = copyLines();
|
|
314
|
+
expect(lines.length).toBe(1);
|
|
315
|
+
expect(lines[0]).toStartWith(COPY_LINE_START);
|
|
316
|
+
expect(lines[0]).toContain("exactly one of text");
|
|
317
|
+
expect(lines[0]).toContain(`at most ${BLOCK_LIMITS.copyTextMaxLength} characters`);
|
|
318
|
+
expect(lines[0]).toContain("never reaches the host");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("names stepper as a copy target only when the stepper is offered", () => {
|
|
322
|
+
expect(copyLines()[0]).toContain("a checklist, list, table, or text block");
|
|
323
|
+
expect(copyLines()[0]).not.toContain("stepper");
|
|
324
|
+
expect(copyLines({stepperActions: ["scaleStepper"]})[0]).toContain(
|
|
325
|
+
"a stepper, checklist, list, table, or text block"
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("is absent when richBlocks is false", () => {
|
|
330
|
+
for (const allowHtml of [false, true]) {
|
|
331
|
+
expect(blocksPromptSection({allowHtml, richBlocks: false})).not.toContain("copy");
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("with richBlocks on", () => {
|
|
337
|
+
it("matches the full rich-blocks prompt with stepper and checklist actions", () => {
|
|
338
|
+
const section = blocksPromptSection(FULL_RICH_OPTIONS);
|
|
339
|
+
expect(section).toBe(blocksPromptSection({...FULL_RICH_OPTIONS, richBlocks: true}));
|
|
340
|
+
expect(section).toMatchSnapshot();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("matches the full rich-blocks prompt with html allowed", () => {
|
|
344
|
+
expect(blocksPromptSection({...FULL_RICH_OPTIONS, allowHtml: true})).toMatchSnapshot();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
24
347
|
});
|
package/src/prompt.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {BLOCK_LIMITS} from "./limits";
|
|
2
|
+
import {COPY_TARGET_TYPES} from "./schema";
|
|
2
3
|
|
|
3
4
|
const DATA_EXAMPLE = `v: 1
|
|
4
5
|
datasets:
|
|
@@ -34,19 +35,97 @@ blocks:
|
|
|
34
35
|
- type: text
|
|
35
36
|
markdown: I could not find any signups for that range. Try a wider date window.`;
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Stepper instructions, offered only when the host registers an action that handles steppers,
|
|
40
|
+
* so the model never writes a stepper whose buttons would do nothing.
|
|
41
|
+
*/
|
|
42
|
+
const stepperPromptLines = (stepperActions: readonly string[]): string[] => [
|
|
43
|
+
"stepper requires id, label, value, min, max, and callback (name and optional payload). It may set step (above 0, default 1), unit, itemsTitle, items, and note. min is below max, and value is between them.",
|
|
44
|
+
"Its decrease and increase buttons send the callback with payload.value set to the next value. The host returns the updated stepper, so write item amounts for the starting value.",
|
|
45
|
+
`A stepper callback name must be one of: ${stepperActions.join(", ")}.`,
|
|
46
|
+
"A stepper item requires label and amount. It may set unit, decimals, and round (nearest or up).",
|
|
47
|
+
`A stepper id is at most ${BLOCK_LIMITS.stepperIdMaxLength} characters. No other id may be <id>_decrease or <id>_increase.`,
|
|
48
|
+
`A stepper label and itemsTitle are at most ${BLOCK_LIMITS.stepperLabelMaxLength} characters, its unit ${BLOCK_LIMITS.stepperUnitMaxLength}, and its note ${BLOCK_LIMITS.stepperNoteMaxLength}.`,
|
|
49
|
+
`A stepper has at most ${BLOCK_LIMITS.stepperItemsMax} items. An item label is at most ${BLOCK_LIMITS.stepperLabelMaxLength} characters, its unit ${BLOCK_LIMITS.stepperItemUnitMaxLength}, and decimals is 0 to ${BLOCK_LIMITS.stepperDecimalsMax}.`,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checklist instructions, offered with rich blocks. A tick only reaches the host through a
|
|
54
|
+
* checklist action, so without one the model is told to leave callback out and ticks stay local.
|
|
55
|
+
*/
|
|
56
|
+
const checklistPromptLines = (checklistActions: readonly string[]): string[] => [
|
|
57
|
+
"checklist requires id and items, and may set title. It shows a done count such as 2 of 8.",
|
|
58
|
+
"A checklist item requires id and text. It may set meta (a short label such as a time, shown under the text), detail (a muted line under that), and checked (true or false).",
|
|
59
|
+
checklistActions.length === 0
|
|
60
|
+
? "Leave callback out of a checklist. Ticks stay on the device."
|
|
61
|
+
: `Set a checklist callback name to one of: ${checklistActions.join(", ")}. Its payload is optional. Each tick sends the callback with payload.itemId, payload.checked, and payload.state, and the host returns the updated checklist.`,
|
|
62
|
+
`A checklist id is at most ${BLOCK_LIMITS.checklistIdMaxLength} characters and an item id ${BLOCK_LIMITS.checklistItemIdMaxLength}. Item ids are unique in the checklist, and no other id may be <id>_<item id>.`,
|
|
63
|
+
`A checklist has ${BLOCK_LIMITS.checklistItemsMin} to ${BLOCK_LIMITS.checklistItemsMax} items. Its title is at most ${BLOCK_LIMITS.checklistTitleMaxLength} characters, an item text ${BLOCK_LIMITS.checklistItemTextMaxLength}, an item meta ${BLOCK_LIMITS.checklistItemMetaMaxLength}, and an item detail ${BLOCK_LIMITS.checklistItemDetailMaxLength}.`,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gallery instructions, offered with rich blocks. Tile srcs defer to the image src rules so the
|
|
68
|
+
* https guidance stays in the one line that knows the host's imageHosts.
|
|
69
|
+
*/
|
|
70
|
+
const galleryPromptLines = (): string[] => [
|
|
71
|
+
`gallery requires images: ${BLOCK_LIMITS.galleryImagesMin} to ${BLOCK_LIMITS.galleryImagesMax} photos shown together. Each image requires src and alt, and may set caption. Each src follows the image src rules above.`,
|
|
72
|
+
`A gallery image alt is at most ${BLOCK_LIMITS.galleryAltMaxLength} characters and its caption ${BLOCK_LIMITS.galleryCaptionMaxLength}.`,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/** Card eyebrow instruction, offered with rich blocks. */
|
|
76
|
+
const cardEyebrowPromptLines = (): string[] => [
|
|
77
|
+
`A card may set eyebrow, a short label shown small above its title. An eyebrow is at most ${BLOCK_LIMITS.cardEyebrowMaxLength} characters.`,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* List instructions, offered with rich blocks. The word "list" names only this block in the
|
|
82
|
+
* prompt, so the model never reads it as a generic instruction. Item image srcs defer to the
|
|
83
|
+
* image src rules, and the alt cap is the image block's.
|
|
84
|
+
*/
|
|
85
|
+
const listPromptLines = (): string[] => [
|
|
86
|
+
`list requires items: ${BLOCK_LIMITS.listItemsMin} to ${BLOCK_LIMITS.listItemsMax} entries stacked one under another. Each item requires title, and may set text (plain text, not markdown), meta (a short label such as a time or a price), and image (src and alt, a thumbnail). Each image src follows the image src rules above.`,
|
|
87
|
+
`A list item title is at most ${BLOCK_LIMITS.listItemTitleMaxLength} characters, its text ${BLOCK_LIMITS.listItemTextMaxLength}, its meta ${BLOCK_LIMITS.listItemMetaMaxLength}, and its image alt ${BLOCK_LIMITS.headingTextMaxLength}.`,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Copy action instruction, offered with rich blocks. The stepper is named as a target only when
|
|
92
|
+
* the prompt offers it, so the prompt never mentions a stepper without a stepper action.
|
|
93
|
+
*/
|
|
94
|
+
const copyPromptLine = (offersStepper: boolean): string => {
|
|
95
|
+
const targets = COPY_TARGET_TYPES.filter((type) => offersStepper || type !== "stepper");
|
|
96
|
+
const named = `${targets.slice(0, -1).join(", ")}, or ${targets.at(-1)}`;
|
|
97
|
+
return `action.kind may also be copy: it copies to the clipboard on the device and never reaches the host. A copy action sets exactly one of text (at most ${BLOCK_LIMITS.copyTextMaxLength} characters, copied as written) or target (the id of a ${named} block in this document, whose current contents are copied).`;
|
|
98
|
+
};
|
|
99
|
+
|
|
37
100
|
/**
|
|
38
101
|
* The system-prompt section a host prepends when a reply must be one block document.
|
|
39
102
|
* Every cap is read from `BLOCK_LIMITS` so the prompt, the schema, and the reference stay aligned.
|
|
103
|
+
* `richBlocks: false` keeps the prompt as it was before rich blocks, for clients that cannot
|
|
104
|
+
* render them yet. The stepper is offered only with a `stepperActions` entry, and the checklist
|
|
105
|
+
* names its callback only with a `checklistActions` entry.
|
|
40
106
|
*/
|
|
41
107
|
export const blocksPromptSection = ({
|
|
42
108
|
allowHtml = false,
|
|
109
|
+
checklistActions = [],
|
|
43
110
|
hostActions = [],
|
|
44
111
|
imageHosts = [],
|
|
112
|
+
richBlocks = true,
|
|
113
|
+
stepperActions = [],
|
|
45
114
|
}: {
|
|
46
115
|
allowHtml?: boolean;
|
|
116
|
+
/** Host actions a checklist callback may name. Empty or omitted means ticks stay local. */
|
|
117
|
+
checklistActions?: readonly string[];
|
|
47
118
|
hostActions?: readonly string[];
|
|
48
119
|
imageHosts?: readonly string[];
|
|
120
|
+
/** Default `true`. `false` leaves every rich block out of the prompt. */
|
|
121
|
+
richBlocks?: boolean;
|
|
122
|
+
/** Host actions that handle a stepper. Without one, the prompt never mentions stepper. */
|
|
123
|
+
stepperActions?: readonly string[];
|
|
49
124
|
} = {}): string => {
|
|
125
|
+
const offersStepper = richBlocks && stepperActions.length > 0;
|
|
126
|
+
const richTypes = richBlocks
|
|
127
|
+
? `${offersStepper ? ", stepper" : ""}, checklist, gallery, list`
|
|
128
|
+
: "";
|
|
50
129
|
const callbacks =
|
|
51
130
|
hostActions.length === 0
|
|
52
131
|
? "No callback names are registered. Do not emit kind callback."
|
|
@@ -60,8 +139,8 @@ export const blocksPromptSection = ({
|
|
|
60
139
|
"The document is YAML or JSON. Keys, when present, are in this order: v, datasets, blocks.",
|
|
61
140
|
"v is 1.",
|
|
62
141
|
allowHtml
|
|
63
|
-
?
|
|
64
|
-
:
|
|
142
|
+
? `Block types: heading, text, metric, badge, divider, context, chart, table, actions, columns, card, html, callout, image, details${richTypes}.`
|
|
143
|
+
: `Block types: heading, text, metric, badge, divider, context, chart, table, actions, columns, card, callout, image, details${richTypes}. Do not emit type html.`,
|
|
65
144
|
`An html field is at most ${BLOCK_LIMITS.htmlMaxBytes} bytes.`,
|
|
66
145
|
...(allowHtml
|
|
67
146
|
? [
|
|
@@ -72,11 +151,17 @@ export const blocksPromptSection = ({
|
|
|
72
151
|
"image requires alt and src. src is a data:image URL, a file: ref, or an https URL on an allowed host.",
|
|
73
152
|
imageHostsLine,
|
|
74
153
|
"details requires title and text.",
|
|
154
|
+
...(offersStepper ? stepperPromptLines(stepperActions) : []),
|
|
155
|
+
...(richBlocks ? checklistPromptLines(checklistActions) : []),
|
|
156
|
+
...(richBlocks ? galleryPromptLines() : []),
|
|
157
|
+
...(richBlocks ? listPromptLines() : []),
|
|
158
|
+
...(richBlocks ? cardEyebrowPromptLines() : []),
|
|
75
159
|
"heading requires text. text requires markdown. metric requires label and value. badge requires text. context requires text.",
|
|
76
160
|
"chart kind is line, bar, area, or donut. Bind it with data, x, and y, or with points of label and value.",
|
|
77
161
|
"table requires data, the name of a dataset. actions requires id and elements.",
|
|
78
162
|
"An element is a button (id, text, action) or a segmented control (id, target, options of label and data).",
|
|
79
163
|
"action.kind is reply (text), open (url or route, exactly one), select (target and data), or callback (name and optional payload).",
|
|
164
|
+
...(richBlocks ? [copyPromptLine(offersStepper)] : []),
|
|
80
165
|
"columns has 2 to 4 children. card has children and an optional title. Do not nest columns or card inside columns or card.",
|
|
81
166
|
`At most ${BLOCK_LIMITS.maxBlocks} blocks, including nested blocks.`,
|
|
82
167
|
`Depth is at most ${BLOCK_LIMITS.maxDepth}.`,
|