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