@open-mercato/ui 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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 (148) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +2 -1
  3. package/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +204 -0
  4. package/dist/ai/AiAssistantLauncher.js +596 -0
  5. package/dist/ai/AiAssistantLauncher.js.map +7 -0
  6. package/dist/ai/AiChat.js +1092 -0
  7. package/dist/ai/AiChat.js.map +7 -0
  8. package/dist/ai/AiChatSessions.js +297 -0
  9. package/dist/ai/AiChatSessions.js.map +7 -0
  10. package/dist/ai/AiDock.js +347 -0
  11. package/dist/ai/AiDock.js.map +7 -0
  12. package/dist/ai/AiMessageContent.js +369 -0
  13. package/dist/ai/AiMessageContent.js.map +7 -0
  14. package/dist/ai/ChatPaneTabs.js +251 -0
  15. package/dist/ai/ChatPaneTabs.js.map +7 -0
  16. package/dist/ai/index.js +115 -0
  17. package/dist/ai/index.js.map +7 -0
  18. package/dist/ai/parts/ConfirmationCard.js +211 -0
  19. package/dist/ai/parts/ConfirmationCard.js.map +7 -0
  20. package/dist/ai/parts/FieldDiffCard.js +119 -0
  21. package/dist/ai/parts/FieldDiffCard.js.map +7 -0
  22. package/dist/ai/parts/MutationPreviewCard.js +224 -0
  23. package/dist/ai/parts/MutationPreviewCard.js.map +7 -0
  24. package/dist/ai/parts/MutationResultCard.js +240 -0
  25. package/dist/ai/parts/MutationResultCard.js.map +7 -0
  26. package/dist/ai/parts/approval-cards-map.js +15 -0
  27. package/dist/ai/parts/approval-cards-map.js.map +7 -0
  28. package/dist/ai/parts/index.js +24 -0
  29. package/dist/ai/parts/index.js.map +7 -0
  30. package/dist/ai/parts/pending-action-api.js +60 -0
  31. package/dist/ai/parts/pending-action-api.js.map +7 -0
  32. package/dist/ai/parts/types.js +1 -0
  33. package/dist/ai/parts/types.js.map +7 -0
  34. package/dist/ai/parts/useAiPendingActionPolling.js +126 -0
  35. package/dist/ai/parts/useAiPendingActionPolling.js.map +7 -0
  36. package/dist/ai/records/ActivityCard.js +83 -0
  37. package/dist/ai/records/ActivityCard.js.map +7 -0
  38. package/dist/ai/records/CompanyCard.js +81 -0
  39. package/dist/ai/records/CompanyCard.js.map +7 -0
  40. package/dist/ai/records/DealCard.js +76 -0
  41. package/dist/ai/records/DealCard.js.map +7 -0
  42. package/dist/ai/records/PersonCard.js +68 -0
  43. package/dist/ai/records/PersonCard.js.map +7 -0
  44. package/dist/ai/records/ProductCard.js +68 -0
  45. package/dist/ai/records/ProductCard.js.map +7 -0
  46. package/dist/ai/records/RecordCard.js +29 -0
  47. package/dist/ai/records/RecordCard.js.map +7 -0
  48. package/dist/ai/records/RecordCardShell.js +103 -0
  49. package/dist/ai/records/RecordCardShell.js.map +7 -0
  50. package/dist/ai/records/index.js +31 -0
  51. package/dist/ai/records/index.js.map +7 -0
  52. package/dist/ai/records/registry.js +51 -0
  53. package/dist/ai/records/registry.js.map +7 -0
  54. package/dist/ai/records/types.js +1 -0
  55. package/dist/ai/records/types.js.map +7 -0
  56. package/dist/ai/ui-part-registry.js +112 -0
  57. package/dist/ai/ui-part-registry.js.map +7 -0
  58. package/dist/ai/ui-part-slots.js +14 -0
  59. package/dist/ai/ui-part-slots.js.map +7 -0
  60. package/dist/ai/ui-parts/pending-phase3-placeholder.js +35 -0
  61. package/dist/ai/ui-parts/pending-phase3-placeholder.js.map +7 -0
  62. package/dist/ai/upload-adapter.js +256 -0
  63. package/dist/ai/upload-adapter.js.map +7 -0
  64. package/dist/ai/useAiChat.js +549 -0
  65. package/dist/ai/useAiChat.js.map +7 -0
  66. package/dist/ai/useAiChatUpload.js +127 -0
  67. package/dist/ai/useAiChatUpload.js.map +7 -0
  68. package/dist/ai/useAiShortcuts.js +43 -0
  69. package/dist/ai/useAiShortcuts.js.map +7 -0
  70. package/dist/backend/AppShell.js +8 -4
  71. package/dist/backend/AppShell.js.map +2 -2
  72. package/dist/backend/BackendChromeProvider.js +2 -0
  73. package/dist/backend/BackendChromeProvider.js.map +2 -2
  74. package/dist/backend/DataTable.js +19 -2
  75. package/dist/backend/DataTable.js.map +2 -2
  76. package/dist/backend/FilterBar.js +19 -15
  77. package/dist/backend/FilterBar.js.map +2 -2
  78. package/dist/backend/dashboard/DashboardScreen.js +31 -3
  79. package/dist/backend/dashboard/DashboardScreen.js.map +2 -2
  80. package/dist/backend/injection/spotIds.js +6 -0
  81. package/dist/backend/injection/spotIds.js.map +2 -2
  82. package/dist/backend/notifications/useNotificationEffect.js +38 -2
  83. package/dist/backend/notifications/useNotificationEffect.js.map +2 -2
  84. package/dist/index.js +1 -0
  85. package/dist/index.js.map +2 -2
  86. package/jest.config.cjs +7 -1
  87. package/jest.markdown-mock.tsx +7 -0
  88. package/package.json +10 -4
  89. package/src/ai/AiAssistantLauncher.tsx +805 -0
  90. package/src/ai/AiChat.tsx +1483 -0
  91. package/src/ai/AiChatSessions.tsx +429 -0
  92. package/src/ai/AiDock.tsx +505 -0
  93. package/src/ai/AiMessageContent.tsx +515 -0
  94. package/src/ai/ChatPaneTabs.tsx +310 -0
  95. package/src/ai/__tests__/AiChat.conversation.test.tsx +160 -0
  96. package/src/ai/__tests__/AiChat.debug.test.tsx +152 -0
  97. package/src/ai/__tests__/AiChat.registry.test.tsx +213 -0
  98. package/src/ai/__tests__/AiChat.test.tsx +257 -0
  99. package/src/ai/__tests__/AiDock.test.tsx +124 -0
  100. package/src/ai/__tests__/AiMessageContent.test.ts +111 -0
  101. package/src/ai/__tests__/ui-part-registry.test.ts +199 -0
  102. package/src/ai/__tests__/ui-part-slots.test.ts +43 -0
  103. package/src/ai/__tests__/upload-adapter.test.ts +213 -0
  104. package/src/ai/__tests__/useAiChatUpload.test.tsx +163 -0
  105. package/src/ai/__tests__/useAiShortcuts.test.tsx +100 -0
  106. package/src/ai/index.ts +125 -0
  107. package/src/ai/parts/ConfirmationCard.tsx +310 -0
  108. package/src/ai/parts/FieldDiffCard.tsx +173 -0
  109. package/src/ai/parts/MutationPreviewCard.tsx +302 -0
  110. package/src/ai/parts/MutationResultCard.tsx +360 -0
  111. package/src/ai/parts/__tests__/ConfirmationCard.test.tsx +169 -0
  112. package/src/ai/parts/__tests__/FieldDiffCard.test.tsx +74 -0
  113. package/src/ai/parts/__tests__/MutationPreviewCard.test.tsx +177 -0
  114. package/src/ai/parts/__tests__/MutationResultCard.test.tsx +127 -0
  115. package/src/ai/parts/__tests__/useAiPendingActionPolling.test.tsx +151 -0
  116. package/src/ai/parts/approval-cards-map.ts +24 -0
  117. package/src/ai/parts/index.ts +27 -0
  118. package/src/ai/parts/pending-action-api.ts +123 -0
  119. package/src/ai/parts/types.ts +84 -0
  120. package/src/ai/parts/useAiPendingActionPolling.ts +210 -0
  121. package/src/ai/records/ActivityCard.tsx +102 -0
  122. package/src/ai/records/CompanyCard.tsx +89 -0
  123. package/src/ai/records/DealCard.tsx +85 -0
  124. package/src/ai/records/PersonCard.tsx +77 -0
  125. package/src/ai/records/ProductCard.tsx +83 -0
  126. package/src/ai/records/RecordCard.tsx +37 -0
  127. package/src/ai/records/RecordCardShell.tsx +169 -0
  128. package/src/ai/records/index.ts +30 -0
  129. package/src/ai/records/registry.tsx +80 -0
  130. package/src/ai/records/types.ts +90 -0
  131. package/src/ai/ui-part-registry.ts +233 -0
  132. package/src/ai/ui-part-slots.ts +32 -0
  133. package/src/ai/ui-parts/pending-phase3-placeholder.tsx +50 -0
  134. package/src/ai/upload-adapter.ts +421 -0
  135. package/src/ai/useAiChat.ts +865 -0
  136. package/src/ai/useAiChatUpload.ts +180 -0
  137. package/src/ai/useAiShortcuts.ts +79 -0
  138. package/src/backend/AppShell.tsx +12 -5
  139. package/src/backend/BackendChromeProvider.tsx +2 -0
  140. package/src/backend/DataTable.tsx +20 -1
  141. package/src/backend/FilterBar.tsx +26 -13
  142. package/src/backend/__tests__/BackendChromeProvider.test.tsx +45 -0
  143. package/src/backend/dashboard/DashboardScreen.tsx +38 -3
  144. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +24 -1
  145. package/src/backend/injection/spotIds.ts +6 -0
  146. package/src/backend/notifications/__tests__/useNotificationEffect.test.tsx +77 -0
  147. package/src/backend/notifications/useNotificationEffect.ts +47 -2
  148. package/src/index.ts +1 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ai/upload-adapter.ts"],
4
+ "sourcesContent": ["/**\n * Framework-agnostic upload adapter for the AI chat composer.\n *\n * Forwards files dropped into {@link AiChat} to the existing attachments API\n * (`POST /api/attachments`, multipart form-data \u2014 see\n * `packages/core/src/modules/attachments/api/route.ts`) and returns the\n * resulting `attachmentIds` so the chat request layer can thread them into the\n * dispatcher body (`POST /api/ai_assistant/ai/chat?agent=<id>` reads\n * `attachmentIds` from JSON).\n *\n * The adapter is intentionally framework-agnostic: no Next.js imports, no\n * React. A thin React hook ({@link useAiChatUpload}) wraps it for the composer.\n */\n\nconst DEFAULT_ATTACHMENTS_ENDPOINT = '/api/attachments'\nconst DEFAULT_AI_CHAT_ENTITY_ID = 'ai-chat-draft'\nconst DEFAULT_CONCURRENCY = 3\n// Hard cap on a single upload's wall-clock time. The previous implementation\n// had no timeout, so a stalled `/api/attachments` request would leave the\n// composer chip spinning forever (the server never returned, the client\n// never unblocked the Send button). 60s is generous for the documented\n// per-file size limits and matches the behaviour of the rest of the\n// backoffice's `apiCall` helpers.\nconst DEFAULT_PER_FILE_TIMEOUT_MS = 60_000\n\nexport type UploadFailureReason =\n | 'mime_rejected'\n | 'size_exceeded'\n | 'network'\n | 'server'\n | 'aborted'\n\nexport interface UploadAttachmentsForChatOptions {\n /** Optional override for the attachments endpoint (defaults to `/api/attachments`). */\n endpoint?: string\n /** Entity identifier recorded alongside the attachment. Defaults to `'ai-chat-draft'`. */\n entityType?: string\n /**\n * Record identifier for the chat draft. When omitted, the adapter mints a\n * per-invocation UUID so every batch groups cleanly in the attachments table.\n */\n recordId?: string\n /** Optional partition code; forwarded verbatim to the attachments route. */\n partitionCode?: string\n /** Optional injectable fetch (tests, portal). Defaults to `globalThis.fetch`. */\n fetchImpl?: typeof fetch\n /** Optional progress callback fired once per file completion. */\n onProgress?: (\n fileIndex: number,\n progress: { loaded: number; total: number },\n ) => void\n /** Abort the whole batch; queued files short-circuit as `'aborted'`. */\n signal?: AbortSignal\n /** Parallelism cap. Defaults to 3. */\n concurrency?: number\n /**\n * Hard timeout per upload, in milliseconds. Defaults to 60_000 (60s).\n * When the upload exceeds the timeout the request is aborted and the\n * file lands in `failed` with `reason: 'aborted'` instead of the chip\n * spinning forever. Pass `0` to disable.\n */\n perFileTimeoutMs?: number\n}\n\nexport interface UploadedAttachment {\n attachmentId: string\n /**\n * Server-returned (possibly sanitized) filename. The original\n * client-side `File.name` is preserved on `originalFileName` so the\n * chat composer can pair the upload result back to its chip without a\n * sanitization-induced map miss.\n */\n fileName: string\n /** The exact `File.name` the caller passed in. Always set. */\n originalFileName: string\n /** Position of this file in the original input array. Always set. */\n inputIndex: number\n mediaType: string\n size: number\n}\n\nexport interface UploadFailure {\n fileName: string\n /** The exact `File.name` the caller passed in. Always set. */\n originalFileName: string\n /** Position of this file in the original input array. Always set. */\n inputIndex: number\n reason: UploadFailureReason\n message: string\n}\n\nexport interface UploadAttachmentsForChatResult {\n items: UploadedAttachment[]\n failed: UploadFailure[]\n}\n\nfunction mintRecordId(): string {\n const cryptoApi = (globalThis as unknown as { crypto?: Crypto }).crypto\n if (cryptoApi && typeof cryptoApi.randomUUID === 'function') {\n return cryptoApi.randomUUID()\n }\n const random = Math.random().toString(36).slice(2, 10)\n const time = Date.now().toString(36)\n return `ai-chat-${time}-${random}`\n}\n\nfunction resolveFetchImpl(explicit?: typeof fetch): typeof fetch {\n if (explicit) return explicit\n const fallback = (globalThis as typeof globalThis & { fetch?: typeof fetch }).fetch\n if (!fallback) {\n throw new Error('No fetch implementation available for uploadAttachmentsForChat')\n }\n return fallback.bind(globalThis) as typeof fetch\n}\n\nfunction normalizeServerErrorMessage(raw: unknown): string {\n if (raw && typeof raw === 'object') {\n const err = (raw as { error?: unknown; message?: unknown }).error\n if (typeof err === 'string' && err.trim()) return err\n const msg = (raw as { message?: unknown }).message\n if (typeof msg === 'string' && msg.trim()) return msg\n }\n return ''\n}\n\nfunction mapStatusToReason(status: number, message: string): UploadFailureReason {\n if (status === 413) return 'size_exceeded'\n if (status === 403 || status === 415) return 'mime_rejected'\n if (status === 400) {\n const lower = message.toLowerCase()\n if (lower.includes('file type') || lower.includes('active content')) {\n return 'mime_rejected'\n }\n if (lower.includes('size') || lower.includes('quota')) {\n return 'size_exceeded'\n }\n }\n return 'server'\n}\n\nfunction parseServerItem(\n payload: unknown,\n fallbackFile: File,\n inputIndex: number,\n): UploadedAttachment | null {\n if (!payload || typeof payload !== 'object') return null\n const item = (payload as { item?: unknown }).item\n if (!item || typeof item !== 'object') return null\n const id = (item as { id?: unknown }).id\n if (typeof id !== 'string' || !id.trim()) return null\n const fileName =\n typeof (item as { fileName?: unknown }).fileName === 'string'\n ? (item as { fileName: string }).fileName\n : fallbackFile.name\n const fileSize = (item as { fileSize?: unknown }).fileSize\n const size =\n typeof fileSize === 'number' && Number.isFinite(fileSize) ? fileSize : fallbackFile.size\n const mimeTypeCandidate = (item as { mimeType?: unknown; mediaType?: unknown })\n const mediaType =\n typeof mimeTypeCandidate.mimeType === 'string' && mimeTypeCandidate.mimeType.trim()\n ? mimeTypeCandidate.mimeType\n : typeof mimeTypeCandidate.mediaType === 'string' && mimeTypeCandidate.mediaType.trim()\n ? mimeTypeCandidate.mediaType\n : fallbackFile.type || 'application/octet-stream'\n return {\n attachmentId: id,\n fileName,\n originalFileName: fallbackFile.name,\n inputIndex,\n mediaType,\n size,\n }\n}\n\ninterface UploadSingleArgs {\n file: File\n fileIndex: number\n endpoint: string\n entityType: string\n recordId: string\n partitionCode?: string\n fetchImpl: typeof fetch\n signal: AbortSignal\n perFileTimeoutMs: number\n onProgress?: UploadAttachmentsForChatOptions['onProgress']\n}\n\ntype SingleOutcome =\n | { ok: true; item: UploadedAttachment }\n | { ok: false; failure: UploadFailure }\n\nasync function uploadSingleFile(args: UploadSingleArgs): Promise<SingleOutcome> {\n const {\n file,\n fileIndex,\n endpoint,\n entityType,\n recordId,\n partitionCode,\n fetchImpl,\n signal,\n perFileTimeoutMs,\n onProgress,\n } = args\n\n const buildFailure = (\n reason: UploadFailureReason,\n message: string,\n ): UploadFailure => ({\n fileName: file.name,\n originalFileName: file.name,\n inputIndex: fileIndex,\n reason,\n message,\n })\n\n if (signal.aborted) {\n return {\n ok: false,\n failure: buildFailure('aborted', 'Upload aborted before starting.'),\n }\n }\n\n const form = new FormData()\n form.append('entityId', entityType)\n form.append('recordId', recordId)\n form.append('file', file)\n if (partitionCode && partitionCode.trim().length > 0) {\n form.append('partitionCode', partitionCode.trim())\n }\n\n // Per-file timeout \u2014 wired through a child AbortController that is also\n // cancelled when the parent batch aborts. Without this guard a stalled\n // server (slow OCR, dead connection) would leave the chip spinning\n // forever and block the composer's Send button indefinitely.\n const localController = new AbortController()\n const onParentAbort = () => localController.abort()\n signal.addEventListener('abort', onParentAbort, { once: true })\n let timeoutHandle: ReturnType<typeof setTimeout> | null = null\n let timedOut = false\n if (perFileTimeoutMs > 0) {\n timeoutHandle = setTimeout(() => {\n timedOut = true\n localController.abort()\n }, perFileTimeoutMs)\n }\n const clearTimers = () => {\n if (timeoutHandle !== null) {\n clearTimeout(timeoutHandle)\n timeoutHandle = null\n }\n signal.removeEventListener('abort', onParentAbort)\n }\n\n let response: Response\n try {\n response = await fetchImpl(endpoint, {\n method: 'POST',\n body: form,\n signal: localController.signal,\n })\n } catch (networkError) {\n clearTimers()\n if (timedOut) {\n return {\n ok: false,\n failure: buildFailure(\n 'aborted',\n `Upload timed out after ${Math.round(perFileTimeoutMs / 1000)}s. The server did not respond \u2014 try again, or attach the file to a record first and reference it in the chat.`,\n ),\n }\n }\n const aborted =\n signal.aborted ||\n localController.signal.aborted ||\n (networkError as { name?: string } | undefined)?.name === 'AbortError'\n if (aborted) {\n return { ok: false, failure: buildFailure('aborted', 'Upload aborted.') }\n }\n const message =\n networkError instanceof Error ? networkError.message : 'Network request failed.'\n return { ok: false, failure: buildFailure('network', message) }\n }\n clearTimers()\n\n let payload: unknown = null\n try {\n const text = await response.text()\n if (text && text.trim()) {\n payload = JSON.parse(text) as unknown\n }\n } catch {\n // Response may not be JSON (HTML error page, empty body, etc.)\n payload = null\n }\n\n if (!response.ok) {\n const rawMessage = normalizeServerErrorMessage(payload)\n const fallbackMessage = rawMessage || `Upload failed (${response.status}).`\n const reason = mapStatusToReason(response.status, rawMessage)\n return { ok: false, failure: buildFailure(reason, fallbackMessage) }\n }\n\n const item = parseServerItem(payload, file, fileIndex)\n if (!item) {\n return {\n ok: false,\n failure: buildFailure(\n 'server',\n 'Attachment API returned an unexpected response shape.',\n ),\n }\n }\n\n if (onProgress) {\n try {\n onProgress(fileIndex, { loaded: item.size, total: item.size })\n } catch {\n // A misbehaving progress callback must never abort the upload pipeline.\n }\n }\n\n return { ok: true, item }\n}\n\n/**\n * Uploads files in parallel (bounded to `concurrency`, default 3) via the\n * attachments API and pairs the returned IDs back to the input order.\n *\n * The batch promise only rejects on programming errors \u2014 server rejections,\n * network errors, and aborts are surfaced via {@link UploadAttachmentsForChatResult.failed}\n * so the caller can render chips and retry UX without try/catch noise.\n */\nexport async function uploadAttachmentsForChat(\n files: File[],\n options: UploadAttachmentsForChatOptions = {},\n): Promise<UploadAttachmentsForChatResult> {\n const items: UploadedAttachment[] = []\n const failed: UploadFailure[] = []\n if (!Array.isArray(files) || files.length === 0) {\n return { items, failed }\n }\n\n const fetchImpl = resolveFetchImpl(options.fetchImpl)\n const endpoint = options.endpoint?.trim() || DEFAULT_ATTACHMENTS_ENDPOINT\n const entityType = options.entityType?.trim() || DEFAULT_AI_CHAT_ENTITY_ID\n const recordId = options.recordId?.trim() || mintRecordId()\n const rawConcurrency = options.concurrency ?? DEFAULT_CONCURRENCY\n const concurrency = Math.max(\n 1,\n Math.min(files.length, Math.floor(rawConcurrency) || DEFAULT_CONCURRENCY),\n )\n const signal = options.signal ?? new AbortController().signal\n const perFileTimeoutMs = (() => {\n const raw = options.perFileTimeoutMs\n if (raw === 0) return 0\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) return raw\n return DEFAULT_PER_FILE_TIMEOUT_MS\n })()\n\n const outcomes: Array<SingleOutcome | null> = new Array(files.length).fill(null)\n let nextIndex = 0\n\n const worker = async (): Promise<void> => {\n while (true) {\n if (signal.aborted) return\n const currentIndex = nextIndex\n if (currentIndex >= files.length) return\n nextIndex = currentIndex + 1\n const file = files[currentIndex]\n outcomes[currentIndex] = await uploadSingleFile({\n file,\n fileIndex: currentIndex,\n endpoint,\n entityType,\n recordId,\n partitionCode: options.partitionCode,\n fetchImpl,\n signal,\n perFileTimeoutMs,\n onProgress: options.onProgress,\n })\n }\n }\n\n const workerCount = Math.min(concurrency, files.length)\n const workers = Array.from({ length: workerCount }, () => worker())\n await Promise.all(workers)\n\n for (let index = 0; index < files.length; index += 1) {\n const outcome = outcomes[index]\n if (outcome && outcome.ok) {\n items.push(outcome.item)\n continue\n }\n if (outcome && !outcome.ok) {\n failed.push(outcome.failure)\n continue\n }\n // Worker exited without processing this slot \u2192 it was skipped due to abort.\n const file = files[index]\n const fallbackName = file?.name ?? `file-${index}`\n failed.push({\n fileName: fallbackName,\n originalFileName: fallbackName,\n inputIndex: index,\n reason: 'aborted',\n message: 'Upload aborted before starting.',\n })\n }\n\n return { items, failed }\n}\n\nexport const __testables = {\n DEFAULT_ATTACHMENTS_ENDPOINT,\n DEFAULT_AI_CHAT_ENTITY_ID,\n DEFAULT_CONCURRENCY,\n DEFAULT_PER_FILE_TIMEOUT_MS,\n mapStatusToReason,\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,549 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { createAiAgentTransport } from "@open-mercato/ai-assistant/modules/ai_assistant/lib/agent-transport";
4
+ import { apiFetch } from "../backend/utils/api.js";
5
+ function makeMessageId() {
6
+ const random = Math.random().toString(36).slice(2, 10);
7
+ const time = Date.now().toString(36);
8
+ return `msg_${time}_${random}`;
9
+ }
10
+ function makeConversationId() {
11
+ const g = globalThis;
12
+ if (g.crypto && typeof g.crypto.randomUUID === "function") {
13
+ try {
14
+ return g.crypto.randomUUID();
15
+ } catch {
16
+ }
17
+ }
18
+ const rand = () => Math.random().toString(16).slice(2, 10);
19
+ return `conv_${Date.now().toString(16)}_${rand()}${rand()}`;
20
+ }
21
+ const SESSION_STORAGE_PREFIX = "om-ai-chat:";
22
+ const SESSION_STORAGE_VERSION = 1;
23
+ function getSessionStorageKey(agent, conversationId) {
24
+ if (typeof conversationId === "string" && conversationId.length > 0) {
25
+ return `${SESSION_STORAGE_PREFIX}${agent}:${conversationId}`;
26
+ }
27
+ return `${SESSION_STORAGE_PREFIX}${agent}`;
28
+ }
29
+ function readPersistedSession(agent, conversationId) {
30
+ if (typeof window === "undefined") return null;
31
+ try {
32
+ const raw = window.localStorage.getItem(getSessionStorageKey(agent, conversationId));
33
+ if (!raw) return null;
34
+ const parsed = JSON.parse(raw);
35
+ if (!parsed || parsed.v !== SESSION_STORAGE_VERSION) return null;
36
+ if (typeof parsed.conversationId !== "string") return null;
37
+ if (!Array.isArray(parsed.messages)) return null;
38
+ const messages = parsed.messages.filter((entry) => {
39
+ return !!entry && typeof entry === "object" && typeof entry.id === "string" && typeof entry.content === "string" && (entry.role === "user" || entry.role === "assistant");
40
+ });
41
+ return { v: SESSION_STORAGE_VERSION, conversationId: parsed.conversationId, messages };
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ function writePersistedSession(agent, session, conversationId) {
47
+ if (typeof window === "undefined") return;
48
+ try {
49
+ const messages = session.messages.map((message) => {
50
+ if (!message.files || message.files.length === 0) return message;
51
+ const safeFiles = message.files.map(({ name, type, previewUrl }) => {
52
+ const durable = typeof previewUrl === "string" && previewUrl.startsWith("data:") ? previewUrl : void 0;
53
+ return durable ? { name, type, previewUrl: durable } : { name, type };
54
+ });
55
+ return { ...message, files: safeFiles };
56
+ });
57
+ window.localStorage.setItem(
58
+ getSessionStorageKey(agent, conversationId),
59
+ JSON.stringify({ ...session, messages })
60
+ );
61
+ } catch {
62
+ }
63
+ }
64
+ function clearPersistedSession(agent, conversationId) {
65
+ if (typeof window === "undefined") return;
66
+ try {
67
+ window.localStorage.removeItem(getSessionStorageKey(agent, conversationId));
68
+ } catch {
69
+ }
70
+ }
71
+ function getTransportEndpoint(agent, apiPath) {
72
+ const transport = createAiAgentTransport({ agentId: agent, endpoint: apiPath });
73
+ void transport;
74
+ const base = apiPath && apiPath.length > 0 ? apiPath : "/api/ai_assistant/ai/chat";
75
+ const separator = base.includes("?") ? "&" : "?";
76
+ return `${base}${separator}agent=${encodeURIComponent(agent)}`;
77
+ }
78
+ function createBuilder() {
79
+ return { text: "", reasoning: "", reasoningStreaming: false, toolCalls: [], uiParts: [] };
80
+ }
81
+ function extractUiPartsFromOutput(output, toolCallId) {
82
+ let parsed = output;
83
+ if (typeof output === "string") {
84
+ const trimmed = output.trim();
85
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return [];
86
+ try {
87
+ parsed = JSON.parse(trimmed);
88
+ } catch {
89
+ return [];
90
+ }
91
+ }
92
+ if (!parsed || typeof parsed !== "object") return [];
93
+ const value = parsed;
94
+ const parts = [];
95
+ if (value.status === "pending-confirmation" || value.status === "awaiting-confirmation") {
96
+ const pendingActionId = typeof value.pendingActionId === "string" && value.pendingActionId.length > 0 ? value.pendingActionId : null;
97
+ if (pendingActionId) {
98
+ const agentId = typeof value.agentId === "string" ? value.agentId : typeof value.agent === "string" ? value.agent : void 0;
99
+ parts.push({
100
+ componentId: "mutation-preview-card",
101
+ pendingActionId,
102
+ payload: {
103
+ pendingActionId,
104
+ expiresAt: typeof value.expiresAt === "string" ? value.expiresAt : void 0,
105
+ agentId,
106
+ toolName: typeof value.toolName === "string" ? value.toolName : void 0
107
+ },
108
+ key: `${toolCallId}:mutation-preview-card`
109
+ });
110
+ }
111
+ }
112
+ if (value.uiPart && typeof value.uiPart === "object") {
113
+ const part = value.uiPart;
114
+ if (typeof part.componentId === "string" && part.componentId.length > 0) {
115
+ parts.push({
116
+ componentId: part.componentId,
117
+ payload: part.payload,
118
+ pendingActionId: typeof part.pendingActionId === "string" ? part.pendingActionId : void 0,
119
+ key: `${toolCallId}:${part.componentId}`
120
+ });
121
+ }
122
+ }
123
+ if (Array.isArray(value.uiParts)) {
124
+ value.uiParts.forEach((entry, index) => {
125
+ if (!entry || typeof entry !== "object") return;
126
+ const part = entry;
127
+ if (typeof part.componentId !== "string" || part.componentId.length === 0) return;
128
+ parts.push({
129
+ componentId: part.componentId,
130
+ payload: part.payload,
131
+ pendingActionId: typeof part.pendingActionId === "string" ? part.pendingActionId : void 0,
132
+ key: `${toolCallId}:${index}:${part.componentId}`
133
+ });
134
+ });
135
+ }
136
+ return parts;
137
+ }
138
+ function updateToolCall(state, id, patch) {
139
+ if (!id) return state;
140
+ const idx = state.toolCalls.findIndex((entry) => entry.id === id);
141
+ if (idx === -1) {
142
+ const next = {
143
+ id,
144
+ toolName: patch.toolName ?? "tool",
145
+ state: patch.state ?? "pending",
146
+ input: patch.input,
147
+ output: patch.output,
148
+ errorMessage: patch.errorMessage
149
+ };
150
+ return { ...state, toolCalls: [...state.toolCalls, next] };
151
+ }
152
+ const current = state.toolCalls[idx];
153
+ const merged = {
154
+ ...current,
155
+ toolName: patch.toolName ?? current.toolName,
156
+ state: patch.state ?? current.state,
157
+ input: patch.input !== void 0 ? patch.input : current.input,
158
+ output: patch.output !== void 0 ? patch.output : current.output,
159
+ errorMessage: patch.errorMessage ?? current.errorMessage
160
+ };
161
+ const nextCalls = state.toolCalls.slice();
162
+ nextCalls[idx] = merged;
163
+ return { ...state, toolCalls: nextCalls };
164
+ }
165
+ function applyChunk(state, chunk) {
166
+ switch (chunk.type) {
167
+ case "text-delta":
168
+ return {
169
+ ...state,
170
+ text: state.text + (typeof chunk.delta === "string" ? chunk.delta : "")
171
+ };
172
+ case "reasoning-start":
173
+ return { ...state, reasoningStreaming: true };
174
+ case "reasoning-delta":
175
+ return {
176
+ ...state,
177
+ reasoning: state.reasoning + (typeof chunk.delta === "string" ? chunk.delta : ""),
178
+ reasoningStreaming: true
179
+ };
180
+ case "reasoning-end":
181
+ return { ...state, reasoningStreaming: false };
182
+ case "tool-input-start":
183
+ return updateToolCall(state, String(chunk.toolCallId ?? ""), {
184
+ toolName: typeof chunk.toolName === "string" ? chunk.toolName : void 0,
185
+ state: "pending"
186
+ });
187
+ case "tool-input-available":
188
+ return updateToolCall(state, String(chunk.toolCallId ?? ""), {
189
+ toolName: typeof chunk.toolName === "string" ? chunk.toolName : void 0,
190
+ input: chunk.input,
191
+ state: "pending"
192
+ });
193
+ case "tool-output-available": {
194
+ const toolCallId = String(chunk.toolCallId ?? "");
195
+ const next = updateToolCall(state, toolCallId, {
196
+ output: chunk.output,
197
+ state: "complete"
198
+ });
199
+ const newParts = extractUiPartsFromOutput(chunk.output, toolCallId);
200
+ if (newParts.length === 0) return next;
201
+ const seen = new Set(next.uiParts.map((entry) => entry.key));
202
+ const merged = [...next.uiParts];
203
+ for (const part of newParts) {
204
+ if (seen.has(part.key)) continue;
205
+ seen.add(part.key);
206
+ merged.push(part);
207
+ }
208
+ if (merged.length === next.uiParts.length) return next;
209
+ return { ...next, uiParts: merged };
210
+ }
211
+ case "tool-output-error":
212
+ return updateToolCall(state, String(chunk.toolCallId ?? ""), {
213
+ state: "error",
214
+ errorMessage: typeof chunk.errorText === "string" ? chunk.errorText : "Tool error"
215
+ });
216
+ case "tool-input-error":
217
+ return updateToolCall(state, String(chunk.toolCallId ?? ""), {
218
+ toolName: typeof chunk.toolName === "string" ? chunk.toolName : void 0,
219
+ input: chunk.input,
220
+ state: "error",
221
+ errorMessage: typeof chunk.errorText === "string" ? chunk.errorText : "Tool error"
222
+ });
223
+ default:
224
+ return state;
225
+ }
226
+ }
227
+ function mergeAssistantMessage(current, state) {
228
+ return {
229
+ ...current,
230
+ content: state.text,
231
+ reasoning: state.reasoning ? state.reasoning : void 0,
232
+ reasoningStreaming: state.reasoning ? state.reasoningStreaming : void 0,
233
+ toolCalls: state.toolCalls.length > 0 ? state.toolCalls : void 0,
234
+ uiParts: state.uiParts.length > 0 ? state.uiParts : void 0
235
+ };
236
+ }
237
+ function parseSseLines(buffer) {
238
+ const events = [];
239
+ let rest = buffer;
240
+ for (; ; ) {
241
+ const idx = rest.indexOf("\n\n");
242
+ if (idx === -1) break;
243
+ events.push(rest.slice(0, idx));
244
+ rest = rest.slice(idx + 2);
245
+ }
246
+ return { events, rest };
247
+ }
248
+ function extractDataPayload(eventBlock) {
249
+ const lines = eventBlock.split("\n");
250
+ const dataLines = [];
251
+ for (const line of lines) {
252
+ if (line.startsWith("data: ")) {
253
+ dataLines.push(line.slice(6));
254
+ } else if (line.startsWith("data:")) {
255
+ dataLines.push(line.slice(5));
256
+ }
257
+ }
258
+ if (dataLines.length === 0) return null;
259
+ return dataLines.join("\n");
260
+ }
261
+ async function readErrorEnvelope(response) {
262
+ try {
263
+ const data = await response.clone().json();
264
+ if (data && typeof data === "object") {
265
+ const rawMessage = typeof data.error === "string" && data.error || typeof data.message === "string" && data.message || "";
266
+ const rawCode = typeof data.code === "string" ? data.code : void 0;
267
+ if (rawMessage || rawCode) {
268
+ return {
269
+ code: rawCode,
270
+ message: rawMessage || "Agent dispatch failed."
271
+ };
272
+ }
273
+ }
274
+ } catch {
275
+ }
276
+ const text = await response.text().catch(() => "");
277
+ return { message: text || `Agent dispatch failed (${response.status}).` };
278
+ }
279
+ function useAiChat(input) {
280
+ const { agent, apiPath, pageContext, attachmentIds, debug, initialMessages, onError, conversationId: conversationIdInput } = input;
281
+ const persistedRef = React.useRef("unread");
282
+ if (persistedRef.current === "unread") {
283
+ persistedRef.current = typeof conversationIdInput === "string" && conversationIdInput.length > 0 ? readPersistedSession(agent, conversationIdInput) : readPersistedSession(agent);
284
+ }
285
+ const persisted = persistedRef.current;
286
+ const mintedConversationIdRef = React.useRef(null);
287
+ if (mintedConversationIdRef.current === null) {
288
+ mintedConversationIdRef.current = persisted?.conversationId ?? makeConversationId();
289
+ }
290
+ const effectiveConversationId = typeof conversationIdInput === "string" && conversationIdInput.length > 0 ? conversationIdInput : mintedConversationIdRef.current;
291
+ const [messages, setMessages] = React.useState(() => {
292
+ if (persisted && persisted.messages.length > 0) {
293
+ return persisted.messages;
294
+ }
295
+ return (initialMessages ?? []).map((entry) => ({
296
+ id: makeMessageId(),
297
+ role: entry.role,
298
+ content: entry.content
299
+ }));
300
+ });
301
+ const [status, setStatusInternal] = React.useState("idle");
302
+ React.useEffect(() => {
303
+ if (status !== "idle") return;
304
+ const persistKey = typeof conversationIdInput === "string" && conversationIdInput.length > 0 ? conversationIdInput : null;
305
+ if (messages.length === 0) {
306
+ clearPersistedSession(agent, persistKey);
307
+ return;
308
+ }
309
+ writePersistedSession(
310
+ agent,
311
+ {
312
+ v: SESSION_STORAGE_VERSION,
313
+ conversationId: effectiveConversationId,
314
+ messages
315
+ },
316
+ persistKey
317
+ );
318
+ }, [agent, conversationIdInput, effectiveConversationId, messages, status]);
319
+ const setStatus = setStatusInternal;
320
+ const [error, setError] = React.useState(null);
321
+ const [lastRequestDebug, setLastRequestDebug] = React.useState(null);
322
+ const [lastResponseDebug, setLastResponseDebug] = React.useState(null);
323
+ const abortRef = React.useRef(null);
324
+ const onErrorRef = React.useRef(onError);
325
+ React.useEffect(() => {
326
+ onErrorRef.current = onError;
327
+ }, [onError]);
328
+ const emitError = React.useCallback((envelope) => {
329
+ setError(envelope);
330
+ try {
331
+ onErrorRef.current?.(envelope);
332
+ } catch {
333
+ }
334
+ }, []);
335
+ const cancel = React.useCallback(() => {
336
+ if (abortRef.current) {
337
+ abortRef.current.abort();
338
+ abortRef.current = null;
339
+ }
340
+ setStatus("idle");
341
+ }, []);
342
+ const reset = React.useCallback(() => {
343
+ cancel();
344
+ setMessages([]);
345
+ setError(null);
346
+ setLastRequestDebug(null);
347
+ setLastResponseDebug(null);
348
+ clearPersistedSession(agent);
349
+ mintedConversationIdRef.current = makeConversationId();
350
+ }, [agent, cancel]);
351
+ const sendMessage = React.useCallback(
352
+ async (textInput, files) => {
353
+ const trimmed = textInput.trim();
354
+ if (!trimmed) return;
355
+ if (abortRef.current) {
356
+ abortRef.current.abort();
357
+ }
358
+ setError(null);
359
+ const userMessage = {
360
+ id: makeMessageId(),
361
+ role: "user",
362
+ content: trimmed,
363
+ files: files && files.length > 0 ? files : void 0
364
+ };
365
+ const assistantMessage = {
366
+ id: makeMessageId(),
367
+ role: "assistant",
368
+ content: ""
369
+ };
370
+ const assistantId = assistantMessage.id;
371
+ const outgoingHistory = [...messages, userMessage];
372
+ setMessages([...outgoingHistory, assistantMessage]);
373
+ setStatus("submitting");
374
+ const controller = new AbortController();
375
+ abortRef.current = controller;
376
+ const url = getTransportEndpoint(agent, apiPath);
377
+ const body = {
378
+ messages: outgoingHistory.map((message) => ({
379
+ role: message.role,
380
+ content: message.content
381
+ })),
382
+ pageContext,
383
+ attachmentIds,
384
+ debug,
385
+ conversationId: effectiveConversationId
386
+ };
387
+ setLastRequestDebug({ url, body });
388
+ let response;
389
+ try {
390
+ response = await apiFetch(url, {
391
+ method: "POST",
392
+ headers: {
393
+ "Content-Type": "application/json",
394
+ Accept: "text/event-stream, text/plain, application/json"
395
+ },
396
+ body: JSON.stringify(body),
397
+ signal: controller.signal
398
+ });
399
+ } catch (requestError) {
400
+ if (requestError?.name === "AbortError") {
401
+ setStatus("idle");
402
+ abortRef.current = null;
403
+ return;
404
+ }
405
+ const message = requestError instanceof Error ? requestError.message : "Network request failed.";
406
+ emitError({ message });
407
+ setStatus("idle");
408
+ abortRef.current = null;
409
+ return;
410
+ }
411
+ if (!response.ok) {
412
+ const envelope = await readErrorEnvelope(response);
413
+ setLastResponseDebug({ status: response.status, text: envelope.message });
414
+ emitError(envelope);
415
+ setStatus("idle");
416
+ setMessages((current) => current.filter((entry) => entry.id !== assistantId));
417
+ abortRef.current = null;
418
+ return;
419
+ }
420
+ const bodyStream = response.body;
421
+ if (!bodyStream) {
422
+ setLastResponseDebug({ status: response.status, text: "" });
423
+ setStatus("idle");
424
+ abortRef.current = null;
425
+ return;
426
+ }
427
+ const headerGet = (name) => {
428
+ const headers = response.headers;
429
+ if (!headers || typeof headers.get !== "function") return null;
430
+ try {
431
+ return headers.get(name);
432
+ } catch {
433
+ return null;
434
+ }
435
+ };
436
+ const isUiMessageStream = headerGet("x-vercel-ai-ui-message-stream") !== null || (headerGet("content-type") ?? "").includes("event-stream");
437
+ setStatus("streaming");
438
+ const reader = bodyStream.getReader();
439
+ const decoder = new TextDecoder();
440
+ let streamedRaw = "";
441
+ let builder = createBuilder();
442
+ let sseBuffer = "";
443
+ const flushUiMessageBuffer = (extra) => {
444
+ if (extra) sseBuffer += extra;
445
+ const { events, rest } = parseSseLines(sseBuffer);
446
+ sseBuffer = rest;
447
+ for (const block of events) {
448
+ const data = extractDataPayload(block);
449
+ if (!data) continue;
450
+ if (data === "[DONE]") continue;
451
+ try {
452
+ const parsed = JSON.parse(data);
453
+ if (parsed && typeof parsed.type === "string") {
454
+ builder = applyChunk(builder, parsed);
455
+ }
456
+ } catch {
457
+ }
458
+ }
459
+ };
460
+ try {
461
+ while (true) {
462
+ const { value, done } = await reader.read();
463
+ if (done) break;
464
+ if (!value) continue;
465
+ const piece = decoder.decode(value, { stream: true });
466
+ if (!piece) continue;
467
+ streamedRaw += piece;
468
+ if (isUiMessageStream) {
469
+ flushUiMessageBuffer(piece);
470
+ } else {
471
+ builder = { ...builder, text: streamedRaw };
472
+ }
473
+ const snapshotBuilder = builder;
474
+ setMessages(
475
+ (current) => current.map(
476
+ (entry) => entry.id === assistantId ? mergeAssistantMessage(entry, snapshotBuilder) : entry
477
+ )
478
+ );
479
+ }
480
+ const tail = decoder.decode();
481
+ if (tail) {
482
+ streamedRaw += tail;
483
+ if (isUiMessageStream) {
484
+ flushUiMessageBuffer(tail);
485
+ } else {
486
+ builder = { ...builder, text: streamedRaw };
487
+ }
488
+ }
489
+ if (isUiMessageStream && sseBuffer.length > 0) {
490
+ flushUiMessageBuffer("\n\n");
491
+ }
492
+ builder = { ...builder, reasoningStreaming: false };
493
+ const finalSnapshot = builder;
494
+ setMessages(
495
+ (current) => current.map(
496
+ (entry) => entry.id === assistantId ? mergeAssistantMessage(entry, finalSnapshot) : entry
497
+ )
498
+ );
499
+ setLastResponseDebug({ status: response.status, text: streamedRaw });
500
+ const isEmpty = !builder.text.trim() && builder.toolCalls.length === 0 && !builder.reasoning;
501
+ if (isEmpty) {
502
+ emitError({
503
+ code: "empty_response",
504
+ message: "The AI agent returned an empty response. This usually means the LLM provider rejected the request (invalid API key, rate limit, or model error). Check your server logs for details."
505
+ });
506
+ setMessages((current) => current.filter((entry) => entry.id !== assistantId));
507
+ }
508
+ } catch (streamError) {
509
+ if (streamError?.name === "AbortError") {
510
+ } else {
511
+ const rawMessage = streamError instanceof Error ? streamError.message : "Stream interrupted.";
512
+ const message = rawMessage.includes("API") ? rawMessage : `${rawMessage} \u2014 check server logs for LLM provider details.`;
513
+ emitError({ code: "stream_error", message });
514
+ setMessages((current) => current.filter((entry) => entry.id !== assistantId));
515
+ }
516
+ } finally {
517
+ reader.releaseLock();
518
+ if (abortRef.current === controller) {
519
+ abortRef.current = null;
520
+ }
521
+ setStatus("idle");
522
+ }
523
+ },
524
+ [agent, apiPath, attachmentIds, debug, effectiveConversationId, emitError, messages, pageContext]
525
+ );
526
+ React.useEffect(() => {
527
+ return () => {
528
+ if (abortRef.current) {
529
+ abortRef.current.abort();
530
+ abortRef.current = null;
531
+ }
532
+ };
533
+ }, []);
534
+ return {
535
+ messages,
536
+ status,
537
+ error,
538
+ lastRequestDebug,
539
+ lastResponseDebug,
540
+ conversationId: effectiveConversationId,
541
+ sendMessage,
542
+ cancel,
543
+ reset
544
+ };
545
+ }
546
+ export {
547
+ useAiChat
548
+ };
549
+ //# sourceMappingURL=useAiChat.js.map