leonardo 2 månader sedan
förälder
incheckning
98a5b71ccb

+ 2 - 2
dist/index.html

@@ -6,8 +6,8 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" />
     <link rel="icon" type="image/x-icon" href="/favicon.ico?v=2" />
     <title>ORÁCULO</title>
-    <script type="module" crossorigin src="/assets/index-BhVpz5hS.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-BEdQmvVX.css">
+    <script type="module" crossorigin src="/assets/index-D3zvInNN.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-DPYpbLP7.css">
   </head>
   <body class="bg-background">
     <noscript>

+ 28 - 7
src/api/chat.js

@@ -1,4 +1,4 @@
-import { apiBaseUrl, apiFetch, buildAuthHeaders } from "./client.js";
+import { apiBaseUrl, apiFetch, buildAuthHeaders, refreshAccessToken, notifyAuthError } from "./client.js";
 
 export async function sendChat(message, { conversationId } = {}) {
   return apiFetch("/api/chat", {
@@ -8,22 +8,43 @@ export async function sendChat(message, { conversationId } = {}) {
 }
 
 export async function sendChatStream(message, { conversationId, onChunk, onSources, onStatus, onDone, onError, signal } = {}) {
-  const headers = buildAuthHeaders();
+  const body = JSON.stringify({ message, ...(conversationId ? { conversationId } : {}) });
 
-  let res;
-  try {
-    res = await fetch(`${apiBaseUrl}/api/chat/stream`, {
+  const doFetch = () =>
+    fetch(`${apiBaseUrl}/api/chat/stream`, {
       method: "POST",
-      headers,
-      body: JSON.stringify({ message, ...(conversationId ? { conversationId } : {}) }),
+      headers: buildAuthHeaders(),
+      body,
       signal
     });
+
+  let res;
+  try {
+    res = await doFetch();
+
+    if (res.status === 401) {
+      let newToken = null;
+      try {
+        newToken = await refreshAccessToken();
+      } catch {
+        notifyAuthError();
+        onError?.(new Error("session_expired"));
+        return;
+      }
+      if (newToken) res = await doFetch();
+    }
   } catch (e) {
     if (e?.name === "AbortError") return;
     onError?.(e);
     return;
   }
 
+  if (res.status === 401) {
+    notifyAuthError();
+    onError?.(new Error("session_expired"));
+    return;
+  }
+
   if (!res.ok) {
     const text = await res.text().catch(() => "");
     onError?.(new Error(text || `http_error:${res.status}`));

+ 14 - 4
src/api/client.js

@@ -34,6 +34,19 @@ export function buildAuthHeaders(extra = {}) {
   return headers;
 }
 
+
+export async function refreshAccessToken() {
+  if (!_refreshFn) return null;
+  if (!_refreshPromise) {
+    _refreshPromise = _refreshFn().finally(() => { _refreshPromise = null; });
+  }
+  return _refreshPromise;
+}
+
+export function notifyAuthError() {
+  _authErrorFn?.();
+}
+
 export async function apiFetch(path, { method = "GET", body } = {}) {
   const headers = { "content-type": "application/json" };
   if (_accessToken) headers["Authorization"] = `Bearer ${_accessToken}`;
@@ -46,10 +59,7 @@ export async function apiFetch(path, { method = "GET", body } = {}) {
 
   if (res.status === 401 && _refreshFn) {
     try {
-      if (!_refreshPromise) {
-        _refreshPromise = _refreshFn().finally(() => { _refreshPromise = null; });
-      }
-      const newToken = await _refreshPromise;
+      const newToken = await refreshAccessToken();
       if (newToken) {
         headers["Authorization"] = `Bearer ${newToken}`;
         res = await fetch(`${apiBaseUrl}${path}`, {

+ 16 - 12
src/composables/useChat.js

@@ -118,6 +118,7 @@ export function useChat() {
   async function setActiveConversation(id) {
     const strId = String(id ?? "").trim();
     if (!strId) return;
+    cancelCurrentStream();
     activeConversationId.value = strId;
     messages.value = [...defaultMessages];
     try {
@@ -136,6 +137,7 @@ export function useChat() {
   }
 
   function newConversation() {
+    cancelCurrentStream();
     activeConversationId.value = null;
     messages.value = [...defaultMessages];
     return null;
@@ -232,7 +234,7 @@ export function useChat() {
           ...conversations.value
         ];
       } catch {
-        // Continue without persistence if creation fails
+       
       }
     }
 
@@ -240,7 +242,8 @@ export function useChat() {
 
     const assistantMsg = { role: "assistant", content: "", sources: [], sentAt: Date.now(), streaming: true, statusMsg: "" };
     messages.value.push(assistantMsg);
-    const msgIndex = messages.value.length - 1;
+    
+    const liveMsg = messages.value[messages.value.length - 1];
 
     loading.value = true;
 
@@ -248,20 +251,21 @@ export function useChat() {
       conversationId: convId,
       signal: _streamAbort.signal,
       onStatus: ({ stage, count }) => {
-        if (stage === "buscando") messages.value[msgIndex].statusMsg = "Buscando documentos...";
-        else if (stage === "encontrou") messages.value[msgIndex].statusMsg = `Encontrei ${count} fonte${count !== 1 ? "s" : ""}...`;
-        else if (stage === "gerando") messages.value[msgIndex].statusMsg = "Gerando resposta...";
+        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) => {
-        messages.value[msgIndex].statusMsg = "";
-        messages.value[msgIndex].content += delta;
+        liveMsg.statusMsg = "";
+        liveMsg.content += delta;
       },
       onSources: (sources) => {
-        messages.value[msgIndex].sources = sources ?? [];
+        liveMsg.sources = sources ?? [];
       },
       onDone: () => {
         _streamAbort = null;
-        messages.value[msgIndex].streaming = false;
+        liveMsg.streaming = false;
         loading.value = false;
         if (convId) {
           conversations.value = conversations.value
@@ -276,10 +280,10 @@ export function useChat() {
       onError: (e) => {
         _streamAbort = null;
         error.value = e?.message || "erro";
-        if (!messages.value[msgIndex].content) {
-          messages.value[msgIndex].content = "Não consegui responder no momento.";
+        if (!liveMsg.content) {
+          liveMsg.content = "Não consegui responder no momento.";
         }
-        messages.value[msgIndex].streaming = false;
+        liveMsg.streaming = false;
         loading.value = false;
       }
     });

+ 13 - 2
src/composables/useDocumentos.js

@@ -8,12 +8,23 @@ export function useDocumentos() {
     error: "",
   });
 
+  const PAGE_SIZE = 200; 
+  const MAX_PAGES = 25; 
+
   async function loadDocs() {
     docs.loading = true;
     docs.error = "";
     try {
-      const data = await listDocuments({ limit: 50, offset: 0 });
-      docs.items = data.items ?? [];
+      
+      const all = [];
+      let offset = 0;
+      for (let page = 0; page < MAX_PAGES; page += 1) {
+        const data = await listDocuments({ limit: PAGE_SIZE, offset });
+        all.push(...(data.items ?? []));
+        if (data.nextOffset == null) break;
+        offset = data.nextOffset;
+      }
+      docs.items = all;
     } catch (e) {
       docs.error = e?.message || "erro";
       docs.items = [];

+ 6 - 5
src/layout/SidebarSistema.vue

@@ -16,7 +16,8 @@ const { sidebarRecolhida, classeRotulo, classeItemSidebar, classeIcon } = useSid
 const { painelAberto, alternarPainel } = usePainelDocumentos();
 
 const rotaAtiva = computed(() => String(route.name ?? ""));
-const isNivel1 = computed(() => String(user.value?.Nivel) === "1");
+
+const isAdmin = computed(() => String(user.value?.Nivel) === "3");
 
 function irPara(nome) {
   router.push({ name: nome });
@@ -81,7 +82,7 @@ async function novaConversa() {
           </svg>
         </button>
       </div>
-      <!-- Seção: Chat -->
+
       <div
         v-if="!sidebarRecolhida"
         class="px-2 pt-1 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"
@@ -145,13 +146,13 @@ async function novaConversa() {
         </button>
       </nav>
 
-      <!-- Lista de conversas recentes (apenas sidebar expandida) -->
+      
       <ListaConversas v-if="!sidebarRecolhida" />
 
-      <!-- Seções inferiores: Biblioteca + Sistema -->
+      
       <div class="mt-auto flex-shrink-0 grid gap-1 pt-2">
         <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
-        <template v-if="!isNivel1">
+        <template v-if="isAdmin">
           <div
             v-if="!sidebarRecolhida"
             class="px-2 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"

+ 2 - 1
src/router/index.js

@@ -36,7 +36,8 @@ router.beforeEach((to) => {
 
   if (to.name === "usuarios") {
     const { user } = useAuth();
-    if (String(user.value?.Nivel) === "1") {
+    
+    if (String(user.value?.Nivel) !== "3") {
       return { name: "configuracoes" };
     }
   }

+ 2 - 2
src/views/configuracoes/ConfiguracoesView.vue

@@ -7,7 +7,7 @@ import { useAuth } from "../../composables/useAuth.js";
 
 const router = useRouter();
 const { user } = useAuth();
-const isNivel1 = computed(() => String(user.value?.Nivel) === "1");
+const isAdmin = computed(() => String(user.value?.Nivel) === "3");
 
 function irParaUsuarios() {
   router.push({ name: "usuarios" });
@@ -50,7 +50,7 @@ function irParaUsuarios() {
           </div>
         </section>
 
-        <section v-if="!isNivel1">
+        <section v-if="isAdmin">
           <div class="mb-3 text-xs font-semibold uppercase tracking-[0.16em] text-gray-500 dark:text-gray-400">
             Usuários & Acesso
           </div>