| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- import { ref } from "vue";
- import { sendChatStream } from "../api/chat.js";
- import {
- listConversations,
- createConversation,
- getConversationMessages,
- updateConversationTitle as updateConversationTitleAPI,
- deleteConversation as deleteConversationAPI,
- exportConversation as exportConversationAPI
- } from "../api/conversations.js";
- import { useToast } from "./useToast.js";
- let singleton;
- let _streamAbort = null;
- export function resetChatSingleton() {
- if (_streamAbort) {
- _streamAbort.abort();
- _streamAbort = null;
- }
- singleton = undefined;
- }
- export function useChat() {
- if (singleton) return singleton;
- const toast = useToast();
- const defaultMessages = [
- {
- role: "assistant",
- content: "Posso responder usando a base de conhecimento da empresa. Envie uma pergunta ou faça upload de documentos.",
- sentAt: 0
- }
- ];
- const CONV_PAGE_SIZE = 20;
- const conversations = ref([]);
- const activeConversationId = ref(null);
- const messages = ref([...defaultMessages]);
- const loading = ref(false);
- const error = ref("");
- const conversationsLoading = ref(false);
- const conversationsError = ref("");
- const conversationsHasMore = ref(false);
- const conversationsOffset = ref(0);
- function normalizeApiMessages(items) {
- return (items ?? []).map((m) => {
- let sources;
- if (m.Sources) {
- if (typeof m.Sources === "string") {
- try { sources = JSON.parse(m.Sources); } catch { sources = undefined; }
- } else {
- sources = m.Sources;
- }
- }
- return {
- role: m.Role === "user" ? "user" : "assistant",
- content: String(m.Content ?? ""),
- sources,
- sentAt: m.SentAt ? new Date(m.SentAt).getTime() : 0
- };
- });
- }
- function normalizeConversation(c) {
- return {
- id: String(c.Id),
- title: c.Title || "Nova conversa",
- createdAt: new Date(c.CreatedAt).getTime(),
- updatedAt: new Date(c.UpdatedAt).getTime(),
- messageCount: Number(c.MessageCount ?? 0)
- };
- }
- async function loadConversationsList() {
- conversationsLoading.value = true;
- conversationsError.value = "";
- conversationsOffset.value = 0;
- try {
- const data = await listConversations({ limit: CONV_PAGE_SIZE, offset: 0 });
- const items = data.items ?? [];
- conversations.value = items.map(normalizeConversation);
- conversationsHasMore.value = items.length >= CONV_PAGE_SIZE;
- conversationsOffset.value = items.length;
- } catch (err) {
- conversations.value = [];
- conversationsHasMore.value = false;
- if (err?.message !== "session_expired") {
- conversationsError.value = err?.message || "Erro ao carregar conversas.";
- }
- } finally {
- conversationsLoading.value = false;
- }
- }
- async function loadMoreConversations() {
- if (!conversationsHasMore.value || conversationsLoading.value) return;
- conversationsLoading.value = true;
- try {
- const data = await listConversations({ limit: CONV_PAGE_SIZE, offset: conversationsOffset.value });
- const items = data.items ?? [];
- conversations.value = [...conversations.value, ...items.map(normalizeConversation)];
- conversationsHasMore.value = items.length >= CONV_PAGE_SIZE;
- conversationsOffset.value += items.length;
- } catch (err) {
- if (err?.message !== "session_expired") {
- toast.erro("Não foi possível carregar mais conversas.");
- console.warn("[useChat] loadMoreConversations:", err);
- }
- } finally {
- conversationsLoading.value = false;
- }
- }
- async function setActiveConversation(id) {
- const strId = String(id ?? "").trim();
- if (!strId) return;
- cancelCurrentStream();
- activeConversationId.value = strId;
- messages.value = [...defaultMessages];
- try {
- const numId = Number(strId);
- if (!numId) return;
- const data = await getConversationMessages(numId);
- const normalized = normalizeApiMessages(data?.items);
- messages.value = normalized.length ? normalized : [...defaultMessages];
- } catch (err) {
- messages.value = [...defaultMessages];
- if (err?.message !== "session_expired") {
- conversationsError.value = "Erro ao carregar mensagens da conversa.";
- console.warn("[useChat] setActiveConversation:", err);
- }
- }
- }
- function newConversation() {
- cancelCurrentStream();
- activeConversationId.value = null;
- messages.value = [...defaultMessages];
- return null;
- }
- async function renameConversation(id, title) {
- const strId = String(id ?? "").trim();
- const numId = Number(strId);
- if (!numId || !title?.trim()) return;
- try {
- await updateConversationTitleAPI(numId, title.trim());
- conversations.value = conversations.value.map((c) =>
- c.id === strId ? { ...c, title: title.trim() } : c
- );
- } catch {
- toast.erro("Não foi possível renomear a conversa.");
- }
- }
- async function deleteConversation(id) {
- const strId = String(id ?? "").trim();
- const numId = Number(strId);
- if (!numId) return;
- try {
- await deleteConversationAPI(numId);
- conversations.value = conversations.value.filter((c) => c.id !== strId);
- if (String(activeConversationId.value) === strId) {
- const next = conversations.value[0];
- if (next) await setActiveConversation(next.id);
- else newConversation();
- }
- } catch {
- toast.erro("Não foi possível excluir a conversa.");
- }
- }
- async function exportConversation(id) {
- const numId = Number(String(id ?? "").trim());
- if (!numId) return;
- const data = await exportConversationAPI(numId);
- const conv = conversations.value.find((c) => c.id === String(id));
- const filename = `conversa-${conv?.title?.slice(0, 40).replace(/[^a-zA-Z0-9À-ÿ\s]/g, "").trim().replace(/\s+/g, "-") || numId}.md`;
- const blob = new Blob([data.markdown ?? ""], { type: "text/markdown" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = filename;
- a.click();
- URL.revokeObjectURL(url);
- }
- function searchConversations(query) {
- const q = String(query ?? "").trim().toLowerCase();
- const sorted = conversations.value
- .slice()
- .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
- if (!q) return sorted;
- return sorted.filter((c) => String(c.title ?? "").toLowerCase().includes(q));
- }
- function clearError() {
- error.value = "";
- }
- function cancelCurrentStream() {
- if (_streamAbort) {
- _streamAbort.abort();
- _streamAbort = null;
- }
- loading.value = false;
- }
- async function send(content) {
- error.value = "";
- const trimmed = String(content ?? "").trim();
- if (!trimmed) return;
- if (_streamAbort) {
- _streamAbort.abort();
- }
- _streamAbort = new AbortController();
- let convId = activeConversationId.value ? Number(activeConversationId.value) : null;
- if (!convId) {
- try {
- const title = trimmed.slice(0, 60);
- const conv = await createConversation(title);
- convId = conv.id;
- activeConversationId.value = String(convId);
- const now = Date.now();
- conversations.value = [
- { id: String(convId), title, createdAt: now, updatedAt: now, messageCount: 0 },
- ...conversations.value
- ];
- } catch {
-
- }
- }
- messages.value.push({ role: "user", content: trimmed, sentAt: Date.now() });
- const assistantMsg = { role: "assistant", content: "", sources: [], sentAt: Date.now(), streaming: true, statusMsg: "" };
- messages.value.push(assistantMsg);
-
- const liveMsg = messages.value[messages.value.length - 1];
- loading.value = true;
- await sendChatStream(trimmed, {
- conversationId: convId,
- signal: _streamAbort.signal,
- onStatus: ({ stage, count }) => {
- if (stage === "buscando") liveMsg.statusMsg = "Buscando documentos...";
- else if (stage === "encontrou") liveMsg.statusMsg = `Encontrei ${count} fonte${count !== 1 ? "s" : ""}...`;
- else if (stage === "reordenando") liveMsg.statusMsg = "Selecionando as fontes mais relevantes...";
- else if (stage === "gerando") liveMsg.statusMsg = "Gerando resposta...";
- },
- onChunk: (delta) => {
- liveMsg.statusMsg = "";
- liveMsg.content += delta;
- },
- onSources: (sources) => {
- liveMsg.sources = sources ?? [];
- },
- onDone: () => {
- _streamAbort = null;
- liveMsg.streaming = false;
- loading.value = false;
- if (convId) {
- conversations.value = conversations.value
- .map((c) =>
- c.id === String(convId)
- ? { ...c, updatedAt: Date.now(), messageCount: (c.messageCount ?? 0) + 2 }
- : c
- )
- .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
- }
- },
- onError: (e) => {
- _streamAbort = null;
- error.value = e?.message || "erro";
- if (!liveMsg.content) {
- liveMsg.content = "Não consegui responder no momento.";
- }
- liveMsg.streaming = false;
- loading.value = false;
- }
- });
- }
- loadConversationsList();
- singleton = {
- conversations,
- activeConversationId,
- messages,
- loading,
- error,
- conversationsLoading,
- conversationsError,
- conversationsHasMore,
- send,
- clearError,
- cancelCurrentStream,
- newConversation,
- setActiveConversation,
- renameConversation,
- deleteConversation,
- searchConversations,
- loadConversationsList,
- loadMoreConversations,
- exportConversation
- };
- return singleton;
- }
|