chat.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { apiBaseUrl, apiFetch, buildAuthHeaders, refreshAccessToken, notifyAuthError, extractErrorMessage } from "./client.js";
  2. export async function sendChatStream(message, { conversationId, mode, onChunk, onSources, onStatus, onDone, onError, signal } = {}) {
  3. const body = JSON.stringify({ message, ...(conversationId ? { conversationId } : {}), ...(mode ? { mode } : {}) });
  4. const doFetch = () =>
  5. fetch(`${apiBaseUrl}/api/chat/stream`, {
  6. method: "POST",
  7. headers: buildAuthHeaders(),
  8. body,
  9. signal
  10. });
  11. let res;
  12. try {
  13. res = await doFetch();
  14. if (res.status === 401) {
  15. let newToken = null;
  16. try {
  17. newToken = await refreshAccessToken();
  18. } catch {
  19. notifyAuthError();
  20. onError?.(new Error("session_expired"));
  21. return;
  22. }
  23. if (newToken) res = await doFetch();
  24. }
  25. } catch (e) {
  26. if (e?.name === "AbortError") return;
  27. onError?.(e);
  28. return;
  29. }
  30. if (res.status === 401) {
  31. notifyAuthError();
  32. onError?.(new Error("session_expired"));
  33. return;
  34. }
  35. if (!res.ok) {
  36. const text = await res.text().catch(() => "");
  37. onError?.(new Error(text || `http_error:${res.status}`));
  38. return;
  39. }
  40. const reader = res.body.getReader();
  41. const decoder = new TextDecoder();
  42. let buffer = "";
  43. const STREAM_INACTIVITY_TIMEOUT_MS = 45_000;
  44. function readWithTimeout() {
  45. return new Promise((resolve, reject) => {
  46. const timer = setTimeout(() => reject(new Error("stream_inactivity_timeout")), STREAM_INACTIVITY_TIMEOUT_MS);
  47. reader.read().then(
  48. (result) => { clearTimeout(timer); resolve(result); },
  49. (err) => { clearTimeout(timer); reject(err); }
  50. );
  51. });
  52. }
  53. try {
  54. while (true) {
  55. const { done, value } = await readWithTimeout();
  56. if (done) break;
  57. buffer += decoder.decode(value, { stream: true });
  58. const lines = buffer.split("\n");
  59. buffer = lines.pop() ?? "";
  60. for (const line of lines) {
  61. if (!line.startsWith("data: ")) continue;
  62. const payload = line.slice(6);
  63. if (payload === "[DONE]") { onDone?.(); return; }
  64. try {
  65. const parsed = JSON.parse(payload);
  66. if (parsed.type === "delta") onChunk?.(parsed.delta);
  67. else if (parsed.type === "sources") onSources?.(parsed.sources);
  68. else if (parsed.type === "status") onStatus?.(parsed);
  69. else if (parsed.type === "error") onError?.(new Error(parsed.error));
  70. } catch {}
  71. }
  72. }
  73. } catch (e) {
  74. if (e?.name === "AbortError") return;
  75. if (e?.message === "stream_inactivity_timeout") {
  76. reader.cancel().catch(() => {});
  77. onError?.(new Error("A resposta demorou demais e foi interrompida."));
  78. return;
  79. }
  80. onError?.(e);
  81. }
  82. onDone?.();
  83. }
  84. export async function ingestDocuments(documents) {
  85. return apiFetch("/api/ingest", {
  86. method: "POST",
  87. body: { documents }
  88. });
  89. }
  90. function sendIngestFile(file, { source, onProgress } = {}) {
  91. return new Promise((resolve, reject) => {
  92. const form = new FormData();
  93. form.append("file", file);
  94. if (source) form.append("source", source);
  95. const xhr = new XMLHttpRequest();
  96. xhr.upload.onprogress = (e) => {
  97. if (e.lengthComputable) onProgress?.(Math.round((e.loaded / e.total) * 100));
  98. };
  99. xhr.onload = () => {
  100. if (xhr.status >= 200 && xhr.status < 300) {
  101. try {
  102. resolve(JSON.parse(xhr.responseText));
  103. } catch {
  104. resolve(xhr.responseText);
  105. }
  106. return;
  107. }
  108. let payload = null;
  109. try { payload = JSON.parse(xhr.responseText); } catch {}
  110. const err = new Error(extractErrorMessage(payload, xhr.status));
  111. err.status = xhr.status;
  112. reject(err);
  113. };
  114. xhr.onerror = () => reject(new Error("network_error"));
  115. xhr.ontimeout = () => reject(new Error("timeout"));
  116. xhr.open("POST", `${apiBaseUrl}/api/ingest/file`);
  117. const authHeaders = buildAuthHeaders();
  118. if (authHeaders["Authorization"]) xhr.setRequestHeader("Authorization", authHeaders["Authorization"]);
  119. xhr.send(form);
  120. });
  121. }
  122. export async function ingestFile(file, { source, onProgress } = {}) {
  123. try {
  124. return await sendIngestFile(file, { source, onProgress });
  125. } catch (err) {
  126. if (err.status !== 401) throw err;
  127. try {
  128. await refreshAccessToken();
  129. } catch {
  130. notifyAuthError();
  131. throw new Error("session_expired");
  132. }
  133. return sendIngestFile(file, { source, onProgress });
  134. }
  135. }
  136. export async function ingestUrl(url, { source } = {}) {
  137. return apiFetch("/api/ingest/url", {
  138. method: "POST",
  139. body: { url, ...(source ? { source } : {}) }
  140. });
  141. }
  142. export async function search(query) {
  143. return apiFetch("/api/search", {
  144. method: "POST",
  145. body: { query }
  146. });
  147. }
  148. export async function listDocuments({ limit = 50, offset = 0 } = {}) {
  149. const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }).toString();
  150. return apiFetch(`/api/documents?${params}`);
  151. }
  152. export async function deleteDocumentsBySource(source) {
  153. return apiFetch(`/api/documents/source/${encodeURIComponent(source)}`, {
  154. method: "DELETE"
  155. });
  156. }