@xom11/whiteboard 0.27.0 → 0.29.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 (67) hide show
  1. package/dist/ai.d.mts +236 -295
  2. package/dist/ai.d.ts +236 -295
  3. package/dist/ai.js +6015 -7577
  4. package/dist/ai.js.map +1 -1
  5. package/dist/ai.mjs +4804 -5426
  6. package/dist/ai.mjs.map +1 -1
  7. package/dist/catalog.json +4 -4
  8. package/dist/{chunk-D5JLJ3PT.mjs → chunk-5JM35CXV.mjs} +4 -4
  9. package/dist/{chunk-D5JLJ3PT.mjs.map → chunk-5JM35CXV.mjs.map} +1 -1
  10. package/dist/{chunk-KYMBUTPO.mjs → chunk-BU5KLO3P.mjs} +3 -3
  11. package/dist/{chunk-KYMBUTPO.mjs.map → chunk-BU5KLO3P.mjs.map} +1 -1
  12. package/dist/{chunk-AJAHD35N.mjs → chunk-E6EDOPGT.mjs} +3 -105
  13. package/dist/chunk-E6EDOPGT.mjs.map +1 -0
  14. package/dist/{chunk-KZGPSTZI.mjs → chunk-GEC2D2EQ.mjs} +4 -4
  15. package/dist/{chunk-KZGPSTZI.mjs.map → chunk-GEC2D2EQ.mjs.map} +1 -1
  16. package/dist/{chunk-AYJPOHCI.mjs → chunk-H22OZYTW.mjs} +3 -3
  17. package/dist/{chunk-AYJPOHCI.mjs.map → chunk-H22OZYTW.mjs.map} +1 -1
  18. package/dist/{chunk-I3L56GVH.mjs → chunk-OQIQNKPQ.mjs} +3 -3
  19. package/dist/{chunk-I3L56GVH.mjs.map → chunk-OQIQNKPQ.mjs.map} +1 -1
  20. package/dist/{chunk-4ETJ4CDY.mjs → chunk-QRUAEXLR.mjs} +4 -4
  21. package/dist/{chunk-4ETJ4CDY.mjs.map → chunk-QRUAEXLR.mjs.map} +1 -1
  22. package/dist/{chunk-HLAOGXEK.mjs → chunk-V3YJ6JFL.mjs} +3 -3
  23. package/dist/{chunk-HLAOGXEK.mjs.map → chunk-V3YJ6JFL.mjs.map} +1 -1
  24. package/dist/{chunk-D5LWSN2Y.mjs → chunk-ZTQBUKLJ.mjs} +30 -13
  25. package/dist/chunk-ZTQBUKLJ.mjs.map +1 -0
  26. package/dist/geometry-2d.d.mts +1 -2
  27. package/dist/geometry-2d.d.ts +1 -2
  28. package/dist/geometry-2d.js +961 -3586
  29. package/dist/geometry-2d.js.map +1 -1
  30. package/dist/geometry-2d.mjs +3 -3
  31. package/dist/geometry-3d.d.mts +1 -2
  32. package/dist/geometry-3d.d.ts +1 -2
  33. package/dist/geometry-3d.js +28 -11
  34. package/dist/geometry-3d.js.map +1 -1
  35. package/dist/geometry-3d.mjs +3 -3
  36. package/dist/graph-2d.d.mts +1 -2
  37. package/dist/graph-2d.d.ts +1 -2
  38. package/dist/graph-2d.js +28 -11
  39. package/dist/graph-2d.js.map +1 -1
  40. package/dist/graph-2d.mjs +3 -3
  41. package/dist/{host-M26FS244.mjs → host-2ISGVO7O.mjs} +5 -5
  42. package/dist/{host-M26FS244.mjs.map → host-2ISGVO7O.mjs.map} +1 -1
  43. package/dist/{host-HAYCJJ2T.mjs → host-HKMZSCIT.mjs} +142 -318
  44. package/dist/host-HKMZSCIT.mjs.map +1 -0
  45. package/dist/{host-LTJHAY5A.mjs → host-HOSJHQ5H.mjs} +6 -6
  46. package/dist/{host-LTJHAY5A.mjs.map → host-HOSJHQ5H.mjs.map} +1 -1
  47. package/dist/index.d.mts +9 -6
  48. package/dist/index.d.ts +9 -6
  49. package/dist/index.js +913 -3540
  50. package/dist/index.js.map +1 -1
  51. package/dist/index.mjs +17 -16
  52. package/dist/index.mjs.map +1 -1
  53. package/dist/latex.d.mts +1 -2
  54. package/dist/latex.d.ts +1 -2
  55. package/dist/serialize-N4G6RFBB.mjs +9 -0
  56. package/dist/{serialize-C3LSUMSA.mjs.map → serialize-N4G6RFBB.mjs.map} +1 -1
  57. package/dist/{types-zc_Pa0mp.d.ts → types-C3FjpoUi.d.mts} +22 -229
  58. package/dist/{types-zc_Pa0mp.d.mts → types-C3FjpoUi.d.ts} +22 -229
  59. package/package.json +1 -9
  60. package/dist/chunk-AJAHD35N.mjs.map +0 -1
  61. package/dist/chunk-D5LWSN2Y.mjs.map +0 -1
  62. package/dist/chunk-T3SOHYB2.mjs +0 -851
  63. package/dist/chunk-T3SOHYB2.mjs.map +0 -1
  64. package/dist/handleExtractProblem-C-U5KluK.d.mts +0 -158
  65. package/dist/handleExtractProblem-C-U5KluK.d.ts +0 -158
  66. package/dist/host-HAYCJJ2T.mjs.map +0 -1
  67. package/dist/serialize-C3LSUMSA.mjs +0 -9
@@ -5,11 +5,6 @@ var immer = require('immer');
5
5
  var React2 = require('react');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
  var reactDom = require('react-dom');
8
- var zod = require('zod');
9
- var Anthropic = require('@anthropic-ai/sdk');
10
- var zodToJsonSchema = require('zod-to-json-schema');
11
-
12
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
8
 
14
9
  function _interopNamespace(e) {
15
10
  if (e && e.__esModule) return e;
@@ -30,7 +25,6 @@ function _interopNamespace(e) {
30
25
  }
31
26
 
32
27
  var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
33
- var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
34
28
 
35
29
  var __defProp = Object.defineProperty;
36
30
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -2293,9 +2287,11 @@ var init_circle = __esm({
2293
2287
  },
2294
2288
  render: (obj, ctx) => {
2295
2289
  const board = ctx.jxg;
2290
+ const isCenterLabel = (l) => /^[A-Z]['′]?\d*$/u.test(l);
2291
+ const isCenter = isCenterLabel(obj.label);
2296
2292
  const baseOpts = {
2297
2293
  name: obj.label,
2298
- withLabel: obj.attrs.showLabel ?? false,
2294
+ withLabel: isCenter ? obj.attrs.showLabel ?? false : true,
2299
2295
  strokeColor: obj.attrs.color ?? "#0f172a",
2300
2296
  strokeWidth: obj.attrs.width ?? 2,
2301
2297
  dash: obj.attrs.dash ?? 0,
@@ -2308,22 +2304,37 @@ var init_circle = __esm({
2308
2304
  const p1 = ctx.resolveRef(c.p1);
2309
2305
  const p2 = ctx.resolveRef(c.p2);
2310
2306
  const p3 = ctx.resolveRef(c.p3);
2307
+ if (isCenter) {
2308
+ const center2 = board.create("circumcenter", [p1, p2, p3], {
2309
+ visible: obj.visible,
2310
+ withLabel: true,
2311
+ fixed: true,
2312
+ name: obj.label
2313
+ });
2314
+ const circ = board.create("circumcircle", [p1, p2, p3], { ...baseOpts, withLabel: false });
2315
+ circ.center = circ.center ?? center2;
2316
+ circ._helpers = [center2];
2317
+ return circ;
2318
+ }
2311
2319
  return board.create("circumcircle", [p1, p2, p3], baseOpts);
2312
2320
  }
2313
2321
  if (c?.kind === "incircle") {
2314
2322
  const p1 = ctx.resolveRef(c.p1);
2315
2323
  const p2 = ctx.resolveRef(c.p2);
2316
2324
  const p3 = ctx.resolveRef(c.p3);
2317
- const center2 = board.create("incenter", [p1, p2, p3], {
2318
- visible: obj.visible,
2319
- withLabel: true,
2320
- fixed: true,
2321
- name: obj.label
2322
- });
2323
- const circ = board.create("incircle", [p1, p2, p3], baseOpts);
2324
- circ.center = circ.center ?? center2;
2325
- circ._helpers = [center2];
2326
- return circ;
2325
+ if (isCenter) {
2326
+ const center2 = board.create("incenter", [p1, p2, p3], {
2327
+ visible: obj.visible,
2328
+ withLabel: true,
2329
+ fixed: true,
2330
+ name: obj.label
2331
+ });
2332
+ const circ = board.create("incircle", [p1, p2, p3], { ...baseOpts, withLabel: false });
2333
+ circ.center = circ.center ?? center2;
2334
+ circ._helpers = [center2];
2335
+ return circ;
2336
+ }
2337
+ return board.create("incircle", [p1, p2, p3], baseOpts);
2327
2338
  }
2328
2339
  if (c?.kind === "excircle") {
2329
2340
  const P = [ctx.resolveRef(c.p1), ctx.resolveRef(c.p2), ctx.resolveRef(c.p3)];
@@ -9244,1504 +9255,853 @@ var init_Toast2 = __esm({
9244
9255
  init_useToast();
9245
9256
  }
9246
9257
  });
9247
- var NameZ;
9248
- var init_names = __esm({
9249
- "src/stamps/geometry-2d/dsl/names.ts"() {
9250
- NameZ = zod.z.string().regex(/^[A-Za-z][A-Za-z0-9_'₀-₉]{0,11}$/);
9251
- }
9252
- });
9253
-
9254
- // src/stamps/geometry-2d/dsl/kinds/_types.ts
9255
- function defineModule(m) {
9256
- return m;
9257
- }
9258
- var init_types5 = __esm({
9259
- "src/stamps/geometry-2d/dsl/kinds/_types.ts"() {
9260
- }
9261
- });
9262
-
9263
- // src/stamps/geometry-2d/dsl/kinds/_shared.ts
9264
- function emitPointObject(id, name, constraint, visible = true) {
9258
+ function useAiFigure(generator) {
9259
+ const [prompt, setPrompt] = React2.useState("");
9260
+ const [isLoading, setIsLoading] = React2.useState(false);
9261
+ const [error, setError] = React2.useState(null);
9262
+ const [tokens, setTokens] = React2.useState(0);
9263
+ const abortRef = React2.useRef(null);
9264
+ const requestIdRef = React2.useRef(0);
9265
+ React2.useEffect(() => () => abortRef.current?.abort(), []);
9266
+ const submit = React2.useCallback(async () => {
9267
+ const problem = prompt.trim();
9268
+ if (!problem) {
9269
+ setError("Nh\u1EADp \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng h\xECnh.");
9270
+ return null;
9271
+ }
9272
+ if (!generator) {
9273
+ setError("T\xEDnh n\u0103ng d\u1EF1ng h\xECnh AI ch\u01B0a \u0111\u01B0\u1EE3c c\u1EA5u h\xECnh.");
9274
+ return null;
9275
+ }
9276
+ abortRef.current?.abort();
9277
+ const controller = new AbortController();
9278
+ const requestId = ++requestIdRef.current;
9279
+ abortRef.current = controller;
9280
+ setIsLoading(true);
9281
+ setError(null);
9282
+ setTokens(0);
9283
+ try {
9284
+ const generated = await generator(problem, {
9285
+ signal: controller.signal,
9286
+ onProgress: (info) => {
9287
+ if (requestId === requestIdRef.current) setTokens(info.tokens);
9288
+ }
9289
+ });
9290
+ if (controller.signal.aborted || requestId !== requestIdRef.current) return null;
9291
+ if (!generated.ok) {
9292
+ setError(generated.message);
9293
+ return null;
9294
+ }
9295
+ return generated.state;
9296
+ } catch (caught) {
9297
+ if (controller.signal.aborted || caught instanceof DOMException && caught.name === "AbortError") {
9298
+ return null;
9299
+ }
9300
+ if (requestId === requestIdRef.current) {
9301
+ setError(
9302
+ caught instanceof Error && caught.message ? caught.message : "Kh\xF4ng th\u1EC3 d\u1EF1ng h\xECnh b\u1EB1ng AI."
9303
+ );
9304
+ }
9305
+ return null;
9306
+ } finally {
9307
+ if (requestId === requestIdRef.current) {
9308
+ abortRef.current = null;
9309
+ setIsLoading(false);
9310
+ }
9311
+ }
9312
+ }, [generator, prompt]);
9313
+ const cancel = React2.useCallback(() => {
9314
+ abortRef.current?.abort();
9315
+ }, []);
9265
9316
  return {
9266
- id,
9267
- kind: "point",
9268
- label: name,
9269
- ...POINT_BASE_FIELDS,
9270
- visible,
9271
- attrs: { constraint }
9317
+ prompt,
9318
+ setPrompt,
9319
+ isLoading,
9320
+ error,
9321
+ submit,
9322
+ cancel,
9323
+ tokens
9272
9324
  };
9273
9325
  }
9274
- function resolveTriangleVertices(ctx, vertices) {
9275
- return [ctx.resolveId(vertices[0]), ctx.resolveId(vertices[1]), ctx.resolveId(vertices[2])];
9276
- }
9277
- var POINT_BASE_FIELDS, SHAPE_BASE_FIELDS;
9278
- var init_shared3 = __esm({
9279
- "src/stamps/geometry-2d/dsl/kinds/_shared.ts"() {
9280
- POINT_BASE_FIELDS = {
9281
- visible: true,
9282
- locked: false,
9283
- layer: "default",
9284
- schemaVersion: 1
9285
- };
9286
- SHAPE_BASE_FIELDS = {
9287
- visible: true,
9288
- locked: false,
9289
- layer: "default",
9290
- schemaVersion: 1
9291
- };
9292
- }
9293
- });
9294
- var freeModule;
9295
- var init_free2 = __esm({
9296
- "src/stamps/geometry-2d/dsl/kinds/points/free.ts"() {
9297
- init_names();
9298
- init_types5();
9299
- init_shared3();
9300
- freeModule = defineModule({
9301
- kind: "free",
9302
- role: "point",
9303
- category: "points",
9304
- prefix: "p",
9305
- schema: zod.z.object({
9306
- name: NameZ,
9307
- kind: zod.z.literal("free"),
9308
- x: zod.z.number().finite(),
9309
- y: zod.z.number().finite()
9310
- }),
9311
- collectRefs: () => [],
9312
- emit: (e, ctx) => [{
9313
- role: "primary",
9314
- object: emitPointObject(ctx.resolveId(e.name), e.name, { kind: "free", x: e.x, y: e.y })
9315
- }]
9316
- });
9317
- }
9318
- });
9319
- var midpointModule;
9320
- var init_midpoint2 = __esm({
9321
- "src/stamps/geometry-2d/dsl/kinds/points/midpoint.ts"() {
9322
- init_names();
9323
- init_types5();
9324
- init_shared3();
9325
- midpointModule = defineModule({
9326
- kind: "midpoint",
9327
- role: "point",
9328
- category: "points",
9329
- prefix: "p",
9330
- schema: zod.z.object({
9331
- name: NameZ,
9332
- kind: zod.z.literal("midpoint"),
9333
- p1: NameZ,
9334
- p2: NameZ,
9335
- visible: zod.z.boolean().optional()
9336
- }),
9337
- collectRefs: (e) => [e.p1, e.p2],
9338
- emit: (e, ctx) => [{
9339
- role: "primary",
9340
- object: emitPointObject(
9341
- ctx.resolveId(e.name),
9342
- e.name,
9343
- { kind: "midpoint", p1: ctx.resolveId(e.p1), p2: ctx.resolveId(e.p2) },
9344
- e.visible ?? true
9345
- )
9346
- }]
9347
- });
9348
- }
9349
- });
9350
- var onSegmentModule;
9351
- var init_onSegment2 = __esm({
9352
- "src/stamps/geometry-2d/dsl/kinds/points/onSegment.ts"() {
9353
- init_names();
9354
- init_types5();
9355
- init_shared3();
9356
- onSegmentModule = defineModule({
9357
- kind: "onSegment",
9358
- role: "point",
9359
- category: "points",
9360
- prefix: "p",
9361
- schema: zod.z.object({
9362
- name: NameZ,
9363
- kind: zod.z.literal("onSegment"),
9364
- segmentId: NameZ,
9365
- t: zod.z.number().min(0).max(1)
9366
- }),
9367
- collectRefs: (e) => [e.segmentId],
9368
- emit: (e, ctx) => [{
9369
- role: "primary",
9370
- object: emitPointObject(
9371
- ctx.resolveId(e.name),
9372
- e.name,
9373
- { kind: "onSegment", segmentId: ctx.resolveId(e.segmentId), t: e.t }
9374
- )
9375
- }]
9376
- });
9377
- }
9378
- });
9379
- var onLineModule;
9380
- var init_onLine2 = __esm({
9381
- "src/stamps/geometry-2d/dsl/kinds/points/onLine.ts"() {
9382
- init_names();
9383
- init_types5();
9384
- init_shared3();
9385
- onLineModule = defineModule({
9386
- kind: "onLine",
9387
- role: "point",
9388
- category: "points",
9389
- prefix: "p",
9390
- schema: zod.z.object({
9391
- name: NameZ,
9392
- kind: zod.z.literal("onLine"),
9393
- lineId: NameZ,
9394
- t: zod.z.number().finite()
9395
- }),
9396
- collectRefs: (e) => [e.lineId],
9397
- emit: (e, ctx) => [{
9398
- role: "primary",
9399
- object: emitPointObject(
9400
- ctx.resolveId(e.name),
9401
- e.name,
9402
- { kind: "onLine", lineId: ctx.resolveId(e.lineId), t: e.t }
9403
- )
9404
- }]
9405
- });
9406
- }
9407
- });
9408
- var onCircleModule;
9409
- var init_onCircle2 = __esm({
9410
- "src/stamps/geometry-2d/dsl/kinds/points/onCircle.ts"() {
9411
- init_names();
9412
- init_types5();
9413
- init_shared3();
9414
- onCircleModule = defineModule({
9415
- kind: "onCircle",
9416
- role: "point",
9417
- category: "points",
9418
- prefix: "p",
9419
- schema: zod.z.object({
9420
- name: NameZ,
9421
- kind: zod.z.literal("onCircle"),
9422
- circleId: NameZ,
9423
- theta: zod.z.number().finite()
9424
- }),
9425
- collectRefs: (e) => [e.circleId],
9426
- emit: (e, ctx) => [{
9427
- role: "primary",
9428
- object: emitPointObject(
9429
- ctx.resolveId(e.name),
9430
- e.name,
9431
- { kind: "onCircle", circleId: ctx.resolveId(e.circleId), theta: e.theta }
9432
- )
9433
- }]
9434
- });
9435
- }
9436
- });
9437
- var perpFootModule;
9438
- var init_perpFoot2 = __esm({
9439
- "src/stamps/geometry-2d/dsl/kinds/points/perpFoot.ts"() {
9440
- init_names();
9441
- init_types5();
9442
- init_shared3();
9443
- perpFootModule = defineModule({
9444
- kind: "perpFoot",
9445
- role: "point",
9446
- category: "points",
9447
- prefix: "p",
9448
- schema: zod.z.object({
9449
- name: NameZ,
9450
- kind: zod.z.literal("perpFoot"),
9451
- from: NameZ,
9452
- onLine: NameZ
9453
- }),
9454
- collectRefs: (e) => [e.from, e.onLine],
9455
- emit: (e, ctx) => [{
9456
- role: "primary",
9457
- object: emitPointObject(
9458
- ctx.resolveId(e.name),
9459
- e.name,
9460
- { kind: "perpFoot", from: ctx.resolveId(e.from), onLine: ctx.resolveId(e.onLine) }
9461
- )
9462
- }]
9463
- });
9464
- }
9465
- });
9466
- var circumcenterModule;
9467
- var init_circumcenter2 = __esm({
9468
- "src/stamps/geometry-2d/dsl/kinds/points/circumcenter.ts"() {
9469
- init_names();
9470
- init_types5();
9471
- init_shared3();
9472
- circumcenterModule = defineModule({
9473
- kind: "circumcenter",
9474
- role: "point",
9475
- category: "points",
9476
- prefix: "p",
9477
- schema: zod.z.object({
9478
- name: NameZ,
9479
- kind: zod.z.literal("circumcenter"),
9480
- vertices: zod.z.tuple([NameZ, NameZ, NameZ])
9481
- }),
9482
- collectRefs: (e) => [...e.vertices],
9483
- emit: (e, ctx) => [{
9484
- role: "primary",
9485
- object: emitPointObject(
9486
- ctx.resolveId(e.name),
9487
- e.name,
9488
- { kind: "circumcenter", vertices: resolveTriangleVertices(ctx, e.vertices) }
9489
- )
9490
- }]
9491
- });
9492
- }
9493
- });
9494
- var incenterModule;
9495
- var init_incenter2 = __esm({
9496
- "src/stamps/geometry-2d/dsl/kinds/points/incenter.ts"() {
9497
- init_names();
9498
- init_types5();
9499
- init_shared3();
9500
- incenterModule = defineModule({
9501
- kind: "incenter",
9502
- role: "point",
9503
- category: "points",
9504
- prefix: "p",
9505
- schema: zod.z.object({
9506
- name: NameZ,
9507
- kind: zod.z.literal("incenter"),
9508
- vertices: zod.z.tuple([NameZ, NameZ, NameZ])
9509
- }),
9510
- collectRefs: (e) => [...e.vertices],
9511
- emit: (e, ctx) => [{
9512
- role: "primary",
9513
- object: emitPointObject(
9514
- ctx.resolveId(e.name),
9515
- e.name,
9516
- { kind: "incenter", vertices: resolveTriangleVertices(ctx, e.vertices) }
9517
- )
9518
- }]
9519
- });
9520
- }
9521
- });
9522
- var centroidModule;
9523
- var init_centroid2 = __esm({
9524
- "src/stamps/geometry-2d/dsl/kinds/points/centroid.ts"() {
9525
- init_names();
9526
- init_types5();
9527
- init_shared3();
9528
- centroidModule = defineModule({
9529
- kind: "centroid",
9530
- role: "point",
9531
- category: "points",
9532
- prefix: "p",
9533
- schema: zod.z.object({
9534
- name: NameZ,
9535
- kind: zod.z.literal("centroid"),
9536
- vertices: zod.z.tuple([NameZ, NameZ, NameZ])
9537
- }),
9538
- collectRefs: (e) => [...e.vertices],
9539
- emit: (e, ctx) => [{
9540
- role: "primary",
9541
- object: emitPointObject(
9542
- ctx.resolveId(e.name),
9543
- e.name,
9544
- { kind: "centroid", vertices: resolveTriangleVertices(ctx, e.vertices) }
9545
- )
9546
- }]
9547
- });
9548
- }
9549
- });
9550
- var orthocenterModule;
9551
- var init_orthocenter2 = __esm({
9552
- "src/stamps/geometry-2d/dsl/kinds/points/orthocenter.ts"() {
9553
- init_names();
9554
- init_types5();
9555
- init_shared3();
9556
- orthocenterModule = defineModule({
9557
- kind: "orthocenter",
9558
- role: "point",
9559
- category: "points",
9560
- prefix: "p",
9561
- schema: zod.z.object({
9562
- name: NameZ,
9563
- kind: zod.z.literal("orthocenter"),
9564
- vertices: zod.z.tuple([NameZ, NameZ, NameZ])
9565
- }),
9566
- collectRefs: (e) => [...e.vertices],
9567
- emit: (e, ctx) => [{
9568
- role: "primary",
9569
- object: emitPointObject(
9570
- ctx.resolveId(e.name),
9571
- e.name,
9572
- { kind: "orthocenter", vertices: resolveTriangleVertices(ctx, e.vertices) }
9573
- )
9574
- }]
9575
- });
9326
+ var init_useAiFigure = __esm({
9327
+ "src/stamps/geometry-2d/editor/useAiFigure.ts"() {
9328
+ "use client";
9576
9329
  }
9577
9330
  });
9578
- var intersectionModule;
9579
- var init_intersection2 = __esm({
9580
- "src/stamps/geometry-2d/dsl/kinds/points/intersection.ts"() {
9581
- init_names();
9582
- init_types5();
9583
- init_shared3();
9584
- intersectionModule = defineModule({
9585
- kind: "intersection",
9586
- role: "point",
9587
- category: "points",
9588
- prefix: "i",
9589
- schema: zod.z.object({
9590
- name: NameZ,
9591
- kind: zod.z.literal("intersection"),
9592
- ref1: NameZ,
9593
- ref2: NameZ,
9594
- branch: zod.z.union([zod.z.literal(0), zod.z.literal(1)]).optional()
9595
- }),
9596
- collectRefs: (e) => [e.ref1, e.ref2],
9597
- emit: (e, ctx) => {
9598
- const r1IsCircle = ctx.hintOf(e.ref1) === "circle";
9599
- const r2IsCircle = ctx.hintOf(e.ref2) === "circle";
9600
- let intersectKind;
9601
- if (r1IsCircle && r2IsCircle) intersectKind = "circleCircle";
9602
- else if (r1IsCircle || r2IsCircle) intersectKind = "lineCircle";
9603
- else intersectKind = "lineLine";
9604
- const attrs = {
9605
- kind: intersectKind,
9606
- ref1: ctx.resolveId(e.ref1),
9607
- ref2: ctx.resolveId(e.ref2)
9608
- };
9609
- if (intersectKind !== "lineLine") {
9610
- attrs.branch = e.branch ?? 0;
9611
- }
9612
- return [{
9613
- role: "primary",
9614
- object: {
9615
- id: ctx.resolveId(e.name),
9616
- kind: "intersection",
9617
- label: e.name,
9618
- ...POINT_BASE_FIELDS,
9619
- attrs
9620
- }
9621
- }];
9331
+ function AiFigurePrompt({ generator, onGenerated }) {
9332
+ const {
9333
+ prompt,
9334
+ setPrompt,
9335
+ isLoading,
9336
+ error,
9337
+ submit,
9338
+ cancel,
9339
+ tokens
9340
+ } = useAiFigure(generator);
9341
+ const [elapsed, setElapsed] = React2.useState(0);
9342
+ React2.useEffect(() => {
9343
+ if (!isLoading) {
9344
+ setElapsed(0);
9345
+ return;
9346
+ }
9347
+ const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
9348
+ return () => clearInterval(id);
9349
+ }, [isLoading]);
9350
+ const textareaRef = React2.useRef(null);
9351
+ const handleSendClick = React2.useCallback(async () => {
9352
+ const generated = await submit();
9353
+ if (generated) onGenerated(generated);
9354
+ }, [submit, onGenerated]);
9355
+ const promptEmpty = !prompt.trim();
9356
+ const sendDisabled = promptEmpty || isLoading;
9357
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-slate-200 bg-slate-50 px-3 py-3", children: [
9358
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mb-2 flex items-center justify-between gap-2", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium tracking-wide text-slate-600", children: "D\u1EF1ng h\xECnh b\u1EB1ng AI" }) }),
9359
+ /* @__PURE__ */ jsxRuntime.jsxs(
9360
+ "div",
9361
+ {
9362
+ className: "group relative flex flex-col rounded-2xl bg-white shadow-sm transition-all duration-150 ring-1 ring-slate-200 focus-within:ring-2 focus-within:ring-emerald-400/70 focus-within:shadow-md",
9363
+ children: [
9364
+ /* @__PURE__ */ jsxRuntime.jsx(
9365
+ "textarea",
9366
+ {
9367
+ ref: textareaRef,
9368
+ id: "geometry-ai-prompt",
9369
+ "aria-label": "\u0110\u1EC1 b\xE0i cho AI",
9370
+ "data-testid": "geometry-ai-textarea",
9371
+ value: prompt,
9372
+ onChange: (e) => setPrompt(e.target.value),
9373
+ onKeyDown: (e) => {
9374
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !sendDisabled) {
9375
+ e.preventDefault();
9376
+ void handleSendClick();
9377
+ }
9378
+ },
9379
+ disabled: isLoading,
9380
+ rows: 2,
9381
+ placeholder: "M\xF4 t\u1EA3 \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng.",
9382
+ className: "block w-full resize-none rounded-2xl bg-transparent px-3.5 pt-2.5 pb-1 text-sm leading-relaxed text-slate-800 placeholder:text-slate-400 outline-none disabled:opacity-60 field-sizing-content max-h-44"
9383
+ }
9384
+ ),
9385
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-end gap-2 px-2 pb-2 pt-1", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
9386
+ isLoading && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] tabular-nums text-slate-500", children: tokens > 0 ? `${tokens}tok \xB7 ${elapsed}s` : `${elapsed}s` }),
9387
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
9388
+ "button",
9389
+ {
9390
+ type: "button",
9391
+ onClick: cancel,
9392
+ "aria-label": "Hu\u1EF7 d\u1EF1ng h\xECnh AI",
9393
+ "data-testid": "geometry-ai-cancel",
9394
+ title: `\u0110ang d\u1EF1ng\u2026 ${elapsed}s \u2014 b\u1EA5m \u0111\u1EC3 hu\u1EF7`,
9395
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-amber-500 text-white shadow-sm transition hover:scale-105 hover:bg-amber-600 active:scale-95",
9396
+ children: /* @__PURE__ */ jsxRuntime.jsx(StopIcon, { className: "h-3.5 w-3.5" })
9397
+ }
9398
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
9399
+ "button",
9400
+ {
9401
+ type: "button",
9402
+ onClick: () => void handleSendClick(),
9403
+ disabled: sendDisabled,
9404
+ "aria-label": "D\u1EF1ng b\u1EB1ng AI",
9405
+ title: "D\u1EF1ng b\u1EB1ng AI (Ctrl/\u2318+Enter)",
9406
+ "data-testid": "geometry-ai-submit",
9407
+ className: "flex h-8 w-8 items-center justify-center rounded-full bg-emerald-600 text-white shadow-sm transition hover:scale-105 hover:bg-emerald-700 active:scale-95 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:hover:scale-100",
9408
+ children: /* @__PURE__ */ jsxRuntime.jsx(ArrowUpIcon, { className: "h-[18px] w-[18px]" })
9409
+ }
9410
+ )
9411
+ ] }) })
9412
+ ]
9622
9413
  }
9623
- });
9624
- }
9625
- });
9626
- var segmentModule;
9627
- var init_segment2 = __esm({
9628
- "src/stamps/geometry-2d/dsl/kinds/lines/segment.ts"() {
9629
- init_names();
9630
- init_types5();
9631
- init_shared3();
9632
- segmentModule = defineModule({
9633
- kind: "segment",
9634
- role: "segment",
9635
- category: "lines",
9636
- prefix: "s",
9637
- schema: zod.z.object({
9638
- name: NameZ,
9639
- kind: zod.z.literal("segment"),
9640
- p1: NameZ,
9641
- p2: NameZ
9642
- }),
9643
- collectRefs: (e) => [e.p1, e.p2],
9644
- emit: (e, ctx) => [{
9645
- role: "primary",
9646
- object: {
9647
- id: ctx.resolveId(e.name),
9648
- kind: "segment",
9649
- label: e.name,
9650
- ...SHAPE_BASE_FIELDS,
9651
- attrs: { p1: ctx.resolveId(e.p1), p2: ctx.resolveId(e.p2) }
9652
- }
9653
- }]
9654
- });
9655
- }
9656
- });
9657
- var lineModule;
9658
- var init_line2 = __esm({
9659
- "src/stamps/geometry-2d/dsl/kinds/lines/line.ts"() {
9660
- init_names();
9661
- init_types5();
9662
- init_shared3();
9663
- lineModule = defineModule({
9664
- kind: "line",
9665
- role: "line",
9666
- category: "lines",
9667
- prefix: "l",
9668
- schema: zod.z.object({
9669
- name: NameZ,
9670
- kind: zod.z.literal("line"),
9671
- p1: NameZ,
9672
- p2: NameZ
9673
- }),
9674
- collectRefs: (e) => [e.p1, e.p2],
9675
- emit: (e, ctx) => [{
9676
- role: "primary",
9677
- object: {
9678
- id: ctx.resolveId(e.name),
9679
- kind: "line",
9680
- label: e.name,
9681
- ...SHAPE_BASE_FIELDS,
9682
- attrs: { p1: ctx.resolveId(e.p1), p2: ctx.resolveId(e.p2) }
9683
- }
9684
- }]
9685
- });
9414
+ ),
9415
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error })
9416
+ ] });
9417
+ }
9418
+ var ArrowUpIcon, StopIcon;
9419
+ var init_AiFigurePrompt = __esm({
9420
+ "src/stamps/geometry-2d/editor/AiFigurePrompt.tsx"() {
9421
+ "use client";
9422
+ init_useAiFigure();
9423
+ ArrowUpIcon = (props) => /* @__PURE__ */ jsxRuntime.jsxs(
9424
+ "svg",
9425
+ {
9426
+ viewBox: "0 0 24 24",
9427
+ fill: "none",
9428
+ stroke: "currentColor",
9429
+ strokeWidth: 2.25,
9430
+ strokeLinecap: "round",
9431
+ strokeLinejoin: "round",
9432
+ "aria-hidden": true,
9433
+ ...props,
9434
+ children: [
9435
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 19V5" }),
9436
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m5 12 7-7 7 7" })
9437
+ ]
9438
+ }
9439
+ );
9440
+ StopIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, ...props, children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) });
9686
9441
  }
9687
9442
  });
9688
- var rayModule;
9689
- var init_ray2 = __esm({
9690
- "src/stamps/geometry-2d/dsl/kinds/lines/ray.ts"() {
9691
- init_names();
9692
- init_types5();
9693
- init_shared3();
9694
- rayModule = defineModule({
9695
- kind: "ray",
9696
- role: "ray",
9697
- category: "lines",
9698
- prefix: "r",
9699
- schema: zod.z.object({
9700
- name: NameZ,
9701
- kind: zod.z.literal("ray"),
9702
- origin: NameZ,
9703
- through: NameZ
9704
- }),
9705
- collectRefs: (e) => [e.origin, e.through],
9706
- emit: (e, ctx) => [{
9707
- role: "primary",
9708
- object: {
9709
- id: ctx.resolveId(e.name),
9710
- kind: "ray",
9711
- label: e.name,
9712
- ...SHAPE_BASE_FIELDS,
9713
- attrs: { origin: ctx.resolveId(e.origin), through: ctx.resolveId(e.through) }
9714
- }
9715
- }]
9716
- });
9443
+
9444
+ // src/stamps/geometry-2d/draft.ts
9445
+ function svgIntrinsicSize(svg) {
9446
+ const w = svg.match(/<svg[^>]*\swidth="([\d.]+)"/);
9447
+ const h = svg.match(/<svg[^>]*\sheight="([\d.]+)"/);
9448
+ if (w && h) return { width: parseFloat(w[1]), height: parseFloat(h[1]) };
9449
+ const vb = svg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/);
9450
+ if (vb) return { width: parseFloat(vb[1]), height: parseFloat(vb[2]) };
9451
+ return { width: 300, height: 200 };
9452
+ }
9453
+ function draftFromViewport(svg, appState, seq) {
9454
+ const { width, height } = svgIntrinsicSize(svg);
9455
+ const zoom = appState.zoom?.value ?? 1;
9456
+ const vw = appState.width ?? 800;
9457
+ const vh = appState.height ?? 600;
9458
+ const cx = appState.scrollX + vw / 2 / zoom;
9459
+ const cy = appState.scrollY + vh / 2 / zoom;
9460
+ return { svg, width, height, x: cx - width / 2, y: cy - height / 2, seq };
9461
+ }
9462
+ function didStateChange(seen, jsonState) {
9463
+ if (seen.last === jsonState) return false;
9464
+ seen.last = jsonState;
9465
+ return true;
9466
+ }
9467
+ var init_draft = __esm({
9468
+ "src/stamps/geometry-2d/draft.ts"() {
9717
9469
  }
9718
9470
  });
9719
- var perpendicularModule;
9720
- var init_perpendicular = __esm({
9721
- "src/stamps/geometry-2d/dsl/kinds/lines/perpendicular.ts"() {
9722
- init_names();
9723
- init_types5();
9724
- init_shared3();
9725
- perpendicularModule = defineModule({
9726
- kind: "perpendicular",
9727
- role: "lineConstruction",
9728
- category: "lines",
9729
- prefix: "l",
9730
- schema: zod.z.object({
9731
- name: NameZ,
9732
- kind: zod.z.literal("perpendicular"),
9733
- throughPoint: NameZ,
9734
- toLine: NameZ
9735
- }),
9736
- collectRefs: (e) => [e.throughPoint, e.toLine],
9737
- emit: (e, ctx) => [{
9738
- role: "primary",
9739
- object: {
9740
- id: ctx.resolveId(e.name),
9741
- kind: "line",
9742
- label: e.name,
9743
- ...SHAPE_BASE_FIELDS,
9744
- attrs: {
9745
- construction: {
9746
- kind: "perpendicular",
9747
- throughPoint: ctx.resolveId(e.throughPoint),
9748
- toLine: ctx.resolveId(e.toLine)
9749
- }
9750
- }
9471
+ function useGeometryDraftEmit({
9472
+ store,
9473
+ handleRef,
9474
+ api,
9475
+ showAxis,
9476
+ showGrid,
9477
+ onGeometryDraft,
9478
+ debounceMs = 350
9479
+ }) {
9480
+ const seqRef = React2.useRef(0);
9481
+ const seenRef = React2.useRef({ last: null });
9482
+ const timerRef = React2.useRef(null);
9483
+ const cbRef = React2.useRef(onGeometryDraft);
9484
+ cbRef.current = onGeometryDraft;
9485
+ React2.useEffect(() => {
9486
+ if (!cbRef.current) return;
9487
+ const emit = () => {
9488
+ const h = handleRef.current;
9489
+ if (!h) return;
9490
+ const state = h.getState();
9491
+ if (Object.keys(state.objects).length === 0) {
9492
+ if (seenRef.current.last !== null) {
9493
+ seenRef.current.last = null;
9494
+ cbRef.current?.(null);
9751
9495
  }
9752
- }]
9753
- });
9496
+ return;
9497
+ }
9498
+ const bbox = h.getBbox();
9499
+ const jsonState = serializeBoard(state, { bbox, showAxis, showGrid });
9500
+ if (!didStateChange(seenRef.current, jsonState)) return;
9501
+ void (async () => {
9502
+ try {
9503
+ const svg = await renderGeometrySvgFromState(jsonState);
9504
+ const appState = api?.getAppState?.() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
9505
+ seqRef.current += 1;
9506
+ cbRef.current?.(draftFromViewport(svg, appState, seqRef.current));
9507
+ } catch (err) {
9508
+ console.warn("[geometry] draft render failed:", err);
9509
+ }
9510
+ })();
9511
+ };
9512
+ const schedule = () => {
9513
+ if (timerRef.current) clearTimeout(timerRef.current);
9514
+ timerRef.current = setTimeout(emit, debounceMs);
9515
+ };
9516
+ const unsub = store.subscribe(schedule);
9517
+ return () => {
9518
+ unsub();
9519
+ if (timerRef.current) clearTimeout(timerRef.current);
9520
+ cbRef.current?.(null);
9521
+ };
9522
+ }, [store, handleRef, api, showAxis, showGrid, debounceMs]);
9523
+ }
9524
+ var init_useGeometryDraftEmit = __esm({
9525
+ "src/stamps/geometry-2d/editor/useGeometryDraftEmit.ts"() {
9526
+ init_serialize();
9527
+ init_render();
9528
+ init_draft();
9754
9529
  }
9755
9530
  });
9756
- var parallelModule;
9757
- var init_parallel = __esm({
9758
- "src/stamps/geometry-2d/dsl/kinds/lines/parallel.ts"() {
9759
- init_names();
9760
- init_types5();
9761
- init_shared3();
9762
- parallelModule = defineModule({
9763
- kind: "parallel",
9764
- role: "lineConstruction",
9765
- category: "lines",
9766
- prefix: "l",
9767
- schema: zod.z.object({
9768
- name: NameZ,
9769
- kind: zod.z.literal("parallel"),
9770
- throughPoint: NameZ,
9771
- toLine: NameZ
9772
- }),
9773
- collectRefs: (e) => [e.throughPoint, e.toLine],
9774
- emit: (e, ctx) => [{
9775
- role: "primary",
9776
- object: {
9777
- id: ctx.resolveId(e.name),
9778
- kind: "line",
9779
- label: e.name,
9780
- ...SHAPE_BASE_FIELDS,
9781
- attrs: {
9782
- construction: {
9783
- kind: "parallel",
9784
- throughPoint: ctx.resolveId(e.throughPoint),
9785
- toLine: ctx.resolveId(e.toLine)
9786
- }
9787
- }
9788
- }
9789
- }]
9790
- });
9791
- }
9792
- });
9793
- var perpBisectorModule;
9794
- var init_perpBisector = __esm({
9795
- "src/stamps/geometry-2d/dsl/kinds/lines/perpBisector.ts"() {
9796
- init_names();
9797
- init_types5();
9798
- init_shared3();
9799
- perpBisectorModule = defineModule({
9800
- kind: "perpBisector",
9801
- role: "lineConstruction",
9802
- category: "lines",
9803
- prefix: "l",
9804
- schema: zod.z.object({
9805
- name: NameZ,
9806
- kind: zod.z.literal("perpBisector"),
9807
- p1: NameZ,
9808
- p2: NameZ
9809
- }),
9810
- collectRefs: (e) => [e.p1, e.p2],
9811
- emit: (e, ctx) => [{
9812
- role: "primary",
9813
- object: {
9814
- id: ctx.resolveId(e.name),
9815
- kind: "line",
9816
- label: e.name,
9817
- ...SHAPE_BASE_FIELDS,
9818
- attrs: {
9819
- construction: {
9820
- kind: "perpBisector",
9821
- p1: ctx.resolveId(e.p1),
9822
- p2: ctx.resolveId(e.p2)
9531
+ var GeometryEditorPanelInner, GeometryEditorPanel;
9532
+ var init_EditorPanel = __esm({
9533
+ "src/stamps/geometry-2d/editor/EditorPanel.tsx"() {
9534
+ "use client";
9535
+ init_MiniBoard();
9536
+ init_serialize();
9537
+ init_render();
9538
+ init_PropertiesPopover();
9539
+ init_MultiPropertiesPopover();
9540
+ init_TransformParamPopover();
9541
+ init_snapshot();
9542
+ init_icons();
9543
+ init_scene();
9544
+ init_constants();
9545
+ init_Toast2();
9546
+ init_AiFigurePrompt();
9547
+ init_useGeometryDraftEmit();
9548
+ GeometryEditorPanelInner = React2.forwardRef(
9549
+ function GeometryEditorPanelInner2({
9550
+ store,
9551
+ onInsert,
9552
+ onClose,
9553
+ withLeftPanel = false,
9554
+ selectedTool,
9555
+ showAxis,
9556
+ showGrid,
9557
+ onHistoryChange,
9558
+ isDark,
9559
+ isMobile = false,
9560
+ onOpenDrawer,
9561
+ onUndo,
9562
+ onRedo,
9563
+ canUndo,
9564
+ canRedo,
9565
+ onSelectionChange,
9566
+ generateGeometryFigure,
9567
+ api,
9568
+ onGeometryDraft
9569
+ }, ref) {
9570
+ const { showToast } = useToast();
9571
+ const handleRef = React2.useRef(null);
9572
+ const [ready, setReady] = React2.useState(false);
9573
+ const [hasContent, setHasContent] = React2.useState(false);
9574
+ const [propsPopover, setPropsPopover] = React2.useState(null);
9575
+ const [multiSelection, setMultiSelection] = React2.useState(null);
9576
+ const [transformPopover, setTransformPopover] = React2.useState(null);
9577
+ const onSelectionChangeRef = React2.useRef(onSelectionChange);
9578
+ React2.useEffect(() => {
9579
+ onSelectionChangeRef.current = onSelectionChange;
9580
+ }, [onSelectionChange]);
9581
+ const onGeometryDraftRef = React2.useRef(onGeometryDraft);
9582
+ React2.useEffect(() => {
9583
+ onGeometryDraftRef.current = onGeometryDraft;
9584
+ }, [onGeometryDraft]);
9585
+ useEditorState({ store, onHistoryChange });
9586
+ useGeometryDraftEmit({ store, handleRef, api, showAxis, showGrid, onGeometryDraft });
9587
+ React2.useEffect(() => {
9588
+ const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
9589
+ sync();
9590
+ return store.subscribe(sync);
9591
+ }, [store]);
9592
+ const handleReady = React2.useCallback(() => {
9593
+ const h = handleRef.current;
9594
+ if (!h) return;
9595
+ setReady(true);
9596
+ h.onSelect((snap) => {
9597
+ setPropsPopover(snap);
9598
+ setMultiSelection(null);
9599
+ onSelectionChangeRef.current?.(snap.id);
9600
+ });
9601
+ h.onTransformParam((info) => setTransformPopover(info));
9602
+ h.onSelectionState((snap) => {
9603
+ if (!snap || snap.ids.length === 0) {
9604
+ setPropsPopover(null);
9605
+ setMultiSelection(null);
9606
+ onSelectionChangeRef.current?.(void 0);
9607
+ return;
9823
9608
  }
9824
- }
9825
- }
9826
- }]
9827
- });
9828
- }
9829
- });
9830
- var angleBisectorModule;
9831
- var init_angleBisector = __esm({
9832
- "src/stamps/geometry-2d/dsl/kinds/lines/angleBisector.ts"() {
9833
- init_names();
9834
- init_types5();
9835
- init_shared3();
9836
- angleBisectorModule = defineModule({
9837
- kind: "angleBisector",
9838
- role: "lineConstruction",
9839
- category: "lines",
9840
- prefix: "l",
9841
- schema: zod.z.object({
9842
- name: NameZ,
9843
- kind: zod.z.literal("angleBisector"),
9844
- p1: NameZ,
9845
- vertex: NameZ,
9846
- p2: NameZ
9847
- }),
9848
- collectRefs: (e) => [e.p1, e.vertex, e.p2],
9849
- emit: (e, ctx) => [{
9850
- role: "primary",
9851
- object: {
9852
- id: ctx.resolveId(e.name),
9853
- kind: "line",
9854
- label: e.name,
9855
- ...SHAPE_BASE_FIELDS,
9856
- attrs: {
9857
- construction: {
9858
- kind: "angleBisector",
9859
- p1: ctx.resolveId(e.p1),
9860
- vertex: ctx.resolveId(e.vertex),
9861
- p2: ctx.resolveId(e.p2)
9609
+ if (snap.ids.length === 1) {
9610
+ const id = snap.ids[0];
9611
+ const single = buildObjectSnapshot(store.getState(), id, snap.anchor);
9612
+ if (single) {
9613
+ setPropsPopover(single);
9614
+ setMultiSelection(null);
9615
+ onSelectionChangeRef.current?.(id);
9616
+ }
9617
+ return;
9862
9618
  }
9619
+ setMultiSelection(snap);
9620
+ setPropsPopover(null);
9621
+ onSelectionChangeRef.current?.(void 0);
9622
+ });
9623
+ }, [store]);
9624
+ const dismissPropsPopover = React2.useCallback(() => {
9625
+ setPropsPopover(null);
9626
+ onSelectionChangeRef.current?.(void 0);
9627
+ }, []);
9628
+ const dismissMultiPopover = React2.useCallback(() => {
9629
+ setMultiSelection(null);
9630
+ handleRef.current?.clearSelection();
9631
+ onSelectionChangeRef.current?.(void 0);
9632
+ }, []);
9633
+ const applyMultiColor = React2.useCallback((color) => {
9634
+ const ids = multiSelection?.ids ?? [];
9635
+ const h = handleRef.current;
9636
+ if (!h) return;
9637
+ for (const id of ids) {
9638
+ h.mutateObject(id, { attrs: { strokeColor: color, color } });
9863
9639
  }
9864
- }
9865
- }]
9866
- });
9867
- }
9868
- });
9869
- var lineThroughModule;
9870
- var init_lineThrough = __esm({
9871
- "src/stamps/geometry-2d/dsl/kinds/lines/lineThrough.ts"() {
9872
- init_names();
9873
- init_types5();
9874
- init_shared3();
9875
- lineThroughModule = defineModule({
9876
- kind: "lineThrough",
9877
- role: "lineConstruction",
9878
- category: "lines",
9879
- prefix: "l",
9880
- schema: zod.z.object({
9881
- name: NameZ,
9882
- kind: zod.z.literal("lineThrough"),
9883
- points: zod.z.array(NameZ).min(2)
9884
- }),
9885
- collectRefs: (e) => [...e.points],
9886
- refSpecs: [{ field: "points", role: "point", many: true }],
9887
- emit: (e, ctx) => [{
9888
- role: "primary",
9889
- object: {
9890
- id: ctx.resolveId(e.name),
9891
- kind: "line",
9892
- label: e.name,
9893
- ...SHAPE_BASE_FIELDS,
9894
- attrs: {
9895
- construction: {
9896
- kind: "lineThrough",
9897
- points: e.points.map((p) => ctx.resolveId(p))
9898
- }
9640
+ }, [multiSelection]);
9641
+ const applyMultiDelete = React2.useCallback(() => {
9642
+ const ids = multiSelection?.ids ?? [];
9643
+ const h = handleRef.current;
9644
+ if (!h) return;
9645
+ for (const id of ids) {
9646
+ h.mutateObject(id, { remove: true });
9899
9647
  }
9900
- }
9901
- }]
9902
- });
9903
- }
9904
- });
9905
- var radicalAxisModule;
9906
- var init_radicalAxis = __esm({
9907
- "src/stamps/geometry-2d/dsl/kinds/lines/radicalAxis.ts"() {
9908
- init_names();
9909
- init_types5();
9910
- init_shared3();
9911
- radicalAxisModule = defineModule({
9912
- kind: "radicalAxis",
9913
- role: "lineConstruction",
9914
- category: "lines",
9915
- prefix: "l",
9916
- schema: zod.z.object({
9917
- name: NameZ,
9918
- kind: zod.z.literal("radicalAxis"),
9919
- circle1: NameZ,
9920
- circle2: NameZ
9921
- }),
9922
- collectRefs: (e) => [e.circle1, e.circle2],
9923
- refSpecs: [
9924
- { field: "circle1", role: "circle" },
9925
- { field: "circle2", role: "circle" }
9926
- ],
9927
- emit: (e, ctx) => [{
9928
- role: "primary",
9929
- object: {
9930
- id: ctx.resolveId(e.name),
9931
- kind: "line",
9932
- label: e.name,
9933
- ...SHAPE_BASE_FIELDS,
9934
- attrs: {
9935
- construction: {
9936
- kind: "radicalAxis",
9937
- circle1: ctx.resolveId(e.circle1),
9938
- circle2: ctx.resolveId(e.circle2)
9648
+ h.clearSelection();
9649
+ setMultiSelection(null);
9650
+ onSelectionChangeRef.current?.(void 0);
9651
+ }, [multiSelection]);
9652
+ const performInsert = React2.useCallback(() => {
9653
+ if (!handleRef.current) return false;
9654
+ const h = handleRef.current;
9655
+ const state = h.getState();
9656
+ if (Object.keys(state.objects).length === 0) return false;
9657
+ const bbox = h.getBbox();
9658
+ const jsonState = serializeBoard(state, { bbox, showAxis, showGrid });
9659
+ void (async () => {
9660
+ try {
9661
+ const svgString = await renderGeometrySvgFromState(jsonState);
9662
+ onInsert(jsonState, svgString);
9663
+ onGeometryDraftRef.current?.(null);
9664
+ } catch (err) {
9665
+ console.error("Geometry insert failed:", err);
9939
9666
  }
9940
- }
9941
- }
9942
- }]
9943
- });
9944
- }
9945
- });
9946
- var tangentModule;
9947
- var init_tangent = __esm({
9948
- "src/stamps/geometry-2d/dsl/kinds/lines/tangent.ts"() {
9949
- init_names();
9950
- init_types5();
9951
- init_shared3();
9952
- tangentModule = defineModule({
9953
- kind: "tangent",
9954
- role: "lineConstruction",
9955
- category: "lines",
9956
- prefix: "l",
9957
- schema: zod.z.object({
9958
- name: NameZ,
9959
- kind: zod.z.literal("tangent"),
9960
- throughPoint: NameZ,
9961
- toCircle: NameZ,
9962
- branch: zod.z.union([zod.z.literal(0), zod.z.literal(1), zod.z.literal("on")]).optional()
9963
- }),
9964
- collectRefs: (e) => [e.throughPoint, e.toCircle],
9965
- emit: (e, ctx) => {
9966
- const construction = {
9967
- kind: "tangent",
9968
- throughPoint: ctx.resolveId(e.throughPoint),
9969
- toCircle: ctx.resolveId(e.toCircle)
9667
+ })();
9668
+ return true;
9669
+ }, [onInsert, showAxis, showGrid]);
9670
+ const loadAiFigure = React2.useCallback((generated) => {
9671
+ handleRef.current?.clearSelection();
9672
+ setPropsPopover(null);
9673
+ setMultiSelection(null);
9674
+ setTransformPopover(null);
9675
+ onSelectionChangeRef.current?.(void 0);
9676
+ const current = store.getState();
9677
+ store.dispatch({
9678
+ type: "LOAD",
9679
+ payload: { state: { ...generated, meta: current.meta } }
9680
+ });
9681
+ }, [store]);
9682
+ React2.useImperativeHandle(ref, () => ({
9683
+ insert: performInsert,
9684
+ hasContent: () => Object.keys(handleRef.current?.getState().objects ?? {}).length > 0,
9685
+ selectObject: (id) => handleRef.current?.highlight(id)
9686
+ }), [performInsert]);
9687
+ const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
9688
+ position: "absolute",
9689
+ top: "50%",
9690
+ left: withLeftPanel ? "calc(50% + 120px)" : "50%",
9691
+ transform: "translate(-50%, -50%)",
9692
+ zIndex: 40
9970
9693
  };
9971
- if (e.branch !== void 0) construction.branch = e.branch;
9972
- return [{
9973
- role: "primary",
9974
- object: {
9975
- id: ctx.resolveId(e.name),
9976
- kind: "line",
9977
- label: e.name,
9978
- ...SHAPE_BASE_FIELDS,
9979
- attrs: { construction }
9980
- }
9981
- }];
9982
- }
9983
- });
9984
- }
9985
- });
9986
- var polygonModule;
9987
- var init_polygon3 = __esm({
9988
- "src/stamps/geometry-2d/dsl/kinds/polygons/polygon.ts"() {
9989
- init_names();
9990
- init_types5();
9991
- init_shared3();
9992
- polygonModule = defineModule({
9993
- kind: "polygon",
9994
- role: "polygon",
9995
- category: "polygons",
9996
- prefix: "poly",
9997
- schema: zod.z.object({
9998
- name: NameZ,
9999
- kind: zod.z.literal("polygon"),
10000
- vertices: zod.z.array(NameZ).min(3)
10001
- }),
10002
- collectRefs: (e) => [...e.vertices],
10003
- emit: (e, ctx) => [{
10004
- role: "primary",
10005
- object: {
10006
- id: ctx.resolveId(e.name),
10007
- kind: "polygon",
10008
- label: e.name,
10009
- ...SHAPE_BASE_FIELDS,
10010
- attrs: { vertices: e.vertices.map((v) => ctx.resolveId(v)) }
10011
- }
10012
- }]
10013
- });
10014
- }
10015
- });
10016
- var circleCPModule;
10017
- var init_circleCP = __esm({
10018
- "src/stamps/geometry-2d/dsl/kinds/circles/circleCP.ts"() {
10019
- init_names();
10020
- init_types5();
10021
- init_shared3();
10022
- circleCPModule = defineModule({
10023
- kind: "circleCP",
10024
- role: "circle",
10025
- category: "circles",
10026
- prefix: "c",
10027
- schema: zod.z.object({
10028
- name: NameZ,
10029
- kind: zod.z.literal("circleCP"),
10030
- center: NameZ,
10031
- surfacePoint: NameZ,
10032
- visible: zod.z.boolean().optional()
10033
- }),
10034
- collectRefs: (e) => [e.center, e.surfacePoint],
10035
- emit: (e, ctx) => [{
10036
- role: "primary",
10037
- object: {
10038
- id: ctx.resolveId(e.name),
10039
- kind: "circle",
10040
- label: e.name,
10041
- ...SHAPE_BASE_FIELDS,
10042
- visible: e.visible ?? true,
10043
- attrs: { center: ctx.resolveId(e.center), surfacePoint: ctx.resolveId(e.surfacePoint) }
10044
- }
10045
- }]
10046
- });
10047
- }
10048
- });
10049
- var circle3Module;
10050
- var init_circle3 = __esm({
10051
- "src/stamps/geometry-2d/dsl/kinds/circles/circle3.ts"() {
10052
- init_names();
10053
- init_types5();
10054
- init_shared3();
10055
- circle3Module = defineModule({
10056
- kind: "circle3",
10057
- role: "circle",
10058
- category: "circles",
10059
- prefix: "c",
10060
- schema: zod.z.object({
10061
- name: NameZ,
10062
- kind: zod.z.literal("circle3"),
10063
- p1: NameZ,
10064
- p2: NameZ,
10065
- p3: NameZ
10066
- }),
10067
- collectRefs: (e) => [e.p1, e.p2, e.p3],
10068
- emit: (e, ctx) => [{
10069
- role: "primary",
10070
- object: {
10071
- id: ctx.resolveId(e.name),
10072
- kind: "circle",
10073
- label: e.name,
10074
- ...SHAPE_BASE_FIELDS,
10075
- attrs: {
10076
- construction: {
10077
- kind: "circumscribed",
10078
- p1: ctx.resolveId(e.p1),
10079
- p2: ctx.resolveId(e.p2),
10080
- p3: ctx.resolveId(e.p3)
10081
- }
10082
- }
10083
- }
10084
- }]
10085
- });
10086
- }
10087
- });
10088
- var circleDiameterModule;
10089
- var init_circleDiameter = __esm({
10090
- "src/stamps/geometry-2d/dsl/kinds/circles/circleDiameter.ts"() {
10091
- init_names();
10092
- init_types5();
10093
- init_shared3();
10094
- circleDiameterModule = defineModule({
10095
- kind: "circleDiameter",
10096
- role: "circle",
10097
- category: "circles",
10098
- prefix: "c",
10099
- schema: zod.z.object({
10100
- name: NameZ,
10101
- kind: zod.z.literal("circleDiameter"),
10102
- p1: NameZ,
10103
- p2: NameZ,
10104
- visible: zod.z.boolean().optional()
10105
- }),
10106
- collectRefs: (e) => [e.p1, e.p2],
10107
- emit: (e, ctx) => [{
10108
- role: "primary",
10109
- object: {
10110
- id: ctx.resolveId(e.name),
10111
- kind: "circle",
10112
- label: e.name,
10113
- ...SHAPE_BASE_FIELDS,
10114
- visible: e.visible ?? true,
10115
- attrs: {
10116
- construction: {
10117
- kind: "diameter",
10118
- p1: ctx.resolveId(e.p1),
10119
- p2: ctx.resolveId(e.p2)
10120
- }
10121
- }
10122
- }
10123
- }]
10124
- });
10125
- }
10126
- });
10127
- var secondIntersectionModule;
10128
- var init_secondIntersection2 = __esm({
10129
- "src/stamps/geometry-2d/dsl/kinds/points/secondIntersection.ts"() {
10130
- init_names();
10131
- init_types5();
10132
- init_shared3();
10133
- secondIntersectionModule = defineModule({
10134
- kind: "secondIntersection",
10135
- role: "point",
10136
- category: "points",
10137
- prefix: "p",
10138
- schema: zod.z.object({
10139
- name: NameZ,
10140
- kind: zod.z.literal("secondIntersection"),
10141
- line: NameZ,
10142
- circle: NameZ,
10143
- other: NameZ
10144
- }),
10145
- collectRefs: (e) => [e.line, e.circle, e.other],
10146
- refSpecs: [
10147
- { field: "line", role: "line-like" },
10148
- { field: "circle", role: "circle" },
10149
- { field: "other", role: "point" }
10150
- ],
10151
- emit: (e, ctx) => [{
10152
- role: "primary",
10153
- object: emitPointObject(
10154
- ctx.resolveId(e.name),
10155
- e.name,
10156
- {
10157
- kind: "secondIntersection",
10158
- line: ctx.resolveId(e.line),
10159
- circle: ctx.resolveId(e.circle),
10160
- other: ctx.resolveId(e.other)
10161
- }
10162
- )
10163
- }]
10164
- });
10165
- }
10166
- });
10167
- var circleIntersectionModule;
10168
- var init_circleIntersection2 = __esm({
10169
- "src/stamps/geometry-2d/dsl/kinds/points/circleIntersection.ts"() {
10170
- init_names();
10171
- init_types5();
10172
- init_shared3();
10173
- circleIntersectionModule = defineModule({
10174
- kind: "circleIntersection",
10175
- role: "point",
10176
- category: "points",
10177
- prefix: "p",
10178
- schema: zod.z.object({
10179
- name: NameZ,
10180
- kind: zod.z.literal("circleIntersection"),
10181
- c1: NameZ,
10182
- c2: NameZ,
10183
- which: zod.z.union([zod.z.literal(0), zod.z.literal(1)])
10184
- }),
10185
- collectRefs: (e) => [e.c1, e.c2],
10186
- refSpecs: [
10187
- { field: "c1", role: "circle" },
10188
- { field: "c2", role: "circle" }
10189
- ],
10190
- emit: (e, ctx) => [{
10191
- role: "primary",
10192
- object: emitPointObject(
10193
- ctx.resolveId(e.name),
10194
- e.name,
10195
- {
10196
- kind: "circleIntersection",
10197
- c1: ctx.resolveId(e.c1),
10198
- c2: ctx.resolveId(e.c2),
10199
- which: e.which
10200
- }
10201
- )
10202
- }]
10203
- });
10204
- }
10205
- });
10206
- var circleSecondIntersectionModule;
10207
- var init_circleSecondIntersection2 = __esm({
10208
- "src/stamps/geometry-2d/dsl/kinds/points/circleSecondIntersection.ts"() {
10209
- init_names();
10210
- init_types5();
10211
- init_shared3();
10212
- circleSecondIntersectionModule = defineModule({
10213
- kind: "circleSecondIntersection",
10214
- role: "point",
10215
- category: "points",
10216
- prefix: "p",
10217
- schema: zod.z.object({
10218
- name: NameZ,
10219
- kind: zod.z.literal("circleSecondIntersection"),
10220
- c1: NameZ,
10221
- c2: NameZ,
10222
- exclude: NameZ
10223
- }),
10224
- collectRefs: (e) => [e.c1, e.c2, e.exclude],
10225
- refSpecs: [
10226
- { field: "c1", role: "circle" },
10227
- { field: "c2", role: "circle" },
10228
- { field: "exclude", role: "point" }
10229
- ],
10230
- emit: (e, ctx) => [{
10231
- role: "primary",
10232
- object: emitPointObject(
10233
- ctx.resolveId(e.name),
10234
- e.name,
10235
- {
10236
- kind: "circleSecondIntersection",
10237
- c1: ctx.resolveId(e.c1),
10238
- c2: ctx.resolveId(e.c2),
10239
- exclude: ctx.resolveId(e.exclude)
10240
- }
10241
- )
10242
- }]
10243
- });
10244
- }
10245
- });
10246
- var tangencyPointModule;
10247
- var init_tangencyPoint2 = __esm({
10248
- "src/stamps/geometry-2d/dsl/kinds/points/tangencyPoint.ts"() {
10249
- init_names();
10250
- init_types5();
10251
- init_shared3();
10252
- tangencyPointModule = defineModule({
10253
- kind: "tangencyPoint",
10254
- role: "point",
10255
- category: "points",
10256
- prefix: "p",
10257
- schema: zod.z.object({
10258
- name: NameZ,
10259
- kind: zod.z.literal("tangencyPoint"),
10260
- circle: NameZ,
10261
- onLine: NameZ
10262
- }),
10263
- collectRefs: (e) => [e.circle, e.onLine],
10264
- refSpecs: [
10265
- { field: "circle", role: "circle" },
10266
- { field: "onLine", role: "line-like" }
10267
- ],
10268
- emit: (e, ctx) => [{
10269
- role: "primary",
10270
- object: emitPointObject(
10271
- ctx.resolveId(e.name),
10272
- e.name,
10273
- {
10274
- kind: "tangencyPoint",
10275
- circle: ctx.resolveId(e.circle),
10276
- onLine: ctx.resolveId(e.onLine)
10277
- }
10278
- )
10279
- }]
10280
- });
10281
- }
10282
- });
10283
- var tangentPointExtModule;
10284
- var init_tangentPointExt2 = __esm({
10285
- "src/stamps/geometry-2d/dsl/kinds/points/tangentPointExt.ts"() {
10286
- init_names();
10287
- init_types5();
10288
- init_shared3();
10289
- tangentPointExtModule = defineModule({
10290
- kind: "tangentPointExt",
10291
- role: "point",
10292
- category: "points",
10293
- prefix: "p",
10294
- schema: zod.z.object({
10295
- name: NameZ,
10296
- kind: zod.z.literal("tangentPointExt"),
10297
- from: NameZ,
10298
- circle: NameZ,
10299
- which: zod.z.union([zod.z.literal(0), zod.z.literal(1)])
10300
- }),
10301
- collectRefs: (e) => [e.from, e.circle],
10302
- refSpecs: [
10303
- { field: "from", role: "point" },
10304
- { field: "circle", role: "circle" }
10305
- ],
10306
- emit: (e, ctx) => [{
10307
- role: "primary",
10308
- object: emitPointObject(
10309
- ctx.resolveId(e.name),
10310
- e.name,
9694
+ return /* @__PURE__ */ jsxRuntime.jsxs(
9695
+ "div",
10311
9696
  {
10312
- kind: "tangentPointExt",
10313
- from: ctx.resolveId(e.from),
10314
- circle: ctx.resolveId(e.circle),
10315
- which: e.which
10316
- }
10317
- )
10318
- }]
10319
- });
10320
- }
10321
- });
10322
- var circleCRModule;
10323
- var init_circleCR = __esm({
10324
- "src/stamps/geometry-2d/dsl/kinds/circles/circleCR.ts"() {
10325
- init_names();
10326
- init_types5();
10327
- init_shared3();
10328
- circleCRModule = defineModule({
10329
- kind: "circleCR",
10330
- role: "circle",
10331
- category: "circles",
10332
- prefix: "c",
10333
- schema: zod.z.object({
10334
- name: NameZ,
10335
- kind: zod.z.literal("circleCR"),
10336
- center: NameZ,
10337
- radius: zod.z.number().positive()
10338
- }),
10339
- collectRefs: (e) => [e.center],
10340
- refSpecs: [{ field: "center", role: "point" }],
10341
- emit: (e, ctx) => [{
10342
- role: "primary",
10343
- object: {
10344
- id: ctx.resolveId(e.name),
10345
- kind: "circle",
10346
- label: e.name,
10347
- ...SHAPE_BASE_FIELDS,
10348
- attrs: { center: ctx.resolveId(e.center), radius: e.radius }
10349
- }
10350
- }]
10351
- });
10352
- }
10353
- });
10354
- var incircleModule;
10355
- var init_incircle = __esm({
10356
- "src/stamps/geometry-2d/dsl/kinds/circles/incircle.ts"() {
10357
- init_names();
10358
- init_types5();
10359
- init_shared3();
10360
- incircleModule = defineModule({
10361
- kind: "incircle",
10362
- role: "circle",
10363
- category: "circles",
10364
- prefix: "c",
10365
- schema: zod.z.object({
10366
- name: NameZ,
10367
- kind: zod.z.literal("incircle"),
10368
- vertices: zod.z.tuple([NameZ, NameZ, NameZ])
10369
- }),
10370
- collectRefs: (e) => [...e.vertices],
10371
- refSpecs: [{ field: "vertices", role: "point", many: true }],
10372
- emit: (e, ctx) => [{
10373
- role: "primary",
10374
- object: {
10375
- id: ctx.resolveId(e.name),
10376
- kind: "circle",
10377
- label: e.name,
10378
- ...SHAPE_BASE_FIELDS,
10379
- attrs: {
10380
- kind: "incircle",
10381
- vertices: [
10382
- ctx.resolveId(e.vertices[0]),
10383
- ctx.resolveId(e.vertices[1]),
10384
- ctx.resolveId(e.vertices[2])
10385
- ]
10386
- }
10387
- }
10388
- }]
10389
- });
10390
- }
10391
- });
10392
- var excircleModule;
10393
- var init_excircle = __esm({
10394
- "src/stamps/geometry-2d/dsl/kinds/circles/excircle.ts"() {
10395
- init_names();
10396
- init_types5();
10397
- init_shared3();
10398
- excircleModule = defineModule({
10399
- kind: "excircle",
10400
- role: "circle",
10401
- category: "circles",
10402
- prefix: "c",
10403
- schema: zod.z.object({
10404
- name: NameZ,
10405
- kind: zod.z.literal("excircle"),
10406
- vertices: zod.z.tuple([NameZ, NameZ, NameZ]),
10407
- opposite: NameZ
10408
- }),
10409
- collectRefs: (e) => [...e.vertices],
10410
- refSpecs: [
10411
- { field: "vertices", role: "point", many: true },
10412
- { field: "opposite", role: "point" }
10413
- ],
10414
- emit: (e, ctx) => [{
10415
- role: "primary",
10416
- object: {
10417
- id: ctx.resolveId(e.name),
10418
- kind: "circle",
10419
- label: e.name,
10420
- ...SHAPE_BASE_FIELDS,
10421
- attrs: {
10422
- construction: {
10423
- kind: "excircle",
10424
- p1: ctx.resolveId(e.vertices[0]),
10425
- p2: ctx.resolveId(e.vertices[1]),
10426
- p3: ctx.resolveId(e.vertices[2]),
10427
- opposite: ctx.resolveId(e.opposite)
10428
- }
10429
- }
10430
- }
10431
- }]
10432
- });
10433
- }
10434
- });
10435
- var arcMidpointModule;
10436
- var init_arcMidpoint2 = __esm({
10437
- "src/stamps/geometry-2d/dsl/kinds/points/arcMidpoint.ts"() {
10438
- init_names();
10439
- init_types5();
10440
- init_shared3();
10441
- arcMidpointModule = defineModule({
10442
- kind: "arcMidpoint",
10443
- role: "point",
10444
- category: "points",
10445
- prefix: "p",
10446
- schema: zod.z.object({
10447
- name: NameZ,
10448
- kind: zod.z.literal("arcMidpoint"),
10449
- circle: NameZ,
10450
- a: NameZ,
10451
- b: NameZ,
10452
- notContaining: NameZ
10453
- }),
10454
- collectRefs: (e) => [e.circle, e.a, e.b, e.notContaining],
10455
- emit: (e, ctx) => [{
10456
- role: "primary",
10457
- object: emitPointObject(ctx.resolveId(e.name), e.name, {
10458
- kind: "arcMidpoint",
10459
- circle: ctx.resolveId(e.circle),
10460
- a: ctx.resolveId(e.a),
10461
- b: ctx.resolveId(e.b),
10462
- notContaining: ctx.resolveId(e.notContaining)
10463
- })
10464
- }]
10465
- });
10466
- }
10467
- });
10468
- var excenterModule;
10469
- var init_excenter2 = __esm({
10470
- "src/stamps/geometry-2d/dsl/kinds/points/excenter.ts"() {
10471
- init_names();
10472
- init_types5();
10473
- init_shared3();
10474
- excenterModule = defineModule({
10475
- kind: "excenter",
10476
- role: "point",
10477
- category: "points",
10478
- prefix: "p",
10479
- schema: zod.z.object({
10480
- name: NameZ,
10481
- kind: zod.z.literal("excenter"),
10482
- vertices: zod.z.tuple([NameZ, NameZ, NameZ]),
10483
- opposite: NameZ
10484
- }),
10485
- collectRefs: (e) => [...e.vertices],
10486
- emit: (e, ctx) => [{
10487
- role: "primary",
10488
- object: emitPointObject(ctx.resolveId(e.name), e.name, {
10489
- kind: "excenter",
10490
- vertices: resolveTriangleVertices(ctx, e.vertices),
10491
- opposite: ctx.resolveId(e.opposite)
10492
- })
10493
- }]
10494
- });
10495
- }
10496
- });
10497
- var reflectPointModule;
10498
- var init_reflectPoint = __esm({
10499
- "src/stamps/geometry-2d/dsl/kinds/points/reflectPoint.ts"() {
10500
- init_names();
10501
- init_types5();
10502
- init_shared3();
10503
- reflectPointModule = defineModule({
10504
- kind: "reflectPoint",
10505
- role: "point",
10506
- category: "points",
10507
- prefix: "p",
10508
- schema: zod.z.object({ name: NameZ, kind: zod.z.literal("reflectPoint"), of: NameZ, through: NameZ }),
10509
- collectRefs: (e) => [e.of, e.through],
10510
- emit: (e, ctx) => [{
10511
- role: "primary",
10512
- object: emitPointObject(ctx.resolveId(e.name), e.name, {
10513
- kind: "transformed",
10514
- source: ctx.resolveId(e.of),
10515
- transform: { kind: "reflectPoint", center: ctx.resolveId(e.through) }
10516
- })
10517
- }]
10518
- });
10519
- }
10520
- });
10521
- var reflectLineModule;
10522
- var init_reflectLine = __esm({
10523
- "src/stamps/geometry-2d/dsl/kinds/points/reflectLine.ts"() {
10524
- init_names();
10525
- init_types5();
10526
- init_shared3();
10527
- reflectLineModule = defineModule({
10528
- kind: "reflectLine",
10529
- role: "point",
10530
- category: "points",
10531
- prefix: "p",
10532
- schema: zod.z.object({ name: NameZ, kind: zod.z.literal("reflectLine"), of: NameZ, through: NameZ }),
10533
- collectRefs: (e) => [e.of, e.through],
10534
- emit: (e, ctx) => [{
10535
- role: "primary",
10536
- object: emitPointObject(ctx.resolveId(e.name), e.name, {
10537
- kind: "transformed",
10538
- source: ctx.resolveId(e.of),
10539
- transform: { kind: "reflectLine", line: ctx.resolveId(e.through) }
10540
- })
10541
- }]
10542
- });
10543
- }
10544
- });
10545
- function withScaleOffset(base, d) {
10546
- const out = { ...base };
10547
- if (d.scale !== void 0) out.scale = d.scale;
10548
- if (d.offset !== void 0) out.offset = d.offset;
10549
- return out;
10550
- }
10551
- var ScaleOffsetZ, DistanceZ, pointAtDistanceModule;
10552
- var init_pointAtDistance2 = __esm({
10553
- "src/stamps/geometry-2d/dsl/kinds/points/pointAtDistance.ts"() {
10554
- init_names();
10555
- init_types5();
10556
- init_shared3();
10557
- ScaleOffsetZ = {
10558
- scale: zod.z.number().positive().optional(),
10559
- offset: zod.z.number().optional()
10560
- };
10561
- DistanceZ = zod.z.discriminatedUnion("kind", [
10562
- zod.z.object({ kind: zod.z.literal("circleRadius"), circle: NameZ, ...ScaleOffsetZ }),
10563
- zod.z.object({ kind: zod.z.literal("segmentLength"), p1: NameZ, p2: NameZ, ...ScaleOffsetZ }),
10564
- zod.z.object({ kind: zod.z.literal("literal"), value: zod.z.number().positive(), ...ScaleOffsetZ })
10565
- ]);
10566
- pointAtDistanceModule = defineModule({
10567
- kind: "pointAtDistance",
10568
- role: "point",
10569
- category: "points",
10570
- prefix: "p",
10571
- schema: zod.z.object({
10572
- name: NameZ,
10573
- kind: zod.z.literal("pointAtDistance"),
10574
- from: NameZ,
10575
- through: NameZ,
10576
- distance: DistanceZ
10577
- }),
10578
- collectRefs: (e) => {
10579
- const d = e.distance;
10580
- const extra = d.kind === "circleRadius" ? [d.circle] : d.kind === "segmentLength" ? [d.p1, d.p2] : [];
10581
- return [e.from, e.through, ...extra];
10582
- },
10583
- // TODO(Mức 1 defer): distance.{circle,p1,p2} là nested trong `distance` — refSpec
10584
- // phẳng đọc top-level không với tới, validate riêng nếu cần. Hiện validate from/through.
10585
- refSpecs: [
10586
- { field: "from", role: "point" },
10587
- { field: "through", role: "point" }
10588
- ],
10589
- emit: (e, ctx) => {
10590
- const d = e.distance;
10591
- const distance = d.kind === "circleRadius" ? withScaleOffset({ kind: "circleRadius", circle: ctx.resolveId(d.circle) }, d) : d.kind === "segmentLength" ? withScaleOffset({ kind: "segmentLength", p1: ctx.resolveId(d.p1), p2: ctx.resolveId(d.p2) }, d) : withScaleOffset({ kind: "literal", value: d.value }, d);
10592
- return [{
10593
- role: "primary",
10594
- object: emitPointObject(ctx.resolveId(e.name), e.name, {
10595
- kind: "pointAtDistance",
10596
- from: ctx.resolveId(e.from),
10597
- through: ctx.resolveId(e.through),
10598
- distance
10599
- })
10600
- }];
9697
+ role: "dialog",
9698
+ "aria-label": "D\u1EF1ng h\xECnh h\u1ECDc",
9699
+ "data-testid": "geometry-editor-panel",
9700
+ "data-stamp-area": "true",
9701
+ "data-mobile-editor": isMobile ? "true" : void 0,
9702
+ style: wrapperStyle,
9703
+ className: [
9704
+ isDark ? "theme--dark " : "",
9705
+ "relative flex flex-col overflow-hidden bg-white",
9706
+ isMobile ? "h-full w-full" : `${STAMP_PANEL_DESKTOP} rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5`
9707
+ ].join(" "),
9708
+ children: [
9709
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-white", children: [
9710
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx(
9711
+ "button",
9712
+ {
9713
+ type: "button",
9714
+ onClick: onOpenDrawer,
9715
+ "aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
9716
+ className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
9717
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
9718
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
9719
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
9720
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
9721
+ ] })
9722
+ }
9723
+ ),
9724
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
9725
+ /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
9726
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "3,18 12,3 21,18" }),
9727
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "3", r: "1.5", fill: "currentColor" }),
9728
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "3", cy: "18", r: "1.5", fill: "currentColor" }),
9729
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "21", cy: "18", r: "1.5", fill: "currentColor" })
9730
+ ] }),
9731
+ "D\u1EF1ng h\xECnh h\u1ECDc"
9732
+ ] }),
9733
+ isMobile && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9734
+ /* @__PURE__ */ jsxRuntime.jsx(
9735
+ "button",
9736
+ {
9737
+ type: "button",
9738
+ onClick: onUndo,
9739
+ disabled: !canUndo,
9740
+ "aria-label": "Ho\xE0n t\xE1c",
9741
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
9742
+ "data-testid": "undo-btn-mobile",
9743
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
9744
+ children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon3, {})
9745
+ }
9746
+ ),
9747
+ /* @__PURE__ */ jsxRuntime.jsx(
9748
+ "button",
9749
+ {
9750
+ type: "button",
9751
+ onClick: onRedo,
9752
+ disabled: !canRedo,
9753
+ "aria-label": "L\xE0m l\u1EA1i",
9754
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
9755
+ "data-testid": "redo-btn-mobile",
9756
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
9757
+ children: /* @__PURE__ */ jsxRuntime.jsx(RedoIcon3, {})
9758
+ }
9759
+ ),
9760
+ /* @__PURE__ */ jsxRuntime.jsx(
9761
+ "button",
9762
+ {
9763
+ type: "button",
9764
+ onClick: performInsert,
9765
+ disabled: !ready || !hasContent,
9766
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
9767
+ "data-testid": "geometry-insert-btn-mobile",
9768
+ className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
9769
+ children: "Ch\xE8n"
9770
+ }
9771
+ )
9772
+ ] }),
9773
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onClose, "aria-label": "\u0110\xF3ng", className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
9774
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
9775
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
9776
+ ] }) })
9777
+ ] }),
9778
+ generateGeometryFigure && /* @__PURE__ */ jsxRuntime.jsx(AiFigurePrompt, { generator: generateGeometryFigure, onGenerated: loadAiFigure }),
9779
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
9780
+ MiniBoard2D,
9781
+ {
9782
+ ref: handleRef,
9783
+ store,
9784
+ selectedTool,
9785
+ showAxis,
9786
+ showGrid,
9787
+ onReady: handleReady,
9788
+ isDark,
9789
+ toast: showToast
9790
+ }
9791
+ ) }) }),
9792
+ propsPopover && (propsPopover.kind === "point" ? /* @__PURE__ */ jsxRuntime.jsx(
9793
+ PropertiesPopover,
9794
+ {
9795
+ kind: "point",
9796
+ anchor: propsPopover.screenCoords,
9797
+ isDark,
9798
+ currentName: propsPopover.name,
9799
+ currentColor: propsPopover.color,
9800
+ currentDash: propsPopover.dash,
9801
+ currentWidth: propsPopover.width,
9802
+ currentFace: propsPopover.face,
9803
+ currentShowLabel: propsPopover.showLabel,
9804
+ getAllNames: () => handleRef.current?.getAllPointNames() ?? [],
9805
+ onClose: dismissPropsPopover,
9806
+ onMutate: (patch) => {
9807
+ handleRef.current?.mutateObject(propsPopover.id, patch);
9808
+ if (patch.remove) dismissPropsPopover();
9809
+ if (typeof patch.valueLabel === "boolean" || patch.attrs) {
9810
+ setPropsPopover((cur) => cur ? { ...cur, showValue: patch.valueLabel ?? cur.showValue } : cur);
9811
+ }
9812
+ }
9813
+ }
9814
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
9815
+ PropertiesPopover,
9816
+ {
9817
+ kind: propsPopover.kind,
9818
+ anchor: propsPopover.screenCoords,
9819
+ isDark,
9820
+ currentName: propsPopover.name,
9821
+ currentColor: propsPopover.color,
9822
+ currentDash: propsPopover.dash,
9823
+ currentWidth: propsPopover.width,
9824
+ currentShowLabel: propsPopover.showLabel,
9825
+ currentShowValue: propsPopover.showValue,
9826
+ getAllNames: () => handleRef.current?.getAllPointNames() ?? [],
9827
+ onClose: dismissPropsPopover,
9828
+ onMutate: (patch) => {
9829
+ handleRef.current?.mutateObject(propsPopover.id, patch);
9830
+ if (patch.remove) dismissPropsPopover();
9831
+ if (typeof patch.valueLabel === "boolean") {
9832
+ setPropsPopover((cur) => cur ? { ...cur, showValue: patch.valueLabel ?? cur.showValue } : cur);
9833
+ }
9834
+ if (patch.attrs && "withLabel" in patch.attrs) {
9835
+ setPropsPopover((cur) => cur ? { ...cur, showLabel: !!patch.attrs?.withLabel } : cur);
9836
+ }
9837
+ }
9838
+ }
9839
+ )),
9840
+ multiSelection && multiSelection.ids.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(
9841
+ MultiPropertiesPopover,
9842
+ {
9843
+ anchor: multiSelection.anchor,
9844
+ count: multiSelection.ids.length,
9845
+ isDark,
9846
+ onColor: applyMultiColor,
9847
+ onDelete: applyMultiDelete,
9848
+ onClose: dismissMultiPopover
9849
+ }
9850
+ ),
9851
+ transformPopover && (transformPopover.tool === "rotate" || transformPopover.tool === "dilate" || transformPopover.tool === "regularPolygon" || transformPopover.tool === "circleCR") && /* @__PURE__ */ jsxRuntime.jsx(
9852
+ TransformParamPopover,
9853
+ {
9854
+ kind: transformPopover.tool,
9855
+ anchor: transformPopover.anchor,
9856
+ defaultValue: transformPopover.tool === "rotate" ? 90 : transformPopover.tool === "dilate" ? 2 : transformPopover.tool === "circleCR" ? 3 : 6,
9857
+ isDark,
9858
+ onConfirm: (v) => {
9859
+ handleRef.current?.confirmTransformParam(v);
9860
+ setTransformPopover(null);
9861
+ },
9862
+ onCancel: () => {
9863
+ handleRef.current?.cancelTransformParam();
9864
+ setTransformPopover(null);
9865
+ }
9866
+ }
9867
+ ),
9868
+ !isMobile && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
9869
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
9870
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
9871
+ /* @__PURE__ */ jsxRuntime.jsx(
9872
+ "button",
9873
+ {
9874
+ onClick: onClose,
9875
+ className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
9876
+ children: "Hu\u1EF7"
9877
+ }
9878
+ ),
9879
+ /* @__PURE__ */ jsxRuntime.jsx(
9880
+ "button",
9881
+ {
9882
+ onClick: performInsert,
9883
+ disabled: !ready || !hasContent,
9884
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
9885
+ "data-testid": "geometry-insert-btn",
9886
+ className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
9887
+ children: "Ch\xE8n"
9888
+ }
9889
+ )
9890
+ ] })
9891
+ ] }),
9892
+ /* @__PURE__ */ jsxRuntime.jsx(ToastHost, {})
9893
+ ]
9894
+ }
9895
+ );
10601
9896
  }
10602
- });
10603
- }
10604
- });
10605
- var ALL_MODULES, POINT_KINDS;
10606
- var init_registry4 = __esm({
10607
- "src/stamps/geometry-2d/dsl/registry.ts"() {
10608
- init_free2();
10609
- init_midpoint2();
10610
- init_onSegment2();
10611
- init_onLine2();
10612
- init_onCircle2();
10613
- init_perpFoot2();
10614
- init_circumcenter2();
10615
- init_incenter2();
10616
- init_centroid2();
10617
- init_orthocenter2();
10618
- init_intersection2();
10619
- init_segment2();
10620
- init_line2();
10621
- init_ray2();
10622
- init_perpendicular();
10623
- init_parallel();
10624
- init_perpBisector();
10625
- init_angleBisector();
10626
- init_lineThrough();
10627
- init_radicalAxis();
10628
- init_tangent();
10629
- init_polygon3();
10630
- init_circleCP();
10631
- init_circle3();
10632
- init_circleDiameter();
10633
- init_secondIntersection2();
10634
- init_circleIntersection2();
10635
- init_circleSecondIntersection2();
10636
- init_tangencyPoint2();
10637
- init_tangentPointExt2();
10638
- init_circleCR();
10639
- init_incircle();
10640
- init_excircle();
10641
- init_arcMidpoint2();
10642
- init_excenter2();
10643
- init_reflectPoint();
10644
- init_reflectLine();
10645
- init_pointAtDistance2();
10646
- ALL_MODULES = [
10647
- freeModule,
10648
- midpointModule,
10649
- onSegmentModule,
10650
- onLineModule,
10651
- onCircleModule,
10652
- perpFootModule,
10653
- circumcenterModule,
10654
- incenterModule,
10655
- centroidModule,
10656
- orthocenterModule,
10657
- intersectionModule,
10658
- // NEW Tier 4+5 points
10659
- secondIntersectionModule,
10660
- circleIntersectionModule,
10661
- circleSecondIntersectionModule,
10662
- tangencyPointModule,
10663
- tangentPointExtModule,
10664
- segmentModule,
10665
- lineModule,
10666
- rayModule,
10667
- perpendicularModule,
10668
- parallelModule,
10669
- perpBisectorModule,
10670
- angleBisectorModule,
10671
- lineThroughModule,
10672
- radicalAxisModule,
10673
- tangentModule,
10674
- polygonModule,
10675
- circleCPModule,
10676
- circle3Module,
10677
- circleDiameterModule,
10678
- // NEW Tier 4+5 circles
10679
- circleCRModule,
10680
- incircleModule,
10681
- excircleModule,
10682
- // Cụm A points
10683
- arcMidpointModule,
10684
- excenterModule,
10685
- reflectPointModule,
10686
- reflectLineModule,
10687
- // Cụm B points
10688
- pointAtDistanceModule
10689
- ];
10690
- new Map(ALL_MODULES.map((m) => [m.kind, m]));
10691
- POINT_KINDS = new Set(
10692
- ALL_MODULES.filter((m) => m.role === "point").map((m) => m.kind)
10693
- );
10694
- new Set(
10695
- ALL_MODULES.filter(
10696
- (m) => m.role === "segment" || m.role === "line" || m.role === "ray" || m.role === "lineConstruction"
10697
- ).map((m) => m.kind)
10698
9897
  );
10699
- new Set(
10700
- ALL_MODULES.filter((m) => m.role === "circle").map((m) => m.kind)
10701
- );
10702
- zod.z.discriminatedUnion(
10703
- "kind",
10704
- ALL_MODULES.map((m) => m.schema)
9898
+ GeometryEditorPanel = React2.forwardRef(
9899
+ function GeometryEditorPanel2(props, ref) {
9900
+ return /* @__PURE__ */ jsxRuntime.jsx(ToastProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(GeometryEditorPanelInner, { ...props, ref }) });
9901
+ }
10705
9902
  );
10706
9903
  }
10707
9904
  });
10708
-
10709
- // src/stamps/geometry-2d/dsl/serialize.ts
10710
- function isValidName(s) {
10711
- return NAME_REGEX.test(s);
10712
- }
10713
- function labelOf2(id, state) {
10714
- const obj = state.objects[id];
10715
- return obj ? obj.label : null;
10716
- }
10717
- function resolveRefs(ids, state) {
10718
- const out = [];
10719
- for (const id of ids) {
10720
- const lab = labelOf2(id, state);
10721
- if (lab == null || !isValidName(lab)) return null;
10722
- out.push(lab);
10723
- }
10724
- return out;
10725
- }
10726
- function fail(reason, detail) {
10727
- return { ok: false, reason, detail };
9905
+ function isFieldFocused() {
9906
+ const ae = typeof document !== "undefined" ? document.activeElement : null;
9907
+ return !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
10728
9908
  }
10729
- function serializePoint(obj, state) {
10730
- const c = obj.attrs?.constraint;
10731
- if (!c) return fail("unsupported-constraint", "missing");
10732
- switch (c.kind) {
10733
- case "free":
10734
- return { ok: true, entity: { name: obj.label, kind: "free", x: c.x, y: c.y } };
10735
- case "midpoint": {
10736
- const refs = resolveRefs([c.p1, c.p2], state);
10737
- if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2}`);
10738
- return { ok: true, entity: { name: obj.label, kind: "midpoint", p1: refs[0], p2: refs[1] } };
10739
- }
10740
- case "onSegment": {
10741
- const refs = resolveRefs([c.segmentId], state);
10742
- if (!refs) return fail("unresolved-ref", c.segmentId);
10743
- return { ok: true, entity: { name: obj.label, kind: "onSegment", segmentId: refs[0], t: c.t } };
10744
- }
9909
+ function useChordShortcut(args) {
9910
+ const { groupOrder, tools, onSelect, enabled } = args;
9911
+ const [chordGroup, setChordGroup] = React2.useState(null);
9912
+ const groupOrderRef = React2.useRef(groupOrder);
9913
+ const toolsRef = React2.useRef(tools);
9914
+ const onSelectRef = React2.useRef(onSelect);
9915
+ const chordGroupRef = React2.useRef(null);
9916
+ groupOrderRef.current = groupOrder;
9917
+ toolsRef.current = tools;
9918
+ onSelectRef.current = onSelect;
9919
+ const cancel = React2.useCallback(() => {
9920
+ chordGroupRef.current = null;
9921
+ setChordGroup(null);
9922
+ }, []);
9923
+ React2.useEffect(() => {
9924
+ if (!enabled) return;
9925
+ const setChord = (next) => {
9926
+ chordGroupRef.current = next;
9927
+ setChordGroup(next);
9928
+ };
9929
+ const onKey = (e) => {
9930
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
9931
+ if (isFieldFocused()) return;
9932
+ const key = e.key;
9933
+ const lower = key.length === 1 ? key.toLowerCase() : key;
9934
+ if (key === "Escape") {
9935
+ if (chordGroupRef.current !== null) {
9936
+ e.preventDefault();
9937
+ e.stopPropagation();
9938
+ setChord(null);
9939
+ }
9940
+ return;
9941
+ }
9942
+ if (lower.length === 1 && lower >= "a" && lower <= "z") {
9943
+ const idx = lower.charCodeAt(0) - A_CODE2;
9944
+ if (idx < groupOrderRef.current.length) {
9945
+ e.preventDefault();
9946
+ e.stopPropagation();
9947
+ setChord(groupOrderRef.current[idx]);
9948
+ }
9949
+ return;
9950
+ }
9951
+ if (key >= "1" && key <= "9") {
9952
+ const active = chordGroupRef.current;
9953
+ if (active === null) return;
9954
+ const n = key.charCodeAt(0) - "1".charCodeAt(0);
9955
+ const toolsInGroup = toolsRef.current.filter(
9956
+ (t) => t.group === active
9957
+ );
9958
+ e.preventDefault();
9959
+ e.stopPropagation();
9960
+ if (n < toolsInGroup.length) {
9961
+ onSelectRef.current(toolsInGroup[n].key);
9962
+ }
9963
+ setChord(null);
9964
+ return;
9965
+ }
9966
+ };
9967
+ window.addEventListener("keydown", onKey, { capture: true });
9968
+ return () => {
9969
+ window.removeEventListener("keydown", onKey, { capture: true });
9970
+ };
9971
+ }, [enabled]);
9972
+ return { chordGroup, cancel };
9973
+ }
9974
+ var A_CODE2;
9975
+ var init_useChordShortcut = __esm({
9976
+ "src/stamps/shared/useChordShortcut.ts"() {
9977
+ A_CODE2 = "a".charCodeAt(0);
9978
+ }
9979
+ });
9980
+
9981
+ // src/stamps/shared/insertImage.ts
9982
+ function buildStampImageElement(api, fileId, width, height, customData, x, y) {
9983
+ const appState = api?.getAppState() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
9984
+ const cx = x ?? appState.scrollX + (appState.width ?? 800) / 2 / (appState.zoom?.value ?? 1) - width / 2;
9985
+ const cy = y ?? appState.scrollY + (appState.height ?? 600) / 2 / (appState.zoom?.value ?? 1) - height / 2;
9986
+ return {
9987
+ type: "image",
9988
+ id: "stamp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8),
9989
+ x: cx,
9990
+ y: cy,
9991
+ width,
9992
+ height,
9993
+ fileId,
9994
+ customData,
9995
+ angle: 0,
9996
+ strokeColor: "transparent",
9997
+ backgroundColor: "transparent",
9998
+ fillStyle: "solid",
9999
+ strokeWidth: 1,
10000
+ strokeStyle: "solid",
10001
+ roughness: 0,
10002
+ opacity: 100,
10003
+ groupIds: [],
10004
+ roundness: null,
10005
+ seed: Math.floor(Math.random() * 1e9),
10006
+ versionNonce: 0,
10007
+ version: 1,
10008
+ isDeleted: false,
10009
+ boundElements: null,
10010
+ updated: Date.now(),
10011
+ link: null,
10012
+ locked: false,
10013
+ status: "saved",
10014
+ scale: [1, 1]
10015
+ };
10016
+ }
10017
+ async function insertStampImage(api, opts) {
10018
+ const { dataURL, fileId, width, height, mimeType } = await createStampFile(opts.svgString);
10019
+ api.addFiles([{ id: fileId, dataURL, mimeType, created: Date.now() }]);
10020
+ const customData = opts.makeCustomData();
10021
+ const elements = api.getSceneElements();
10022
+ const editingId = opts.editingElementId ?? null;
10023
+ if (editingId) {
10024
+ const updated = elements.map(
10025
+ (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
10026
+ );
10027
+ api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
10028
+ return { fileId, width, height, elementId: editingId };
10029
+ }
10030
+ const newElement = buildStampImageElement(
10031
+ api,
10032
+ fileId,
10033
+ width,
10034
+ height,
10035
+ customData,
10036
+ opts.position?.x,
10037
+ opts.position?.y
10038
+ );
10039
+ api.updateScene({
10040
+ elements: [...elements, newElement],
10041
+ appState: clearAppStateAfterInsert()
10042
+ });
10043
+ return { fileId, width, height, elementId: newElement.id };
10044
+ }
10045
+ var clearAppStateAfterInsert;
10046
+ var init_insertImage = __esm({
10047
+ "src/stamps/shared/insertImage.ts"() {
10048
+ init_svgToStampFile();
10049
+ clearAppStateAfterInsert = () => ({
10050
+ selectedElementIds: {},
10051
+ croppingElementId: null
10052
+ });
10053
+ }
10054
+ });
10055
+ function useStampStore(domain, editingElement, parseInitial) {
10056
+ const ref = React2.useRef(null);
10057
+ if (!ref.current) {
10058
+ const initial = editingElement?.customData ? parseInitial(editingElement.customData) ?? createEmptyState(domain) : createEmptyState(domain);
10059
+ ref.current = createStore(initial);
10060
+ }
10061
+ return ref.current;
10062
+ }
10063
+ var init_useStampStore = __esm({
10064
+ "src/stamps/shared/useStampStore.ts"() {
10065
+ init_scene();
10066
+ }
10067
+ });
10068
+
10069
+ // src/stamps/geometry-2d/dsl/serialize.ts
10070
+ function isValidName(s) {
10071
+ return NAME_REGEX.test(s);
10072
+ }
10073
+ function labelOf2(id, state) {
10074
+ const obj = state.objects[id];
10075
+ return obj ? obj.label : null;
10076
+ }
10077
+ function resolveRefs(ids, state) {
10078
+ const out = [];
10079
+ for (const id of ids) {
10080
+ const lab = labelOf2(id, state);
10081
+ if (lab == null || !isValidName(lab)) return null;
10082
+ out.push(lab);
10083
+ }
10084
+ return out;
10085
+ }
10086
+ function fail(reason, detail) {
10087
+ return { ok: false, reason, detail };
10088
+ }
10089
+ function serializePoint(obj, state) {
10090
+ const c = obj.attrs?.constraint;
10091
+ if (!c) return fail("unsupported-constraint", "missing");
10092
+ switch (c.kind) {
10093
+ case "free":
10094
+ return { ok: true, entity: { name: obj.label, kind: "free", x: c.x, y: c.y } };
10095
+ case "midpoint": {
10096
+ const refs = resolveRefs([c.p1, c.p2], state);
10097
+ if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2}`);
10098
+ return { ok: true, entity: { name: obj.label, kind: "midpoint", p1: refs[0], p2: refs[1] } };
10099
+ }
10100
+ case "onSegment": {
10101
+ const refs = resolveRefs([c.segmentId], state);
10102
+ if (!refs) return fail("unresolved-ref", c.segmentId);
10103
+ return { ok: true, entity: { name: obj.label, kind: "onSegment", segmentId: refs[0], t: c.t } };
10104
+ }
10745
10105
  case "onLine": {
10746
10106
  const refs = resolveRefs([c.lineId], state);
10747
10107
  if (!refs) return fail("unresolved-ref", c.lineId);
@@ -10978,2111 +10338,124 @@ function serializePolygon(obj, state) {
10978
10338
  const a = obj.attrs;
10979
10339
  if (a.construction) return fail("unsupported-construction", a.construction.kind);
10980
10340
  if (!Array.isArray(a.vertices) || a.vertices.length < 3) {
10981
- return fail("unsupported-construction", "missing vertices");
10982
- }
10983
- const refs = resolveRefs(a.vertices, state);
10984
- if (!refs) return fail("unresolved-ref", a.vertices.join(","));
10985
- return { ok: true, entity: { name: obj.label, kind: "polygon", vertices: refs } };
10986
- }
10987
- function serializeCircle(obj, state) {
10988
- const a = obj.attrs;
10989
- const c = (() => {
10990
- if (a.construction) return a.construction;
10991
- const raw = a;
10992
- if (raw.kind === "incircle" && raw.vertices) {
10993
- return {
10994
- kind: "incircle",
10995
- p1: raw.vertices[0],
10996
- p2: raw.vertices[1],
10997
- p3: raw.vertices[2]
10998
- };
10999
- }
11000
- if (raw.kind === "excircle" && raw.vertices && raw.opposite) {
11001
- return {
11002
- kind: "excircle",
11003
- p1: raw.vertices[0],
11004
- p2: raw.vertices[1],
11005
- p3: raw.vertices[2],
11006
- opposite: raw.opposite
11007
- };
11008
- }
11009
- if (raw.kind === "circleDiameter" && raw.p1 && raw.p2) {
11010
- return {
11011
- kind: "diameter",
11012
- p1: raw.p1,
11013
- p2: raw.p2
11014
- };
11015
- }
11016
- return void 0;
11017
- })();
11018
- if (!c) {
11019
- if (typeof a.radius === "number") {
11020
- if (!a.center) return fail("unsupported-construction", "missing center");
11021
- const refs2 = resolveRefs([a.center], state);
11022
- if (!refs2) return fail("unresolved-ref", `${a.center}`);
11023
- return {
11024
- ok: true,
11025
- entity: { name: obj.label, kind: "circleCR", center: refs2[0], radius: a.radius }
11026
- };
11027
- }
11028
- if (!a.center || !a.surfacePoint) {
11029
- return fail("unsupported-construction", "missing center/surfacePoint");
11030
- }
11031
- const refs = resolveRefs([a.center, a.surfacePoint], state);
11032
- if (!refs) return fail("unresolved-ref", `${a.center},${a.surfacePoint}`);
11033
- return {
11034
- ok: true,
11035
- entity: { name: obj.label, kind: "circleCP", center: refs[0], surfacePoint: refs[1] }
11036
- };
11037
- }
11038
- if (c.kind === "circumscribed") {
11039
- const refs = resolveRefs([c.p1, c.p2, c.p3], state);
11040
- if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2},${c.p3}`);
11041
- return {
11042
- ok: true,
11043
- entity: { name: obj.label, kind: "circle3", p1: refs[0], p2: refs[1], p3: refs[2] }
11044
- };
11045
- }
11046
- if (c.kind === "incircle") {
11047
- const refs = resolveRefs([c.p1, c.p2, c.p3], state);
11048
- if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2},${c.p3}`);
11049
- return {
11050
- ok: true,
11051
- entity: { name: obj.label, kind: "incircle", vertices: [refs[0], refs[1], refs[2]] }
11052
- };
11053
- }
11054
- if (c.kind === "excircle") {
11055
- const refs = resolveRefs([c.p1, c.p2, c.p3, c.opposite], state);
11056
- if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2},${c.p3},${c.opposite}`);
11057
- return {
11058
- ok: true,
11059
- entity: { name: obj.label, kind: "excircle", vertices: [refs[0], refs[1], refs[2]], opposite: refs[3] }
11060
- };
11061
- }
11062
- if (c.kind === "diameter") {
11063
- const refs = resolveRefs([c.p1, c.p2], state);
11064
- if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2}`);
11065
- return {
11066
- ok: true,
11067
- entity: { name: obj.label, kind: "circleDiameter", p1: refs[0], p2: refs[1] }
11068
- };
11069
- }
11070
- return fail("unsupported-construction");
11071
- }
11072
- function serializeObject(obj, state) {
11073
- if (!isValidName(obj.label)) {
11074
- return fail("invalid-label", obj.label);
11075
- }
11076
- switch (obj.kind) {
11077
- case "point":
11078
- return serializePoint(obj, state);
11079
- case "segment":
11080
- return serializeSegment(obj, state);
11081
- case "ray":
11082
- return serializeRay(obj, state);
11083
- case "line":
11084
- return serializeLine(obj, state);
11085
- case "polygon":
11086
- return serializePolygon(obj, state);
11087
- case "circle":
11088
- return serializeCircle(obj, state);
11089
- case "intersection":
11090
- return serializeIntersection(obj, state);
11091
- default:
11092
- return fail("unsupported-kind", obj.kind);
11093
- }
11094
- }
11095
- function serializeState(state) {
11096
- const points = [];
11097
- const shapes = [];
11098
- const unsupported = [];
11099
- for (const id of state.order) {
11100
- const obj = state.objects[id];
11101
- if (!obj) continue;
11102
- const r = serializeObject(obj, state);
11103
- if (!r.ok) {
11104
- unsupported.push({
11105
- id: obj.id,
11106
- label: obj.label,
11107
- kind: obj.kind,
11108
- reason: r.reason,
11109
- detail: r.detail
11110
- });
11111
- continue;
11112
- }
11113
- if (POINT_KINDS.has(r.entity.kind)) {
11114
- points.push(r.entity);
11115
- } else {
11116
- shapes.push(r.entity);
11117
- }
11118
- }
11119
- return {
11120
- dsl: { version: 1, points, shapes },
11121
- unsupported
11122
- };
11123
- }
11124
- var NAME_REGEX;
11125
- var init_serialize2 = __esm({
11126
- "src/stamps/geometry-2d/dsl/serialize.ts"() {
11127
- init_registry4();
11128
- NAME_REGEX = /^[A-Za-z][A-Za-z0-9_'₀-₉]{0,11}$/;
11129
- }
11130
- });
11131
- function useAiFigure(generator, options = {}) {
11132
- const { currentState } = options;
11133
- const [prompt, setPrompt] = React2.useState("");
11134
- const [isLoading, setIsLoading] = React2.useState(false);
11135
- const [error, setError] = React2.useState(null);
11136
- const [tokens, setTokens] = React2.useState(0);
11137
- const abortRef = React2.useRef(null);
11138
- const requestIdRef = React2.useRef(0);
11139
- const { dsl: currentDsl, unsupported, entityCount, hasContent } = React2.useMemo(() => {
11140
- if (!currentState || currentState.order.length === 0) {
11141
- return {
11142
- dsl: null,
11143
- unsupported: [],
11144
- entityCount: { points: 0, shapes: 0 },
11145
- hasContent: false
11146
- };
11147
- }
11148
- const { dsl, unsupported: unsupported2 } = serializeState(currentState);
11149
- return {
11150
- dsl,
11151
- unsupported: unsupported2,
11152
- entityCount: { points: dsl.points.length, shapes: dsl.shapes.length },
11153
- hasContent: true
11154
- };
11155
- }, [currentState]);
11156
- const hasUnsupported = unsupported.length > 0;
11157
- const initialMode = hasContent && !hasUnsupported ? "refine" : "build";
11158
- const [mode, setModeInternal] = React2.useState(initialMode);
11159
- React2.useEffect(() => {
11160
- if (!hasContent && mode === "refine") setModeInternal("build");
11161
- if (hasUnsupported && mode === "refine") setModeInternal("build");
11162
- }, [hasContent, hasUnsupported, mode]);
11163
- const setMode = React2.useCallback((next) => {
11164
- setModeInternal(next);
11165
- }, []);
11166
- React2.useEffect(() => () => abortRef.current?.abort(), []);
11167
- const submit = React2.useCallback(async () => {
11168
- const problem = prompt.trim();
11169
- if (!problem) {
11170
- setError("Nh\u1EADp \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng h\xECnh.");
11171
- return null;
11172
- }
11173
- if (!generator) {
11174
- setError("T\xEDnh n\u0103ng d\u1EF1ng h\xECnh AI ch\u01B0a \u0111\u01B0\u1EE3c c\u1EA5u h\xECnh.");
11175
- return null;
11176
- }
11177
- abortRef.current?.abort();
11178
- const controller = new AbortController();
11179
- const requestId = ++requestIdRef.current;
11180
- abortRef.current = controller;
11181
- setIsLoading(true);
11182
- setError(null);
11183
- setTokens(0);
11184
- try {
11185
- const generated = await generator(problem, {
11186
- signal: controller.signal,
11187
- onProgress: (info) => {
11188
- if (requestId === requestIdRef.current) setTokens(info.tokens);
11189
- },
11190
- ...mode === "refine" && currentDsl ? { currentDsl } : {}
11191
- });
11192
- if (controller.signal.aborted || requestId !== requestIdRef.current) return null;
11193
- if (!generated.ok) {
11194
- setError(generated.message);
11195
- return null;
11196
- }
11197
- return generated.state;
11198
- } catch (caught) {
11199
- if (controller.signal.aborted || caught instanceof DOMException && caught.name === "AbortError") {
11200
- return null;
11201
- }
11202
- if (requestId === requestIdRef.current) {
11203
- setError(
11204
- caught instanceof Error && caught.message ? caught.message : "Kh\xF4ng th\u1EC3 d\u1EF1ng h\xECnh b\u1EB1ng AI."
11205
- );
11206
- }
11207
- return null;
11208
- } finally {
11209
- if (requestId === requestIdRef.current) {
11210
- abortRef.current = null;
11211
- setIsLoading(false);
11212
- }
11213
- }
11214
- }, [generator, prompt, mode, currentDsl]);
11215
- const cancel = React2.useCallback(() => {
11216
- abortRef.current?.abort();
11217
- }, []);
11218
- return {
11219
- prompt,
11220
- setPrompt,
11221
- isLoading,
11222
- error,
11223
- submit,
11224
- cancel,
11225
- tokens,
11226
- mode,
11227
- setMode,
11228
- entityCount,
11229
- hasUnsupported
11230
- };
11231
- }
11232
- var init_useAiFigure = __esm({
11233
- "src/stamps/geometry-2d/editor/useAiFigure.ts"() {
11234
- "use client";
11235
- init_serialize2();
11236
- }
11237
- });
11238
- function toUsage(u) {
11239
- return {
11240
- inputTokens: u.input_tokens,
11241
- outputTokens: u.output_tokens,
11242
- cacheReadTokens: u.cache_read_input_tokens ?? 0,
11243
- cacheCreationTokens: u.cache_creation_input_tokens ?? 0
11244
- };
11245
- }
11246
- var TOOL_NAME, VISION_TOOL_NAME, AnthropicProvider;
11247
- var init_anthropic = __esm({
11248
- "src/stamps/geometry-2d/ai/providers/anthropic.ts"() {
11249
- TOOL_NAME = "emit_figure_envelope";
11250
- VISION_TOOL_NAME = "extract_problem_envelope";
11251
- AnthropicProvider = class {
11252
- constructor(opts) {
11253
- this.opts = opts;
11254
- this.name = "anthropic";
11255
- this.defaultModel = "claude-opus-4-7";
11256
- if (!opts.apiKey) throw new Error("AnthropicProvider: apiKey b\u1EAFt bu\u1ED9c");
11257
- }
11258
- async call(req) {
11259
- const enableCaching = this.opts.enableCaching !== false;
11260
- const systemBlock = enableCaching ? { type: "text", text: req.systemPrompt, cache_control: { type: "ephemeral" } } : { type: "text", text: req.systemPrompt };
11261
- const tool = {
11262
- name: TOOL_NAME,
11263
- 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.',
11264
- input_schema: req.schema
11265
- };
11266
- const client = new Anthropic__default.default({ apiKey: this.opts.apiKey });
11267
- let resp;
11268
- try {
11269
- resp = await client.messages.create(
11270
- {
11271
- model: req.model,
11272
- max_tokens: req.maxTokens,
11273
- system: [systemBlock],
11274
- tools: [tool],
11275
- tool_choice: { type: "tool", name: TOOL_NAME },
11276
- messages: [{ role: "user", content: req.userPrompt }]
11277
- },
11278
- req.signal ? { signal: req.signal } : void 0
11279
- );
11280
- } catch (e) {
11281
- const err = e;
11282
- return {
11283
- kind: "error",
11284
- message: err.message ?? "L\u1ED7i g\u1ECDi Anthropic API",
11285
- ...err.status !== void 0 ? { status: err.status } : {}
11286
- };
11287
- }
11288
- const usage = toUsage(resp.usage);
11289
- const toolUse = resp.content.find((c) => c.type === "tool_use");
11290
- if (!toolUse || toolUse.type !== "tool_use") {
11291
- return {
11292
- kind: "error",
11293
- message: "Claude kh\xF4ng g\u1ECDi tool. stop_reason=" + resp.stop_reason
11294
- };
11295
- }
11296
- if (toolUse.name !== TOOL_NAME) {
11297
- return {
11298
- kind: "error",
11299
- message: `Tool kh\xF4ng x\xE1c \u0111\u1ECBnh: "${toolUse.name}"`
11300
- };
11301
- }
11302
- return { kind: "json", data: toolUse.input, usage };
11303
- }
11304
- async extractText(req) {
11305
- const model = req.model ?? this.defaultModel;
11306
- const enableCaching = this.opts.enableCaching !== false;
11307
- const systemBlock = enableCaching ? { type: "text", text: req.systemPrompt, cache_control: { type: "ephemeral" } } : { type: "text", text: req.systemPrompt };
11308
- const tool = {
11309
- name: VISION_TOOL_NAME,
11310
- description: "Tr\xEDch \u0111\u1EC1 b\xE0i h\xECnh h\u1ECDc t\u1EEB \u1EA3nh, ho\u1EB7c t\u1EEB ch\u1ED1i n\u1EBFu kh\xF4ng ph\u1EA3i \u0111\u1EC1 to\xE1n.",
11311
- input_schema: req.schema
11312
- };
11313
- const imageBlocks = req.images.map((img) => ({
11314
- type: "image",
11315
- source: {
11316
- type: "base64",
11317
- media_type: img.mediaType,
11318
- data: img.base64
11319
- }
11320
- }));
11321
- const client = new Anthropic__default.default({ apiKey: this.opts.apiKey });
11322
- let resp;
11323
- try {
11324
- resp = await client.messages.create(
11325
- {
11326
- model,
11327
- max_tokens: req.maxTokens,
11328
- system: [systemBlock],
11329
- tools: [tool],
11330
- tool_choice: { type: "tool", name: VISION_TOOL_NAME },
11331
- messages: [
11332
- {
11333
- role: "user",
11334
- content: [...imageBlocks, { type: "text", text: req.userPrompt }]
11335
- }
11336
- ]
11337
- },
11338
- req.signal ? { signal: req.signal } : void 0
11339
- );
11340
- } catch (e) {
11341
- const err = e;
11342
- return {
11343
- kind: "error",
11344
- message: err.message ?? "L\u1ED7i g\u1ECDi Anthropic Vision API",
11345
- ...err.status !== void 0 ? { status: err.status } : {}
11346
- };
11347
- }
11348
- const usage = toUsage(resp.usage);
11349
- const toolUse = resp.content.find((c) => c.type === "tool_use");
11350
- if (!toolUse || toolUse.type !== "tool_use") {
11351
- return { kind: "error", message: "Claude kh\xF4ng g\u1ECDi vision tool. stop_reason=" + resp.stop_reason };
11352
- }
11353
- if (toolUse.name !== VISION_TOOL_NAME) {
11354
- return { kind: "error", message: `Claude g\u1ECDi sai tool: ${toolUse.name}` };
11355
- }
11356
- return { kind: "json", data: toolUse.input, usage };
11357
- }
11358
- };
11359
- }
11360
- });
11361
-
11362
- // src/stamps/geometry-2d/ai/providers/claude-agent-sdk.ts
11363
- function memoSchemaJson(schema) {
11364
- let s = SCHEMA_CACHE.get(schema);
11365
- if (s) return s;
11366
- s = JSON.stringify(schema, null, 2);
11367
- SCHEMA_CACHE.set(schema, s);
11368
- return s;
11369
- }
11370
- var DEFAULT_MODEL, SCHEMA_CACHE, ClaudeAgentSdkProvider;
11371
- var init_claude_agent_sdk = __esm({
11372
- "src/stamps/geometry-2d/ai/providers/claude-agent-sdk.ts"() {
11373
- DEFAULT_MODEL = "claude-sonnet-4-6";
11374
- SCHEMA_CACHE = /* @__PURE__ */ new WeakMap();
11375
- ClaudeAgentSdkProvider = class {
11376
- constructor(opts = {}) {
11377
- this.name = "claude-agent-sdk";
11378
- this.defaultModel = opts.defaultModel ?? DEFAULT_MODEL;
11379
- this.oauthToken = opts.oauthToken;
11380
- this.queryImpl = opts.queryImpl ?? null;
11381
- }
11382
- async resolveQuery() {
11383
- if (this.queryImpl) return this.queryImpl;
11384
- const mod = await import('@anthropic-ai/claude-agent-sdk');
11385
- return mod.query;
11386
- }
11387
- async call(req) {
11388
- if (this.oauthToken) {
11389
- process.env.CLAUDE_CODE_OAUTH_TOKEN = this.oauthToken;
11390
- }
11391
- let query;
11392
- try {
11393
- query = await this.resolveQuery();
11394
- } catch (e) {
11395
- return {
11396
- kind: "error",
11397
- message: "ClaudeAgentSdkProvider: SDK kh\xF4ng kh\u1EA3 d\u1EE5ng. " + (e.message ?? "")
11398
- };
11399
- }
11400
- const schemaText = memoSchemaJson(req.schema);
11401
- const constrainedSystem = `${req.systemPrompt}
11402
-
11403
- QUAN TR\u1ECCNG: Output PH\u1EA2I l\xE0 valid JSON \u0111\xFAng schema sau. KH\xD4NG markdown wrapper, KH\xD4NG prose gi\u1EA3i th\xEDch, CH\u1EC8 raw JSON:
11404
-
11405
- ${schemaText}`;
11406
- let assistantText = "";
11407
- let usageInput = 0;
11408
- let usageOutput = 0;
11409
- let cacheRead = 0;
11410
- let cacheCreation = 0;
11411
- try {
11412
- for await (const msg of query({
11413
- prompt: req.userPrompt,
11414
- options: {
11415
- systemPrompt: constrainedSystem,
11416
- allowedTools: [],
11417
- model: req.model ?? this.defaultModel,
11418
- ...req.signal ? { abortSignal: req.signal } : {}
11419
- }
11420
- })) {
11421
- if (msg.type === "assistant") {
11422
- const message = msg.message;
11423
- for (const block of message.content) {
11424
- const b = block;
11425
- if (b.type === "text" && typeof b.text === "string") {
11426
- assistantText += b.text;
11427
- if (req.onToken) {
11428
- try {
11429
- req.onToken(b.text);
11430
- } catch {
11431
- }
11432
- }
11433
- }
11434
- }
11435
- } else if (msg.type === "result") {
11436
- const r = msg;
11437
- if (r.subtype && r.subtype !== "success") {
11438
- return {
11439
- kind: "error",
11440
- message: `ClaudeAgentSdkProvider: result subtype=${r.subtype}`
11441
- };
11442
- }
11443
- if (r.usage) {
11444
- usageInput = r.usage.input_tokens ?? 0;
11445
- usageOutput = r.usage.output_tokens ?? 0;
11446
- cacheRead = r.usage.cache_read_input_tokens ?? 0;
11447
- cacheCreation = r.usage.cache_creation_input_tokens ?? 0;
11448
- }
11449
- }
11450
- }
11451
- } catch (e) {
11452
- return {
11453
- kind: "error",
11454
- message: "ClaudeAgentSdkProvider: query() throw: " + (e.message ?? "?")
11455
- };
11456
- }
11457
- if (!assistantText.trim()) {
11458
- return {
11459
- kind: "error",
11460
- message: "ClaudeAgentSdkProvider: assistant tr\u1EA3 response r\u1ED7ng."
11461
- };
11462
- }
11463
- let cleaned = assistantText.trim();
11464
- cleaned = cleaned.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?\s*```\s*$/, "");
11465
- let data;
11466
- try {
11467
- data = JSON.parse(cleaned);
11468
- } catch (e) {
11469
- return {
11470
- kind: "error",
11471
- message: "ClaudeAgentSdkProvider: output kh\xF4ng parse JSON: " + (e.message ?? "?") + ". Output preview: " + cleaned.slice(0, 200)
11472
- };
11473
- }
11474
- return {
11475
- kind: "json",
11476
- data,
11477
- usage: {
11478
- inputTokens: usageInput,
11479
- outputTokens: usageOutput,
11480
- cacheReadTokens: cacheRead,
11481
- cacheCreationTokens: cacheCreation
11482
- }
11483
- };
11484
- }
11485
- };
11486
- }
11487
- });
11488
-
11489
- // src/stamps/geometry-2d/ai/providers/claude-cli.ts
11490
- function toUsage2(u) {
11491
- return {
11492
- inputTokens: u?.input_tokens ?? 0,
11493
- outputTokens: u?.output_tokens ?? 0,
11494
- cacheReadTokens: u?.cache_read_input_tokens ?? 0,
11495
- cacheCreationTokens: u?.cache_creation_input_tokens ?? 0
11496
- };
11497
- }
11498
- var DEFAULT_BIN, DEFAULT_MODEL2, DEFAULT_MAX_BUDGET_USD, ClaudeCliProvider;
11499
- var init_claude_cli = __esm({
11500
- "src/stamps/geometry-2d/ai/providers/claude-cli.ts"() {
11501
- DEFAULT_BIN = "claude";
11502
- DEFAULT_MODEL2 = "claude-sonnet-4-6";
11503
- DEFAULT_MAX_BUDGET_USD = 0.5;
11504
- ClaudeCliProvider = class {
11505
- constructor(opts = {}) {
11506
- this.name = "claude-cli";
11507
- this.bin = opts.bin ?? DEFAULT_BIN;
11508
- this.defaultModel = opts.defaultModel ?? DEFAULT_MODEL2;
11509
- this.maxBudgetUsd = opts.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD;
11510
- this.spawnImpl = opts.spawnImpl ?? null;
11511
- }
11512
- async resolveSpawn() {
11513
- if (this.spawnImpl) return this.spawnImpl;
11514
- const mod = await import('child_process');
11515
- return mod.spawn;
11516
- }
11517
- async call(req) {
11518
- let spawn;
11519
- try {
11520
- spawn = await this.resolveSpawn();
11521
- } catch (e) {
11522
- return {
11523
- kind: "error",
11524
- message: "ClaudeCliProvider: node:child_process kh\xF4ng kh\u1EA3 d\u1EE5ng (ch\u1EC9 ch\u1EA1y \u0111\u01B0\u1EE3c \u1EDF Node env). " + (e.message ?? "")
11525
- };
11526
- }
11527
- const args = [
11528
- "--print",
11529
- "--output-format",
11530
- "json",
11531
- "--json-schema",
11532
- JSON.stringify(req.schema),
11533
- "--append-system-prompt",
11534
- req.systemPrompt,
11535
- "--tools",
11536
- "",
11537
- "--model",
11538
- req.model,
11539
- "--max-budget-usd",
11540
- String(this.maxBudgetUsd)
11541
- ];
11542
- return new Promise((resolve) => {
11543
- let child;
11544
- try {
11545
- child = spawn(this.bin, args, { stdio: ["pipe", "pipe", "pipe"] });
11546
- } catch (e) {
11547
- resolve({
11548
- kind: "error",
11549
- message: `ClaudeCliProvider: spawn '${this.bin}' th\u1EA5t b\u1EA1i. ` + (e.message ?? "Ki\u1EC3m tra claude CLI \u0111\xE3 c\xE0i ch\u01B0a.")
11550
- });
11551
- return;
11552
- }
11553
- let stdout = "";
11554
- let stderr = "";
11555
- let settled = false;
11556
- const settle = (out) => {
11557
- if (settled) return;
11558
- settled = true;
11559
- resolve(out);
11560
- };
11561
- const onAbort = () => {
11562
- child.kill("SIGTERM");
11563
- settle({ kind: "error", message: "ClaudeCliProvider: aborted" });
11564
- };
11565
- if (req.signal) {
11566
- if (req.signal.aborted) {
11567
- onAbort();
11568
- return;
11569
- }
11570
- req.signal.addEventListener("abort", onAbort, { once: true });
11571
- }
11572
- child.stdout?.on("data", (chunk) => {
11573
- stdout += chunk.toString("utf8");
11574
- });
11575
- child.stderr?.on("data", (chunk) => {
11576
- stderr += chunk.toString("utf8");
11577
- });
11578
- child.on("error", (e) => {
11579
- settle({
11580
- kind: "error",
11581
- message: `ClaudeCliProvider: subprocess error: ${e.message}`
11582
- });
11583
- });
11584
- child.on("close", (code) => {
11585
- if (settled) return;
11586
- if (code !== 0) {
11587
- settle({
11588
- kind: "error",
11589
- message: `ClaudeCliProvider: exit code ${code}. stderr: ${stderr.trim() || "(empty)"}`,
11590
- ...typeof code === "number" ? { status: code } : {}
11591
- });
11592
- return;
11593
- }
11594
- let env;
11595
- try {
11596
- env = JSON.parse(stdout.trim());
11597
- } catch (e) {
11598
- settle({
11599
- kind: "error",
11600
- message: "ClaudeCliProvider: stdout kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: " + (e.message ?? "?")
11601
- });
11602
- return;
11603
- }
11604
- if (env.is_error) {
11605
- settle({
11606
- kind: "error",
11607
- message: `ClaudeCliProvider: CLI b\xE1o l\u1ED7i (subtype=${env.subtype}, api_status=${env.api_error_status ?? "n/a"})`
11608
- });
11609
- return;
11610
- }
11611
- if (env.structured_output === void 0 || env.structured_output === null) {
11612
- settle({
11613
- kind: "error",
11614
- message: `ClaudeCliProvider: thi\u1EBFu structured_output trong response. result="${(env.result ?? "").slice(0, 200)}"`
11615
- });
11616
- return;
11617
- }
11618
- const usage = toUsage2(env.usage);
11619
- settle({ kind: "json", data: env.structured_output, usage });
11620
- });
11621
- try {
11622
- child.stdin?.write(req.userPrompt);
11623
- child.stdin?.end();
11624
- } catch (e) {
11625
- settle({
11626
- kind: "error",
11627
- message: "ClaudeCliProvider: ghi stdin th\u1EA5t b\u1EA1i: " + (e.message ?? "?")
11628
- });
11629
- }
11630
- });
11631
- }
11632
- };
11633
- }
11634
- });
11635
-
11636
- // src/stamps/geometry-2d/ai/providers/ollama.ts
11637
- var DEFAULT_BASE_URL, DEFAULT_MODEL3, OllamaProvider;
11638
- var init_ollama = __esm({
11639
- "src/stamps/geometry-2d/ai/providers/ollama.ts"() {
11640
- DEFAULT_BASE_URL = "http://localhost:11434";
11641
- DEFAULT_MODEL3 = "gemma3:4b";
11642
- OllamaProvider = class {
11643
- constructor(opts = {}) {
11644
- this.name = "ollama";
11645
- this.baseUrl = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
11646
- this.defaultModel = opts.defaultModel ?? DEFAULT_MODEL3;
11647
- this.fetchImpl = opts.fetchImpl ?? null;
11648
- }
11649
- resolveFetch() {
11650
- if (this.fetchImpl) return this.fetchImpl;
11651
- if (typeof fetch === "undefined") {
11652
- throw new Error(
11653
- "OllamaProvider: global `fetch` kh\xF4ng kh\u1EA3 d\u1EE5ng. Truy\u1EC1n `fetchImpl` qua constructor ho\u1EB7c ch\u1EA1y \u1EDF Node 18+ / browser."
11654
- );
11655
- }
11656
- return fetch;
11657
- }
11658
- async call(req) {
11659
- const body = {
11660
- model: req.model,
11661
- messages: [
11662
- { role: "system", content: req.systemPrompt },
11663
- { role: "user", content: req.userPrompt }
11664
- ],
11665
- format: req.schema,
11666
- stream: true,
11667
- options: { num_predict: req.maxTokens, temperature: 0.2 }
11668
- };
11669
- let doFetch;
11670
- try {
11671
- doFetch = this.resolveFetch();
11672
- } catch (e) {
11673
- const err = e;
11674
- return { kind: "error", message: err.message ?? "fetch kh\xF4ng kh\u1EA3 d\u1EE5ng" };
11675
- }
11676
- let resp;
11677
- try {
11678
- resp = await doFetch(`${this.baseUrl}/api/chat`, {
11679
- method: "POST",
11680
- headers: { "content-type": "application/json" },
11681
- body: JSON.stringify(body),
11682
- signal: req.signal
11683
- });
11684
- } catch (e) {
11685
- const err = e;
11686
- return {
11687
- kind: "error",
11688
- message: err.message ?? `Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Ollama \u1EDF ${this.baseUrl}`
11689
- };
11690
- }
11691
- if (!resp.ok || !resp.body) {
11692
- let detail = "";
11693
- try {
11694
- detail = await resp.text();
11695
- } catch {
11696
- }
11697
- return {
11698
- kind: "error",
11699
- message: `Ollama HTTP ${resp.status}: ${detail || resp.statusText}`,
11700
- status: resp.status
11701
- };
11702
- }
11703
- const reader = resp.body.getReader();
11704
- const decoder = new TextDecoder();
11705
- let buffer = "";
11706
- let content = "";
11707
- let promptEvalCount = 0;
11708
- let evalCount = 0;
11709
- while (true) {
11710
- const { value, done } = await reader.read();
11711
- if (done) break;
11712
- buffer += decoder.decode(value, { stream: true });
11713
- let nl;
11714
- while ((nl = buffer.indexOf("\n")) !== -1) {
11715
- const line = buffer.slice(0, nl).trim();
11716
- buffer = buffer.slice(nl + 1);
11717
- if (!line) continue;
11718
- try {
11719
- const chunk = JSON.parse(line);
11720
- if (chunk.message?.content) {
11721
- content += chunk.message.content;
11722
- if (req.onToken) {
11723
- try {
11724
- req.onToken(chunk.message.content);
11725
- } catch {
11726
- }
11727
- }
11728
- }
11729
- if (chunk.done) {
11730
- promptEvalCount = chunk.prompt_eval_count ?? promptEvalCount;
11731
- evalCount = chunk.eval_count ?? evalCount;
11732
- }
11733
- } catch {
11734
- }
11735
- }
11736
- }
11737
- const trimmed = content.trim();
11738
- if (!trimmed) return { kind: "error", message: "Ollama tr\u1EA3 message.content r\u1ED7ng" };
11739
- let data;
11740
- try {
11741
- data = JSON.parse(trimmed);
11742
- } catch (e) {
11743
- const err = e;
11744
- return {
11745
- kind: "error",
11746
- message: "Ollama content kh\xF4ng parse \u0111\u01B0\u1EE3c JSON: " + (err.message ?? "?")
11747
- };
11748
- }
11749
- return {
11750
- kind: "json",
11751
- data,
11752
- usage: {
11753
- inputTokens: promptEvalCount,
11754
- outputTokens: evalCount,
11755
- cacheReadTokens: 0,
11756
- cacheCreationTokens: 0
11757
- }
11758
- };
11759
- }
11760
- // Vision: gửi ảnh qua images[] field trong message (Ollama multimodal API).
11761
- // Model cần hỗ trợ vision (gemma3, llava, ...). Output vẫn là JSON envelope.
11762
- async extractText(req) {
11763
- const model = req.model ?? this.defaultModel;
11764
- const body = {
11765
- model,
11766
- messages: [
11767
- { role: "system", content: req.systemPrompt },
11768
- {
11769
- role: "user",
11770
- content: req.userPrompt,
11771
- images: req.images.map((i) => i.base64)
11772
- }
11773
- ],
11774
- format: req.schema,
11775
- stream: false,
11776
- options: { num_predict: req.maxTokens, temperature: 0.2 }
11777
- };
11778
- let doFetch;
11779
- try {
11780
- doFetch = this.resolveFetch();
11781
- } catch (e) {
11782
- return { kind: "error", message: e.message ?? "fetch kh\xF4ng kh\u1EA3 d\u1EE5ng" };
11783
- }
11784
- let resp;
11785
- try {
11786
- resp = await doFetch(`${this.baseUrl}/api/chat`, {
11787
- method: "POST",
11788
- headers: { "content-type": "application/json" },
11789
- body: JSON.stringify(body),
11790
- signal: req.signal
11791
- });
11792
- } catch (e) {
11793
- return {
11794
- kind: "error",
11795
- message: e.message ?? `Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c Ollama \u1EDF ${this.baseUrl}`
11796
- };
11797
- }
11798
- if (!resp.ok) {
11799
- let detail = "";
11800
- try {
11801
- detail = await resp.text();
11802
- } catch {
11803
- }
11804
- return {
11805
- kind: "error",
11806
- message: `Ollama Vision HTTP ${resp.status}: ${detail || resp.statusText}`,
11807
- status: resp.status
11808
- };
11809
- }
11810
- let json;
11811
- try {
11812
- json = await resp.json();
11813
- } catch (e) {
11814
- return { kind: "error", message: "Ollama vision response kh\xF4ng ph\u1EA3i JSON: " + (e.message ?? "?") };
11815
- }
11816
- const content = json.message?.content?.trim();
11817
- if (!content) return { kind: "error", message: "Ollama vision tr\u1EA3 content r\u1ED7ng" };
11818
- let data;
11819
- try {
11820
- data = JSON.parse(content);
11821
- } catch (e) {
11822
- return { kind: "error", message: "Ollama vision content kh\xF4ng parse JSON: " + (e.message ?? "?") };
11823
- }
11824
- const usage = {
11825
- inputTokens: json.prompt_eval_count ?? 0,
11826
- outputTokens: json.eval_count ?? 0,
11827
- cacheReadTokens: 0,
11828
- cacheCreationTokens: 0
11829
- };
11830
- return { kind: "json", data, usage };
11831
- }
11832
- };
11833
- }
11834
- });
11835
-
11836
- // src/stamps/geometry-2d/ai/providers/index.ts
11837
- function selectProvider(opts = {}) {
11838
- if (opts.provider) return opts.provider;
11839
- if (opts.apiKey) {
11840
- return new AnthropicProvider({
11841
- apiKey: opts.apiKey,
11842
- enableCaching: opts.enableCaching
11843
- });
11844
- }
11845
- const env = opts.env ?? readEnv();
11846
- const wanted = (env.WHITEBOARD_AI_PROVIDER ?? "claude-agent-sdk").toLowerCase();
11847
- if (wanted === "anthropic") {
11848
- const key = env.ANTHROPIC_API_KEY;
11849
- if (!key) {
11850
- throw new Error(
11851
- "selectProvider: WHITEBOARD_AI_PROVIDER=anthropic nh\u01B0ng thi\u1EBFu env ANTHROPIC_API_KEY"
11852
- );
11853
- }
11854
- return new AnthropicProvider({ apiKey: key, enableCaching: opts.enableCaching });
11855
- }
11856
- if (wanted === "claude-cli") {
11857
- const budgetEnv = env.CLAUDE_CLI_MAX_BUDGET_USD;
11858
- const budgetNum = budgetEnv !== void 0 ? Number(budgetEnv) : void 0;
11859
- return new ClaudeCliProvider({
11860
- bin: opts.claudeCliBin ?? env.CLAUDE_CLI_BIN,
11861
- defaultModel: opts.claudeCliDefaultModel ?? env.CLAUDE_CLI_MODEL,
11862
- maxBudgetUsd: opts.claudeCliMaxBudgetUsd ?? (budgetNum !== void 0 && Number.isFinite(budgetNum) ? budgetNum : void 0)
11863
- });
11864
- }
11865
- if (wanted === "claude-agent-sdk") {
11866
- return new ClaudeAgentSdkProvider({
11867
- oauthToken: opts.claudeAgentSdkOauthToken ?? env.CLAUDE_CODE_OAUTH_TOKEN,
11868
- defaultModel: opts.claudeAgentSdkDefaultModel ?? env.CLAUDE_AGENT_SDK_MODEL
11869
- });
11870
- }
11871
- if (wanted === "ollama") {
11872
- return new OllamaProvider({
11873
- baseUrl: opts.ollamaBaseUrl ?? env.OLLAMA_BASE_URL,
11874
- defaultModel: opts.ollamaDefaultModel ?? env.OLLAMA_DEFAULT_MODEL
11875
- });
11876
- }
11877
- throw new Error(
11878
- `selectProvider: WHITEBOARD_AI_PROVIDER="${wanted}" kh\xF4ng h\u1EE3p l\u1EC7 (anthropic|claude-cli|claude-agent-sdk|ollama)`
11879
- );
11880
- }
11881
- function readEnv() {
11882
- if (typeof process !== "undefined" && process.env) {
11883
- return process.env;
11884
- }
11885
- return {};
11886
- }
11887
- var init_providers = __esm({
11888
- "src/stamps/geometry-2d/ai/providers/index.ts"() {
11889
- init_anthropic();
11890
- init_claude_agent_sdk();
11891
- init_claude_cli();
11892
- init_ollama();
11893
- }
11894
- });
11895
- function visionEnvelopeJsonSchema() {
11896
- return zodToJsonSchema.zodToJsonSchema(VisionEnvelopeZ, {
11897
- $refStrategy: "none",
11898
- target: "jsonSchema7"
11899
- });
11900
- }
11901
- var VisionEnvelopeZ;
11902
- var init_envelope = __esm({
11903
- "src/stamps/geometry-2d/ai/vision/envelope.ts"() {
11904
- VisionEnvelopeZ = zod.z.object({
11905
- decision: zod.z.enum(["extract", "refuse"]),
11906
- text: zod.z.string().optional(),
11907
- confidence: zod.z.enum(["high", "low"]).optional(),
11908
- reason: zod.z.string().optional()
11909
- }).refine(
11910
- (e) => e.decision === "extract" ? e.text != null && e.text.length > 0 : e.reason != null && e.reason.length > 0,
11911
- { message: "extract c\u1EA7n text kh\xF4ng r\u1ED7ng; refuse c\u1EA7n reason kh\xF4ng r\u1ED7ng" }
11912
- );
11913
- }
11914
- });
11915
-
11916
- // src/stamps/geometry-2d/ai/vision/prompt.ts
11917
- function buildVisionSystemPrompt() {
11918
- return [
11919
- "B\u1EA1n l\xE0 OCR chuy\xEAn \u0111\u1ECDc \u0111\u1EC1 to\xE1n h\xECnh h\u1ECDc ti\u1EBFng Vi\u1EC7t t\u1EEB \u1EA3nh.",
11920
- "",
11921
- "NHI\u1EC6M V\u1EE4:",
11922
- "1. \u0110\u1ECDc text trong \u1EA3nh, tr\u1EA3 v\u1EC1 ph\u1EA7n \u0110\u1EC0 B\xC0I (l\u1EDDi v\u0103n + c\xF4ng th\u1EE9c inline).",
11923
- "2. GI\u1EEE NGUY\xCAN c\xE1c k\xFD hi\u1EC7u to\xE1n Unicode: \u0394 \u22A5 \u2225 \xB0 \u2299 \u03C0 \u2192 \u2264 \u2265 \u2208 \u2209 \u2229 \u222A.",
11924
- "3. B\u1ECE QUA h\xECnh v\u1EBD minh ho\u1EA1 \u2014 ch\u1EC9 tr\u1EA3 ph\u1EA7n text.",
11925
- '4. N\u1EBFu \u1EA3nh KH\xD4NG ph\u1EA3i \u0111\u1EC1 to\xE1n h\xECnh h\u1ECDc (vd: v\u0103n h\u1ECDc, \u1EA3nh \u0111\u1EDDi th\u01B0\u1EDDng, code, c\xF4ng th\u1EE9c kh\xF4ng li\xEAn quan): decision="refuse" v\u1EDBi reason c\u1EE5 th\u1EC3 b\u1EB1ng ti\u1EBFng Vi\u1EC7t.',
11926
- "5. \u0110\xE1nh gi\xE1 confidence:",
11927
- ' - "high": \u2265 80% k\xFD t\u1EF1 \u0111\u1ECDc r\xF5 r\xE0ng, kh\xF4ng nghi ng\u1EDD.',
11928
- ' - "low": \u1EA3nh m\u1EDD, c\xF3 ch\u1EEF kh\xF4ng ch\u1EAFc ch\u1EAFn, ho\u1EB7c < 80% k\xFD t\u1EF1 confident.',
11929
- "",
11930
- "OUTPUT: JSON theo schema sau, kh\xF4ng markdown, kh\xF4ng gi\u1EA3i th\xEDch th\xEAm.",
11931
- ' { "decision": "extract", "text": "...", "confidence": "high"|"low" }',
11932
- ' { "decision": "refuse", "reason": "..." }',
11933
- "",
11934
- "V\xCD D\u1EE4 extract success:",
11935
- ' { "decision": "extract", "text": "Cho tam gi\xE1c ABC vu\xF4ng t\u1EA1i A. K\u1EBB \u0111\u01B0\u1EDDng cao AH \u22A5 BC. Ch\u1EE9ng minh AH\xB2 = BH \xB7 CH.", "confidence": "high" }',
11936
- "",
11937
- "V\xCD D\u1EE4 refuse:",
11938
- ' { "decision": "refuse", "reason": "\u1EA2nh kh\xF4ng ph\u1EA3i \u0111\u1EC1 to\xE1n \u2014 \u0111\xE2y l\xE0 m\u1ED9t \u0111o\u1EA1n v\u0103n v\u1EC1 Truy\u1EC7n Ki\u1EC1u." }'
11939
- ].join("\n");
11940
- }
11941
- var VISION_USER_PROMPT;
11942
- var init_prompt = __esm({
11943
- "src/stamps/geometry-2d/ai/vision/prompt.ts"() {
11944
- VISION_USER_PROMPT = "\u0110\u1ECDc \u0111\u1EC1 b\xE0i h\xECnh h\u1ECDc trong \u1EA3nh sau.";
11945
- }
11946
- });
11947
-
11948
- // src/stamps/geometry-2d/ai/vision/tesseract.ts
11949
- async function runTesseractOcr(image, opts = {}) {
11950
- if (opts.signal?.aborted) {
11951
- const err = new Error("Tesseract OCR aborted");
11952
- err.name = "AbortError";
11953
- throw err;
11954
- }
11955
- const { createWorker } = await import('tesseract.js');
11956
- const lang = opts.lang ?? DEFAULT_LANG;
11957
- const worker = await createWorker(lang);
11958
- try {
11959
- const dataUrl = `data:${image.mediaType};base64,${image.base64}`;
11960
- const { data } = await worker.recognize(dataUrl);
11961
- return { text: data.text, confidence: data.confidence };
11962
- } finally {
11963
- await worker.terminate();
11964
- }
11965
- }
11966
- var DEFAULT_LANG;
11967
- var init_tesseract = __esm({
11968
- "src/stamps/geometry-2d/ai/vision/tesseract.ts"() {
11969
- DEFAULT_LANG = "vie+eng";
11970
- }
11971
- });
11972
-
11973
- // src/stamps/geometry-2d/ai/vision/extractProblem.ts
11974
- function pickVisionModel(providerDefault, opts, env) {
11975
- return opts.visionModel ?? env.WHITEBOARD_AI_VISION_MODEL ?? providerDefault;
11976
- }
11977
- async function extractProblemFromImage(image, opts = {}) {
11978
- const engine = opts.engine ?? "tesseract";
11979
- if (engine === "tesseract") {
11980
- return extractViaTesseract(image, opts);
11981
- }
11982
- return extractViaLlm(image, opts);
11983
- }
11984
- async function extractViaTesseract(image, opts) {
11985
- let raw;
11986
- try {
11987
- raw = await runTesseractOcr(image, {
11988
- ...opts.tesseractLang ? { lang: opts.tesseractLang } : {},
11989
- ...opts.signal ? { signal: opts.signal } : {}
11990
- });
11991
- } catch (e) {
11992
- const err = e;
11993
- return {
11994
- ok: false,
11995
- reason: "unreadable",
11996
- message: "Tesseract OCR fail: " + (err.message ?? "?")
11997
- };
11998
- }
11999
- const text = postProcess(raw.text);
12000
- if (text.length === 0) {
12001
- return { ok: false, reason: "empty", message: "Tesseract kh\xF4ng tr\xEDch \u0111\u01B0\u1EE3c text." };
12002
- }
12003
- const tooShort = text.length < MIN_HIGH_CONFIDENCE_CHARS;
12004
- const lowConf = raw.confidence < TESSERACT_HIGH_CONFIDENCE_THRESHOLD;
12005
- const confidence = tooShort || lowConf ? "low" : "high";
12006
- return {
12007
- ok: true,
12008
- text,
12009
- confidence,
12010
- usage: { inputTokens: 0, outputTokens: 0 }
12011
- };
12012
- }
12013
- async function extractViaLlm(image, opts) {
12014
- const provider = opts.provider ?? selectProvider(opts);
12015
- if (!provider.extractText) {
12016
- return {
12017
- ok: false,
12018
- reason: "unsupported",
12019
- message: `Provider "${provider.name}" kh\xF4ng h\u1ED7 tr\u1EE3 \u0111\u1ECDc \u1EA3nh.`
12020
- };
12021
- }
12022
- const env = opts.env ?? readEnv2();
12023
- const model = pickVisionModel(provider.defaultModel, opts, env);
12024
- const req = {
12025
- systemPrompt: buildVisionSystemPrompt(),
12026
- userPrompt: VISION_USER_PROMPT,
12027
- schema: visionEnvelopeJsonSchema(),
12028
- images: [image],
12029
- model,
12030
- maxTokens: opts.maxTokens ?? 1024,
12031
- ...opts.signal ? { signal: opts.signal } : {}
12032
- };
12033
- const out = await provider.extractText(req);
12034
- if (out.kind === "error") {
12035
- return { ok: false, reason: "unreadable", message: out.message };
12036
- }
12037
- const parsed = VisionEnvelopeZ.safeParse(out.data);
12038
- if (!parsed.success) {
12039
- return {
12040
- ok: false,
12041
- reason: "empty",
12042
- message: "Kh\xF4ng parse \u0111\u01B0\u1EE3c output OCR: " + parsed.error.message
12043
- };
12044
- }
12045
- const env_ = parsed.data;
12046
- if (env_.decision === "refuse") {
12047
- return {
12048
- ok: false,
12049
- reason: "not-math",
12050
- message: env_.reason ?? "\u1EA2nh kh\xF4ng ph\u1EA3i \u0111\u1EC1 to\xE1n."
12051
- };
12052
- }
12053
- const rawText = env_.text ?? "";
12054
- const text = postProcess(rawText);
12055
- if (text.length === 0) {
12056
- return { ok: false, reason: "empty", message: "OCR kh\xF4ng tr\xEDch \u0111\u01B0\u1EE3c text." };
12057
- }
12058
- const tooShort = text.length < MIN_HIGH_CONFIDENCE_CHARS;
12059
- const confidence = env_.confidence === "low" || tooShort ? "low" : "high";
12060
- const usage = out.usage ?? { inputTokens: 0, outputTokens: 0 };
12061
- return {
12062
- ok: true,
12063
- text,
12064
- confidence,
12065
- usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens }
12066
- };
12067
- }
12068
- function postProcess(raw) {
12069
- let t = raw.trim();
12070
- t = t.replace(/\*\*(.+?)\*\*/g, "$1");
12071
- t = t.replace(/\*(.+?)\*/g, "$1");
12072
- t = t.replace(/_(.+?)_/g, "$1");
12073
- t = t.replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1");
12074
- t = t.replace(/\s+/g, " ").trim();
12075
- t = t.normalize("NFC");
12076
- if (t.length > MAX_TEXT_CHARS) t = t.slice(0, MAX_TEXT_CHARS);
12077
- return t;
12078
- }
12079
- function readEnv2() {
12080
- if (typeof process !== "undefined" && process.env) {
12081
- return process.env;
12082
- }
12083
- return {};
12084
- }
12085
- var MIN_HIGH_CONFIDENCE_CHARS, MAX_TEXT_CHARS, TESSERACT_HIGH_CONFIDENCE_THRESHOLD;
12086
- var init_extractProblem = __esm({
12087
- "src/stamps/geometry-2d/ai/vision/extractProblem.ts"() {
12088
- init_providers();
12089
- init_envelope();
12090
- init_prompt();
12091
- init_tesseract();
12092
- MIN_HIGH_CONFIDENCE_CHARS = 10;
12093
- MAX_TEXT_CHARS = 2e3;
12094
- TESSERACT_HIGH_CONFIDENCE_THRESHOLD = 70;
12095
- }
12096
- });
12097
-
12098
- // src/stamps/geometry-2d/ai/handleExtractProblem.ts
12099
- async function handleExtractProblem(image, opts = {}) {
12100
- try {
12101
- const r = await extractProblemFromImage(image, opts);
12102
- if (r.ok) {
12103
- if (r.confidence === "low") {
12104
- return {
12105
- kind: "low-confidence",
12106
- text: r.text,
12107
- warning: "OCR c\xF3 th\u1EC3 kh\xF4ng ch\xEDnh x\xE1c, ki\u1EC3m tra tr\u01B0\u1EDBc khi v\u1EBD.",
12108
- usage: r.usage
12109
- };
12110
- }
12111
- return { kind: "success", text: r.text, usage: r.usage };
12112
- }
12113
- if (r.reason === "not-math") {
12114
- return { kind: "refused", reason: "not-math", message: r.message };
12115
- }
12116
- if (r.reason === "unsupported") {
12117
- return { kind: "error", code: "unsupported", message: r.message };
12118
- }
12119
- if (r.reason === "unreadable") {
12120
- return { kind: "error", code: "network", message: r.message };
12121
- }
12122
- return { kind: "error", code: "empty", message: r.message };
12123
- } catch (e) {
12124
- return {
12125
- kind: "error",
12126
- code: "unexpected",
12127
- message: e instanceof Error ? e.message : String(e)
12128
- };
12129
- }
12130
- }
12131
- var init_handleExtractProblem = __esm({
12132
- "src/stamps/geometry-2d/ai/handleExtractProblem.ts"() {
12133
- init_extractProblem();
12134
- }
12135
- });
12136
-
12137
- // src/stamps/geometry-2d/ai/vision/preprocess.ts
12138
- function inferMediaType(file) {
12139
- const t = file.type.toLowerCase();
12140
- if (ALLOWED_TYPES.includes(t)) return t;
12141
- return null;
12142
- }
12143
- function validateFile(file) {
12144
- const mt = inferMediaType(file);
12145
- if (mt == null) {
12146
- return {
12147
- ok: false,
12148
- code: "invalid-format",
12149
- message: "Ch\u1EC9 h\u1ED7 tr\u1EE3 PNG, JPEG, WEBP."
12150
- };
12151
- }
12152
- if (file.size > MAX_RAW_BYTES) {
12153
- return {
12154
- ok: false,
12155
- code: "too-large",
12156
- message: `\u1EA2nh qu\xE1 l\u1EDBn (> ${Math.round(MAX_RAW_BYTES / 1024 / 1024)} MB). Crop ho\u1EB7c resize tr\u01B0\u1EDBc.`
12157
- };
12158
- }
12159
- return { ok: true, mediaType: mt };
12160
- }
12161
- async function fileToImagePart(file) {
12162
- const v = validateFile(file);
12163
- if (!v.ok) throw new Error(v.message);
12164
- const bitmap = await createImageBitmap(file);
12165
- const { width, height } = bitmap;
12166
- const maxEdge = Math.max(width, height);
12167
- const scale = maxEdge > MAX_EDGE_PX ? MAX_EDGE_PX / maxEdge : 1;
12168
- const targetW = Math.round(width * scale);
12169
- const targetH = Math.round(height * scale);
12170
- const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(targetW, targetH) : Object.assign(document.createElement("canvas"), { width: targetW, height: targetH });
12171
- const ctx = canvas.getContext("2d");
12172
- if (!ctx) throw new Error("Kh\xF4ng t\u1EA1o \u0111\u01B0\u1EE3c canvas 2D context");
12173
- ctx.drawImage(bitmap, 0, 0, targetW, targetH);
12174
- bitmap.close();
12175
- let outputType = v.mediaType === "image/png" ? "image/png" : "image/jpeg";
12176
- let finalBlob = await canvasToBlob(canvas, outputType, 0.92);
12177
- if (finalBlob.size > MAX_ENCODED_BYTES) {
12178
- outputType = "image/jpeg";
12179
- finalBlob = await canvasToBlob(canvas, "image/jpeg", 0.7);
12180
- }
12181
- const base64 = await blobToBase64(finalBlob);
12182
- return { mediaType: outputType, base64 };
12183
- }
12184
- async function canvasToBlob(canvas, type, quality) {
12185
- if ("convertToBlob" in canvas) {
12186
- return canvas.convertToBlob({ type, quality });
12187
- }
12188
- return new Promise((resolve, reject) => {
12189
- canvas.toBlob(
12190
- (b) => b ? resolve(b) : reject(new Error("Canvas encode fail")),
12191
- type,
12192
- quality
12193
- );
12194
- });
12195
- }
12196
- async function blobToBase64(blob) {
12197
- const buf = await blob.arrayBuffer();
12198
- let binary = "";
12199
- const bytes = new Uint8Array(buf);
12200
- const chunk = 32768;
12201
- for (let i = 0; i < bytes.length; i += chunk) {
12202
- binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
12203
- }
12204
- return typeof btoa === "function" ? btoa(binary) : Buffer.from(binary, "binary").toString("base64");
12205
- }
12206
- var MAX_EDGE_PX, MAX_RAW_BYTES, MAX_ENCODED_BYTES, ALLOWED_TYPES;
12207
- var init_preprocess = __esm({
12208
- "src/stamps/geometry-2d/ai/vision/preprocess.ts"() {
12209
- MAX_EDGE_PX = 2048;
12210
- MAX_RAW_BYTES = 10 * 1024 * 1024;
12211
- MAX_ENCODED_BYTES = 3 * 1024 * 1024;
12212
- ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp"];
12213
- }
12214
- });
12215
- function AiFigurePrompt({
12216
- generator,
12217
- onGenerated,
12218
- currentState,
12219
- extractProblem = handleExtractProblem
12220
- }) {
12221
- const {
12222
- prompt,
12223
- setPrompt,
12224
- isLoading,
12225
- error,
12226
- submit,
12227
- cancel,
12228
- tokens,
12229
- mode,
12230
- setMode,
12231
- entityCount,
12232
- hasUnsupported
12233
- } = useAiFigure(generator, { currentState });
12234
- const [elapsed, setElapsed] = React2.useState(0);
12235
- React2.useEffect(() => {
12236
- if (!isLoading) {
12237
- setElapsed(0);
12238
- return;
12239
- }
12240
- const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
12241
- return () => clearInterval(id);
12242
- }, [isLoading]);
12243
- const [image, setImage] = React2.useState(null);
12244
- const [ocrLoading, setOcrLoading] = React2.useState(false);
12245
- const [ocrError, setOcrError] = React2.useState(null);
12246
- const [ocrWarning, setOcrWarning] = React2.useState(null);
12247
- const [isDragOver, setIsDragOver] = React2.useState(false);
12248
- const fileInputRef = React2.useRef(null);
12249
- const textareaRef = React2.useRef(null);
12250
- const imagePreview = image ? `data:${image.mediaType};base64,${image.base64}` : null;
12251
- React2.useEffect(() => {
12252
- setOcrError(null);
12253
- setOcrWarning(null);
12254
- }, [image]);
12255
- const handleFile = React2.useCallback(
12256
- async (file) => {
12257
- if (isLoading || ocrLoading) return;
12258
- const v = validateFile(file);
12259
- if (!v.ok) {
12260
- setOcrError(v.message);
12261
- return;
12262
- }
12263
- try {
12264
- const part = await fileToImagePart(file);
12265
- setImage(part);
12266
- } catch (e) {
12267
- setOcrError(e instanceof Error ? e.message : "Kh\xF4ng decode \u0111\u01B0\u1EE3c \u1EA3nh");
12268
- }
12269
- },
12270
- [isLoading, ocrLoading]
12271
- );
12272
- const handleFileInput = React2.useCallback(
12273
- (e) => {
12274
- const file = e.target.files?.[0];
12275
- if (file) void handleFile(file);
12276
- e.target.value = "";
12277
- },
12278
- [handleFile]
12279
- );
12280
- const handlePaste = React2.useCallback(
12281
- (e) => {
12282
- const item = Array.from(e.clipboardData.items).find(
12283
- (it) => it.kind === "file" && it.type.startsWith("image/")
12284
- );
12285
- if (!item) return;
12286
- const file = item.getAsFile();
12287
- if (!file) return;
12288
- e.preventDefault();
12289
- void handleFile(file);
12290
- },
12291
- [handleFile]
12292
- );
12293
- const handleDrop = React2.useCallback(
12294
- (e) => {
12295
- e.preventDefault();
12296
- setIsDragOver(false);
12297
- const file = Array.from(e.dataTransfer.files).find(
12298
- (f) => f.type.startsWith("image/")
12299
- );
12300
- if (file) void handleFile(file);
12301
- },
12302
- [handleFile]
12303
- );
12304
- const runOcr = React2.useCallback(async () => {
12305
- if (!image) return;
12306
- setOcrLoading(true);
12307
- setOcrError(null);
12308
- setOcrWarning(null);
12309
- try {
12310
- const r = await extractProblem(image);
12311
- if (r.kind === "success" || r.kind === "low-confidence") {
12312
- setPrompt(r.text);
12313
- if (r.kind === "low-confidence") setOcrWarning(r.warning);
12314
- requestAnimationFrame(() => textareaRef.current?.focus());
12315
- } else {
12316
- setOcrError(r.message);
12317
- }
12318
- } finally {
12319
- setOcrLoading(false);
12320
- }
12321
- }, [image, setPrompt, extractProblem]);
12322
- const handleSendClick = React2.useCallback(async () => {
12323
- if (image && !prompt.trim() && !ocrLoading) {
12324
- await runOcr();
12325
- return;
12326
- }
12327
- const generated = await submit();
12328
- if (generated) onGenerated(generated);
12329
- }, [image, prompt, ocrLoading, runOcr, submit, onGenerated]);
12330
- const handleSwitchToBuild = React2.useCallback(() => {
12331
- if (currentState && currentState.order.length > 0) {
12332
- const ok = window.confirm(
12333
- "D\u1EF1ng m\u1EDBi s\u1EBD thay to\xE0n b\u1ED9 h\xECnh hi\u1EC7n t\u1EA1i b\u1EB1ng h\xECnh m\u1EDBi t\u1EEB AI. Ti\u1EBFp t\u1EE5c?"
12334
- );
12335
- if (!ok) return;
12336
- }
12337
- setMode("build");
12338
- }, [currentState, setMode]);
12339
- const hasContent = currentState != null && currentState.order.length > 0;
12340
- const promptEmpty = !prompt.trim();
12341
- const willOcr = image != null && promptEmpty;
12342
- const sendDisabled = !image && promptEmpty || ocrLoading || isLoading && !willOcr;
12343
- const refineChipLabel = entityCount.points + entityCount.shapes > 0 ? `Th\xEAm v\xE0o \xB7 ${entityCount.points}\u0111, ${entityCount.shapes}\u0111o\u1EA1n` : "Th\xEAm v\xE0o";
12344
- const placeholder = willOcr ? "B\u1EA5m g\u1EEDi \u0111\u1EC3 \u0111\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh \u2014 ho\u1EB7c t\u1EF1 g\xF5 \u1EDF \u0111\xE2y\u2026" : mode === "refine" ? "M\xF4 t\u1EA3 ph\u1EA7n c\u1EA7n th\xEAm (vd: trung \u0111i\u1EC3m M c\u1EE7a BC). C\xF3 th\u1EC3 d\xE1n \u1EA3nh \u0111\u1EC1 (Ctrl+V)." : "M\xF4 t\u1EA3 \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng \u2014 ho\u1EB7c d\xE1n/\u0111\xEDnh \u1EA3nh \u0111\u1EC1 (Ctrl+V).";
12345
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-slate-200 bg-slate-50 px-3 py-3", children: [
12346
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
12347
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium tracking-wide text-slate-600", children: "D\u1EF1ng h\xECnh b\u1EB1ng AI" }),
12348
- hasContent && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", role: "tablist", "aria-label": "Ch\u1EBF \u0111\u1ED9 AI", children: [
12349
- /* @__PURE__ */ jsxRuntime.jsx(
12350
- "button",
12351
- {
12352
- type: "button",
12353
- role: "tab",
12354
- "aria-selected": mode === "refine",
12355
- "data-testid": "geometry-ai-mode-refine",
12356
- onClick: () => setMode("refine"),
12357
- disabled: isLoading || hasUnsupported,
12358
- title: hasUnsupported ? "H\xECnh c\xF3 \u0111\u1ED1i t\u01B0\u1EE3ng ngo\xE0i DSL \u2014 ch\u1EC9 d\u1EF1ng m\u1EDBi \u0111\u01B0\u1EE3c" : refineChipLabel,
12359
- className: `rounded-full px-2.5 py-0.5 text-[11px] transition ${mode === "refine" ? "bg-emerald-600 text-white shadow-sm" : "text-slate-500 hover:text-emerald-700"} ${hasUnsupported ? "cursor-not-allowed opacity-40" : ""}`,
12360
- children: refineChipLabel
12361
- }
12362
- ),
12363
- /* @__PURE__ */ jsxRuntime.jsx(
12364
- "button",
12365
- {
12366
- type: "button",
12367
- role: "tab",
12368
- "aria-selected": mode === "build",
12369
- "data-testid": "geometry-ai-mode-build",
12370
- onClick: handleSwitchToBuild,
12371
- disabled: isLoading,
12372
- className: `rounded-full px-2.5 py-0.5 text-[11px] transition ${mode === "build" ? "bg-emerald-600 text-white shadow-sm" : "text-slate-500 hover:text-emerald-700"}`,
12373
- children: "D\u1EF1ng m\u1EDBi"
12374
- }
12375
- )
12376
- ] })
12377
- ] }),
12378
- hasUnsupported && /* @__PURE__ */ jsxRuntime.jsx(
12379
- "p",
12380
- {
12381
- className: "mb-1.5 text-[10px] text-amber-700",
12382
- "data-testid": "geometry-ai-unsupported-warning",
12383
- children: "H\xECnh c\xF3 \u0111\u1ED1i t\u01B0\u1EE3ng ngo\xE0i DSL \u2014 ch\u1EC9 d\u1EF1ng m\u1EDBi \u0111\u01B0\u1EE3c"
12384
- }
12385
- ),
12386
- /* @__PURE__ */ jsxRuntime.jsxs(
12387
- "div",
12388
- {
12389
- onDragOver: (e) => {
12390
- e.preventDefault();
12391
- setIsDragOver(true);
12392
- },
12393
- onDragLeave: () => setIsDragOver(false),
12394
- onDrop: handleDrop,
12395
- onPaste: handlePaste,
12396
- "aria-label": "Khu v\u1EF1c k\xE9o th\u1EA3 \u1EA3nh",
12397
- role: "region",
12398
- className: "group relative flex flex-col rounded-2xl bg-white shadow-sm transition-all duration-150 ring-1 ring-slate-200 focus-within:ring-2 focus-within:ring-emerald-400/70 focus-within:shadow-md " + (isDragOver ? "ring-2 ring-emerald-500 bg-emerald-50/40" : ""),
12399
- children: [
12400
- image && imagePreview && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2 px-3 pt-2.5", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "group/chip relative", children: [
12401
- /* @__PURE__ */ jsxRuntime.jsx(
12402
- "img",
12403
- {
12404
- src: imagePreview,
12405
- alt: "\u1EA2nh \u0111\u1EC1 b\xE0i",
12406
- className: "max-h-48 max-w-full h-auto w-auto rounded-lg border border-slate-200 shadow-sm"
12407
- }
12408
- ),
12409
- /* @__PURE__ */ jsxRuntime.jsx(
12410
- "button",
12411
- {
12412
- type: "button",
12413
- onClick: () => setImage(null),
12414
- disabled: ocrLoading || isLoading,
12415
- "aria-label": "Xo\xE1 \u1EA3nh",
12416
- className: "absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full bg-slate-900/85 text-[11px] font-medium text-white shadow ring-2 ring-white transition hover:bg-slate-900 disabled:opacity-50",
12417
- children: "\xD7"
12418
- }
12419
- )
12420
- ] }) }),
12421
- /* @__PURE__ */ jsxRuntime.jsx(
12422
- "textarea",
12423
- {
12424
- ref: textareaRef,
12425
- id: "geometry-ai-prompt",
12426
- "aria-label": "\u0110\u1EC1 b\xE0i cho AI",
12427
- "data-testid": "geometry-ai-textarea",
12428
- value: prompt,
12429
- onChange: (e) => setPrompt(e.target.value),
12430
- onKeyDown: (e) => {
12431
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !sendDisabled) {
12432
- e.preventDefault();
12433
- void handleSendClick();
12434
- }
12435
- },
12436
- disabled: isLoading,
12437
- rows: 2,
12438
- placeholder,
12439
- className: "block w-full resize-none rounded-2xl bg-transparent px-3.5 pt-2.5 pb-1 text-sm leading-relaxed text-slate-800 placeholder:text-slate-400 outline-none disabled:opacity-60 field-sizing-content max-h-44"
12440
- }
12441
- ),
12442
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 px-2 pb-2 pt-1", children: [
12443
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
12444
- /* @__PURE__ */ jsxRuntime.jsx(
12445
- "button",
12446
- {
12447
- type: "button",
12448
- onClick: () => fileInputRef.current?.click(),
12449
- disabled: isLoading || ocrLoading,
12450
- "aria-label": "\u0110\xEDnh \u1EA3nh \u0111\u1EC1 b\xE0i",
12451
- title: "\u0110\xEDnh \u1EA3nh (c\u0169ng c\xF3 th\u1EC3 d\xE1n b\u1EB1ng Ctrl+V ho\u1EB7c k\xE9o th\u1EA3)",
12452
- className: "flex h-8 w-8 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-emerald-700 disabled:opacity-40",
12453
- children: /* @__PURE__ */ jsxRuntime.jsx(PaperclipIcon, { className: "h-[18px] w-[18px]" })
12454
- }
12455
- ),
12456
- /* @__PURE__ */ jsxRuntime.jsx(
12457
- "input",
12458
- {
12459
- ref: fileInputRef,
12460
- type: "file",
12461
- accept: "image/png,image/jpeg,image/webp",
12462
- className: "sr-only",
12463
- onChange: handleFileInput,
12464
- disabled: isLoading || ocrLoading,
12465
- "aria-label": "Ch\u1ECDn \u1EA3nh \u0111\u1EC1 b\xE0i"
12466
- }
12467
- )
12468
- ] }),
12469
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
12470
- (isLoading || ocrLoading) && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono text-[10px] tabular-nums text-slate-500", children: ocrLoading ? "\u0111\u1ECDc \u1EA3nh\u2026" : tokens > 0 ? `${tokens}tok \xB7 ${elapsed}s` : `${elapsed}s` }),
12471
- isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
12472
- "button",
12473
- {
12474
- type: "button",
12475
- onClick: cancel,
12476
- "aria-label": "Hu\u1EF7 d\u1EF1ng h\xECnh AI",
12477
- "data-testid": "geometry-ai-cancel",
12478
- title: `\u0110ang d\u1EF1ng\u2026 ${elapsed}s \u2014 b\u1EA5m \u0111\u1EC3 hu\u1EF7`,
12479
- className: "flex h-8 w-8 items-center justify-center rounded-full bg-amber-500 text-white shadow-sm transition hover:scale-105 hover:bg-amber-600 active:scale-95",
12480
- children: /* @__PURE__ */ jsxRuntime.jsx(StopIcon, { className: "h-3.5 w-3.5" })
12481
- }
12482
- ) : /* @__PURE__ */ jsxRuntime.jsx(
12483
- "button",
12484
- {
12485
- type: "button",
12486
- onClick: () => void handleSendClick(),
12487
- disabled: sendDisabled,
12488
- "aria-label": willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh" : "D\u1EF1ng b\u1EB1ng AI",
12489
- title: willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh (s\u1EBD \u0111i\u1EC1n v\xE0o \xF4 chat)" : "D\u1EF1ng b\u1EB1ng AI (Ctrl/\u2318+Enter)",
12490
- "data-testid": willOcr ? "geometry-ai-ocr" : "geometry-ai-submit",
12491
- className: "flex h-8 w-8 items-center justify-center rounded-full bg-emerald-600 text-white shadow-sm transition hover:scale-105 hover:bg-emerald-700 active:scale-95 disabled:cursor-not-allowed disabled:bg-slate-300 disabled:hover:scale-100",
12492
- children: /* @__PURE__ */ jsxRuntime.jsx(ArrowUpIcon, { className: "h-[18px] w-[18px]" })
12493
- }
12494
- )
12495
- ] })
12496
- ] }),
12497
- isDragOver && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl bg-emerald-50/60 text-xs font-medium text-emerald-700", children: "Th\u1EA3 \u1EA3nh v\xE0o \u0111\xE2y" })
12498
- ]
12499
- }
12500
- ),
12501
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mt-1.5 px-1 text-[10px] text-slate-500", children: [
12502
- "D\xE1n \u1EA3nh (Ctrl+V), k\xE9o th\u1EA3, ho\u1EB7c b\u1EA5m ",
12503
- /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, children: "\u{1F4CE}" }),
12504
- " \u0111\u1EC3 \u0111\xEDnh \u1EA3nh \u0111\u1EC1."
12505
- ] }),
12506
- error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error }),
12507
- ocrError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: ocrError }),
12508
- ocrWarning && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-1 rounded bg-amber-50 px-2 py-1 text-[11px] text-amber-700", children: ocrWarning })
12509
- ] });
12510
- }
12511
- var PaperclipIcon, ArrowUpIcon, StopIcon;
12512
- var init_AiFigurePrompt = __esm({
12513
- "src/stamps/geometry-2d/editor/AiFigurePrompt.tsx"() {
12514
- "use client";
12515
- init_useAiFigure();
12516
- init_handleExtractProblem();
12517
- init_preprocess();
12518
- PaperclipIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
12519
- "svg",
12520
- {
12521
- viewBox: "0 0 24 24",
12522
- fill: "none",
12523
- stroke: "currentColor",
12524
- strokeWidth: 1.75,
12525
- strokeLinecap: "round",
12526
- strokeLinejoin: "round",
12527
- "aria-hidden": true,
12528
- ...props,
12529
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21.44 11.05 12.25 20.24a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.41 17.41a2 2 0 1 1-2.83-2.83l8.49-8.49" })
12530
- }
12531
- );
12532
- ArrowUpIcon = (props) => /* @__PURE__ */ jsxRuntime.jsxs(
12533
- "svg",
12534
- {
12535
- viewBox: "0 0 24 24",
12536
- fill: "none",
12537
- stroke: "currentColor",
12538
- strokeWidth: 2.25,
12539
- strokeLinecap: "round",
12540
- strokeLinejoin: "round",
12541
- "aria-hidden": true,
12542
- ...props,
12543
- children: [
12544
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 19V5" }),
12545
- /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m5 12 7-7 7 7" })
12546
- ]
12547
- }
12548
- );
12549
- StopIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, ...props, children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) });
12550
- }
12551
- });
12552
- var GeometryEditorPanelInner, GeometryEditorPanel;
12553
- var init_EditorPanel = __esm({
12554
- "src/stamps/geometry-2d/editor/EditorPanel.tsx"() {
12555
- "use client";
12556
- init_MiniBoard();
12557
- init_serialize();
12558
- init_render();
12559
- init_PropertiesPopover();
12560
- init_MultiPropertiesPopover();
12561
- init_TransformParamPopover();
12562
- init_snapshot();
12563
- init_icons();
12564
- init_scene();
12565
- init_constants();
12566
- init_Toast2();
12567
- init_AiFigurePrompt();
12568
- GeometryEditorPanelInner = React2.forwardRef(
12569
- function GeometryEditorPanelInner2({
12570
- store,
12571
- onInsert,
12572
- onClose,
12573
- withLeftPanel = false,
12574
- selectedTool,
12575
- showAxis,
12576
- showGrid,
12577
- onHistoryChange,
12578
- isDark,
12579
- isMobile = false,
12580
- onOpenDrawer,
12581
- onUndo,
12582
- onRedo,
12583
- canUndo,
12584
- canRedo,
12585
- onSelectionChange,
12586
- generateGeometryFigure
12587
- }, ref) {
12588
- const { showToast } = useToast();
12589
- const handleRef = React2.useRef(null);
12590
- const [ready, setReady] = React2.useState(false);
12591
- const [hasContent, setHasContent] = React2.useState(false);
12592
- const [propsPopover, setPropsPopover] = React2.useState(null);
12593
- const [multiSelection, setMultiSelection] = React2.useState(null);
12594
- const [transformPopover, setTransformPopover] = React2.useState(null);
12595
- const onSelectionChangeRef = React2.useRef(onSelectionChange);
12596
- React2.useEffect(() => {
12597
- onSelectionChangeRef.current = onSelectionChange;
12598
- }, [onSelectionChange]);
12599
- useEditorState({ store, onHistoryChange });
12600
- const currentSceneState = React2.useSyncExternalStore(
12601
- (cb) => store.subscribe(cb),
12602
- () => store.getState(),
12603
- () => store.getState()
12604
- );
12605
- React2.useEffect(() => {
12606
- const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
12607
- sync();
12608
- return store.subscribe(sync);
12609
- }, [store]);
12610
- const handleReady = React2.useCallback(() => {
12611
- const h = handleRef.current;
12612
- if (!h) return;
12613
- setReady(true);
12614
- h.onSelect((snap) => {
12615
- setPropsPopover(snap);
12616
- setMultiSelection(null);
12617
- onSelectionChangeRef.current?.(snap.id);
12618
- });
12619
- h.onTransformParam((info) => setTransformPopover(info));
12620
- h.onSelectionState((snap) => {
12621
- if (!snap || snap.ids.length === 0) {
12622
- setPropsPopover(null);
12623
- setMultiSelection(null);
12624
- onSelectionChangeRef.current?.(void 0);
12625
- return;
12626
- }
12627
- if (snap.ids.length === 1) {
12628
- const id = snap.ids[0];
12629
- const single = buildObjectSnapshot(store.getState(), id, snap.anchor);
12630
- if (single) {
12631
- setPropsPopover(single);
12632
- setMultiSelection(null);
12633
- onSelectionChangeRef.current?.(id);
12634
- }
12635
- return;
12636
- }
12637
- setMultiSelection(snap);
12638
- setPropsPopover(null);
12639
- onSelectionChangeRef.current?.(void 0);
12640
- });
12641
- }, [store]);
12642
- const dismissPropsPopover = React2.useCallback(() => {
12643
- setPropsPopover(null);
12644
- onSelectionChangeRef.current?.(void 0);
12645
- }, []);
12646
- const dismissMultiPopover = React2.useCallback(() => {
12647
- setMultiSelection(null);
12648
- handleRef.current?.clearSelection();
12649
- onSelectionChangeRef.current?.(void 0);
12650
- }, []);
12651
- const applyMultiColor = React2.useCallback((color) => {
12652
- const ids = multiSelection?.ids ?? [];
12653
- const h = handleRef.current;
12654
- if (!h) return;
12655
- for (const id of ids) {
12656
- h.mutateObject(id, { attrs: { strokeColor: color, color } });
12657
- }
12658
- }, [multiSelection]);
12659
- const applyMultiDelete = React2.useCallback(() => {
12660
- const ids = multiSelection?.ids ?? [];
12661
- const h = handleRef.current;
12662
- if (!h) return;
12663
- for (const id of ids) {
12664
- h.mutateObject(id, { remove: true });
12665
- }
12666
- h.clearSelection();
12667
- setMultiSelection(null);
12668
- onSelectionChangeRef.current?.(void 0);
12669
- }, [multiSelection]);
12670
- const performInsert = React2.useCallback(() => {
12671
- if (!handleRef.current) return false;
12672
- const h = handleRef.current;
12673
- const state = h.getState();
12674
- if (Object.keys(state.objects).length === 0) return false;
12675
- const bbox = h.getBbox();
12676
- const jsonState = serializeBoard(state, { bbox, showAxis, showGrid });
12677
- void (async () => {
12678
- try {
12679
- const svgString = await renderGeometrySvgFromState(jsonState);
12680
- onInsert(jsonState, svgString);
12681
- } catch (err) {
12682
- console.error("Geometry insert failed:", err);
12683
- }
12684
- })();
12685
- return true;
12686
- }, [onInsert, showAxis, showGrid]);
12687
- const handleInsert = React2.useCallback(() => {
12688
- performInsert();
12689
- }, [performInsert]);
12690
- const loadAiFigure = React2.useCallback((generated) => {
12691
- handleRef.current?.clearSelection();
12692
- setPropsPopover(null);
12693
- setMultiSelection(null);
12694
- setTransformPopover(null);
12695
- onSelectionChangeRef.current?.(void 0);
12696
- const current = store.getState();
12697
- store.dispatch({
12698
- type: "LOAD",
12699
- payload: { state: { ...generated, meta: current.meta } }
12700
- });
12701
- }, [store]);
12702
- React2.useImperativeHandle(ref, () => ({
12703
- insert: performInsert,
12704
- hasContent: () => Object.keys(handleRef.current?.getState().objects ?? {}).length > 0,
12705
- selectObject: (id) => handleRef.current?.highlight(id)
12706
- }), [performInsert]);
12707
- const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
12708
- position: "absolute",
12709
- top: "50%",
12710
- left: withLeftPanel ? "calc(50% + 120px)" : "50%",
12711
- transform: "translate(-50%, -50%)",
12712
- zIndex: 40
12713
- };
12714
- return /* @__PURE__ */ jsxRuntime.jsxs(
12715
- "div",
12716
- {
12717
- role: "dialog",
12718
- "aria-label": "D\u1EF1ng h\xECnh h\u1ECDc",
12719
- "data-testid": "geometry-editor-panel",
12720
- "data-stamp-area": "true",
12721
- "data-mobile-editor": isMobile ? "true" : void 0,
12722
- style: wrapperStyle,
12723
- className: [
12724
- isDark ? "theme--dark " : "",
12725
- "relative flex flex-col overflow-hidden bg-white",
12726
- isMobile ? "h-full w-full" : `${STAMP_PANEL_DESKTOP} rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5`
12727
- ].join(" "),
12728
- children: [
12729
- /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-white", children: [
12730
- isMobile && /* @__PURE__ */ jsxRuntime.jsx(
12731
- "button",
12732
- {
12733
- type: "button",
12734
- onClick: onOpenDrawer,
12735
- "aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
12736
- className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
12737
- children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
12738
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
12739
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
12740
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
12741
- ] })
12742
- }
12743
- ),
12744
- /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
12745
- /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
12746
- /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "3,18 12,3 21,18" }),
12747
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "3", r: "1.5", fill: "currentColor" }),
12748
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "3", cy: "18", r: "1.5", fill: "currentColor" }),
12749
- /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "21", cy: "18", r: "1.5", fill: "currentColor" })
12750
- ] }),
12751
- "D\u1EF1ng h\xECnh h\u1ECDc"
12752
- ] }),
12753
- isMobile && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
12754
- /* @__PURE__ */ jsxRuntime.jsx(
12755
- "button",
12756
- {
12757
- type: "button",
12758
- onClick: onUndo,
12759
- disabled: !canUndo,
12760
- "aria-label": "Ho\xE0n t\xE1c",
12761
- title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
12762
- "data-testid": "undo-btn-mobile",
12763
- className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
12764
- children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon3, {})
12765
- }
12766
- ),
12767
- /* @__PURE__ */ jsxRuntime.jsx(
12768
- "button",
12769
- {
12770
- type: "button",
12771
- onClick: onRedo,
12772
- disabled: !canRedo,
12773
- "aria-label": "L\xE0m l\u1EA1i",
12774
- title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
12775
- "data-testid": "redo-btn-mobile",
12776
- className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
12777
- children: /* @__PURE__ */ jsxRuntime.jsx(RedoIcon3, {})
12778
- }
12779
- ),
12780
- /* @__PURE__ */ jsxRuntime.jsx(
12781
- "button",
12782
- {
12783
- type: "button",
12784
- onClick: handleInsert,
12785
- disabled: !ready || !hasContent,
12786
- title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
12787
- "data-testid": "geometry-insert-btn-mobile",
12788
- className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
12789
- children: "Ch\xE8n"
12790
- }
12791
- )
12792
- ] }),
12793
- /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onClose, "aria-label": "\u0110\xF3ng", className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
12794
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
12795
- /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
12796
- ] }) })
12797
- ] }),
12798
- generateGeometryFigure && /* @__PURE__ */ jsxRuntime.jsx(AiFigurePrompt, { generator: generateGeometryFigure, onGenerated: loadAiFigure, currentState: currentSceneState }),
12799
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
12800
- MiniBoard2D,
12801
- {
12802
- ref: handleRef,
12803
- store,
12804
- selectedTool,
12805
- showAxis,
12806
- showGrid,
12807
- onReady: handleReady,
12808
- isDark,
12809
- toast: showToast
12810
- }
12811
- ) }) }),
12812
- propsPopover && (propsPopover.kind === "point" ? /* @__PURE__ */ jsxRuntime.jsx(
12813
- PropertiesPopover,
12814
- {
12815
- kind: "point",
12816
- anchor: propsPopover.screenCoords,
12817
- isDark,
12818
- currentName: propsPopover.name,
12819
- currentColor: propsPopover.color,
12820
- currentDash: propsPopover.dash,
12821
- currentWidth: propsPopover.width,
12822
- currentFace: propsPopover.face,
12823
- currentShowLabel: propsPopover.showLabel,
12824
- getAllNames: () => handleRef.current?.getAllPointNames() ?? [],
12825
- onClose: dismissPropsPopover,
12826
- onMutate: (patch) => {
12827
- handleRef.current?.mutateObject(propsPopover.id, patch);
12828
- if (patch.remove) dismissPropsPopover();
12829
- if (typeof patch.valueLabel === "boolean" || patch.attrs) {
12830
- setPropsPopover((cur) => cur ? { ...cur, showValue: patch.valueLabel ?? cur.showValue } : cur);
12831
- }
12832
- }
12833
- }
12834
- ) : /* @__PURE__ */ jsxRuntime.jsx(
12835
- PropertiesPopover,
12836
- {
12837
- kind: propsPopover.kind,
12838
- anchor: propsPopover.screenCoords,
12839
- isDark,
12840
- currentName: propsPopover.name,
12841
- currentColor: propsPopover.color,
12842
- currentDash: propsPopover.dash,
12843
- currentWidth: propsPopover.width,
12844
- currentShowLabel: propsPopover.showLabel,
12845
- currentShowValue: propsPopover.showValue,
12846
- getAllNames: () => handleRef.current?.getAllPointNames() ?? [],
12847
- onClose: dismissPropsPopover,
12848
- onMutate: (patch) => {
12849
- handleRef.current?.mutateObject(propsPopover.id, patch);
12850
- if (patch.remove) dismissPropsPopover();
12851
- if (typeof patch.valueLabel === "boolean") {
12852
- setPropsPopover((cur) => cur ? { ...cur, showValue: patch.valueLabel ?? cur.showValue } : cur);
12853
- }
12854
- if (patch.attrs && "withLabel" in patch.attrs) {
12855
- setPropsPopover((cur) => cur ? { ...cur, showLabel: !!patch.attrs?.withLabel } : cur);
12856
- }
12857
- }
12858
- }
12859
- )),
12860
- multiSelection && multiSelection.ids.length > 1 && /* @__PURE__ */ jsxRuntime.jsx(
12861
- MultiPropertiesPopover,
12862
- {
12863
- anchor: multiSelection.anchor,
12864
- count: multiSelection.ids.length,
12865
- isDark,
12866
- onColor: applyMultiColor,
12867
- onDelete: applyMultiDelete,
12868
- onClose: dismissMultiPopover
12869
- }
12870
- ),
12871
- transformPopover && (transformPopover.tool === "rotate" || transformPopover.tool === "dilate" || transformPopover.tool === "regularPolygon" || transformPopover.tool === "circleCR") && /* @__PURE__ */ jsxRuntime.jsx(
12872
- TransformParamPopover,
12873
- {
12874
- kind: transformPopover.tool,
12875
- anchor: transformPopover.anchor,
12876
- defaultValue: transformPopover.tool === "rotate" ? 90 : transformPopover.tool === "dilate" ? 2 : transformPopover.tool === "circleCR" ? 3 : 6,
12877
- isDark,
12878
- onConfirm: (v) => {
12879
- handleRef.current?.confirmTransformParam(v);
12880
- setTransformPopover(null);
12881
- },
12882
- onCancel: () => {
12883
- handleRef.current?.cancelTransformParam();
12884
- setTransformPopover(null);
12885
- }
12886
- }
12887
- ),
12888
- !isMobile && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
12889
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
12890
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
12891
- /* @__PURE__ */ jsxRuntime.jsx(
12892
- "button",
12893
- {
12894
- onClick: onClose,
12895
- className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
12896
- children: "Hu\u1EF7"
12897
- }
12898
- ),
12899
- /* @__PURE__ */ jsxRuntime.jsx(
12900
- "button",
12901
- {
12902
- onClick: handleInsert,
12903
- disabled: !ready || !hasContent,
12904
- title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
12905
- "data-testid": "geometry-insert-btn",
12906
- className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
12907
- children: "Ch\xE8n"
12908
- }
12909
- )
12910
- ] })
12911
- ] }),
12912
- /* @__PURE__ */ jsxRuntime.jsx(ToastHost, {})
12913
- ]
12914
- }
12915
- );
12916
- }
12917
- );
12918
- GeometryEditorPanel = React2.forwardRef(
12919
- function GeometryEditorPanel2(props, ref) {
12920
- return /* @__PURE__ */ jsxRuntime.jsx(ToastProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(GeometryEditorPanelInner, { ...props, ref }) });
12921
- }
12922
- );
12923
- }
12924
- });
12925
- function isFieldFocused() {
12926
- const ae = typeof document !== "undefined" ? document.activeElement : null;
12927
- return !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
10341
+ return fail("unsupported-construction", "missing vertices");
10342
+ }
10343
+ const refs = resolveRefs(a.vertices, state);
10344
+ if (!refs) return fail("unresolved-ref", a.vertices.join(","));
10345
+ return { ok: true, entity: { name: obj.label, kind: "polygon", vertices: refs } };
12928
10346
  }
12929
- function useChordShortcut(args) {
12930
- const { groupOrder, tools, onSelect, enabled } = args;
12931
- const [chordGroup, setChordGroup] = React2.useState(null);
12932
- const groupOrderRef = React2.useRef(groupOrder);
12933
- const toolsRef = React2.useRef(tools);
12934
- const onSelectRef = React2.useRef(onSelect);
12935
- const chordGroupRef = React2.useRef(null);
12936
- groupOrderRef.current = groupOrder;
12937
- toolsRef.current = tools;
12938
- onSelectRef.current = onSelect;
12939
- const cancel = React2.useCallback(() => {
12940
- chordGroupRef.current = null;
12941
- setChordGroup(null);
12942
- }, []);
12943
- React2.useEffect(() => {
12944
- if (!enabled) return;
12945
- const setChord = (next) => {
12946
- chordGroupRef.current = next;
12947
- setChordGroup(next);
10347
+ function serializeCircle(obj, state) {
10348
+ const a = obj.attrs;
10349
+ const c = (() => {
10350
+ if (a.construction) return a.construction;
10351
+ const raw = a;
10352
+ if (raw.kind === "incircle" && raw.vertices) {
10353
+ return {
10354
+ kind: "incircle",
10355
+ p1: raw.vertices[0],
10356
+ p2: raw.vertices[1],
10357
+ p3: raw.vertices[2]
10358
+ };
10359
+ }
10360
+ if (raw.kind === "excircle" && raw.vertices && raw.opposite) {
10361
+ return {
10362
+ kind: "excircle",
10363
+ p1: raw.vertices[0],
10364
+ p2: raw.vertices[1],
10365
+ p3: raw.vertices[2],
10366
+ opposite: raw.opposite
10367
+ };
10368
+ }
10369
+ if (raw.kind === "circleDiameter" && raw.p1 && raw.p2) {
10370
+ return {
10371
+ kind: "diameter",
10372
+ p1: raw.p1,
10373
+ p2: raw.p2
10374
+ };
10375
+ }
10376
+ return void 0;
10377
+ })();
10378
+ if (!c) {
10379
+ if (typeof a.radius === "number") {
10380
+ if (!a.center) return fail("unsupported-construction", "missing center");
10381
+ const refs2 = resolveRefs([a.center], state);
10382
+ if (!refs2) return fail("unresolved-ref", `${a.center}`);
10383
+ return {
10384
+ ok: true,
10385
+ entity: { name: obj.label, kind: "circleCR", center: refs2[0], radius: a.radius }
10386
+ };
10387
+ }
10388
+ if (!a.center || !a.surfacePoint) {
10389
+ return fail("unsupported-construction", "missing center/surfacePoint");
10390
+ }
10391
+ const refs = resolveRefs([a.center, a.surfacePoint], state);
10392
+ if (!refs) return fail("unresolved-ref", `${a.center},${a.surfacePoint}`);
10393
+ return {
10394
+ ok: true,
10395
+ entity: { name: obj.label, kind: "circleCP", center: refs[0], surfacePoint: refs[1] }
12948
10396
  };
12949
- const onKey = (e) => {
12950
- if (e.metaKey || e.ctrlKey || e.altKey) return;
12951
- if (isFieldFocused()) return;
12952
- const key = e.key;
12953
- const lower = key.length === 1 ? key.toLowerCase() : key;
12954
- if (key === "Escape") {
12955
- if (chordGroupRef.current !== null) {
12956
- e.preventDefault();
12957
- e.stopPropagation();
12958
- setChord(null);
12959
- }
12960
- return;
12961
- }
12962
- if (lower.length === 1 && lower >= "a" && lower <= "z") {
12963
- const idx = lower.charCodeAt(0) - A_CODE2;
12964
- if (idx < groupOrderRef.current.length) {
12965
- e.preventDefault();
12966
- e.stopPropagation();
12967
- setChord(groupOrderRef.current[idx]);
12968
- }
12969
- return;
12970
- }
12971
- if (key >= "1" && key <= "9") {
12972
- const active = chordGroupRef.current;
12973
- if (active === null) return;
12974
- const n = key.charCodeAt(0) - "1".charCodeAt(0);
12975
- const toolsInGroup = toolsRef.current.filter(
12976
- (t) => t.group === active
12977
- );
12978
- e.preventDefault();
12979
- e.stopPropagation();
12980
- if (n < toolsInGroup.length) {
12981
- onSelectRef.current(toolsInGroup[n].key);
12982
- }
12983
- setChord(null);
12984
- return;
12985
- }
10397
+ }
10398
+ if (c.kind === "circumscribed") {
10399
+ const refs = resolveRefs([c.p1, c.p2, c.p3], state);
10400
+ if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2},${c.p3}`);
10401
+ return {
10402
+ ok: true,
10403
+ entity: { name: obj.label, kind: "circle3", p1: refs[0], p2: refs[1], p3: refs[2] }
12986
10404
  };
12987
- window.addEventListener("keydown", onKey, { capture: true });
12988
- return () => {
12989
- window.removeEventListener("keydown", onKey, { capture: true });
10405
+ }
10406
+ if (c.kind === "incircle") {
10407
+ const refs = resolveRefs([c.p1, c.p2, c.p3], state);
10408
+ if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2},${c.p3}`);
10409
+ return {
10410
+ ok: true,
10411
+ entity: { name: obj.label, kind: "incircle", vertices: [refs[0], refs[1], refs[2]] }
12990
10412
  };
12991
- }, [enabled]);
12992
- return { chordGroup, cancel };
12993
- }
12994
- var A_CODE2;
12995
- var init_useChordShortcut = __esm({
12996
- "src/stamps/shared/useChordShortcut.ts"() {
12997
- A_CODE2 = "a".charCodeAt(0);
12998
10413
  }
12999
- });
13000
-
13001
- // src/stamps/shared/insertImage.ts
13002
- function buildStampImageElement(api, fileId, width, height, customData, x, y) {
13003
- const appState = api?.getAppState() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
13004
- const cx = x ?? appState.scrollX + (appState.width ?? 800) / 2 / (appState.zoom?.value ?? 1) - width / 2;
13005
- const cy = y ?? appState.scrollY + (appState.height ?? 600) / 2 / (appState.zoom?.value ?? 1) - height / 2;
13006
- return {
13007
- type: "image",
13008
- id: "stamp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8),
13009
- x: cx,
13010
- y: cy,
13011
- width,
13012
- height,
13013
- fileId,
13014
- customData,
13015
- angle: 0,
13016
- strokeColor: "transparent",
13017
- backgroundColor: "transparent",
13018
- fillStyle: "solid",
13019
- strokeWidth: 1,
13020
- strokeStyle: "solid",
13021
- roughness: 0,
13022
- opacity: 100,
13023
- groupIds: [],
13024
- roundness: null,
13025
- seed: Math.floor(Math.random() * 1e9),
13026
- versionNonce: 0,
13027
- version: 1,
13028
- isDeleted: false,
13029
- boundElements: null,
13030
- updated: Date.now(),
13031
- link: null,
13032
- locked: false,
13033
- status: "saved",
13034
- scale: [1, 1]
13035
- };
13036
- }
13037
- async function insertStampImage(api, opts) {
13038
- const { dataURL, fileId, width, height, mimeType } = await createStampFile(opts.svgString);
13039
- api.addFiles([{ id: fileId, dataURL, mimeType, created: Date.now() }]);
13040
- const customData = opts.makeCustomData();
13041
- const elements = api.getSceneElements();
13042
- const editingId = opts.editingElementId ?? null;
13043
- if (editingId) {
13044
- const updated = elements.map(
13045
- (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
13046
- );
13047
- api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
13048
- return { fileId, width, height, elementId: editingId };
10414
+ if (c.kind === "excircle") {
10415
+ const refs = resolveRefs([c.p1, c.p2, c.p3, c.opposite], state);
10416
+ if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2},${c.p3},${c.opposite}`);
10417
+ return {
10418
+ ok: true,
10419
+ entity: { name: obj.label, kind: "excircle", vertices: [refs[0], refs[1], refs[2]], opposite: refs[3] }
10420
+ };
13049
10421
  }
13050
- const newElement = buildStampImageElement(
13051
- api,
13052
- fileId,
13053
- width,
13054
- height,
13055
- customData,
13056
- opts.position?.x,
13057
- opts.position?.y
13058
- );
13059
- api.updateScene({
13060
- elements: [...elements, newElement],
13061
- appState: clearAppStateAfterInsert()
13062
- });
13063
- return { fileId, width, height, elementId: newElement.id };
10422
+ if (c.kind === "diameter") {
10423
+ const refs = resolveRefs([c.p1, c.p2], state);
10424
+ if (!refs) return fail("unresolved-ref", `${c.p1},${c.p2}`);
10425
+ return {
10426
+ ok: true,
10427
+ entity: { name: obj.label, kind: "circleDiameter", p1: refs[0], p2: refs[1] }
10428
+ };
10429
+ }
10430
+ return fail("unsupported-construction");
13064
10431
  }
13065
- var clearAppStateAfterInsert;
13066
- var init_insertImage = __esm({
13067
- "src/stamps/shared/insertImage.ts"() {
13068
- init_svgToStampFile();
13069
- clearAppStateAfterInsert = () => ({
13070
- selectedElementIds: {},
13071
- croppingElementId: null
13072
- });
10432
+ function serializeObject(obj, state) {
10433
+ if (!isValidName(obj.label)) {
10434
+ return fail("invalid-label", obj.label);
13073
10435
  }
13074
- });
13075
- function useStampStore(domain, editingElement, parseInitial) {
13076
- const ref = React2.useRef(null);
13077
- if (!ref.current) {
13078
- const initial = editingElement?.customData ? parseInitial(editingElement.customData) ?? createEmptyState(domain) : createEmptyState(domain);
13079
- ref.current = createStore(initial);
10436
+ switch (obj.kind) {
10437
+ case "point":
10438
+ return serializePoint(obj, state);
10439
+ case "segment":
10440
+ return serializeSegment(obj, state);
10441
+ case "ray":
10442
+ return serializeRay(obj, state);
10443
+ case "line":
10444
+ return serializeLine(obj, state);
10445
+ case "polygon":
10446
+ return serializePolygon(obj, state);
10447
+ case "circle":
10448
+ return serializeCircle(obj, state);
10449
+ case "intersection":
10450
+ return serializeIntersection(obj, state);
10451
+ default:
10452
+ return fail("unsupported-kind", obj.kind);
13080
10453
  }
13081
- return ref.current;
13082
10454
  }
13083
- var init_useStampStore = __esm({
13084
- "src/stamps/shared/useStampStore.ts"() {
13085
- init_scene();
10455
+ var NAME_REGEX;
10456
+ var init_serialize2 = __esm({
10457
+ "src/stamps/geometry-2d/dsl/serialize.ts"() {
10458
+ NAME_REGEX = /^[A-Za-z][A-Za-z0-9_'₀-₉]{0,11}$/;
13086
10459
  }
13087
10460
  });
13088
10461
 
@@ -13260,7 +10633,7 @@ var init_host = __esm({
13260
10633
  init_useStampStore();
13261
10634
  init_dslRenderRow();
13262
10635
  GeometryStampHost = React2.forwardRef(
13263
- function GeometryStampHost2({ api, editingElement, onClose, isDark, generateGeometryFigure }, ref) {
10636
+ function GeometryStampHost2({ api, editingElement, onClose, isDark, generateGeometryFigure, onGeometryDraft }, ref) {
13264
10637
  const panelRef = React2.useRef(null);
13265
10638
  const { isMobile } = useIsMobile();
13266
10639
  const [drawerOpen, setDrawerOpen] = React2.useState(false);
@@ -13375,7 +10748,9 @@ var init_host = __esm({
13375
10748
  canUndo,
13376
10749
  canRedo,
13377
10750
  onSelectionChange: setSelectedObjectId,
13378
- generateGeometryFigure
10751
+ generateGeometryFigure,
10752
+ api,
10753
+ onGeometryDraft
13379
10754
  }
13380
10755
  )
13381
10756
  ] });