@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
@@ -1,22 +1,21 @@
1
1
  "use client";
2
2
  import { useChordShortcut } from './chunk-HNQLZIEP.mjs';
3
3
  import { useToolStateMachine } from './chunk-NVJ7K3DK.mjs';
4
- import { safeJsx, initJxgBoard, attachJxgWheelZoom, useToast, ToastHost, STAMP_PANEL_DESKTOP, ToastProvider, useStampStore, StampLeftPanel, ObjectRow } from './chunk-I3L56GVH.mjs';
5
- import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData, deserializeBoard } from './chunk-AYJPOHCI.mjs';
4
+ import { safeJsx, initJxgBoard, attachJxgWheelZoom, useToast, ToastHost, STAMP_PANEL_DESKTOP, ToastProvider, useStampStore, StampLeftPanel, ObjectRow } from './chunk-OQIQNKPQ.mjs';
5
+ import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData, deserializeBoard } from './chunk-H22OZYTW.mjs';
6
6
  import { themeLabel, paletteFor, themeAxis, themeGrid } from './chunk-R5FL6S7L.mjs';
7
7
  import { JxgRenderer } from './chunk-SZDAS7LK.mjs';
8
8
  import './chunk-ICR4CVOE.mjs';
9
- import { nextLabel, useEditorState, listObjects } from './chunk-D5LWSN2Y.mjs';
9
+ import { nextLabel, useEditorState, listObjects } from './chunk-ZTQBUKLJ.mjs';
10
10
  import './chunk-IHUFOV7L.mjs';
11
- import { validateFile, fileToImagePart, describeDsl, serializeState } from './chunk-AJAHD35N.mjs';
12
- import { handleExtractProblem } from './chunk-T3SOHYB2.mjs';
11
+ import { describeDsl } from './chunk-E6EDOPGT.mjs';
13
12
  import { DEFAULT_VIEW_2D } from './chunk-73Q7ADVL.mjs';
14
13
  import './chunk-B4NJJZFR.mjs';
15
14
  import { useIsMobile } from './chunk-P2AOIF7S.mjs';
16
15
  import { insertStampImage } from './chunk-QGNU34T7.mjs';
17
16
  import './chunk-5UTGXHLJ.mjs';
18
17
  import { __export } from './chunk-J5LGTIGS.mjs';
19
- import { forwardRef, useRef, useId, useState, useEffect, useCallback, useImperativeHandle, useSyncExternalStore, useMemo, useLayoutEffect } from 'react';
18
+ import { forwardRef, useRef, useId, useState, useEffect, useCallback, useImperativeHandle, useMemo, useLayoutEffect } from 'react';
20
19
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
21
20
  import { createPortal } from 'react-dom';
22
21
 
@@ -3268,41 +3267,13 @@ var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel,
3268
3267
  );
3269
3268
  return createPortal(node, document.body);
3270
3269
  };
3271
- function useAiFigure(generator, options = {}) {
3272
- const { currentState } = options;
3270
+ function useAiFigure(generator) {
3273
3271
  const [prompt, setPrompt] = useState("");
3274
3272
  const [isLoading, setIsLoading] = useState(false);
3275
3273
  const [error, setError] = useState(null);
3276
3274
  const [tokens, setTokens] = useState(0);
3277
3275
  const abortRef = useRef(null);
3278
3276
  const requestIdRef = useRef(0);
3279
- const { dsl: currentDsl, unsupported, entityCount, hasContent } = useMemo(() => {
3280
- if (!currentState || currentState.order.length === 0) {
3281
- return {
3282
- dsl: null,
3283
- unsupported: [],
3284
- entityCount: { points: 0, shapes: 0 },
3285
- hasContent: false
3286
- };
3287
- }
3288
- const { dsl, unsupported: unsupported2 } = serializeState(currentState);
3289
- return {
3290
- dsl,
3291
- unsupported: unsupported2,
3292
- entityCount: { points: dsl.points.length, shapes: dsl.shapes.length },
3293
- hasContent: true
3294
- };
3295
- }, [currentState]);
3296
- const hasUnsupported = unsupported.length > 0;
3297
- const initialMode = hasContent && !hasUnsupported ? "refine" : "build";
3298
- const [mode, setModeInternal] = useState(initialMode);
3299
- useEffect(() => {
3300
- if (!hasContent && mode === "refine") setModeInternal("build");
3301
- if (hasUnsupported && mode === "refine") setModeInternal("build");
3302
- }, [hasContent, hasUnsupported, mode]);
3303
- const setMode = useCallback((next) => {
3304
- setModeInternal(next);
3305
- }, []);
3306
3277
  useEffect(() => () => abortRef.current?.abort(), []);
3307
3278
  const submit = useCallback(async () => {
3308
3279
  const problem = prompt.trim();
@@ -3326,8 +3297,7 @@ function useAiFigure(generator, options = {}) {
3326
3297
  signal: controller.signal,
3327
3298
  onProgress: (info) => {
3328
3299
  if (requestId === requestIdRef.current) setTokens(info.tokens);
3329
- },
3330
- ...mode === "refine" && currentDsl ? { currentDsl } : {}
3300
+ }
3331
3301
  });
3332
3302
  if (controller.signal.aborted || requestId !== requestIdRef.current) return null;
3333
3303
  if (!generated.ok) {
@@ -3351,7 +3321,7 @@ function useAiFigure(generator, options = {}) {
3351
3321
  setIsLoading(false);
3352
3322
  }
3353
3323
  }
3354
- }, [generator, prompt, mode, currentDsl]);
3324
+ }, [generator, prompt]);
3355
3325
  const cancel = useCallback(() => {
3356
3326
  abortRef.current?.abort();
3357
3327
  }, []);
@@ -3362,27 +3332,9 @@ function useAiFigure(generator, options = {}) {
3362
3332
  error,
3363
3333
  submit,
3364
3334
  cancel,
3365
- tokens,
3366
- mode,
3367
- setMode,
3368
- entityCount,
3369
- hasUnsupported
3335
+ tokens
3370
3336
  };
3371
3337
  }
3372
- var PaperclipIcon = (props) => /* @__PURE__ */ jsx(
3373
- "svg",
3374
- {
3375
- viewBox: "0 0 24 24",
3376
- fill: "none",
3377
- stroke: "currentColor",
3378
- strokeWidth: 1.75,
3379
- strokeLinecap: "round",
3380
- strokeLinejoin: "round",
3381
- "aria-hidden": true,
3382
- ...props,
3383
- children: /* @__PURE__ */ 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" })
3384
- }
3385
- );
3386
3338
  var ArrowUpIcon = (props) => /* @__PURE__ */ jsxs(
3387
3339
  "svg",
3388
3340
  {
@@ -3401,12 +3353,7 @@ var ArrowUpIcon = (props) => /* @__PURE__ */ jsxs(
3401
3353
  }
3402
3354
  );
3403
3355
  var StopIcon = (props) => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, ...props, children: /* @__PURE__ */ jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }) });
3404
- function AiFigurePrompt({
3405
- generator,
3406
- onGenerated,
3407
- currentState,
3408
- extractProblem = handleExtractProblem
3409
- }) {
3356
+ function AiFigurePrompt({ generator, onGenerated }) {
3410
3357
  const {
3411
3358
  prompt,
3412
3359
  setPrompt,
@@ -3414,12 +3361,8 @@ function AiFigurePrompt({
3414
3361
  error,
3415
3362
  submit,
3416
3363
  cancel,
3417
- tokens,
3418
- mode,
3419
- setMode,
3420
- entityCount,
3421
- hasUnsupported
3422
- } = useAiFigure(generator, { currentState });
3364
+ tokens
3365
+ } = useAiFigure(generator);
3423
3366
  const [elapsed, setElapsed] = useState(0);
3424
3367
  useEffect(() => {
3425
3368
  if (!isLoading) {
@@ -3429,184 +3372,20 @@ function AiFigurePrompt({
3429
3372
  const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
3430
3373
  return () => clearInterval(id);
3431
3374
  }, [isLoading]);
3432
- const [image, setImage] = useState(null);
3433
- const [ocrLoading, setOcrLoading] = useState(false);
3434
- const [ocrError, setOcrError] = useState(null);
3435
- const [ocrWarning, setOcrWarning] = useState(null);
3436
- const [isDragOver, setIsDragOver] = useState(false);
3437
- const fileInputRef = useRef(null);
3438
3375
  const textareaRef = useRef(null);
3439
- const imagePreview = image ? `data:${image.mediaType};base64,${image.base64}` : null;
3440
- useEffect(() => {
3441
- setOcrError(null);
3442
- setOcrWarning(null);
3443
- }, [image]);
3444
- const handleFile = useCallback(
3445
- async (file) => {
3446
- if (isLoading || ocrLoading) return;
3447
- const v = validateFile(file);
3448
- if (!v.ok) {
3449
- setOcrError(v.message);
3450
- return;
3451
- }
3452
- try {
3453
- const part = await fileToImagePart(file);
3454
- setImage(part);
3455
- } catch (e) {
3456
- setOcrError(e instanceof Error ? e.message : "Kh\xF4ng decode \u0111\u01B0\u1EE3c \u1EA3nh");
3457
- }
3458
- },
3459
- [isLoading, ocrLoading]
3460
- );
3461
- const handleFileInput = useCallback(
3462
- (e) => {
3463
- const file = e.target.files?.[0];
3464
- if (file) void handleFile(file);
3465
- e.target.value = "";
3466
- },
3467
- [handleFile]
3468
- );
3469
- const handlePaste = useCallback(
3470
- (e) => {
3471
- const item = Array.from(e.clipboardData.items).find(
3472
- (it) => it.kind === "file" && it.type.startsWith("image/")
3473
- );
3474
- if (!item) return;
3475
- const file = item.getAsFile();
3476
- if (!file) return;
3477
- e.preventDefault();
3478
- void handleFile(file);
3479
- },
3480
- [handleFile]
3481
- );
3482
- const handleDrop = useCallback(
3483
- (e) => {
3484
- e.preventDefault();
3485
- setIsDragOver(false);
3486
- const file = Array.from(e.dataTransfer.files).find(
3487
- (f) => f.type.startsWith("image/")
3488
- );
3489
- if (file) void handleFile(file);
3490
- },
3491
- [handleFile]
3492
- );
3493
- const runOcr = useCallback(async () => {
3494
- if (!image) return;
3495
- setOcrLoading(true);
3496
- setOcrError(null);
3497
- setOcrWarning(null);
3498
- try {
3499
- const r = await extractProblem(image);
3500
- if (r.kind === "success" || r.kind === "low-confidence") {
3501
- setPrompt(r.text);
3502
- if (r.kind === "low-confidence") setOcrWarning(r.warning);
3503
- requestAnimationFrame(() => textareaRef.current?.focus());
3504
- } else {
3505
- setOcrError(r.message);
3506
- }
3507
- } finally {
3508
- setOcrLoading(false);
3509
- }
3510
- }, [image, setPrompt, extractProblem]);
3511
3376
  const handleSendClick = useCallback(async () => {
3512
- if (image && !prompt.trim() && !ocrLoading) {
3513
- await runOcr();
3514
- return;
3515
- }
3516
3377
  const generated = await submit();
3517
3378
  if (generated) onGenerated(generated);
3518
- }, [image, prompt, ocrLoading, runOcr, submit, onGenerated]);
3519
- const handleSwitchToBuild = useCallback(() => {
3520
- if (currentState && currentState.order.length > 0) {
3521
- const ok = window.confirm(
3522
- "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?"
3523
- );
3524
- if (!ok) return;
3525
- }
3526
- setMode("build");
3527
- }, [currentState, setMode]);
3528
- const hasContent = currentState != null && currentState.order.length > 0;
3379
+ }, [submit, onGenerated]);
3529
3380
  const promptEmpty = !prompt.trim();
3530
- const willOcr = image != null && promptEmpty;
3531
- const sendDisabled = !image && promptEmpty || ocrLoading || isLoading && !willOcr;
3532
- const refineChipLabel = entityCount.points + entityCount.shapes > 0 ? `Th\xEAm v\xE0o \xB7 ${entityCount.points}\u0111, ${entityCount.shapes}\u0111o\u1EA1n` : "Th\xEAm v\xE0o";
3533
- 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).";
3381
+ const sendDisabled = promptEmpty || isLoading;
3534
3382
  return /* @__PURE__ */ jsxs("div", { className: "border-b border-slate-200 bg-slate-50 px-3 py-3", children: [
3535
- /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
3536
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium tracking-wide text-slate-600", children: "D\u1EF1ng h\xECnh b\u1EB1ng AI" }),
3537
- hasContent && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", role: "tablist", "aria-label": "Ch\u1EBF \u0111\u1ED9 AI", children: [
3538
- /* @__PURE__ */ jsx(
3539
- "button",
3540
- {
3541
- type: "button",
3542
- role: "tab",
3543
- "aria-selected": mode === "refine",
3544
- "data-testid": "geometry-ai-mode-refine",
3545
- onClick: () => setMode("refine"),
3546
- disabled: isLoading || hasUnsupported,
3547
- 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,
3548
- 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" : ""}`,
3549
- children: refineChipLabel
3550
- }
3551
- ),
3552
- /* @__PURE__ */ jsx(
3553
- "button",
3554
- {
3555
- type: "button",
3556
- role: "tab",
3557
- "aria-selected": mode === "build",
3558
- "data-testid": "geometry-ai-mode-build",
3559
- onClick: handleSwitchToBuild,
3560
- disabled: isLoading,
3561
- 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"}`,
3562
- children: "D\u1EF1ng m\u1EDBi"
3563
- }
3564
- )
3565
- ] })
3566
- ] }),
3567
- hasUnsupported && /* @__PURE__ */ jsx(
3568
- "p",
3569
- {
3570
- className: "mb-1.5 text-[10px] text-amber-700",
3571
- "data-testid": "geometry-ai-unsupported-warning",
3572
- children: "H\xECnh c\xF3 \u0111\u1ED1i t\u01B0\u1EE3ng ngo\xE0i DSL \u2014 ch\u1EC9 d\u1EF1ng m\u1EDBi \u0111\u01B0\u1EE3c"
3573
- }
3574
- ),
3383
+ /* @__PURE__ */ jsx("div", { className: "mb-2 flex items-center justify-between gap-2", children: /* @__PURE__ */ jsx("span", { className: "text-xs font-medium tracking-wide text-slate-600", children: "D\u1EF1ng h\xECnh b\u1EB1ng AI" }) }),
3575
3384
  /* @__PURE__ */ jsxs(
3576
3385
  "div",
3577
3386
  {
3578
- onDragOver: (e) => {
3579
- e.preventDefault();
3580
- setIsDragOver(true);
3581
- },
3582
- onDragLeave: () => setIsDragOver(false),
3583
- onDrop: handleDrop,
3584
- onPaste: handlePaste,
3585
- "aria-label": "Khu v\u1EF1c k\xE9o th\u1EA3 \u1EA3nh",
3586
- role: "region",
3587
- 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" : ""),
3387
+ 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",
3588
3388
  children: [
3589
- image && imagePreview && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 px-3 pt-2.5", children: /* @__PURE__ */ jsxs("div", { className: "group/chip relative", children: [
3590
- /* @__PURE__ */ jsx(
3591
- "img",
3592
- {
3593
- src: imagePreview,
3594
- alt: "\u1EA2nh \u0111\u1EC1 b\xE0i",
3595
- className: "max-h-48 max-w-full h-auto w-auto rounded-lg border border-slate-200 shadow-sm"
3596
- }
3597
- ),
3598
- /* @__PURE__ */ jsx(
3599
- "button",
3600
- {
3601
- type: "button",
3602
- onClick: () => setImage(null),
3603
- disabled: ocrLoading || isLoading,
3604
- "aria-label": "Xo\xE1 \u1EA3nh",
3605
- 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",
3606
- children: "\xD7"
3607
- }
3608
- )
3609
- ] }) }),
3610
3389
  /* @__PURE__ */ jsx(
3611
3390
  "textarea",
3612
3391
  {
@@ -3624,79 +3403,122 @@ function AiFigurePrompt({
3624
3403
  },
3625
3404
  disabled: isLoading,
3626
3405
  rows: 2,
3627
- placeholder,
3406
+ placeholder: "M\xF4 t\u1EA3 \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng.",
3628
3407
  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"
3629
3408
  }
3630
3409
  ),
3631
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-2 pb-2 pt-1", children: [
3632
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
3633
- /* @__PURE__ */ jsx(
3634
- "button",
3635
- {
3636
- type: "button",
3637
- onClick: () => fileInputRef.current?.click(),
3638
- disabled: isLoading || ocrLoading,
3639
- "aria-label": "\u0110\xEDnh \u1EA3nh \u0111\u1EC1 b\xE0i",
3640
- title: "\u0110\xEDnh \u1EA3nh (c\u0169ng c\xF3 th\u1EC3 d\xE1n b\u1EB1ng Ctrl+V ho\u1EB7c k\xE9o th\u1EA3)",
3641
- 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",
3642
- children: /* @__PURE__ */ jsx(PaperclipIcon, { className: "h-[18px] w-[18px]" })
3643
- }
3644
- ),
3645
- /* @__PURE__ */ jsx(
3646
- "input",
3647
- {
3648
- ref: fileInputRef,
3649
- type: "file",
3650
- accept: "image/png,image/jpeg,image/webp",
3651
- className: "sr-only",
3652
- onChange: handleFileInput,
3653
- disabled: isLoading || ocrLoading,
3654
- "aria-label": "Ch\u1ECDn \u1EA3nh \u0111\u1EC1 b\xE0i"
3655
- }
3656
- )
3657
- ] }),
3658
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3659
- (isLoading || ocrLoading) && /* @__PURE__ */ 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` }),
3660
- isLoading ? /* @__PURE__ */ jsx(
3661
- "button",
3662
- {
3663
- type: "button",
3664
- onClick: cancel,
3665
- "aria-label": "Hu\u1EF7 d\u1EF1ng h\xECnh AI",
3666
- "data-testid": "geometry-ai-cancel",
3667
- title: `\u0110ang d\u1EF1ng\u2026 ${elapsed}s \u2014 b\u1EA5m \u0111\u1EC3 hu\u1EF7`,
3668
- 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",
3669
- children: /* @__PURE__ */ jsx(StopIcon, { className: "h-3.5 w-3.5" })
3670
- }
3671
- ) : /* @__PURE__ */ jsx(
3672
- "button",
3673
- {
3674
- type: "button",
3675
- onClick: () => void handleSendClick(),
3676
- disabled: sendDisabled,
3677
- "aria-label": willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh" : "D\u1EF1ng b\u1EB1ng AI",
3678
- 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)",
3679
- "data-testid": willOcr ? "geometry-ai-ocr" : "geometry-ai-submit",
3680
- 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",
3681
- children: /* @__PURE__ */ jsx(ArrowUpIcon, { className: "h-[18px] w-[18px]" })
3682
- }
3683
- )
3684
- ] })
3685
- ] }),
3686
- isDragOver && /* @__PURE__ */ 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" })
3410
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end gap-2 px-2 pb-2 pt-1", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3411
+ isLoading && /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px] tabular-nums text-slate-500", children: tokens > 0 ? `${tokens}tok \xB7 ${elapsed}s` : `${elapsed}s` }),
3412
+ isLoading ? /* @__PURE__ */ jsx(
3413
+ "button",
3414
+ {
3415
+ type: "button",
3416
+ onClick: cancel,
3417
+ "aria-label": "Hu\u1EF7 d\u1EF1ng h\xECnh AI",
3418
+ "data-testid": "geometry-ai-cancel",
3419
+ title: `\u0110ang d\u1EF1ng\u2026 ${elapsed}s \u2014 b\u1EA5m \u0111\u1EC3 hu\u1EF7`,
3420
+ 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",
3421
+ children: /* @__PURE__ */ jsx(StopIcon, { className: "h-3.5 w-3.5" })
3422
+ }
3423
+ ) : /* @__PURE__ */ jsx(
3424
+ "button",
3425
+ {
3426
+ type: "button",
3427
+ onClick: () => void handleSendClick(),
3428
+ disabled: sendDisabled,
3429
+ "aria-label": "D\u1EF1ng b\u1EB1ng AI",
3430
+ title: "D\u1EF1ng b\u1EB1ng AI (Ctrl/\u2318+Enter)",
3431
+ "data-testid": "geometry-ai-submit",
3432
+ 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",
3433
+ children: /* @__PURE__ */ jsx(ArrowUpIcon, { className: "h-[18px] w-[18px]" })
3434
+ }
3435
+ )
3436
+ ] }) })
3687
3437
  ]
3688
3438
  }
3689
3439
  ),
3690
- /* @__PURE__ */ jsxs("p", { className: "mt-1.5 px-1 text-[10px] text-slate-500", children: [
3691
- "D\xE1n \u1EA3nh (Ctrl+V), k\xE9o th\u1EA3, ho\u1EB7c b\u1EA5m ",
3692
- /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u{1F4CE}" }),
3693
- " \u0111\u1EC3 \u0111\xEDnh \u1EA3nh \u0111\u1EC1."
3694
- ] }),
3695
- error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error }),
3696
- ocrError && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: ocrError }),
3697
- ocrWarning && /* @__PURE__ */ jsx("p", { className: "mt-1 rounded bg-amber-50 px-2 py-1 text-[11px] text-amber-700", children: ocrWarning })
3440
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error })
3698
3441
  ] });
3699
3442
  }
3443
+
3444
+ // src/stamps/geometry-2d/draft.ts
3445
+ function svgIntrinsicSize(svg) {
3446
+ const w = svg.match(/<svg[^>]*\swidth="([\d.]+)"/);
3447
+ const h = svg.match(/<svg[^>]*\sheight="([\d.]+)"/);
3448
+ if (w && h) return { width: parseFloat(w[1]), height: parseFloat(h[1]) };
3449
+ const vb = svg.match(/viewBox="0 0 ([\d.]+) ([\d.]+)"/);
3450
+ if (vb) return { width: parseFloat(vb[1]), height: parseFloat(vb[2]) };
3451
+ return { width: 300, height: 200 };
3452
+ }
3453
+ function draftFromViewport(svg, appState, seq) {
3454
+ const { width, height } = svgIntrinsicSize(svg);
3455
+ const zoom = appState.zoom?.value ?? 1;
3456
+ const vw = appState.width ?? 800;
3457
+ const vh = appState.height ?? 600;
3458
+ const cx = appState.scrollX + vw / 2 / zoom;
3459
+ const cy = appState.scrollY + vh / 2 / zoom;
3460
+ return { svg, width, height, x: cx - width / 2, y: cy - height / 2, seq };
3461
+ }
3462
+ function didStateChange(seen, jsonState) {
3463
+ if (seen.last === jsonState) return false;
3464
+ seen.last = jsonState;
3465
+ return true;
3466
+ }
3467
+
3468
+ // src/stamps/geometry-2d/editor/useGeometryDraftEmit.ts
3469
+ function useGeometryDraftEmit({
3470
+ store,
3471
+ handleRef,
3472
+ api,
3473
+ showAxis,
3474
+ showGrid,
3475
+ onGeometryDraft,
3476
+ debounceMs = 350
3477
+ }) {
3478
+ const seqRef = useRef(0);
3479
+ const seenRef = useRef({ last: null });
3480
+ const timerRef = useRef(null);
3481
+ const cbRef = useRef(onGeometryDraft);
3482
+ cbRef.current = onGeometryDraft;
3483
+ useEffect(() => {
3484
+ if (!cbRef.current) return;
3485
+ const emit = () => {
3486
+ const h = handleRef.current;
3487
+ if (!h) return;
3488
+ const state = h.getState();
3489
+ if (Object.keys(state.objects).length === 0) {
3490
+ if (seenRef.current.last !== null) {
3491
+ seenRef.current.last = null;
3492
+ cbRef.current?.(null);
3493
+ }
3494
+ return;
3495
+ }
3496
+ const bbox = h.getBbox();
3497
+ const jsonState = serializeBoard(state, { bbox, showAxis, showGrid });
3498
+ if (!didStateChange(seenRef.current, jsonState)) return;
3499
+ void (async () => {
3500
+ try {
3501
+ const svg = await renderGeometrySvgFromState(jsonState);
3502
+ const appState = api?.getAppState?.() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
3503
+ seqRef.current += 1;
3504
+ cbRef.current?.(draftFromViewport(svg, appState, seqRef.current));
3505
+ } catch (err) {
3506
+ console.warn("[geometry] draft render failed:", err);
3507
+ }
3508
+ })();
3509
+ };
3510
+ const schedule = () => {
3511
+ if (timerRef.current) clearTimeout(timerRef.current);
3512
+ timerRef.current = setTimeout(emit, debounceMs);
3513
+ };
3514
+ const unsub = store.subscribe(schedule);
3515
+ return () => {
3516
+ unsub();
3517
+ if (timerRef.current) clearTimeout(timerRef.current);
3518
+ cbRef.current?.(null);
3519
+ };
3520
+ }, [store, handleRef, api, showAxis, showGrid, debounceMs]);
3521
+ }
3700
3522
  var GeometryEditorPanelInner = forwardRef(
3701
3523
  function GeometryEditorPanelInner2({
3702
3524
  store,
@@ -3715,7 +3537,9 @@ var GeometryEditorPanelInner = forwardRef(
3715
3537
  canUndo,
3716
3538
  canRedo,
3717
3539
  onSelectionChange,
3718
- generateGeometryFigure
3540
+ generateGeometryFigure,
3541
+ api,
3542
+ onGeometryDraft
3719
3543
  }, ref) {
3720
3544
  const { showToast } = useToast();
3721
3545
  const handleRef = useRef(null);
@@ -3728,12 +3552,12 @@ var GeometryEditorPanelInner = forwardRef(
3728
3552
  useEffect(() => {
3729
3553
  onSelectionChangeRef.current = onSelectionChange;
3730
3554
  }, [onSelectionChange]);
3555
+ const onGeometryDraftRef = useRef(onGeometryDraft);
3556
+ useEffect(() => {
3557
+ onGeometryDraftRef.current = onGeometryDraft;
3558
+ }, [onGeometryDraft]);
3731
3559
  useEditorState({ store, onHistoryChange });
3732
- const currentSceneState = useSyncExternalStore(
3733
- (cb) => store.subscribe(cb),
3734
- () => store.getState(),
3735
- () => store.getState()
3736
- );
3560
+ useGeometryDraftEmit({ store, handleRef, api, showAxis, showGrid, onGeometryDraft });
3737
3561
  useEffect(() => {
3738
3562
  const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
3739
3563
  sync();
@@ -3810,15 +3634,13 @@ var GeometryEditorPanelInner = forwardRef(
3810
3634
  try {
3811
3635
  const svgString = await renderGeometrySvgFromState(jsonState);
3812
3636
  onInsert(jsonState, svgString);
3637
+ onGeometryDraftRef.current?.(null);
3813
3638
  } catch (err) {
3814
3639
  console.error("Geometry insert failed:", err);
3815
3640
  }
3816
3641
  })();
3817
3642
  return true;
3818
3643
  }, [onInsert, showAxis, showGrid]);
3819
- const handleInsert = useCallback(() => {
3820
- performInsert();
3821
- }, [performInsert]);
3822
3644
  const loadAiFigure = useCallback((generated) => {
3823
3645
  handleRef.current?.clearSelection();
3824
3646
  setPropsPopover(null);
@@ -3913,7 +3735,7 @@ var GeometryEditorPanelInner = forwardRef(
3913
3735
  "button",
3914
3736
  {
3915
3737
  type: "button",
3916
- onClick: handleInsert,
3738
+ onClick: performInsert,
3917
3739
  disabled: !ready || !hasContent,
3918
3740
  title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
3919
3741
  "data-testid": "geometry-insert-btn-mobile",
@@ -3927,7 +3749,7 @@ var GeometryEditorPanelInner = forwardRef(
3927
3749
  /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
3928
3750
  ] }) })
3929
3751
  ] }),
3930
- generateGeometryFigure && /* @__PURE__ */ jsx(AiFigurePrompt, { generator: generateGeometryFigure, onGenerated: loadAiFigure, currentState: currentSceneState }),
3752
+ generateGeometryFigure && /* @__PURE__ */ jsx(AiFigurePrompt, { generator: generateGeometryFigure, onGenerated: loadAiFigure }),
3931
3753
  /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1", children: /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
3932
3754
  MiniBoard2D,
3933
3755
  {
@@ -4031,7 +3853,7 @@ var GeometryEditorPanelInner = forwardRef(
4031
3853
  /* @__PURE__ */ jsx(
4032
3854
  "button",
4033
3855
  {
4034
- onClick: handleInsert,
3856
+ onClick: performInsert,
4035
3857
  disabled: !ready || !hasContent,
4036
3858
  title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
4037
3859
  "data-testid": "geometry-insert-btn",
@@ -4087,7 +3909,7 @@ function parseInitialState(data) {
4087
3909
  return deserializeBoard(data.jsonState);
4088
3910
  }
4089
3911
  var GeometryStampHost = forwardRef(
4090
- function GeometryStampHost2({ api, editingElement, onClose, isDark, generateGeometryFigure }, ref) {
3912
+ function GeometryStampHost2({ api, editingElement, onClose, isDark, generateGeometryFigure, onGeometryDraft }, ref) {
4091
3913
  const panelRef = useRef(null);
4092
3914
  const { isMobile } = useIsMobile();
4093
3915
  const [drawerOpen, setDrawerOpen] = useState(false);
@@ -4202,7 +4024,9 @@ var GeometryStampHost = forwardRef(
4202
4024
  canUndo,
4203
4025
  canRedo,
4204
4026
  onSelectionChange: setSelectedObjectId,
4205
- generateGeometryFigure
4027
+ generateGeometryFigure,
4028
+ api,
4029
+ onGeometryDraft
4206
4030
  }
4207
4031
  )
4208
4032
  ] });
@@ -4210,5 +4034,5 @@ var GeometryStampHost = forwardRef(
4210
4034
  );
4211
4035
 
4212
4036
  export { GeometryStampHost };
4213
- //# sourceMappingURL=host-HAYCJJ2T.mjs.map
4214
- //# sourceMappingURL=host-HAYCJJ2T.mjs.map
4037
+ //# sourceMappingURL=host-HKMZSCIT.mjs.map
4038
+ //# sourceMappingURL=host-HKMZSCIT.mjs.map