@epam/ai-dial-chat-hooks 1.1.0-dev.433 → 1.1.0-dev.438

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.
@@ -256,7 +256,7 @@ var s = (e) => e >= 1e6 ? `${e / 1e6}M tokens` : e >= 1e3 ? `${e / 1e3}K tokens`
256
256
  };
257
257
  }, T = (e) => {
258
258
  if (e == null) return;
259
- let { unit: t } = e, n = Object.entries(e).flatMap(([e, n]) => {
259
+ let t = typeof e.unit == "string" ? e.unit : void 0, n = Object.entries(e).flatMap(([e, n]) => {
260
260
  if (e === l || typeof n != "string" || n.trim() === "") return [];
261
261
  let i = r(n, t);
262
262
  return i == null ? [] : [{
@@ -1,48 +1,49 @@
1
1
  import { collectAttachmentRefs as e } from "./attachment-refs.js";
2
2
  import { resolveDialFileBucketAndPath as t } from "./dial-file-resolve.js";
3
- import { generateUUID as n } from "@epam/ai-dial-chat-shared";
3
+ import { generateUUID as n, sanitizeConversationName as r, stripTrailingDots as i, truncateToUtf8Bytes as a } from "@epam/ai-dial-chat-shared";
4
4
  //#region src/conversation/conversation-transfer/import-conversation.ts
5
- var r = class extends Error {
5
+ var o = class extends Error {
6
6
  constructor(e) {
7
7
  super(e), this.name = "UnsupportedImportFormatError";
8
8
  }
9
- }, i = (e) => typeof e == "object" && !!e, a = 5, o = (e) => {
9
+ }, s = (e) => typeof e == "object" && !!e, c = 5, l = (e) => {
10
10
  let t;
11
11
  try {
12
12
  t = JSON.parse(e);
13
13
  } catch {
14
- throw new r("Import file is not valid JSON");
14
+ throw new o("Import file is not valid JSON");
15
15
  }
16
- if (!i(t) || t.version !== a || !Array.isArray(t.history)) throw new r("Import file is not a supported export format");
16
+ if (!s(t) || t.version !== c || !Array.isArray(t.history)) throw new o("Import file is not a supported export format");
17
17
  let n = Array.isArray(t.folders) ? t.folders : [];
18
18
  return {
19
- version: a,
19
+ version: c,
20
20
  history: t.history,
21
21
  folders: n
22
22
  };
23
- }, s = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, c = (e) => {
24
- let t = e.lastIndexOf("__");
25
- if (t < 0) return e;
26
- let n = e.slice(t + 2);
27
- return s.test(n) ? e.slice(0, t) : e;
28
- }, l = "conversations/", u = (e) => e.startsWith(l) ? e.slice(14) : e, d = (e, t) => {
29
- let r = u(e.id), i = r.split("/"), a = u(e.folderId), o = i[0], s = a.startsWith(`${o}/`) ? a.slice(o.length + 1).split("/").filter(Boolean) : [], l = i.slice(1).slice(s.length), d = l.slice(0, -1), f = `${c(l.at(-1) ?? i.at(-1) ?? r)}__${n()}`, p = [
30
- ...s,
31
- ...d,
32
- f
33
- ].join("/"), m = s.length ? `${t}/${s.join("/")}` : t;
23
+ }, u = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, d = "__", f = 255, p = "applications", m = /[-+]/, h = /^\d+$/, g = (e) => e ? e.split(m)[0].split(".").every((e) => h.test(e)) : !1, _ = (e) => i(a(e.replace(/\r\n|\r/g, "\n").split("\n").map((e) => r(e).trim()).filter(Boolean)[0] ?? "", f)).trimEnd(), v = (e, t, r) => {
24
+ let i = e.split(d);
25
+ if (i.length < 2) return [e, n()].join(d);
26
+ let a = r && g(i[1]) ? i.slice(0, 2) : i.slice(0, 1), o = a.length > 1 ? u.test(i[i.length - 1]) : i.length >= 3, s = i.slice(a.length, o ? -1 : void 0).join(d);
27
+ return [
28
+ ...a,
29
+ t || s,
30
+ n()
31
+ ].join(d);
32
+ }, y = "conversations/", b = (e) => e.startsWith(y) ? e.slice(14) : e, x = (e, t) => {
33
+ let n = b(e.id), r = n.split("/"), i = b(e.folderId), a = r[0], o = i.startsWith(`${a}/`) ? i.slice(a.length + 1).split("/").filter(Boolean) : [], s = r.slice(1).slice(o.length), c = s.slice(0, -1), l = s.at(-1) ?? r.at(-1) ?? n, u = [...o, ...c], d = _(e.name), f = v(l, d, u[0] === p), m = [...u, f].join("/"), h = o.length ? `${t}/${o.join("/")}` : t;
34
34
  return {
35
35
  conversation: {
36
36
  ...e,
37
- id: `${t}/${p}`,
38
- folderId: m
37
+ ...d ? { name: d } : {},
38
+ id: `${t}/${m}`,
39
+ folderId: h
39
40
  },
40
- subPath: p
41
+ subPath: m
41
42
  };
42
- }, f = (e) => {
43
- let t = u(e.folderId).split("/").filter(Boolean).slice(1);
43
+ }, S = (e) => {
44
+ let t = b(e.folderId).split("/").filter(Boolean).slice(1);
44
45
  return t.length ? t.join(" / ") : void 0;
45
- }, p = (e) => e.map((e) => `"${e}"`).join(", "), m = (e, t) => ({
46
+ }, C = (e) => e.map((e) => `"${e}"`).join(", "), w = (e, t) => ({
46
47
  ...e,
47
48
  messages: e.messages.map((e) => {
48
49
  let n = e.custom_content?.attachments;
@@ -64,12 +65,12 @@ var r = class extends Error {
64
65
  }
65
66
  } : e;
66
67
  })
67
- }), h = (e) => e.split("/").pop() || e, g = (n, r, i) => {
68
+ }), T = (e) => e.split("/").pop() || e, E = (n, r, i) => {
68
69
  let a = [], o = [];
69
70
  for (let s of e(n)) {
70
71
  let e = t(s.fileId);
71
72
  if (!e) {
72
- o.push(h(s.fileId));
73
+ o.push(T(s.fileId));
73
74
  continue;
74
75
  }
75
76
  let n = e.path.split("/").pop() ?? e.path, c = r.get(e.path);
@@ -90,4 +91,4 @@ var r = class extends Error {
90
91
  };
91
92
  };
92
93
  //#endregion
93
- export { r as UnsupportedImportFormatError, p as formatQuotedNameList, f as getFolderBreadcrumb, o as parseImportEnvelope, g as planAttachmentUploads, d as rebaseConversationId, m as rewriteAttachmentUrls };
94
+ export { o as UnsupportedImportFormatError, C as formatQuotedNameList, S as getFolderBreadcrumb, l as parseImportEnvelope, E as planAttachmentUploads, x as rebaseConversationId, w as rewriteAttachmentUrls };
@@ -1,5 +1,9 @@
1
1
  //#region src/conversation/create-chat-stream-api.ts
2
- var e = { "Content-Type": "application/json" }, t = (e, t, n) => {
2
+ var e = { "Content-Type": "application/json" }, t = 409, n = "A response is already being generated in this conversation. Wait for it to finish or stop it before sending another message.", r = class extends Error {
3
+ constructor(e = n) {
4
+ super(e), this.name = "GenerationConflictError";
5
+ }
6
+ }, i = (e, t, n) => {
3
7
  let r = e.trim();
4
8
  if (!r || r.startsWith(":") || !r.startsWith("data: ")) return;
5
9
  let i = r.slice(6);
@@ -11,75 +15,79 @@ var e = { "Content-Type": "application/json" }, t = (e, t, n) => {
11
15
  }
12
16
  t(e);
13
17
  } catch {}
14
- }, n = (n) => {
15
- let { completionsBasePath: r } = n, i = (...e) => (n.fetchImpl ?? fetch)(...e);
18
+ }, a = (n) => {
19
+ let { completionsBasePath: a } = n, o = (...e) => (n.fetchImpl ?? fetch)(...e);
16
20
  return {
17
- streamCompletion: (a, o, s, c, l, u, d, f, p) => {
18
- let { onChunk: m, onComplete: h, onError: g, signal: _ } = c;
21
+ streamCompletion: (s, c, l, u, d, f, p, m, h) => {
22
+ let { onChunk: g, onComplete: _, onError: v, signal: y } = u;
19
23
  (async () => {
20
- let c;
24
+ let u;
21
25
  try {
22
26
  let t = n.getTimezone?.();
23
- c = await i(`${r}/completions`, {
27
+ u = await o(`${a}/completions`, {
24
28
  method: "POST",
25
29
  credentials: "include",
26
- signal: _,
30
+ signal: y,
27
31
  headers: {
28
32
  ...e,
29
33
  ...n.getCsrfToken() == null ? {} : { "X-CSRF-Token": n.getCsrfToken() },
30
34
  ...t ? { "X-Timezone": t } : {}
31
35
  },
32
36
  body: JSON.stringify({
33
- path: a,
34
- message: o ?? "",
35
- model: s,
36
- custom_content: l || {},
37
- ...u != null && { generationId: u },
38
- ...d != null && { mode: d },
39
- ...f != null && { messageIndex: f },
40
- ...p != null && { clientChannelId: p }
37
+ path: s,
38
+ message: c ?? "",
39
+ model: l,
40
+ custom_content: d || {},
41
+ ...f != null && { generationId: f },
42
+ ...p != null && { mode: p },
43
+ ...m != null && { messageIndex: m },
44
+ ...h != null && { clientChannelId: h }
41
45
  })
42
46
  });
43
47
  } catch (e) {
44
48
  if (e instanceof Error && e.name === "AbortError") return;
45
- g(e instanceof Error ? e : Error(String(e)));
49
+ v(e instanceof Error ? e : Error(String(e)));
46
50
  return;
47
51
  }
48
- let v = c.headers.get("x-csrf-token");
49
- if (v && n.setCsrfToken(v), !c.ok) {
50
- g(/* @__PURE__ */ Error(`Stream request failed with status ${c.status}`));
52
+ let b = u.headers.get("x-csrf-token");
53
+ if (b && n.setCsrfToken(b), !u.ok) {
54
+ if (u.status === t) {
55
+ v(new r());
56
+ return;
57
+ }
58
+ v(/* @__PURE__ */ Error(`Stream request failed with status ${u.status}`));
51
59
  return;
52
60
  }
53
- if (!c.body) {
54
- g(/* @__PURE__ */ Error("No response body"));
61
+ if (!u.body) {
62
+ v(/* @__PURE__ */ Error("No response body"));
55
63
  return;
56
64
  }
57
- let y = c.body.getReader(), b = new TextDecoder(), x = "", S = !1, C = (e) => {
58
- S = !0, g(e);
65
+ let x = u.body.getReader(), S = new TextDecoder(), C = "", w = !1, T = (e) => {
66
+ w = !0, v(e);
59
67
  };
60
68
  try {
61
69
  for (;;) {
62
- let { done: e, value: n } = await y.read();
70
+ let { done: e, value: t } = await x.read();
63
71
  if (e) {
64
- x.trim() && t(x, m, C);
72
+ C.trim() && i(C, g, T);
65
73
  break;
66
74
  }
67
- x += b.decode(n, { stream: !0 });
68
- let r = x.split("\n");
69
- x = r.pop() ?? "";
70
- for (let e of r) t(e, m, C);
75
+ C += S.decode(t, { stream: !0 });
76
+ let n = C.split("\n");
77
+ C = n.pop() ?? "";
78
+ for (let e of n) i(e, g, T);
71
79
  }
72
- S || h();
80
+ w || _();
73
81
  } catch (e) {
74
82
  if (e instanceof Error && e.name === "AbortError") return;
75
- g(e instanceof Error ? e : Error(String(e)));
83
+ v(e instanceof Error ? e : Error(String(e)));
76
84
  } finally {
77
- y.releaseLock();
85
+ x.releaseLock();
78
86
  }
79
87
  })();
80
88
  },
81
89
  stopCompletion: async (t) => {
82
- let a = await i(`${r}/completions/stop`, {
90
+ let r = await o(`${a}/completions/stop`, {
83
91
  method: "POST",
84
92
  credentials: "include",
85
93
  headers: {
@@ -87,10 +95,10 @@ var e = { "Content-Type": "application/json" }, t = (e, t, n) => {
87
95
  ...n.getCsrfToken() == null ? {} : { "X-CSRF-Token": n.getCsrfToken() }
88
96
  },
89
97
  body: JSON.stringify(t)
90
- }), o = a.headers.get("x-csrf-token");
91
- if (o && n.setCsrfToken(o), !a.ok) throw Error(`stopCompletion failed: ${a.status}`);
98
+ }), i = r.headers.get("x-csrf-token");
99
+ if (i && n.setCsrfToken(i), !r.ok) throw Error(`stopCompletion failed: ${r.status}`);
92
100
  }
93
101
  };
94
102
  };
95
103
  //#endregion
96
- export { n as createChatStreamApi };
104
+ export { n as DEFAULT_GENERATION_CONFLICT_MESSAGE, r as GenerationConflictError, a as createChatStreamApi };
@@ -1,8 +1,12 @@
1
+ import { MessageRole as e } from "@epam/ai-dial-chat-shared";
1
2
  //#region src/conversation/useConversationHandlers/message-utils.ts
2
- var e = (e) => e != null && Object.keys(e).length > 0, t = (e, t, n, r) => {
3
+ var t = (e) => e != null && Object.keys(e).length > 0, n = (e, t, n, r) => {
3
4
  if (t !== e.content || r.length > 0) return !0;
4
5
  let i = e.custom_content?.attachments?.length ?? 0;
5
6
  return n.length !== i;
7
+ }, r = (t) => t == null || t.role !== e.Assistant || !!t.wasStoppedByUser || t.streamErrorMessage != null, i = (e, t, i, a, o) => {
8
+ let s = e[t];
9
+ return s ? n(s, i, a, o) || r(e[t + 1]) : !1;
6
10
  };
7
11
  //#endregion
8
- export { e as hasActiveToolConfig, t as isMessageChanged };
12
+ export { t as hasActiveToolConfig, r as isAnswerIncomplete, n as isMessageChanged, i as shouldRerunGenerationOnEdit };
@@ -1,7 +1,7 @@
1
1
  import { useAttachmentUpload as e } from "../useAttachmentUpload/useAttachmentUpload.js";
2
2
  import { attachmentsToDtos as t } from "./attachment-to-dto.js";
3
3
  import { createMessagePair as n } from "./message-factory.js";
4
- import { hasActiveToolConfig as r, isMessageChanged as i } from "./message-utils.js";
4
+ import { hasActiveToolConfig as r, shouldRerunGenerationOnEdit as i } from "./message-utils.js";
5
5
  import { getStarterSubmitText as a } from "./starter-option.js";
6
6
  import { getConversationPath as o } from "../useConversationStream/conversation-path.js";
7
7
  import { MessageRole as s, generateUUID as c } from "@epam/ai-dial-chat-shared";
@@ -211,7 +211,7 @@ var f = ({ conversation: f, conversationId: p, bucket: m, isStreaming: h, startS
211
211
  let o = e;
212
212
  if (o === -1 || f.messages[o].role !== s.User) return;
213
213
  let l = f.messages[o];
214
- if (!i(l, n, r, a)) {
214
+ if (!i(f.messages, o, n, r, a)) {
215
215
  k((e) => {
216
216
  let t = new Set(e);
217
217
  return t.delete(o), t;
@@ -240,7 +240,7 @@ var f = ({ conversation: f, conversationId: p, bucket: m, isStreaming: h, startS
240
240
  ...f,
241
241
  messages: D
242
242
  };
243
- _(() => (v.current = O, O));
243
+ v.current = O, _(O);
244
244
  let A = S();
245
245
  g(p, n, D.length - 1, A, C, c(), d.Edit), k(/* @__PURE__ */ new Set());
246
246
  }, [
@@ -154,7 +154,7 @@ var b = 5, x = 5, S = (e) => /\.(dial|zip)$/i.test(e.name), C = (e) => new Promi
154
154
  llmNamingDone: !0
155
155
  } }
156
156
  }, { signal: f }), f.aborted) return;
157
- k.push(e.name);
157
+ k.push(n.name);
158
158
  } catch (t) {
159
159
  if (f.aborted) return;
160
160
  if (F(t)) {
@@ -1,148 +1,152 @@
1
1
  import { safeDecodeURI as e } from "../../shared/string-utils.js";
2
- import { getConversationPath as t } from "./conversation-path.js";
3
- import { applyChunkToMessages as n } from "./apply-chunk.js";
4
- import { restoreBufferedMessage as r } from "./buffered-generation.js";
5
- import { createResumeIfAwaitingGeneration as i } from "./generation-resume.js";
6
- import { generateUUID as a } from "@epam/ai-dial-chat-shared";
7
- import { useCallback as o, useEffect as s, useMemo as c, useRef as l, useState as u } from "react";
8
- import { SendCompletionDtoModeEnum as d } from "@epam/ai-dial-chat-api-client";
2
+ import { DEFAULT_GENERATION_CONFLICT_MESSAGE as t, GenerationConflictError as n } from "../create-chat-stream-api.js";
3
+ import { getConversationPath as r } from "./conversation-path.js";
4
+ import { applyChunkToMessages as i } from "./apply-chunk.js";
5
+ import { restoreBufferedMessage as a } from "./buffered-generation.js";
6
+ import { createResumeIfAwaitingGeneration as o } from "./generation-resume.js";
7
+ import { generateUUID as s } from "@epam/ai-dial-chat-shared";
8
+ import { useCallback as c, useEffect as l, useMemo as u, useRef as d, useState as f } from "react";
9
+ import { SendCompletionDtoModeEnum as p } from "@epam/ai-dial-chat-api-client";
9
10
  //#region src/conversation/useConversationStream/useConversationStream.ts
10
- var f = 2e4, p = ({ conversationId: p, state: { setConversation: m, conversationRef: h }, transport: g, generation: { startGeneration: _, completeGeneration: v }, channel: y, overlay: b, onStopError: x }) => {
11
- let [S, C] = u(() => /* @__PURE__ */ new Set()), [w, T] = u(null), E = l(null), D = l(null), O = l(/* @__PURE__ */ new Set()), k = l(/* @__PURE__ */ new Map()), A = l(/* @__PURE__ */ new Set()), j = l(p);
12
- s(() => {
13
- j.current = p;
14
- }, [p]);
15
- let M = o((e) => t(j.current ?? "") === e, []), N = o((e) => {
16
- C((t) => {
11
+ var m = 2e4, h = ({ conversationId: h, state: { setConversation: g, conversationRef: _ }, transport: v, generation: { startGeneration: y, completeGeneration: b }, channel: x, overlay: S, onStopError: C, generationConflictMessage: w = t }) => {
12
+ let [T, E] = f(() => /* @__PURE__ */ new Set()), [D, O] = f(null), k = d(null), A = d(null), j = d(/* @__PURE__ */ new Set()), M = d(/* @__PURE__ */ new Map()), N = d(/* @__PURE__ */ new Set()), P = d(/* @__PURE__ */ new Map()), F = d(h);
13
+ l(() => {
14
+ F.current = h;
15
+ }, [h]);
16
+ let I = c((e) => r(F.current ?? "") === e, []), L = c((e) => {
17
+ E((t) => {
17
18
  if (t.has(e)) return t;
18
19
  let n = new Set(t);
19
20
  return n.add(e), n;
20
21
  });
21
- }, []), P = o((e) => {
22
- C((t) => {
22
+ }, []), R = c((e) => {
23
+ E((t) => {
23
24
  if (!t.has(e)) return t;
24
25
  let n = new Set(t);
25
26
  return n.delete(e), n;
26
27
  });
27
- }, []), F = o((i, o, s, c, l, u, p = d.Append) => {
28
- let x = u ?? a(), S = t(i);
29
- E.current = x, D.current = S, T(S);
30
- let C;
31
- p === d.Regenerate ? C = s : p === d.Edit && (C = s - 1);
32
- let w = _(S, x), O = h.current?.messages[s];
33
- O ? k.current.set(S, {
34
- generationId: x,
35
- messageIndex: s,
36
- message: O
37
- }) : k.current.delete(S), N(S), b?.notifyGenerationStart?.(), y?.ensureConnected();
38
- let j = {
39
- signal: w.signal,
28
+ }, []), z = c((t, o, c, l, u, d, f = p.Append) => {
29
+ let h = d ?? s(), C = r(t);
30
+ k.current = h, A.current = C, P.current.set(C, h), O(C);
31
+ let T;
32
+ f === p.Regenerate ? T = c : f === p.Edit && (T = c - 1);
33
+ let E = () => P.current.get(C) !== h, D = y(C, h), j = _.current?.messages[c];
34
+ j ? M.current.set(C, {
35
+ generationId: h,
36
+ messageIndex: c,
37
+ message: j
38
+ }) : M.current.delete(C), L(C), S?.notifyGenerationStart?.(), x?.ensureConnected();
39
+ let F = {
40
+ signal: D.signal,
40
41
  onChunk: (e) => {
41
- if (E.current !== x) return;
42
- let t = k.current.get(S);
43
- if (t?.generationId === x) {
44
- let r = n([t.message], 0, e);
45
- r && (t.message = r[0]);
42
+ if (k.current !== h) return;
43
+ let t = M.current.get(C);
44
+ if (t?.generationId === h) {
45
+ let n = i([t.message], 0, e);
46
+ n && (t.message = n[0]);
46
47
  }
47
- M(S) && m((t) => {
48
+ I(C) && g((t) => {
48
49
  if (!t) return t;
49
- let i = k.current.get(S), a;
50
- if (i?.generationId === x) a = r(t, i);
50
+ let n = M.current.get(C), r;
51
+ if (n?.generationId === h) r = a(t, n);
51
52
  else {
52
- let r = n(t.messages, s, e);
53
- if (!r) return t;
54
- a = {
53
+ let n = i(t.messages, c, e);
54
+ if (!n) return t;
55
+ r = {
55
56
  ...t,
56
- messages: r
57
+ messages: n
57
58
  };
58
59
  }
59
- return h.current = a, a;
60
+ return _.current = r, r;
60
61
  });
61
62
  },
62
63
  onComplete: async () => {
63
- if (k.current.get(S)?.generationId === x && k.current.delete(S), P(S), E.current === x && (E.current = null, D.current = null, T(null)), v(S, x), y?.notifyGenerationSettled?.(), A.current.has(x) ? A.current.delete(x) : b?.notifyGenerationEnd?.(), M(S)) try {
64
- let t = await g.getConversation(e(i));
65
- if (!M(S)) return;
66
- m(t), h.current = t;
64
+ if (M.current.get(C)?.generationId === h && M.current.delete(C), E() || R(C), k.current === h && (k.current = null, A.current = null, O(null)), b(C, h), x?.notifyGenerationSettled?.(), N.current.has(h) ? N.current.delete(h) : E() || S?.notifyGenerationEnd?.(), !E() && I(C)) try {
65
+ let n = await v.getConversation(e(t));
66
+ if (E() || !I(C)) return;
67
+ g(n), _.current = n;
67
68
  } catch {}
68
69
  },
69
70
  onError: (e) => {
70
- let t = k.current.get(S), n = t?.generationId === x ? t : void 0;
71
- n && k.current.delete(S), P(S), E.current === x && (E.current = null, D.current = null, T(null)), v(S, x), y?.notifyGenerationSettled?.(), M(S) && m((t) => {
72
- if (!t) return t;
73
- let i = n?.generationId === x ? r(t, n) : t, a = {
74
- ...i,
75
- messages: i.messages.map((t, n) => n === s ? {
76
- ...t,
77
- streamErrorMessage: e.message
78
- } : t)
71
+ let t = M.current.get(C), r = t?.generationId === h ? t : void 0;
72
+ if (r && M.current.delete(C), E() || R(C), k.current === h && (k.current = null, A.current = null, O(null)), b(C, h), x?.notifyGenerationSettled?.(), E() || !I(C)) return;
73
+ let i = e instanceof n ? w : e.message;
74
+ g((e) => {
75
+ if (!e) return e;
76
+ let t = r?.generationId === h ? a(e, r) : e, n = {
77
+ ...t,
78
+ messages: t.messages.map((e, t) => t === c ? {
79
+ ...e,
80
+ streamErrorMessage: i
81
+ } : e)
79
82
  };
80
- return h.current = a, a;
83
+ return _.current = n, n;
81
84
  });
82
85
  }
83
86
  };
84
87
  (async () => {
85
88
  try {
86
- let e = y?.channelId ?? await y?.waitForChannel(f) ?? void 0;
87
- g.streamCompletion(S, o, c, j, l, x, p, C, e);
89
+ let e = x?.channelId ?? await x?.waitForChannel(m) ?? void 0;
90
+ v.streamCompletion(C, o, l, F, u, h, f, T, e);
88
91
  } catch (e) {
89
- j.onError(e instanceof Error ? e : Error(String(e)));
92
+ F.onError(e instanceof Error ? e : Error(String(e)));
90
93
  }
91
94
  })();
92
95
  }, [
93
- _,
94
- v,
95
- N,
96
- P,
97
- M,
98
- y?.channelId,
99
- y?.ensureConnected,
100
- y?.waitForChannel,
101
- y?.notifyGenerationSettled,
96
+ y,
102
97
  b,
103
- g
104
- ]), I = o((e, n) => {
105
- let i = t(e), a = k.current.get(i);
106
- return a ? r(n, a) : n;
107
- }, []), L = o(() => {
108
- let e = E.current;
109
- if (!e || !p) return;
110
- let n = t(p);
111
- D.current === n && (A.current.add(e), b?.notifyStopGenerating?.(), g.stopCompletion({
98
+ L,
99
+ R,
100
+ I,
101
+ x?.channelId,
102
+ x?.ensureConnected,
103
+ x?.waitForChannel,
104
+ x?.notifyGenerationSettled,
105
+ S,
106
+ v,
107
+ w
108
+ ]), B = c((e, t) => {
109
+ let n = r(e), i = M.current.get(n);
110
+ return i ? a(t, i) : t;
111
+ }, []), V = c(() => {
112
+ let e = k.current;
113
+ if (!e || !h) return;
114
+ let t = r(h);
115
+ A.current === t && (N.current.add(e), S?.notifyStopGenerating?.(), v.stopCompletion({
112
116
  generationId: e,
113
- path: n
117
+ path: t
114
118
  }).catch((e) => {
115
119
  let t = e instanceof Error ? e : Error(String(e));
116
- x?.(t);
120
+ C?.(t);
117
121
  }));
118
122
  }, [
119
- p,
120
- x,
121
- b,
122
- g
123
- ]), R = c(() => i({
124
- transport: g,
125
- setConversation: m,
126
- conversationRef: h,
127
- resumingPathsRef: O,
128
- bufferedGenerationsRef: k,
129
- addStreamingPath: N,
130
- removeStreamingPath: P,
131
- isPathDisplayed: M
123
+ h,
124
+ C,
125
+ S,
126
+ v
127
+ ]), H = u(() => o({
128
+ transport: v,
129
+ setConversation: g,
130
+ conversationRef: _,
131
+ resumingPathsRef: j,
132
+ bufferedGenerationsRef: M,
133
+ addStreamingPath: L,
134
+ removeStreamingPath: R,
135
+ isPathDisplayed: I
132
136
  }), [
133
- N,
134
- P,
135
- M,
136
- g
137
- ]), z = p != null && S.has(t(p)), B = p == null ? null : t(p);
137
+ L,
138
+ R,
139
+ I,
140
+ v
141
+ ]), U = h != null && T.has(r(h)), W = h == null ? null : r(h);
138
142
  return {
139
- startStream: F,
140
- handleStop: L,
141
- resumeIfAwaitingGeneration: R,
142
- restoreBufferedGeneration: I,
143
- isStreaming: z,
144
- canStopStreaming: B != null && w === B
143
+ startStream: z,
144
+ handleStop: V,
145
+ resumeIfAwaitingGeneration: H,
146
+ restoreBufferedGeneration: B,
147
+ isStreaming: U,
148
+ canStopStreaming: W != null && D === W
145
149
  };
146
150
  };
147
151
  //#endregion
148
- export { p as useConversationStream };
152
+ export { h as useConversationStream };
package/conversation.d.ts CHANGED
@@ -308,6 +308,9 @@ export declare const createDeploymentChangedMessage: (previousDeploymentId: stri
308
308
  /** Builds a user message and its empty assistant placeholder, both timestamped `now`. */
309
309
  export declare const createMessagePair: (content: string, customContent?: MessageCustomContent, deploymentId?: string | null) => MessagePair;
310
310
 
311
+ /** Fallback text of a {@link GenerationConflictError} when no message is supplied. */
312
+ export declare const DEFAULT_GENERATION_CONFLICT_MESSAGE = "A response is already being generated in this conversation. Wait for it to finish or stop it before sending another message.";
313
+
311
314
  /**
312
315
  * Derives the row-action visibility state for a single conversation item.
313
316
  *
@@ -331,6 +334,17 @@ export declare const deriveConversationRowActionState: (item: Pick<ConversationL
331
334
  */
332
335
  export declare const formatAppVersion: (version: string) => string;
333
336
 
337
+ /**
338
+ * Reported through `onError` when the backend rejects a completion because
339
+ * another generation is already active on the same conversation — what a
340
+ * second browser tab of the same session hits when it submits into a
341
+ * conversation the first tab is still generating. Distinct from a generic
342
+ * transport failure so callers can present it as an expected state.
343
+ */
344
+ export declare class GenerationConflictError extends Error {
345
+ constructor(message?: string);
346
+ }
347
+
334
348
  /**
335
349
  * Strips the leading bucket segment from a conversation id and decodes any
336
350
  * already-percent-encoded remainder back to raw, so callers that re-encode
@@ -453,6 +467,13 @@ export declare const hasAnnouncementContent: (content: AnnouncementContent) => b
453
467
  */
454
468
  export declare const hasStructuredAnnouncement: ({ title, description, }: AnnouncementContent) => boolean;
455
469
 
470
+ /**
471
+ * Whether the assistant answer that follows an edited user message is missing
472
+ * or incomplete: no message at all, a non-assistant message, an answer the
473
+ * user stopped, or one that ended with a stream error.
474
+ */
475
+ export declare const isAnswerIncomplete: (answer: Message | undefined) => boolean;
476
+
456
477
  /**
457
478
  * True when the conversation's last message is an unresolved assistant
458
479
  * placeholder: the backend only persists a conversation at generation start
@@ -561,6 +582,16 @@ export declare const sanitizeAnnouncementMessageHtml: (html: string) => string;
561
582
  /** Sanitizes footer message HTML to the allowed tag/attribute set. */
562
583
  export declare const sanitizeFooterHtml: (html: string) => string;
563
584
 
585
+ /**
586
+ * Whether submitting an edit has to re-run the generation.
587
+ *
588
+ * True whenever the message itself changed, and also when it is unchanged but
589
+ * its answer never completed — after stopping a generation the user submits
590
+ * the same message precisely to get a full answer, so treating that as "no
591
+ * change" would leave the conversation stuck with the partial one.
592
+ */
593
+ export declare const shouldRerunGenerationOnEdit: (messages: Message[], messageIndex: number, newText: string, keptDisplayAttachments: DisplayAttachment[], newAttachments: Attachment[]) => boolean;
594
+
564
595
  /** True when the first user/assistant exchange is complete and LLM naming may still run. */
565
596
  export declare const shouldWatchForDisplayNameUpdate: (conversation: Conversation) => boolean;
566
597
 
@@ -847,7 +878,7 @@ export declare interface UseConversationPanelItemsResolvers {
847
878
  * `transport`/`generation`/`channel`/`overlay` capabilities rather than any
848
879
  * app context or `server-api` import.
849
880
  */
850
- export declare const useConversationStream: ({ conversationId, state: { setConversation, conversationRef }, transport, generation: { startGeneration, completeGeneration }, channel, overlay, onStopError, }: UseConversationStreamParams) => UseConversationStreamResult;
881
+ export declare const useConversationStream: ({ conversationId, state: { setConversation, conversationRef }, transport, generation: { startGeneration, completeGeneration }, channel, overlay, onStopError, generationConflictMessage, }: UseConversationStreamParams) => UseConversationStreamResult;
851
882
 
852
883
  /** Parameters for {@link useConversationStream}. */
853
884
  export declare interface UseConversationStreamParams {
@@ -858,6 +889,13 @@ export declare interface UseConversationStreamParams {
858
889
  channel?: ConversationStreamChannel;
859
890
  overlay?: ConversationStreamOverlayNotifier;
860
891
  onStopError?: (error: Error) => void;
892
+ /**
893
+ * Message shown on the message bubble when the backend rejects a completion
894
+ * because this conversation is already generating — the case a second
895
+ * browser tab of the same session hits. Defaults to
896
+ * {@link DEFAULT_GENERATION_CONFLICT_MESSAGE}.
897
+ */
898
+ generationConflictMessage?: string;
861
899
  }
862
900
 
863
901
  /** Return value of {@link useConversationStream}. */