@xom11/whiteboard 0.29.0 → 0.30.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.
package/dist/index.d.mts CHANGED
@@ -14,6 +14,7 @@ export { geometry3dStamp } from './geometry-3d.mjs';
14
14
  import { Graph2DCustomData } from './graph-2d.mjs';
15
15
  export { graph2dStamp } from './graph-2d.mjs';
16
16
  import { PDFDocumentProxy } from 'pdfjs-dist';
17
+ export { E as ExtractUiResult, H as HandleExtractProblemOptions, I as ImagePart, h as handleExtractProblem } from './handleExtractProblem-BrDY9ifM.mjs';
17
18
  import 'react';
18
19
 
19
20
  interface SyncableAppState {
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export { geometry3dStamp } from './geometry-3d.js';
14
14
  import { Graph2DCustomData } from './graph-2d.js';
15
15
  export { graph2dStamp } from './graph-2d.js';
16
16
  import { PDFDocumentProxy } from 'pdfjs-dist';
17
+ export { E as ExtractUiResult, H as HandleExtractProblemOptions, I as ImagePart, h as handleExtractProblem } from './handleExtractProblem-BrDY9ifM.js';
17
18
  import 'react';
18
19
 
19
20
  interface SyncableAppState {
package/dist/index.js CHANGED
@@ -9331,6 +9331,200 @@ var init_useAiFigure = __esm({
9331
9331
  "use client";
9332
9332
  }
9333
9333
  });
9334
+
9335
+ // src/stamps/geometry-2d/ai/vision/tesseract.ts
9336
+ async function runTesseractOcr(image, opts = {}) {
9337
+ if (opts.signal?.aborted) {
9338
+ const err = new Error("Tesseract OCR aborted");
9339
+ err.name = "AbortError";
9340
+ throw err;
9341
+ }
9342
+ const { createWorker } = await import('tesseract.js');
9343
+ const lang = opts.lang ?? DEFAULT_LANG;
9344
+ const worker = await createWorker(lang);
9345
+ try {
9346
+ const dataUrl = `data:${image.mediaType};base64,${image.base64}`;
9347
+ const { data } = await worker.recognize(dataUrl);
9348
+ return { text: data.text, confidence: data.confidence };
9349
+ } finally {
9350
+ await worker.terminate();
9351
+ }
9352
+ }
9353
+ var DEFAULT_LANG;
9354
+ var init_tesseract = __esm({
9355
+ "src/stamps/geometry-2d/ai/vision/tesseract.ts"() {
9356
+ DEFAULT_LANG = "vie+eng";
9357
+ }
9358
+ });
9359
+
9360
+ // src/stamps/geometry-2d/ai/vision/extractProblem.ts
9361
+ async function extractProblemFromImage(image, opts = {}) {
9362
+ let raw;
9363
+ try {
9364
+ raw = await runTesseractOcr(image, {
9365
+ ...opts.tesseractLang ? { lang: opts.tesseractLang } : {},
9366
+ ...opts.signal ? { signal: opts.signal } : {}
9367
+ });
9368
+ } catch (e) {
9369
+ const err = e;
9370
+ return {
9371
+ ok: false,
9372
+ reason: "unreadable",
9373
+ message: "Tesseract OCR fail: " + (err.message ?? "?")
9374
+ };
9375
+ }
9376
+ const text = postProcess(raw.text);
9377
+ if (text.length === 0) {
9378
+ return { ok: false, reason: "empty", message: "Tesseract kh\xF4ng tr\xEDch \u0111\u01B0\u1EE3c text." };
9379
+ }
9380
+ const tooShort = text.length < MIN_HIGH_CONFIDENCE_CHARS;
9381
+ const lowConf = raw.confidence < TESSERACT_HIGH_CONFIDENCE_THRESHOLD;
9382
+ const confidence = tooShort || lowConf ? "low" : "high";
9383
+ return {
9384
+ ok: true,
9385
+ text,
9386
+ confidence,
9387
+ usage: { inputTokens: 0, outputTokens: 0 }
9388
+ };
9389
+ }
9390
+ function postProcess(raw) {
9391
+ let t = raw.trim();
9392
+ t = t.replace(/\*\*(.+?)\*\*/g, "$1");
9393
+ t = t.replace(/\*(.+?)\*/g, "$1");
9394
+ t = t.replace(/_(.+?)_/g, "$1");
9395
+ t = t.replace(/```[\s\S]*?```/g, "").replace(/`([^`]+)`/g, "$1");
9396
+ t = t.replace(/\s+/g, " ").trim();
9397
+ t = t.normalize("NFC");
9398
+ if (t.length > MAX_TEXT_CHARS) t = t.slice(0, MAX_TEXT_CHARS);
9399
+ return t;
9400
+ }
9401
+ var MIN_HIGH_CONFIDENCE_CHARS, MAX_TEXT_CHARS, TESSERACT_HIGH_CONFIDENCE_THRESHOLD;
9402
+ var init_extractProblem = __esm({
9403
+ "src/stamps/geometry-2d/ai/vision/extractProblem.ts"() {
9404
+ init_tesseract();
9405
+ MIN_HIGH_CONFIDENCE_CHARS = 10;
9406
+ MAX_TEXT_CHARS = 2e3;
9407
+ TESSERACT_HIGH_CONFIDENCE_THRESHOLD = 70;
9408
+ }
9409
+ });
9410
+
9411
+ // src/stamps/geometry-2d/ai/handleExtractProblem.ts
9412
+ async function handleExtractProblem(image, opts = {}) {
9413
+ try {
9414
+ const r = await extractProblemFromImage(image, opts);
9415
+ if (r.ok) {
9416
+ if (r.confidence === "low") {
9417
+ return {
9418
+ kind: "low-confidence",
9419
+ text: r.text,
9420
+ warning: "OCR c\xF3 th\u1EC3 kh\xF4ng ch\xEDnh x\xE1c, ki\u1EC3m tra tr\u01B0\u1EDBc khi v\u1EBD.",
9421
+ usage: r.usage
9422
+ };
9423
+ }
9424
+ return { kind: "success", text: r.text, usage: r.usage };
9425
+ }
9426
+ if (r.reason === "not-math") {
9427
+ return { kind: "refused", reason: "not-math", message: r.message };
9428
+ }
9429
+ if (r.reason === "unsupported") {
9430
+ return { kind: "error", code: "unsupported", message: r.message };
9431
+ }
9432
+ if (r.reason === "unreadable") {
9433
+ return { kind: "error", code: "network", message: r.message };
9434
+ }
9435
+ return { kind: "error", code: "empty", message: r.message };
9436
+ } catch (e) {
9437
+ return {
9438
+ kind: "error",
9439
+ code: "unexpected",
9440
+ message: e instanceof Error ? e.message : String(e)
9441
+ };
9442
+ }
9443
+ }
9444
+ var init_handleExtractProblem = __esm({
9445
+ "src/stamps/geometry-2d/ai/handleExtractProblem.ts"() {
9446
+ init_extractProblem();
9447
+ }
9448
+ });
9449
+
9450
+ // src/stamps/geometry-2d/ai/vision/preprocess.ts
9451
+ function inferMediaType(file) {
9452
+ const t = file.type.toLowerCase();
9453
+ if (ALLOWED_TYPES.includes(t)) return t;
9454
+ return null;
9455
+ }
9456
+ function validateFile(file) {
9457
+ const mt = inferMediaType(file);
9458
+ if (mt == null) {
9459
+ return {
9460
+ ok: false,
9461
+ code: "invalid-format",
9462
+ message: "Ch\u1EC9 h\u1ED7 tr\u1EE3 PNG, JPEG, WEBP."
9463
+ };
9464
+ }
9465
+ if (file.size > MAX_RAW_BYTES) {
9466
+ return {
9467
+ ok: false,
9468
+ code: "too-large",
9469
+ message: `\u1EA2nh qu\xE1 l\u1EDBn (> ${Math.round(MAX_RAW_BYTES / 1024 / 1024)} MB). Crop ho\u1EB7c resize tr\u01B0\u1EDBc.`
9470
+ };
9471
+ }
9472
+ return { ok: true, mediaType: mt };
9473
+ }
9474
+ async function fileToImagePart(file) {
9475
+ const v = validateFile(file);
9476
+ if (!v.ok) throw new Error(v.message);
9477
+ const bitmap = await createImageBitmap(file);
9478
+ const { width, height } = bitmap;
9479
+ const maxEdge = Math.max(width, height);
9480
+ const scale3 = maxEdge > MAX_EDGE_PX ? MAX_EDGE_PX / maxEdge : 1;
9481
+ const targetW = Math.round(width * scale3);
9482
+ const targetH = Math.round(height * scale3);
9483
+ const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(targetW, targetH) : Object.assign(document.createElement("canvas"), { width: targetW, height: targetH });
9484
+ const ctx = canvas.getContext("2d");
9485
+ if (!ctx) throw new Error("Kh\xF4ng t\u1EA1o \u0111\u01B0\u1EE3c canvas 2D context");
9486
+ ctx.drawImage(bitmap, 0, 0, targetW, targetH);
9487
+ bitmap.close();
9488
+ let outputType = v.mediaType === "image/png" ? "image/png" : "image/jpeg";
9489
+ let finalBlob = await canvasToBlob(canvas, outputType, 0.92);
9490
+ if (finalBlob.size > MAX_ENCODED_BYTES) {
9491
+ outputType = "image/jpeg";
9492
+ finalBlob = await canvasToBlob(canvas, "image/jpeg", 0.7);
9493
+ }
9494
+ const base64 = await blobToBase64(finalBlob);
9495
+ return { mediaType: outputType, base64 };
9496
+ }
9497
+ async function canvasToBlob(canvas, type, quality) {
9498
+ if ("convertToBlob" in canvas) {
9499
+ return canvas.convertToBlob({ type, quality });
9500
+ }
9501
+ return new Promise((resolve, reject) => {
9502
+ canvas.toBlob(
9503
+ (b) => b ? resolve(b) : reject(new Error("Canvas encode fail")),
9504
+ type,
9505
+ quality
9506
+ );
9507
+ });
9508
+ }
9509
+ async function blobToBase64(blob) {
9510
+ const buf = await blob.arrayBuffer();
9511
+ let binary = "";
9512
+ const bytes = new Uint8Array(buf);
9513
+ const chunk = 32768;
9514
+ for (let i = 0; i < bytes.length; i += chunk) {
9515
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
9516
+ }
9517
+ return typeof btoa === "function" ? btoa(binary) : Buffer.from(binary, "binary").toString("base64");
9518
+ }
9519
+ var MAX_EDGE_PX, MAX_RAW_BYTES, MAX_ENCODED_BYTES, ALLOWED_TYPES;
9520
+ var init_preprocess = __esm({
9521
+ "src/stamps/geometry-2d/ai/vision/preprocess.ts"() {
9522
+ MAX_EDGE_PX = 2048;
9523
+ MAX_RAW_BYTES = 10 * 1024 * 1024;
9524
+ MAX_ENCODED_BYTES = 3 * 1024 * 1024;
9525
+ ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp"];
9526
+ }
9527
+ });
9334
9528
  function AiFigurePrompt({ generator, onGenerated }) {
9335
9529
  const {
9336
9530
  prompt,
@@ -9350,20 +9544,132 @@ function AiFigurePrompt({ generator, onGenerated }) {
9350
9544
  const id = setInterval(() => setElapsed((s) => s + 1), 1e3);
9351
9545
  return () => clearInterval(id);
9352
9546
  }, [isLoading]);
9547
+ const [image, setImage] = React19.useState(null);
9548
+ const [ocrLoading, setOcrLoading] = React19.useState(false);
9549
+ const [ocrError, setOcrError] = React19.useState(null);
9550
+ const [ocrWarning, setOcrWarning] = React19.useState(null);
9551
+ const [isDragOver, setIsDragOver] = React19.useState(false);
9552
+ const fileInputRef = React19.useRef(null);
9353
9553
  const textareaRef = React19.useRef(null);
9554
+ const imagePreview = image ? `data:${image.mediaType};base64,${image.base64}` : null;
9555
+ React19.useEffect(() => {
9556
+ setOcrError(null);
9557
+ setOcrWarning(null);
9558
+ }, [image]);
9559
+ const handleFile = React19.useCallback(
9560
+ async (file) => {
9561
+ if (isLoading || ocrLoading) return;
9562
+ const v = validateFile(file);
9563
+ if (!v.ok) {
9564
+ setOcrError(v.message);
9565
+ return;
9566
+ }
9567
+ try {
9568
+ const part = await fileToImagePart(file);
9569
+ setImage(part);
9570
+ } catch (e) {
9571
+ setOcrError(e instanceof Error ? e.message : "Kh\xF4ng decode \u0111\u01B0\u1EE3c \u1EA3nh");
9572
+ }
9573
+ },
9574
+ [isLoading, ocrLoading]
9575
+ );
9576
+ const handleFileInput = React19.useCallback(
9577
+ (e) => {
9578
+ const file = e.target.files?.[0];
9579
+ if (file) void handleFile(file);
9580
+ e.target.value = "";
9581
+ },
9582
+ [handleFile]
9583
+ );
9584
+ const handlePaste = React19.useCallback(
9585
+ (e) => {
9586
+ const item = Array.from(e.clipboardData.items).find(
9587
+ (it) => it.kind === "file" && it.type.startsWith("image/")
9588
+ );
9589
+ if (!item) return;
9590
+ const file = item.getAsFile();
9591
+ if (!file) return;
9592
+ e.preventDefault();
9593
+ void handleFile(file);
9594
+ },
9595
+ [handleFile]
9596
+ );
9597
+ const handleDrop = React19.useCallback(
9598
+ (e) => {
9599
+ e.preventDefault();
9600
+ setIsDragOver(false);
9601
+ const file = Array.from(e.dataTransfer.files).find(
9602
+ (f) => f.type.startsWith("image/")
9603
+ );
9604
+ if (file) void handleFile(file);
9605
+ },
9606
+ [handleFile]
9607
+ );
9608
+ const runOcr = React19.useCallback(async () => {
9609
+ if (!image) return;
9610
+ setOcrLoading(true);
9611
+ setOcrError(null);
9612
+ setOcrWarning(null);
9613
+ try {
9614
+ const r = await handleExtractProblem(image);
9615
+ if (r.kind === "success" || r.kind === "low-confidence") {
9616
+ setPrompt(r.text);
9617
+ if (r.kind === "low-confidence") setOcrWarning(r.warning);
9618
+ requestAnimationFrame(() => textareaRef.current?.focus());
9619
+ } else {
9620
+ setOcrError(r.message);
9621
+ }
9622
+ } finally {
9623
+ setOcrLoading(false);
9624
+ }
9625
+ }, [image, setPrompt]);
9354
9626
  const handleSendClick = React19.useCallback(async () => {
9627
+ if (image && !prompt.trim() && !ocrLoading) {
9628
+ await runOcr();
9629
+ return;
9630
+ }
9355
9631
  const generated = await submit();
9356
9632
  if (generated) onGenerated(generated);
9357
- }, [submit, onGenerated]);
9633
+ }, [image, prompt, ocrLoading, runOcr, submit, onGenerated]);
9358
9634
  const promptEmpty = !prompt.trim();
9359
- const sendDisabled = promptEmpty || isLoading;
9635
+ const willOcr = image != null && promptEmpty;
9636
+ const sendDisabled = !image && promptEmpty || ocrLoading || isLoading && !willOcr;
9637
+ 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" : "M\xF4 t\u1EA3 \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng \u2014 ho\u1EB7c d\xE1n/\u0111\xEDnh \u1EA3nh \u0111\u1EC1 (Ctrl+V).";
9360
9638
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-slate-200 bg-slate-50 px-3 py-3", children: [
9361
9639
  /* @__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
9640
  /* @__PURE__ */ jsxRuntime.jsxs(
9363
9641
  "div",
9364
9642
  {
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",
9643
+ onDragOver: (e) => {
9644
+ e.preventDefault();
9645
+ setIsDragOver(true);
9646
+ },
9647
+ onDragLeave: () => setIsDragOver(false),
9648
+ onDrop: handleDrop,
9649
+ onPaste: handlePaste,
9650
+ 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" : ""),
9366
9651
  children: [
9652
+ 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: [
9653
+ /* @__PURE__ */ jsxRuntime.jsx(
9654
+ "img",
9655
+ {
9656
+ src: imagePreview,
9657
+ alt: "\u1EA2nh \u0111\u1EC1 b\xE0i",
9658
+ className: "max-h-48 max-w-full h-auto w-auto rounded-lg border border-slate-200 shadow-sm"
9659
+ }
9660
+ ),
9661
+ /* @__PURE__ */ jsxRuntime.jsx(
9662
+ "button",
9663
+ {
9664
+ type: "button",
9665
+ onClick: () => setImage(null),
9666
+ disabled: ocrLoading || isLoading,
9667
+ "aria-label": "Xo\xE1 \u1EA3nh",
9668
+ 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",
9669
+ children: "\xD7"
9670
+ }
9671
+ )
9672
+ ] }) }),
9367
9673
  /* @__PURE__ */ jsxRuntime.jsx(
9368
9674
  "textarea",
9369
9675
  {
@@ -9381,48 +9687,88 @@ function AiFigurePrompt({ generator, onGenerated }) {
9381
9687
  },
9382
9688
  disabled: isLoading,
9383
9689
  rows: 2,
9384
- placeholder: "M\xF4 t\u1EA3 \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng.",
9690
+ placeholder,
9385
9691
  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
9692
  }
9387
9693
  ),
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
- ] }) })
9694
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2 px-2 pb-2 pt-1", children: [
9695
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
9696
+ /* @__PURE__ */ jsxRuntime.jsx(
9697
+ "button",
9698
+ {
9699
+ type: "button",
9700
+ onClick: () => fileInputRef.current?.click(),
9701
+ disabled: isLoading || ocrLoading,
9702
+ "aria-label": "\u0110\xEDnh \u1EA3nh \u0111\u1EC1 b\xE0i",
9703
+ title: "\u0110\xEDnh \u1EA3nh (c\u0169ng c\xF3 th\u1EC3 d\xE1n b\u1EB1ng Ctrl+V ho\u1EB7c k\xE9o th\u1EA3)",
9704
+ 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",
9705
+ children: /* @__PURE__ */ jsxRuntime.jsx(PaperclipIcon, { className: "h-[18px] w-[18px]" })
9706
+ }
9707
+ ),
9708
+ /* @__PURE__ */ jsxRuntime.jsx(
9709
+ "input",
9710
+ {
9711
+ ref: fileInputRef,
9712
+ type: "file",
9713
+ accept: "image/png,image/jpeg,image/webp",
9714
+ className: "sr-only",
9715
+ onChange: handleFileInput,
9716
+ disabled: isLoading || ocrLoading,
9717
+ "aria-label": "Ch\u1ECDn \u1EA3nh \u0111\u1EC1 b\xE0i"
9718
+ }
9719
+ )
9720
+ ] }),
9721
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
9722
+ (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` }),
9723
+ isLoading ? /* @__PURE__ */ jsxRuntime.jsx(
9724
+ "button",
9725
+ {
9726
+ type: "button",
9727
+ onClick: cancel,
9728
+ "aria-label": "Hu\u1EF7 d\u1EF1ng h\xECnh AI",
9729
+ "data-testid": "geometry-ai-cancel",
9730
+ title: `\u0110ang d\u1EF1ng\u2026 ${elapsed}s \u2014 b\u1EA5m \u0111\u1EC3 hu\u1EF7`,
9731
+ 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",
9732
+ children: /* @__PURE__ */ jsxRuntime.jsx(StopIcon, { className: "h-3.5 w-3.5" })
9733
+ }
9734
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
9735
+ "button",
9736
+ {
9737
+ type: "button",
9738
+ onClick: () => void handleSendClick(),
9739
+ disabled: sendDisabled,
9740
+ "aria-label": willOcr ? "\u0110\u1ECDc \u0111\u1EC1 t\u1EEB \u1EA3nh" : "D\u1EF1ng b\u1EB1ng AI",
9741
+ 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)",
9742
+ "data-testid": willOcr ? "geometry-ai-ocr" : "geometry-ai-submit",
9743
+ 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",
9744
+ children: /* @__PURE__ */ jsxRuntime.jsx(ArrowUpIcon, { className: "h-[18px] w-[18px]" })
9745
+ }
9746
+ )
9747
+ ] })
9748
+ ] }),
9749
+ 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" })
9415
9750
  ]
9416
9751
  }
9417
9752
  ),
9753
+ ocrWarning && /* @__PURE__ */ jsxRuntime.jsx(
9754
+ "p",
9755
+ {
9756
+ className: "mt-1 px-1 text-xs text-amber-700",
9757
+ "data-testid": "geometry-ai-ocr-warning",
9758
+ children: ocrWarning
9759
+ }
9760
+ ),
9761
+ ocrError && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: ocrError }),
9418
9762
  error && /* @__PURE__ */ jsxRuntime.jsx("p", { role: "alert", className: "mt-1 px-1 text-xs text-red-600", children: error })
9419
9763
  ] });
9420
9764
  }
9421
- var ArrowUpIcon, StopIcon;
9765
+ var ArrowUpIcon, StopIcon, PaperclipIcon;
9422
9766
  var init_AiFigurePrompt = __esm({
9423
9767
  "src/stamps/geometry-2d/editor/AiFigurePrompt.tsx"() {
9424
9768
  "use client";
9425
9769
  init_useAiFigure();
9770
+ init_handleExtractProblem();
9771
+ init_preprocess();
9426
9772
  ArrowUpIcon = (props) => /* @__PURE__ */ jsxRuntime.jsxs(
9427
9773
  "svg",
9428
9774
  {
@@ -9441,6 +9787,20 @@ var init_AiFigurePrompt = __esm({
9441
9787
  }
9442
9788
  );
9443
9789
  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" }) });
9790
+ PaperclipIcon = (props) => /* @__PURE__ */ jsxRuntime.jsx(
9791
+ "svg",
9792
+ {
9793
+ viewBox: "0 0 24 24",
9794
+ fill: "none",
9795
+ stroke: "currentColor",
9796
+ strokeWidth: 1.75,
9797
+ strokeLinecap: "round",
9798
+ strokeLinejoin: "round",
9799
+ "aria-hidden": true,
9800
+ ...props,
9801
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" })
9802
+ }
9803
+ );
9444
9804
  }
9445
9805
  });
9446
9806
 
@@ -17944,6 +18304,9 @@ function Whiteboard({
17944
18304
  ] });
17945
18305
  }
17946
18306
 
18307
+ // src/index.ts
18308
+ init_handleExtractProblem();
18309
+
17947
18310
  exports.ALL_STAMPS = ALL_STAMPS;
17948
18311
  exports.DEFAULT_STAMPS = DEFAULT_STAMPS;
17949
18312
  exports.EXPERIMENTAL_STAMPS = EXPERIMENTAL_STAMPS;
@@ -17957,6 +18320,7 @@ exports.findStampForCustomData = findStampForCustomData;
17957
18320
  exports.geometry3dStamp = geometry3dStamp;
17958
18321
  exports.geometryStamp = geometryStamp;
17959
18322
  exports.graph2dStamp = graph2dStamp;
18323
+ exports.handleExtractProblem = handleExtractProblem;
17960
18324
  exports.insertPdfPages = insertPdfPages;
17961
18325
  exports.insertRasterizedPagesIntoScene = insertRasterizedPagesIntoScene;
17962
18326
  exports.isStampElement = isStampElement;