@fayz-ai/plugin-menu 0.10.8 → 0.10.10

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.
@@ -1,10 +1,10 @@
1
- import { normalizeExtraction, normalizeName } from './chunk-C6W53TML.js';
2
- import { createPluginContext, PageActions, useAgentSurface, askAI, RightRailPage, formatCurrency, requestChatUpload } from '@fayz-ai/admin';
3
- import { useState, useMemo, useEffect } from 'react';
1
+ import { normalizeName } from './chunk-FVDAVCK4.js';
2
+ import { createPluginContext, PageActions, useAgentSurface, askAI, RightRailPage, formatCurrency, requestChatUpload, useChatStore } from '@fayz-ai/admin';
3
+ import { useState, useEffect, useMemo } from 'react';
4
4
  import { Plus, Sparkles, Search, UtensilsCrossed, MoreVertical, Pencil, Copy, Power, Eye, Trash2, GripVertical, ChevronRight, ChevronDown, Loader2, MoreHorizontal, X, ImageOff, Star, XCircle } from 'lucide-react';
5
5
  import { useSensors, useSensor, PointerSensor, DndContext, closestCenter, DragOverlay } from '@dnd-kit/core';
6
6
  import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
7
- import { useTranslation, getSupabaseClientOptional } from '@fayz-ai/core';
7
+ import { useTranslation } from '@fayz-ai/core';
8
8
  import { PageHeaderActions, PageTabs, Button, toast, ConfirmDialog, Dropdown, DropdownTrigger, Hint, DropdownContent, DropdownItem, DropdownSeparator, DraftCommitBar, SheetBody, Input, Switch, SheetFooter, ImageDrop, CurrencyInput, SegmentedControl, Modal, ModalContent, ModalHeader, ModalTitle, ModalBody, TimePicker } from '@fayz-ai/ui';
9
9
  import { CSS } from '@dnd-kit/utilities';
10
10
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -62,79 +62,20 @@ function describeAvailability(availability, locale, alwaysLabel) {
62
62
  if (rule.from || rule.to) parts.push(`${rule.from ?? "\u2026"} \u2192 ${rule.to ?? "\u2026"}`);
63
63
  return parts.length > 0 ? parts.join(" \xB7 ") : alwaysLabel;
64
64
  }
65
- function resolveMenuExtractor(options) {
66
- const client = getSupabaseClientOptional();
67
- if (!client?.functions?.invoke) return null;
68
- const functionName = "menu-extract";
69
- return async (images, hint) => {
70
- const { data, error } = await client.functions.invoke(functionName, {
71
- body: { images, hint }
72
- });
73
- if (error) {
74
- const detail = await safeMessage(error) ?? error.message;
75
- throw new Error(detail || "menu extraction failed");
76
- }
77
- return normalizeExtraction(data);
78
- };
79
- }
80
- async function safeMessage(error) {
81
- try {
82
- const body = await error?.context?.json?.();
83
- return typeof body?.error === "string" ? body.error : null;
84
- } catch {
85
- return null;
86
- }
87
- }
88
-
89
- // src/lib/images.ts
90
- async function downscaleImage(file, options) {
91
- const maxDimension = 1600;
92
- const quality = 0.82;
93
- const bitmap = await loadImage(file);
94
- const scale = Math.min(1, maxDimension / Math.max(bitmap.width, bitmap.height));
95
- const width = Math.round(bitmap.width * scale);
96
- const height = Math.round(bitmap.height * scale);
97
- const canvas = document.createElement("canvas");
98
- canvas.width = width;
99
- canvas.height = height;
100
- const context = canvas.getContext("2d");
101
- if (!context) return fileToDataUrl(file);
102
- context.drawImage(bitmap, 0, 0, width, height);
103
- return canvas.toDataURL("image/jpeg", quality);
104
- }
105
- async function loadImage(file) {
106
- if (typeof createImageBitmap === "function") {
107
- try {
108
- return await createImageBitmap(file);
109
- } catch {
110
- }
111
- }
112
- const url = await fileToDataUrl(file);
113
- return new Promise((resolve, reject) => {
114
- const image = new Image();
115
- image.onload = () => resolve(image);
116
- image.onerror = () => reject(new Error("Could not read the image"));
117
- image.src = url;
118
- });
119
- }
120
- function fileToDataUrl(file) {
121
- return new Promise((resolve, reject) => {
122
- const reader = new FileReader();
123
- reader.onload = () => resolve(String(reader.result));
124
- reader.onerror = () => reject(new Error("Could not read the file"));
125
- reader.readAsDataURL(file);
126
- });
127
- }
128
65
  var INSTRUCTIONS = `Voc\xEA est\xE1 com a tela de Card\xE1pio aberta e \xE9 VOC\xCA quem conduz o cadastro.
129
66
  - Trabalhe sempre no card\xE1pio ativo mostrado no estado; se o usu\xE1rio pedir outro, troque ou crie.
130
67
  - Se\xE7\xF5es e itens s\xE3o identificados por NOME, do jeito que o usu\xE1rio fala. Pre\xE7o \xE9 n\xFAmero (29.9), sem moeda.
131
68
 
132
- Cadastro por FOTO (o caminho principal para um card\xE1pio novo):
133
- 1. Diga em uma linha o que vai fazer e chame requestMenuPhotos. Isso p\xF5e um bot\xE3o grande aqui na conversa ("Subir foto do card\xE1pio") que abre a c\xE2mera ou os arquivos \u2014 n\xE3o pe\xE7a para o usu\xE1rio usar o clipe, e n\xE3o mande ele fazer nada na tela.
134
- 2. Espere. O envio dispara a leitura sozinho.
135
- 3. Quando \`draft\` aparecer no estado, LEIA em voz alta o que veio: quantas se\xE7\xF5es, quantos itens, e quais ficaram com pre\xE7o 0.
136
- 4. Resolva as pend\xEAncias conversando \u2014 pergunte o pre\xE7o que faltou e chame updateMenuDraftItem. O usu\xE1rio tamb\xE9m pode editar direto na tela; o estado sempre reflete o que est\xE1 l\xE1.
137
- 5. S\xF3 chame commitMenuDraft depois que o usu\xE1rio confirmar. Se ele desistir, discardMenuDraft.
69
+ Cadastro em MASSA (foto, PDF ou texto de card\xE1pio):
70
+ - Se a foto/texto do card\xE1pio J\xC1 EST\xC1 nesta conversa: leia voc\xEA mesmo e chame stageMenuImport UMA vez com todas as se\xE7\xF5es e itens. NUNCA cadastre se\xE7\xE3o por se\xE7\xE3o ou item por item com createMenuSection/createMenuItem \u2014 isso estoura o limite de passos e duplica se\xE7\xF5es. A ferramenta j\xE1 reaproveita se\xE7\xF5es existentes e pula itens repetidos.
71
+ - Se o usu\xE1rio ainda n\xE3o mandou a foto: diga em uma linha o que vai fazer e chame requestMenuPhotos. Isso p\xF5e um bot\xE3o grande aqui na conversa ("Subir foto do card\xE1pio") que abre a c\xE2mera ou os arquivos \u2014 n\xE3o pe\xE7a para o usu\xE1rio usar o clipe, e n\xE3o mande ele fazer nada na tela. A foto enviada chega AQUI na conversa como anexo da pr\xF3xima mensagem: leia e chame stageMenuImport.
72
+ - Este fluxo \xE9 todo conversado: NUNCA use startFeatureSetup ou highlightFeature para o cadastro de card\xE1pio \u2014 n\xE3o existe tour para isso, o bot\xE3o de foto \xE9 a ferramenta.
73
+ Depois, nos dois caminhos:
74
+ 1. Quando \`draft\` aparecer no estado, LEIA em voz alta o que veio: quantas se\xE7\xF5es, quantos itens, e quais ficaram com pre\xE7o 0.
75
+ 2. Resolva as pend\xEAncias conversando \u2014 pergunte o pre\xE7o que faltou e chame updateMenuDraftItem. O usu\xE1rio tamb\xE9m pode editar direto na tela; o estado sempre reflete o que est\xE1 l\xE1.
76
+ 3. S\xF3 chame commitMenuDraft depois que o usu\xE1rio confirmar. Se ele desistir, discardMenuDraft.
77
+
78
+ createMenuSection e createMenuItem s\xE3o para ajustes pontuais (uma se\xE7\xE3o nova, um prato novo) \u2014 nunca para importar um card\xE1pio inteiro.
138
79
 
139
80
  NUNCA escreva um bot\xE3o em texto (nada de "[Subir foto do card\xE1pio]" ou "clique no bot\xE3o abaixo") \u2014 o bot\xE3o s\xF3 existe se voc\xEA chamar a ferramenta. Se ela n\xE3o estiver dispon\xEDvel, diga isso com todas as letras e ofere\xE7a cadastrar item a item.
140
81
 
@@ -188,6 +129,8 @@ function useMenuAgentSurface(deps) {
188
129
  ["name"]
189
130
  ),
190
131
  execute: async ({ name }) => {
132
+ const existing = deps.sections.find((s) => normalizeName(s.name) === normalizeName(String(name)));
133
+ if (existing) return { ok: true, section: existing.name, alreadyExisted: true };
191
134
  const created = await deps.actions.createSection(String(name));
192
135
  return created ? { ok: true, section: created.name } : { error: "section_not_created" };
193
136
  }
@@ -315,21 +258,67 @@ function useMenuAgentSurface(deps) {
315
258
  return { ok: true, menu: created.name };
316
259
  }
317
260
  },
261
+ {
262
+ tool: tool(
263
+ "menu.surface.stage-import",
264
+ "stageMenuImport",
265
+ 'IMPORTA O CARD\xC1PIO ("importa/cadastra esse card\xE1pio", foto de card\xE1pio na conversa): voc\xEA l\xEA a foto ou o texto e passa o card\xE1pio INTEIRO (todas as se\xE7\xF5es com seus itens) numa chamada s\xF3 \u2014 vira um rascunho na tela para o usu\xE1rio conferir. Se\xE7\xF5es com nome de uma se\xE7\xE3o existente recebem os itens nela (sem duplicar), e itens que j\xE1 existem s\xE3o pulados. Nada \xE9 gravado at\xE9 commitMenuDraft. Nunca use tour/setup para importar card\xE1pio.',
266
+ {
267
+ sections: {
268
+ type: "array",
269
+ description: "Todas as se\xE7\xF5es do card\xE1pio lido",
270
+ items: {
271
+ type: "object",
272
+ properties: {
273
+ name: { type: "string", description: "Nome da se\xE7\xE3o (ex.: Pizzas, Bebidas)" },
274
+ items: {
275
+ type: "array",
276
+ items: {
277
+ type: "object",
278
+ properties: {
279
+ name: { type: "string" },
280
+ price: { type: "number", description: "N\xFAmero, sem moeda. 0 se a foto n\xE3o mostra o pre\xE7o." },
281
+ description: { type: "string" }
282
+ },
283
+ required: ["name", "price"]
284
+ }
285
+ }
286
+ },
287
+ required: ["name", "items"]
288
+ }
289
+ }
290
+ },
291
+ ["sections"]
292
+ ),
293
+ execute: async ({ sections }) => {
294
+ if (!Array.isArray(sections) || sections.length === 0) return { error: "empty_import" };
295
+ const staged = deps.actions.stageDraft(sections);
296
+ if (staged.sections === 0) {
297
+ return {
298
+ ok: true,
299
+ staged,
300
+ note: "Nada novo para cadastrar \u2014 tudo que veio j\xE1 existe no card\xE1pio."
301
+ };
302
+ }
303
+ return {
304
+ ok: true,
305
+ staged,
306
+ next: "O rascunho est\xE1 na tela. Leia para o usu\xE1rio o que veio (se\xE7\xF5es, itens, pre\xE7os zerados), resolva pend\xEAncias e s\xF3 chame commitMenuDraft depois que ele confirmar."
307
+ };
308
+ }
309
+ },
318
310
  {
319
311
  tool: tool(
320
312
  "menu.surface.request-photos",
321
313
  "requestMenuPhotos",
322
- "Pede as fotos do card\xE1pio ao usu\xE1rio: aparece um bot\xE3o grande aqui na conversa para tirar a foto ou escolher o arquivo. O que for lido vira um rascunho no estado, que voc\xEA confere com ele antes de gravar.",
314
+ "Pede as fotos do card\xE1pio ao usu\xE1rio: aparece um bot\xE3o grande aqui na conversa para tirar a foto ou escolher o arquivo. A foto enviada chega como anexo da pr\xF3xima mensagem do usu\xE1rio \u2014 voc\xEA l\xEA e chama stageMenuImport.",
323
315
  {},
324
316
  [],
325
317
  "read"
326
318
  ),
327
319
  execute: async () => {
328
- if (!deps.hasExtractor) {
329
- return { error: "extractor_unavailable", message: "Este espa\xE7o ainda n\xE3o tem a leitura por foto configurada. O cadastro pode ser feito item a item." };
330
- }
331
320
  deps.actions.requestPhotos();
332
- return { ok: true, asked: true, next: "O bot\xE3o j\xE1 est\xE1 na conversa. Espere o usu\xE1rio enviar \u2014 o rascunho aparece no estado quando a leitura terminar." };
321
+ return { ok: true, asked: true, next: "O bot\xE3o j\xE1 est\xE1 na conversa. A foto chega como anexo da pr\xF3xima mensagem: leia e chame stageMenuImport com o card\xE1pio inteiro." };
333
322
  }
334
323
  },
335
324
  {
@@ -1235,7 +1224,6 @@ function DraftRow({ item, onChange, onRemove }) {
1235
1224
  }
1236
1225
  function DraftSectionCard({ section, sections, onPatchSection, onRemoveSection, onPatchItem, onRemoveItem }) {
1237
1226
  const t = useTranslation();
1238
- const existing = sections.find((s) => s.id === section.targetSectionId);
1239
1227
  return /* @__PURE__ */ jsxs("div", { className: "overflow-hidden rounded-card border-2 border-dashed border-primary/40 bg-primary/[0.03]", children: [
1240
1228
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b border-dashed border-primary/30 px-3 py-2", children: [
1241
1229
  /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5 text-primary" }),
@@ -1247,30 +1235,38 @@ function DraftSectionCard({ section, sections, onPatchSection, onRemoveSection,
1247
1235
  className: "rounded border border-transparent bg-transparent px-1 py-0.5 text-sm font-semibold hover:border-border focus:border-border focus:outline-none focus:ring-2 focus:ring-ring"
1248
1236
  }
1249
1237
  ),
1250
- /* @__PURE__ */ jsxs(
1251
- "select",
1252
- {
1253
- value: section.targetSectionId ?? "",
1254
- onChange: (e) => onPatchSection({ targetSectionId: e.target.value || void 0 }),
1255
- className: "h-7 rounded-md border bg-card px-2 text-xs",
1256
- children: [
1257
- /* @__PURE__ */ jsx("option", { value: "", children: t("menu.import.createSection") }),
1258
- sections.map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: t("menu.import.intoSection", { name: s.name }) }, s.id))
1259
- ]
1260
- }
1261
- ),
1262
1238
  /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: t("menu.manager.itemsCount", { count: String(section.items.length) }) }),
1263
- existing && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-2 py-0.5 text-[10px] text-muted-foreground", children: t("menu.import.willAppend", { name: existing.name }) }),
1239
+ /* @__PURE__ */ jsxs("label", { className: "ml-auto flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
1240
+ t("menu.import.destination"),
1241
+ /* @__PURE__ */ jsxs(
1242
+ "select",
1243
+ {
1244
+ value: section.targetSectionId ?? "",
1245
+ onChange: (e) => onPatchSection({ targetSectionId: e.target.value || void 0 }),
1246
+ className: "h-7 rounded-md border bg-card px-2 text-xs text-foreground",
1247
+ children: [
1248
+ /* @__PURE__ */ jsx("option", { value: "", children: t("menu.import.createSection") }),
1249
+ sections.map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: s.name }, s.id))
1250
+ ]
1251
+ }
1252
+ )
1253
+ ] }),
1264
1254
  /* @__PURE__ */ jsx(
1265
1255
  "button",
1266
1256
  {
1267
1257
  onClick: onRemoveSection,
1268
- className: "ml-auto rounded p-1 text-muted-foreground hover:text-destructive",
1258
+ className: "rounded p-1 text-muted-foreground hover:text-destructive",
1269
1259
  "aria-label": t("menu.import.dropSection"),
1270
1260
  children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
1271
1261
  }
1272
1262
  )
1273
1263
  ] }),
1264
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-dashed border-primary/20 px-3 py-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: [
1265
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 px-1", children: t("menu.itemForm.name") }),
1266
+ /* @__PURE__ */ jsx("span", { className: "hidden min-w-0 flex-1 px-1 sm:block", children: t("menu.itemForm.description") }),
1267
+ /* @__PURE__ */ jsx("span", { className: "w-20 px-1.5 text-right", children: t("menu.itemForm.price") }),
1268
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "w-[22px]" })
1269
+ ] }),
1274
1270
  /* @__PURE__ */ jsx("div", { children: section.items.map((item) => /* @__PURE__ */ jsx(
1275
1271
  DraftRow,
1276
1272
  {
@@ -1372,13 +1368,13 @@ function MenuManagerView() {
1372
1368
  const setItemStatus = useMenuStore((s) => s.setItemStatus);
1373
1369
  const toggleAvailability = useMenuStore((s) => s.toggleAvailability);
1374
1370
  const uploadImage = useMenuStore((s) => s.uploadImage);
1375
- const runExtraction = useMenuStore((s) => s.runExtraction);
1376
1371
  const updateDraftItem = useMenuStore((s) => s.updateDraftItem);
1377
1372
  const removeDraftItem = useMenuStore((s) => s.removeDraftItem);
1378
1373
  const updateDraftSection = useMenuStore((s) => s.updateDraftSection);
1379
1374
  const removeDraftSection = useMenuStore((s) => s.removeDraftSection);
1380
1375
  const discardDraft = useMenuStore((s) => s.discardDraft);
1381
1376
  const commitDraft = useMenuStore((s) => s.commitDraft);
1377
+ const stageDraft = useMenuStore((s) => s.stageDraft);
1382
1378
  const [search, setSearch] = useState("");
1383
1379
  const [menuEditor, setMenuEditor] = useState({ open: false });
1384
1380
  const [itemEditor, setItemEditor] = useState({ open: false, sectionId: "" });
@@ -1386,11 +1382,8 @@ function MenuManagerView() {
1386
1382
  const [confirm, setConfirm] = useState(null);
1387
1383
  const [dragging, setDragging] = useState(null);
1388
1384
  const [committing, setCommitting] = useState(false);
1389
- const extractor = useMemo(() => resolveMenuExtractor(), []);
1390
1385
  const readPhotos = async (files) => {
1391
- if (!extractor) return;
1392
- const images = await Promise.all(files.map((file) => downscaleImage(file)));
1393
- await runExtraction(images, extractor);
1386
+ useChatStore.getState().queuePrompt(t("menu.import.photoSentPrompt"), files);
1394
1387
  };
1395
1388
  const askForPhotos = () => requestChatUpload({
1396
1389
  label: t("menu.import.uploadCta"),
@@ -1499,7 +1492,9 @@ function MenuManagerView() {
1499
1492
  placements,
1500
1493
  itemsById,
1501
1494
  draft,
1502
- hasExtractor: !!extractor,
1495
+ // The chat reads the photo now; the ask-for-photo card never depends on a
1496
+ // dedicated extraction backend.
1497
+ hasExtractor: true,
1503
1498
  actions: {
1504
1499
  createSection: (name) => createSection(name),
1505
1500
  renameSection: (sectionId, name) => updateSection(sectionId, { name }),
@@ -1514,6 +1509,7 @@ function MenuManagerView() {
1514
1509
  }),
1515
1510
  duplicateMenu,
1516
1511
  requestPhotos: askForPhotos,
1512
+ stageDraft,
1517
1513
  updateDraftItem,
1518
1514
  removeDraftItem,
1519
1515
  updateDraftSection,
@@ -1608,7 +1604,7 @@ function MenuManagerView() {
1608
1604
  creates: true,
1609
1605
  onSelect: () => setMenuEditor({ open: true })
1610
1606
  }] : [],
1611
- ...extractor ? [{
1607
+ {
1612
1608
  id: "ai-import",
1613
1609
  label: t("menu.import.action"),
1614
1610
  description: t("menu.import.actionDesc"),
@@ -1618,7 +1614,7 @@ function MenuManagerView() {
1618
1614
  intent: multiMenu ? "secondary" : "primary",
1619
1615
  creates: true,
1620
1616
  onSelect: () => startAiImport()
1621
- }] : []
1617
+ }
1622
1618
  ]
1623
1619
  }
1624
1620
  ) }),
@@ -1687,7 +1683,7 @@ function MenuManagerView() {
1687
1683
  " ",
1688
1684
  t("menu.manager.addSection")
1689
1685
  ] }),
1690
- extractor && /* @__PURE__ */ jsxs(Button, { onClick: startAiImport, children: [
1686
+ /* @__PURE__ */ jsxs(Button, { onClick: startAiImport, children: [
1691
1687
  /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5" }),
1692
1688
  " ",
1693
1689
  t("menu.import.action")
@@ -1819,5 +1815,5 @@ function MenuPage({ config, provider, store, registries }) {
1819
1815
  }
1820
1816
 
1821
1817
  export { MenuPage };
1822
- //# sourceMappingURL=MenuPage-SO7EDR6R.js.map
1823
- //# sourceMappingURL=MenuPage-SO7EDR6R.js.map
1818
+ //# sourceMappingURL=MenuPage-GC6ATJTT.js.map
1819
+ //# sourceMappingURL=MenuPage-GC6ATJTT.js.map