@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.mjs CHANGED
@@ -1,146 +1,18 @@
1
+ import { KIND_REGISTRY, POINT_KINDS, LINE_LIKE_SHAPE_KINDS, CIRCLE_KINDS } from './chunk-VNCCIV6O.mjs';
1
2
  import { createEmptyState } from './chunk-73Q7ADVL.mjs';
3
+ import './chunk-B4NJJZFR.mjs';
2
4
  import { z } from 'zod';
3
- import Anthropic from '@anthropic-ai/sdk';
4
5
  import { zodToJsonSchema } from 'zod-to-json-schema';
6
+ import Anthropic from '@anthropic-ai/sdk';
5
7
 
6
- var NameZ = z.string().regex(/^[A-Za-z][A-Za-z0-9_'₀-₉]{0,11}$/);
7
- var DslPoint = z.discriminatedUnion("kind", [
8
- z.object({
9
- name: NameZ,
10
- kind: z.literal("free"),
11
- x: z.number().finite(),
12
- y: z.number().finite()
13
- }),
14
- z.object({
15
- name: NameZ,
16
- kind: z.literal("midpoint"),
17
- p1: NameZ,
18
- p2: NameZ
19
- }),
20
- z.object({
21
- name: NameZ,
22
- kind: z.literal("onSegment"),
23
- segmentId: NameZ,
24
- t: z.number().min(0).max(1)
25
- }),
26
- z.object({
27
- name: NameZ,
28
- kind: z.literal("onLine"),
29
- lineId: NameZ,
30
- t: z.number().finite()
31
- }),
32
- z.object({
33
- name: NameZ,
34
- kind: z.literal("onCircle"),
35
- circleId: NameZ,
36
- theta: z.number().finite()
37
- }),
38
- z.object({
39
- name: NameZ,
40
- kind: z.literal("perpFoot"),
41
- from: NameZ,
42
- onLine: NameZ
43
- }),
44
- z.object({
45
- name: NameZ,
46
- kind: z.literal("circumcenter"),
47
- vertices: z.tuple([NameZ, NameZ, NameZ])
48
- }),
49
- z.object({
50
- name: NameZ,
51
- kind: z.literal("incenter"),
52
- vertices: z.tuple([NameZ, NameZ, NameZ])
53
- }),
54
- z.object({
55
- name: NameZ,
56
- kind: z.literal("centroid"),
57
- vertices: z.tuple([NameZ, NameZ, NameZ])
58
- }),
59
- z.object({
60
- name: NameZ,
61
- kind: z.literal("orthocenter"),
62
- vertices: z.tuple([NameZ, NameZ, NameZ])
63
- }),
64
- z.object({
65
- name: NameZ,
66
- kind: z.literal("intersection"),
67
- ref1: NameZ,
68
- ref2: NameZ,
69
- branch: z.union([z.literal(0), z.literal(1)]).optional()
70
- })
71
- ]);
72
- var DslShape = z.discriminatedUnion("kind", [
73
- z.object({
74
- name: NameZ,
75
- kind: z.literal("segment"),
76
- p1: NameZ,
77
- p2: NameZ
78
- }),
79
- z.object({
80
- name: NameZ,
81
- kind: z.literal("line"),
82
- p1: NameZ,
83
- p2: NameZ
84
- }),
85
- z.object({
86
- name: NameZ,
87
- kind: z.literal("ray"),
88
- origin: NameZ,
89
- through: NameZ
90
- }),
91
- z.object({
92
- name: NameZ,
93
- kind: z.literal("polygon"),
94
- vertices: z.array(NameZ).min(3)
95
- }),
96
- // Line constructions
97
- z.object({
98
- name: NameZ,
99
- kind: z.literal("perpendicular"),
100
- throughPoint: NameZ,
101
- toLine: NameZ
102
- }),
103
- z.object({
104
- name: NameZ,
105
- kind: z.literal("parallel"),
106
- throughPoint: NameZ,
107
- toLine: NameZ
108
- }),
109
- z.object({
110
- name: NameZ,
111
- kind: z.literal("perpBisector"),
112
- p1: NameZ,
113
- p2: NameZ
114
- }),
115
- z.object({
116
- name: NameZ,
117
- kind: z.literal("angleBisector"),
118
- p1: NameZ,
119
- vertex: NameZ,
120
- p2: NameZ
121
- }),
122
- z.object({
123
- name: NameZ,
124
- kind: z.literal("tangent"),
125
- throughPoint: NameZ,
126
- toCircle: NameZ,
127
- branch: z.union([z.literal(0), z.literal(1), z.literal("on")]).optional()
128
- }),
129
- // Circle constructions
130
- z.object({
131
- name: NameZ,
132
- kind: z.literal("circleCP"),
133
- center: NameZ,
134
- surfacePoint: NameZ
135
- }),
136
- z.object({
137
- name: NameZ,
138
- kind: z.literal("circle3"),
139
- p1: NameZ,
140
- p2: NameZ,
141
- p3: NameZ
142
- })
143
- ]);
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));
144
16
  var DslInput = z.object({
145
17
  version: z.literal(1),
146
18
  points: z.array(DslPoint),
@@ -178,17 +50,6 @@ function buildSymbols(dsl) {
178
50
  function isPointLike(sym) {
179
51
  return !!sym && sym.role === "point";
180
52
  }
181
- var LINE_LIKE_SHAPE_KINDS = /* @__PURE__ */ new Set([
182
- "line",
183
- "segment",
184
- "ray",
185
- "perpendicular",
186
- "parallel",
187
- "perpBisector",
188
- "angleBisector",
189
- "tangent"
190
- ]);
191
- var CIRCLE_KINDS = /* @__PURE__ */ new Set(["circleCP", "circle3"]);
192
53
  function isLineLike(sym) {
193
54
  if (!sym || sym.role !== "shape") return false;
194
55
  return LINE_LIKE_SHAPE_KINDS.has(sym.entity.kind);
@@ -303,50 +164,9 @@ function validateRefs(dsl, symbols) {
303
164
  return { errors };
304
165
  }
305
166
  function collectRefs(entity) {
306
- if ("kind" in entity) {
307
- switch (entity.kind) {
308
- case "free":
309
- return [];
310
- case "midpoint":
311
- return [entity.p1, entity.p2];
312
- case "onSegment":
313
- return [entity.segmentId];
314
- case "onLine":
315
- return [entity.lineId];
316
- case "onCircle":
317
- return [entity.circleId];
318
- case "perpFoot":
319
- return [entity.from, entity.onLine];
320
- case "circumcenter":
321
- case "incenter":
322
- case "centroid":
323
- case "orthocenter":
324
- return [...entity.vertices];
325
- case "intersection":
326
- return [entity.ref1, entity.ref2];
327
- case "segment":
328
- case "line":
329
- return [entity.p1, entity.p2];
330
- case "ray":
331
- return [entity.origin, entity.through];
332
- case "polygon":
333
- return [...entity.vertices];
334
- case "perpendicular":
335
- case "parallel":
336
- return [entity.throughPoint, entity.toLine];
337
- case "perpBisector":
338
- return [entity.p1, entity.p2];
339
- case "angleBisector":
340
- return [entity.p1, entity.vertex, entity.p2];
341
- case "tangent":
342
- return [entity.throughPoint, entity.toCircle];
343
- case "circleCP":
344
- return [entity.center, entity.surfacePoint];
345
- case "circle3":
346
- return [entity.p1, entity.p2, entity.p3];
347
- }
348
- }
349
- return [];
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);
350
170
  }
351
171
 
352
172
  // src/stamps/geometry-2d/dsl/transpile/cycles.ts
@@ -404,229 +224,45 @@ function detectCycles(symbols) {
404
224
  }
405
225
 
406
226
  // src/stamps/geometry-2d/dsl/transpile/ids.ts
407
- function prefixFor(sym) {
408
- if (sym.role === "point") {
409
- const p = sym.entity;
410
- return p.kind === "intersection" ? "i" : "p";
411
- }
412
- const s = sym.entity;
413
- switch (s.kind) {
414
- case "segment":
415
- return "s";
416
- case "ray":
417
- return "r";
418
- case "polygon":
419
- return "poly";
420
- case "circleCP":
421
- case "circle3":
422
- return "c";
423
- // line + 5 line-constructions all share 'l'
424
- case "line":
425
- case "perpendicular":
426
- case "parallel":
427
- case "perpBisector":
428
- case "angleBisector":
429
- case "tangent":
430
- return "l";
431
- }
432
- }
433
227
  function assignIds(symbols) {
434
- const counters = { p: 0, i: 0, s: 0, l: 0, r: 0, poly: 0, c: 0 };
228
+ const counters = /* @__PURE__ */ new Map();
435
229
  const ids = /* @__PURE__ */ new Map();
436
230
  for (const [name, sym] of symbols.entries()) {
437
- const prefix = prefixFor(sym);
438
- counters[prefix] += 1;
439
- ids.set(name, `${prefix}${counters[prefix]}`);
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)}`);
440
236
  }
441
237
  return ids;
442
238
  }
443
239
 
444
- // src/stamps/geometry-2d/dsl/transpile/emitPoint.ts
445
- function resolveId(ids, name) {
446
- const id = ids.get(name);
447
- if (!id) throw new Error(`emitPoint: id not assigned for "${name}"`);
448
- return id;
449
- }
450
- function emitPoint(p, ids, kindHints) {
451
- const baseId = resolveId(ids, p.name);
452
- const baseFields = {
453
- label: p.name,
454
- visible: true,
455
- locked: false,
456
- layer: "default",
457
- schemaVersion: 1
458
- };
459
- if (p.kind === "intersection") {
460
- const r1Hint = kindHints.get(p.ref1);
461
- const r2Hint = kindHints.get(p.ref2);
462
- const r1IsCircle = r1Hint === "circle";
463
- const r2IsCircle = r2Hint === "circle";
464
- let intersectKind;
465
- if (r1IsCircle && r2IsCircle) intersectKind = "circleCircle";
466
- else if (r1IsCircle || r2IsCircle) intersectKind = "lineCircle";
467
- else intersectKind = "lineLine";
468
- const attrs = {
469
- kind: intersectKind,
470
- ref1: resolveId(ids, p.ref1),
471
- ref2: resolveId(ids, p.ref2)
472
- };
473
- if (intersectKind !== "lineLine") {
474
- attrs.branch = p.branch ?? 0;
475
- }
476
- return {
477
- id: baseId,
478
- kind: "intersection",
479
- ...baseFields,
480
- attrs
481
- };
482
- }
483
- let constraint;
484
- switch (p.kind) {
485
- case "free":
486
- constraint = { kind: "free", x: p.x, y: p.y };
487
- break;
488
- case "midpoint":
489
- constraint = { kind: "midpoint", p1: resolveId(ids, p.p1), p2: resolveId(ids, p.p2) };
490
- break;
491
- case "onSegment":
492
- constraint = { kind: "onSegment", segmentId: resolveId(ids, p.segmentId), t: p.t };
493
- break;
494
- case "onLine":
495
- constraint = { kind: "onLine", lineId: resolveId(ids, p.lineId), t: p.t };
496
- break;
497
- case "onCircle":
498
- constraint = { kind: "onCircle", circleId: resolveId(ids, p.circleId), theta: p.theta };
499
- break;
500
- case "perpFoot":
501
- constraint = { kind: "perpFoot", from: resolveId(ids, p.from), onLine: resolveId(ids, p.onLine) };
502
- break;
503
- case "circumcenter":
504
- case "incenter":
505
- case "centroid":
506
- case "orthocenter":
507
- constraint = {
508
- kind: p.kind,
509
- vertices: [resolveId(ids, p.vertices[0]), resolveId(ids, p.vertices[1]), resolveId(ids, p.vertices[2])]
510
- };
511
- break;
512
- }
513
- return {
514
- id: baseId,
515
- kind: "point",
516
- ...baseFields,
517
- attrs: { constraint }
518
- };
519
- }
520
-
521
- // src/stamps/geometry-2d/dsl/transpile/emitShape.ts
522
- function r(ids, name) {
523
- const id = ids.get(name);
524
- if (!id) throw new Error(`emitShape: id not assigned for "${name}"`);
525
- return id;
526
- }
527
- function emitShape(s, ids) {
528
- const id = r(ids, s.name);
529
- const base = {
530
- label: s.name,
531
- visible: true,
532
- locked: false,
533
- layer: "default",
534
- schemaVersion: 1
535
- };
536
- switch (s.kind) {
537
- case "segment":
538
- return { id, kind: "segment", ...base, attrs: { p1: r(ids, s.p1), p2: r(ids, s.p2) } };
539
- case "line":
540
- return { id, kind: "line", ...base, attrs: { p1: r(ids, s.p1), p2: r(ids, s.p2) } };
541
- case "ray":
542
- return { id, kind: "ray", ...base, attrs: { origin: r(ids, s.origin), through: r(ids, s.through) } };
543
- case "polygon":
544
- return { id, kind: "polygon", ...base, attrs: { vertices: s.vertices.map((v) => r(ids, v)) } };
545
- case "perpendicular":
546
- case "parallel":
547
- return {
548
- id,
549
- kind: "line",
550
- ...base,
551
- attrs: { construction: { kind: s.kind, throughPoint: r(ids, s.throughPoint), toLine: r(ids, s.toLine) } }
552
- };
553
- case "perpBisector":
554
- return {
555
- id,
556
- kind: "line",
557
- ...base,
558
- attrs: { construction: { kind: "perpBisector", p1: r(ids, s.p1), p2: r(ids, s.p2) } }
559
- };
560
- case "angleBisector":
561
- return {
562
- id,
563
- kind: "line",
564
- ...base,
565
- attrs: { construction: { kind: "angleBisector", p1: r(ids, s.p1), vertex: r(ids, s.vertex), p2: r(ids, s.p2) } }
566
- };
567
- case "tangent": {
568
- const construction = {
569
- kind: "tangent",
570
- throughPoint: r(ids, s.throughPoint),
571
- toCircle: r(ids, s.toCircle)
572
- };
573
- if (s.branch !== void 0) construction.branch = s.branch;
574
- return { id, kind: "line", ...base, attrs: { construction } };
575
- }
576
- case "circleCP":
577
- return {
578
- id,
579
- kind: "circle",
580
- ...base,
581
- attrs: { center: r(ids, s.center), surfacePoint: r(ids, s.surfacePoint) }
582
- };
583
- case "circle3":
584
- return {
585
- id,
586
- kind: "circle",
587
- ...base,
588
- attrs: { construction: { kind: "circumscribed", p1: r(ids, s.p1), p2: r(ids, s.p2), p3: r(ids, s.p3) } }
589
- };
590
- }
591
- }
592
-
593
240
  // src/stamps/geometry-2d/dsl/transpile.ts
594
241
  function hintOf(entity) {
595
- if ("kind" in entity) {
596
- switch (entity.kind) {
597
- case "free":
598
- case "midpoint":
599
- case "onSegment":
600
- case "onLine":
601
- case "onCircle":
602
- case "perpFoot":
603
- case "circumcenter":
604
- case "incenter":
605
- case "centroid":
606
- case "orthocenter":
607
- case "intersection":
608
- return "point";
609
- case "segment":
610
- return "segment";
611
- case "line":
612
- return "line";
613
- case "ray":
614
- return "ray";
615
- case "polygon":
616
- return "point";
617
- // not used as ref target in MVP
618
- case "perpendicular":
619
- case "parallel":
620
- case "perpBisector":
621
- case "angleBisector":
622
- case "tangent":
623
- return "lineConstruction";
624
- case "circleCP":
625
- case "circle3":
626
- return "circle";
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}`;
627
264
  }
628
- }
629
- return "point";
265
+ };
630
266
  }
631
267
  function transpile(dslRaw) {
632
268
  const parsed = DslInput.safeParse(dslRaw);
@@ -649,16 +285,18 @@ function transpile(dslRaw) {
649
285
  }
650
286
  const objects = {};
651
287
  const order = [];
652
- for (const p of dsl.points) {
653
- const obj = emitPoint(p, ids, kindHints);
654
- objects[obj.id] = obj;
655
- order.push(obj.id);
656
- }
657
- for (const s of dsl.shapes) {
658
- const obj = emitShape(s, ids);
659
- objects[obj.id] = obj;
660
- order.push(obj.id);
661
- }
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);
662
300
  const empty = createEmptyState("2d");
663
301
  const state = {
664
302
  objects,
@@ -668,20 +306,29 @@ function transpile(dslRaw) {
668
306
  };
669
307
  return { ok: true, state };
670
308
  }
671
- async function callProvider(args) {
672
- const client = new Anthropic({ apiKey: args.apiKey });
673
- const resp = await client.messages.create(
674
- {
675
- model: args.model,
676
- max_tokens: args.maxTokens,
677
- system: args.system,
678
- tools: args.tools,
679
- tool_choice: args.toolChoice,
680
- messages: args.messages
681
- },
682
- args.signal ? { signal: args.signal } : void 0
683
- );
684
- return resp;
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;
685
332
  }
686
333
 
687
334
  // src/stamps/geometry-2d/dsl/fixtures/triangle-equilateral.ts
@@ -849,62 +496,267 @@ var fixture9 = {
849
496
  }
850
497
  };
851
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
+
852
640
  // src/stamps/geometry-2d/ai/prompt.ts
853
- var FIXTURES = [fixture, fixture2, fixture3, fixture4, fixture5, fixture6, fixture7, fixture8, fixture9];
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
+ ];
854
659
  function buildSystemPrompt() {
855
660
  const examples = FIXTURES.map(
856
661
  (f, i) => `### V\xED d\u1EE5 ${i + 1}
857
662
  **\u0110\u1EC1:** ${f.problem}
858
- **DSL:**
859
- \`\`\`json
860
- ${JSON.stringify(f.dsl, null, 2)}
861
- \`\`\``
663
+ **Output:**
664
+ ${JSON.stringify({ decision: "build", figure: f.dsl }, null, 2)}`
862
665
  ).join("\n\n");
863
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.
864
667
 
865
668
  ## Nhi\u1EC7m v\u1EE5
866
- \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.
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" }
867
675
 
868
676
  ## Quy t\u1EAFc
869
- 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).
870
- 2. \u01AFu ti\xEAn derived points (midpoint, perpFoot, circumcenter, ...) thay v\xEC t\u1EF1 compute to\u1EA1 \u0111\u1ED9.
871
- 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).
872
- 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.
873
- 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).
874
- 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).
875
- 7. N\u1EBFu \u0111\u1EC1 m\u01A1 h\u1ED3: ch\u1ECDn case ph\u1ED5 bi\u1EBFn nh\u1EA5t, kh\xF4ng h\u1ECFi l\u1EA1i.
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.
876
695
 
877
696
  ## Primitives s\u1EB5n c\xF3
878
697
  **Points:** free, midpoint, onSegment, onLine, onCircle, perpFoot, circumcenter, incenter, centroid, orthocenter, intersection
879
698
  **Shapes:** segment, line, ray, polygon, perpendicular, parallel, perpBisector, angleBisector, tangent, circleCP, circle3
880
699
 
881
- ## 9 v\xED d\u1EE5
700
+ ## ${FIXTURES.length} v\xED d\u1EE5
882
701
  ${examples}
883
702
 
884
- ## Khi kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c
885
- 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").`;
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.`;
886
704
  }
887
- var BUILD_FIGURE_TOOL = {
888
- name: "build_figure",
889
- 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.",
890
- input_schema: zodToJsonSchema(DslInput, {
891
- target: "jsonSchema7",
892
- $refStrategy: "none"
893
- })
894
- };
895
- var RefuseInputZ = z.object({
896
- reason: z.string().min(1).describe("L\xFD do kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c (ti\u1EBFng Vi\u1EC7t)")
897
- });
898
- var REFUSE_TOOL = {
899
- name: "refuse",
900
- 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+).",
901
- input_schema: zodToJsonSchema(RefuseInputZ, { target: "jsonSchema7" })
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
+ }
902
759
  };
903
- var TOOLS = [BUILD_FIGURE_TOOL, REFUSE_TOOL];
904
-
905
- // src/stamps/geometry-2d/ai/buildFigure.ts
906
- var DEFAULT_MODEL = "claude-opus-4-7";
907
- var DEFAULT_MAX_TOKENS = 8192;
908
760
  function toUsage(u) {
909
761
  return {
910
762
  inputTokens: u.input_tokens,
@@ -913,87 +765,731 @@ function toUsage(u) {
913
765
  cacheCreationTokens: u.cache_creation_input_tokens ?? 0
914
766
  };
915
767
  }
916
- async function generateFigure(problem, opts) {
917
- if (!opts.apiKey) {
918
- return { ok: false, reason: "api_error", message: "apiKey b\u1EAFt bu\u1ED9c" };
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;
919
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 = {}) {
920
908
  if (!problem || !problem.trim()) {
921
909
  return { ok: false, reason: "api_error", message: "\u0110\u1EC1 b\xE0i r\u1ED7ng" };
922
910
  }
923
- const systemText = buildSystemPrompt();
924
- const enableCaching = opts.enableCaching !== false;
925
- const systemBlock = enableCaching ? { type: "text", text: systemText, cache_control: { type: "ephemeral" } } : { type: "text", text: systemText };
926
- let response;
911
+ let provider;
927
912
  try {
928
- response = await callProvider({
929
- apiKey: opts.apiKey,
930
- model: opts.model ?? DEFAULT_MODEL,
931
- maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
932
- system: [systemBlock],
933
- tools: TOOLS,
934
- toolChoice: { type: "any" },
935
- messages: [{ role: "user", content: problem }],
936
- signal: opts.signal
937
- });
913
+ provider = selectProvider(opts);
938
914
  } catch (e) {
939
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") {
940
929
  return {
941
930
  ok: false,
942
931
  reason: "api_error",
943
- message: err.message ?? "L\u1ED7i g\u1ECDi Claude API",
944
- ...err.status !== void 0 ? { status: err.status } : {}
932
+ message: out.message,
933
+ ...out.status !== void 0 ? { status: out.status } : {},
934
+ provider: provider.name
945
935
  };
946
936
  }
947
- const usage = toUsage(response.usage);
948
- const toolUse = response.content.find((c) => c.type === "tool_use");
949
- if (!toolUse || toolUse.type !== "tool_use") {
950
- const text = response.content.find((c) => c.type === "text");
951
- const textStr = text?.type === "text" ? text.text : "(empty)";
937
+ const usage = toUsage2(out.usage);
938
+ const parsed = FigureEnvelopeZ.safeParse(out.data);
939
+ if (!parsed.success) {
952
940
  return {
953
941
  ok: false,
954
942
  reason: "parse_error",
955
- message: "AI kh\xF4ng g\u1ECDi tool n\xE0o. Response: " + textStr,
956
- raw: response.content,
957
- usage
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
958
947
  };
959
948
  }
960
- if (toolUse.name === "refuse") {
961
- const input = toolUse.input;
949
+ const env = parsed.data;
950
+ if (env.decision === "refuse") {
962
951
  return {
963
952
  ok: false,
964
953
  reason: "refused",
965
- message: input.reason ?? "AI t\u1EEB ch\u1ED1i kh\xF4ng n\xEAu l\xFD do",
966
- usage
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
967
1327
  };
968
1328
  }
969
- if (toolUse.name !== "build_figure") {
1329
+ const usage = toUsage3(out.usage);
1330
+ const parsed = FigureRefineEnvelopeZ.safeParse(out.data);
1331
+ if (!parsed.success) {
970
1332
  return {
971
1333
  ok: false,
972
1334
  reason: "parse_error",
973
- message: `Tool kh\xF4ng x\xE1c \u0111\u1ECBnh: "${toolUse.name}"`,
974
- raw: toolUse,
975
- usage
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
976
1339
  };
977
1340
  }
978
- const tResult = transpile(toolUse.input);
979
- if (!tResult.ok) {
1341
+ const env = parsed.data;
1342
+ if (env.decision === "refuse") {
980
1343
  return {
981
1344
  ok: false,
982
- reason: "transpile_error",
983
- message: "DSL t\u1EEB AI kh\xF4ng h\u1EE3p l\u1EC7",
984
- errors: tResult.errors,
985
- dsl: toolUse.input,
986
- usage
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
987
1364
  };
988
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
+ }
989
1376
  return {
990
1377
  ok: true,
991
1378
  state: tResult.state,
992
- dsl: toolUse.input,
993
- usage
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
994
1430
  };
995
1431
  }
996
1432
 
997
- export { generateFigure };
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 };
998
1494
  //# sourceMappingURL=ai.mjs.map
999
1495
  //# sourceMappingURL=ai.mjs.map