chat.js 4.6 KB

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