@xom11/whiteboard 0.24.1 → 0.25.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.
Files changed (85) hide show
  1. package/README.md +85 -12
  2. package/dist/ai.d.mts +472 -0
  3. package/dist/ai.d.ts +472 -0
  4. package/dist/ai.js +2156 -0
  5. package/dist/ai.js.map +1 -0
  6. package/dist/ai.mjs +1495 -0
  7. package/dist/ai.mjs.map +1 -0
  8. package/dist/catalog.json +4 -4
  9. package/dist/chunk-73Q7ADVL.mjs +35 -0
  10. package/dist/chunk-73Q7ADVL.mjs.map +1 -0
  11. package/dist/{chunk-BKSXPNPQ.mjs → chunk-AYSFWUPK.mjs} +4 -3
  12. package/dist/chunk-AYSFWUPK.mjs.map +1 -0
  13. package/dist/chunk-B4NJJZFR.mjs +18 -0
  14. package/dist/chunk-B4NJJZFR.mjs.map +1 -0
  15. package/dist/{chunk-LVNCYP4J.mjs → chunk-CJBLJUWG.mjs} +5 -5
  16. package/dist/{chunk-LVNCYP4J.mjs.map → chunk-CJBLJUWG.mjs.map} +1 -1
  17. package/dist/{chunk-YIPI3WUL.mjs → chunk-ESVPQWHX.mjs} +5 -5
  18. package/dist/{chunk-YIPI3WUL.mjs.map → chunk-ESVPQWHX.mjs.map} +1 -1
  19. package/dist/{chunk-IBTRMWD6.mjs → chunk-I24QOHPU.mjs} +3 -3
  20. package/dist/{chunk-IBTRMWD6.mjs.map → chunk-I24QOHPU.mjs.map} +1 -1
  21. package/dist/{chunk-ZBJBQKJ2.mjs → chunk-IHUFOV7L.mjs} +4 -19
  22. package/dist/chunk-IHUFOV7L.mjs.map +1 -0
  23. package/dist/{chunk-AZIARTGX.mjs → chunk-M42TGYT6.mjs} +3 -3
  24. package/dist/{chunk-AZIARTGX.mjs.map → chunk-M42TGYT6.mjs.map} +1 -1
  25. package/dist/{chunk-WWMQ2VHZ.mjs → chunk-NDEZJKNY.mjs} +4 -4
  26. package/dist/{chunk-WWMQ2VHZ.mjs.map → chunk-NDEZJKNY.mjs.map} +1 -1
  27. package/dist/{chunk-CSCF3YFZ.mjs → chunk-ONBCUWVI.mjs} +6 -4
  28. package/dist/chunk-ONBCUWVI.mjs.map +1 -0
  29. package/dist/{chunk-6V4SH4JJ.mjs → chunk-REIJZDVZ.mjs} +6 -35
  30. package/dist/chunk-REIJZDVZ.mjs.map +1 -0
  31. package/dist/{chunk-4D5CSIJO.mjs → chunk-TB4CL25L.mjs} +10 -8
  32. package/dist/chunk-TB4CL25L.mjs.map +1 -0
  33. package/dist/chunk-VNCCIV6O.mjs +938 -0
  34. package/dist/chunk-VNCCIV6O.mjs.map +1 -0
  35. package/dist/{chunk-MFOGFFIL.mjs → chunk-VRHWDZ66.mjs} +6 -5
  36. package/dist/chunk-VRHWDZ66.mjs.map +1 -0
  37. package/dist/{chunk-CRAPWQKJ.mjs → chunk-YSJOVBCD.mjs} +4 -4
  38. package/dist/{chunk-CRAPWQKJ.mjs.map → chunk-YSJOVBCD.mjs.map} +1 -1
  39. package/dist/geometry-2d.d.mts +2 -1
  40. package/dist/geometry-2d.d.ts +2 -1
  41. package/dist/geometry-2d.js +1383 -23
  42. package/dist/geometry-2d.js.map +1 -1
  43. package/dist/geometry-2d.mjs +7 -5
  44. package/dist/geometry-3d.d.mts +2 -1
  45. package/dist/geometry-3d.d.ts +2 -1
  46. package/dist/geometry-3d.js +2 -2
  47. package/dist/geometry-3d.js.map +1 -1
  48. package/dist/geometry-3d.mjs +6 -4
  49. package/dist/graph-2d.d.mts +2 -1
  50. package/dist/graph-2d.d.ts +2 -1
  51. package/dist/graph-2d.js +2 -2
  52. package/dist/graph-2d.js.map +1 -1
  53. package/dist/graph-2d.mjs +9 -7
  54. package/dist/{host-TLIXN4CF.mjs → host-A64ITWVX.mjs} +8 -6
  55. package/dist/host-A64ITWVX.mjs.map +1 -0
  56. package/dist/{host-DOAYVL35.mjs → host-L7FMFZUW.mjs} +228 -30
  57. package/dist/host-L7FMFZUW.mjs.map +1 -0
  58. package/dist/{host-GKNQBBUE.mjs → host-QK53UYMD.mjs} +12 -10
  59. package/dist/host-QK53UYMD.mjs.map +1 -0
  60. package/dist/index.d.mts +4 -616
  61. package/dist/index.d.ts +4 -616
  62. package/dist/index.js +8889 -8529
  63. package/dist/index.js.map +1 -1
  64. package/dist/index.mjs +21 -1012
  65. package/dist/index.mjs.map +1 -1
  66. package/dist/latex.d.mts +2 -1
  67. package/dist/latex.d.ts +2 -1
  68. package/dist/render-3WTY7NZB.mjs +9 -0
  69. package/dist/{render-SA4JTOW3.mjs.map → render-3WTY7NZB.mjs.map} +1 -1
  70. package/dist/serialize-SRJVKYUG.mjs +8 -0
  71. package/dist/{serialize-3NZS6A6Q.mjs.map → serialize-SRJVKYUG.mjs.map} +1 -1
  72. package/dist/{types-rA4slL08.d.ts → types-DWRyCa2m.d.mts} +139 -1
  73. package/dist/{types-rA4slL08.d.mts → types-DWRyCa2m.d.ts} +139 -1
  74. package/package.json +6 -1
  75. package/dist/chunk-4D5CSIJO.mjs.map +0 -1
  76. package/dist/chunk-6V4SH4JJ.mjs.map +0 -1
  77. package/dist/chunk-BKSXPNPQ.mjs.map +0 -1
  78. package/dist/chunk-CSCF3YFZ.mjs.map +0 -1
  79. package/dist/chunk-MFOGFFIL.mjs.map +0 -1
  80. package/dist/chunk-ZBJBQKJ2.mjs.map +0 -1
  81. package/dist/host-DOAYVL35.mjs.map +0 -1
  82. package/dist/host-GKNQBBUE.mjs.map +0 -1
  83. package/dist/host-TLIXN4CF.mjs.map +0 -1
  84. package/dist/render-SA4JTOW3.mjs +0 -8
  85. package/dist/serialize-3NZS6A6Q.mjs +0 -6
package/dist/ai.mjs ADDED
@@ -0,0 +1,1495 @@
1
+ import { KIND_REGISTRY, POINT_KINDS, LINE_LIKE_SHAPE_KINDS, CIRCLE_KINDS } from './chunk-VNCCIV6O.mjs';
2
+ import { createEmptyState } from './chunk-73Q7ADVL.mjs';
3
+ import './chunk-B4NJJZFR.mjs';
4
+ import { z } from 'zod';
5
+ import { zodToJsonSchema } from 'zod-to-json-schema';
6
+ import Anthropic from '@anthropic-ai/sdk';
7
+
8
+ function asTuple(arr) {
9
+ if (arr.length < 2) throw new Error("schema: need at least 2 variants for discriminatedUnion");
10
+ return arr;
11
+ }
12
+ var pointSchemas = Array.from(KIND_REGISTRY.values()).filter((m) => POINT_KINDS.has(m.kind)).map((m) => m.schema);
13
+ var shapeSchemas = Array.from(KIND_REGISTRY.values()).filter((m) => !POINT_KINDS.has(m.kind)).map((m) => m.schema);
14
+ var DslPoint = z.discriminatedUnion("kind", asTuple(pointSchemas));
15
+ var DslShape = z.discriminatedUnion("kind", asTuple(shapeSchemas));
16
+ var DslInput = z.object({
17
+ version: z.literal(1),
18
+ points: z.array(DslPoint),
19
+ shapes: z.array(DslShape).default([])
20
+ });
21
+
22
+ // src/stamps/geometry-2d/dsl/transpile/errors.ts
23
+ function mkError(code, message, opts) {
24
+ return { code, message, path: opts?.path, hint: opts?.hint };
25
+ }
26
+
27
+ // src/stamps/geometry-2d/dsl/transpile/symbols.ts
28
+ function buildSymbols(dsl) {
29
+ const symbols = /* @__PURE__ */ new Map();
30
+ const errors = [];
31
+ let counter = 0;
32
+ for (const p of dsl.points) {
33
+ if (symbols.has(p.name)) {
34
+ errors.push(mkError("DUPLICATE_NAME", `T\xEAn tr\xF9ng: "${p.name}"`, { path: [p.name] }));
35
+ continue;
36
+ }
37
+ symbols.set(p.name, { name: p.name, role: "point", entity: p, index: counter++ });
38
+ }
39
+ for (const s of dsl.shapes) {
40
+ if (symbols.has(s.name)) {
41
+ errors.push(mkError("DUPLICATE_NAME", `T\xEAn tr\xF9ng: "${s.name}"`, { path: [s.name] }));
42
+ continue;
43
+ }
44
+ symbols.set(s.name, { name: s.name, role: "shape", entity: s, index: counter++ });
45
+ }
46
+ return { symbols, errors };
47
+ }
48
+
49
+ // src/stamps/geometry-2d/dsl/transpile/refs.ts
50
+ function isPointLike(sym) {
51
+ return !!sym && sym.role === "point";
52
+ }
53
+ function isLineLike(sym) {
54
+ if (!sym || sym.role !== "shape") return false;
55
+ return LINE_LIKE_SHAPE_KINDS.has(sym.entity.kind);
56
+ }
57
+ function isCircleLike(sym) {
58
+ if (!sym || sym.role !== "shape") return false;
59
+ return CIRCLE_KINDS.has(sym.entity.kind);
60
+ }
61
+ function isSegmentExact(sym) {
62
+ return !!sym && sym.role === "shape" && sym.entity.kind === "segment";
63
+ }
64
+ function validateRefs(dsl, symbols) {
65
+ const errors = [];
66
+ const check = (owner, field, refName, predicate, expected) => {
67
+ const sym = symbols.get(refName);
68
+ if (!sym) {
69
+ errors.push(mkError(
70
+ "UNKNOWN_REF",
71
+ `${owner}.${field} tham chi\u1EBFu "${refName}" kh\xF4ng t\u1ED3n t\u1EA1i`,
72
+ { path: [owner, field] }
73
+ ));
74
+ return;
75
+ }
76
+ if (!predicate(sym)) {
77
+ errors.push(mkError(
78
+ "KIND_MISMATCH",
79
+ `${owner}.${field}="${refName}" sai ki\u1EC3u (c\u1EA7n ${expected}, g\u1EB7p ${sym.role === "point" ? "point" : sym.entity.kind})`,
80
+ { path: [owner, field] }
81
+ ));
82
+ }
83
+ };
84
+ for (const p of dsl.points) {
85
+ switch (p.kind) {
86
+ case "free":
87
+ break;
88
+ case "midpoint":
89
+ check(p.name, "p1", p.p1, isPointLike, "point");
90
+ check(p.name, "p2", p.p2, isPointLike, "point");
91
+ break;
92
+ case "onSegment":
93
+ check(p.name, "segmentId", p.segmentId, isSegmentExact, "segment");
94
+ break;
95
+ case "onLine":
96
+ check(p.name, "lineId", p.lineId, isLineLike, "line-like");
97
+ break;
98
+ case "onCircle":
99
+ check(p.name, "circleId", p.circleId, isCircleLike, "circle");
100
+ break;
101
+ case "perpFoot":
102
+ check(p.name, "from", p.from, isPointLike, "point");
103
+ check(p.name, "onLine", p.onLine, isLineLike, "line-like");
104
+ break;
105
+ case "circumcenter":
106
+ case "incenter":
107
+ case "centroid":
108
+ case "orthocenter":
109
+ for (let i = 0; i < 3; i++) {
110
+ check(p.name, `vertices[${i}]`, p.vertices[i], isPointLike, "point");
111
+ }
112
+ break;
113
+ case "intersection": {
114
+ const refPredicate = (s) => isLineLike(s) || isCircleLike(s);
115
+ check(p.name, "ref1", p.ref1, refPredicate, "line-like ho\u1EB7c circle");
116
+ check(p.name, "ref2", p.ref2, refPredicate, "line-like ho\u1EB7c circle");
117
+ break;
118
+ }
119
+ }
120
+ }
121
+ for (const s of dsl.shapes) {
122
+ switch (s.kind) {
123
+ case "segment":
124
+ case "line":
125
+ check(s.name, "p1", s.p1, isPointLike, "point");
126
+ check(s.name, "p2", s.p2, isPointLike, "point");
127
+ break;
128
+ case "ray":
129
+ check(s.name, "origin", s.origin, isPointLike, "point");
130
+ check(s.name, "through", s.through, isPointLike, "point");
131
+ break;
132
+ case "polygon":
133
+ s.vertices.forEach((v, i) => check(s.name, `vertices[${i}]`, v, isPointLike, "point"));
134
+ break;
135
+ case "perpendicular":
136
+ case "parallel":
137
+ check(s.name, "throughPoint", s.throughPoint, isPointLike, "point");
138
+ check(s.name, "toLine", s.toLine, isLineLike, "line-like");
139
+ break;
140
+ case "perpBisector":
141
+ check(s.name, "p1", s.p1, isPointLike, "point");
142
+ check(s.name, "p2", s.p2, isPointLike, "point");
143
+ break;
144
+ case "angleBisector":
145
+ check(s.name, "p1", s.p1, isPointLike, "point");
146
+ check(s.name, "vertex", s.vertex, isPointLike, "point");
147
+ check(s.name, "p2", s.p2, isPointLike, "point");
148
+ break;
149
+ case "tangent":
150
+ check(s.name, "throughPoint", s.throughPoint, isPointLike, "point");
151
+ check(s.name, "toCircle", s.toCircle, isCircleLike, "circle");
152
+ break;
153
+ case "circleCP":
154
+ check(s.name, "center", s.center, isPointLike, "point");
155
+ check(s.name, "surfacePoint", s.surfacePoint, isPointLike, "point");
156
+ break;
157
+ case "circle3":
158
+ check(s.name, "p1", s.p1, isPointLike, "point");
159
+ check(s.name, "p2", s.p2, isPointLike, "point");
160
+ check(s.name, "p3", s.p3, isPointLike, "point");
161
+ break;
162
+ }
163
+ }
164
+ return { errors };
165
+ }
166
+ function collectRefs(entity) {
167
+ const mod = KIND_REGISTRY.get(entity.kind);
168
+ if (!mod) throw new Error(`collectRefs: no registry entry for kind "${entity.kind}"`);
169
+ return mod.collectRefs(entity);
170
+ }
171
+
172
+ // src/stamps/geometry-2d/dsl/transpile/cycles.ts
173
+ function detectCycles(symbols) {
174
+ const color = /* @__PURE__ */ new Map();
175
+ const parent = /* @__PURE__ */ new Map();
176
+ const errors = [];
177
+ const reportedCycles = /* @__PURE__ */ new Set();
178
+ for (const name of symbols.keys()) color.set(name, "white");
179
+ function reportCycle(start, hit) {
180
+ const chain = [start];
181
+ let cur = parent.get(start);
182
+ while (cur && cur !== hit && chain.length < symbols.size + 2) {
183
+ chain.push(cur);
184
+ cur = parent.get(cur);
185
+ }
186
+ chain.push(hit);
187
+ const minIdx = chain.indexOf(chain.reduce((a, b) => a < b ? a : b));
188
+ const rotated = [...chain.slice(minIdx), ...chain.slice(0, minIdx)];
189
+ const key = rotated.join("\u2192");
190
+ if (reportedCycles.has(key)) return;
191
+ reportedCycles.add(key);
192
+ errors.push(mkError(
193
+ "CYCLE",
194
+ `Ph\u1EE5 thu\u1ED9c v\xF2ng: ${chain.reverse().join(" \u2192 ")}`,
195
+ { path: [...chain], hint: "Ki\u1EC3m tra l\u1EA1i quan h\u1EC7 midpoint/perpFoot/intersection." }
196
+ ));
197
+ }
198
+ function dfs(name) {
199
+ color.set(name, "gray");
200
+ const sym = symbols.get(name);
201
+ if (sym) {
202
+ for (const ref of collectRefs(sym.entity)) {
203
+ if (!symbols.has(ref)) continue;
204
+ const c = color.get(ref);
205
+ if (c === "gray") {
206
+ reportCycle(name, ref);
207
+ continue;
208
+ }
209
+ if (c === "white") {
210
+ parent.set(ref, name);
211
+ dfs(ref);
212
+ }
213
+ }
214
+ }
215
+ color.set(name, "black");
216
+ }
217
+ for (const name of symbols.keys()) {
218
+ if (color.get(name) === "white") {
219
+ parent.set(name, null);
220
+ dfs(name);
221
+ }
222
+ }
223
+ return { errors };
224
+ }
225
+
226
+ // src/stamps/geometry-2d/dsl/transpile/ids.ts
227
+ function assignIds(symbols) {
228
+ const counters = /* @__PURE__ */ new Map();
229
+ const ids = /* @__PURE__ */ new Map();
230
+ for (const [name, sym] of symbols.entries()) {
231
+ const mod = KIND_REGISTRY.get(sym.entity.kind);
232
+ if (!mod) throw new Error(`assignIds: no registry entry for kind "${sym.entity.kind}"`);
233
+ const prefix = mod.prefix;
234
+ counters.set(prefix, (counters.get(prefix) ?? 0) + 1);
235
+ ids.set(name, `${prefix}${counters.get(prefix)}`);
236
+ }
237
+ return ids;
238
+ }
239
+
240
+ // src/stamps/geometry-2d/dsl/transpile.ts
241
+ function hintOf(entity) {
242
+ const mod = KIND_REGISTRY.get(entity.kind);
243
+ if (!mod) throw new Error(`hintOf: no registry entry for kind "${entity.kind}"`);
244
+ return mod.role === "polygon" ? "point" : mod.role;
245
+ }
246
+ function buildEmitContext(ids, kindHints) {
247
+ const auxCounters = /* @__PURE__ */ new Map();
248
+ return {
249
+ resolveId(name) {
250
+ const id = ids.get(name);
251
+ if (!id) throw new Error(`emit: id not assigned for "${name}"`);
252
+ return id;
253
+ },
254
+ hintOf(name) {
255
+ const hint = kindHints.get(name);
256
+ if (!hint) throw new Error(`emit: hint not assigned for "${name}"`);
257
+ return hint;
258
+ },
259
+ mintAuxId(parentName, suffix) {
260
+ const key = `${parentName}.${suffix}`;
261
+ auxCounters.set(key, (auxCounters.get(key) ?? 0) + 1);
262
+ const seq = auxCounters.get(key);
263
+ return `aux_${parentName}_${suffix}${seq}`;
264
+ }
265
+ };
266
+ }
267
+ function transpile(dslRaw) {
268
+ const parsed = DslInput.safeParse(dslRaw);
269
+ if (!parsed.success) {
270
+ const errors = parsed.error.issues.map(
271
+ (iss) => mkError("SCHEMA", iss.message, { path: iss.path.map(String) })
272
+ );
273
+ return { ok: false, errors };
274
+ }
275
+ const dsl = parsed.data;
276
+ const { symbols, errors: dupErrors } = buildSymbols(dsl);
277
+ const { errors: refErrors } = validateRefs(dsl, symbols);
278
+ const { errors: cycleErrors } = detectCycles(symbols);
279
+ const allErrors = [...dupErrors, ...refErrors, ...cycleErrors];
280
+ if (allErrors.length > 0) return { ok: false, errors: allErrors };
281
+ const ids = assignIds(symbols);
282
+ const kindHints = /* @__PURE__ */ new Map();
283
+ for (const [name, sym] of symbols.entries()) {
284
+ kindHints.set(name, hintOf(sym.entity));
285
+ }
286
+ const objects = {};
287
+ const order = [];
288
+ const ctx = buildEmitContext(ids, kindHints);
289
+ const emitEntity = (entity) => {
290
+ const mod = KIND_REGISTRY.get(entity.kind);
291
+ if (!mod) throw new Error(`emit: no registry entry for kind "${entity.kind}"`);
292
+ const emitted = mod.emit(entity, ctx);
293
+ for (const ent of emitted) {
294
+ objects[ent.object.id] = ent.object;
295
+ order.push(ent.object.id);
296
+ }
297
+ };
298
+ for (const p of dsl.points) emitEntity(p);
299
+ for (const s of dsl.shapes) emitEntity(s);
300
+ const empty = createEmptyState("2d");
301
+ const state = {
302
+ objects,
303
+ order,
304
+ counter: order.length,
305
+ meta: empty.meta
306
+ };
307
+ return { ok: true, state };
308
+ }
309
+ var FigureEnvelopeZ = z.object({
310
+ decision: z.enum(["build", "refuse"]),
311
+ // figure: DslInput khi build; bỏ qua khi refuse.
312
+ figure: DslInput.optional(),
313
+ // reason: lý do từ chối (Việt) khi refuse; bỏ qua khi build.
314
+ reason: z.string().optional()
315
+ }).refine(
316
+ (e) => e.decision === "build" ? e.figure != null : e.reason != null && e.reason.length > 0,
317
+ {
318
+ message: "decision=build c\u1EA7n `figure`; decision=refuse c\u1EA7n `reason` kh\xF4ng r\u1ED7ng"
319
+ }
320
+ );
321
+ function envelopeJsonSchema() {
322
+ return zodToJsonSchema(FigureEnvelopeZ, {
323
+ target: "jsonSchema7",
324
+ $refStrategy: "none"
325
+ });
326
+ }
327
+ function envelopeBuildDsl(env) {
328
+ if (env.decision !== "build" || env.figure == null) {
329
+ throw new Error("envelopeBuildDsl: envelope kh\xF4ng ph\u1EA3i decision=build ho\u1EB7c thi\u1EBFu figure");
330
+ }
331
+ return env.figure;
332
+ }
333
+
334
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-equilateral.ts
335
+ var fixture = {
336
+ problem: "Cho tam gi\xE1c \u0111\u1EC1u ABC c\u1EA1nh 4.",
337
+ dsl: {
338
+ version: 1,
339
+ points: [
340
+ { name: "A", kind: "free", x: 0, y: 0 },
341
+ { name: "B", kind: "free", x: 4, y: 0 },
342
+ { name: "C", kind: "free", x: 2, y: 3.464 }
343
+ ],
344
+ shapes: [
345
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }
346
+ ]
347
+ }
348
+ };
349
+
350
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-median.ts
351
+ var fixture2 = {
352
+ problem: "Tam gi\xE1c ABC, M l\xE0 trung \u0111i\u1EC3m BC. V\u1EBD AM.",
353
+ dsl: {
354
+ version: 1,
355
+ points: [
356
+ { name: "A", kind: "free", x: 0, y: 3 },
357
+ { name: "B", kind: "free", x: -2, y: 0 },
358
+ { name: "C", kind: "free", x: 3, y: 0 },
359
+ { name: "M", kind: "midpoint", p1: "B", p2: "C" }
360
+ ],
361
+ shapes: [
362
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
363
+ { name: "AM", kind: "segment", p1: "A", p2: "M" }
364
+ ]
365
+ }
366
+ };
367
+
368
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-altitude.ts
369
+ var fixture3 = {
370
+ problem: "Tam gi\xE1c ABC, AH l\xE0 \u0111\u01B0\u1EDDng cao xu\u1ED1ng BC.",
371
+ dsl: {
372
+ version: 1,
373
+ points: [
374
+ { name: "A", kind: "free", x: 1, y: 3 },
375
+ { name: "B", kind: "free", x: -2, y: 0 },
376
+ { name: "C", kind: "free", x: 3, y: 0 },
377
+ { name: "H", kind: "perpFoot", from: "A", onLine: "BC" }
378
+ ],
379
+ shapes: [
380
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
381
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
382
+ { name: "AH", kind: "segment", p1: "A", p2: "H" }
383
+ ]
384
+ }
385
+ };
386
+
387
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-centroid.ts
388
+ var fixture4 = {
389
+ problem: "Tam gi\xE1c ABC, G l\xE0 tr\u1ECDng t\xE2m.",
390
+ dsl: {
391
+ version: 1,
392
+ points: [
393
+ { name: "A", kind: "free", x: 0, y: 3 },
394
+ { name: "B", kind: "free", x: -2, y: 0 },
395
+ { name: "C", kind: "free", x: 3, y: 0 },
396
+ { name: "G", kind: "centroid", vertices: ["A", "B", "C"] }
397
+ ],
398
+ shapes: [
399
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }
400
+ ]
401
+ }
402
+ };
403
+
404
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-orthocenter.ts
405
+ var fixture5 = {
406
+ problem: "Tam gi\xE1c ABC, H l\xE0 tr\u1EF1c t\xE2m.",
407
+ dsl: {
408
+ version: 1,
409
+ points: [
410
+ { name: "A", kind: "free", x: 0, y: 3 },
411
+ { name: "B", kind: "free", x: -2, y: 0 },
412
+ { name: "C", kind: "free", x: 3, y: 0 },
413
+ { name: "H", kind: "orthocenter", vertices: ["A", "B", "C"] }
414
+ ],
415
+ shapes: [
416
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }
417
+ ]
418
+ }
419
+ };
420
+
421
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-circumcircle.ts
422
+ var fixture6 = {
423
+ problem: "Tam gi\xE1c ABC n\u1ED9i ti\u1EBFp \u0111\u01B0\u1EDDng tr\xF2n t\xE2m O.",
424
+ dsl: {
425
+ version: 1,
426
+ points: [
427
+ { name: "A", kind: "free", x: 0, y: 3 },
428
+ { name: "B", kind: "free", x: -2, y: 0 },
429
+ { name: "C", kind: "free", x: 3, y: 0 },
430
+ { name: "O", kind: "circumcenter", vertices: ["A", "B", "C"] }
431
+ ],
432
+ shapes: [
433
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
434
+ { name: "k", kind: "circle3", p1: "A", p2: "B", p3: "C" }
435
+ ]
436
+ }
437
+ };
438
+
439
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-incircle.ts
440
+ var fixture7 = {
441
+ problem: "Tam gi\xE1c ABC, I l\xE0 t\xE2m n\u1ED9i ti\u1EBFp, \u0111\u01B0\u1EDDng tr\xF2n (I) ti\u1EBFp x\xFAc BC t\u1EA1i D.",
442
+ dsl: {
443
+ version: 1,
444
+ points: [
445
+ { name: "A", kind: "free", x: 0, y: 3 },
446
+ { name: "B", kind: "free", x: -2, y: 0 },
447
+ { name: "C", kind: "free", x: 3, y: 0 },
448
+ { name: "I", kind: "incenter", vertices: ["A", "B", "C"] },
449
+ { name: "D", kind: "perpFoot", from: "I", onLine: "BC" }
450
+ ],
451
+ shapes: [
452
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
453
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
454
+ { name: "incircle", kind: "circleCP", center: "I", surfacePoint: "D" }
455
+ ]
456
+ }
457
+ };
458
+
459
+ // src/stamps/geometry-2d/dsl/fixtures/parallelogram.ts
460
+ var fixture8 = {
461
+ problem: "H\xECnh b\xECnh h\xE0nh ABCD, hai \u0111\u01B0\u1EDDng ch\xE9o AC, BD c\u1EAFt nhau t\u1EA1i O.",
462
+ dsl: {
463
+ version: 1,
464
+ points: [
465
+ { name: "A", kind: "free", x: 0, y: 0 },
466
+ { name: "B", kind: "free", x: 4, y: 0 },
467
+ { name: "C", kind: "free", x: 5, y: 2 },
468
+ { name: "D", kind: "free", x: 1, y: 2 },
469
+ { name: "O", kind: "intersection", ref1: "AC", ref2: "BD" }
470
+ ],
471
+ shapes: [
472
+ { name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] },
473
+ { name: "AC", kind: "segment", p1: "A", p2: "C" },
474
+ { name: "BD", kind: "segment", p1: "B", p2: "D" }
475
+ ]
476
+ }
477
+ };
478
+
479
+ // src/stamps/geometry-2d/dsl/fixtures/two-circles-intersect.ts
480
+ var fixture9 = {
481
+ problem: "Hai \u0111\u01B0\u1EDDng tr\xF2n (O\u2081), (O\u2082) c\u1EAFt nhau t\u1EA1i P, Q.",
482
+ dsl: {
483
+ version: 1,
484
+ points: [
485
+ { name: "O1", kind: "free", x: 0, y: 0 },
486
+ { name: "A1", kind: "free", x: 2, y: 0 },
487
+ { name: "O2", kind: "free", x: 3, y: 0 },
488
+ { name: "A2", kind: "free", x: 5, y: 0 },
489
+ { name: "P", kind: "intersection", ref1: "k1", ref2: "k2", branch: 0 },
490
+ { name: "Q", kind: "intersection", ref1: "k1", ref2: "k2", branch: 1 }
491
+ ],
492
+ shapes: [
493
+ { name: "k1", kind: "circleCP", center: "O1", surfacePoint: "A1" },
494
+ { name: "k2", kind: "circleCP", center: "O2", surfacePoint: "A2" }
495
+ ]
496
+ }
497
+ };
498
+
499
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-angle-bisector.ts
500
+ var fixture10 = {
501
+ problem: "Tam gi\xE1c ABC, AD l\xE0 ph\xE2n gi\xE1c g\xF3c A (D thu\u1ED9c BC).",
502
+ dsl: {
503
+ version: 1,
504
+ points: [
505
+ { name: "A", kind: "free", x: 0, y: 3 },
506
+ { name: "B", kind: "free", x: -2, y: 0 },
507
+ { name: "C", kind: "free", x: 3, y: 0 },
508
+ // D = giao của đường phân giác góc A với BC. KHÔNG dùng segment AD
509
+ // vì AD lại tham chiếu D → cycle.
510
+ { name: "D", kind: "intersection", ref1: "bisA", ref2: "BC" }
511
+ ],
512
+ shapes: [
513
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
514
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
515
+ // Đường phân giác (line construction), chứ không phải segment.
516
+ { name: "bisA", kind: "angleBisector", p1: "B", vertex: "A", p2: "C" },
517
+ // Sau khi đã có D, mới dựng được segment AD.
518
+ { name: "AD", kind: "segment", p1: "A", p2: "D" }
519
+ ]
520
+ }
521
+ };
522
+
523
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-median-altitude.ts
524
+ var fixture11 = {
525
+ problem: "Tam gi\xE1c ABC, M l\xE0 trung \u0111i\u1EC3m BC v\xE0 AH l\xE0 \u0111\u01B0\u1EDDng cao xu\u1ED1ng BC.",
526
+ dsl: {
527
+ version: 1,
528
+ points: [
529
+ { name: "A", kind: "free", x: 1, y: 3 },
530
+ { name: "B", kind: "free", x: -2, y: 0 },
531
+ { name: "C", kind: "free", x: 3, y: 0 },
532
+ // M = trung điểm B và C. KHÔNG dùng perpFoot ở đây — đó là H.
533
+ { name: "M", kind: "midpoint", p1: "B", p2: "C" },
534
+ // H = chân đường vuông góc từ A xuống cạnh BC.
535
+ { name: "H", kind: "perpFoot", from: "A", onLine: "BC" }
536
+ ],
537
+ shapes: [
538
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
539
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
540
+ { name: "AM", kind: "segment", p1: "A", p2: "M" },
541
+ { name: "AH", kind: "segment", p1: "A", p2: "H" }
542
+ ]
543
+ }
544
+ };
545
+
546
+ // src/stamps/geometry-2d/dsl/fixtures/trapezoid.ts
547
+ var fixture12 = {
548
+ problem: "H\xECnh thang ABCD c\xF3 AB song song CD.",
549
+ dsl: {
550
+ version: 1,
551
+ points: [
552
+ { name: "A", kind: "free", x: 0, y: 0 },
553
+ { name: "B", kind: "free", x: 4, y: 0 },
554
+ { name: "C", kind: "free", x: 5, y: 3 },
555
+ { name: "D", kind: "free", x: -1, y: 3 }
556
+ ],
557
+ shapes: [
558
+ { name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] }
559
+ ]
560
+ }
561
+ };
562
+
563
+ // src/stamps/geometry-2d/dsl/fixtures/rhombus.ts
564
+ var fixture13 = {
565
+ problem: "H\xECnh thoi ABCD, hai \u0111\u01B0\u1EDDng ch\xE9o AC, BD c\u1EAFt nhau t\u1EA1i O.",
566
+ dsl: {
567
+ version: 1,
568
+ points: [
569
+ { name: "A", kind: "free", x: 0, y: 2 },
570
+ { name: "B", kind: "free", x: 3, y: 0 },
571
+ { name: "C", kind: "free", x: 0, y: -2 },
572
+ { name: "D", kind: "free", x: -3, y: 0 },
573
+ { name: "O", kind: "intersection", ref1: "AC", ref2: "BD" }
574
+ ],
575
+ shapes: [
576
+ { name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] },
577
+ { name: "AC", kind: "segment", p1: "A", p2: "C" },
578
+ { name: "BD", kind: "segment", p1: "B", p2: "D" }
579
+ ]
580
+ }
581
+ };
582
+
583
+ // src/stamps/geometry-2d/dsl/fixtures/right-triangle-altitude.ts
584
+ var fixture14 = {
585
+ problem: "Tam gi\xE1c ABC vu\xF4ng t\u1EA1i A, AH l\xE0 \u0111\u01B0\u1EDDng cao xu\u1ED1ng c\u1EA1nh huy\u1EC1n BC.",
586
+ dsl: {
587
+ version: 1,
588
+ points: [
589
+ { name: "A", kind: "free", x: 0, y: 0 },
590
+ { name: "B", kind: "free", x: 0, y: 3 },
591
+ { name: "C", kind: "free", x: 4, y: 0 },
592
+ { name: "H", kind: "perpFoot", from: "A", onLine: "BC" }
593
+ ],
594
+ shapes: [
595
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
596
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
597
+ { name: "AH", kind: "segment", p1: "A", p2: "H" }
598
+ ]
599
+ }
600
+ };
601
+
602
+ // src/stamps/geometry-2d/dsl/fixtures/tangent-from-point.ts
603
+ var fixture15 = {
604
+ problem: "T\u1EEB \u0111i\u1EC3m M ngo\xE0i \u0111\u01B0\u1EDDng tr\xF2n (O), k\u1EBB hai ti\u1EBFp tuy\u1EBFn t\u1EDBi (O).",
605
+ dsl: {
606
+ version: 1,
607
+ points: [
608
+ { name: "O", kind: "free", x: 0, y: 0 },
609
+ // P là điểm trên đường tròn dùng để định bán kính.
610
+ { name: "P", kind: "free", x: 2, y: 0 },
611
+ { name: "M", kind: "free", x: 5, y: 0 }
612
+ ],
613
+ shapes: [
614
+ { name: "k", kind: "circleCP", center: "O", surfacePoint: "P" },
615
+ { name: "t1", kind: "tangent", throughPoint: "M", toCircle: "k", branch: 0 },
616
+ { name: "t2", kind: "tangent", throughPoint: "M", toCircle: "k", branch: 1 }
617
+ ]
618
+ }
619
+ };
620
+
621
+ // src/stamps/geometry-2d/dsl/fixtures/internal-external-bisector.ts
622
+ var fixture16 = {
623
+ problem: "Tam gi\xE1c ABC, v\u1EBD tia ph\xE2n gi\xE1c trong v\xE0 ph\xE2n gi\xE1c ngo\xE0i t\u1EA1i \u0111\u1EC9nh A.",
624
+ dsl: {
625
+ version: 1,
626
+ points: [
627
+ { name: "A", kind: "free", x: 0, y: 3 },
628
+ { name: "B", kind: "free", x: -2, y: 0 },
629
+ { name: "C", kind: "free", x: 3, y: 0 }
630
+ ],
631
+ shapes: [
632
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
633
+ { name: "bisIn", kind: "angleBisector", p1: "B", vertex: "A", p2: "C" },
634
+ // Phân giác ngoài = vuông góc với phân giác trong tại đỉnh A.
635
+ { name: "bisExt", kind: "perpendicular", throughPoint: "A", toLine: "bisIn" }
636
+ ]
637
+ }
638
+ };
639
+
640
+ // src/stamps/geometry-2d/ai/prompt.ts
641
+ var FIXTURES = [
642
+ fixture,
643
+ fixture2,
644
+ fixture3,
645
+ fixture4,
646
+ fixture5,
647
+ fixture6,
648
+ fixture7,
649
+ fixture10,
650
+ fixture11,
651
+ fixture14,
652
+ fixture16,
653
+ fixture15,
654
+ fixture8,
655
+ fixture13,
656
+ fixture12,
657
+ fixture9
658
+ ];
659
+ function buildSystemPrompt() {
660
+ const examples = FIXTURES.map(
661
+ (f, i) => `### V\xED d\u1EE5 ${i + 1}
662
+ **\u0110\u1EC1:** ${f.problem}
663
+ **Output:**
664
+ ${JSON.stringify({ decision: "build", figure: f.dsl }, null, 2)}`
665
+ ).join("\n\n");
666
+ return `B\u1EA1n l\xE0 tr\u1EE3 l\xFD v\u1EBD h\xECnh h\u1ECDc 2D cho h\u1ECDc sinh THCS v\xE0 l\u1EDBp 10 Vi\u1EC7t Nam.
667
+
668
+ ## Nhi\u1EC7m v\u1EE5
669
+ \u0110\u1ECDc \u0111\u1EC1 b\xE0i ti\u1EBFng Vi\u1EC7t \u2192 emit JSON envelope m\xF4 t\u1EA3 h\xECnh. H\u1EC7 th\u1ED1ng s\u1EBD render h\xECnh t\u1EEB DSL.
670
+
671
+ ## Output format (CH\u1EC8 JSON, kh\xF4ng markdown, kh\xF4ng text kh\xE1c)
672
+ { "decision": "build", "figure": { /* DSL */ } }
673
+ ho\u1EB7c
674
+ { "decision": "refuse", "reason": "l\xFD do ti\u1EBFng Vi\u1EC7t" }
675
+
676
+ ## Quy t\u1EAFc
677
+ 1. V\u1EBD \u0111\u01B0\u1EE3c \u2192 decision="build" + figure \u0111\u1EA7y \u0111\u1EE7 DSL.
678
+ 2. \u0110\u1EC1 ngo\xE0i ph\u1EA1m vi \u2192 decision="refuse" + reason ti\u1EBFng Vi\u1EC7t c\u1EE5 th\u1EC3. Bao g\u1ED3m:
679
+ - T\xEDnh to\xE1n \u0111\u1EA1i s\u1ED1 / l\u01B0\u1EE3ng gi\xE1c / gi\u1EA3i ph\u01B0\u01A1ng tr\xECnh (kh\xF4ng y\xEAu c\u1EA7u v\u1EBD).
680
+ - H\xECnh 3D, l\u1EADp th\u1EC3, kh\xF4ng gian.
681
+ - Ph\xE9p bi\u1EBFn h\xECnh affine / t\u1ECBnh ti\u1EBFn / v\u1ECB t\u1EF1 / quay (l\u1EDBp 11+).
682
+ - \u0110\u1EC1 m\xF4 t\u1EA3 kh\xF4ng \u0111\u1EE7 th\xF4ng tin \u0111\u1EC3 d\u1EF1ng (vd "\u0111i\u1EC3m M b\u1EA5t k\u1EF3 tr\xEAn m\u1EB7t ph\u1EB3ng").
683
+ 3. **\u01AFu ti\xEAn derived points** (midpoint, perpFoot, intersection, circumcenter, \u2026) thay v\xEC t\u1EF1 compute to\u1EA1 \u0111\u1ED9. Ch\u1EC9 d\xF9ng \`free\` cho \u0111i\u1EC3m g\u1ED1c t\u1EF1 do (A, B, C c\u1EE7a tam gi\xE1c \u2014 \u0111\u1EB7t coord -5..5).
684
+ 4. **\u0110\u01B0\u1EDDng cao**: d\xF9ng \`perpFoot\` cho ch\xE2n, KH\xD4NG free anchor.
685
+ 5. **Ph\xE2n gi\xE1c g\xF3c A \u0111\u1EBFn BC**: emit line \`angleBisector\` (B, A, C) + segment BC + point D = \`intersection\` c\u1EE7a 2 line \u0111\xF3. KH\xD4NG emit \`onSegment\` v\u1EDBi segmentId ch\xEDnh l\xE0 segment \u0111ang d\u1EF1ng (cycle).
686
+ 6. **\u0110\u01B0\u1EDDng tr\xF2n ngo\u1EA1i ti\u1EBFp**: d\xF9ng \`circle3\` (3 \u0111i\u1EC3m), kh\xF4ng ph\u1EA3i \`polygon\`.
687
+ 7. **\u0110\u01B0\u1EDDng tr\xF2n n\u1ED9i ti\u1EBFp / ti\u1EBFp x\xFAc**: t\xE2m b\u1EB1ng \`incenter\`, \u0111i\u1EC3m ti\u1EBFp x\xFAc b\u1EB1ng \`perpFoot\` t\u1EEB t\xE2m xu\u1ED1ng c\u1EA1nh t\u01B0\u01A1ng \u1EE9ng, r\u1ED3i \`circleCP\`.
688
+ 8. **\u0110\u01B0\u1EDDng tr\xF2n (O; R) b\xE1n k\xEDnh s\u1ED1**: emit free helper tr\xEAn \u0111\u01B0\u1EDDng tr\xF2n r\u1ED3i d\xF9ng \`circleCP\` (DSL kh\xF4ng h\u1ED7 tr\u1EE3 radius numeric tr\u1EF1c ti\u1EBFp).
689
+ 9. **\u0110\u1EC1 gh\xE9p nhi\u1EC1u y\xEAu c\u1EA7u** (vd "trung \u0111i\u1EC3m M v\xE0 \u0111\u01B0\u1EDDng cao AH"): m\u1ED7i y\xEAu c\u1EA7u l\xE0 1 derived point ri\xEAng v\u1EDBi kind \u0111\xFAng. KH\xD4NG \u0111\u1EB7t chung 1 point tho\u1EA3 nhi\u1EC1u r\xE0ng bu\u1ED9c. V\u1EBD th\xEAm segment cho t\u1EEBng y\xEAu c\u1EA7u (AM, AH, ...) khi \u0111\u1EC1 m\xF4 t\u1EA3 "d\u1EF1ng" ho\u1EB7c "v\u1EBD".
690
+
691
+ ## Anti-pattern (B\u1EAET BU\u1ED8C tr\xE1nh)
692
+ - **Cycle / forward-ref**: KH\xD4NG \u0111\u01B0\u1EE3c tham chi\u1EBFu ch\xE9o. N\u1EBFu point D \u2208 segment AD \u2192 cycle (AD c\u1EA7n D, D c\u1EA7n AD). \u0110\xFAng pattern: D = intersection c\u1EE7a line n\xE0o \u0111\xF3 v\u1EDBi BC, sau \u0111\xF3 AD = segment(A, D).
693
+ - **M\u1ECDi name b\u1ECB reference ph\u1EA3i \u0111\u01B0\u1EE3c \u0111\u1ECBnh ngh\u0129a tr\u01B0\u1EDBc** (DSL topological sort: free \u2192 derived \u2192 shape).
694
+ - **\u0110a gi\xE1c polygon KH\xD4NG thay th\u1EBF cho \u0111\u01B0\u1EDDng tr\xF2n**: n\u1EBFu \u0111\u1EC1 n\xF3i "\u0111\u01B0\u1EDDng tr\xF2n qua 3 \u0111i\u1EC3m" m\xE0 b\u1EA1n emit polygon th\xEC sai.
695
+
696
+ ## Primitives s\u1EB5n c\xF3
697
+ **Points:** free, midpoint, onSegment, onLine, onCircle, perpFoot, circumcenter, incenter, centroid, orthocenter, intersection
698
+ **Shapes:** segment, line, ray, polygon, perpendicular, parallel, perpBisector, angleBisector, tangent, circleCP, circle3
699
+
700
+ ## ${FIXTURES.length} v\xED d\u1EE5
701
+ ${examples}
702
+
703
+ Tr\u1EA3 v\u1EC1 CH\u1EC8 1 JSON object \u0111\xFAng schema. Kh\xF4ng c\xF3 l\u1EDDi d\u1EABn, kh\xF4ng markdown fence.`;
704
+ }
705
+ var TOOL_NAME = "emit_figure_envelope";
706
+ var AnthropicProvider = class {
707
+ constructor(opts) {
708
+ this.opts = opts;
709
+ this.name = "anthropic";
710
+ this.defaultModel = "claude-opus-4-7";
711
+ if (!opts.apiKey) throw new Error("AnthropicProvider: apiKey b\u1EAFt bu\u1ED9c");
712
+ }
713
+ async call(req) {
714
+ const enableCaching = this.opts.enableCaching !== false;
715
+ const systemBlock = enableCaching ? { type: "text", text: req.systemPrompt, cache_control: { type: "ephemeral" } } : { type: "text", text: req.systemPrompt };
716
+ const tool = {
717
+ name: TOOL_NAME,
718
+ description: 'Emit envelope JSON cho ph\xE9p v\u1EBD h\xECnh ho\u1EB7c t\u1EEB ch\u1ED1i. decision="build" k\xE8m figure (DSL h\xECnh h\u1ECDc); decision="refuse" k\xE8m reason.',
719
+ input_schema: req.schema
720
+ };
721
+ const client = new Anthropic({ apiKey: this.opts.apiKey });
722
+ let resp;
723
+ try {
724
+ resp = await client.messages.create(
725
+ {
726
+ model: req.model,
727
+ max_tokens: req.maxTokens,
728
+ system: [systemBlock],
729
+ tools: [tool],
730
+ tool_choice: { type: "tool", name: TOOL_NAME },
731
+ messages: [{ role: "user", content: req.userPrompt }]
732
+ },
733
+ req.signal ? { signal: req.signal } : void 0
734
+ );
735
+ } catch (e) {
736
+ const err = e;
737
+ return {
738
+ kind: "error",
739
+ message: err.message ?? "L\u1ED7i g\u1ECDi Anthropic API",
740
+ ...err.status !== void 0 ? { status: err.status } : {}
741
+ };
742
+ }
743
+ const usage = toUsage(resp.usage);
744
+ const toolUse = resp.content.find((c) => c.type === "tool_use");
745
+ if (!toolUse || toolUse.type !== "tool_use") {
746
+ return {
747
+ kind: "error",
748
+ message: "Claude kh\xF4ng g\u1ECDi tool. stop_reason=" + resp.stop_reason
749
+ };
750
+ }
751
+ if (toolUse.name !== TOOL_NAME) {
752
+ return {
753
+ kind: "error",
754
+ message: `Tool kh\xF4ng x\xE1c \u0111\u1ECBnh: "${toolUse.name}"`
755
+ };
756
+ }
757
+ return { kind: "json", data: toolUse.input, usage };
758
+ }
759
+ };
760
+ function toUsage(u) {
761
+ return {
762
+ inputTokens: u.input_tokens,
763
+ outputTokens: u.output_tokens,
764
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
765
+ cacheCreationTokens: u.cache_creation_input_tokens ?? 0
766
+ };
767
+ }
768
+
769
+ // src/stamps/geometry-2d/ai/providers/ollama.ts
770
+ var DEFAULT_BASE_URL = "http://localhost:11434";
771
+ var DEFAULT_MODEL = "gemma3:4b";
772
+ var OllamaProvider = class {
773
+ constructor(opts = {}) {
774
+ this.name = "ollama";
775
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
776
+ this.defaultModel = opts.defaultModel ?? DEFAULT_MODEL;
777
+ this.fetchImpl = opts.fetchImpl ?? null;
778
+ }
779
+ resolveFetch() {
780
+ if (this.fetchImpl) return this.fetchImpl;
781
+ if (typeof fetch === "undefined") {
782
+ throw new Error(
783
+ "OllamaProvider: global `fetch` kh\xF4ng kh\u1EA3 d\u1EE5ng. Truy\u1EC1n `fetchImpl` qua constructor ho\u1EB7c ch\u1EA1y \u1EDF Node 18+ / browser."
784
+ );
785
+ }
786
+ return fetch;
787
+ }
788
+ async call(req) {
789
+ const body = {
790
+ model: req.model,
791
+ messages: [
792
+ { role: "system", content: req.systemPrompt },
793
+ { role: "user", content: req.userPrompt }
794
+ ],
795
+ // Ollama v0.5+ structured outputs: model bị constrain emit JSON đúng schema.
796
+ format: req.schema,
797
+ stream: false,
798
+ options: {
799
+ // num_predict ≈ max_tokens
800
+ num_predict: req.maxTokens,
801
+ // temperature thấp cho output deterministic hơn (DSL cần consistent).
802
+ temperature: 0.2
803
+ }
804
+ };
805
+ let resp;
806
+ let doFetch;
807
+ try {
808
+ doFetch = this.resolveFetch();
809
+ } catch (e) {
810
+ const err = e;
811
+ return { kind: "error", message: err.message ?? "fetch kh\xF4ng kh\u1EA3 d\u1EE5ng" };
812
+ }
813
+ try {
814
+ resp = await doFetch(`${this.baseUrl}/api/chat`, {
815
+ method: "POST",
816
+ headers: { "content-type": "application/json" },
817
+ body: JSON.stringify(body),
818
+ signal: req.signal
819
+ });
820
+ } catch (e) {
821
+ const err = e;
822
+ return {
823
+ kind: "error",
824
+ message: err.message ?? `Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Ollama \u1EDF ${this.baseUrl}`
825
+ };
826
+ }
827
+ if (!resp.ok) {
828
+ let detail = "";
829
+ try {
830
+ detail = await resp.text();
831
+ } catch {
832
+ }
833
+ return {
834
+ kind: "error",
835
+ message: `Ollama HTTP ${resp.status}: ${detail || resp.statusText}`,
836
+ status: resp.status
837
+ };
838
+ }
839
+ let json;
840
+ try {
841
+ json = await resp.json();
842
+ } catch (e) {
843
+ const err = e;
844
+ return { kind: "error", message: "Ollama response kh\xF4ng ph\u1EA3i JSON: " + (err.message ?? "?") };
845
+ }
846
+ const content = json.message?.content?.trim();
847
+ if (!content) {
848
+ return { kind: "error", message: "Ollama tr\u1EA3 message.content r\u1ED7ng" };
849
+ }
850
+ let data;
851
+ try {
852
+ data = JSON.parse(content);
853
+ } catch (e) {
854
+ const err = e;
855
+ return {
856
+ kind: "error",
857
+ message: "Ollama content kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: " + (err.message ?? "?")
858
+ };
859
+ }
860
+ const usage = {
861
+ inputTokens: json.prompt_eval_count ?? 0,
862
+ outputTokens: json.eval_count ?? 0,
863
+ cacheReadTokens: 0,
864
+ cacheCreationTokens: 0
865
+ };
866
+ return { kind: "json", data, usage };
867
+ }
868
+ };
869
+
870
+ // src/stamps/geometry-2d/ai/providers/index.ts
871
+ function selectProvider(opts = {}) {
872
+ if (opts.provider) return opts.provider;
873
+ if (opts.apiKey) {
874
+ return new AnthropicProvider({
875
+ apiKey: opts.apiKey,
876
+ enableCaching: opts.enableCaching
877
+ });
878
+ }
879
+ const env = opts.env ?? readEnv();
880
+ const wanted = (env.WHITEBOARD_AI_PROVIDER ?? "ollama").toLowerCase();
881
+ if (wanted === "anthropic") {
882
+ const key = env.ANTHROPIC_API_KEY;
883
+ if (!key) {
884
+ throw new Error(
885
+ "selectProvider: WHITEBOARD_AI_PROVIDER=anthropic nh\u01B0ng thi\u1EBFu env ANTHROPIC_API_KEY"
886
+ );
887
+ }
888
+ return new AnthropicProvider({ apiKey: key, enableCaching: opts.enableCaching });
889
+ }
890
+ if (wanted === "ollama") {
891
+ return new OllamaProvider({
892
+ baseUrl: opts.ollamaBaseUrl ?? env.OLLAMA_BASE_URL,
893
+ defaultModel: opts.ollamaDefaultModel ?? env.OLLAMA_DEFAULT_MODEL
894
+ });
895
+ }
896
+ throw new Error(`selectProvider: WHITEBOARD_AI_PROVIDER="${wanted}" kh\xF4ng h\u1EE3p l\u1EC7 (anthropic|ollama)`);
897
+ }
898
+ function readEnv() {
899
+ if (typeof process !== "undefined" && process.env) {
900
+ return process.env;
901
+ }
902
+ return {};
903
+ }
904
+
905
+ // src/stamps/geometry-2d/ai/buildFigure.ts
906
+ var DEFAULT_MAX_TOKENS = 8192;
907
+ async function generateFigure(problem, opts = {}) {
908
+ if (!problem || !problem.trim()) {
909
+ return { ok: false, reason: "api_error", message: "\u0110\u1EC1 b\xE0i r\u1ED7ng" };
910
+ }
911
+ let provider;
912
+ try {
913
+ provider = selectProvider(opts);
914
+ } catch (e) {
915
+ const err = e;
916
+ return { ok: false, reason: "api_error", message: err.message ?? "Kh\xF4ng ch\u1ECDn \u0111\u01B0\u1EE3c provider" };
917
+ }
918
+ const systemPrompt = buildSystemPrompt();
919
+ const schema = envelopeJsonSchema();
920
+ const out = await provider.call({
921
+ systemPrompt,
922
+ userPrompt: problem,
923
+ schema,
924
+ model: opts.model ?? provider.defaultModel,
925
+ maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
926
+ signal: opts.signal
927
+ });
928
+ if (out.kind === "error") {
929
+ return {
930
+ ok: false,
931
+ reason: "api_error",
932
+ message: out.message,
933
+ ...out.status !== void 0 ? { status: out.status } : {},
934
+ provider: provider.name
935
+ };
936
+ }
937
+ const usage = toUsage2(out.usage);
938
+ const parsed = FigureEnvelopeZ.safeParse(out.data);
939
+ if (!parsed.success) {
940
+ return {
941
+ ok: false,
942
+ reason: "parse_error",
943
+ message: "Envelope kh\xF4ng kh\u1EDBp schema: " + parsed.error.issues.map((i) => i.message).join("; "),
944
+ raw: out.data,
945
+ usage,
946
+ provider: provider.name
947
+ };
948
+ }
949
+ const env = parsed.data;
950
+ if (env.decision === "refuse") {
951
+ return {
952
+ ok: false,
953
+ reason: "refused",
954
+ message: env.reason ?? "AI t\u1EEB ch\u1ED1i kh\xF4ng n\xEAu l\xFD do",
955
+ usage,
956
+ provider: provider.name
957
+ };
958
+ }
959
+ const dsl = envelopeBuildDsl(env);
960
+ const tResult = transpile(dsl);
961
+ if (!tResult.ok) {
962
+ return {
963
+ ok: false,
964
+ reason: "transpile_error",
965
+ message: "DSL t\u1EEB AI kh\xF4ng h\u1EE3p l\u1EC7",
966
+ errors: tResult.errors,
967
+ dsl,
968
+ usage,
969
+ provider: provider.name
970
+ };
971
+ }
972
+ return {
973
+ ok: true,
974
+ state: tResult.state,
975
+ dsl,
976
+ usage,
977
+ provider: provider.name
978
+ };
979
+ }
980
+ function toUsage2(u) {
981
+ return {
982
+ inputTokens: u?.inputTokens ?? 0,
983
+ outputTokens: u?.outputTokens ?? 0,
984
+ cacheReadTokens: u?.cacheReadTokens ?? 0,
985
+ cacheCreationTokens: u?.cacheCreationTokens ?? 0
986
+ };
987
+ }
988
+
989
+ // src/stamps/geometry-2d/ai/handleGenerateFigure.ts
990
+ var DEFAULT_MAX_ATTEMPTS = 2;
991
+ async function handleGenerateFigure(input, opts = {}) {
992
+ const { onResult, maxAttempts: rawMax, ...generateOpts } = opts;
993
+ const maxAttempts = clampAttempts(rawMax ?? DEFAULT_MAX_ATTEMPTS);
994
+ let lastResult = null;
995
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
996
+ const result = await generateFigure(input.problem, generateOpts);
997
+ lastResult = result;
998
+ if (onResult) {
999
+ try {
1000
+ onResult(result, attempt);
1001
+ } catch {
1002
+ }
1003
+ }
1004
+ if (result.ok) {
1005
+ return { ok: true, state: result.state };
1006
+ }
1007
+ if (result.reason === "transpile_error" && attempt < maxAttempts) {
1008
+ continue;
1009
+ }
1010
+ break;
1011
+ }
1012
+ return mapErrorToUi(lastResult);
1013
+ }
1014
+ function clampAttempts(n) {
1015
+ if (!Number.isFinite(n)) return DEFAULT_MAX_ATTEMPTS;
1016
+ return Math.max(1, Math.min(5, Math.floor(n)));
1017
+ }
1018
+ function mapErrorToUi(result) {
1019
+ if (result.ok) return { ok: true, state: result.state };
1020
+ switch (result.reason) {
1021
+ case "refused":
1022
+ return { ok: false, message: result.message };
1023
+ case "parse_error":
1024
+ return {
1025
+ ok: false,
1026
+ message: "AI tr\u1EA3 v\u1EC1 d\u1EEF li\u1EC7u kh\xF4ng h\u1EE3p l\u1EC7. Vui l\xF2ng th\u1EED l\u1EA1i ho\u1EB7c di\u1EC5n \u0111\u1EA1t l\u1EA1i \u0111\u1EC1 b\xE0i."
1027
+ };
1028
+ case "transpile_error":
1029
+ return {
1030
+ ok: false,
1031
+ message: "AI t\u1EA1o h\xECnh kh\xF4ng h\u1EE3p l\u1EC7 (\u0111\xE3 th\u1EED l\u1EA1i). Vui l\xF2ng t\xE1ch th\xE0nh 1 y\xEAu c\u1EA7u/l\u1EA7n ho\u1EB7c di\u1EC5n \u0111\u1EA1t kh\xE1c."
1032
+ };
1033
+ case "api_error":
1034
+ default:
1035
+ return { ok: false, message: result.message };
1036
+ }
1037
+ }
1038
+ var FigureRefineEnvelopeZ = z.object({
1039
+ decision: z.enum(["add", "replace", "refuse"]),
1040
+ figure: DslInput.optional(),
1041
+ reason: z.string().optional()
1042
+ }).refine(
1043
+ (e) => e.decision === "refuse" ? e.reason != null && e.reason.length > 0 : e.figure != null,
1044
+ {
1045
+ message: "decision=add/replace c\u1EA7n `figure`; decision=refuse c\u1EA7n `reason` kh\xF4ng r\u1ED7ng"
1046
+ }
1047
+ );
1048
+ function refineEnvelopeJsonSchema() {
1049
+ return zodToJsonSchema(FigureRefineEnvelopeZ, {
1050
+ target: "jsonSchema7",
1051
+ $refStrategy: "none"
1052
+ });
1053
+ }
1054
+
1055
+ // src/stamps/geometry-2d/ai/refineFixtures.ts
1056
+ var triangleABC = {
1057
+ version: 1,
1058
+ points: [
1059
+ { name: "A", kind: "free", x: 0, y: 3 },
1060
+ { name: "B", kind: "free", x: -2, y: 0 },
1061
+ { name: "C", kind: "free", x: 3, y: 0 }
1062
+ ],
1063
+ shapes: [{ name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }]
1064
+ };
1065
+ var rightTriangleAtA = {
1066
+ version: 1,
1067
+ points: [
1068
+ { name: "A", kind: "free", x: 0, y: 0 },
1069
+ { name: "B", kind: "free", x: 4, y: 0 },
1070
+ { name: "C", kind: "free", x: 0, y: 3 }
1071
+ ],
1072
+ shapes: [{ name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }]
1073
+ };
1074
+ var parallelogramABCD = {
1075
+ version: 1,
1076
+ points: [
1077
+ { name: "A", kind: "free", x: -2, y: 0 },
1078
+ { name: "B", kind: "free", x: 3, y: 0 },
1079
+ { name: "C", kind: "free", x: 4, y: 2 },
1080
+ { name: "D", kind: "free", x: -1, y: 2 }
1081
+ ],
1082
+ shapes: [{ name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] }]
1083
+ };
1084
+ var circleOnA = {
1085
+ version: 1,
1086
+ points: [
1087
+ { name: "O", kind: "free", x: 0, y: 0 },
1088
+ { name: "A", kind: "free", x: 3, y: 0 }
1089
+ ],
1090
+ shapes: [{ name: "omega", kind: "circleCP", center: "O", surfacePoint: "A" }]
1091
+ };
1092
+ var REFINE_FIXTURES = [
1093
+ {
1094
+ name: "triangle-add-midpoint",
1095
+ currentDsl: triangleABC,
1096
+ instruction: "Th\xEAm trung \u0111i\u1EC3m M c\u1EE7a BC",
1097
+ expectedEnvelope: {
1098
+ decision: "add",
1099
+ figure: {
1100
+ version: 1,
1101
+ points: [{ name: "M", kind: "midpoint", p1: "B", p2: "C" }],
1102
+ shapes: [{ name: "AM", kind: "segment", p1: "A", p2: "M" }]
1103
+ }
1104
+ }
1105
+ },
1106
+ {
1107
+ name: "triangle-add-altitude",
1108
+ currentDsl: triangleABC,
1109
+ instruction: "D\u1EF1ng \u0111\u01B0\u1EDDng cao AH xu\u1ED1ng BC",
1110
+ expectedEnvelope: {
1111
+ decision: "add",
1112
+ figure: {
1113
+ version: 1,
1114
+ points: [{ name: "H", kind: "perpFoot", from: "A", onLine: "BC_line" }],
1115
+ shapes: [
1116
+ { name: "BC_line", kind: "line", p1: "B", p2: "C" },
1117
+ { name: "AH", kind: "segment", p1: "A", p2: "H" }
1118
+ ]
1119
+ }
1120
+ }
1121
+ },
1122
+ {
1123
+ name: "triangle-add-circumcircle",
1124
+ currentDsl: triangleABC,
1125
+ instruction: "V\u1EBD \u0111\u01B0\u1EDDng tr\xF2n ngo\u1EA1i ti\u1EBFp tam gi\xE1c ABC",
1126
+ expectedEnvelope: {
1127
+ decision: "add",
1128
+ figure: {
1129
+ version: 1,
1130
+ points: [{ name: "O", kind: "circumcenter", vertices: ["A", "B", "C"] }],
1131
+ shapes: [{ name: "omega", kind: "circle3", p1: "A", p2: "B", p3: "C" }]
1132
+ }
1133
+ }
1134
+ },
1135
+ {
1136
+ name: "right-triangle-add-centroid",
1137
+ currentDsl: rightTriangleAtA,
1138
+ instruction: "Th\xEAm tr\u1ECDng t\xE2m G c\u1EE7a tam gi\xE1c",
1139
+ expectedEnvelope: {
1140
+ decision: "add",
1141
+ figure: {
1142
+ version: 1,
1143
+ points: [{ name: "G", kind: "centroid", vertices: ["A", "B", "C"] }],
1144
+ shapes: []
1145
+ }
1146
+ }
1147
+ },
1148
+ {
1149
+ name: "parallelogram-add-diagonals",
1150
+ currentDsl: parallelogramABCD,
1151
+ instruction: "V\u1EBD hai \u0111\u01B0\u1EDDng ch\xE9o AC, BD v\xE0 giao \u0111i\u1EC3m O",
1152
+ expectedEnvelope: {
1153
+ decision: "add",
1154
+ figure: {
1155
+ version: 1,
1156
+ points: [{ name: "O", kind: "intersection", ref1: "AC", ref2: "BD" }],
1157
+ shapes: [
1158
+ { name: "AC", kind: "segment", p1: "A", p2: "C" },
1159
+ { name: "BD", kind: "segment", p1: "B", p2: "D" }
1160
+ ]
1161
+ }
1162
+ }
1163
+ },
1164
+ {
1165
+ name: "circle-add-tangent",
1166
+ currentDsl: circleOnA,
1167
+ instruction: "K\u1EBB ti\u1EBFp tuy\u1EBFn t\u1EA1i A c\u1EE7a \u0111\u01B0\u1EDDng tr\xF2n",
1168
+ expectedEnvelope: {
1169
+ decision: "add",
1170
+ figure: {
1171
+ version: 1,
1172
+ points: [],
1173
+ shapes: [{ name: "t", kind: "tangent", throughPoint: "A", toCircle: "omega" }]
1174
+ }
1175
+ }
1176
+ },
1177
+ {
1178
+ name: "triangle-replace-equilateral",
1179
+ currentDsl: triangleABC,
1180
+ instruction: "B\u1ECF tam gi\xE1c n\xE0y, v\u1EBD tam gi\xE1c \u0111\u1EC1u ABC thay v\xE0o",
1181
+ expectedEnvelope: {
1182
+ decision: "replace",
1183
+ figure: {
1184
+ version: 1,
1185
+ points: [
1186
+ { name: "A", kind: "free", x: 0, y: 2 },
1187
+ { name: "B", kind: "free", x: -1.732, y: -1 },
1188
+ { name: "C", kind: "free", x: 1.732, y: -1 }
1189
+ ],
1190
+ shapes: [{ name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }]
1191
+ }
1192
+ }
1193
+ },
1194
+ {
1195
+ name: "triangle-replace-rhombus",
1196
+ currentDsl: triangleABC,
1197
+ instruction: "\u0110\u1ED5i sang h\xECnh thoi ABCD",
1198
+ expectedEnvelope: {
1199
+ decision: "replace",
1200
+ figure: {
1201
+ version: 1,
1202
+ points: [
1203
+ { name: "A", kind: "free", x: -2, y: 0 },
1204
+ { name: "B", kind: "free", x: 0, y: 1.5 },
1205
+ { name: "C", kind: "free", x: 2, y: 0 },
1206
+ { name: "D", kind: "free", x: 0, y: -1.5 }
1207
+ ],
1208
+ shapes: [{ name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] }]
1209
+ }
1210
+ }
1211
+ },
1212
+ {
1213
+ name: "refuse-calculation",
1214
+ currentDsl: triangleABC,
1215
+ instruction: "T\xEDnh di\u1EC7n t\xEDch tam gi\xE1c ABC",
1216
+ expectedEnvelope: {
1217
+ decision: "refuse",
1218
+ reason: "Y\xEAu c\u1EA7u t\xEDnh to\xE1n, kh\xF4ng ph\u1EA3i v\u1EBD h\xECnh."
1219
+ }
1220
+ },
1221
+ {
1222
+ name: "refuse-3d",
1223
+ currentDsl: triangleABC,
1224
+ instruction: "V\u1EBD h\xECnh ch\xF3p SABC v\u1EDBi S n\u1EB1m tr\xEAn tam gi\xE1c",
1225
+ expectedEnvelope: {
1226
+ decision: "refuse",
1227
+ reason: "H\xECnh 3D ngo\xE0i ph\u1EA1m vi geometry-2d."
1228
+ }
1229
+ }
1230
+ ];
1231
+ var REFINE_PROMPT_FIXTURES = REFINE_FIXTURES.slice(0, 8);
1232
+
1233
+ // src/stamps/geometry-2d/ai/refinePrompt.ts
1234
+ function namesOf(dsl) {
1235
+ return {
1236
+ points: dsl.points.map((p) => p.name),
1237
+ shapes: dsl.shapes.map((s) => s.name)
1238
+ };
1239
+ }
1240
+ function buildRefineSystemPrompt(currentDsl) {
1241
+ const names = namesOf(currentDsl);
1242
+ const examples = REFINE_PROMPT_FIXTURES.map((f, i) => {
1243
+ const env = f.expectedEnvelope;
1244
+ return `### V\xED d\u1EE5 ${i + 1}
1245
+ **H\xECnh hi\u1EC7n t\u1EA1i:**
1246
+ ${JSON.stringify(f.currentDsl, null, 2)}
1247
+ **Y\xEAu c\u1EA7u ch\u1EC9nh s\u1EEDa:** ${f.instruction}
1248
+ **Output:**
1249
+ ${JSON.stringify(env, null, 2)}`;
1250
+ }).join("\n\n");
1251
+ return `B\u1EA1n l\xE0 tr\u1EE3 l\xFD v\u1EBD h\xECnh h\u1ECDc 2D. H\u1ECDc sinh \u0111\xE3 c\xF3 H\xCCNH HI\u1EC6N T\u1EA0I v\xE0 mu\u1ED1n TH\xCAM/S\u1EECA.
1252
+
1253
+ ## H\xECnh hi\u1EC7n t\u1EA1i (DSL JSON)
1254
+ ${JSON.stringify(currentDsl, null, 2)}
1255
+
1256
+ ## T\xEAn \u0111\xE3 d\xF9ng (KH\xD4NG \u0111\u01B0\u1EE3c redefine)
1257
+ points: ${names.points.join(", ") || "(ch\u01B0a c\xF3)"}
1258
+ shapes: ${names.shapes.join(", ") || "(ch\u01B0a c\xF3)"}
1259
+
1260
+ ## Nhi\u1EC7m v\u1EE5
1261
+ \u0110\u1ECDc Y\xCAU C\u1EA6U CH\u1EC8NH S\u1EECA \u2192 emit JSON envelope \u0111\xFAng 1 trong 3 d\u1EA1ng:
1262
+
1263
+ { "decision": "add", "figure": <DSL ch\u1EC9 ch\u1EE9a entity M\u1EDAI> }
1264
+ { "decision": "replace", "figure": <DSL ho\xE0n ch\u1EC9nh thay th\u1EBF h\xECnh c\u0169> }
1265
+ { "decision": "refuse", "reason": "l\xFD do ti\u1EBFng Vi\u1EC7t" }
1266
+
1267
+ ## Khi n\xE0o d\xF9ng decision n\xE0o?
1268
+ - **"add"**: user mu\u1ED1n TH\xCAM primitive v\xE0o h\xECnh hi\u1EC7n t\u1EA1i (vd: "th\xEAm trung \u0111i\u1EC3m M c\u1EE7a BC", "d\u1EF1ng \u0111\u01B0\u1EDDng cao AH").
1269
+ \u2192 figure ch\u1EC9 ch\u1EE9a point/shape M\u1EDAI. Ref t\xEAn c\u0169 (A, B, C, \u2026) l\xE0 OK. KH\xD4NG redefine t\xEAn c\u0169.
1270
+ - **"replace"**: user mu\u1ED1n v\u1EBD L\u1EA0I ho\u1EB7c \u0111\u1ED5i sang h\xECnh kh\xE1c h\u1EB3n (vd: "v\u1EBD tam gi\xE1c \u0111\u1EC1u thay v\xE0o", "b\u1ECF tam gi\xE1c, d\u1EF1ng h\xECnh thoi").
1271
+ \u2192 figure \u0111\u1EA7y \u0111\u1EE7 nh\u01B0 prompt m\u1EDBi (gi\u1ED1ng mode build).
1272
+ - **"refuse"**: y\xEAu c\u1EA7u ngo\xE0i ph\u1EA1m vi (3D, l\u01B0\u1EE3ng gi\xE1c, bi\u1EBFn h\xECnh l\u1EDBp 11+, t\xEDnh to\xE1n \u0111\u1EA1i s\u1ED1).
1273
+
1274
+ ## Quy t\u1EAFc decision=add
1275
+ 1. M\u1ECDi name M\u1EDAI KH\xD4NG \u0111\u01B0\u1EE3c tr\xF9ng v\u1EDBi t\xEAn \u0111\xE3 d\xF9ng \u1EDF tr\xEAn. Tr\xF9ng \u2192 \u0111\u1EB7t kh\xE1c (M', M1, \u2026).
1276
+ 2. \u01AFU TI\xCAN derived points: midpoint, perpFoot, intersection, circumcenter, incenter, centroid, orthocenter.
1277
+ 3. Ref t\u1EDBi t\xEAn c\u0169 (A, B, C) l\xE0 OK \u2014 AI bi\u1EBFt c\xE1c t\xEAn \u0111\xF3 t\u1ED3n t\u1EA1i.
1278
+ 4. KH\xD4NG copy l\u1EA1i entity c\u0169 v\xE0o figure delta (delta ch\u1EC9 ch\u1EE9a c\xE1i M\u1EDAI).
1279
+
1280
+ ## Anti-pattern (B\u1EAET BU\u1ED8C tr\xE1nh)
1281
+ - KH\xD4NG redefine t\xEAn \u0111\xE3 d\xF9ng (A, B, C \u0111\xE3 c\xF3 \u2192 KH\xD4NG \u0111\u1EB7t l\u1EA1i).
1282
+ - KH\xD4NG ref t\u1EDBi t\xEAn ch\u01B0a c\xF3 ngo\xE0i "T\xEAn \u0111\xE3 d\xF9ng" + t\xEAn v\u1EEBa \u0111\u1ECBnh ngh\u0129a trong delta.
1283
+ - KH\xD4NG emit add v\u1EDBi figure ch\u1EE9a c\u1EA3 entity c\u0169 (\u0111\xF3 l\xE0 replace).
1284
+
1285
+ ## Primitives s\u1EB5n c\xF3
1286
+ **Points:** free, midpoint, onSegment, onLine, onCircle, perpFoot, circumcenter, incenter, centroid, orthocenter, intersection
1287
+ **Shapes:** segment, line, ray, polygon, perpendicular, parallel, perpBisector, angleBisector, tangent, circleCP, circle3
1288
+
1289
+ ## ${REFINE_PROMPT_FIXTURES.length} v\xED d\u1EE5
1290
+
1291
+ ${examples}
1292
+
1293
+ Tr\u1EA3 v\u1EC1 CH\u1EC8 1 JSON object \u0111\xFAng schema. Kh\xF4ng c\xF3 l\u1EDDi d\u1EABn, kh\xF4ng markdown fence.`;
1294
+ }
1295
+
1296
+ // src/stamps/geometry-2d/ai/buildFigureDelta.ts
1297
+ var DEFAULT_MAX_TOKENS2 = 8192;
1298
+ async function generateFigureDelta(input, opts = {}) {
1299
+ const { problem, currentDsl } = input;
1300
+ if (!problem || !problem.trim()) {
1301
+ return { ok: false, reason: "api_error", message: "\u0110\u1EC1 b\xE0i r\u1ED7ng" };
1302
+ }
1303
+ let provider;
1304
+ try {
1305
+ provider = selectProvider(opts);
1306
+ } catch (e) {
1307
+ const err = e;
1308
+ return { ok: false, reason: "api_error", message: err.message ?? "Kh\xF4ng ch\u1ECDn \u0111\u01B0\u1EE3c provider" };
1309
+ }
1310
+ const systemPrompt = buildRefineSystemPrompt(currentDsl);
1311
+ const schema = refineEnvelopeJsonSchema();
1312
+ const out = await provider.call({
1313
+ systemPrompt,
1314
+ userPrompt: problem,
1315
+ schema,
1316
+ model: opts.model ?? provider.defaultModel,
1317
+ maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS2,
1318
+ signal: opts.signal
1319
+ });
1320
+ if (out.kind === "error") {
1321
+ return {
1322
+ ok: false,
1323
+ reason: "api_error",
1324
+ message: out.message,
1325
+ ...out.status !== void 0 ? { status: out.status } : {},
1326
+ provider: provider.name
1327
+ };
1328
+ }
1329
+ const usage = toUsage3(out.usage);
1330
+ const parsed = FigureRefineEnvelopeZ.safeParse(out.data);
1331
+ if (!parsed.success) {
1332
+ return {
1333
+ ok: false,
1334
+ reason: "parse_error",
1335
+ message: "Envelope kh\xF4ng kh\u1EDBp schema: " + parsed.error.issues.map((i) => i.message).join("; "),
1336
+ raw: out.data,
1337
+ usage,
1338
+ provider: provider.name
1339
+ };
1340
+ }
1341
+ const env = parsed.data;
1342
+ if (env.decision === "refuse") {
1343
+ return {
1344
+ ok: false,
1345
+ reason: "refused",
1346
+ message: env.reason ?? "AI t\u1EEB ch\u1ED1i kh\xF4ng n\xEAu l\xFD do",
1347
+ usage,
1348
+ provider: provider.name
1349
+ };
1350
+ }
1351
+ if (env.decision === "replace") {
1352
+ const figure = env.figure;
1353
+ const tResult2 = transpile(figure);
1354
+ if (!tResult2.ok) {
1355
+ return liftTranspileError(tResult2.errors, figure, usage, provider.name);
1356
+ }
1357
+ return {
1358
+ ok: true,
1359
+ state: tResult2.state,
1360
+ mergedDsl: figure,
1361
+ mode: "replace",
1362
+ usage,
1363
+ provider: provider.name
1364
+ };
1365
+ }
1366
+ const delta = env.figure;
1367
+ const merged = {
1368
+ version: 1,
1369
+ points: [...currentDsl.points, ...delta.points],
1370
+ shapes: [...currentDsl.shapes, ...delta.shapes]
1371
+ };
1372
+ const tResult = transpile(merged);
1373
+ if (!tResult.ok) {
1374
+ return liftTranspileError(tResult.errors, merged, usage, provider.name);
1375
+ }
1376
+ return {
1377
+ ok: true,
1378
+ state: tResult.state,
1379
+ mergedDsl: merged,
1380
+ mode: "add",
1381
+ usage,
1382
+ provider: provider.name
1383
+ };
1384
+ }
1385
+ function liftTranspileError(errors, dsl, usage, providerName) {
1386
+ const dupes = errors.filter((e) => e.code === "DUPLICATE_NAME");
1387
+ if (dupes.length > 0) {
1388
+ const collisions = Array.from(new Set(dupes.flatMap((e) => e.path ?? []).filter(Boolean)));
1389
+ return {
1390
+ ok: false,
1391
+ reason: "name_collision",
1392
+ message: "AI t\u1EA1o entity tr\xF9ng t\xEAn v\u1EDBi h\xECnh hi\u1EC7n t\u1EA1i: " + (collisions.length > 0 ? collisions.join(", ") : "kh\xF4ng x\xE1c \u0111\u1ECBnh"),
1393
+ collisions,
1394
+ errors,
1395
+ dsl,
1396
+ usage,
1397
+ provider: providerName
1398
+ };
1399
+ }
1400
+ const unresolved = errors.filter((e) => e.code === "UNKNOWN_REF");
1401
+ if (unresolved.length > 0) {
1402
+ const refs = Array.from(new Set(unresolved.flatMap((e) => e.path ?? []).filter(Boolean)));
1403
+ return {
1404
+ ok: false,
1405
+ reason: "unresolved_ref",
1406
+ message: "AI tham chi\u1EBFu t\xEAn kh\xF4ng c\xF3: " + (refs.length > 0 ? refs.join(", ") : "kh\xF4ng x\xE1c \u0111\u1ECBnh"),
1407
+ refs,
1408
+ errors,
1409
+ dsl,
1410
+ usage,
1411
+ provider: providerName
1412
+ };
1413
+ }
1414
+ return {
1415
+ ok: false,
1416
+ reason: "transpile_error",
1417
+ message: "DSL t\u1EEB AI kh\xF4ng h\u1EE3p l\u1EC7",
1418
+ errors,
1419
+ dsl,
1420
+ usage,
1421
+ provider: providerName
1422
+ };
1423
+ }
1424
+ function toUsage3(u) {
1425
+ return {
1426
+ inputTokens: u?.inputTokens ?? 0,
1427
+ outputTokens: u?.outputTokens ?? 0,
1428
+ cacheReadTokens: u?.cacheReadTokens ?? 0,
1429
+ cacheCreationTokens: u?.cacheCreationTokens ?? 0
1430
+ };
1431
+ }
1432
+
1433
+ // src/stamps/geometry-2d/ai/handleGenerateFigureDelta.ts
1434
+ var DEFAULT_MAX_ATTEMPTS2 = 2;
1435
+ async function handleGenerateFigureDelta(input, opts = {}) {
1436
+ const { onResult, maxAttempts: rawMax, ...generateOpts } = opts;
1437
+ const maxAttempts = clampAttempts2(rawMax ?? DEFAULT_MAX_ATTEMPTS2);
1438
+ let lastResult = null;
1439
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1440
+ const result = await generateFigureDelta(input, generateOpts);
1441
+ lastResult = result;
1442
+ if (onResult) {
1443
+ try {
1444
+ onResult(result, attempt);
1445
+ } catch {
1446
+ }
1447
+ }
1448
+ if (result.ok) {
1449
+ return { ok: true, state: result.state };
1450
+ }
1451
+ if (result.reason === "transpile_error" && attempt < maxAttempts) {
1452
+ continue;
1453
+ }
1454
+ break;
1455
+ }
1456
+ return mapErrorToUi2(lastResult);
1457
+ }
1458
+ function clampAttempts2(n) {
1459
+ if (!Number.isFinite(n)) return DEFAULT_MAX_ATTEMPTS2;
1460
+ return Math.max(1, Math.min(5, Math.floor(n)));
1461
+ }
1462
+ function mapErrorToUi2(result) {
1463
+ if (result.ok) return { ok: true, state: result.state };
1464
+ switch (result.reason) {
1465
+ case "refused":
1466
+ return { ok: false, message: result.message };
1467
+ case "parse_error":
1468
+ return {
1469
+ ok: false,
1470
+ message: "AI tr\u1EA3 v\u1EC1 d\u1EEF li\u1EC7u kh\xF4ng h\u1EE3p l\u1EC7. Vui l\xF2ng th\u1EED l\u1EA1i ho\u1EB7c di\u1EC5n \u0111\u1EA1t l\u1EA1i."
1471
+ };
1472
+ case "transpile_error":
1473
+ return {
1474
+ ok: false,
1475
+ message: "AI t\u1EA1o h\xECnh kh\xF4ng h\u1EE3p l\u1EC7 (\u0111\xE3 th\u1EED l\u1EA1i). Vui l\xF2ng t\xE1ch th\xE0nh 1 y\xEAu c\u1EA7u/l\u1EA7n ho\u1EB7c di\u1EC5n \u0111\u1EA1t kh\xE1c."
1476
+ };
1477
+ case "name_collision":
1478
+ return {
1479
+ ok: false,
1480
+ message: `AI t\u1EA1o \u0111i\u1EC3m tr\xF9ng t\xEAn v\u1EDBi h\xECnh hi\u1EC7n t\u1EA1i (${result.collisions.join(", ")}). Vui l\xF2ng di\u1EC5n \u0111\u1EA1t l\u1EA1i.`
1481
+ };
1482
+ case "unresolved_ref":
1483
+ return {
1484
+ ok: false,
1485
+ message: `AI tham chi\u1EBFu sai t\xEAn \u0111\u1ED1i t\u01B0\u1EE3ng (${result.refs.join(", ")}). Vui l\xF2ng di\u1EC5n \u0111\u1EA1t l\u1EA1i.`
1486
+ };
1487
+ case "api_error":
1488
+ default:
1489
+ return { ok: false, message: result.message };
1490
+ }
1491
+ }
1492
+
1493
+ export { AnthropicProvider, FigureEnvelopeZ, FigureRefineEnvelopeZ, OllamaProvider, envelopeBuildDsl, envelopeJsonSchema, generateFigure, generateFigureDelta, handleGenerateFigure, handleGenerateFigureDelta, refineEnvelopeJsonSchema, selectProvider };
1494
+ //# sourceMappingURL=ai.mjs.map
1495
+ //# sourceMappingURL=ai.mjs.map