@paymanai/payman-ask-sdk 4.0.18 → 4.0.20

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.js CHANGED
@@ -40,7 +40,10 @@ var remarkBreaks__default = /*#__PURE__*/_interopDefault(remarkBreaks);
40
40
 
41
41
  var __defProp = Object.defineProperty;
42
42
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
43
- var __publicField = (obj, key, value) => __defNormalProp(obj, key + "", value);
43
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
44
+ var __defProp2 = Object.defineProperty;
45
+ var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
46
+ var __publicField2 = (obj, key, value) => __defNormalProp2(obj, key + "", value);
44
47
  function generateId() {
45
48
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
46
49
  const r = Math.random() * 16 | 0;
@@ -736,7 +739,8 @@ function buildRequestBody(config, userMessage, sessionId, options) {
736
739
  sessionAttributes,
737
740
  analysisMode: options?.analysisMode,
738
741
  locale: resolveLocale(config.locale),
739
- timezone: resolveTimezone(config.timezone)
742
+ timezone: resolveTimezone(config.timezone),
743
+ ...options?.attachments?.length ? { attachments: options.attachments } : {}
740
744
  };
741
745
  }
742
746
  function resolveLocale(configured) {
@@ -781,6 +785,7 @@ function buildResolveImagesUrl(config) {
781
785
  const basePath = normalizedEndpointPath.endsWith("/stream") ? normalizedEndpointPath.slice(0, -"/stream".length) : normalizedEndpointPath;
782
786
  return `${config.api.baseUrl}${basePath}/resolve-image-urls`;
783
787
  }
788
+ var NGROK_SKIP_BROWSER_WARNING = "ngrok-skip-browser-warning";
784
789
  function buildRequestHeaders(config) {
785
790
  const headers = {
786
791
  ...config.api.headers
@@ -788,6 +793,9 @@ function buildRequestHeaders(config) {
788
793
  if (config.api.authToken) {
789
794
  headers.Authorization = `Bearer ${config.api.authToken}`;
790
795
  }
796
+ if (!headers[NGROK_SKIP_BROWSER_WARNING]) {
797
+ headers[NGROK_SKIP_BROWSER_WARNING] = "true";
798
+ }
791
799
  return headers;
792
800
  }
793
801
  var RAG_IMAGE_REGEX = /\/api\/rag\/chunks\/[^"'\s]+\/image/;
@@ -873,17 +881,6 @@ function useStreamManagerV2(config, callbacks, setMessages, setIsWaitingForRespo
873
881
  signal: abortController.signal,
874
882
  onEvent: (event) => {
875
883
  if (abortController.signal.aborted) return;
876
- try {
877
- const et = event?.eventType;
878
- if (et === "RUN_IN_PROGRESS" || et === "INTENT_PROGRESS" || et === "THINKING_DELTA") {
879
- const len = (event.partialText ?? event.text ?? "").length;
880
- console.log(`[stream] ${et} (+${len} chars)`);
881
- } else {
882
- console.log(`[stream] ${et ?? "?"}:`, JSON.stringify(event));
883
- }
884
- } catch {
885
- console.log("[stream] (unserializable event)", event?.eventType);
886
- }
887
884
  processStreamEventV2(event, state);
888
885
  if (state.lastUserAction) {
889
886
  callbacksRef.current.onUserActionRequired?.(state.lastUserAction);
@@ -1079,10 +1076,85 @@ function createCancelledMessageUpdate(steps, currentMessage) {
1079
1076
  currentMessage: currentMessage || "Thinking..."
1080
1077
  };
1081
1078
  }
1079
+ var DEFAULT_SIGNED_URL_ENDPOINT = "/api/files/signed-url";
1080
+ function fileExtension(filename) {
1081
+ const ext = filename.split(".").pop()?.toLowerCase().replace(/^\./, "");
1082
+ return ext && ext.length > 0 ? ext : "bin";
1083
+ }
1084
+ function resolveMimeType(file) {
1085
+ if (file.type && file.type.trim().length > 0) return file.type;
1086
+ const ext = fileExtension(file.name);
1087
+ const byExt = {
1088
+ pdf: "application/pdf",
1089
+ png: "image/png",
1090
+ jpg: "image/jpeg",
1091
+ jpeg: "image/jpeg",
1092
+ gif: "image/gif",
1093
+ webp: "image/webp"
1094
+ };
1095
+ return byExt[ext] ?? "application/octet-stream";
1096
+ }
1097
+ function buildSignedUrlEndpoint(config, ext) {
1098
+ const endpoint = config.api.signedUrlEndpoint || DEFAULT_SIGNED_URL_ENDPOINT;
1099
+ const queryParams = new URLSearchParams({ extn: ext.replace(/^\./, "") });
1100
+ return `${config.api.baseUrl}${endpoint}?${queryParams.toString()}`;
1101
+ }
1102
+ async function requestSignedUrl(config, ext, signal) {
1103
+ const url = buildSignedUrlEndpoint(config, ext);
1104
+ const headers = buildRequestHeaders(config);
1105
+ const response = await fetch(url, {
1106
+ method: "GET",
1107
+ headers,
1108
+ signal
1109
+ });
1110
+ if (!response.ok) {
1111
+ const errorText = await response.text().catch(() => "");
1112
+ throw new Error(
1113
+ errorText ? `Failed to get upload URL (${response.status}): ${errorText}` : `Failed to get upload URL (${response.status})`
1114
+ );
1115
+ }
1116
+ const data = await response.json();
1117
+ if (!data.key || !data.url) {
1118
+ throw new Error("Signed URL response missing key or url");
1119
+ }
1120
+ return { key: data.key, url: data.url };
1121
+ }
1122
+ async function uploadToSignedUrl(signedUrl, file, mimeType, signal) {
1123
+ const response = await fetch(signedUrl, {
1124
+ method: "PUT",
1125
+ headers: {
1126
+ "Content-Type": mimeType,
1127
+ "x-ms-blob-type": "BlockBlob"
1128
+ },
1129
+ body: file,
1130
+ signal
1131
+ });
1132
+ if (!response.ok) {
1133
+ const errorText = await response.text().catch(() => "");
1134
+ throw new Error(
1135
+ errorText ? `Failed to upload file (${response.status}): ${errorText}` : `Failed to upload file (${response.status})`
1136
+ );
1137
+ }
1138
+ }
1139
+ async function uploadAttachment(config, file, signal) {
1140
+ const ext = fileExtension(file.name);
1141
+ const mimeType = resolveMimeType(file);
1142
+ const { key, url } = await requestSignedUrl(config, ext, signal);
1143
+ await uploadToSignedUrl(url, file, mimeType, signal);
1144
+ return {
1145
+ tempKey: key,
1146
+ filename: file.name,
1147
+ mimeType
1148
+ };
1149
+ }
1150
+ async function uploadAttachments(config, files, signal) {
1151
+ const uploads = files.map((file) => uploadAttachment(config, file, signal));
1152
+ return Promise.all(uploads);
1153
+ }
1082
1154
  var UserActionStaleError = class extends Error {
1083
1155
  constructor(userActionId, message = "User action is no longer actionable") {
1084
1156
  super(message);
1085
- __publicField(this, "userActionId");
1157
+ __publicField2(this, "userActionId");
1086
1158
  this.name = "UserActionStaleError";
1087
1159
  this.userActionId = userActionId;
1088
1160
  }
@@ -1143,12 +1215,32 @@ function getStoredOrInitialMessages(config) {
1143
1215
  function getSessionIdFromMessages(messages) {
1144
1216
  return messages.find((message) => message.sessionId)?.sessionId;
1145
1217
  }
1218
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["jpg", "jpeg", "png", "gif", "webp", "avif"]);
1219
+ function attachmentKindFromFile(file) {
1220
+ const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
1221
+ if (IMAGE_EXTENSIONS.has(ext) || file.type.startsWith("image/")) return "image";
1222
+ return "file";
1223
+ }
1224
+ function buildMessageAttachments(files) {
1225
+ return files.map((file, index) => {
1226
+ const kind = attachmentKindFromFile(file);
1227
+ return {
1228
+ id: `att-${Date.now()}-${index}`,
1229
+ filename: file.name,
1230
+ mimeType: file.type || "application/octet-stream",
1231
+ // Blob URL for all local files so PDFs/docs stay previewable after send.
1232
+ previewUrl: URL.createObjectURL(file),
1233
+ kind
1234
+ };
1235
+ });
1236
+ }
1146
1237
  function useChatV2(config, callbacks = {}) {
1147
1238
  const [messages, setMessages] = react.useState(() => getStoredOrInitialMessages(config));
1148
1239
  const [isWaitingForResponse, setIsWaitingForResponse] = react.useState(() => {
1149
1240
  if (!config.userId) return false;
1150
1241
  return activeStreamStore.get(config.userId)?.isWaiting ?? false;
1151
1242
  });
1243
+ const [isUploadingAttachments, setIsUploadingAttachments] = react.useState(false);
1152
1244
  const sessionIdRef = react.useRef(
1153
1245
  getSessionIdFromMessages(getStoredOrInitialMessages(config)) ?? config.initialSessionId ?? void 0
1154
1246
  );
@@ -1223,21 +1315,45 @@ function useChatV2(config, callbacks = {}) {
1223
1315
  );
1224
1316
  const sendMessage = react.useCallback(
1225
1317
  async (userMessage, options) => {
1226
- if (!userMessage.trim()) return;
1318
+ const trimmedMessage = userMessage.trim();
1319
+ const files = options?.files ?? [];
1320
+ const hasPreuploadedAttachments = (options?.attachments?.length ?? 0) > 0;
1321
+ if (!trimmedMessage && files.length === 0 && !hasPreuploadedAttachments) return;
1322
+ let streamAttachments = options?.attachments;
1323
+ if (!streamAttachments && files.length > 0) {
1324
+ setIsUploadingAttachments(true);
1325
+ try {
1326
+ streamAttachments = await uploadAttachments(configRef.current, files);
1327
+ } catch (error) {
1328
+ if (error.name !== "AbortError") {
1329
+ callbacksRef.current.onError?.(error);
1330
+ }
1331
+ throw error;
1332
+ } finally {
1333
+ setIsUploadingAttachments(false);
1334
+ }
1335
+ }
1227
1336
  if (!sessionIdRef.current && configRef.current.autoGenerateSessionId !== false) {
1228
1337
  sessionIdRef.current = generateId();
1229
1338
  callbacksRef.current.onSessionIdChange?.(sessionIdRef.current);
1230
1339
  }
1340
+ const messageAttachments = files.length > 0 ? buildMessageAttachments(files) : streamAttachments?.length ? streamAttachments.map((attachment, index) => ({
1341
+ id: `att-${Date.now()}-${index}`,
1342
+ filename: attachment.filename,
1343
+ mimeType: attachment.mimeType,
1344
+ kind: attachment.mimeType.startsWith("image/") ? "image" : "file"
1345
+ })) : void 0;
1231
1346
  const userMessageId = `user-${Date.now()}`;
1232
1347
  const userMsg = {
1233
1348
  id: userMessageId,
1234
1349
  sessionId: sessionIdRef.current,
1235
1350
  role: "user",
1236
- content: userMessage,
1237
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1351
+ content: trimmedMessage,
1352
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1353
+ attachments: messageAttachments
1238
1354
  };
1239
1355
  setMessages((prev) => [...prev, userMsg]);
1240
- callbacksRef.current.onMessageSent?.(userMessage);
1356
+ callbacksRef.current.onMessageSent?.(trimmedMessage);
1241
1357
  setIsWaitingForResponse(true);
1242
1358
  callbacksRef.current.onStreamStart?.();
1243
1359
  const streamingId = `assistant-${Date.now()}`;
@@ -1264,11 +1380,14 @@ function useChatV2(config, callbacks = {}) {
1264
1380
  activeStreamStore.start(userId, abortController, initialMessages);
1265
1381
  }
1266
1382
  const newSessionId = await startStream(
1267
- userMessage,
1383
+ trimmedMessage,
1268
1384
  streamingId,
1269
1385
  sessionIdRef.current,
1270
1386
  abortController,
1271
- options
1387
+ {
1388
+ analysisMode: options?.analysisMode,
1389
+ attachments: streamAttachments
1390
+ }
1272
1391
  );
1273
1392
  const finalStreamUserId = streamUserIdRef.current ?? userId;
1274
1393
  if (finalStreamUserId) {
@@ -1446,6 +1565,18 @@ function useChatV2(config, callbacks = {}) {
1446
1565
  setMessages(config.initialMessages);
1447
1566
  sessionIdRef.current = getSessionIdFromMessages(config.initialMessages) ?? config.initialSessionId;
1448
1567
  }, [config.initialMessages, config.initialSessionId, config.userId]);
1568
+ const hydratedSessionIdRef = react.useRef(void 0);
1569
+ react.useEffect(() => {
1570
+ if (config.userId) return;
1571
+ if (!config.initialMessages?.length) return;
1572
+ const sessionId = config.initialSessionId;
1573
+ if (hydratedSessionIdRef.current === sessionId && messagesRef.current.length > 0) {
1574
+ return;
1575
+ }
1576
+ hydratedSessionIdRef.current = sessionId;
1577
+ setMessages(config.initialMessages);
1578
+ sessionIdRef.current = getSessionIdFromMessages(config.initialMessages) ?? sessionId;
1579
+ }, [config.initialMessages, config.initialSessionId, config.userId]);
1449
1580
  react.useEffect(() => {
1450
1581
  const prevUserId = prevUserIdRef.current;
1451
1582
  prevUserIdRef.current = config.userId;
@@ -1480,6 +1611,7 @@ function useChatV2(config, callbacks = {}) {
1480
1611
  getSessionId,
1481
1612
  getMessages,
1482
1613
  isWaitingForResponse,
1614
+ isUploadingAttachments,
1483
1615
  sessionId: sessionIdRef.current,
1484
1616
  userActionState,
1485
1617
  submitUserAction: submitUserAction2,
@@ -1703,6 +1835,102 @@ function useVoice(config = {}, callbacks = {}) {
1703
1835
  reset
1704
1836
  };
1705
1837
  }
1838
+ function useAttachmentUpload(config) {
1839
+ const configRef = react.useRef(config);
1840
+ configRef.current = config;
1841
+ const [entries, setEntries] = react.useState(/* @__PURE__ */ new Map());
1842
+ const [orderedIds, setOrderedIds] = react.useState([]);
1843
+ const abortControllersRef = react.useRef(/* @__PURE__ */ new Map());
1844
+ const activeIdsRef = react.useRef(/* @__PURE__ */ new Set());
1845
+ const startUpload = react.useCallback((id, file) => {
1846
+ if (activeIdsRef.current.has(id)) return;
1847
+ activeIdsRef.current.add(id);
1848
+ const controller = new AbortController();
1849
+ abortControllersRef.current.set(id, controller);
1850
+ setEntries((prev) => {
1851
+ const next = new Map(prev);
1852
+ next.set(id, { id, status: "uploading" });
1853
+ return next;
1854
+ });
1855
+ void uploadAttachment(configRef.current, file, controller.signal).then((payload) => {
1856
+ if (controller.signal.aborted) return;
1857
+ setEntries((prev) => {
1858
+ const next = new Map(prev);
1859
+ next.set(id, { id, status: "done", payload });
1860
+ return next;
1861
+ });
1862
+ }).catch((error) => {
1863
+ if (error.name === "AbortError") return;
1864
+ setEntries((prev) => {
1865
+ const next = new Map(prev);
1866
+ next.set(id, {
1867
+ id,
1868
+ status: "error",
1869
+ error: error.message || "Upload failed"
1870
+ });
1871
+ return next;
1872
+ });
1873
+ }).finally(() => {
1874
+ abortControllersRef.current.delete(id);
1875
+ });
1876
+ }, []);
1877
+ const syncAttachments = react.useCallback(
1878
+ (attachments) => {
1879
+ const nextIds = attachments.map((attachment) => attachment.id);
1880
+ setOrderedIds(nextIds);
1881
+ const nextIdSet = new Set(nextIds);
1882
+ for (const id of activeIdsRef.current) {
1883
+ if (nextIdSet.has(id)) continue;
1884
+ abortControllersRef.current.get(id)?.abort();
1885
+ abortControllersRef.current.delete(id);
1886
+ activeIdsRef.current.delete(id);
1887
+ }
1888
+ setEntries((prev) => {
1889
+ const next = new Map(prev);
1890
+ for (const id of next.keys()) {
1891
+ if (!nextIdSet.has(id)) next.delete(id);
1892
+ }
1893
+ return next;
1894
+ });
1895
+ for (const attachment of attachments) {
1896
+ startUpload(attachment.id, attachment.file);
1897
+ }
1898
+ },
1899
+ [startUpload]
1900
+ );
1901
+ const clearAll = react.useCallback(() => {
1902
+ for (const controller of abortControllersRef.current.values()) {
1903
+ controller.abort();
1904
+ }
1905
+ abortControllersRef.current.clear();
1906
+ activeIdsRef.current.clear();
1907
+ setOrderedIds([]);
1908
+ setEntries(/* @__PURE__ */ new Map());
1909
+ }, []);
1910
+ const entryList = react.useMemo(
1911
+ () => orderedIds.map((id) => entries.get(id)).filter((entry) => entry != null),
1912
+ [entries, orderedIds]
1913
+ );
1914
+ const isUploading = entryList.some((entry) => entry.status === "uploading");
1915
+ const hasErrors = entryList.some((entry) => entry.status === "error");
1916
+ const allReady = orderedIds.length === 0 || orderedIds.every((id) => entries.get(id)?.status === "done");
1917
+ const payloads = entryList.filter((entry) => entry.status === "done" && entry.payload).map((entry) => entry.payload);
1918
+ const statusById = react.useMemo(
1919
+ () => Object.fromEntries(
1920
+ entryList.map((entry) => [entry.id, entry.status])
1921
+ ),
1922
+ [entryList]
1923
+ );
1924
+ return {
1925
+ syncAttachments,
1926
+ clearAll,
1927
+ isUploading,
1928
+ hasErrors,
1929
+ allReady,
1930
+ payloads,
1931
+ statusById
1932
+ };
1933
+ }
1706
1934
  function classifyField(field) {
1707
1935
  if (!field) return "text";
1708
1936
  if (Array.isArray(field.oneOf) && field.oneOf.length > 0) return "select";
@@ -1819,6 +2047,66 @@ function buildContent(schema, values) {
1819
2047
  }
1820
2048
  return content;
1821
2049
  }
2050
+ var ATTACHMENTS_SUFFIX_RE = /\n\n\[Attachments:[^\]]*\]\s*$/;
2051
+ function stripAttachmentsSuffixFromIntent(intent) {
2052
+ return intent.replace(ATTACHMENTS_SUFFIX_RE, "").trimEnd();
2053
+ }
2054
+ function mapFeedback(feedback) {
2055
+ if (!feedback?.feedback) return null;
2056
+ return feedback.feedback === "POSITIVE" ? "up" : "down";
2057
+ }
2058
+ function mapHistoryAttachments(executionId, attachments) {
2059
+ const mapped = (attachments ?? []).map((attachment, index) => ({
2060
+ id: `${executionId}:att:${index}`,
2061
+ filename: attachment.filename,
2062
+ mimeType: attachment.mimeType,
2063
+ url: attachment.url ?? void 0,
2064
+ kind: attachment.mimeType.startsWith("image/") ? "image" : "file"
2065
+ }));
2066
+ return mapped.length > 0 ? mapped : void 0;
2067
+ }
2068
+ function mapExecutionHistoryToChatMessages(message) {
2069
+ const timestamp = message.startTime || message.endTime || (/* @__PURE__ */ new Date()).toISOString();
2070
+ const executionId = message.executionId ?? message.traceId ?? message.id;
2071
+ const attachments = mapHistoryAttachments(message.id, message.attachments);
2072
+ const rows = [
2073
+ {
2074
+ id: `${message.id}:user`,
2075
+ role: "user",
2076
+ content: stripAttachmentsSuffixFromIntent(message.sessionUserIntent),
2077
+ timestamp,
2078
+ attachments
2079
+ }
2080
+ ];
2081
+ if (message.agentResponse) {
2082
+ rows.push({
2083
+ id: `${message.id}:assistant`,
2084
+ role: "assistant",
2085
+ content: message.agentResponse,
2086
+ timestamp: message.endTime || timestamp,
2087
+ isError: message.status === "FAILED",
2088
+ executionId,
2089
+ feedback: mapFeedback(message.feedback)
2090
+ });
2091
+ }
2092
+ return rows;
2093
+ }
2094
+ function mapExecutionHistoryPageToChatMessages(messages) {
2095
+ return messages.flatMap(mapExecutionHistoryToChatMessages);
2096
+ }
2097
+ function attachmentDisplayUrl(attachment) {
2098
+ return attachment.previewUrl ?? attachment.url;
2099
+ }
2100
+ function isImageAttachment(attachment) {
2101
+ const displayUrl = attachmentDisplayUrl(attachment);
2102
+ return attachment.kind === "image" || Boolean(displayUrl) && attachment.mimeType.startsWith("image/");
2103
+ }
2104
+ function isPdfAttachmentMeta(attachment) {
2105
+ return attachment.mimeType === "application/pdf" || /\.pdf$/i.test(attachment.filename);
2106
+ }
2107
+ function isPdfFile(file) {
2108
+ return file.type === "application/pdf" || /\.pdf$/i.test(file.name);
2109
+ }
1822
2110
  var PaymanChatContext = react.createContext(void 0);
1823
2111
  function usePaymanChat() {
1824
2112
  const context = react.useContext(PaymanChatContext);
@@ -1929,6 +2217,111 @@ function subscribeToCfRay(urlPattern, listener) {
1929
2217
  };
1930
2218
  }
1931
2219
 
2220
+ // src/utils/attachmentConfig.ts
2221
+ var DEFAULT_IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp"];
2222
+ var DEFAULT_DOCUMENT_EXTENSIONS = ["pdf", "docx", "xlsx", "xls"];
2223
+ function resolveChatAttachmentConfig(config) {
2224
+ const nested = config.attachments;
2225
+ const enabled = nested?.enabled ?? config.showAttachmentButton ?? true;
2226
+ const uploadImage = nested?.uploadImage ?? config.showUploadImageButton ?? true;
2227
+ const attachFile = nested?.attachFile ?? config.showAttachFileButton ?? true;
2228
+ return {
2229
+ showAttachmentButton: enabled && (uploadImage || attachFile),
2230
+ showUploadImageButton: enabled && uploadImage,
2231
+ showAttachFileButton: enabled && attachFile,
2232
+ maxCount: nested?.maxCount ?? nested?.maxImages ?? nested?.maxDocuments,
2233
+ maxFileBytes: nested?.maxFileBytes,
2234
+ maxTotalBytes: nested?.maxTotalBytes,
2235
+ allowedImageExtensions: nested?.imageExtensions ?? DEFAULT_IMAGE_EXTENSIONS,
2236
+ allowedFileExtensions: nested?.documentExtensions ?? DEFAULT_DOCUMENT_EXTENSIONS
2237
+ };
2238
+ }
2239
+
2240
+ // src/utils/formatAttachmentBytes.ts
2241
+ function formatAttachmentBytes(bytes) {
2242
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
2243
+ const units = ["B", "KB", "MB", "GB"];
2244
+ let value = bytes;
2245
+ let unitIndex = 0;
2246
+ while (value >= 1024 && unitIndex < units.length - 1) {
2247
+ value /= 1024;
2248
+ unitIndex += 1;
2249
+ }
2250
+ const rounded = unitIndex === 0 ? String(Math.round(value)) : value >= 10 ? value.toFixed(0) : value.toFixed(1).replace(/\.0$/, "");
2251
+ return `${rounded} ${units[unitIndex]}`;
2252
+ }
2253
+
2254
+ // src/utils/pdfLink.ts
2255
+ function filenameFromContentDisposition(value) {
2256
+ if (!value) return void 0;
2257
+ let decoded = value;
2258
+ try {
2259
+ decoded = decodeURIComponent(value);
2260
+ } catch {
2261
+ decoded = value;
2262
+ }
2263
+ const quoted = decoded.match(/filename\*=(?:UTF-8''([^;]+)|"([^"]+)")/i);
2264
+ if (quoted?.[1] || quoted?.[2]) {
2265
+ return (quoted[1] ?? quoted[2])?.trim();
2266
+ }
2267
+ const unquoted = decoded.match(/filename=([^;]+)/i);
2268
+ return unquoted?.[1]?.replace(/"/g, "").trim();
2269
+ }
2270
+ function filenameFromUrlPath(href) {
2271
+ try {
2272
+ const parts = new URL(href).pathname.split("/").filter(Boolean);
2273
+ const last = parts[parts.length - 1];
2274
+ return last ? decodeURIComponent(last) : void 0;
2275
+ } catch {
2276
+ return void 0;
2277
+ }
2278
+ }
2279
+ function isPdfUrl(href) {
2280
+ if (!href) return false;
2281
+ const lower = href.toLowerCase();
2282
+ if (lower.includes(".pdf")) return true;
2283
+ try {
2284
+ const url = new URL(href);
2285
+ if (url.pathname.toLowerCase().endsWith(".pdf")) return true;
2286
+ const filename = url.searchParams.get("filename");
2287
+ if (filename?.toLowerCase().includes(".pdf")) return true;
2288
+ for (const key of ["rscd", "response-content-disposition"]) {
2289
+ const fromDisposition = filenameFromContentDisposition(
2290
+ url.searchParams.get(key)
2291
+ );
2292
+ if (fromDisposition?.toLowerCase().includes(".pdf")) return true;
2293
+ }
2294
+ } catch {
2295
+ return lower.endsWith(".pdf");
2296
+ }
2297
+ return false;
2298
+ }
2299
+ function getPdfTitleFromUrl(href, linkText) {
2300
+ const text = linkText?.trim();
2301
+ if (text) return text;
2302
+ try {
2303
+ const url = new URL(href);
2304
+ const filename = url.searchParams.get("filename");
2305
+ if (filename) {
2306
+ return filename.replace(/\.pdf$/i, "").replace(/[-_]+/g, " ").trim();
2307
+ }
2308
+ for (const key of ["rscd", "response-content-disposition"]) {
2309
+ const fromDisposition = filenameFromContentDisposition(
2310
+ url.searchParams.get(key)
2311
+ );
2312
+ if (fromDisposition) {
2313
+ return fromDisposition.replace(/\.pdf$/i, "").replace(/[-_]+/g, " ").trim();
2314
+ }
2315
+ }
2316
+ } catch {
2317
+ }
2318
+ const fromPath = filenameFromUrlPath(href);
2319
+ if (fromPath) {
2320
+ return fromPath.replace(/\.pdf$/i, "").replace(/[-_]+/g, " ").trim();
2321
+ }
2322
+ return "Document";
2323
+ }
2324
+
1932
2325
  // src/utils/slashCommands.ts
1933
2326
  var DEFAULT_SLASH_COMMANDS = [
1934
2327
  {
@@ -3080,6 +3473,108 @@ function ActionTooltipV2({ label, children }) {
3080
3473
  ] }) })
3081
3474
  ] });
3082
3475
  }
3476
+ function FilePreviewShell({
3477
+ filename,
3478
+ typeLabel,
3479
+ onClick,
3480
+ thumbnail,
3481
+ className,
3482
+ "aria-label": ariaLabel
3483
+ }) {
3484
+ const shellClass = cn(
3485
+ "payman-v2-file-preview-shell",
3486
+ "payman-v2-file-preview-shell-sent",
3487
+ onClick && "payman-v2-file-preview-shell-clickable",
3488
+ className
3489
+ );
3490
+ const body = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3491
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-file-preview-thumb", children: thumbnail ?? /* @__PURE__ */ jsxRuntime.jsx(
3492
+ lucideReact.FileText,
3493
+ {
3494
+ size: 16,
3495
+ strokeWidth: 1.75,
3496
+ className: "payman-v2-file-preview-doc-icon"
3497
+ }
3498
+ ) }),
3499
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-file-preview-info", children: [
3500
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "payman-v2-file-preview-name", children: filename }),
3501
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "payman-v2-file-preview-type", children: typeLabel })
3502
+ ] })
3503
+ ] });
3504
+ if (onClick) {
3505
+ return /* @__PURE__ */ jsxRuntime.jsx(
3506
+ "button",
3507
+ {
3508
+ type: "button",
3509
+ className: shellClass,
3510
+ onClick,
3511
+ "aria-label": ariaLabel ?? `Open ${filename}`,
3512
+ children: body
3513
+ }
3514
+ );
3515
+ }
3516
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: shellClass, children: body });
3517
+ }
3518
+ function FilePreviewBlockLayout({
3519
+ children,
3520
+ className,
3521
+ ...rest
3522
+ }) {
3523
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("payman-v2-file-preview-item", className), ...rest, children });
3524
+ }
3525
+ function attachmentTypeLabel(attachment) {
3526
+ if (isImageAttachment(attachment)) {
3527
+ return "Image";
3528
+ }
3529
+ const ext = attachment.filename.split(".").pop()?.toUpperCase();
3530
+ return ext || "Document";
3531
+ }
3532
+ function isPdfAttachment(attachment) {
3533
+ const displayUrl = attachmentDisplayUrl(attachment);
3534
+ return isPdfAttachmentMeta(attachment) || Boolean(displayUrl) && isPdfUrl(displayUrl);
3535
+ }
3536
+ function AttachmentPreviewBlock({
3537
+ attachment,
3538
+ onImageClick
3539
+ }) {
3540
+ const chatContext = react.useContext(PaymanChatContext);
3541
+ const displayUrl = attachmentDisplayUrl(attachment);
3542
+ const typeLabel = attachmentTypeLabel(attachment);
3543
+ const isImage = isImageAttachment(attachment) && displayUrl;
3544
+ const openAttachment = () => {
3545
+ if (!displayUrl) return;
3546
+ if (isImageAttachment(attachment)) {
3547
+ onImageClick?.(displayUrl, attachment.filename);
3548
+ return;
3549
+ }
3550
+ if (isPdfAttachment(attachment)) {
3551
+ chatContext?.openPdfSheet(displayUrl, attachment.filename);
3552
+ return;
3553
+ }
3554
+ window.open(displayUrl, "_blank", "noopener,noreferrer");
3555
+ };
3556
+ if (isImage) {
3557
+ return /* @__PURE__ */ jsxRuntime.jsx(FilePreviewBlockLayout, { children: /* @__PURE__ */ jsxRuntime.jsx(
3558
+ FilePreviewShell,
3559
+ {
3560
+ filename: attachment.filename,
3561
+ typeLabel,
3562
+ onClick: openAttachment,
3563
+ "aria-label": `Preview ${attachment.filename}`,
3564
+ thumbnail: /* @__PURE__ */ jsxRuntime.jsx("img", { src: displayUrl, alt: "", draggable: false })
3565
+ }
3566
+ ) });
3567
+ }
3568
+ return /* @__PURE__ */ jsxRuntime.jsx(FilePreviewBlockLayout, { children: /* @__PURE__ */ jsxRuntime.jsx(
3569
+ FilePreviewShell,
3570
+ {
3571
+ filename: attachment.filename,
3572
+ typeLabel,
3573
+ onClick: displayUrl ? openAttachment : void 0,
3574
+ "aria-label": `Open ${attachment.filename}`
3575
+ }
3576
+ ) });
3577
+ }
3083
3578
  function formatMessageTime(timestamp) {
3084
3579
  const value = new Date(timestamp);
3085
3580
  if (Number.isNaN(value.getTime())) return "";
@@ -3092,6 +3587,7 @@ function UserMessageV2({
3092
3587
  message,
3093
3588
  onEdit,
3094
3589
  onRetry,
3590
+ onImageClick,
3095
3591
  retryDisabled = false,
3096
3592
  actions
3097
3593
  }) {
@@ -3174,7 +3670,15 @@ function UserMessageV2({
3174
3670
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3175
3671
  toastPortal,
3176
3672
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-user-msg payman-v2-fade-in", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-user-msg-group", children: [
3177
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-user-msg-bubble", children: parsedCommand ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-user-msg-command", children: [
3673
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-file-preview payman-v2-user-msg-attachments", children: message.attachments.map((attachment) => /* @__PURE__ */ jsxRuntime.jsx(
3674
+ AttachmentPreviewBlock,
3675
+ {
3676
+ attachment,
3677
+ onImageClick
3678
+ },
3679
+ attachment.id
3680
+ )) }),
3681
+ (message.content.trim().length > 0 || !message.attachments?.length) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-user-msg-bubble", children: parsedCommand ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-user-msg-command", children: [
3178
3682
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "payman-v2-user-msg-command-chip", children: parsedCommand.command }),
3179
3683
  parsedCommand.body.trim() ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-user-msg-text", children: parsedCommand.body }) : null
3180
3684
  ] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-user-msg-text", children: message.content }) }),
@@ -3399,9 +3903,44 @@ function MarkdownImageV2({
3399
3903
  }
3400
3904
  ) });
3401
3905
  }
3402
- function buildComponents(onImageClick, isResolvingRef) {
3906
+ function PdfBlockV2({ title, href, onOpen, autoOpen = false }) {
3907
+ const autoOpenedHrefRef = react.useRef(null);
3908
+ react.useEffect(() => {
3909
+ if (!autoOpen || autoOpenedHrefRef.current === href) return;
3910
+ autoOpenedHrefRef.current = href;
3911
+ onOpen(href, title, { auto: true });
3912
+ }, [href, autoOpen]);
3913
+ return /* @__PURE__ */ jsxRuntime.jsx(FilePreviewBlockLayout, { "data-payman-file-block": true, children: /* @__PURE__ */ jsxRuntime.jsx(
3914
+ FilePreviewShell,
3915
+ {
3916
+ filename: title,
3917
+ typeLabel: "PDF",
3918
+ onClick: () => onOpen(href, title),
3919
+ "aria-label": `Open PDF: ${title}`
3920
+ }
3921
+ ) });
3922
+ }
3923
+ function childrenToText(children) {
3924
+ if (typeof children === "string") return children;
3925
+ if (typeof children === "number") return String(children);
3926
+ if (Array.isArray(children)) return children.map(childrenToText).join("");
3927
+ if (children && typeof children === "object" && "props" in children) {
3928
+ return childrenToText(children.props.children);
3929
+ }
3930
+ return "";
3931
+ }
3932
+ function isFileBlockChild(child) {
3933
+ return react.isValidElement(child) && typeof child.props === "object" && child.props !== null && "data-payman-file-block" in child.props;
3934
+ }
3935
+ function buildComponents(onImageClick, isResolvingRef, onPdfClick, autoOpenPdf) {
3403
3936
  return {
3404
- p: ({ children }) => /* @__PURE__ */ jsxRuntime.jsx("p", { children }),
3937
+ p: ({ children }) => {
3938
+ const childArray = react.Children.toArray(children);
3939
+ if (childArray.length === 1 && isFileBlockChild(childArray[0])) {
3940
+ return childArray[0];
3941
+ }
3942
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { children });
3943
+ },
3405
3944
  code: ({ children }) => /* @__PURE__ */ jsxRuntime.jsx("code", { children }),
3406
3945
  pre: ({ children }) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-markdown-pre", children: /* @__PURE__ */ jsxRuntime.jsx("pre", { children }) }),
3407
3946
  ul: ({ children }) => /* @__PURE__ */ jsxRuntime.jsx("ul", { children }),
@@ -3414,7 +3953,15 @@ function buildComponents(onImageClick, isResolvingRef) {
3414
3953
  em: ({ children }) => /* @__PURE__ */ jsxRuntime.jsx("em", { children }),
3415
3954
  blockquote: ({ children }) => /* @__PURE__ */ jsxRuntime.jsx("blockquote", { children }),
3416
3955
  hr: () => /* @__PURE__ */ jsxRuntime.jsx("hr", {}),
3417
- a: ({ href, children }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href, target: "_blank", rel: "noopener noreferrer", children }),
3956
+ a: ({ href, children }) => {
3957
+ const url = href ?? "";
3958
+ if (onPdfClick && isPdfUrl(url)) {
3959
+ const linkText = childrenToText(children).trim();
3960
+ const title = getPdfTitleFromUrl(url, linkText);
3961
+ return /* @__PURE__ */ jsxRuntime.jsx(PdfBlockV2, { href: url, title, onOpen: onPdfClick, autoOpen: autoOpenPdf });
3962
+ }
3963
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { href, target: "_blank", rel: "noopener noreferrer", children });
3964
+ },
3418
3965
  img: ({ src, alt }) => /* @__PURE__ */ jsxRuntime.jsx(
3419
3966
  MarkdownImageV2,
3420
3967
  {
@@ -3435,13 +3982,15 @@ function MarkdownRendererV2({
3435
3982
  content,
3436
3983
  isStreaming,
3437
3984
  isResolvingImages,
3438
- onImageClick
3985
+ onImageClick,
3986
+ onPdfClick,
3987
+ autoOpenPdf
3439
3988
  }) {
3440
3989
  const isResolvingRef = react.useRef(isResolvingImages);
3441
3990
  isResolvingRef.current = isResolvingImages;
3442
3991
  const components = react.useMemo(
3443
- () => buildComponents(onImageClick, isResolvingRef),
3444
- [onImageClick]
3992
+ () => buildComponents(onImageClick, isResolvingRef, onPdfClick, autoOpenPdf),
3993
+ [onImageClick, onPdfClick, autoOpenPdf]
3445
3994
  );
3446
3995
  return /* @__PURE__ */ jsxRuntime.jsx(
3447
3996
  "div",
@@ -3810,6 +4359,17 @@ function stripIncompleteImageToken(text) {
3810
4359
  if (/^!\[[^\]]*\]\([^)]*\)/.test(after)) return text;
3811
4360
  return text.slice(0, lastBang);
3812
4361
  }
4362
+ function stripIncompleteLinkToken(text) {
4363
+ const lastBracket = text.lastIndexOf("[");
4364
+ if (lastBracket === -1) return text;
4365
+ if (lastBracket > 0 && text[lastBracket - 1] === "!") return text;
4366
+ const after = text.slice(lastBracket);
4367
+ if (/^\[[^\]]*\]\([^)]*\)/.test(after)) return text;
4368
+ return text.slice(0, lastBracket);
4369
+ }
4370
+ function stripIncompleteMarkdownTokens(text) {
4371
+ return stripIncompleteLinkToken(stripIncompleteImageToken(text));
4372
+ }
3813
4373
  function getFeedbackState(message) {
3814
4374
  const feedback = message.feedback;
3815
4375
  if (feedback === "up" || feedback === "down") return feedback;
@@ -3833,6 +4393,12 @@ function AssistantMessageV2({
3833
4393
  () => getFeedbackState(message)
3834
4394
  );
3835
4395
  const [reasonModalOpen, setReasonModalOpen] = react.useState(false);
4396
+ const chatContext = react.useContext(PaymanChatContext);
4397
+ const chatContextRef = react.useRef(chatContext);
4398
+ chatContextRef.current = chatContext;
4399
+ const handlePdfClick = react.useCallback((href, title, options) => {
4400
+ chatContextRef.current?.openPdfSheet(href, title, options);
4401
+ }, []);
3836
4402
  const canSubmitFeedback = !!onSubmitFeedback && !!message.executionId;
3837
4403
  const [toast, setToast] = react.useState(null);
3838
4404
  const copyResetTimerRef = react.useRef(null);
@@ -3857,7 +4423,7 @@ function AssistantMessageV2({
3857
4423
  const raw = message.isStreaming ? message.streamingContent || message.content : message.content;
3858
4424
  if (!raw) return "";
3859
4425
  const normalized = raw.replace(/\\n/g, "\n");
3860
- return message.isStreaming ? stripIncompleteImageToken(normalized) : normalized;
4426
+ return message.isStreaming ? stripIncompleteMarkdownTokens(normalized) : normalized;
3861
4427
  })();
3862
4428
  const isThinkingStreaming = !!message.isStreaming && !rawResponseContent && !message.isError;
3863
4429
  const responseTypingEnabled = hasEverStreamed.current && Boolean(rawResponseContent) && !message.isError;
@@ -4027,10 +4593,12 @@ function AssistantMessageV2({
4027
4593
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-assistant-msg-content-area", children: displayContent ? /* @__PURE__ */ jsxRuntime.jsx(
4028
4594
  MarkdownRendererV2,
4029
4595
  {
4030
- content: displayContent,
4596
+ content: message.isStreaming && !isCancelled || isResponseTyping ? stripIncompleteMarkdownTokens(displayContent) : displayContent,
4031
4597
  isStreaming: message.isStreaming && !isCancelled || isResponseTyping,
4032
4598
  isResolvingImages: message.isResolvingImages,
4033
- onImageClick
4599
+ onImageClick,
4600
+ onPdfClick: handlePdfClick,
4601
+ autoOpenPdf: hasEverStreamed.current
4034
4602
  }
4035
4603
  ) : !isThinkingStreaming ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "payman-v2-assistant-msg-placeholder", children: "..." }) : null }),
4036
4604
  isCancelled && message.isStreaming && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-assistant-msg-paused", children: [
@@ -4247,7 +4815,6 @@ function VerificationInline({
4247
4815
  const [code, setCode] = react.useState("");
4248
4816
  const [errored, setErrored] = react.useState(false);
4249
4817
  const [resendSec, setResendSec] = react.useState(0);
4250
- const [hiddenAfterResend, setHiddenAfterResend] = react.useState(false);
4251
4818
  const lastSubmittedRef = react.useRef(null);
4252
4819
  const resendTimerRef = react.useRef(void 0);
4253
4820
  const status = prompt.status;
@@ -4261,9 +4828,6 @@ function VerificationInline({
4261
4828
  lastSubmittedRef.current = null;
4262
4829
  }
4263
4830
  }, [prompt.subAction, prompt.userActionId]);
4264
- react.useEffect(() => {
4265
- setHiddenAfterResend(false);
4266
- }, [prompt.expirySeconds, prompt.message, prompt.subAction, prompt.userActionId]);
4267
4831
  react.useEffect(() => {
4268
4832
  if (code.length < codeLen) lastSubmittedRef.current = null;
4269
4833
  }, [code, codeLen]);
@@ -4314,13 +4878,11 @@ function VerificationInline({
4314
4878
  if (locked || resendSec > 0) return;
4315
4879
  setErrored(false);
4316
4880
  setCode("");
4317
- setHiddenAfterResend(true);
4318
4881
  lastSubmittedRef.current = null;
4319
4882
  try {
4320
4883
  await onResend(prompt.userActionId);
4321
4884
  startResendCooldown();
4322
4885
  } catch {
4323
- setHiddenAfterResend(false);
4324
4886
  }
4325
4887
  }, [locked, onResend, prompt.userActionId, resendSec, startResendCooldown]);
4326
4888
  const handleCancel = react.useCallback(() => {
@@ -4328,7 +4890,6 @@ function VerificationInline({
4328
4890
  void onCancel(prompt.userActionId);
4329
4891
  }, [busy, onCancel, prompt.userActionId]);
4330
4892
  const description = prompt.message?.trim() || (isNumeric ? `Enter the ${codeLen}-digit code to continue` : "Enter the verification code to continue");
4331
- if (hiddenAfterResend) return null;
4332
4893
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-ua", role: "group", "aria-label": "Verification required", children: [
4333
4894
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-ua-head", children: [
4334
4895
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ShieldCheck, { className: "payman-v2-ua-icon", size: 15, strokeWidth: 1.75, "aria-hidden": true }),
@@ -4424,9 +4985,6 @@ function SchemaFormInline({
4424
4985
  const busy = status === "submitting";
4425
4986
  const stale = status === "stale";
4426
4987
  const locked = busy || stale || expired;
4427
- const isUserConfirmation = prompt.subAction === "UserConfirmation";
4428
- const messageFormat = prompt.metadata?.["payman/messageFormat"];
4429
- const renderMarkdown = messageFormat === "markdown";
4430
4988
  const setValue = (key, value) => {
4431
4989
  setValues((prev) => ({ ...prev, [key]: value }));
4432
4990
  setErrors((prev) => {
@@ -4452,8 +5010,8 @@ function SchemaFormInline({
4452
5010
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "payman-v2-ua-title", children: "Action required" }),
4453
5011
  typeof secondsLeft === "number" && !stale && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "payman-v2-ua-timer", children: expired ? "Expired" : `${secondsLeft}s` })
4454
5012
  ] }),
4455
- prompt.message?.trim() && (renderMarkdown ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-ua-markdown", children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownRendererV2, { content: prompt.message }) }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-ua-desc", children: prompt.message })),
4456
- stale ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-ua-stale", children: "This request is no longer available." }) : fields.length === 0 ? null : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-ua-form", children: fields.map(([key, field]) => {
5013
+ prompt.message?.trim() && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-ua-desc", children: prompt.message }),
5014
+ stale ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-ua-stale", children: "This request is no longer available." }) : fields.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-ua-desc", children: "This action has no inputs to fill." }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-ua-form", children: fields.map(([key, field]) => {
4457
5015
  const widget = classifyField(field);
4458
5016
  const label = field.title || key;
4459
5017
  const required = isRequired(schema, key);
@@ -4526,7 +5084,7 @@ function SchemaFormInline({
4526
5084
  className: "payman-v2-ua-btn payman-v2-ua-btn-primary",
4527
5085
  disabled: locked,
4528
5086
  onClick: handleSubmit,
4529
- children: busy ? "Submitting\u2026" : isUserConfirmation ? "Confirm" : "Submit"
5087
+ children: busy ? "Submitting\u2026" : "Submit"
4530
5088
  }
4531
5089
  ),
4532
5090
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4582,25 +5140,10 @@ function useExpiryCountdown(prompt) {
4582
5140
  const expired = initial !== void 0 && secondsLeft === 0;
4583
5141
  return [secondsLeft, expired];
4584
5142
  }
4585
- function UserActionInline({
4586
- prompt,
4587
- onSubmit,
4588
- onCancel,
4589
- onResend,
4590
- onExpired
4591
- }) {
5143
+ function UserActionInline({ prompt, onSubmit, onCancel, onResend }) {
4592
5144
  const [secondsLeft, expired] = useExpiryCountdown(prompt);
4593
- react.useEffect(() => {
4594
- if (expired && prompt.kind !== "notification") onExpired?.();
4595
- }, [expired, onExpired, prompt.kind]);
4596
5145
  let body;
4597
- if (expired && prompt.kind !== "notification") {
4598
- const note = {
4599
- id: `${prompt.userActionId}-expired`,
4600
- message: prompt.kind === "verification" ? "Verification Request Expired" : "User Form Expired"
4601
- };
4602
- body = /* @__PURE__ */ jsxRuntime.jsx(NotificationInline, { notification: note });
4603
- } else if (prompt.kind === "verification") {
5146
+ if (prompt.kind === "verification") {
4604
5147
  body = /* @__PURE__ */ jsxRuntime.jsx(
4605
5148
  VerificationInline,
4606
5149
  {
@@ -4640,23 +5183,10 @@ function UserActionInline({
4640
5183
  }
4641
5184
  var SCROLL_THRESHOLD2 = 100;
4642
5185
  var USER_SCROLL_UP_EPSILON = 4;
4643
- var PROMPT_KEY_SEPARATOR = "";
4644
- function getPromptSlotKey(prompt) {
4645
- return prompt.toolCallId || prompt.userActionId;
4646
- }
4647
- function getPromptViewKey(prompt) {
4648
- return [
4649
- getPromptSlotKey(prompt),
4650
- prompt.userActionId,
4651
- prompt.subAction ?? "",
4652
- prompt.expirySeconds ?? "",
4653
- prompt.message ?? ""
4654
- ].join(PROMPT_KEY_SEPARATOR);
4655
- }
4656
5186
  var MessageListV2 = react.forwardRef(
4657
5187
  function MessageListV22({
4658
5188
  messages,
4659
- isStreaming = false,
5189
+ isStreaming: _isStreaming = false,
4660
5190
  onEditUserMessage,
4661
5191
  onRetryUserMessage,
4662
5192
  onImageClick,
@@ -4670,7 +5200,8 @@ var MessageListV2 = react.forwardRef(
4670
5200
  onResendUserAction,
4671
5201
  onDismissNotification,
4672
5202
  onSubmitFeedback,
4673
- typingSpeed = 4
5203
+ typingSpeed = 4,
5204
+ sidePanelOpen = false
4674
5205
  }, ref) {
4675
5206
  const noop = react.useCallback(async () => {
4676
5207
  }, []);
@@ -4678,10 +5209,11 @@ var MessageListV2 = react.forwardRef(
4678
5209
  const scrollInnerRef = react.useRef(null);
4679
5210
  const isNearBottomRef = react.useRef(true);
4680
5211
  const [showScrollBtn, setShowScrollBtn] = react.useState(false);
4681
- const [expiredPromptViewState, setExpiredPromptViewState] = react.useState({});
4682
5212
  const prevCountRef = react.useRef(messages.length);
4683
5213
  const pauseStickUntilUserMessageRef = react.useRef(false);
4684
5214
  const followingBottomRef = react.useRef(true);
5215
+ const sidePanelOpenRef = react.useRef(sidePanelOpen);
5216
+ sidePanelOpenRef.current = sidePanelOpen;
4685
5217
  const lastPinAtRef = react.useRef(0);
4686
5218
  const prevScrollTopRef = react.useRef(0);
4687
5219
  const getDistanceFromBottom = react.useCallback(() => {
@@ -4689,81 +5221,6 @@ var MessageListV2 = react.forwardRef(
4689
5221
  if (!el) return 0;
4690
5222
  return el.scrollHeight - el.scrollTop - el.clientHeight;
4691
5223
  }, []);
4692
- const messageActivityFingerprint = react.useMemo(() => {
4693
- const last = messages[messages.length - 1];
4694
- const promptFingerprint = (userActionPrompts ?? []).map((prompt) => `${getPromptViewKey(prompt)}:${prompt.status}`).join(PROMPT_KEY_SEPARATOR);
4695
- const notificationFingerprint = (notifications ?? []).map((notification) => notification.id).join(PROMPT_KEY_SEPARATOR);
4696
- if (!last) {
4697
- return [
4698
- 0,
4699
- isStreaming ? "streaming" : "idle",
4700
- promptFingerprint,
4701
- notificationFingerprint
4702
- ].join(PROMPT_KEY_SEPARATOR);
4703
- }
4704
- return [
4705
- messages.length,
4706
- isStreaming ? "streaming" : "idle",
4707
- last.id,
4708
- last.role,
4709
- last.content ?? "",
4710
- last.isStreaming ? "streaming" : "done",
4711
- last.streamProgress ?? "",
4712
- last.steps?.length ?? 0,
4713
- last.errorDetails ?? "",
4714
- promptFingerprint,
4715
- notificationFingerprint
4716
- ].join(PROMPT_KEY_SEPARATOR);
4717
- }, [isStreaming, messages, notifications, userActionPrompts]);
4718
- const handleUserActionExpired = react.useCallback(
4719
- (promptKey) => {
4720
- setExpiredPromptViewState((prev) => {
4721
- if (prev[promptKey]) return prev;
4722
- return {
4723
- ...prev,
4724
- [promptKey]: { baseline: messageActivityFingerprint, hidden: false }
4725
- };
4726
- });
4727
- },
4728
- [messageActivityFingerprint]
4729
- );
4730
- react.useEffect(() => {
4731
- setExpiredPromptViewState((prev) => {
4732
- let changed = false;
4733
- const next = {};
4734
- for (const [key, state] of Object.entries(prev)) {
4735
- if (!state.hidden && state.baseline !== messageActivityFingerprint) {
4736
- next[key] = { ...state, hidden: true };
4737
- changed = true;
4738
- } else {
4739
- next[key] = state;
4740
- }
4741
- }
4742
- return changed ? next : prev;
4743
- });
4744
- }, [messageActivityFingerprint]);
4745
- react.useEffect(() => {
4746
- const livePromptKeys = new Set((userActionPrompts ?? []).map(getPromptViewKey));
4747
- setExpiredPromptViewState((prev) => {
4748
- let changed = false;
4749
- const next = {};
4750
- for (const [key, state] of Object.entries(prev)) {
4751
- if (livePromptKeys.has(key)) {
4752
- next[key] = state;
4753
- } else {
4754
- changed = true;
4755
- }
4756
- }
4757
- return changed ? next : prev;
4758
- });
4759
- }, [userActionPrompts]);
4760
- const visibleUserActionPrompts = react.useMemo(
4761
- () => userActionPrompts?.filter((prompt) => {
4762
- const promptKey = getPromptViewKey(prompt);
4763
- return !expiredPromptViewState[promptKey]?.hidden;
4764
- }),
4765
- [expiredPromptViewState, userActionPrompts]
4766
- );
4767
5224
  const pinToBottom = react.useCallback((behavior = "instant") => {
4768
5225
  const el = scrollRef.current;
4769
5226
  if (!el) return;
@@ -4804,17 +5261,25 @@ var MessageListV2 = react.forwardRef(
4804
5261
  const nearBottom = distance <= SCROLL_THRESHOLD2;
4805
5262
  isNearBottomRef.current = nearBottom;
4806
5263
  setShowScrollBtn(!nearBottom);
4807
- const sincePin = performance.now() - lastPinAtRef.current;
4808
- if (sincePin < 250) return;
4809
5264
  const scrolledUp = currentScrollTop < prevScrollTop - USER_SCROLL_UP_EPSILON;
4810
5265
  if (scrolledUp) {
4811
5266
  followingBottomRef.current = false;
4812
5267
  pauseStickUntilUserMessageRef.current = true;
4813
- } else if (nearBottom) {
5268
+ return;
5269
+ }
5270
+ const sincePin = performance.now() - lastPinAtRef.current;
5271
+ if (sincePin < 250) return;
5272
+ if (nearBottom) {
4814
5273
  followingBottomRef.current = true;
4815
5274
  pauseStickUntilUserMessageRef.current = false;
4816
5275
  }
4817
5276
  }, [getDistanceFromBottom]);
5277
+ const handleWheel = react.useCallback((e) => {
5278
+ if (e.deltaY < 0) {
5279
+ followingBottomRef.current = false;
5280
+ pauseStickUntilUserMessageRef.current = true;
5281
+ }
5282
+ }, []);
4818
5283
  react.useEffect(() => {
4819
5284
  const prevCount = prevCountRef.current;
4820
5285
  prevCountRef.current = messages.length;
@@ -4824,7 +5289,7 @@ var MessageListV2 = react.forwardRef(
4824
5289
  pauseStickUntilUserMessageRef.current = false;
4825
5290
  followingBottomRef.current = true;
4826
5291
  requestAnimationFrame(() => scrollToBottom());
4827
- } else if (!pauseStickUntilUserMessageRef.current && followingBottomRef.current) {
5292
+ } else if (!sidePanelOpenRef.current && !pauseStickUntilUserMessageRef.current && followingBottomRef.current) {
4828
5293
  requestAnimationFrame(() => scrollToBottom("instant"));
4829
5294
  }
4830
5295
  }
@@ -4833,27 +5298,16 @@ var MessageListV2 = react.forwardRef(
4833
5298
  const inner = scrollInnerRef.current;
4834
5299
  if (!inner) return;
4835
5300
  const pinIfFollowing = () => {
5301
+ if (sidePanelOpenRef.current) return;
4836
5302
  if (pauseStickUntilUserMessageRef.current) return;
4837
5303
  if (!followingBottomRef.current) return;
5304
+ if (getDistanceFromBottom() > SCROLL_THRESHOLD2) return;
4838
5305
  pinToBottom("instant");
4839
5306
  };
4840
- const ro = new ResizeObserver(() => {
4841
- pinIfFollowing();
4842
- });
5307
+ const ro = new ResizeObserver(pinIfFollowing);
4843
5308
  ro.observe(inner);
4844
- const mo = new MutationObserver(() => {
4845
- pinIfFollowing();
4846
- });
4847
- mo.observe(inner, {
4848
- childList: true,
4849
- subtree: true,
4850
- characterData: true
4851
- });
4852
- return () => {
4853
- ro.disconnect();
4854
- mo.disconnect();
4855
- };
4856
- }, [pinToBottom]);
5309
+ return () => ro.disconnect();
5310
+ }, [pinToBottom, getDistanceFromBottom]);
4857
5311
  react.useEffect(() => {
4858
5312
  if (messages.length > 0) {
4859
5313
  setTimeout(() => scrollToBottom("instant"), 50);
@@ -4865,6 +5319,7 @@ var MessageListV2 = react.forwardRef(
4865
5319
  {
4866
5320
  ref: scrollRef,
4867
5321
  onScroll: handleScroll,
5322
+ onWheel: handleWheel,
4868
5323
  className: "payman-v2-message-scroll payman-v2-scrollbar",
4869
5324
  children: /* @__PURE__ */ jsxRuntime.jsxs(
4870
5325
  "div",
@@ -4878,6 +5333,7 @@ var MessageListV2 = react.forwardRef(
4878
5333
  message,
4879
5334
  onEdit: onEditUserMessage,
4880
5335
  onRetry: onRetryUserMessage,
5336
+ onImageClick,
4881
5337
  retryDisabled,
4882
5338
  actions: messageActions?.userMessageActions
4883
5339
  }
@@ -4900,20 +5356,16 @@ var MessageListV2 = react.forwardRef(
4900
5356
  },
4901
5357
  note.id
4902
5358
  )),
4903
- visibleUserActionPrompts?.map((prompt) => {
4904
- const promptKey = getPromptViewKey(prompt);
4905
- return /* @__PURE__ */ jsxRuntime.jsx(
4906
- UserActionInline,
4907
- {
4908
- prompt,
4909
- onSubmit: onSubmitUserAction ?? noop,
4910
- onCancel: onCancelUserAction ?? noop,
4911
- onResend: onResendUserAction ?? noop,
4912
- onExpired: () => handleUserActionExpired(promptKey)
4913
- },
4914
- promptKey
4915
- );
4916
- })
5359
+ userActionPrompts?.map((prompt) => /* @__PURE__ */ jsxRuntime.jsx(
5360
+ UserActionInline,
5361
+ {
5362
+ prompt,
5363
+ onSubmit: onSubmitUserAction ?? noop,
5364
+ onCancel: onCancelUserAction ?? noop,
5365
+ onResend: onResendUserAction ?? noop
5366
+ },
5367
+ prompt.toolCallId || prompt.userActionId
5368
+ ))
4917
5369
  ]
4918
5370
  }
4919
5371
  )
@@ -4943,45 +5395,281 @@ var MessageListV2 = react.forwardRef(
4943
5395
  ] });
4944
5396
  }
4945
5397
  );
4946
- var ChatInputV2 = react.forwardRef(
4947
- function ChatInputV22({
4948
- onSend,
4949
- disabled = false,
4950
- isStreaming = false,
4951
- placeholder = "Reply...",
4952
- enableVoice = false,
4953
- voiceAvailable = false,
4954
- isRecording = false,
4955
- onVoicePress,
4956
- transcribedText = "",
4957
- showResetSession = false,
4958
- onResetSession,
4959
- showAttachmentButton = true,
4960
- showUploadImageButton = true,
4961
- showAttachFileButton = true,
4962
- onUploadImageClick,
4963
- onAttachFileClick,
4964
- editingMessageId = null,
4965
- onClearEditing,
4966
- analysisMode,
4967
- onAnalysisModeChange,
4968
- slashCommands = []
4969
- }, ref) {
4970
- const [value, setValue] = react.useState("");
4971
- const [isFocused, setIsFocused] = react.useState(false);
4972
- const [showActions, setShowActions] = react.useState(false);
4973
- const [showVoiceTooltip, setShowVoiceTooltip] = react.useState(false);
4974
- const [selectedCommandIndex, setSelectedCommandIndex] = react.useState(0);
4975
- const [inlineHint, setInlineHint] = react.useState(null);
4976
- const [commandMenuDismissed, setCommandMenuDismissed] = react.useState(false);
4977
- const textareaRef = react.useRef(null);
4978
- const actionsRef = react.useRef(null);
4979
- const preRecordTextRef = react.useRef("");
4980
- const voiceTooltipTimerRef = react.useRef(
5398
+ function FilePreviewModal({ src, name, onClose }) {
5399
+ const [isMounted, setIsMounted] = react.useState(false);
5400
+ const [isLoaded, setIsLoaded] = react.useState(false);
5401
+ react.useEffect(() => {
5402
+ setIsMounted(true);
5403
+ return () => setIsMounted(false);
5404
+ }, []);
5405
+ react.useEffect(() => {
5406
+ setIsLoaded(false);
5407
+ }, [src]);
5408
+ const handleKeyDown = react.useCallback(
5409
+ (e) => {
5410
+ if (e.key === "Escape") onClose();
5411
+ },
5412
+ [onClose]
5413
+ );
5414
+ react.useEffect(() => {
5415
+ if (!src || typeof document === "undefined") return;
5416
+ document.addEventListener("keydown", handleKeyDown);
5417
+ const prev = document.body.style.overflow;
5418
+ document.body.style.overflow = "hidden";
5419
+ return () => {
5420
+ document.removeEventListener("keydown", handleKeyDown);
5421
+ document.body.style.overflow = prev;
5422
+ };
5423
+ }, [src, handleKeyDown]);
5424
+ if (!isMounted || typeof document === "undefined") return null;
5425
+ return reactDom.createPortal(
5426
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: src ? /* @__PURE__ */ jsxRuntime.jsxs(
5427
+ framerMotion.motion.div,
5428
+ {
5429
+ className: "payman-v2-file-preview-overlay",
5430
+ initial: { opacity: 0 },
5431
+ animate: { opacity: 1 },
5432
+ exit: { opacity: 0 },
5433
+ transition: { duration: 0.18 },
5434
+ onClick: onClose,
5435
+ role: "dialog",
5436
+ "aria-modal": "true",
5437
+ "aria-label": `Preview: ${name}`,
5438
+ children: [
5439
+ /* @__PURE__ */ jsxRuntime.jsx(
5440
+ "button",
5441
+ {
5442
+ type: "button",
5443
+ className: "payman-v2-file-preview-close",
5444
+ "aria-label": "Close preview",
5445
+ onClick: (e) => {
5446
+ e.stopPropagation();
5447
+ onClose();
5448
+ },
5449
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 18, strokeWidth: 2 })
5450
+ }
5451
+ ),
5452
+ /* @__PURE__ */ jsxRuntime.jsx(
5453
+ framerMotion.motion.div,
5454
+ {
5455
+ className: "payman-v2-file-preview-inner",
5456
+ initial: { scale: 0.93, opacity: 0 },
5457
+ animate: { scale: isLoaded ? 1 : 0.93, opacity: isLoaded ? 1 : 0 },
5458
+ exit: { scale: 0.93, opacity: 0 },
5459
+ transition: { duration: 0.2 },
5460
+ onClick: (e) => e.stopPropagation(),
5461
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5462
+ "img",
5463
+ {
5464
+ src,
5465
+ alt: name,
5466
+ className: "payman-v2-file-preview-img",
5467
+ draggable: false,
5468
+ onLoad: () => setIsLoaded(true)
5469
+ }
5470
+ )
5471
+ }
5472
+ )
5473
+ ]
5474
+ },
5475
+ "file-preview"
5476
+ ) : null }),
5477
+ document.body
5478
+ );
5479
+ }
5480
+ function normalizeExtension(ext) {
5481
+ return ext.replace(/^\./, "").toLowerCase();
5482
+ }
5483
+ function extOf(filename) {
5484
+ return filename.split(".").pop()?.toLowerCase() ?? "";
5485
+ }
5486
+ function isAllowedImage(file, allowedExtensions) {
5487
+ const allowed = new Set(allowedExtensions.map(normalizeExtension));
5488
+ const ext = extOf(file.name);
5489
+ if (allowed.has(ext)) return true;
5490
+ if (!file.type.startsWith("image/")) return false;
5491
+ const mimeSubtype = file.type.slice("image/".length).toLowerCase();
5492
+ return allowed.has(mimeSubtype) || mimeSubtype === "jpeg" && allowed.has("jpg");
5493
+ }
5494
+ function isAllowedDocument(file, allowedExtensions) {
5495
+ const allowed = new Set(allowedExtensions.map(normalizeExtension));
5496
+ return allowed.has(extOf(file.name));
5497
+ }
5498
+ function fileTypeLabel(file, kind) {
5499
+ if (kind === "image") return "Image";
5500
+ const ext = extOf(file.name);
5501
+ return ext ? ext.toUpperCase() : "Document";
5502
+ }
5503
+ var ChatInputV2 = react.forwardRef(
5504
+ function ChatInputV22({
5505
+ onSend,
5506
+ disabled = false,
5507
+ isStreaming = false,
5508
+ isUploadingAttachments = false,
5509
+ attachmentsReady = true,
5510
+ hasAttachmentUploadErrors = false,
5511
+ attachmentUploadStatusById = {},
5512
+ uploadedAttachmentPayloads = [],
5513
+ placeholder = "Reply...",
5514
+ enableVoice = false,
5515
+ voiceAvailable = false,
5516
+ isRecording = false,
5517
+ onVoicePress,
5518
+ transcribedText = "",
5519
+ showResetSession = false,
5520
+ onResetSession,
5521
+ showAttachmentButton = true,
5522
+ showUploadImageButton = true,
5523
+ showAttachFileButton = true,
5524
+ onUploadImageClick,
5525
+ onAttachFileClick,
5526
+ allowedImageExtensions = ["png", "jpg", "jpeg", "gif", "webp"],
5527
+ allowedFileExtensions = ["pdf", "docx", "xlsx", "xls"],
5528
+ maxCount,
5529
+ maxFileBytes,
5530
+ maxTotalBytes,
5531
+ onFilesChange,
5532
+ onAttachmentsChange,
5533
+ editingMessageId = null,
5534
+ onClearEditing,
5535
+ analysisMode,
5536
+ onAnalysisModeChange,
5537
+ slashCommands = []
5538
+ }, ref) {
5539
+ const [value, setValue] = react.useState("");
5540
+ const [isFocused, setIsFocused] = react.useState(false);
5541
+ const [showActions, setShowActions] = react.useState(false);
5542
+ const [showVoiceTooltip, setShowVoiceTooltip] = react.useState(false);
5543
+ const [selectedCommandIndex, setSelectedCommandIndex] = react.useState(0);
5544
+ const [inlineHint, setInlineHint] = react.useState(null);
5545
+ const [commandMenuDismissed, setCommandMenuDismissed] = react.useState(false);
5546
+ const [attachedFiles, setAttachedFiles] = react.useState([]);
5547
+ const [previewFile, setPreviewFile] = react.useState(null);
5548
+ const [isSending, setIsSending] = react.useState(false);
5549
+ const textareaRef = react.useRef(null);
5550
+ const actionsRef = react.useRef(null);
5551
+ const imageInputRef = react.useRef(null);
5552
+ const fileInputRef = react.useRef(null);
5553
+ const preRecordTextRef = react.useRef("");
5554
+ const voiceTooltipTimerRef = react.useRef(
4981
5555
  null
4982
5556
  );
4983
5557
  const voiceDraftSyncActiveRef = react.useRef(false);
4984
- const isInputLocked = disabled || isRecording;
5558
+ const chatContext = react.useContext(PaymanChatContext);
5559
+ react.useEffect(() => {
5560
+ return () => {
5561
+ attachedFiles.forEach((f) => URL.revokeObjectURL(f.objectUrl));
5562
+ };
5563
+ }, []);
5564
+ const notifyAttachmentList = react.useCallback(
5565
+ (files) => {
5566
+ onFilesChange?.(files.map((f) => f.file));
5567
+ onAttachmentsChange?.(files.map((f) => ({ id: f.id, file: f.file })));
5568
+ },
5569
+ [onAttachmentsChange, onFilesChange]
5570
+ );
5571
+ const clearAttachmentsFromInput = react.useCallback(() => {
5572
+ setAttachedFiles((prev) => {
5573
+ prev.forEach((f) => URL.revokeObjectURL(f.objectUrl));
5574
+ return [];
5575
+ });
5576
+ setPreviewFile(null);
5577
+ onFilesChange?.([]);
5578
+ onAttachmentsChange?.([]);
5579
+ }, [onAttachmentsChange, onFilesChange]);
5580
+ const addFiles = react.useCallback(
5581
+ (incoming, source) => {
5582
+ const isImage = source === "image";
5583
+ const allowedExtensions = isImage ? allowedImageExtensions : allowedFileExtensions;
5584
+ const isAllowed = isImage ? isAllowedImage : isAllowedDocument;
5585
+ const kindLabel = isImage ? "image" : "document";
5586
+ setAttachedFiles((prev) => {
5587
+ const accepted = [];
5588
+ let rejectedType = false;
5589
+ let hitCountLimit = false;
5590
+ let hitFileSizeLimit = false;
5591
+ let hitTotalSizeLimit = false;
5592
+ let oversizedName = "";
5593
+ const currentTotalBytes = prev.reduce(
5594
+ (sum, item) => sum + item.file.size,
5595
+ 0
5596
+ );
5597
+ let addedBytes = 0;
5598
+ for (const file of incoming) {
5599
+ if (!isAllowed(file, allowedExtensions)) {
5600
+ rejectedType = true;
5601
+ continue;
5602
+ }
5603
+ if (maxFileBytes != null && file.size > maxFileBytes) {
5604
+ hitFileSizeLimit = true;
5605
+ oversizedName = file.name;
5606
+ continue;
5607
+ }
5608
+ if (maxTotalBytes != null && currentTotalBytes + addedBytes + file.size > maxTotalBytes) {
5609
+ hitTotalSizeLimit = true;
5610
+ break;
5611
+ }
5612
+ if (maxCount != null && prev.length + accepted.length >= maxCount) {
5613
+ hitCountLimit = true;
5614
+ break;
5615
+ }
5616
+ accepted.push({
5617
+ id: `${Date.now()}-${Math.random()}`,
5618
+ file,
5619
+ kind: source,
5620
+ objectUrl: URL.createObjectURL(file)
5621
+ });
5622
+ addedBytes += file.size;
5623
+ }
5624
+ if (rejectedType) {
5625
+ setInlineHint(
5626
+ `Unsupported ${kindLabel} type. Allowed: ${allowedExtensions.map(normalizeExtension).join(", ")}.`
5627
+ );
5628
+ } else if (hitFileSizeLimit) {
5629
+ setInlineHint(
5630
+ maxFileBytes != null ? `"${oversizedName}" exceeds the ${formatAttachmentBytes(maxFileBytes)} per-file limit.` : "File exceeds the maximum allowed size."
5631
+ );
5632
+ } else if (hitTotalSizeLimit) {
5633
+ setInlineHint(
5634
+ maxTotalBytes != null ? `Total attachment size would exceed ${formatAttachmentBytes(maxTotalBytes)}.` : "Total attachment size exceeds the limit."
5635
+ );
5636
+ } else if (hitCountLimit) {
5637
+ setInlineHint(
5638
+ maxCount === 1 ? "Only 1 attachment is allowed per message." : `Maximum ${maxCount} attachments allowed per message.`
5639
+ );
5640
+ }
5641
+ if (accepted.length === 0) return prev;
5642
+ const updated = [...prev, ...accepted];
5643
+ notifyAttachmentList(updated);
5644
+ return updated;
5645
+ });
5646
+ },
5647
+ [
5648
+ allowedFileExtensions,
5649
+ allowedImageExtensions,
5650
+ maxCount,
5651
+ maxFileBytes,
5652
+ maxTotalBytes,
5653
+ notifyAttachmentList
5654
+ ]
5655
+ );
5656
+ const removeFile = react.useCallback(
5657
+ (id) => {
5658
+ setAttachedFiles((prev) => {
5659
+ const target = prev.find((f) => f.id === id);
5660
+ if (target) URL.revokeObjectURL(target.objectUrl);
5661
+ const updated = prev.filter((f) => f.id !== id);
5662
+ notifyAttachmentList(updated);
5663
+ return updated;
5664
+ });
5665
+ setPreviewFile((p) => p?.id === id ? null : p);
5666
+ },
5667
+ [notifyAttachmentList]
5668
+ );
5669
+ const imageAccept = allowedImageExtensions.map((e) => `.${e.replace(/^\./, "")}`).join(",");
5670
+ const fileAccept = allowedFileExtensions.map((e) => `.${e.replace(/^\./, "")}`).join(",");
5671
+ const isInputBusy = isSending || isUploadingAttachments;
5672
+ const isInputLocked = disabled || isRecording || isInputBusy;
4985
5673
  const hasAttachmentOptions = showUploadImageButton || showAttachFileButton;
4986
5674
  const showAttachmentMenuButton = showAttachmentButton && hasAttachmentOptions;
4987
5675
  const showVoiceButton = enableVoice && onVoicePress != null;
@@ -5021,9 +5709,10 @@ var ChatInputV2 = react.forwardRef(
5021
5709
  const end = message.length;
5022
5710
  textarea.setSelectionRange(end, end);
5023
5711
  });
5024
- }
5712
+ },
5713
+ clearAttachments: clearAttachmentsFromInput
5025
5714
  }),
5026
- [disabled]
5715
+ [disabled, clearAttachmentsFromInput]
5027
5716
  );
5028
5717
  react.useEffect(() => {
5029
5718
  if (!showActions) return;
@@ -5057,8 +5746,23 @@ var ChatInputV2 = react.forwardRef(
5057
5746
  const separator = base && !base.endsWith(" ") && transcribedText ? " " : "";
5058
5747
  setValue(`${base}${separator}${transcribedText}`);
5059
5748
  }, [isRecording, transcribedText]);
5060
- const handleSend = react.useCallback(() => {
5061
- if (!value.trim() || disabled) return;
5749
+ function uploadStatusLabel(status, file, kind) {
5750
+ if (status === "uploading") return "Uploading\u2026";
5751
+ if (status === "error") return "Upload failed";
5752
+ return fileTypeLabel(file, kind);
5753
+ }
5754
+ const handleSend = react.useCallback(async () => {
5755
+ const hasText = value.trim().length > 0;
5756
+ const hasFiles = attachedFiles.length > 0;
5757
+ if (!hasText && !hasFiles || disabled || isSending || isUploadingAttachments) return;
5758
+ if (hasFiles && !attachmentsReady) {
5759
+ setInlineHint("Waiting for attachments to finish uploading.");
5760
+ return;
5761
+ }
5762
+ if (hasFiles && hasAttachmentUploadErrors) {
5763
+ setInlineHint("Remove or re-add attachments that failed to upload.");
5764
+ return;
5765
+ }
5062
5766
  const commandHint = getSlashCommandValidationHint(value);
5063
5767
  if (commandHint) {
5064
5768
  setInlineHint(commandHint);
@@ -5068,15 +5772,38 @@ var ChatInputV2 = react.forwardRef(
5068
5772
  preRecordTextRef.current = "";
5069
5773
  setInlineHint(null);
5070
5774
  onClearEditing?.();
5071
- onSend(value.trim());
5775
+ const textToSend = value.trim();
5776
+ const filesToSend = attachedFiles.map((f) => f.file);
5777
+ const attachmentsToSend = [...uploadedAttachmentPayloads];
5072
5778
  setValue("");
5779
+ clearAttachmentsFromInput();
5073
5780
  requestAnimationFrame(() => {
5074
5781
  if (textareaRef.current) {
5075
5782
  textareaRef.current.style.height = "auto";
5076
5783
  textareaRef.current.focus();
5077
5784
  }
5078
5785
  });
5079
- }, [value, disabled, onClearEditing, onSend]);
5786
+ setIsSending(true);
5787
+ try {
5788
+ await onSend(textToSend, filesToSend, attachmentsToSend);
5789
+ } catch {
5790
+ setInlineHint("Failed to send message. Please try again.");
5791
+ } finally {
5792
+ setIsSending(false);
5793
+ }
5794
+ }, [
5795
+ value,
5796
+ attachedFiles,
5797
+ uploadedAttachmentPayloads,
5798
+ disabled,
5799
+ isSending,
5800
+ isUploadingAttachments,
5801
+ attachmentsReady,
5802
+ hasAttachmentUploadErrors,
5803
+ onClearEditing,
5804
+ onSend,
5805
+ clearAttachmentsFromInput
5806
+ ]);
5080
5807
  const selectCommand = react.useCallback(
5081
5808
  (command) => {
5082
5809
  const insertText = command.insertText ?? `${command.name} `;
@@ -5121,18 +5848,30 @@ var ChatInputV2 = react.forwardRef(
5121
5848
  }
5122
5849
  if (e.key === "Enter" && !e.shiftKey) {
5123
5850
  e.preventDefault();
5124
- if (isStreaming) return;
5125
- handleSend();
5851
+ if (isStreaming || isSending || isUploadingAttachments) return;
5852
+ void handleSend();
5126
5853
  }
5127
5854
  };
5128
5855
  const handleUploadImageClick = () => {
5856
+ imageInputRef.current?.click();
5129
5857
  onUploadImageClick?.();
5130
5858
  setShowActions(false);
5131
5859
  };
5132
5860
  const handleAttachFileClick = () => {
5861
+ fileInputRef.current?.click();
5133
5862
  onAttachFileClick?.();
5134
5863
  setShowActions(false);
5135
5864
  };
5865
+ const handleImageFilesSelected = (e) => {
5866
+ const files = Array.from(e.target.files ?? []);
5867
+ if (files.length) addFiles(files, "image");
5868
+ e.target.value = "";
5869
+ };
5870
+ const handleDocFilesSelected = (e) => {
5871
+ const files = Array.from(e.target.files ?? []);
5872
+ if (files.length) addFiles(files, "file");
5873
+ e.target.value = "";
5874
+ };
5136
5875
  const hideVoiceTooltip = react.useCallback(() => {
5137
5876
  if (voiceTooltipTimerRef.current) {
5138
5877
  clearTimeout(voiceTooltipTimerRef.current);
@@ -5160,9 +5899,39 @@ var ChatInputV2 = react.forwardRef(
5160
5899
  }
5161
5900
  onVoicePress();
5162
5901
  };
5163
- const canSend = value.trim().length > 0 && !disabled;
5164
- const sendDisabled = !canSend || isStreaming;
5902
+ const canSend = (value.trim().length > 0 || attachedFiles.length > 0) && !disabled;
5903
+ const sendDisabled = !canSend || isStreaming || isUploadingAttachments || !attachmentsReady || hasAttachmentUploadErrors || isSending;
5165
5904
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-input-container", children: [
5905
+ /* @__PURE__ */ jsxRuntime.jsx(
5906
+ "input",
5907
+ {
5908
+ ref: imageInputRef,
5909
+ type: "file",
5910
+ accept: imageAccept,
5911
+ multiple: maxCount == null || maxCount > 1,
5912
+ style: { display: "none" },
5913
+ onChange: handleImageFilesSelected
5914
+ }
5915
+ ),
5916
+ /* @__PURE__ */ jsxRuntime.jsx(
5917
+ "input",
5918
+ {
5919
+ ref: fileInputRef,
5920
+ type: "file",
5921
+ accept: fileAccept,
5922
+ multiple: maxCount == null || maxCount > 1,
5923
+ style: { display: "none" },
5924
+ onChange: handleDocFilesSelected
5925
+ }
5926
+ ),
5927
+ /* @__PURE__ */ jsxRuntime.jsx(
5928
+ FilePreviewModal,
5929
+ {
5930
+ src: previewFile?.kind === "image" ? previewFile.objectUrl : null,
5931
+ name: previewFile?.file.name ?? "",
5932
+ onClose: () => setPreviewFile(null)
5933
+ }
5934
+ ),
5166
5935
  /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: showCommandSuggestions && /* @__PURE__ */ jsxRuntime.jsx(
5167
5936
  framerMotion.motion.div,
5168
5937
  {
@@ -5201,6 +5970,115 @@ var ChatInputV2 = react.forwardRef(
5201
5970
  ),
5202
5971
  children: [
5203
5972
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-input-body", children: [
5973
+ /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { initial: false, children: attachedFiles.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
5974
+ framerMotion.motion.div,
5975
+ {
5976
+ initial: { opacity: 0, height: 0 },
5977
+ animate: { opacity: 1, height: "auto" },
5978
+ exit: { opacity: 0, height: 0 },
5979
+ transition: { duration: 0.15, ease: "easeOut" },
5980
+ className: "payman-v2-file-preview",
5981
+ children: attachedFiles.map((af) => {
5982
+ const uploadStatus = attachmentUploadStatusById[af.id];
5983
+ const isUploadingFile = uploadStatus === "uploading";
5984
+ const hasUploadError = uploadStatus === "error";
5985
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-file-preview-item", children: [
5986
+ af.kind === "image" ? /* @__PURE__ */ jsxRuntime.jsxs(
5987
+ "button",
5988
+ {
5989
+ type: "button",
5990
+ className: cn(
5991
+ "payman-v2-file-preview-shell payman-v2-file-preview-shell-clickable",
5992
+ hasUploadError && "payman-v2-file-preview-shell-error"
5993
+ ),
5994
+ onClick: () => setPreviewFile(af),
5995
+ disabled: isUploadingFile,
5996
+ "aria-label": `Preview ${af.file.name}`,
5997
+ children: [
5998
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-file-preview-thumb", children: [
5999
+ /* @__PURE__ */ jsxRuntime.jsx(
6000
+ "img",
6001
+ {
6002
+ src: af.objectUrl,
6003
+ alt: "",
6004
+ draggable: false
6005
+ }
6006
+ ),
6007
+ isUploadingFile && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-file-preview-uploading", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 14, className: "payman-v2-spin" }) })
6008
+ ] }),
6009
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-file-preview-info", children: [
6010
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-file-preview-name", children: af.file.name }),
6011
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-file-preview-type", children: uploadStatusLabel(uploadStatus, af.file, af.kind) })
6012
+ ] })
6013
+ ]
6014
+ }
6015
+ ) : isPdfFile(af.file) ? /* @__PURE__ */ jsxRuntime.jsxs(
6016
+ "button",
6017
+ {
6018
+ type: "button",
6019
+ className: cn(
6020
+ "payman-v2-file-preview-shell payman-v2-file-preview-shell-clickable",
6021
+ hasUploadError && "payman-v2-file-preview-shell-error"
6022
+ ),
6023
+ onClick: () => chatContext?.openPdfSheet(
6024
+ af.objectUrl,
6025
+ af.file.name
6026
+ ),
6027
+ disabled: isUploadingFile,
6028
+ "aria-label": `Preview ${af.file.name}`,
6029
+ children: [
6030
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-file-preview-thumb", children: isUploadingFile ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "payman-v2-spin payman-v2-file-preview-doc-icon" }) : /* @__PURE__ */ jsxRuntime.jsx(
6031
+ lucideReact.FileText,
6032
+ {
6033
+ size: 16,
6034
+ strokeWidth: 1.75,
6035
+ className: "payman-v2-file-preview-doc-icon"
6036
+ }
6037
+ ) }),
6038
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-file-preview-info", children: [
6039
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-file-preview-name", children: af.file.name }),
6040
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-file-preview-type", children: uploadStatusLabel(uploadStatus, af.file, af.kind) })
6041
+ ] })
6042
+ ]
6043
+ }
6044
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
6045
+ "div",
6046
+ {
6047
+ className: cn(
6048
+ "payman-v2-file-preview-shell",
6049
+ hasUploadError && "payman-v2-file-preview-shell-error"
6050
+ ),
6051
+ children: [
6052
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-file-preview-thumb", children: isUploadingFile ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 16, className: "payman-v2-spin payman-v2-file-preview-doc-icon" }) : /* @__PURE__ */ jsxRuntime.jsx(
6053
+ lucideReact.FileText,
6054
+ {
6055
+ size: 16,
6056
+ strokeWidth: 1.75,
6057
+ className: "payman-v2-file-preview-doc-icon"
6058
+ }
6059
+ ) }),
6060
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-file-preview-info", children: [
6061
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-file-preview-name", children: af.file.name }),
6062
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-file-preview-type", children: uploadStatusLabel(uploadStatus, af.file, af.kind) })
6063
+ ] })
6064
+ ]
6065
+ }
6066
+ ),
6067
+ /* @__PURE__ */ jsxRuntime.jsx(
6068
+ "button",
6069
+ {
6070
+ type: "button",
6071
+ className: "payman-v2-file-preview-remove",
6072
+ onClick: () => removeFile(af.id),
6073
+ "aria-label": `Remove ${af.file.name}`,
6074
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 12, strokeWidth: 2.5 })
6075
+ }
6076
+ )
6077
+ ] }, af.id);
6078
+ })
6079
+ },
6080
+ "file-preview"
6081
+ ) }),
5204
6082
  /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { initial: false, children: editingMessageId && /* @__PURE__ */ jsxRuntime.jsxs(
5205
6083
  framerMotion.motion.div,
5206
6084
  {
@@ -5432,13 +6310,13 @@ var ChatInputV2 = react.forwardRef(
5432
6310
  "button",
5433
6311
  {
5434
6312
  type: "button",
5435
- onClick: handleSend,
6313
+ onClick: () => void handleSend(),
5436
6314
  disabled: sendDisabled,
5437
6315
  className: cn(
5438
6316
  "payman-v2-input-send-btn",
5439
6317
  sendDisabled && "payman-v2-input-send-btn-disabled"
5440
6318
  ),
5441
- "aria-label": "Send message",
6319
+ "aria-label": isUploadingAttachments || isSending ? "Uploading attachments" : "Send message",
5442
6320
  children: /* @__PURE__ */ jsxRuntime.jsx(
5443
6321
  lucideReact.ArrowUp,
5444
6322
  {
@@ -5973,6 +6851,389 @@ function TimelineBars({
5973
6851
  )
5974
6852
  ] });
5975
6853
  }
6854
+
6855
+ // src/utils/pdfPreview.ts
6856
+ var PdfPreviewError = class extends Error {
6857
+ constructor(kind, title, userMessage) {
6858
+ super(userMessage);
6859
+ __publicField(this, "kind");
6860
+ __publicField(this, "title");
6861
+ __publicField(this, "userMessage");
6862
+ this.name = "PdfPreviewError";
6863
+ this.kind = kind;
6864
+ this.title = title;
6865
+ this.userMessage = userMessage;
6866
+ }
6867
+ };
6868
+ function classifyErrorBody(body, status) {
6869
+ const text = body.replace(/\s+/g, " ").trim();
6870
+ if (/signed expiry time|must be after signed start time|expired|expir/i.test(text)) {
6871
+ return new PdfPreviewError(
6872
+ "expired",
6873
+ "Link expired",
6874
+ "This download link has expired. Ask the assistant to generate a new copy of the document."
6875
+ );
6876
+ }
6877
+ if (/AuthenticationFailed|authorization header|formed correctly including the signature/i.test(
6878
+ text
6879
+ )) {
6880
+ return new PdfPreviewError(
6881
+ "forbidden",
6882
+ "Link no longer valid",
6883
+ "This preview link is no longer valid. Request a fresh download link and try again."
6884
+ );
6885
+ }
6886
+ if (status === 404 || /BlobNotFound|ResourceNotFound|The specified blob does not exist/i.test(text)) {
6887
+ return new PdfPreviewError(
6888
+ "not_found",
6889
+ "Document not found",
6890
+ "The file is no longer available. It may have been removed or the link is incorrect."
6891
+ );
6892
+ }
6893
+ if (status === 403) {
6894
+ return new PdfPreviewError(
6895
+ "forbidden",
6896
+ "Can't open document",
6897
+ "You may not have access to this file, or the link has expired."
6898
+ );
6899
+ }
6900
+ if (status === 401) {
6901
+ return new PdfPreviewError(
6902
+ "forbidden",
6903
+ "Access denied",
6904
+ "This preview link could not be verified. Request a new download link."
6905
+ );
6906
+ }
6907
+ return new PdfPreviewError(
6908
+ "unknown",
6909
+ "Can't preview document",
6910
+ "Something went wrong while opening this file. Try downloading it or request a new link."
6911
+ );
6912
+ }
6913
+ function normalizePdfPreviewError(error) {
6914
+ if (error instanceof PdfPreviewError) return error;
6915
+ if (error instanceof TypeError) {
6916
+ return new PdfPreviewError(
6917
+ "network",
6918
+ "Connection problem",
6919
+ "Could not load the document. Check your connection and try again."
6920
+ );
6921
+ }
6922
+ return new PdfPreviewError(
6923
+ "unknown",
6924
+ "Can't preview document",
6925
+ "Something went wrong while opening this file. Try downloading it or request a new link."
6926
+ );
6927
+ }
6928
+ async function assertPdfBlob(blob, status) {
6929
+ if (!blob.size) {
6930
+ return Promise.reject(
6931
+ new PdfPreviewError(
6932
+ "empty",
6933
+ "Document unavailable",
6934
+ "The file response was empty. Request a new download link and try again."
6935
+ )
6936
+ );
6937
+ }
6938
+ const head = await blob.slice(0, Math.min(blob.size, 1024)).text();
6939
+ if (head.startsWith("%PDF")) {
6940
+ return blob;
6941
+ }
6942
+ throw classifyErrorBody(head, status);
6943
+ }
6944
+ async function fetchPdfBlob(src) {
6945
+ let response;
6946
+ try {
6947
+ response = await fetch(src, {
6948
+ method: "GET",
6949
+ headers: { Accept: "application/pdf,*/*" }
6950
+ });
6951
+ } catch {
6952
+ throw new PdfPreviewError(
6953
+ "network",
6954
+ "Connection problem",
6955
+ "Could not load the document. Check your connection and try again."
6956
+ );
6957
+ }
6958
+ const blob = await response.blob();
6959
+ if (!response.ok) {
6960
+ const body = await blob.text().catch(() => "");
6961
+ throw classifyErrorBody(body, response.status);
6962
+ }
6963
+ return assertPdfBlob(blob, response.status);
6964
+ }
6965
+ var MIN_WIDTH = 320;
6966
+ var MAX_WIDTH_RATIO = 0.6;
6967
+ var DEFAULT_WIDTH = 520;
6968
+ var SPRING = { type: "spring", stiffness: 340, damping: 34, mass: 0.85 };
6969
+ var SHEET_EXIT = { duration: 0.22, ease: [0.4, 0, 1, 1] };
6970
+ function pdfDownloadName(title) {
6971
+ const base = title.trim() || "document";
6972
+ return base.toLowerCase().endsWith(".pdf") ? base : `${base}.pdf`;
6973
+ }
6974
+ function clampSplitWidth(w) {
6975
+ const maxW = Math.floor(window.innerWidth * MAX_WIDTH_RATIO);
6976
+ return Math.max(MIN_WIDTH, Math.min(maxW, w));
6977
+ }
6978
+ function PdfSheetV2({
6979
+ src,
6980
+ title,
6981
+ onClose,
6982
+ mode = "split"
6983
+ }) {
6984
+ return /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: src && /* @__PURE__ */ jsxRuntime.jsx(
6985
+ PdfSheetPanel,
6986
+ {
6987
+ src,
6988
+ title,
6989
+ onClose,
6990
+ mode
6991
+ },
6992
+ "pdf-panel"
6993
+ ) });
6994
+ }
6995
+ function PdfSheetPanel({
6996
+ src,
6997
+ title,
6998
+ onClose,
6999
+ mode
7000
+ }) {
7001
+ const isSheet = mode === "sheet";
7002
+ const [isLoaded, setIsLoaded] = react.useState(false);
7003
+ const [isDownloading, setIsDownloading] = react.useState(false);
7004
+ const [previewUrl, setPreviewUrl] = react.useState(null);
7005
+ const [previewError, setPreviewError] = react.useState(null);
7006
+ const [loadAttempt, setLoadAttempt] = react.useState(0);
7007
+ const blobRef = react.useRef(null);
7008
+ const objectUrlRef = react.useRef(null);
7009
+ const [panelWidth, setPanelWidth] = react.useState(DEFAULT_WIDTH);
7010
+ const [splitOpened, setSplitOpened] = react.useState(false);
7011
+ const [isDragging, setIsDragging] = react.useState(false);
7012
+ react.useEffect(() => {
7013
+ setIsLoaded(false);
7014
+ setPreviewUrl(null);
7015
+ setPreviewError(null);
7016
+ setPanelWidth(DEFAULT_WIDTH);
7017
+ setSplitOpened(false);
7018
+ blobRef.current = null;
7019
+ if (objectUrlRef.current) {
7020
+ URL.revokeObjectURL(objectUrlRef.current);
7021
+ objectUrlRef.current = null;
7022
+ }
7023
+ let cancelled = false;
7024
+ const loadPreview = async () => {
7025
+ try {
7026
+ const blob = await fetchPdfBlob(src);
7027
+ if (cancelled) return;
7028
+ blobRef.current = blob;
7029
+ const objectUrl = URL.createObjectURL(blob);
7030
+ objectUrlRef.current = objectUrl;
7031
+ setPreviewUrl(objectUrl);
7032
+ } catch (error) {
7033
+ if (!cancelled) {
7034
+ setPreviewError(normalizePdfPreviewError(error));
7035
+ }
7036
+ }
7037
+ };
7038
+ void loadPreview();
7039
+ return () => {
7040
+ cancelled = true;
7041
+ if (objectUrlRef.current) {
7042
+ URL.revokeObjectURL(objectUrlRef.current);
7043
+ objectUrlRef.current = null;
7044
+ }
7045
+ blobRef.current = null;
7046
+ };
7047
+ }, [src, loadAttempt]);
7048
+ react.useEffect(() => {
7049
+ if (isSheet) return;
7050
+ const onWindowResize = () => setPanelWidth((size) => clampSplitWidth(size));
7051
+ window.addEventListener("resize", onWindowResize);
7052
+ return () => window.removeEventListener("resize", onWindowResize);
7053
+ }, [isSheet]);
7054
+ const handleKeyDown = react.useCallback(
7055
+ (e) => {
7056
+ if (e.key === "Escape") onClose();
7057
+ },
7058
+ [onClose]
7059
+ );
7060
+ react.useEffect(() => {
7061
+ document.addEventListener("keydown", handleKeyDown);
7062
+ return () => document.removeEventListener("keydown", handleKeyDown);
7063
+ }, [handleKeyDown]);
7064
+ const canDownload = isLoaded && previewUrl != null && !previewError;
7065
+ const handleDownload = async () => {
7066
+ const filename = pdfDownloadName(title);
7067
+ setIsDownloading(true);
7068
+ try {
7069
+ const blob = blobRef.current ?? await fetchPdfBlob(src);
7070
+ const objectUrl = URL.createObjectURL(blob);
7071
+ const a = document.createElement("a");
7072
+ a.href = objectUrl;
7073
+ a.download = filename;
7074
+ a.target = "_blank";
7075
+ a.rel = "noopener noreferrer";
7076
+ document.body.appendChild(a);
7077
+ a.click();
7078
+ a.remove();
7079
+ URL.revokeObjectURL(objectUrl);
7080
+ } catch (error) {
7081
+ setPreviewError(normalizePdfPreviewError(error));
7082
+ } finally {
7083
+ setIsDownloading(false);
7084
+ }
7085
+ };
7086
+ const handleRetry = () => {
7087
+ setPreviewError(null);
7088
+ setIsLoaded(false);
7089
+ setPreviewUrl(null);
7090
+ setLoadAttempt((attempt) => attempt + 1);
7091
+ };
7092
+ const handleOpenInNewTab = () => {
7093
+ window.open(src, "_blank", "noopener,noreferrer");
7094
+ };
7095
+ const onResizeMouseDown = (e) => {
7096
+ if (e.button !== 0) return;
7097
+ e.preventDefault();
7098
+ const startX = e.clientX;
7099
+ const startW = panelWidth;
7100
+ setIsDragging(true);
7101
+ const onMove = (ev) => {
7102
+ setPanelWidth(clampSplitWidth(startW + (startX - ev.clientX)));
7103
+ };
7104
+ const onUp = () => {
7105
+ setIsDragging(false);
7106
+ document.removeEventListener("mousemove", onMove);
7107
+ document.removeEventListener("mouseup", onUp);
7108
+ };
7109
+ document.addEventListener("mousemove", onMove);
7110
+ document.addEventListener("mouseup", onUp);
7111
+ };
7112
+ return /* @__PURE__ */ jsxRuntime.jsxs(
7113
+ framerMotion.motion.div,
7114
+ {
7115
+ className: cn(
7116
+ "payman-v2-root payman-v2-pdf-sheet-panel",
7117
+ isSheet && "payman-v2-pdf-sheet-panel--sheet"
7118
+ ),
7119
+ style: { minWidth: 0 },
7120
+ initial: isSheet ? { x: "100%", opacity: 0 } : { width: 0, opacity: 0 },
7121
+ animate: isSheet ? { x: 0, opacity: 1 } : { width: panelWidth, opacity: 1 },
7122
+ exit: isSheet ? { x: "100%", opacity: 0, transition: { x: SHEET_EXIT, opacity: SHEET_EXIT } } : { width: 0, opacity: 0, transition: { width: SHEET_EXIT, opacity: SHEET_EXIT } },
7123
+ transition: isSheet ? { x: SPRING, opacity: SPRING } : {
7124
+ width: splitOpened ? { duration: 0 } : SPRING,
7125
+ opacity: SPRING
7126
+ },
7127
+ onAnimationComplete: () => {
7128
+ if (!isSheet && !splitOpened) setSplitOpened(true);
7129
+ },
7130
+ children: [
7131
+ !isSheet && /* @__PURE__ */ jsxRuntime.jsx(
7132
+ "div",
7133
+ {
7134
+ className: "payman-v2-pdf-sheet-resize-handle",
7135
+ onMouseDown: onResizeMouseDown,
7136
+ "aria-hidden": "true",
7137
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-pdf-sheet-resize-grip" })
7138
+ }
7139
+ ),
7140
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-pdf-sheet-header", children: [
7141
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-pdf-sheet-header-left", children: [
7142
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-pdf-sheet-file-icon", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 14, strokeWidth: 1.75 }) }),
7143
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "payman-v2-pdf-sheet-title", title, children: title || "Document" })
7144
+ ] }),
7145
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-pdf-sheet-header-actions", children: [
7146
+ /* @__PURE__ */ jsxRuntime.jsxs(
7147
+ "button",
7148
+ {
7149
+ type: "button",
7150
+ className: "payman-v2-pdf-sheet-download-btn",
7151
+ "aria-label": "Download PDF",
7152
+ disabled: !canDownload || isDownloading,
7153
+ onClick: () => void handleDownload(),
7154
+ children: [
7155
+ isDownloading ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { size: 13, strokeWidth: 2, style: { animation: "payman-v2-spin 0.65s linear infinite" } }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Download, { size: 13, strokeWidth: 2 }),
7156
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: isDownloading ? "Downloading\u2026" : "Download" })
7157
+ ]
7158
+ }
7159
+ ),
7160
+ /* @__PURE__ */ jsxRuntime.jsx(
7161
+ "button",
7162
+ {
7163
+ type: "button",
7164
+ className: "payman-v2-pdf-sheet-close-btn",
7165
+ "aria-label": "Close preview",
7166
+ onClick: (e) => {
7167
+ e.stopPropagation();
7168
+ onClose();
7169
+ },
7170
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 15, strokeWidth: 2.25 })
7171
+ }
7172
+ )
7173
+ ] })
7174
+ ] }),
7175
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-pdf-sheet-body", children: previewError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-pdf-sheet-error", role: "alert", children: [
7176
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-pdf-sheet-error-icon", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlertCircle, { size: 22, strokeWidth: 1.75 }) }),
7177
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-pdf-sheet-error-title", children: previewError.title }),
7178
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-pdf-sheet-error-message", children: previewError.userMessage }),
7179
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-pdf-sheet-error-actions", children: [
7180
+ previewError.kind === "network" && /* @__PURE__ */ jsxRuntime.jsxs(
7181
+ "button",
7182
+ {
7183
+ type: "button",
7184
+ className: "payman-v2-pdf-sheet-error-btn",
7185
+ onClick: handleRetry,
7186
+ children: [
7187
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { size: 14, strokeWidth: 2 }),
7188
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Try again" })
7189
+ ]
7190
+ }
7191
+ ),
7192
+ (previewError.kind === "network" || previewError.kind === "unknown") && /* @__PURE__ */ jsxRuntime.jsxs(
7193
+ "button",
7194
+ {
7195
+ type: "button",
7196
+ className: "payman-v2-pdf-sheet-error-btn payman-v2-pdf-sheet-error-btn-secondary",
7197
+ onClick: handleOpenInNewTab,
7198
+ children: [
7199
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { size: 14, strokeWidth: 2 }),
7200
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Open link" })
7201
+ ]
7202
+ }
7203
+ )
7204
+ ] })
7205
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
7206
+ (!isLoaded || !previewUrl) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "payman-v2-pdf-sheet-loading", children: [
7207
+ /* @__PURE__ */ jsxRuntime.jsx(
7208
+ lucideReact.Loader2,
7209
+ {
7210
+ size: 20,
7211
+ strokeWidth: 2,
7212
+ style: { animation: "payman-v2-spin 0.65s linear infinite", color: "var(--payman-v2-text-3)" }
7213
+ }
7214
+ ),
7215
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "payman-v2-pdf-sheet-loading-text", children: previewUrl ? "Preparing preview\u2026" : "Opening document\u2026" })
7216
+ ] }),
7217
+ previewUrl && /* @__PURE__ */ jsxRuntime.jsx(
7218
+ "iframe",
7219
+ {
7220
+ src: previewUrl,
7221
+ title: title || "PDF Preview",
7222
+ className: "payman-v2-pdf-sheet-iframe",
7223
+ style: {
7224
+ opacity: isLoaded ? 1 : 0,
7225
+ transition: "opacity 0.3s ease",
7226
+ pointerEvents: isDragging ? "none" : "auto"
7227
+ },
7228
+ onLoad: () => setIsLoaded(true)
7229
+ },
7230
+ previewUrl
7231
+ )
7232
+ ] }) })
7233
+ ]
7234
+ }
7235
+ );
7236
+ }
5976
7237
  var DEFAULT_USER_ACTION_STATE = {
5977
7238
  prompts: [],
5978
7239
  notifications: []
@@ -6067,7 +7328,8 @@ var PaymanChatInner = react.forwardRef(function PaymanChatInner2({
6067
7328
  onLoadMoreMessages,
6068
7329
  isLoadingMoreMessages = false,
6069
7330
  hasMoreMessages = false,
6070
- chat
7331
+ chat,
7332
+ attachmentUpload
6071
7333
  }, ref) {
6072
7334
  const [inputValue, setInputValue] = react.useState("");
6073
7335
  const prevInputValueRef = react.useRef(inputValue);
@@ -6146,6 +7408,19 @@ var PaymanChatInner = react.forwardRef(function PaymanChatInner2({
6146
7408
  }
6147
7409
  }
6148
7410
  );
7411
+ const [pdfSheet, setPdfSheet] = react.useState(null);
7412
+ const autoOpenedPdfHrefsRef = react.useRef(/* @__PURE__ */ new Set());
7413
+ const pdfPreviewMode = config.pdfPreviewMode ?? "split";
7414
+ const openPdfSheet = react.useCallback((href, title, options) => {
7415
+ if (options?.auto) {
7416
+ if (autoOpenedPdfHrefsRef.current.has(href)) return;
7417
+ autoOpenedPdfHrefsRef.current.add(href);
7418
+ }
7419
+ setPdfSheet({ href, title });
7420
+ }, []);
7421
+ const closePdfSheet = react.useCallback(() => {
7422
+ setPdfSheet(null);
7423
+ }, []);
6149
7424
  const contextValue = react.useMemo(
6150
7425
  () => ({
6151
7426
  resetSession,
@@ -6154,7 +7429,8 @@ var PaymanChatInner = react.forwardRef(function PaymanChatInner2({
6154
7429
  cancelStream,
6155
7430
  getSessionId,
6156
7431
  getMessages,
6157
- isWaitingForResponse
7432
+ isWaitingForResponse,
7433
+ openPdfSheet
6158
7434
  }),
6159
7435
  [
6160
7436
  resetSession,
@@ -6163,7 +7439,8 @@ var PaymanChatInner = react.forwardRef(function PaymanChatInner2({
6163
7439
  cancelStream,
6164
7440
  getSessionId,
6165
7441
  getMessages,
6166
- isWaitingForResponse
7442
+ isWaitingForResponse,
7443
+ openPdfSheet
6167
7444
  ]
6168
7445
  );
6169
7446
  const {
@@ -6219,7 +7496,10 @@ var PaymanChatInner = react.forwardRef(function PaymanChatInner2({
6219
7496
  setInputValue("");
6220
7497
  setLightboxSrc(null);
6221
7498
  setLightboxAlt("");
7499
+ setPdfSheet(null);
7500
+ autoOpenedPdfHrefsRef.current.clear();
6222
7501
  chatInputV2Ref.current?.setDraft("");
7502
+ chatInputV2Ref.current?.clearAttachments();
6223
7503
  clearTranscript();
6224
7504
  if (isRecording) {
6225
7505
  stopRecording();
@@ -6270,14 +7550,20 @@ var PaymanChatInner = react.forwardRef(function PaymanChatInner2({
6270
7550
  emptyStateComponent,
6271
7551
  showResetSession = false,
6272
7552
  enableDeepModeToggle = true,
6273
- showAttachmentButton = true,
6274
- showUploadImageButton = true,
6275
- showAttachFileButton = true,
6276
7553
  messageActions: messageActionsConfig,
6277
7554
  enableSlashCommands = true,
6278
7555
  slashCommands: slashCommandsConfig,
6279
7556
  commandPermissions
6280
7557
  } = config;
7558
+ const attachmentSettings = react.useMemo(
7559
+ () => resolveChatAttachmentConfig(config),
7560
+ [
7561
+ config.attachments,
7562
+ config.showAttachmentButton,
7563
+ config.showUploadImageButton,
7564
+ config.showAttachFileButton
7565
+ ]
7566
+ );
6281
7567
  const messageActions = react.useMemo(
6282
7568
  () => ({
6283
7569
  userMessageActions: {
@@ -6365,11 +7651,22 @@ var PaymanChatInner = react.forwardRef(function PaymanChatInner2({
6365
7651
  };
6366
7652
  const userActionPrompts = isUserActionSupported ? userActionState.prompts : void 0;
6367
7653
  const notifications = userActionState.notifications;
6368
- const handleV2Send = (text) => {
7654
+ const handleAttachmentsChange = react.useCallback(
7655
+ (attachments) => {
7656
+ attachmentUpload.syncAttachments(attachments);
7657
+ },
7658
+ [attachmentUpload]
7659
+ );
7660
+ const handleV2Send = (text, files = [], attachments = []) => {
6369
7661
  if (isRecording) stopRecording();
6370
- if (text.trim() && !disableInput && isSessionParamsConfigured) {
7662
+ if ((text.trim() || files.length > 0) && !disableInput && isSessionParamsConfigured) {
7663
+ if (files.length > 0 && attachments.length === 0) return;
6371
7664
  setEditingMessageId(null);
6372
- void sendMessage(text.trim(), { analysisMode: effectiveAnalysisMode });
7665
+ void sendMessage(text.trim(), {
7666
+ analysisMode: effectiveAnalysisMode,
7667
+ attachments,
7668
+ files
7669
+ });
6373
7670
  }
6374
7671
  };
6375
7672
  const handleVoicePress = react.useCallback(async () => {
@@ -6417,155 +7714,214 @@ var PaymanChatInner = react.forwardRef(function PaymanChatInner2({
6417
7714
  style,
6418
7715
  children: [
6419
7716
  children,
6420
- /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", children: isEmpty && !hasEverSentMessage ? /* @__PURE__ */ jsxRuntime.jsx(
6421
- framerMotion.motion.div,
7717
+ /* @__PURE__ */ jsxRuntime.jsxs(
7718
+ "div",
6422
7719
  {
6423
- initial: { opacity: 1 },
6424
- exit: { opacity: 0 },
6425
- transition: { duration: 0.3 },
6426
- className: "payman-v2-chat-layout",
6427
- style: { justifyContent: "center", alignItems: "center" },
6428
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", flex: 1, width: "100%" }, children: [
6429
- /* @__PURE__ */ jsxRuntime.jsx(
6430
- MessageList,
6431
- {
6432
- messages,
6433
- isLoading: false,
6434
- emptyStateText,
6435
- showEmptyStateIcon,
6436
- emptyStateComponent,
6437
- layout,
6438
- showTimestamps,
6439
- stage: config.stage || "DEVELOPMENT",
6440
- animated,
6441
- showAgentName,
6442
- agentName,
6443
- showAvatars,
6444
- showUserAvatar,
6445
- showAssistantAvatar,
6446
- showExecutionSteps,
6447
- showStreamingDot,
6448
- streamingStepsText,
6449
- completedStepsText,
6450
- onExecutionTraceClick,
6451
- onLoadMoreMessages,
6452
- isLoadingMoreMessages,
6453
- hasMoreMessages
6454
- }
6455
- ),
6456
- /* @__PURE__ */ jsxRuntime.jsx(
7720
+ className: cn(
7721
+ "payman-v2-split-layout",
7722
+ pdfPreviewMode === "sheet" && "payman-v2-split-layout--sheet"
7723
+ ),
7724
+ children: [
7725
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "payman-v2-chat-column", children: /* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { mode: "wait", children: isEmpty && !hasEverSentMessage ? /* @__PURE__ */ jsxRuntime.jsx(
6457
7726
  framerMotion.motion.div,
6458
7727
  {
6459
- initial: { opacity: 0, y: 12 },
6460
- animate: { opacity: 1, y: 0 },
6461
- transition: { delay: 0.2, duration: 0.4, ease: [0.25, 0.46, 0.45, 0.94] },
6462
- style: { width: "100%" },
6463
- children: hasAskPermission && /* @__PURE__ */ jsxRuntime.jsx(
6464
- ChatInputV2,
6465
- {
6466
- ref: chatInputV2Ref,
6467
- onSend: handleV2Send,
6468
- onCancel: cancelStream,
6469
- disabled: isV2InputDisabled,
6470
- isStreaming: isWaitingForResponse,
6471
- placeholder: isRecording ? "Listening..." : placeholder,
6472
- enableVoice: config.enableVoice === true,
6473
- transcribedText: config.enableVoice === true ? transcribedText : "",
6474
- voiceAvailable: config.enableVoice === true && voiceAvailable,
6475
- isRecording,
6476
- onVoicePress: config.enableVoice === true ? handleVoicePress : void 0,
6477
- onCancelRecording: handleCancelRecording,
6478
- onConfirmRecording: handleConfirmRecording,
6479
- showResetSession,
6480
- onResetSession: requestResetSession,
6481
- showAttachmentButton,
6482
- showUploadImageButton,
6483
- showAttachFileButton,
6484
- onUploadImageClick,
6485
- onAttachFileClick,
6486
- editingMessageId,
6487
- onClearEditing: handleClearEditing,
6488
- analysisMode: enableDeepModeToggle ? analysisMode : void 0,
6489
- onAnalysisModeChange: enableDeepModeToggle ? setAnalysisMode : void 0,
6490
- slashCommands
6491
- }
6492
- )
6493
- }
6494
- )
6495
- ] })
6496
- },
6497
- "v2-empty"
6498
- ) : /* @__PURE__ */ jsxRuntime.jsxs(
6499
- framerMotion.motion.div,
6500
- {
6501
- initial: hasEverSentMessage ? { opacity: 0 } : false,
6502
- animate: { opacity: 1 },
6503
- transition: { duration: 0.3 },
6504
- className: "payman-v2-chat-layout",
6505
- children: [
6506
- /* @__PURE__ */ jsxRuntime.jsx(
6507
- MessageListV2,
7728
+ initial: { opacity: 1 },
7729
+ exit: { opacity: 0 },
7730
+ transition: { duration: 0.3 },
7731
+ className: "payman-v2-chat-layout",
7732
+ style: { justifyContent: "center", alignItems: "center" },
7733
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", flex: 1, width: "100%" }, children: [
7734
+ /* @__PURE__ */ jsxRuntime.jsx(
7735
+ MessageList,
7736
+ {
7737
+ messages,
7738
+ isLoading: false,
7739
+ emptyStateText,
7740
+ showEmptyStateIcon,
7741
+ emptyStateComponent,
7742
+ layout,
7743
+ showTimestamps,
7744
+ stage: config.stage || "DEVELOPMENT",
7745
+ animated,
7746
+ showAgentName,
7747
+ agentName,
7748
+ showAvatars,
7749
+ showUserAvatar,
7750
+ showAssistantAvatar,
7751
+ showExecutionSteps,
7752
+ showStreamingDot,
7753
+ streamingStepsText,
7754
+ completedStepsText,
7755
+ onExecutionTraceClick,
7756
+ onLoadMoreMessages,
7757
+ isLoadingMoreMessages,
7758
+ hasMoreMessages
7759
+ }
7760
+ ),
7761
+ /* @__PURE__ */ jsxRuntime.jsx(
7762
+ framerMotion.motion.div,
7763
+ {
7764
+ initial: { opacity: 0, y: 12 },
7765
+ animate: { opacity: 1, y: 0 },
7766
+ transition: { delay: 0.2, duration: 0.4, ease: [0.25, 0.46, 0.45, 0.94] },
7767
+ style: { width: "100%" },
7768
+ children: hasAskPermission && /* @__PURE__ */ jsxRuntime.jsx(
7769
+ ChatInputV2,
7770
+ {
7771
+ ref: chatInputV2Ref,
7772
+ onSend: handleV2Send,
7773
+ onCancel: cancelStream,
7774
+ disabled: isV2InputDisabled,
7775
+ isStreaming: isWaitingForResponse,
7776
+ isUploadingAttachments: attachmentUpload.isUploading,
7777
+ attachmentsReady: attachmentUpload.allReady,
7778
+ hasAttachmentUploadErrors: attachmentUpload.hasErrors,
7779
+ attachmentUploadStatusById: attachmentUpload.statusById,
7780
+ uploadedAttachmentPayloads: attachmentUpload.payloads,
7781
+ onAttachmentsChange: handleAttachmentsChange,
7782
+ placeholder: isRecording ? "Listening..." : placeholder,
7783
+ enableVoice: config.enableVoice === true,
7784
+ transcribedText: config.enableVoice === true ? transcribedText : "",
7785
+ voiceAvailable: config.enableVoice === true && voiceAvailable,
7786
+ isRecording,
7787
+ onVoicePress: config.enableVoice === true ? handleVoicePress : void 0,
7788
+ onCancelRecording: handleCancelRecording,
7789
+ onConfirmRecording: handleConfirmRecording,
7790
+ showResetSession,
7791
+ onResetSession: requestResetSession,
7792
+ showAttachmentButton: attachmentSettings.showAttachmentButton,
7793
+ showUploadImageButton: attachmentSettings.showUploadImageButton,
7794
+ showAttachFileButton: attachmentSettings.showAttachFileButton,
7795
+ allowedImageExtensions: attachmentSettings.allowedImageExtensions,
7796
+ allowedFileExtensions: attachmentSettings.allowedFileExtensions,
7797
+ maxCount: attachmentSettings.maxCount,
7798
+ maxFileBytes: attachmentSettings.maxFileBytes,
7799
+ maxTotalBytes: attachmentSettings.maxTotalBytes,
7800
+ onUploadImageClick,
7801
+ onAttachFileClick,
7802
+ editingMessageId,
7803
+ onClearEditing: handleClearEditing,
7804
+ analysisMode: enableDeepModeToggle ? analysisMode : void 0,
7805
+ onAnalysisModeChange: enableDeepModeToggle ? setAnalysisMode : void 0,
7806
+ slashCommands
7807
+ }
7808
+ )
7809
+ }
7810
+ )
7811
+ ] })
7812
+ },
7813
+ "v2-empty"
7814
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
7815
+ framerMotion.motion.div,
6508
7816
  {
6509
- ref: messageListV2Ref,
6510
- messages,
6511
- isStreaming: isWaitingForResponse,
6512
- onEditUserMessage: handleEditMessageDraft,
6513
- onRetryUserMessage: handleRetryUserMessage,
6514
- onImageClick: handleImageClick,
6515
- onExecutionTraceClick,
6516
- messageActions,
6517
- retryDisabled: isWaitingForResponse,
6518
- typingSpeed: config.typingSpeed ?? 4,
6519
- userActionPrompts,
6520
- notifications,
6521
- onSubmitUserAction: isUserActionSupported ? submitUserAction2 : void 0,
6522
- onCancelUserAction: isUserActionSupported ? cancelUserAction2 : void 0,
6523
- onResendUserAction: isUserActionSupported ? resendUserAction2 : void 0,
6524
- onDismissNotification: dismissNotification,
6525
- onSubmitFeedback: handleSubmitFeedback
6526
- }
6527
- ),
6528
- /* @__PURE__ */ jsxRuntime.jsx(
6529
- StreamingIndicatorV2,
7817
+ initial: hasEverSentMessage ? { opacity: 0 } : false,
7818
+ animate: { opacity: 1 },
7819
+ transition: { duration: 0.3 },
7820
+ className: "payman-v2-chat-layout",
7821
+ children: [
7822
+ /* @__PURE__ */ jsxRuntime.jsx(
7823
+ MessageListV2,
7824
+ {
7825
+ ref: messageListV2Ref,
7826
+ messages,
7827
+ isStreaming: isWaitingForResponse,
7828
+ sidePanelOpen: !!pdfSheet,
7829
+ onEditUserMessage: handleEditMessageDraft,
7830
+ onRetryUserMessage: handleRetryUserMessage,
7831
+ onImageClick: handleImageClick,
7832
+ onExecutionTraceClick,
7833
+ messageActions,
7834
+ retryDisabled: isWaitingForResponse,
7835
+ typingSpeed: config.typingSpeed ?? 4,
7836
+ userActionPrompts,
7837
+ notifications,
7838
+ onSubmitUserAction: isUserActionSupported ? submitUserAction2 : void 0,
7839
+ onCancelUserAction: isUserActionSupported ? cancelUserAction2 : void 0,
7840
+ onResendUserAction: isUserActionSupported ? resendUserAction2 : void 0,
7841
+ onDismissNotification: dismissNotification,
7842
+ onSubmitFeedback: handleSubmitFeedback
7843
+ }
7844
+ ),
7845
+ /* @__PURE__ */ jsxRuntime.jsx(
7846
+ StreamingIndicatorV2,
7847
+ {
7848
+ isStreaming: isWaitingForResponse,
7849
+ loadingAnimation: config.loadingAnimation
7850
+ }
7851
+ ),
7852
+ hasAskPermission && /* @__PURE__ */ jsxRuntime.jsx(
7853
+ ChatInputV2,
7854
+ {
7855
+ ref: chatInputV2Ref,
7856
+ onSend: handleV2Send,
7857
+ onCancel: cancelStream,
7858
+ disabled: isV2InputDisabled,
7859
+ isStreaming: isWaitingForResponse,
7860
+ isUploadingAttachments: attachmentUpload.isUploading,
7861
+ attachmentsReady: attachmentUpload.allReady,
7862
+ hasAttachmentUploadErrors: attachmentUpload.hasErrors,
7863
+ attachmentUploadStatusById: attachmentUpload.statusById,
7864
+ uploadedAttachmentPayloads: attachmentUpload.payloads,
7865
+ onAttachmentsChange: handleAttachmentsChange,
7866
+ placeholder: isRecording ? "Listening..." : placeholder,
7867
+ enableVoice: config.enableVoice === true,
7868
+ transcribedText: config.enableVoice === true ? transcribedText : "",
7869
+ voiceAvailable: config.enableVoice === true && voiceAvailable,
7870
+ isRecording,
7871
+ onVoicePress: config.enableVoice === true ? handleVoicePress : void 0,
7872
+ onCancelRecording: handleCancelRecording,
7873
+ onConfirmRecording: handleConfirmRecording,
7874
+ showResetSession,
7875
+ onResetSession: requestResetSession,
7876
+ showAttachmentButton: attachmentSettings.showAttachmentButton,
7877
+ showUploadImageButton: attachmentSettings.showUploadImageButton,
7878
+ showAttachFileButton: attachmentSettings.showAttachFileButton,
7879
+ allowedImageExtensions: attachmentSettings.allowedImageExtensions,
7880
+ allowedFileExtensions: attachmentSettings.allowedFileExtensions,
7881
+ maxCount: attachmentSettings.maxCount,
7882
+ maxFileBytes: attachmentSettings.maxFileBytes,
7883
+ maxTotalBytes: attachmentSettings.maxTotalBytes,
7884
+ onUploadImageClick,
7885
+ onAttachFileClick,
7886
+ editingMessageId,
7887
+ onClearEditing: handleClearEditing,
7888
+ analysisMode: enableDeepModeToggle ? analysisMode : void 0,
7889
+ onAnalysisModeChange: enableDeepModeToggle ? setAnalysisMode : void 0,
7890
+ slashCommands
7891
+ }
7892
+ )
7893
+ ]
7894
+ },
7895
+ "v2-chat"
7896
+ ) }) }),
7897
+ pdfPreviewMode === "split" && /* @__PURE__ */ jsxRuntime.jsx(
7898
+ PdfSheetV2,
6530
7899
  {
6531
- isStreaming: isWaitingForResponse,
6532
- loadingAnimation: config.loadingAnimation
7900
+ src: pdfSheet?.href ?? null,
7901
+ title: pdfSheet?.title ?? "",
7902
+ onClose: closePdfSheet,
7903
+ mode: "split"
6533
7904
  }
6534
7905
  ),
6535
- hasAskPermission && /* @__PURE__ */ jsxRuntime.jsx(
6536
- ChatInputV2,
7906
+ pdfPreviewMode === "sheet" && /* @__PURE__ */ jsxRuntime.jsx(
7907
+ "div",
6537
7908
  {
6538
- ref: chatInputV2Ref,
6539
- onSend: handleV2Send,
6540
- onCancel: cancelStream,
6541
- disabled: isV2InputDisabled,
6542
- isStreaming: isWaitingForResponse,
6543
- placeholder: isRecording ? "Listening..." : placeholder,
6544
- enableVoice: config.enableVoice === true,
6545
- transcribedText: config.enableVoice === true ? transcribedText : "",
6546
- voiceAvailable: config.enableVoice === true && voiceAvailable,
6547
- isRecording,
6548
- onVoicePress: config.enableVoice === true ? handleVoicePress : void 0,
6549
- onCancelRecording: handleCancelRecording,
6550
- onConfirmRecording: handleConfirmRecording,
6551
- showResetSession,
6552
- onResetSession: requestResetSession,
6553
- showAttachmentButton,
6554
- showUploadImageButton,
6555
- showAttachFileButton,
6556
- onUploadImageClick,
6557
- onAttachFileClick,
6558
- editingMessageId,
6559
- onClearEditing: handleClearEditing,
6560
- analysisMode: enableDeepModeToggle ? analysisMode : void 0,
6561
- onAnalysisModeChange: enableDeepModeToggle ? setAnalysisMode : void 0,
6562
- slashCommands
7909
+ className: "payman-v2-pdf-sheet-top-anchor",
7910
+ "aria-hidden": !pdfSheet,
7911
+ children: /* @__PURE__ */ jsxRuntime.jsx(
7912
+ PdfSheetV2,
7913
+ {
7914
+ src: pdfSheet?.href ?? null,
7915
+ title: pdfSheet?.title ?? "",
7916
+ onClose: closePdfSheet,
7917
+ mode: "sheet"
7918
+ }
7919
+ )
6563
7920
  }
6564
7921
  )
6565
7922
  ]
6566
- },
6567
- "v2-chat"
6568
- ) }),
7923
+ }
7924
+ ),
6569
7925
  /* @__PURE__ */ jsxRuntime.jsx(
6570
7926
  ImageLightboxV2,
6571
7927
  {
@@ -6600,19 +7956,39 @@ var PaymanChat = react.forwardRef(
6600
7956
  function PaymanChat2(props, ref) {
6601
7957
  const mergedCallbacks = useSentryChatCallbacks(props.callbacks, props.config);
6602
7958
  const chat = useChatV2(props.config, mergedCallbacks);
6603
- return /* @__PURE__ */ jsxRuntime.jsx(PaymanChatInner, { ...props, chat, ref });
7959
+ const attachmentUpload = useAttachmentUpload(props.config);
7960
+ return /* @__PURE__ */ jsxRuntime.jsx(
7961
+ PaymanChatInner,
7962
+ {
7963
+ ...props,
7964
+ chat,
7965
+ attachmentUpload,
7966
+ ref
7967
+ }
7968
+ );
6604
7969
  }
6605
7970
  );
6606
7971
 
6607
7972
  exports.PaymanChat = PaymanChat;
6608
7973
  exports.PaymanChatContext = PaymanChatContext;
7974
+ exports.PdfSheetV2 = PdfSheetV2;
6609
7975
  exports.UserActionStaleError = UserActionStaleError;
7976
+ exports.buildSignedUrlEndpoint = buildSignedUrlEndpoint;
6610
7977
  exports.cancelUserAction = cancelUserAction;
6611
7978
  exports.captureSentryError = captureSentryError;
6612
7979
  exports.cn = cn;
7980
+ exports.formatAttachmentBytes = formatAttachmentBytes;
6613
7981
  exports.formatDate = formatDate;
7982
+ exports.getPdfTitleFromUrl = getPdfTitleFromUrl;
7983
+ exports.isPdfUrl = isPdfUrl;
7984
+ exports.mapExecutionHistoryPageToChatMessages = mapExecutionHistoryPageToChatMessages;
7985
+ exports.mapExecutionHistoryToChatMessages = mapExecutionHistoryToChatMessages;
6614
7986
  exports.resendUserAction = resendUserAction;
7987
+ exports.stripAttachmentsSuffixFromIntent = stripAttachmentsSuffixFromIntent;
6615
7988
  exports.submitUserAction = submitUserAction;
7989
+ exports.uploadAttachment = uploadAttachment;
7990
+ exports.uploadAttachments = uploadAttachments;
7991
+ exports.useAttachmentUpload = useAttachmentUpload;
6616
7992
  exports.useChatV2 = useChatV2;
6617
7993
  exports.usePaymanChat = usePaymanChat;
6618
7994
  exports.useVoice = useVoice;