@arbidocs/react 0.4.10 → 0.4.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -82,6 +82,8 @@ interface ArbiChatMessage {
82
82
  citations?: ArbiChatCitation[];
83
83
  /** The reader stopped the answer; `content` is what had streamed. */
84
84
  stopped?: boolean;
85
+ /** The backend id of a finished assistant answer: what a follow-up chains from. */
86
+ responseId?: string;
85
87
  }
86
88
  interface ArbiChatThread {
87
89
  id: string;
@@ -100,6 +102,15 @@ interface UseArbiChatOptions {
100
102
  model?: string;
101
103
  /** Longest auto-title in characters (used until the backend has titled the conversation). Default 48. */
102
104
  titleLength?: number;
105
+ /**
106
+ * When the answer stream ends without the terminal `response.completed` event (a dropped
107
+ * connection, a stalled run), the stored message is polled this often (ms) and for at most
108
+ * `timeoutMs` before the turn is shown as failed with a Retry. Default 3000 / 120000.
109
+ */
110
+ streamRecovery?: {
111
+ intervalMs?: number;
112
+ timeoutMs?: number;
113
+ };
103
114
  }
104
115
  interface ArbiChat {
105
116
  threads: ArbiChatThread[];
package/dist/index.d.ts CHANGED
@@ -82,6 +82,8 @@ interface ArbiChatMessage {
82
82
  citations?: ArbiChatCitation[];
83
83
  /** The reader stopped the answer; `content` is what had streamed. */
84
84
  stopped?: boolean;
85
+ /** The backend id of a finished assistant answer: what a follow-up chains from. */
86
+ responseId?: string;
85
87
  }
86
88
  interface ArbiChatThread {
87
89
  id: string;
@@ -100,6 +102,15 @@ interface UseArbiChatOptions {
100
102
  model?: string;
101
103
  /** Longest auto-title in characters (used until the backend has titled the conversation). Default 48. */
102
104
  titleLength?: number;
105
+ /**
106
+ * When the answer stream ends without the terminal `response.completed` event (a dropped
107
+ * connection, a stalled run), the stored message is polled this often (ms) and for at most
108
+ * `timeoutMs` before the turn is shown as failed with a Retry. Default 3000 / 120000.
109
+ */
110
+ streamRecovery?: {
111
+ intervalMs?: number;
112
+ timeoutMs?: number;
113
+ };
103
114
  }
104
115
  interface ArbiChat {
105
116
  threads: ArbiChatThread[];
package/dist/index.js CHANGED
@@ -1,10 +1,7 @@
1
1
  import { formatRelativeTime } from './chunk-FSMIN5VX.js';
2
2
  export { DEFAULT_NOTIFY_TONE, NotificationBell, Toaster, formatRelativeTime, notify, seedNotifications, selectUnread, toast, useNotify } from './chunk-FSMIN5VX.js';
3
- import { useDocuments, useConversations, useAssistantQuery, useConversationThreads, arbiQueryKeys, useWorkspaces, useParsedDocument, useTags, useActivity, useThumbnails } from './chunk-UV5ZEUNV.js';
4
- export { DEFAULT_USD_PER_CREDIT, arbiQueryKeys, cleanDraftMarkdown, creditsToUsd, formatCredits, isUsageMetered, promoteBoldTitle, repairParsedTitle, stripAgentPreamble, unescapeMarkdown, unwrapCitationLinks, useActivity, useAgents, useAssistantQuery, useConfigs, useConversationThreads, useConversations, useDeleteAgents, useDocuments, useFacts, useHealth, useParsedDocument, useProjectInvoices, useProjectUsage, useProjects, useProjectsUsage, useSessions, useTags, useTasks, useThumbnails, useUserUsageToday, useWorkspaces } from './chunk-UV5ZEUNV.js';
5
- import { AudioViewer, ImageViewer, VideoViewer } from './chunk-HDCRPPBW.js';
6
- import { useArbi } from './chunk-J4MX2W4H.js';
7
- export { ArbiProvider, useArbi } from './chunk-J4MX2W4H.js';
3
+ import { useDocuments, useConversations, useAssistantQuery, useConversationThreads, arbiQueryKeys, useWorkspaces, useParsedDocument, useTags, useActivity, useThumbnails } from './chunk-QFELRTTG.js';
4
+ export { DEFAULT_USD_PER_CREDIT, arbiQueryKeys, cleanDraftMarkdown, creditsToUsd, formatCredits, isUsageMetered, promoteBoldTitle, repairParsedTitle, stripAgentPreamble, unescapeMarkdown, unwrapCitationLinks, useActivity, useAgents, useAssistantQuery, useConfigs, useConversationThreads, useConversations, useDeleteAgents, useDocuments, useFacts, useHealth, useParsedDocument, useProjectInvoices, useProjectUsage, useProjects, useProjectsUsage, useSessions, useTags, useTasks, useThumbnails, useUserUsageToday, useWorkspaces } from './chunk-QFELRTTG.js';
8
5
  import { MarkMenu, AiMarkdown, Card, Button, Badge, TagEntity, Input, FILE_TYPE_COLORS, getEntityIcon, UserEntity, getEntityColorStyles, AgentBadge, Textarea, Label, Slider, Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DocumentEntity, DocumentCard, useAudioRecorder, MicWaveformIndicator, IconToggle, classifyMediaError, formatElapsed, PassageCard } from './chunk-NVMQLJI4.js';
9
6
  export { AgentBadge, AiMarkdown, ArbiBotHeadIcon, AudIcon, Avatar, AvatarFallback, AvatarImage, BASE_COLORS, Badge, Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DictateProvider, DocumentCard, DocumentEntity, ENTITY_MAPPINGS, EmlIcon, EntityIcon, EntityPill, FILE_TYPE_COLORS, HoverCard, HoverCardContent, HoverCardTrigger, IconToggle, ImgIcon, InlineMicButton, Input, Label, MarkMenu, MicRecordingOverlay, MicWaveformIndicator, NEUTRAL_GRAY, PLACEHOLDER_COLOR, PassageCard, PdfIcon, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, PresenceLabel, Progress, ProjectEntity, RadioGroup, RadioGroupItem, RagDot, RelevanceDot, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Slider, StatusPill, Switch, TAG_TYPE_ICONS, TAG_TYPE_LABELS, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, TagEntity, Textarea, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UserEntity, UserMenu, VISIBILITY_ICONS, VidIcon, VisibilityIcon, badgeVariants, buttonVariants, cardVariants, configureEntityRegistry, configureRagLabels, configureStatusRegistry, formatElapsed, getDocIcon, getEntityColor, getEntityColorStyles, getEntityIcon, getEntityLabel, getStatusVariant, getTagTypeIcon, getTagTypeLabel, getWorkspaceColor, toggleVariants, useAudioRecorder } from './chunk-NVMQLJI4.js';
10
7
  import { ScrollArea, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from './chunk-N5SVBDLC.js';
@@ -13,6 +10,9 @@ import { normalizeCitationMarkup, MarkdownRenderer } from './chunk-TGGQGFS3.js';
13
10
  export { MarkdownRenderer, MarkdownStreamingContext, closeOpenEmphasis, markdownBaseComponents, normalizeCitationMarkup, splitMarkdownBlocks, trimOpenCitation } from './chunk-TGGQGFS3.js';
14
11
  import { cn } from './chunk-SN5YV6FF.js';
15
12
  export { cn } from './chunk-SN5YV6FF.js';
13
+ import { AudioViewer, ImageViewer, VideoViewer } from './chunk-HDCRPPBW.js';
14
+ import { useArbi } from './chunk-J4MX2W4H.js';
15
+ export { ArbiProvider, useArbi } from './chunk-J4MX2W4H.js';
16
16
  import React, { memo, useState, createContext, useMemo, useCallback, useEffect, useRef, useLayoutEffect, useContext, useId, Fragment as Fragment$1 } from 'react';
17
17
  import { resolveCitations, summarizeCitations, connectWithReconnect } from '@arbidocs/sdk/browser';
18
18
  export { Arbi, ArbiError } from '@arbidocs/sdk/browser';
@@ -75,22 +75,38 @@ var storedThread = (conversationId) => ({
75
75
  isLoaded: false
76
76
  });
77
77
  var publicId = (t) => t.conversationId ?? t.key;
78
+ var UNFINISHED_STATUSES = /* @__PURE__ */ new Set(["failed", "in_progress", "queued"]);
79
+ var FAILED_MESSAGE = "The assistant request failed.";
80
+ var UNFINISHED_MESSAGE = "The answer did not finish. Try again.";
81
+ var isUnfinished = (m) => UNFINISHED_STATUSES.has(m.status);
78
82
  function messagesFromHistory(history) {
79
- return history.filter((m) => m.role !== "system" && m.content.trim() !== "").map((m) => {
83
+ return history.filter(
84
+ (m) => m.role !== "system" && (m.content.trim() !== "" || m.role === "assistant" && isUnfinished(m))
85
+ ).map((m) => {
80
86
  if (m.role === "user") {
81
87
  return { id: m.external_id, role: "user", content: m.content, status: "complete" };
82
88
  }
83
89
  const citations = toChatCitations({ tools: m.tools });
90
+ const unfinished = isUnfinished(m);
84
91
  return {
85
92
  id: m.external_id,
86
93
  role: "assistant",
87
94
  content: m.content,
88
- status: m.status === "failed" ? "error" : "complete",
89
- ...m.status === "failed" ? { error: "The assistant request failed." } : {},
95
+ status: unfinished ? "error" : "complete",
96
+ ...unfinished ? { error: m.status === "failed" ? FAILED_MESSAGE : UNFINISHED_MESSAGE } : {},
97
+ ...unfinished ? {} : { responseId: m.external_id },
90
98
  ...citations.length > 0 ? { citations } : {}
91
99
  };
92
100
  });
93
101
  }
102
+ function chainId(messages) {
103
+ for (let i = messages.length - 1; i >= 0; i--) {
104
+ const m = messages[i];
105
+ if (m.role === "assistant" && m.status === "complete" && m.responseId) return m.responseId;
106
+ }
107
+ return null;
108
+ }
109
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
94
110
  function titleFromQuestion(question, maxLength = 48) {
95
111
  const flat = question.replace(/\s+/g, " ").trim();
96
112
  if (flat.length <= maxLength) return flat;
@@ -103,7 +119,9 @@ var messageOf = (e, fallback) => e instanceof Error ? e.message : fallback;
103
119
  var isAbort = (e) => e instanceof DOMException && e.name === "AbortError";
104
120
  var TITLE_REFRESH_DELAYS_MS = [2500, 6e3, 12e3];
105
121
  function useArbiChat(options = {}) {
106
- const { workspaceId, model, titleLength = 48 } = options;
122
+ const { workspaceId, model, titleLength = 48, streamRecovery } = options;
123
+ const recoveryIntervalMs = streamRecovery?.intervalMs ?? 3e3;
124
+ const recoveryTimeoutMs = streamRecovery?.timeoutMs ?? 12e4;
107
125
  const arbi = useArbi();
108
126
  const { data: docs = [], isLoading: isLoadingDocuments, refetch } = useDocuments(workspaceId);
109
127
  const {
@@ -167,13 +185,14 @@ function useArbiChat(options = {}) {
167
185
  const data = loaded.data;
168
186
  if (!needsLoad || !data || data.conversation_ext_id !== active.conversationId) return;
169
187
  const primary = [...data.threads].sort((a, b) => b.history.length - a.history.length)[0];
188
+ const messages = messagesFromHistory(primary?.history ?? []);
170
189
  setThreads(
171
190
  (all) => all.map(
172
191
  (t) => t.key === active.key ? {
173
192
  ...t,
174
193
  isLoaded: true,
175
- messages: messagesFromHistory(primary?.history ?? []),
176
- responseId: primary?.leaf_message_ext_id ?? null
194
+ messages,
195
+ responseId: chainId(messages)
177
196
  } : t
178
197
  )
179
198
  );
@@ -212,6 +231,29 @@ function useArbiChat(options = {}) {
212
231
  (delay) => setTimeout(() => void refetchConversations(), delay)
213
232
  );
214
233
  }, [refetchConversations]);
234
+ const recoverStoredAnswer = useCallback(
235
+ async (conversationId, messageId, signal) => {
236
+ if (!conversationId || !messageId) return null;
237
+ const deadline = Date.now() + recoveryTimeoutMs;
238
+ let failedReads = 0;
239
+ for (; ; ) {
240
+ try {
241
+ const { threads: all } = await arbi.conversations.getThreads(conversationId);
242
+ const found = all.flatMap((t) => t.history).find((m) => m.external_id === messageId);
243
+ if (found?.status === "completed" && found.content.trim() !== "") {
244
+ return { kind: "completed", message: found };
245
+ }
246
+ if (found?.status === "failed") return { kind: "failed" };
247
+ failedReads = 0;
248
+ } catch {
249
+ if (++failedReads >= 3) return null;
250
+ }
251
+ if (signal.aborted || Date.now() + recoveryIntervalMs > deadline) return null;
252
+ await sleep(recoveryIntervalMs);
253
+ }
254
+ },
255
+ [arbi, recoveryIntervalMs, recoveryTimeoutMs]
256
+ );
215
257
  const ask = useCallback(
216
258
  async (thread, text) => {
217
259
  const userMessage = {
@@ -237,6 +279,17 @@ function useArbiChat(options = {}) {
237
279
  let buffer = "";
238
280
  let steps = 0;
239
281
  let streamError = null;
282
+ let conversationId = thread.conversationId;
283
+ let backendMessageId = null;
284
+ const revertChain = () => patchThread(thread.key, (t) => ({ ...t, responseId: chainId(t.messages) }));
285
+ const fail = (error2, content) => {
286
+ patchMessage(thread.key, answer.id, {
287
+ status: "error",
288
+ error: error2,
289
+ ...content !== void 0 ? { content } : {}
290
+ });
291
+ revertChain();
292
+ };
240
293
  try {
241
294
  const result = await mutateAsync({
242
295
  question: text,
@@ -246,6 +299,7 @@ function useArbiChat(options = {}) {
246
299
  signal: controller.signal,
247
300
  onStreamStart: (data) => {
248
301
  if (data.assistant_message_ext_id) {
302
+ backendMessageId = data.assistant_message_ext_id;
249
303
  patchThread(thread.key, (t) => ({
250
304
  ...t,
251
305
  responseId: data.assistant_message_ext_id ?? t.responseId
@@ -253,6 +307,7 @@ function useArbiChat(options = {}) {
253
307
  }
254
308
  },
255
309
  onUserMessage: (ev) => {
310
+ conversationId = conversationId ?? ev.conversation_ext_id;
256
311
  patchThread(thread.key, (t) => ({
257
312
  ...t,
258
313
  conversationId: t.conversationId ?? ev.conversation_ext_id
@@ -279,20 +334,43 @@ function useArbiChat(options = {}) {
279
334
  });
280
335
  const finalText = result.text || buffer;
281
336
  const failure = streamError || result.errors[0] || null;
337
+ const messageId = result.assistantMessageExtId ?? backendMessageId;
282
338
  if (failure && !finalText) {
283
- patchMessage(thread.key, answer.id, { status: "error", error: failure });
339
+ fail(failure);
340
+ } else if (!result.completed && !failure) {
341
+ const stored2 = await recoverStoredAnswer(conversationId, messageId, controller.signal);
342
+ if (stored2?.kind === "completed") {
343
+ const citations = toChatCitations({ tools: stored2.message.tools });
344
+ patchMessage(thread.key, answer.id, {
345
+ status: "complete",
346
+ content: stored2.message.content,
347
+ stepCount: result.agentSteps.length || steps || void 0,
348
+ responseId: stored2.message.external_id,
349
+ ...citations.length > 0 ? { citations } : {}
350
+ });
351
+ patchThread(thread.key, (t) => ({ ...t, responseId: stored2.message.external_id }));
352
+ } else if (controller.signal.aborted) {
353
+ patchMessage(thread.key, answer.id, {
354
+ status: "complete",
355
+ content: finalText,
356
+ stopped: true
357
+ });
358
+ } else {
359
+ fail(
360
+ stored2?.kind === "failed" ? FAILED_MESSAGE : UNFINISHED_MESSAGE,
361
+ finalText || void 0
362
+ );
363
+ }
284
364
  } else {
285
365
  const citations = toChatCitations(result.metadata);
286
366
  patchMessage(thread.key, answer.id, {
287
367
  status: "complete",
288
368
  content: finalText,
289
369
  stepCount: result.agentSteps.length || steps || void 0,
370
+ ...messageId ? { responseId: messageId } : {},
290
371
  ...citations.length > 0 ? { citations } : {}
291
372
  });
292
- patchThread(thread.key, (t) => ({
293
- ...t,
294
- responseId: result.assistantMessageExtId ?? t.responseId
295
- }));
373
+ patchThread(thread.key, (t) => ({ ...t, responseId: messageId ?? t.responseId }));
296
374
  }
297
375
  } catch (e) {
298
376
  if (controller.signal.aborted || isAbort(e)) {
@@ -302,10 +380,7 @@ function useArbiChat(options = {}) {
302
380
  stopped: true
303
381
  });
304
382
  } else {
305
- patchMessage(thread.key, answer.id, {
306
- status: "error",
307
- error: messageOf(e, "The assistant request failed.")
308
- });
383
+ fail(messageOf(e, FAILED_MESSAGE));
309
384
  }
310
385
  } finally {
311
386
  if (abortRef.current === controller) abortRef.current = null;
@@ -313,7 +388,16 @@ function useArbiChat(options = {}) {
313
388
  refreshTitles();
314
389
  }
315
390
  },
316
- [mutateAsync, patchMessage, patchThread, refreshTitles, selectedIds, model, titleLength]
391
+ [
392
+ mutateAsync,
393
+ patchMessage,
394
+ patchThread,
395
+ refreshTitles,
396
+ recoverStoredAnswer,
397
+ selectedIds,
398
+ model,
399
+ titleLength
400
+ ]
317
401
  );
318
402
  const send = useCallback(
319
403
  async (text) => {
@@ -332,7 +416,8 @@ function useArbiChat(options = {}) {
332
416
  const failed = messages[messages.length - 1];
333
417
  const question = messages[messages.length - 2];
334
418
  if (failed?.status !== "error" || question?.role !== "user") return;
335
- const trimmed = { ...active, messages: messages.slice(0, -2) };
419
+ const rest = messages.slice(0, -2);
420
+ const trimmed = { ...active, messages: rest, responseId: chainId(rest) };
336
421
  patchThread(active.key, () => trimmed);
337
422
  await ask(trimmed, question.content);
338
423
  }, [active, ask, isRunning, patchThread]);