@xom11/whiteboard 0.24.2 → 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 (84) hide show
  1. package/README.md +84 -11
  2. package/dist/ai.d.mts +422 -566
  3. package/dist/ai.d.ts +422 -566
  4. package/dist/ai.js +1527 -407
  5. package/dist/ai.js.map +1 -1
  6. package/dist/ai.mjs +1008 -512
  7. package/dist/ai.mjs.map +1 -1
  8. package/dist/catalog.json +4 -4
  9. package/dist/{chunk-BKSXPNPQ.mjs → chunk-AYSFWUPK.mjs} +4 -3
  10. package/dist/chunk-AYSFWUPK.mjs.map +1 -0
  11. package/dist/chunk-B4NJJZFR.mjs +18 -0
  12. package/dist/chunk-B4NJJZFR.mjs.map +1 -0
  13. package/dist/{chunk-LVNCYP4J.mjs → chunk-CJBLJUWG.mjs} +5 -5
  14. package/dist/{chunk-LVNCYP4J.mjs.map → chunk-CJBLJUWG.mjs.map} +1 -1
  15. package/dist/{chunk-7WQXXEVR.mjs → chunk-ESVPQWHX.mjs} +5 -5
  16. package/dist/{chunk-7WQXXEVR.mjs.map → chunk-ESVPQWHX.mjs.map} +1 -1
  17. package/dist/{chunk-KRC2XOIG.mjs → chunk-I24QOHPU.mjs} +3 -3
  18. package/dist/{chunk-KRC2XOIG.mjs.map → chunk-I24QOHPU.mjs.map} +1 -1
  19. package/dist/{chunk-ZBJBQKJ2.mjs → chunk-IHUFOV7L.mjs} +4 -19
  20. package/dist/chunk-IHUFOV7L.mjs.map +1 -0
  21. package/dist/{chunk-AZIARTGX.mjs → chunk-M42TGYT6.mjs} +3 -3
  22. package/dist/{chunk-AZIARTGX.mjs.map → chunk-M42TGYT6.mjs.map} +1 -1
  23. package/dist/{chunk-45CGKJ7S.mjs → chunk-NDEZJKNY.mjs} +4 -4
  24. package/dist/{chunk-45CGKJ7S.mjs.map → chunk-NDEZJKNY.mjs.map} +1 -1
  25. package/dist/{chunk-BEZSQKPY.mjs → chunk-ONBCUWVI.mjs} +5 -4
  26. package/dist/chunk-ONBCUWVI.mjs.map +1 -0
  27. package/dist/{chunk-WM2VDYQA.mjs → chunk-REIJZDVZ.mjs} +4 -3
  28. package/dist/chunk-REIJZDVZ.mjs.map +1 -0
  29. package/dist/{chunk-2WF6KIGF.mjs → chunk-TB4CL25L.mjs} +9 -8
  30. package/dist/chunk-TB4CL25L.mjs.map +1 -0
  31. package/dist/chunk-VNCCIV6O.mjs +938 -0
  32. package/dist/chunk-VNCCIV6O.mjs.map +1 -0
  33. package/dist/{chunk-CGZZO4BX.mjs → chunk-VRHWDZ66.mjs} +5 -5
  34. package/dist/{chunk-CGZZO4BX.mjs.map → chunk-VRHWDZ66.mjs.map} +1 -1
  35. package/dist/{chunk-4DS3MKID.mjs → chunk-YSJOVBCD.mjs} +4 -4
  36. package/dist/{chunk-4DS3MKID.mjs.map → chunk-YSJOVBCD.mjs.map} +1 -1
  37. package/dist/geometry-2d.d.mts +2 -2
  38. package/dist/geometry-2d.d.ts +2 -2
  39. package/dist/geometry-2d.js +1383 -23
  40. package/dist/geometry-2d.js.map +1 -1
  41. package/dist/geometry-2d.mjs +6 -5
  42. package/dist/geometry-3d.d.mts +2 -2
  43. package/dist/geometry-3d.d.ts +2 -2
  44. package/dist/geometry-3d.js +2 -2
  45. package/dist/geometry-3d.js.map +1 -1
  46. package/dist/geometry-3d.mjs +5 -4
  47. package/dist/graph-2d.d.mts +2 -2
  48. package/dist/graph-2d.d.ts +2 -2
  49. package/dist/graph-2d.js +2 -2
  50. package/dist/graph-2d.js.map +1 -1
  51. package/dist/graph-2d.mjs +8 -7
  52. package/dist/{host-ZIQ77W33.mjs → host-A64ITWVX.mjs} +7 -6
  53. package/dist/host-A64ITWVX.mjs.map +1 -0
  54. package/dist/{host-EPZCNFLH.mjs → host-L7FMFZUW.mjs} +226 -29
  55. package/dist/host-L7FMFZUW.mjs.map +1 -0
  56. package/dist/{host-LKCMYEAV.mjs → host-QK53UYMD.mjs} +11 -10
  57. package/dist/host-QK53UYMD.mjs.map +1 -0
  58. package/dist/index.d.mts +3 -3
  59. package/dist/index.d.ts +3 -3
  60. package/dist/index.js +1414 -54
  61. package/dist/index.js.map +1 -1
  62. package/dist/index.mjs +18 -17
  63. package/dist/index.mjs.map +1 -1
  64. package/dist/latex.d.mts +2 -2
  65. package/dist/latex.d.ts +2 -2
  66. package/dist/render-3WTY7NZB.mjs +9 -0
  67. package/dist/{render-SA4JTOW3.mjs.map → render-3WTY7NZB.mjs.map} +1 -1
  68. package/dist/serialize-SRJVKYUG.mjs +8 -0
  69. package/dist/{serialize-JAVOU22E.mjs.map → serialize-SRJVKYUG.mjs.map} +1 -1
  70. package/dist/{types-vtvyKGAA.d.mts → types-DWRyCa2m.d.mts} +187 -2
  71. package/dist/{types-Crbefnfe.d.ts → types-DWRyCa2m.d.ts} +187 -2
  72. package/package.json +1 -1
  73. package/dist/chunk-2WF6KIGF.mjs.map +0 -1
  74. package/dist/chunk-BEZSQKPY.mjs.map +0 -1
  75. package/dist/chunk-BKSXPNPQ.mjs.map +0 -1
  76. package/dist/chunk-WM2VDYQA.mjs.map +0 -1
  77. package/dist/chunk-ZBJBQKJ2.mjs.map +0 -1
  78. package/dist/host-EPZCNFLH.mjs.map +0 -1
  79. package/dist/host-LKCMYEAV.mjs.map +0 -1
  80. package/dist/host-ZIQ77W33.mjs.map +0 -1
  81. package/dist/render-SA4JTOW3.mjs +0 -8
  82. package/dist/serialize-JAVOU22E.mjs +0 -7
  83. package/dist/types-DxlMPh-6.d.mts +0 -49
  84. package/dist/types-DxlMPh-6.d.ts +0 -49
package/dist/ai.js CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var zod = require('zod');
4
- var Anthropic = require('@anthropic-ai/sdk');
5
4
  var zodToJsonSchema = require('zod-to-json-schema');
5
+ var Anthropic = require('@anthropic-ai/sdk');
6
6
 
7
7
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
8
 
@@ -10,143 +10,628 @@ var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
10
10
 
11
11
  // src/stamps/geometry-2d/dsl/schema.ts
12
12
  var NameZ = zod.z.string().regex(/^[A-Za-z][A-Za-z0-9_'₀-₉]{0,11}$/);
13
- var DslPoint = zod.z.discriminatedUnion("kind", [
14
- zod.z.object({
13
+
14
+ // src/stamps/geometry-2d/dsl/kinds/_types.ts
15
+ function defineModule(m) {
16
+ return m;
17
+ }
18
+
19
+ // src/stamps/geometry-2d/dsl/kinds/_shared.ts
20
+ var POINT_BASE_FIELDS = {
21
+ visible: true,
22
+ locked: false,
23
+ layer: "default",
24
+ schemaVersion: 1
25
+ };
26
+ function emitPointObject(id, name, constraint) {
27
+ return {
28
+ id,
29
+ kind: "point",
30
+ label: name,
31
+ ...POINT_BASE_FIELDS,
32
+ attrs: { constraint }
33
+ };
34
+ }
35
+ function resolveTriangleVertices(ctx, vertices) {
36
+ return [ctx.resolveId(vertices[0]), ctx.resolveId(vertices[1]), ctx.resolveId(vertices[2])];
37
+ }
38
+ var SHAPE_BASE_FIELDS = {
39
+ visible: true,
40
+ locked: false,
41
+ layer: "default",
42
+ schemaVersion: 1
43
+ };
44
+
45
+ // src/stamps/geometry-2d/dsl/kinds/points/free.ts
46
+ var freeModule = defineModule({
47
+ kind: "free",
48
+ role: "point",
49
+ category: "points",
50
+ prefix: "p",
51
+ schema: zod.z.object({
15
52
  name: NameZ,
16
53
  kind: zod.z.literal("free"),
17
54
  x: zod.z.number().finite(),
18
55
  y: zod.z.number().finite()
19
56
  }),
20
- zod.z.object({
57
+ collectRefs: () => [],
58
+ emit: (e, ctx) => [{
59
+ role: "primary",
60
+ object: emitPointObject(ctx.resolveId(e.name), e.name, { kind: "free", x: e.x, y: e.y })
61
+ }]
62
+ });
63
+ var midpointModule = defineModule({
64
+ kind: "midpoint",
65
+ role: "point",
66
+ category: "points",
67
+ prefix: "p",
68
+ schema: zod.z.object({
21
69
  name: NameZ,
22
70
  kind: zod.z.literal("midpoint"),
23
71
  p1: NameZ,
24
72
  p2: NameZ
25
73
  }),
26
- zod.z.object({
74
+ collectRefs: (e) => [e.p1, e.p2],
75
+ emit: (e, ctx) => [{
76
+ role: "primary",
77
+ object: emitPointObject(
78
+ ctx.resolveId(e.name),
79
+ e.name,
80
+ { kind: "midpoint", p1: ctx.resolveId(e.p1), p2: ctx.resolveId(e.p2) }
81
+ )
82
+ }]
83
+ });
84
+ var onSegmentModule = defineModule({
85
+ kind: "onSegment",
86
+ role: "point",
87
+ category: "points",
88
+ prefix: "p",
89
+ schema: zod.z.object({
27
90
  name: NameZ,
28
91
  kind: zod.z.literal("onSegment"),
29
92
  segmentId: NameZ,
30
93
  t: zod.z.number().min(0).max(1)
31
94
  }),
32
- zod.z.object({
95
+ collectRefs: (e) => [e.segmentId],
96
+ emit: (e, ctx) => [{
97
+ role: "primary",
98
+ object: emitPointObject(
99
+ ctx.resolveId(e.name),
100
+ e.name,
101
+ { kind: "onSegment", segmentId: ctx.resolveId(e.segmentId), t: e.t }
102
+ )
103
+ }]
104
+ });
105
+ var onLineModule = defineModule({
106
+ kind: "onLine",
107
+ role: "point",
108
+ category: "points",
109
+ prefix: "p",
110
+ schema: zod.z.object({
33
111
  name: NameZ,
34
112
  kind: zod.z.literal("onLine"),
35
113
  lineId: NameZ,
36
114
  t: zod.z.number().finite()
37
115
  }),
38
- zod.z.object({
116
+ collectRefs: (e) => [e.lineId],
117
+ emit: (e, ctx) => [{
118
+ role: "primary",
119
+ object: emitPointObject(
120
+ ctx.resolveId(e.name),
121
+ e.name,
122
+ { kind: "onLine", lineId: ctx.resolveId(e.lineId), t: e.t }
123
+ )
124
+ }]
125
+ });
126
+ var onCircleModule = defineModule({
127
+ kind: "onCircle",
128
+ role: "point",
129
+ category: "points",
130
+ prefix: "p",
131
+ schema: zod.z.object({
39
132
  name: NameZ,
40
133
  kind: zod.z.literal("onCircle"),
41
134
  circleId: NameZ,
42
135
  theta: zod.z.number().finite()
43
136
  }),
44
- zod.z.object({
137
+ collectRefs: (e) => [e.circleId],
138
+ emit: (e, ctx) => [{
139
+ role: "primary",
140
+ object: emitPointObject(
141
+ ctx.resolveId(e.name),
142
+ e.name,
143
+ { kind: "onCircle", circleId: ctx.resolveId(e.circleId), theta: e.theta }
144
+ )
145
+ }]
146
+ });
147
+ var perpFootModule = defineModule({
148
+ kind: "perpFoot",
149
+ role: "point",
150
+ category: "points",
151
+ prefix: "p",
152
+ schema: zod.z.object({
45
153
  name: NameZ,
46
154
  kind: zod.z.literal("perpFoot"),
47
155
  from: NameZ,
48
156
  onLine: NameZ
49
157
  }),
50
- zod.z.object({
158
+ collectRefs: (e) => [e.from, e.onLine],
159
+ emit: (e, ctx) => [{
160
+ role: "primary",
161
+ object: emitPointObject(
162
+ ctx.resolveId(e.name),
163
+ e.name,
164
+ { kind: "perpFoot", from: ctx.resolveId(e.from), onLine: ctx.resolveId(e.onLine) }
165
+ )
166
+ }]
167
+ });
168
+ var circumcenterModule = defineModule({
169
+ kind: "circumcenter",
170
+ role: "point",
171
+ category: "points",
172
+ prefix: "p",
173
+ schema: zod.z.object({
51
174
  name: NameZ,
52
175
  kind: zod.z.literal("circumcenter"),
53
176
  vertices: zod.z.tuple([NameZ, NameZ, NameZ])
54
177
  }),
55
- zod.z.object({
178
+ collectRefs: (e) => [...e.vertices],
179
+ emit: (e, ctx) => [{
180
+ role: "primary",
181
+ object: emitPointObject(
182
+ ctx.resolveId(e.name),
183
+ e.name,
184
+ { kind: "circumcenter", vertices: resolveTriangleVertices(ctx, e.vertices) }
185
+ )
186
+ }]
187
+ });
188
+ var incenterModule = defineModule({
189
+ kind: "incenter",
190
+ role: "point",
191
+ category: "points",
192
+ prefix: "p",
193
+ schema: zod.z.object({
56
194
  name: NameZ,
57
195
  kind: zod.z.literal("incenter"),
58
196
  vertices: zod.z.tuple([NameZ, NameZ, NameZ])
59
197
  }),
60
- zod.z.object({
198
+ collectRefs: (e) => [...e.vertices],
199
+ emit: (e, ctx) => [{
200
+ role: "primary",
201
+ object: emitPointObject(
202
+ ctx.resolveId(e.name),
203
+ e.name,
204
+ { kind: "incenter", vertices: resolveTriangleVertices(ctx, e.vertices) }
205
+ )
206
+ }]
207
+ });
208
+ var centroidModule = defineModule({
209
+ kind: "centroid",
210
+ role: "point",
211
+ category: "points",
212
+ prefix: "p",
213
+ schema: zod.z.object({
61
214
  name: NameZ,
62
215
  kind: zod.z.literal("centroid"),
63
216
  vertices: zod.z.tuple([NameZ, NameZ, NameZ])
64
217
  }),
65
- zod.z.object({
218
+ collectRefs: (e) => [...e.vertices],
219
+ emit: (e, ctx) => [{
220
+ role: "primary",
221
+ object: emitPointObject(
222
+ ctx.resolveId(e.name),
223
+ e.name,
224
+ { kind: "centroid", vertices: resolveTriangleVertices(ctx, e.vertices) }
225
+ )
226
+ }]
227
+ });
228
+ var orthocenterModule = defineModule({
229
+ kind: "orthocenter",
230
+ role: "point",
231
+ category: "points",
232
+ prefix: "p",
233
+ schema: zod.z.object({
66
234
  name: NameZ,
67
235
  kind: zod.z.literal("orthocenter"),
68
236
  vertices: zod.z.tuple([NameZ, NameZ, NameZ])
69
237
  }),
70
- zod.z.object({
238
+ collectRefs: (e) => [...e.vertices],
239
+ emit: (e, ctx) => [{
240
+ role: "primary",
241
+ object: emitPointObject(
242
+ ctx.resolveId(e.name),
243
+ e.name,
244
+ { kind: "orthocenter", vertices: resolveTriangleVertices(ctx, e.vertices) }
245
+ )
246
+ }]
247
+ });
248
+ var intersectionModule = defineModule({
249
+ kind: "intersection",
250
+ role: "point",
251
+ category: "points",
252
+ prefix: "i",
253
+ schema: zod.z.object({
71
254
  name: NameZ,
72
255
  kind: zod.z.literal("intersection"),
73
256
  ref1: NameZ,
74
257
  ref2: NameZ,
75
258
  branch: zod.z.union([zod.z.literal(0), zod.z.literal(1)]).optional()
76
- })
77
- ]);
78
- var DslShape = zod.z.discriminatedUnion("kind", [
79
- zod.z.object({
259
+ }),
260
+ collectRefs: (e) => [e.ref1, e.ref2],
261
+ emit: (e, ctx) => {
262
+ const r1IsCircle = ctx.hintOf(e.ref1) === "circle";
263
+ const r2IsCircle = ctx.hintOf(e.ref2) === "circle";
264
+ let intersectKind;
265
+ if (r1IsCircle && r2IsCircle) intersectKind = "circleCircle";
266
+ else if (r1IsCircle || r2IsCircle) intersectKind = "lineCircle";
267
+ else intersectKind = "lineLine";
268
+ const attrs = {
269
+ kind: intersectKind,
270
+ ref1: ctx.resolveId(e.ref1),
271
+ ref2: ctx.resolveId(e.ref2)
272
+ };
273
+ if (intersectKind !== "lineLine") {
274
+ attrs.branch = e.branch ?? 0;
275
+ }
276
+ return [{
277
+ role: "primary",
278
+ object: {
279
+ id: ctx.resolveId(e.name),
280
+ kind: "intersection",
281
+ label: e.name,
282
+ ...POINT_BASE_FIELDS,
283
+ attrs
284
+ }
285
+ }];
286
+ }
287
+ });
288
+ var segmentModule = defineModule({
289
+ kind: "segment",
290
+ role: "segment",
291
+ category: "lines",
292
+ prefix: "s",
293
+ schema: zod.z.object({
80
294
  name: NameZ,
81
295
  kind: zod.z.literal("segment"),
82
296
  p1: NameZ,
83
297
  p2: NameZ
84
298
  }),
85
- zod.z.object({
299
+ collectRefs: (e) => [e.p1, e.p2],
300
+ emit: (e, ctx) => [{
301
+ role: "primary",
302
+ object: {
303
+ id: ctx.resolveId(e.name),
304
+ kind: "segment",
305
+ label: e.name,
306
+ ...SHAPE_BASE_FIELDS,
307
+ attrs: { p1: ctx.resolveId(e.p1), p2: ctx.resolveId(e.p2) }
308
+ }
309
+ }]
310
+ });
311
+ var lineModule = defineModule({
312
+ kind: "line",
313
+ role: "line",
314
+ category: "lines",
315
+ prefix: "l",
316
+ schema: zod.z.object({
86
317
  name: NameZ,
87
318
  kind: zod.z.literal("line"),
88
319
  p1: NameZ,
89
320
  p2: NameZ
90
321
  }),
91
- zod.z.object({
322
+ collectRefs: (e) => [e.p1, e.p2],
323
+ emit: (e, ctx) => [{
324
+ role: "primary",
325
+ object: {
326
+ id: ctx.resolveId(e.name),
327
+ kind: "line",
328
+ label: e.name,
329
+ ...SHAPE_BASE_FIELDS,
330
+ attrs: { p1: ctx.resolveId(e.p1), p2: ctx.resolveId(e.p2) }
331
+ }
332
+ }]
333
+ });
334
+ var rayModule = defineModule({
335
+ kind: "ray",
336
+ role: "ray",
337
+ category: "lines",
338
+ prefix: "r",
339
+ schema: zod.z.object({
92
340
  name: NameZ,
93
341
  kind: zod.z.literal("ray"),
94
342
  origin: NameZ,
95
343
  through: NameZ
96
344
  }),
97
- zod.z.object({
98
- name: NameZ,
99
- kind: zod.z.literal("polygon"),
100
- vertices: zod.z.array(NameZ).min(3)
101
- }),
102
- // Line constructions
103
- zod.z.object({
345
+ collectRefs: (e) => [e.origin, e.through],
346
+ emit: (e, ctx) => [{
347
+ role: "primary",
348
+ object: {
349
+ id: ctx.resolveId(e.name),
350
+ kind: "ray",
351
+ label: e.name,
352
+ ...SHAPE_BASE_FIELDS,
353
+ attrs: { origin: ctx.resolveId(e.origin), through: ctx.resolveId(e.through) }
354
+ }
355
+ }]
356
+ });
357
+ var perpendicularModule = defineModule({
358
+ kind: "perpendicular",
359
+ role: "lineConstruction",
360
+ category: "lines",
361
+ prefix: "l",
362
+ schema: zod.z.object({
104
363
  name: NameZ,
105
364
  kind: zod.z.literal("perpendicular"),
106
365
  throughPoint: NameZ,
107
366
  toLine: NameZ
108
367
  }),
109
- zod.z.object({
368
+ collectRefs: (e) => [e.throughPoint, e.toLine],
369
+ emit: (e, ctx) => [{
370
+ role: "primary",
371
+ object: {
372
+ id: ctx.resolveId(e.name),
373
+ kind: "line",
374
+ label: e.name,
375
+ ...SHAPE_BASE_FIELDS,
376
+ attrs: {
377
+ construction: {
378
+ kind: "perpendicular",
379
+ throughPoint: ctx.resolveId(e.throughPoint),
380
+ toLine: ctx.resolveId(e.toLine)
381
+ }
382
+ }
383
+ }
384
+ }]
385
+ });
386
+ var parallelModule = defineModule({
387
+ kind: "parallel",
388
+ role: "lineConstruction",
389
+ category: "lines",
390
+ prefix: "l",
391
+ schema: zod.z.object({
110
392
  name: NameZ,
111
393
  kind: zod.z.literal("parallel"),
112
394
  throughPoint: NameZ,
113
395
  toLine: NameZ
114
396
  }),
115
- zod.z.object({
397
+ collectRefs: (e) => [e.throughPoint, e.toLine],
398
+ emit: (e, ctx) => [{
399
+ role: "primary",
400
+ object: {
401
+ id: ctx.resolveId(e.name),
402
+ kind: "line",
403
+ label: e.name,
404
+ ...SHAPE_BASE_FIELDS,
405
+ attrs: {
406
+ construction: {
407
+ kind: "parallel",
408
+ throughPoint: ctx.resolveId(e.throughPoint),
409
+ toLine: ctx.resolveId(e.toLine)
410
+ }
411
+ }
412
+ }
413
+ }]
414
+ });
415
+ var perpBisectorModule = defineModule({
416
+ kind: "perpBisector",
417
+ role: "lineConstruction",
418
+ category: "lines",
419
+ prefix: "l",
420
+ schema: zod.z.object({
116
421
  name: NameZ,
117
422
  kind: zod.z.literal("perpBisector"),
118
423
  p1: NameZ,
119
424
  p2: NameZ
120
425
  }),
121
- zod.z.object({
426
+ collectRefs: (e) => [e.p1, e.p2],
427
+ emit: (e, ctx) => [{
428
+ role: "primary",
429
+ object: {
430
+ id: ctx.resolveId(e.name),
431
+ kind: "line",
432
+ label: e.name,
433
+ ...SHAPE_BASE_FIELDS,
434
+ attrs: {
435
+ construction: {
436
+ kind: "perpBisector",
437
+ p1: ctx.resolveId(e.p1),
438
+ p2: ctx.resolveId(e.p2)
439
+ }
440
+ }
441
+ }
442
+ }]
443
+ });
444
+ var angleBisectorModule = defineModule({
445
+ kind: "angleBisector",
446
+ role: "lineConstruction",
447
+ category: "lines",
448
+ prefix: "l",
449
+ schema: zod.z.object({
122
450
  name: NameZ,
123
451
  kind: zod.z.literal("angleBisector"),
124
452
  p1: NameZ,
125
453
  vertex: NameZ,
126
454
  p2: NameZ
127
455
  }),
128
- zod.z.object({
456
+ collectRefs: (e) => [e.p1, e.vertex, e.p2],
457
+ emit: (e, ctx) => [{
458
+ role: "primary",
459
+ object: {
460
+ id: ctx.resolveId(e.name),
461
+ kind: "line",
462
+ label: e.name,
463
+ ...SHAPE_BASE_FIELDS,
464
+ attrs: {
465
+ construction: {
466
+ kind: "angleBisector",
467
+ p1: ctx.resolveId(e.p1),
468
+ vertex: ctx.resolveId(e.vertex),
469
+ p2: ctx.resolveId(e.p2)
470
+ }
471
+ }
472
+ }
473
+ }]
474
+ });
475
+ var tangentModule = defineModule({
476
+ kind: "tangent",
477
+ role: "lineConstruction",
478
+ category: "lines",
479
+ prefix: "l",
480
+ schema: zod.z.object({
129
481
  name: NameZ,
130
482
  kind: zod.z.literal("tangent"),
131
483
  throughPoint: NameZ,
132
484
  toCircle: NameZ,
133
485
  branch: zod.z.union([zod.z.literal(0), zod.z.literal(1), zod.z.literal("on")]).optional()
134
486
  }),
135
- // Circle constructions
136
- zod.z.object({
487
+ collectRefs: (e) => [e.throughPoint, e.toCircle],
488
+ emit: (e, ctx) => {
489
+ const construction = {
490
+ kind: "tangent",
491
+ throughPoint: ctx.resolveId(e.throughPoint),
492
+ toCircle: ctx.resolveId(e.toCircle)
493
+ };
494
+ if (e.branch !== void 0) construction.branch = e.branch;
495
+ return [{
496
+ role: "primary",
497
+ object: {
498
+ id: ctx.resolveId(e.name),
499
+ kind: "line",
500
+ label: e.name,
501
+ ...SHAPE_BASE_FIELDS,
502
+ attrs: { construction }
503
+ }
504
+ }];
505
+ }
506
+ });
507
+ var polygonModule = defineModule({
508
+ kind: "polygon",
509
+ role: "polygon",
510
+ category: "polygons",
511
+ prefix: "poly",
512
+ schema: zod.z.object({
513
+ name: NameZ,
514
+ kind: zod.z.literal("polygon"),
515
+ vertices: zod.z.array(NameZ).min(3)
516
+ }),
517
+ collectRefs: (e) => [...e.vertices],
518
+ emit: (e, ctx) => [{
519
+ role: "primary",
520
+ object: {
521
+ id: ctx.resolveId(e.name),
522
+ kind: "polygon",
523
+ label: e.name,
524
+ ...SHAPE_BASE_FIELDS,
525
+ attrs: { vertices: e.vertices.map((v) => ctx.resolveId(v)) }
526
+ }
527
+ }]
528
+ });
529
+ var circleCPModule = defineModule({
530
+ kind: "circleCP",
531
+ role: "circle",
532
+ category: "circles",
533
+ prefix: "c",
534
+ schema: zod.z.object({
137
535
  name: NameZ,
138
536
  kind: zod.z.literal("circleCP"),
139
537
  center: NameZ,
140
538
  surfacePoint: NameZ
141
539
  }),
142
- zod.z.object({
540
+ collectRefs: (e) => [e.center, e.surfacePoint],
541
+ emit: (e, ctx) => [{
542
+ role: "primary",
543
+ object: {
544
+ id: ctx.resolveId(e.name),
545
+ kind: "circle",
546
+ label: e.name,
547
+ ...SHAPE_BASE_FIELDS,
548
+ attrs: { center: ctx.resolveId(e.center), surfacePoint: ctx.resolveId(e.surfacePoint) }
549
+ }
550
+ }]
551
+ });
552
+ var circle3Module = defineModule({
553
+ kind: "circle3",
554
+ role: "circle",
555
+ category: "circles",
556
+ prefix: "c",
557
+ schema: zod.z.object({
143
558
  name: NameZ,
144
559
  kind: zod.z.literal("circle3"),
145
560
  p1: NameZ,
146
561
  p2: NameZ,
147
562
  p3: NameZ
148
- })
149
- ]);
563
+ }),
564
+ collectRefs: (e) => [e.p1, e.p2, e.p3],
565
+ emit: (e, ctx) => [{
566
+ role: "primary",
567
+ object: {
568
+ id: ctx.resolveId(e.name),
569
+ kind: "circle",
570
+ label: e.name,
571
+ ...SHAPE_BASE_FIELDS,
572
+ attrs: {
573
+ construction: {
574
+ kind: "circumscribed",
575
+ p1: ctx.resolveId(e.p1),
576
+ p2: ctx.resolveId(e.p2),
577
+ p3: ctx.resolveId(e.p3)
578
+ }
579
+ }
580
+ }
581
+ }]
582
+ });
583
+
584
+ // src/stamps/geometry-2d/dsl/registry.ts
585
+ var ALL_MODULES = [
586
+ freeModule,
587
+ midpointModule,
588
+ onSegmentModule,
589
+ onLineModule,
590
+ onCircleModule,
591
+ perpFootModule,
592
+ circumcenterModule,
593
+ incenterModule,
594
+ centroidModule,
595
+ orthocenterModule,
596
+ intersectionModule,
597
+ segmentModule,
598
+ lineModule,
599
+ rayModule,
600
+ perpendicularModule,
601
+ parallelModule,
602
+ perpBisectorModule,
603
+ angleBisectorModule,
604
+ tangentModule,
605
+ polygonModule,
606
+ circleCPModule,
607
+ circle3Module
608
+ ];
609
+ var KIND_REGISTRY = new Map(ALL_MODULES.map((m) => [m.kind, m]));
610
+ var POINT_KINDS = new Set(
611
+ ALL_MODULES.filter((m) => m.role === "point").map((m) => m.kind)
612
+ );
613
+ var LINE_LIKE_SHAPE_KINDS = new Set(
614
+ ALL_MODULES.filter(
615
+ (m) => m.role === "segment" || m.role === "line" || m.role === "ray" || m.role === "lineConstruction"
616
+ ).map((m) => m.kind)
617
+ );
618
+ var CIRCLE_KINDS = new Set(
619
+ ALL_MODULES.filter((m) => m.role === "circle").map((m) => m.kind)
620
+ );
621
+ zod.z.discriminatedUnion(
622
+ "kind",
623
+ ALL_MODULES.map((m) => m.schema)
624
+ );
625
+
626
+ // src/stamps/geometry-2d/dsl/schema.ts
627
+ function asTuple(arr) {
628
+ if (arr.length < 2) throw new Error("schema: need at least 2 variants for discriminatedUnion");
629
+ return arr;
630
+ }
631
+ var pointSchemas = Array.from(KIND_REGISTRY.values()).filter((m) => POINT_KINDS.has(m.kind)).map((m) => m.schema);
632
+ var shapeSchemas = Array.from(KIND_REGISTRY.values()).filter((m) => !POINT_KINDS.has(m.kind)).map((m) => m.schema);
633
+ var DslPoint = zod.z.discriminatedUnion("kind", asTuple(pointSchemas));
634
+ var DslShape = zod.z.discriminatedUnion("kind", asTuple(shapeSchemas));
150
635
  var DslInput = zod.z.object({
151
636
  version: zod.z.literal(1),
152
637
  points: zod.z.array(DslPoint),
@@ -215,17 +700,6 @@ function buildSymbols(dsl) {
215
700
  function isPointLike(sym) {
216
701
  return !!sym && sym.role === "point";
217
702
  }
218
- var LINE_LIKE_SHAPE_KINDS = /* @__PURE__ */ new Set([
219
- "line",
220
- "segment",
221
- "ray",
222
- "perpendicular",
223
- "parallel",
224
- "perpBisector",
225
- "angleBisector",
226
- "tangent"
227
- ]);
228
- var CIRCLE_KINDS = /* @__PURE__ */ new Set(["circleCP", "circle3"]);
229
703
  function isLineLike(sym) {
230
704
  if (!sym || sym.role !== "shape") return false;
231
705
  return LINE_LIKE_SHAPE_KINDS.has(sym.entity.kind);
@@ -340,50 +814,9 @@ function validateRefs(dsl, symbols) {
340
814
  return { errors };
341
815
  }
342
816
  function collectRefs(entity) {
343
- if ("kind" in entity) {
344
- switch (entity.kind) {
345
- case "free":
346
- return [];
347
- case "midpoint":
348
- return [entity.p1, entity.p2];
349
- case "onSegment":
350
- return [entity.segmentId];
351
- case "onLine":
352
- return [entity.lineId];
353
- case "onCircle":
354
- return [entity.circleId];
355
- case "perpFoot":
356
- return [entity.from, entity.onLine];
357
- case "circumcenter":
358
- case "incenter":
359
- case "centroid":
360
- case "orthocenter":
361
- return [...entity.vertices];
362
- case "intersection":
363
- return [entity.ref1, entity.ref2];
364
- case "segment":
365
- case "line":
366
- return [entity.p1, entity.p2];
367
- case "ray":
368
- return [entity.origin, entity.through];
369
- case "polygon":
370
- return [...entity.vertices];
371
- case "perpendicular":
372
- case "parallel":
373
- return [entity.throughPoint, entity.toLine];
374
- case "perpBisector":
375
- return [entity.p1, entity.p2];
376
- case "angleBisector":
377
- return [entity.p1, entity.vertex, entity.p2];
378
- case "tangent":
379
- return [entity.throughPoint, entity.toCircle];
380
- case "circleCP":
381
- return [entity.center, entity.surfacePoint];
382
- case "circle3":
383
- return [entity.p1, entity.p2, entity.p3];
384
- }
385
- }
386
- return [];
817
+ const mod = KIND_REGISTRY.get(entity.kind);
818
+ if (!mod) throw new Error(`collectRefs: no registry entry for kind "${entity.kind}"`);
819
+ return mod.collectRefs(entity);
387
820
  }
388
821
 
389
822
  // src/stamps/geometry-2d/dsl/transpile/cycles.ts
@@ -441,229 +874,45 @@ function detectCycles(symbols) {
441
874
  }
442
875
 
443
876
  // src/stamps/geometry-2d/dsl/transpile/ids.ts
444
- function prefixFor(sym) {
445
- if (sym.role === "point") {
446
- const p = sym.entity;
447
- return p.kind === "intersection" ? "i" : "p";
448
- }
449
- const s = sym.entity;
450
- switch (s.kind) {
451
- case "segment":
452
- return "s";
453
- case "ray":
454
- return "r";
455
- case "polygon":
456
- return "poly";
457
- case "circleCP":
458
- case "circle3":
459
- return "c";
460
- // line + 5 line-constructions all share 'l'
461
- case "line":
462
- case "perpendicular":
463
- case "parallel":
464
- case "perpBisector":
465
- case "angleBisector":
466
- case "tangent":
467
- return "l";
468
- }
469
- }
470
877
  function assignIds(symbols) {
471
- const counters = { p: 0, i: 0, s: 0, l: 0, r: 0, poly: 0, c: 0 };
878
+ const counters = /* @__PURE__ */ new Map();
472
879
  const ids = /* @__PURE__ */ new Map();
473
880
  for (const [name, sym] of symbols.entries()) {
474
- const prefix = prefixFor(sym);
475
- counters[prefix] += 1;
476
- ids.set(name, `${prefix}${counters[prefix]}`);
881
+ const mod = KIND_REGISTRY.get(sym.entity.kind);
882
+ if (!mod) throw new Error(`assignIds: no registry entry for kind "${sym.entity.kind}"`);
883
+ const prefix = mod.prefix;
884
+ counters.set(prefix, (counters.get(prefix) ?? 0) + 1);
885
+ ids.set(name, `${prefix}${counters.get(prefix)}`);
477
886
  }
478
887
  return ids;
479
888
  }
480
889
 
481
- // src/stamps/geometry-2d/dsl/transpile/emitPoint.ts
482
- function resolveId(ids, name) {
483
- const id = ids.get(name);
484
- if (!id) throw new Error(`emitPoint: id not assigned for "${name}"`);
485
- return id;
486
- }
487
- function emitPoint(p, ids, kindHints) {
488
- const baseId = resolveId(ids, p.name);
489
- const baseFields = {
490
- label: p.name,
491
- visible: true,
492
- locked: false,
493
- layer: "default",
494
- schemaVersion: 1
495
- };
496
- if (p.kind === "intersection") {
497
- const r1Hint = kindHints.get(p.ref1);
498
- const r2Hint = kindHints.get(p.ref2);
499
- const r1IsCircle = r1Hint === "circle";
500
- const r2IsCircle = r2Hint === "circle";
501
- let intersectKind;
502
- if (r1IsCircle && r2IsCircle) intersectKind = "circleCircle";
503
- else if (r1IsCircle || r2IsCircle) intersectKind = "lineCircle";
504
- else intersectKind = "lineLine";
505
- const attrs = {
506
- kind: intersectKind,
507
- ref1: resolveId(ids, p.ref1),
508
- ref2: resolveId(ids, p.ref2)
509
- };
510
- if (intersectKind !== "lineLine") {
511
- attrs.branch = p.branch ?? 0;
512
- }
513
- return {
514
- id: baseId,
515
- kind: "intersection",
516
- ...baseFields,
517
- attrs
518
- };
519
- }
520
- let constraint;
521
- switch (p.kind) {
522
- case "free":
523
- constraint = { kind: "free", x: p.x, y: p.y };
524
- break;
525
- case "midpoint":
526
- constraint = { kind: "midpoint", p1: resolveId(ids, p.p1), p2: resolveId(ids, p.p2) };
527
- break;
528
- case "onSegment":
529
- constraint = { kind: "onSegment", segmentId: resolveId(ids, p.segmentId), t: p.t };
530
- break;
531
- case "onLine":
532
- constraint = { kind: "onLine", lineId: resolveId(ids, p.lineId), t: p.t };
533
- break;
534
- case "onCircle":
535
- constraint = { kind: "onCircle", circleId: resolveId(ids, p.circleId), theta: p.theta };
536
- break;
537
- case "perpFoot":
538
- constraint = { kind: "perpFoot", from: resolveId(ids, p.from), onLine: resolveId(ids, p.onLine) };
539
- break;
540
- case "circumcenter":
541
- case "incenter":
542
- case "centroid":
543
- case "orthocenter":
544
- constraint = {
545
- kind: p.kind,
546
- vertices: [resolveId(ids, p.vertices[0]), resolveId(ids, p.vertices[1]), resolveId(ids, p.vertices[2])]
547
- };
548
- break;
549
- }
550
- return {
551
- id: baseId,
552
- kind: "point",
553
- ...baseFields,
554
- attrs: { constraint }
555
- };
556
- }
557
-
558
- // src/stamps/geometry-2d/dsl/transpile/emitShape.ts
559
- function r(ids, name) {
560
- const id = ids.get(name);
561
- if (!id) throw new Error(`emitShape: id not assigned for "${name}"`);
562
- return id;
563
- }
564
- function emitShape(s, ids) {
565
- const id = r(ids, s.name);
566
- const base = {
567
- label: s.name,
568
- visible: true,
569
- locked: false,
570
- layer: "default",
571
- schemaVersion: 1
572
- };
573
- switch (s.kind) {
574
- case "segment":
575
- return { id, kind: "segment", ...base, attrs: { p1: r(ids, s.p1), p2: r(ids, s.p2) } };
576
- case "line":
577
- return { id, kind: "line", ...base, attrs: { p1: r(ids, s.p1), p2: r(ids, s.p2) } };
578
- case "ray":
579
- return { id, kind: "ray", ...base, attrs: { origin: r(ids, s.origin), through: r(ids, s.through) } };
580
- case "polygon":
581
- return { id, kind: "polygon", ...base, attrs: { vertices: s.vertices.map((v) => r(ids, v)) } };
582
- case "perpendicular":
583
- case "parallel":
584
- return {
585
- id,
586
- kind: "line",
587
- ...base,
588
- attrs: { construction: { kind: s.kind, throughPoint: r(ids, s.throughPoint), toLine: r(ids, s.toLine) } }
589
- };
590
- case "perpBisector":
591
- return {
592
- id,
593
- kind: "line",
594
- ...base,
595
- attrs: { construction: { kind: "perpBisector", p1: r(ids, s.p1), p2: r(ids, s.p2) } }
596
- };
597
- case "angleBisector":
598
- return {
599
- id,
600
- kind: "line",
601
- ...base,
602
- attrs: { construction: { kind: "angleBisector", p1: r(ids, s.p1), vertex: r(ids, s.vertex), p2: r(ids, s.p2) } }
603
- };
604
- case "tangent": {
605
- const construction = {
606
- kind: "tangent",
607
- throughPoint: r(ids, s.throughPoint),
608
- toCircle: r(ids, s.toCircle)
609
- };
610
- if (s.branch !== void 0) construction.branch = s.branch;
611
- return { id, kind: "line", ...base, attrs: { construction } };
612
- }
613
- case "circleCP":
614
- return {
615
- id,
616
- kind: "circle",
617
- ...base,
618
- attrs: { center: r(ids, s.center), surfacePoint: r(ids, s.surfacePoint) }
619
- };
620
- case "circle3":
621
- return {
622
- id,
623
- kind: "circle",
624
- ...base,
625
- attrs: { construction: { kind: "circumscribed", p1: r(ids, s.p1), p2: r(ids, s.p2), p3: r(ids, s.p3) } }
626
- };
627
- }
628
- }
629
-
630
890
  // src/stamps/geometry-2d/dsl/transpile.ts
631
891
  function hintOf(entity) {
632
- if ("kind" in entity) {
633
- switch (entity.kind) {
634
- case "free":
635
- case "midpoint":
636
- case "onSegment":
637
- case "onLine":
638
- case "onCircle":
639
- case "perpFoot":
640
- case "circumcenter":
641
- case "incenter":
642
- case "centroid":
643
- case "orthocenter":
644
- case "intersection":
645
- return "point";
646
- case "segment":
647
- return "segment";
648
- case "line":
649
- return "line";
650
- case "ray":
651
- return "ray";
652
- case "polygon":
653
- return "point";
654
- // not used as ref target in MVP
655
- case "perpendicular":
656
- case "parallel":
657
- case "perpBisector":
658
- case "angleBisector":
659
- case "tangent":
660
- return "lineConstruction";
661
- case "circleCP":
662
- case "circle3":
663
- return "circle";
892
+ const mod = KIND_REGISTRY.get(entity.kind);
893
+ if (!mod) throw new Error(`hintOf: no registry entry for kind "${entity.kind}"`);
894
+ return mod.role === "polygon" ? "point" : mod.role;
895
+ }
896
+ function buildEmitContext(ids, kindHints) {
897
+ const auxCounters = /* @__PURE__ */ new Map();
898
+ return {
899
+ resolveId(name) {
900
+ const id = ids.get(name);
901
+ if (!id) throw new Error(`emit: id not assigned for "${name}"`);
902
+ return id;
903
+ },
904
+ hintOf(name) {
905
+ const hint = kindHints.get(name);
906
+ if (!hint) throw new Error(`emit: hint not assigned for "${name}"`);
907
+ return hint;
908
+ },
909
+ mintAuxId(parentName, suffix) {
910
+ const key = `${parentName}.${suffix}`;
911
+ auxCounters.set(key, (auxCounters.get(key) ?? 0) + 1);
912
+ const seq = auxCounters.get(key);
913
+ return `aux_${parentName}_${suffix}${seq}`;
664
914
  }
665
- }
666
- return "point";
915
+ };
667
916
  }
668
917
  function transpile(dslRaw) {
669
918
  const parsed = DslInput.safeParse(dslRaw);
@@ -686,16 +935,18 @@ function transpile(dslRaw) {
686
935
  }
687
936
  const objects = {};
688
937
  const order = [];
689
- for (const p of dsl.points) {
690
- const obj = emitPoint(p, ids, kindHints);
691
- objects[obj.id] = obj;
692
- order.push(obj.id);
693
- }
694
- for (const s of dsl.shapes) {
695
- const obj = emitShape(s, ids);
696
- objects[obj.id] = obj;
697
- order.push(obj.id);
698
- }
938
+ const ctx = buildEmitContext(ids, kindHints);
939
+ const emitEntity = (entity) => {
940
+ const mod = KIND_REGISTRY.get(entity.kind);
941
+ if (!mod) throw new Error(`emit: no registry entry for kind "${entity.kind}"`);
942
+ const emitted = mod.emit(entity, ctx);
943
+ for (const ent of emitted) {
944
+ objects[ent.object.id] = ent.object;
945
+ order.push(ent.object.id);
946
+ }
947
+ };
948
+ for (const p of dsl.points) emitEntity(p);
949
+ for (const s of dsl.shapes) emitEntity(s);
699
950
  const empty = createEmptyState("2d");
700
951
  const state = {
701
952
  objects,
@@ -705,20 +956,29 @@ function transpile(dslRaw) {
705
956
  };
706
957
  return { ok: true, state };
707
958
  }
708
- async function callProvider(args) {
709
- const client = new Anthropic__default.default({ apiKey: args.apiKey });
710
- const resp = await client.messages.create(
711
- {
712
- model: args.model,
713
- max_tokens: args.maxTokens,
714
- system: args.system,
715
- tools: args.tools,
716
- tool_choice: args.toolChoice,
717
- messages: args.messages
718
- },
719
- args.signal ? { signal: args.signal } : void 0
720
- );
721
- return resp;
959
+ var FigureEnvelopeZ = zod.z.object({
960
+ decision: zod.z.enum(["build", "refuse"]),
961
+ // figure: DslInput khi build; bỏ qua khi refuse.
962
+ figure: DslInput.optional(),
963
+ // reason: lý do từ chối (Việt) khi refuse; bỏ qua khi build.
964
+ reason: zod.z.string().optional()
965
+ }).refine(
966
+ (e) => e.decision === "build" ? e.figure != null : e.reason != null && e.reason.length > 0,
967
+ {
968
+ message: "decision=build c\u1EA7n `figure`; decision=refuse c\u1EA7n `reason` kh\xF4ng r\u1ED7ng"
969
+ }
970
+ );
971
+ function envelopeJsonSchema() {
972
+ return zodToJsonSchema.zodToJsonSchema(FigureEnvelopeZ, {
973
+ target: "jsonSchema7",
974
+ $refStrategy: "none"
975
+ });
976
+ }
977
+ function envelopeBuildDsl(env) {
978
+ if (env.decision !== "build" || env.figure == null) {
979
+ throw new Error("envelopeBuildDsl: envelope kh\xF4ng ph\u1EA3i decision=build ho\u1EB7c thi\u1EBFu figure");
980
+ }
981
+ return env.figure;
722
982
  }
723
983
 
724
984
  // src/stamps/geometry-2d/dsl/fixtures/triangle-equilateral.ts
@@ -886,62 +1146,267 @@ var fixture9 = {
886
1146
  }
887
1147
  };
888
1148
 
1149
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-angle-bisector.ts
1150
+ var fixture10 = {
1151
+ problem: "Tam gi\xE1c ABC, AD l\xE0 ph\xE2n gi\xE1c g\xF3c A (D thu\u1ED9c BC).",
1152
+ dsl: {
1153
+ version: 1,
1154
+ points: [
1155
+ { name: "A", kind: "free", x: 0, y: 3 },
1156
+ { name: "B", kind: "free", x: -2, y: 0 },
1157
+ { name: "C", kind: "free", x: 3, y: 0 },
1158
+ // D = giao của đường phân giác góc A với BC. KHÔNG dùng segment AD
1159
+ // vì AD lại tham chiếu D → cycle.
1160
+ { name: "D", kind: "intersection", ref1: "bisA", ref2: "BC" }
1161
+ ],
1162
+ shapes: [
1163
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
1164
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
1165
+ // Đường phân giác (line construction), chứ không phải segment.
1166
+ { name: "bisA", kind: "angleBisector", p1: "B", vertex: "A", p2: "C" },
1167
+ // Sau khi đã có D, mới dựng được segment AD.
1168
+ { name: "AD", kind: "segment", p1: "A", p2: "D" }
1169
+ ]
1170
+ }
1171
+ };
1172
+
1173
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-median-altitude.ts
1174
+ var fixture11 = {
1175
+ problem: "Tam gi\xE1c ABC, M l\xE0 trung \u0111i\u1EC3m BC v\xE0 AH l\xE0 \u0111\u01B0\u1EDDng cao xu\u1ED1ng BC.",
1176
+ dsl: {
1177
+ version: 1,
1178
+ points: [
1179
+ { name: "A", kind: "free", x: 1, y: 3 },
1180
+ { name: "B", kind: "free", x: -2, y: 0 },
1181
+ { name: "C", kind: "free", x: 3, y: 0 },
1182
+ // M = trung điểm B và C. KHÔNG dùng perpFoot ở đây — đó là H.
1183
+ { name: "M", kind: "midpoint", p1: "B", p2: "C" },
1184
+ // H = chân đường vuông góc từ A xuống cạnh BC.
1185
+ { name: "H", kind: "perpFoot", from: "A", onLine: "BC" }
1186
+ ],
1187
+ shapes: [
1188
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
1189
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
1190
+ { name: "AM", kind: "segment", p1: "A", p2: "M" },
1191
+ { name: "AH", kind: "segment", p1: "A", p2: "H" }
1192
+ ]
1193
+ }
1194
+ };
1195
+
1196
+ // src/stamps/geometry-2d/dsl/fixtures/trapezoid.ts
1197
+ var fixture12 = {
1198
+ problem: "H\xECnh thang ABCD c\xF3 AB song song CD.",
1199
+ dsl: {
1200
+ version: 1,
1201
+ points: [
1202
+ { name: "A", kind: "free", x: 0, y: 0 },
1203
+ { name: "B", kind: "free", x: 4, y: 0 },
1204
+ { name: "C", kind: "free", x: 5, y: 3 },
1205
+ { name: "D", kind: "free", x: -1, y: 3 }
1206
+ ],
1207
+ shapes: [
1208
+ { name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] }
1209
+ ]
1210
+ }
1211
+ };
1212
+
1213
+ // src/stamps/geometry-2d/dsl/fixtures/rhombus.ts
1214
+ var fixture13 = {
1215
+ problem: "H\xECnh thoi ABCD, hai \u0111\u01B0\u1EDDng ch\xE9o AC, BD c\u1EAFt nhau t\u1EA1i O.",
1216
+ dsl: {
1217
+ version: 1,
1218
+ points: [
1219
+ { name: "A", kind: "free", x: 0, y: 2 },
1220
+ { name: "B", kind: "free", x: 3, y: 0 },
1221
+ { name: "C", kind: "free", x: 0, y: -2 },
1222
+ { name: "D", kind: "free", x: -3, y: 0 },
1223
+ { name: "O", kind: "intersection", ref1: "AC", ref2: "BD" }
1224
+ ],
1225
+ shapes: [
1226
+ { name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] },
1227
+ { name: "AC", kind: "segment", p1: "A", p2: "C" },
1228
+ { name: "BD", kind: "segment", p1: "B", p2: "D" }
1229
+ ]
1230
+ }
1231
+ };
1232
+
1233
+ // src/stamps/geometry-2d/dsl/fixtures/right-triangle-altitude.ts
1234
+ var fixture14 = {
1235
+ problem: "Tam gi\xE1c ABC vu\xF4ng t\u1EA1i A, AH l\xE0 \u0111\u01B0\u1EDDng cao xu\u1ED1ng c\u1EA1nh huy\u1EC1n BC.",
1236
+ dsl: {
1237
+ version: 1,
1238
+ points: [
1239
+ { name: "A", kind: "free", x: 0, y: 0 },
1240
+ { name: "B", kind: "free", x: 0, y: 3 },
1241
+ { name: "C", kind: "free", x: 4, y: 0 },
1242
+ { name: "H", kind: "perpFoot", from: "A", onLine: "BC" }
1243
+ ],
1244
+ shapes: [
1245
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
1246
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
1247
+ { name: "AH", kind: "segment", p1: "A", p2: "H" }
1248
+ ]
1249
+ }
1250
+ };
1251
+
1252
+ // src/stamps/geometry-2d/dsl/fixtures/tangent-from-point.ts
1253
+ var fixture15 = {
1254
+ problem: "T\u1EEB \u0111i\u1EC3m M ngo\xE0i \u0111\u01B0\u1EDDng tr\xF2n (O), k\u1EBB hai ti\u1EBFp tuy\u1EBFn t\u1EDBi (O).",
1255
+ dsl: {
1256
+ version: 1,
1257
+ points: [
1258
+ { name: "O", kind: "free", x: 0, y: 0 },
1259
+ // P là điểm trên đường tròn dùng để định bán kính.
1260
+ { name: "P", kind: "free", x: 2, y: 0 },
1261
+ { name: "M", kind: "free", x: 5, y: 0 }
1262
+ ],
1263
+ shapes: [
1264
+ { name: "k", kind: "circleCP", center: "O", surfacePoint: "P" },
1265
+ { name: "t1", kind: "tangent", throughPoint: "M", toCircle: "k", branch: 0 },
1266
+ { name: "t2", kind: "tangent", throughPoint: "M", toCircle: "k", branch: 1 }
1267
+ ]
1268
+ }
1269
+ };
1270
+
1271
+ // src/stamps/geometry-2d/dsl/fixtures/internal-external-bisector.ts
1272
+ var fixture16 = {
1273
+ 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.",
1274
+ dsl: {
1275
+ version: 1,
1276
+ points: [
1277
+ { name: "A", kind: "free", x: 0, y: 3 },
1278
+ { name: "B", kind: "free", x: -2, y: 0 },
1279
+ { name: "C", kind: "free", x: 3, y: 0 }
1280
+ ],
1281
+ shapes: [
1282
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
1283
+ { name: "bisIn", kind: "angleBisector", p1: "B", vertex: "A", p2: "C" },
1284
+ // Phân giác ngoài = vuông góc với phân giác trong tại đỉnh A.
1285
+ { name: "bisExt", kind: "perpendicular", throughPoint: "A", toLine: "bisIn" }
1286
+ ]
1287
+ }
1288
+ };
1289
+
889
1290
  // src/stamps/geometry-2d/ai/prompt.ts
890
- var FIXTURES = [fixture, fixture2, fixture3, fixture4, fixture5, fixture6, fixture7, fixture8, fixture9];
1291
+ var FIXTURES = [
1292
+ fixture,
1293
+ fixture2,
1294
+ fixture3,
1295
+ fixture4,
1296
+ fixture5,
1297
+ fixture6,
1298
+ fixture7,
1299
+ fixture10,
1300
+ fixture11,
1301
+ fixture14,
1302
+ fixture16,
1303
+ fixture15,
1304
+ fixture8,
1305
+ fixture13,
1306
+ fixture12,
1307
+ fixture9
1308
+ ];
891
1309
  function buildSystemPrompt() {
892
1310
  const examples = FIXTURES.map(
893
1311
  (f, i) => `### V\xED d\u1EE5 ${i + 1}
894
1312
  **\u0110\u1EC1:** ${f.problem}
895
- **DSL:**
896
- \`\`\`json
897
- ${JSON.stringify(f.dsl, null, 2)}
898
- \`\`\``
1313
+ **Output:**
1314
+ ${JSON.stringify({ decision: "build", figure: f.dsl }, null, 2)}`
899
1315
  ).join("\n\n");
900
1316
  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.
901
1317
 
902
1318
  ## Nhi\u1EC7m v\u1EE5
903
- \u0110\u1ECDc \u0111\u1EC1 b\xE0i ti\u1EBFng Vi\u1EC7t \u2192 emit DSL JSON m\xF4 t\u1EA3 h\xECnh. H\u1EC7 th\u1ED1ng s\u1EBD render h\xECnh t\u1EEB DSL.
1319
+ \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.
1320
+
1321
+ ## Output format (CH\u1EC8 JSON, kh\xF4ng markdown, kh\xF4ng text kh\xE1c)
1322
+ { "decision": "build", "figure": { /* DSL */ } }
1323
+ ho\u1EB7c
1324
+ { "decision": "refuse", "reason": "l\xFD do ti\u1EBFng Vi\u1EC7t" }
904
1325
 
905
1326
  ## Quy t\u1EAFc
906
- 1. D\xF9ng tool \`build_figure\` khi v\u1EBD \u0111\u01B0\u1EE3c. D\xF9ng tool \`refuse\` khi kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c ho\u1EB7c \u0111\u1EC1 ngo\xE0i ph\u1EA1m vi (3D, l\u01B0\u1EE3ng gi\xE1c, ph\xE9p bi\u1EBFn h\xECnh l\u1EDBp 11+, \u0111\u1EA1i s\u1ED1).
907
- 2. \u01AFu ti\xEAn derived points (midpoint, perpFoot, circumcenter, ...) thay v\xEC t\u1EF1 compute to\u1EA1 \u0111\u1ED9.
908
- 3. Anchor (free) ch\u1EC9 d\xF9ng cho \u0111i\u1EC3m g\u1ED1c (th\u01B0\u1EDDng A, B, C c\u1EE7a tam gi\xE1c). \u0110\u1EB7t coord h\u1EE3p l\xFD quanh g\u1ED1c (-5..5).
909
- 4. M\u1ECDi \u0111i\u1EC3m + h\xECnh ph\u1EA3i c\xF3 \`name\` (label "A", "M", "O\u2081", ...). Tham chi\u1EBFu b\u1EB1ng name, kh\xF4ng ph\u1EA3i id.
910
- 5. Tam gi\xE1c: emit c\u1EA3 \`polygon\` (v\u1EBD vi\u1EC1n) + segment/\u0111\u01B0\u1EDDng ph\u1EE5 ri\xEAng n\u1EBFu \u0111\u1EC1 y\xEAu c\u1EA7u (\u0111\u01B0\u1EDDng cao, trung tuy\u1EBFn).
911
- 6. \u0110\u01B0\u1EDDng tr\xF2n (O; R) cho tr\u01B0\u1EDBc b\xE1n k\xEDnh s\u1ED1: emit anchor 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).
912
- 7. N\u1EBFu \u0111\u1EC1 m\u01A1 h\u1ED3: ch\u1ECDn case ph\u1ED5 bi\u1EBFn nh\u1EA5t, kh\xF4ng h\u1ECFi l\u1EA1i.
1327
+ 1. V\u1EBD \u0111\u01B0\u1EE3c \u2192 decision="build" + figure \u0111\u1EA7y \u0111\u1EE7 DSL.
1328
+ 2. \u0110\u1EC1 ngo\xE0i ph\u1EA1m vi \u2192 decision="refuse" + reason ti\u1EBFng Vi\u1EC7t c\u1EE5 th\u1EC3. Bao g\u1ED3m:
1329
+ - 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).
1330
+ - H\xECnh 3D, l\u1EADp th\u1EC3, kh\xF4ng gian.
1331
+ - Ph\xE9p bi\u1EBFn h\xECnh affine / t\u1ECBnh ti\u1EBFn / v\u1ECB t\u1EF1 / quay (l\u1EDBp 11+).
1332
+ - \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").
1333
+ 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).
1334
+ 4. **\u0110\u01B0\u1EDDng cao**: d\xF9ng \`perpFoot\` cho ch\xE2n, KH\xD4NG free anchor.
1335
+ 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).
1336
+ 6. **\u0110\u01B0\u1EDDng tr\xF2n ngo\u1EA1i ti\u1EBFp**: d\xF9ng \`circle3\` (3 \u0111i\u1EC3m), kh\xF4ng ph\u1EA3i \`polygon\`.
1337
+ 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\`.
1338
+ 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).
1339
+ 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".
1340
+
1341
+ ## Anti-pattern (B\u1EAET BU\u1ED8C tr\xE1nh)
1342
+ - **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).
1343
+ - **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).
1344
+ - **\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.
913
1345
 
914
1346
  ## Primitives s\u1EB5n c\xF3
915
1347
  **Points:** free, midpoint, onSegment, onLine, onCircle, perpFoot, circumcenter, incenter, centroid, orthocenter, intersection
916
1348
  **Shapes:** segment, line, ray, polygon, perpendicular, parallel, perpBisector, angleBisector, tangent, circleCP, circle3
917
1349
 
918
- ## 9 v\xED d\u1EE5
1350
+ ## ${FIXTURES.length} v\xED d\u1EE5
919
1351
  ${examples}
920
1352
 
921
- ## Khi kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c
922
- G\u1ECDi \`refuse\` v\u1EDBi \`reason\` ti\u1EBFng Vi\u1EC7t gi\u1EA3i th\xEDch c\u1EE5 th\u1EC3 (vd: "\u0110\u1EC1 thu\u1ED9c l\u1EDBp 11, ngo\xE0i ph\u1EA1m vi MVP" ho\u1EB7c "\u0110\u1EC1 kh\xF4ng r\xF5 v\u1ECB tr\xED \u0111i\u1EC3m M").`;
1353
+ Tr\u1EA3 v\u1EC1 CH\u1EC8 1 JSON object \u0111\xFAng schema. Kh\xF4ng c\xF3 l\u1EDDi d\u1EABn, kh\xF4ng markdown fence.`;
923
1354
  }
924
- var BUILD_FIGURE_TOOL = {
925
- name: "build_figure",
926
- description: "V\u1EBD h\xECnh h\u1ECDc 2D theo \u0111\u1EC1 b\xE0i. Emit DSL JSON m\xF4 t\u1EA3 c\xE1c \u0111i\u1EC3m v\xE0 h\xECnh.",
927
- input_schema: zodToJsonSchema.zodToJsonSchema(DslInput, {
928
- target: "jsonSchema7",
929
- $refStrategy: "none"
930
- })
931
- };
932
- var RefuseInputZ = zod.z.object({
933
- reason: zod.z.string().min(1).describe("L\xFD do kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c (ti\u1EBFng Vi\u1EC7t)")
934
- });
935
- var REFUSE_TOOL = {
936
- name: "refuse",
937
- description: "T\u1EEB ch\u1ED1i khi kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c ho\u1EB7c \u0111\u1EC1 ngo\xE0i ph\u1EA1m vi (3D, l\u01B0\u1EE3ng gi\xE1c, l\u1EDBp 11+).",
938
- input_schema: zodToJsonSchema.zodToJsonSchema(RefuseInputZ, { target: "jsonSchema7" })
1355
+ var TOOL_NAME = "emit_figure_envelope";
1356
+ var AnthropicProvider = class {
1357
+ constructor(opts) {
1358
+ this.opts = opts;
1359
+ this.name = "anthropic";
1360
+ this.defaultModel = "claude-opus-4-7";
1361
+ if (!opts.apiKey) throw new Error("AnthropicProvider: apiKey b\u1EAFt bu\u1ED9c");
1362
+ }
1363
+ async call(req) {
1364
+ const enableCaching = this.opts.enableCaching !== false;
1365
+ const systemBlock = enableCaching ? { type: "text", text: req.systemPrompt, cache_control: { type: "ephemeral" } } : { type: "text", text: req.systemPrompt };
1366
+ const tool = {
1367
+ name: TOOL_NAME,
1368
+ 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.',
1369
+ input_schema: req.schema
1370
+ };
1371
+ const client = new Anthropic__default.default({ apiKey: this.opts.apiKey });
1372
+ let resp;
1373
+ try {
1374
+ resp = await client.messages.create(
1375
+ {
1376
+ model: req.model,
1377
+ max_tokens: req.maxTokens,
1378
+ system: [systemBlock],
1379
+ tools: [tool],
1380
+ tool_choice: { type: "tool", name: TOOL_NAME },
1381
+ messages: [{ role: "user", content: req.userPrompt }]
1382
+ },
1383
+ req.signal ? { signal: req.signal } : void 0
1384
+ );
1385
+ } catch (e) {
1386
+ const err = e;
1387
+ return {
1388
+ kind: "error",
1389
+ message: err.message ?? "L\u1ED7i g\u1ECDi Anthropic API",
1390
+ ...err.status !== void 0 ? { status: err.status } : {}
1391
+ };
1392
+ }
1393
+ const usage = toUsage(resp.usage);
1394
+ const toolUse = resp.content.find((c) => c.type === "tool_use");
1395
+ if (!toolUse || toolUse.type !== "tool_use") {
1396
+ return {
1397
+ kind: "error",
1398
+ message: "Claude kh\xF4ng g\u1ECDi tool. stop_reason=" + resp.stop_reason
1399
+ };
1400
+ }
1401
+ if (toolUse.name !== TOOL_NAME) {
1402
+ return {
1403
+ kind: "error",
1404
+ message: `Tool kh\xF4ng x\xE1c \u0111\u1ECBnh: "${toolUse.name}"`
1405
+ };
1406
+ }
1407
+ return { kind: "json", data: toolUse.input, usage };
1408
+ }
939
1409
  };
940
- var TOOLS = [BUILD_FIGURE_TOOL, REFUSE_TOOL];
941
-
942
- // src/stamps/geometry-2d/ai/buildFigure.ts
943
- var DEFAULT_MODEL = "claude-opus-4-7";
944
- var DEFAULT_MAX_TOKENS = 8192;
945
1410
  function toUsage(u) {
946
1411
  return {
947
1412
  inputTokens: u.input_tokens,
@@ -950,87 +1415,742 @@ function toUsage(u) {
950
1415
  cacheCreationTokens: u.cache_creation_input_tokens ?? 0
951
1416
  };
952
1417
  }
953
- async function generateFigure(problem, opts) {
954
- if (!opts.apiKey) {
955
- return { ok: false, reason: "api_error", message: "apiKey b\u1EAFt bu\u1ED9c" };
1418
+
1419
+ // src/stamps/geometry-2d/ai/providers/ollama.ts
1420
+ var DEFAULT_BASE_URL = "http://localhost:11434";
1421
+ var DEFAULT_MODEL = "gemma3:4b";
1422
+ var OllamaProvider = class {
1423
+ constructor(opts = {}) {
1424
+ this.name = "ollama";
1425
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
1426
+ this.defaultModel = opts.defaultModel ?? DEFAULT_MODEL;
1427
+ this.fetchImpl = opts.fetchImpl ?? null;
1428
+ }
1429
+ resolveFetch() {
1430
+ if (this.fetchImpl) return this.fetchImpl;
1431
+ if (typeof fetch === "undefined") {
1432
+ throw new Error(
1433
+ "OllamaProvider: global `fetch` kh\xF4ng kh\u1EA3 d\u1EE5ng. Truy\u1EC1n `fetchImpl` qua constructor ho\u1EB7c ch\u1EA1y \u1EDF Node 18+ / browser."
1434
+ );
1435
+ }
1436
+ return fetch;
1437
+ }
1438
+ async call(req) {
1439
+ const body = {
1440
+ model: req.model,
1441
+ messages: [
1442
+ { role: "system", content: req.systemPrompt },
1443
+ { role: "user", content: req.userPrompt }
1444
+ ],
1445
+ // Ollama v0.5+ structured outputs: model bị constrain emit JSON đúng schema.
1446
+ format: req.schema,
1447
+ stream: false,
1448
+ options: {
1449
+ // num_predict ≈ max_tokens
1450
+ num_predict: req.maxTokens,
1451
+ // temperature thấp cho output deterministic hơn (DSL cần consistent).
1452
+ temperature: 0.2
1453
+ }
1454
+ };
1455
+ let resp;
1456
+ let doFetch;
1457
+ try {
1458
+ doFetch = this.resolveFetch();
1459
+ } catch (e) {
1460
+ const err = e;
1461
+ return { kind: "error", message: err.message ?? "fetch kh\xF4ng kh\u1EA3 d\u1EE5ng" };
1462
+ }
1463
+ try {
1464
+ resp = await doFetch(`${this.baseUrl}/api/chat`, {
1465
+ method: "POST",
1466
+ headers: { "content-type": "application/json" },
1467
+ body: JSON.stringify(body),
1468
+ signal: req.signal
1469
+ });
1470
+ } catch (e) {
1471
+ const err = e;
1472
+ return {
1473
+ kind: "error",
1474
+ message: err.message ?? `Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Ollama \u1EDF ${this.baseUrl}`
1475
+ };
1476
+ }
1477
+ if (!resp.ok) {
1478
+ let detail = "";
1479
+ try {
1480
+ detail = await resp.text();
1481
+ } catch {
1482
+ }
1483
+ return {
1484
+ kind: "error",
1485
+ message: `Ollama HTTP ${resp.status}: ${detail || resp.statusText}`,
1486
+ status: resp.status
1487
+ };
1488
+ }
1489
+ let json;
1490
+ try {
1491
+ json = await resp.json();
1492
+ } catch (e) {
1493
+ const err = e;
1494
+ return { kind: "error", message: "Ollama response kh\xF4ng ph\u1EA3i JSON: " + (err.message ?? "?") };
1495
+ }
1496
+ const content = json.message?.content?.trim();
1497
+ if (!content) {
1498
+ return { kind: "error", message: "Ollama tr\u1EA3 message.content r\u1ED7ng" };
1499
+ }
1500
+ let data;
1501
+ try {
1502
+ data = JSON.parse(content);
1503
+ } catch (e) {
1504
+ const err = e;
1505
+ return {
1506
+ kind: "error",
1507
+ message: "Ollama content kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: " + (err.message ?? "?")
1508
+ };
1509
+ }
1510
+ const usage = {
1511
+ inputTokens: json.prompt_eval_count ?? 0,
1512
+ outputTokens: json.eval_count ?? 0,
1513
+ cacheReadTokens: 0,
1514
+ cacheCreationTokens: 0
1515
+ };
1516
+ return { kind: "json", data, usage };
1517
+ }
1518
+ };
1519
+
1520
+ // src/stamps/geometry-2d/ai/providers/index.ts
1521
+ function selectProvider(opts = {}) {
1522
+ if (opts.provider) return opts.provider;
1523
+ if (opts.apiKey) {
1524
+ return new AnthropicProvider({
1525
+ apiKey: opts.apiKey,
1526
+ enableCaching: opts.enableCaching
1527
+ });
1528
+ }
1529
+ const env = opts.env ?? readEnv();
1530
+ const wanted = (env.WHITEBOARD_AI_PROVIDER ?? "ollama").toLowerCase();
1531
+ if (wanted === "anthropic") {
1532
+ const key = env.ANTHROPIC_API_KEY;
1533
+ if (!key) {
1534
+ throw new Error(
1535
+ "selectProvider: WHITEBOARD_AI_PROVIDER=anthropic nh\u01B0ng thi\u1EBFu env ANTHROPIC_API_KEY"
1536
+ );
1537
+ }
1538
+ return new AnthropicProvider({ apiKey: key, enableCaching: opts.enableCaching });
1539
+ }
1540
+ if (wanted === "ollama") {
1541
+ return new OllamaProvider({
1542
+ baseUrl: opts.ollamaBaseUrl ?? env.OLLAMA_BASE_URL,
1543
+ defaultModel: opts.ollamaDefaultModel ?? env.OLLAMA_DEFAULT_MODEL
1544
+ });
1545
+ }
1546
+ throw new Error(`selectProvider: WHITEBOARD_AI_PROVIDER="${wanted}" kh\xF4ng h\u1EE3p l\u1EC7 (anthropic|ollama)`);
1547
+ }
1548
+ function readEnv() {
1549
+ if (typeof process !== "undefined" && process.env) {
1550
+ return process.env;
956
1551
  }
1552
+ return {};
1553
+ }
1554
+
1555
+ // src/stamps/geometry-2d/ai/buildFigure.ts
1556
+ var DEFAULT_MAX_TOKENS = 8192;
1557
+ async function generateFigure(problem, opts = {}) {
957
1558
  if (!problem || !problem.trim()) {
958
1559
  return { ok: false, reason: "api_error", message: "\u0110\u1EC1 b\xE0i r\u1ED7ng" };
959
1560
  }
960
- const systemText = buildSystemPrompt();
961
- const enableCaching = opts.enableCaching !== false;
962
- const systemBlock = enableCaching ? { type: "text", text: systemText, cache_control: { type: "ephemeral" } } : { type: "text", text: systemText };
963
- let response;
1561
+ let provider;
964
1562
  try {
965
- response = await callProvider({
966
- apiKey: opts.apiKey,
967
- model: opts.model ?? DEFAULT_MODEL,
968
- maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
969
- system: [systemBlock],
970
- tools: TOOLS,
971
- toolChoice: { type: "any" },
972
- messages: [{ role: "user", content: problem }],
973
- signal: opts.signal
974
- });
1563
+ provider = selectProvider(opts);
975
1564
  } catch (e) {
976
1565
  const err = e;
1566
+ return { ok: false, reason: "api_error", message: err.message ?? "Kh\xF4ng ch\u1ECDn \u0111\u01B0\u1EE3c provider" };
1567
+ }
1568
+ const systemPrompt = buildSystemPrompt();
1569
+ const schema = envelopeJsonSchema();
1570
+ const out = await provider.call({
1571
+ systemPrompt,
1572
+ userPrompt: problem,
1573
+ schema,
1574
+ model: opts.model ?? provider.defaultModel,
1575
+ maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
1576
+ signal: opts.signal
1577
+ });
1578
+ if (out.kind === "error") {
977
1579
  return {
978
1580
  ok: false,
979
1581
  reason: "api_error",
980
- message: err.message ?? "L\u1ED7i g\u1ECDi Claude API",
981
- ...err.status !== void 0 ? { status: err.status } : {}
1582
+ message: out.message,
1583
+ ...out.status !== void 0 ? { status: out.status } : {},
1584
+ provider: provider.name
982
1585
  };
983
1586
  }
984
- const usage = toUsage(response.usage);
985
- const toolUse = response.content.find((c) => c.type === "tool_use");
986
- if (!toolUse || toolUse.type !== "tool_use") {
987
- const text = response.content.find((c) => c.type === "text");
988
- const textStr = text?.type === "text" ? text.text : "(empty)";
1587
+ const usage = toUsage2(out.usage);
1588
+ const parsed = FigureEnvelopeZ.safeParse(out.data);
1589
+ if (!parsed.success) {
989
1590
  return {
990
1591
  ok: false,
991
1592
  reason: "parse_error",
992
- message: "AI kh\xF4ng g\u1ECDi tool n\xE0o. Response: " + textStr,
993
- raw: response.content,
994
- usage
1593
+ message: "Envelope kh\xF4ng kh\u1EDBp schema: " + parsed.error.issues.map((i) => i.message).join("; "),
1594
+ raw: out.data,
1595
+ usage,
1596
+ provider: provider.name
995
1597
  };
996
1598
  }
997
- if (toolUse.name === "refuse") {
998
- const input = toolUse.input;
1599
+ const env = parsed.data;
1600
+ if (env.decision === "refuse") {
999
1601
  return {
1000
1602
  ok: false,
1001
1603
  reason: "refused",
1002
- message: input.reason ?? "AI t\u1EEB ch\u1ED1i kh\xF4ng n\xEAu l\xFD do",
1003
- usage
1604
+ message: env.reason ?? "AI t\u1EEB ch\u1ED1i kh\xF4ng n\xEAu l\xFD do",
1605
+ usage,
1606
+ provider: provider.name
1607
+ };
1608
+ }
1609
+ const dsl = envelopeBuildDsl(env);
1610
+ const tResult = transpile(dsl);
1611
+ if (!tResult.ok) {
1612
+ return {
1613
+ ok: false,
1614
+ reason: "transpile_error",
1615
+ message: "DSL t\u1EEB AI kh\xF4ng h\u1EE3p l\u1EC7",
1616
+ errors: tResult.errors,
1617
+ dsl,
1618
+ usage,
1619
+ provider: provider.name
1620
+ };
1621
+ }
1622
+ return {
1623
+ ok: true,
1624
+ state: tResult.state,
1625
+ dsl,
1626
+ usage,
1627
+ provider: provider.name
1628
+ };
1629
+ }
1630
+ function toUsage2(u) {
1631
+ return {
1632
+ inputTokens: u?.inputTokens ?? 0,
1633
+ outputTokens: u?.outputTokens ?? 0,
1634
+ cacheReadTokens: u?.cacheReadTokens ?? 0,
1635
+ cacheCreationTokens: u?.cacheCreationTokens ?? 0
1636
+ };
1637
+ }
1638
+
1639
+ // src/stamps/geometry-2d/ai/handleGenerateFigure.ts
1640
+ var DEFAULT_MAX_ATTEMPTS = 2;
1641
+ async function handleGenerateFigure(input, opts = {}) {
1642
+ const { onResult, maxAttempts: rawMax, ...generateOpts } = opts;
1643
+ const maxAttempts = clampAttempts(rawMax ?? DEFAULT_MAX_ATTEMPTS);
1644
+ let lastResult = null;
1645
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1646
+ const result = await generateFigure(input.problem, generateOpts);
1647
+ lastResult = result;
1648
+ if (onResult) {
1649
+ try {
1650
+ onResult(result, attempt);
1651
+ } catch {
1652
+ }
1653
+ }
1654
+ if (result.ok) {
1655
+ return { ok: true, state: result.state };
1656
+ }
1657
+ if (result.reason === "transpile_error" && attempt < maxAttempts) {
1658
+ continue;
1659
+ }
1660
+ break;
1661
+ }
1662
+ return mapErrorToUi(lastResult);
1663
+ }
1664
+ function clampAttempts(n) {
1665
+ if (!Number.isFinite(n)) return DEFAULT_MAX_ATTEMPTS;
1666
+ return Math.max(1, Math.min(5, Math.floor(n)));
1667
+ }
1668
+ function mapErrorToUi(result) {
1669
+ if (result.ok) return { ok: true, state: result.state };
1670
+ switch (result.reason) {
1671
+ case "refused":
1672
+ return { ok: false, message: result.message };
1673
+ case "parse_error":
1674
+ return {
1675
+ ok: false,
1676
+ 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."
1677
+ };
1678
+ case "transpile_error":
1679
+ return {
1680
+ ok: false,
1681
+ 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."
1682
+ };
1683
+ case "api_error":
1684
+ default:
1685
+ return { ok: false, message: result.message };
1686
+ }
1687
+ }
1688
+ var FigureRefineEnvelopeZ = zod.z.object({
1689
+ decision: zod.z.enum(["add", "replace", "refuse"]),
1690
+ figure: DslInput.optional(),
1691
+ reason: zod.z.string().optional()
1692
+ }).refine(
1693
+ (e) => e.decision === "refuse" ? e.reason != null && e.reason.length > 0 : e.figure != null,
1694
+ {
1695
+ message: "decision=add/replace c\u1EA7n `figure`; decision=refuse c\u1EA7n `reason` kh\xF4ng r\u1ED7ng"
1696
+ }
1697
+ );
1698
+ function refineEnvelopeJsonSchema() {
1699
+ return zodToJsonSchema.zodToJsonSchema(FigureRefineEnvelopeZ, {
1700
+ target: "jsonSchema7",
1701
+ $refStrategy: "none"
1702
+ });
1703
+ }
1704
+
1705
+ // src/stamps/geometry-2d/ai/refineFixtures.ts
1706
+ var triangleABC = {
1707
+ version: 1,
1708
+ points: [
1709
+ { name: "A", kind: "free", x: 0, y: 3 },
1710
+ { name: "B", kind: "free", x: -2, y: 0 },
1711
+ { name: "C", kind: "free", x: 3, y: 0 }
1712
+ ],
1713
+ shapes: [{ name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }]
1714
+ };
1715
+ var rightTriangleAtA = {
1716
+ version: 1,
1717
+ points: [
1718
+ { name: "A", kind: "free", x: 0, y: 0 },
1719
+ { name: "B", kind: "free", x: 4, y: 0 },
1720
+ { name: "C", kind: "free", x: 0, y: 3 }
1721
+ ],
1722
+ shapes: [{ name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }]
1723
+ };
1724
+ var parallelogramABCD = {
1725
+ version: 1,
1726
+ points: [
1727
+ { name: "A", kind: "free", x: -2, y: 0 },
1728
+ { name: "B", kind: "free", x: 3, y: 0 },
1729
+ { name: "C", kind: "free", x: 4, y: 2 },
1730
+ { name: "D", kind: "free", x: -1, y: 2 }
1731
+ ],
1732
+ shapes: [{ name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] }]
1733
+ };
1734
+ var circleOnA = {
1735
+ version: 1,
1736
+ points: [
1737
+ { name: "O", kind: "free", x: 0, y: 0 },
1738
+ { name: "A", kind: "free", x: 3, y: 0 }
1739
+ ],
1740
+ shapes: [{ name: "omega", kind: "circleCP", center: "O", surfacePoint: "A" }]
1741
+ };
1742
+ var REFINE_FIXTURES = [
1743
+ {
1744
+ name: "triangle-add-midpoint",
1745
+ currentDsl: triangleABC,
1746
+ instruction: "Th\xEAm trung \u0111i\u1EC3m M c\u1EE7a BC",
1747
+ expectedEnvelope: {
1748
+ decision: "add",
1749
+ figure: {
1750
+ version: 1,
1751
+ points: [{ name: "M", kind: "midpoint", p1: "B", p2: "C" }],
1752
+ shapes: [{ name: "AM", kind: "segment", p1: "A", p2: "M" }]
1753
+ }
1754
+ }
1755
+ },
1756
+ {
1757
+ name: "triangle-add-altitude",
1758
+ currentDsl: triangleABC,
1759
+ instruction: "D\u1EF1ng \u0111\u01B0\u1EDDng cao AH xu\u1ED1ng BC",
1760
+ expectedEnvelope: {
1761
+ decision: "add",
1762
+ figure: {
1763
+ version: 1,
1764
+ points: [{ name: "H", kind: "perpFoot", from: "A", onLine: "BC_line" }],
1765
+ shapes: [
1766
+ { name: "BC_line", kind: "line", p1: "B", p2: "C" },
1767
+ { name: "AH", kind: "segment", p1: "A", p2: "H" }
1768
+ ]
1769
+ }
1770
+ }
1771
+ },
1772
+ {
1773
+ name: "triangle-add-circumcircle",
1774
+ currentDsl: triangleABC,
1775
+ instruction: "V\u1EBD \u0111\u01B0\u1EDDng tr\xF2n ngo\u1EA1i ti\u1EBFp tam gi\xE1c ABC",
1776
+ expectedEnvelope: {
1777
+ decision: "add",
1778
+ figure: {
1779
+ version: 1,
1780
+ points: [{ name: "O", kind: "circumcenter", vertices: ["A", "B", "C"] }],
1781
+ shapes: [{ name: "omega", kind: "circle3", p1: "A", p2: "B", p3: "C" }]
1782
+ }
1783
+ }
1784
+ },
1785
+ {
1786
+ name: "right-triangle-add-centroid",
1787
+ currentDsl: rightTriangleAtA,
1788
+ instruction: "Th\xEAm tr\u1ECDng t\xE2m G c\u1EE7a tam gi\xE1c",
1789
+ expectedEnvelope: {
1790
+ decision: "add",
1791
+ figure: {
1792
+ version: 1,
1793
+ points: [{ name: "G", kind: "centroid", vertices: ["A", "B", "C"] }],
1794
+ shapes: []
1795
+ }
1796
+ }
1797
+ },
1798
+ {
1799
+ name: "parallelogram-add-diagonals",
1800
+ currentDsl: parallelogramABCD,
1801
+ instruction: "V\u1EBD hai \u0111\u01B0\u1EDDng ch\xE9o AC, BD v\xE0 giao \u0111i\u1EC3m O",
1802
+ expectedEnvelope: {
1803
+ decision: "add",
1804
+ figure: {
1805
+ version: 1,
1806
+ points: [{ name: "O", kind: "intersection", ref1: "AC", ref2: "BD" }],
1807
+ shapes: [
1808
+ { name: "AC", kind: "segment", p1: "A", p2: "C" },
1809
+ { name: "BD", kind: "segment", p1: "B", p2: "D" }
1810
+ ]
1811
+ }
1812
+ }
1813
+ },
1814
+ {
1815
+ name: "circle-add-tangent",
1816
+ currentDsl: circleOnA,
1817
+ instruction: "K\u1EBB ti\u1EBFp tuy\u1EBFn t\u1EA1i A c\u1EE7a \u0111\u01B0\u1EDDng tr\xF2n",
1818
+ expectedEnvelope: {
1819
+ decision: "add",
1820
+ figure: {
1821
+ version: 1,
1822
+ points: [],
1823
+ shapes: [{ name: "t", kind: "tangent", throughPoint: "A", toCircle: "omega" }]
1824
+ }
1825
+ }
1826
+ },
1827
+ {
1828
+ name: "triangle-replace-equilateral",
1829
+ currentDsl: triangleABC,
1830
+ instruction: "B\u1ECF tam gi\xE1c n\xE0y, v\u1EBD tam gi\xE1c \u0111\u1EC1u ABC thay v\xE0o",
1831
+ expectedEnvelope: {
1832
+ decision: "replace",
1833
+ figure: {
1834
+ version: 1,
1835
+ points: [
1836
+ { name: "A", kind: "free", x: 0, y: 2 },
1837
+ { name: "B", kind: "free", x: -1.732, y: -1 },
1838
+ { name: "C", kind: "free", x: 1.732, y: -1 }
1839
+ ],
1840
+ shapes: [{ name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }]
1841
+ }
1842
+ }
1843
+ },
1844
+ {
1845
+ name: "triangle-replace-rhombus",
1846
+ currentDsl: triangleABC,
1847
+ instruction: "\u0110\u1ED5i sang h\xECnh thoi ABCD",
1848
+ expectedEnvelope: {
1849
+ decision: "replace",
1850
+ figure: {
1851
+ version: 1,
1852
+ points: [
1853
+ { name: "A", kind: "free", x: -2, y: 0 },
1854
+ { name: "B", kind: "free", x: 0, y: 1.5 },
1855
+ { name: "C", kind: "free", x: 2, y: 0 },
1856
+ { name: "D", kind: "free", x: 0, y: -1.5 }
1857
+ ],
1858
+ shapes: [{ name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] }]
1859
+ }
1860
+ }
1861
+ },
1862
+ {
1863
+ name: "refuse-calculation",
1864
+ currentDsl: triangleABC,
1865
+ instruction: "T\xEDnh di\u1EC7n t\xEDch tam gi\xE1c ABC",
1866
+ expectedEnvelope: {
1867
+ decision: "refuse",
1868
+ reason: "Y\xEAu c\u1EA7u t\xEDnh to\xE1n, kh\xF4ng ph\u1EA3i v\u1EBD h\xECnh."
1869
+ }
1870
+ },
1871
+ {
1872
+ name: "refuse-3d",
1873
+ currentDsl: triangleABC,
1874
+ instruction: "V\u1EBD h\xECnh ch\xF3p SABC v\u1EDBi S n\u1EB1m tr\xEAn tam gi\xE1c",
1875
+ expectedEnvelope: {
1876
+ decision: "refuse",
1877
+ reason: "H\xECnh 3D ngo\xE0i ph\u1EA1m vi geometry-2d."
1878
+ }
1879
+ }
1880
+ ];
1881
+ var REFINE_PROMPT_FIXTURES = REFINE_FIXTURES.slice(0, 8);
1882
+
1883
+ // src/stamps/geometry-2d/ai/refinePrompt.ts
1884
+ function namesOf(dsl) {
1885
+ return {
1886
+ points: dsl.points.map((p) => p.name),
1887
+ shapes: dsl.shapes.map((s) => s.name)
1888
+ };
1889
+ }
1890
+ function buildRefineSystemPrompt(currentDsl) {
1891
+ const names = namesOf(currentDsl);
1892
+ const examples = REFINE_PROMPT_FIXTURES.map((f, i) => {
1893
+ const env = f.expectedEnvelope;
1894
+ return `### V\xED d\u1EE5 ${i + 1}
1895
+ **H\xECnh hi\u1EC7n t\u1EA1i:**
1896
+ ${JSON.stringify(f.currentDsl, null, 2)}
1897
+ **Y\xEAu c\u1EA7u ch\u1EC9nh s\u1EEDa:** ${f.instruction}
1898
+ **Output:**
1899
+ ${JSON.stringify(env, null, 2)}`;
1900
+ }).join("\n\n");
1901
+ 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.
1902
+
1903
+ ## H\xECnh hi\u1EC7n t\u1EA1i (DSL JSON)
1904
+ ${JSON.stringify(currentDsl, null, 2)}
1905
+
1906
+ ## T\xEAn \u0111\xE3 d\xF9ng (KH\xD4NG \u0111\u01B0\u1EE3c redefine)
1907
+ points: ${names.points.join(", ") || "(ch\u01B0a c\xF3)"}
1908
+ shapes: ${names.shapes.join(", ") || "(ch\u01B0a c\xF3)"}
1909
+
1910
+ ## Nhi\u1EC7m v\u1EE5
1911
+ \u0110\u1ECDc Y\xCAU C\u1EA6U CH\u1EC8NH S\u1EECA \u2192 emit JSON envelope \u0111\xFAng 1 trong 3 d\u1EA1ng:
1912
+
1913
+ { "decision": "add", "figure": <DSL ch\u1EC9 ch\u1EE9a entity M\u1EDAI> }
1914
+ { "decision": "replace", "figure": <DSL ho\xE0n ch\u1EC9nh thay th\u1EBF h\xECnh c\u0169> }
1915
+ { "decision": "refuse", "reason": "l\xFD do ti\u1EBFng Vi\u1EC7t" }
1916
+
1917
+ ## Khi n\xE0o d\xF9ng decision n\xE0o?
1918
+ - **"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").
1919
+ \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.
1920
+ - **"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").
1921
+ \u2192 figure \u0111\u1EA7y \u0111\u1EE7 nh\u01B0 prompt m\u1EDBi (gi\u1ED1ng mode build).
1922
+ - **"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).
1923
+
1924
+ ## Quy t\u1EAFc decision=add
1925
+ 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).
1926
+ 2. \u01AFU TI\xCAN derived points: midpoint, perpFoot, intersection, circumcenter, incenter, centroid, orthocenter.
1927
+ 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.
1928
+ 4. KH\xD4NG copy l\u1EA1i entity c\u0169 v\xE0o figure delta (delta ch\u1EC9 ch\u1EE9a c\xE1i M\u1EDAI).
1929
+
1930
+ ## Anti-pattern (B\u1EAET BU\u1ED8C tr\xE1nh)
1931
+ - KH\xD4NG redefine t\xEAn \u0111\xE3 d\xF9ng (A, B, C \u0111\xE3 c\xF3 \u2192 KH\xD4NG \u0111\u1EB7t l\u1EA1i).
1932
+ - 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.
1933
+ - KH\xD4NG emit add v\u1EDBi figure ch\u1EE9a c\u1EA3 entity c\u0169 (\u0111\xF3 l\xE0 replace).
1934
+
1935
+ ## Primitives s\u1EB5n c\xF3
1936
+ **Points:** free, midpoint, onSegment, onLine, onCircle, perpFoot, circumcenter, incenter, centroid, orthocenter, intersection
1937
+ **Shapes:** segment, line, ray, polygon, perpendicular, parallel, perpBisector, angleBisector, tangent, circleCP, circle3
1938
+
1939
+ ## ${REFINE_PROMPT_FIXTURES.length} v\xED d\u1EE5
1940
+
1941
+ ${examples}
1942
+
1943
+ Tr\u1EA3 v\u1EC1 CH\u1EC8 1 JSON object \u0111\xFAng schema. Kh\xF4ng c\xF3 l\u1EDDi d\u1EABn, kh\xF4ng markdown fence.`;
1944
+ }
1945
+
1946
+ // src/stamps/geometry-2d/ai/buildFigureDelta.ts
1947
+ var DEFAULT_MAX_TOKENS2 = 8192;
1948
+ async function generateFigureDelta(input, opts = {}) {
1949
+ const { problem, currentDsl } = input;
1950
+ if (!problem || !problem.trim()) {
1951
+ return { ok: false, reason: "api_error", message: "\u0110\u1EC1 b\xE0i r\u1ED7ng" };
1952
+ }
1953
+ let provider;
1954
+ try {
1955
+ provider = selectProvider(opts);
1956
+ } catch (e) {
1957
+ const err = e;
1958
+ return { ok: false, reason: "api_error", message: err.message ?? "Kh\xF4ng ch\u1ECDn \u0111\u01B0\u1EE3c provider" };
1959
+ }
1960
+ const systemPrompt = buildRefineSystemPrompt(currentDsl);
1961
+ const schema = refineEnvelopeJsonSchema();
1962
+ const out = await provider.call({
1963
+ systemPrompt,
1964
+ userPrompt: problem,
1965
+ schema,
1966
+ model: opts.model ?? provider.defaultModel,
1967
+ maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS2,
1968
+ signal: opts.signal
1969
+ });
1970
+ if (out.kind === "error") {
1971
+ return {
1972
+ ok: false,
1973
+ reason: "api_error",
1974
+ message: out.message,
1975
+ ...out.status !== void 0 ? { status: out.status } : {},
1976
+ provider: provider.name
1004
1977
  };
1005
1978
  }
1006
- if (toolUse.name !== "build_figure") {
1979
+ const usage = toUsage3(out.usage);
1980
+ const parsed = FigureRefineEnvelopeZ.safeParse(out.data);
1981
+ if (!parsed.success) {
1007
1982
  return {
1008
1983
  ok: false,
1009
1984
  reason: "parse_error",
1010
- message: `Tool kh\xF4ng x\xE1c \u0111\u1ECBnh: "${toolUse.name}"`,
1011
- raw: toolUse,
1012
- usage
1985
+ message: "Envelope kh\xF4ng kh\u1EDBp schema: " + parsed.error.issues.map((i) => i.message).join("; "),
1986
+ raw: out.data,
1987
+ usage,
1988
+ provider: provider.name
1013
1989
  };
1014
1990
  }
1015
- const tResult = transpile(toolUse.input);
1016
- if (!tResult.ok) {
1991
+ const env = parsed.data;
1992
+ if (env.decision === "refuse") {
1017
1993
  return {
1018
1994
  ok: false,
1019
- reason: "transpile_error",
1020
- message: "DSL t\u1EEB AI kh\xF4ng h\u1EE3p l\u1EC7",
1021
- errors: tResult.errors,
1022
- dsl: toolUse.input,
1023
- usage
1995
+ reason: "refused",
1996
+ message: env.reason ?? "AI t\u1EEB ch\u1ED1i kh\xF4ng n\xEAu l\xFD do",
1997
+ usage,
1998
+ provider: provider.name
1999
+ };
2000
+ }
2001
+ if (env.decision === "replace") {
2002
+ const figure = env.figure;
2003
+ const tResult2 = transpile(figure);
2004
+ if (!tResult2.ok) {
2005
+ return liftTranspileError(tResult2.errors, figure, usage, provider.name);
2006
+ }
2007
+ return {
2008
+ ok: true,
2009
+ state: tResult2.state,
2010
+ mergedDsl: figure,
2011
+ mode: "replace",
2012
+ usage,
2013
+ provider: provider.name
1024
2014
  };
1025
2015
  }
2016
+ const delta = env.figure;
2017
+ const merged = {
2018
+ version: 1,
2019
+ points: [...currentDsl.points, ...delta.points],
2020
+ shapes: [...currentDsl.shapes, ...delta.shapes]
2021
+ };
2022
+ const tResult = transpile(merged);
2023
+ if (!tResult.ok) {
2024
+ return liftTranspileError(tResult.errors, merged, usage, provider.name);
2025
+ }
1026
2026
  return {
1027
2027
  ok: true,
1028
2028
  state: tResult.state,
1029
- dsl: toolUse.input,
1030
- usage
2029
+ mergedDsl: merged,
2030
+ mode: "add",
2031
+ usage,
2032
+ provider: provider.name
2033
+ };
2034
+ }
2035
+ function liftTranspileError(errors, dsl, usage, providerName) {
2036
+ const dupes = errors.filter((e) => e.code === "DUPLICATE_NAME");
2037
+ if (dupes.length > 0) {
2038
+ const collisions = Array.from(new Set(dupes.flatMap((e) => e.path ?? []).filter(Boolean)));
2039
+ return {
2040
+ ok: false,
2041
+ reason: "name_collision",
2042
+ 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"),
2043
+ collisions,
2044
+ errors,
2045
+ dsl,
2046
+ usage,
2047
+ provider: providerName
2048
+ };
2049
+ }
2050
+ const unresolved = errors.filter((e) => e.code === "UNKNOWN_REF");
2051
+ if (unresolved.length > 0) {
2052
+ const refs = Array.from(new Set(unresolved.flatMap((e) => e.path ?? []).filter(Boolean)));
2053
+ return {
2054
+ ok: false,
2055
+ reason: "unresolved_ref",
2056
+ message: "AI tham chi\u1EBFu t\xEAn kh\xF4ng c\xF3: " + (refs.length > 0 ? refs.join(", ") : "kh\xF4ng x\xE1c \u0111\u1ECBnh"),
2057
+ refs,
2058
+ errors,
2059
+ dsl,
2060
+ usage,
2061
+ provider: providerName
2062
+ };
2063
+ }
2064
+ return {
2065
+ ok: false,
2066
+ reason: "transpile_error",
2067
+ message: "DSL t\u1EEB AI kh\xF4ng h\u1EE3p l\u1EC7",
2068
+ errors,
2069
+ dsl,
2070
+ usage,
2071
+ provider: providerName
1031
2072
  };
1032
2073
  }
2074
+ function toUsage3(u) {
2075
+ return {
2076
+ inputTokens: u?.inputTokens ?? 0,
2077
+ outputTokens: u?.outputTokens ?? 0,
2078
+ cacheReadTokens: u?.cacheReadTokens ?? 0,
2079
+ cacheCreationTokens: u?.cacheCreationTokens ?? 0
2080
+ };
2081
+ }
2082
+
2083
+ // src/stamps/geometry-2d/ai/handleGenerateFigureDelta.ts
2084
+ var DEFAULT_MAX_ATTEMPTS2 = 2;
2085
+ async function handleGenerateFigureDelta(input, opts = {}) {
2086
+ const { onResult, maxAttempts: rawMax, ...generateOpts } = opts;
2087
+ const maxAttempts = clampAttempts2(rawMax ?? DEFAULT_MAX_ATTEMPTS2);
2088
+ let lastResult = null;
2089
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2090
+ const result = await generateFigureDelta(input, generateOpts);
2091
+ lastResult = result;
2092
+ if (onResult) {
2093
+ try {
2094
+ onResult(result, attempt);
2095
+ } catch {
2096
+ }
2097
+ }
2098
+ if (result.ok) {
2099
+ return { ok: true, state: result.state };
2100
+ }
2101
+ if (result.reason === "transpile_error" && attempt < maxAttempts) {
2102
+ continue;
2103
+ }
2104
+ break;
2105
+ }
2106
+ return mapErrorToUi2(lastResult);
2107
+ }
2108
+ function clampAttempts2(n) {
2109
+ if (!Number.isFinite(n)) return DEFAULT_MAX_ATTEMPTS2;
2110
+ return Math.max(1, Math.min(5, Math.floor(n)));
2111
+ }
2112
+ function mapErrorToUi2(result) {
2113
+ if (result.ok) return { ok: true, state: result.state };
2114
+ switch (result.reason) {
2115
+ case "refused":
2116
+ return { ok: false, message: result.message };
2117
+ case "parse_error":
2118
+ return {
2119
+ ok: false,
2120
+ 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."
2121
+ };
2122
+ case "transpile_error":
2123
+ return {
2124
+ ok: false,
2125
+ 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."
2126
+ };
2127
+ case "name_collision":
2128
+ return {
2129
+ ok: false,
2130
+ 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.`
2131
+ };
2132
+ case "unresolved_ref":
2133
+ return {
2134
+ ok: false,
2135
+ 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.`
2136
+ };
2137
+ case "api_error":
2138
+ default:
2139
+ return { ok: false, message: result.message };
2140
+ }
2141
+ }
1033
2142
 
2143
+ exports.AnthropicProvider = AnthropicProvider;
2144
+ exports.FigureEnvelopeZ = FigureEnvelopeZ;
2145
+ exports.FigureRefineEnvelopeZ = FigureRefineEnvelopeZ;
2146
+ exports.OllamaProvider = OllamaProvider;
2147
+ exports.envelopeBuildDsl = envelopeBuildDsl;
2148
+ exports.envelopeJsonSchema = envelopeJsonSchema;
1034
2149
  exports.generateFigure = generateFigure;
2150
+ exports.generateFigureDelta = generateFigureDelta;
2151
+ exports.handleGenerateFigure = handleGenerateFigure;
2152
+ exports.handleGenerateFigureDelta = handleGenerateFigureDelta;
2153
+ exports.refineEnvelopeJsonSchema = refineEnvelopeJsonSchema;
2154
+ exports.selectProvider = selectProvider;
1035
2155
  //# sourceMappingURL=ai.js.map
1036
2156
  //# sourceMappingURL=ai.js.map