Quellcode durchsuchen

refeito paginas de documentos e carregar documentos

leonardo vor 1 Monat
Ursprung
Commit
784a86a7e2

+ 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-rjf6_E6r.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-W09r-C3W.css">
+    <script type="module" crossorigin src="/assets/index-Bgaph4bj.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-CbSmb0WN.css">
   </head>
   <body class="bg-background">
     <noscript>

BIN
docs/.DS_Store


+ 86 - 40
src/components/DocumentList.vue

@@ -62,6 +62,50 @@ function formatDate(iso) {
     return null;
   }
 }
+
+const FILE_TYPES = {
+  pdf: {
+    chipClass: "bg-red-100 text-red-600 dark:bg-red-500/10 dark:text-red-400",
+    icon: "doc",
+  },
+  doc: {
+    chipClass: "bg-blue-100 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400",
+    icon: "doc",
+  },
+  docx: {
+    chipClass: "bg-blue-100 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400",
+    icon: "doc",
+  },
+  png: {
+    chipClass: "bg-violet-100 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400",
+    icon: "image",
+  },
+  jpg: {
+    chipClass: "bg-violet-100 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400",
+    icon: "image",
+  },
+  jpeg: {
+    chipClass: "bg-violet-100 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400",
+    icon: "image",
+  },
+  webp: {
+    chipClass: "bg-violet-100 text-violet-600 dark:bg-violet-500/10 dark:text-violet-400",
+    icon: "image",
+  },
+};
+
+const GENERIC_FILE_TYPE = {
+  chipClass: "bg-gray-100 text-gray-500 dark:bg-white/10 dark:text-gray-400",
+  icon: "doc",
+};
+
+function fileType(source) {
+  if (!source) return GENERIC_FILE_TYPE;
+  const clean = source.split(/[?#]/)[0];
+  const match = /\.([a-z0-9]+)$/i.exec(clean);
+  const ext = match ? match[1].toLowerCase() : "";
+  return FILE_TYPES[ext] ?? GENERIC_FILE_TYPE;
+}
 </script>
 
 <template>
@@ -98,7 +142,7 @@ function formatDate(iso) {
       {{ error }}
     </div>
 
-    <SkeletonDocumentCard v-if="loading" :count="3" />
+    <SkeletonDocumentCard v-if="loading" :count="10" />
 
     <div
       v-else-if="items.length === 0 && !error"
@@ -115,30 +159,51 @@ function formatDate(iso) {
       </div>
     </div>
 
-    <div v-else class="grid max-h-[520px] gap-2 overflow-auto">
+    <div v-else class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
       <div
         v-for="(chunks, source) in filteredGrouped"
         :key="source"
-        class="overflow-hidden rounded-xl border border-gray-200 bg-background/60 dark:border-gray-700 dark:bg-background/10"
+        :title="source || 'sem fonte'"
+        class="group relative flex aspect-square flex-col items-center justify-center gap-1.5 overflow-hidden rounded-xl border border-gray-200 bg-background/60 p-3 text-center transition-all duration-150 hover:-translate-y-0.5 hover:border-gray-300 hover:shadow-md dark:border-gray-700 dark:bg-background/10 dark:hover:border-white/20"
       >
-        <div class="flex items-center justify-between gap-2 px-3 py-2.5">
-          <div class="flex min-w-0 items-center gap-2">
-            <svg class="h-4 w-4 shrink-0 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
-              <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
-            </svg>
-            <div class="min-w-0">
-              <span class="block truncate text-sm font-medium text-gray-900 dark:text-gray-100">{{ source || "sem fonte" }}</span>
-              <span v-if="chunks[0]?.ingestedAt" class="text-[10px] text-gray-400 dark:text-gray-500">
-                {{ formatDate(chunks[0].ingestedAt) }}
-              </span>
-            </div>
-            <span class="shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-500 dark:bg-white/10 dark:text-gray-400">
-              {{ chunks.length }} chunk{{ chunks.length !== 1 ? 's' : '' }}
-            </span>
-          </div>
+        <button
+          v-if="source"
+          class="absolute right-1 top-1 rounded-lg border border-transparent p-1.5 text-gray-400 opacity-0 transition-opacity hover:border-red-200 hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:border-red-900/50 dark:hover:bg-red-950/30 dark:hover:text-red-400"
+          :title="`Excluir '${source}'`"
+          @click="confirmingSource = source"
+        >
+          <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+            <path stroke-linecap="round" stroke-linejoin="round" d="m19 7-.867 12.142A2 2 0 0 1 16.138 21H7.862a2 2 0 0 1-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v3M4 7h16" />
+          </svg>
+        </button>
+
+        <div
+          class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg"
+          :class="fileType(source).chipClass"
+        >
+          <svg v-if="fileType(source).icon === 'image'" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
+            <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6.75A2.25 2.25 0 0 1 4.5 4.5h15a2.25 2.25 0 0 1 2.25 2.25v10.5A2.25 2.25 0 0 1 19.5 19.5h-15a2.25 2.25 0 0 1-2.25-2.25V6.75Z" />
+            <path stroke-linecap="round" stroke-linejoin="round" d="m3 16.5 5.159-5.159a2.25 2.25 0 0 1 3.182 0L16.5 16.5m-3-3 1.409-1.409a2.25 2.25 0 0 1 3.182 0L21 15" />
+            <circle cx="8.25" cy="8.625" r="1.125" />
+          </svg>
+          <svg v-else class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
+            <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
+          </svg>
+        </div>
 
-          <div v-if="confirmingSource === source" class="flex shrink-0 items-center gap-1.5">
-            <span class="text-xs text-gray-500 dark:text-gray-400">Excluir tudo?</span>
+        <span class="line-clamp-2 w-full break-words text-xs font-medium text-gray-900 dark:text-gray-100">
+          {{ source || "sem fonte" }}
+        </span>
+        <span class="text-[10px] text-gray-400 dark:text-gray-500">
+          {{ chunks.length }} chunk{{ chunks.length !== 1 ? 's' : '' }}<template v-if="chunks[0]?.ingestedAt"> · {{ formatDate(chunks[0].ingestedAt) }}</template>
+        </span>
+
+        <div
+          v-if="confirmingSource === source"
+          class="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-background p-3 dark:bg-[#101827]"
+        >
+          <span class="text-xs text-gray-500 dark:text-gray-400">Excluir tudo?</span>
+          <div class="flex items-center gap-1.5">
             <BaseButton
               variant="danger"
               size="sm"
@@ -152,31 +217,12 @@ function formatDate(iso) {
               Cancelar
             </BaseButton>
           </div>
-          <button
-            v-else-if="source"
-            class="shrink-0 rounded-lg border border-transparent p-1.5 text-gray-400 transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 dark:hover:border-red-900/50 dark:hover:bg-red-950/30 dark:hover:text-red-400"
-            :title="`Excluir '${source}'`"
-            @click="confirmingSource = source"
-          >
-            <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
-              <path stroke-linecap="round" stroke-linejoin="round" d="m19 7-.867 12.142A2 2 0 0 1 16.138 21H7.862a2 2 0 0 1-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v3M4 7h16" />
-            </svg>
-          </button>
-        </div>
-
-        <div class="border-t border-gray-100 px-3 py-2.5 dark:border-gray-700/50">
-          <div class="line-clamp-3 whitespace-pre-wrap text-xs leading-relaxed text-gray-500 dark:text-gray-400">
-            {{ chunks[0]?.text }}
-          </div>
-          <div v-if="chunks.length > 1" class="mt-1 text-xs text-gray-400 dark:text-gray-500">
-            + {{ chunks.length - 1 }} chunk{{ chunks.length - 1 !== 1 ? 's' : '' }} adicionais
-          </div>
         </div>
       </div>
 
       <div
         v-if="filterQuery && Object.keys(filteredGrouped).length === 0"
-        class="py-6 text-center text-sm text-gray-400 dark:text-gray-500"
+        class="col-span-full py-6 text-center text-sm text-gray-400 dark:text-gray-500"
       >
         Nenhum documento corresponde ao filtro.
       </div>

+ 0 - 40
src/components/base/BaseInput.vue

@@ -1,40 +0,0 @@
-<template>
-  <div class="grid gap-1.5">
-    <label v-if="label" :for="inputId" class="text-sm font-medium text-gray-700 dark:text-gray-300">
-      {{ label }}
-    </label>
-    <input
-      :id="inputId"
-      v-bind="$attrs"
-      :value="modelValue"
-      :class="[
-        'w-full rounded-xl border bg-background px-3 py-2 text-sm text-foreground placeholder:text-gray-400 outline-none transition-colors focus:ring-2 focus:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-gray-500',
-        error
-          ? 'border-red-400 focus:border-red-400 dark:border-red-500/60'
-          : 'border-gray-200 focus:border-primary dark:border-gray-700'
-      ]"
-      @input="$emit('update:modelValue', $event.target.value)"
-    />
-    <p v-if="error" class="text-xs text-red-600 dark:text-red-400">{{ error }}</p>
-    <p v-else-if="hint" class="text-xs text-gray-500 dark:text-gray-400">{{ hint }}</p>
-  </div>
-</template>
-
-<script setup>
-import { computed } from "vue";
-
-defineOptions({ inheritAttrs: false });
-
-const props = defineProps({
-  modelValue: { type: String, default: "" },
-  label: { type: String, default: "" },
-  hint: { type: String, default: "" },
-  error: { type: String, default: "" },
-  id: { type: String, default: "" }
-});
-
-defineEmits(["update:modelValue"]);
-
-let _counter = 0;
-const inputId = computed(() => props.id || `base-input-${++_counter}`);
-</script>

+ 10 - 8
src/components/skeletons/SkeletonDocumentCard.vue

@@ -1,18 +1,20 @@
 <template>
-  <div class="flex flex-col gap-2">
-    <div v-for="i in count" :key="i" class="rounded-xl border border-gray-200 dark:border-gray-700 p-3 flex items-center gap-3">
-      <div class="h-8 w-8 rounded-lg skeleton shrink-0" />
-      <div class="flex-1 flex flex-col gap-1.5">
-        <div class="h-3.5 rounded skeleton w-[60%]" />
-        <div class="h-3 rounded skeleton w-[40%]" />
-      </div>
+  <div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
+    <div
+      v-for="i in count"
+      :key="i"
+      class="flex aspect-square flex-col items-center justify-center gap-2 rounded-xl border border-gray-200 p-3 dark:border-gray-700"
+    >
+      <div class="h-10 w-10 rounded-lg skeleton shrink-0" />
+      <div class="h-3 w-[70%] rounded skeleton" />
+      <div class="h-2.5 w-[45%] rounded skeleton" />
     </div>
   </div>
 </template>
 
 <script setup>
 defineProps({
-  count: { type: Number, default: 3 }
+  count: { type: Number, default: 10 }
 });
 </script>
 

+ 2 - 0
src/layout/HeaderSistema.vue

@@ -31,6 +31,8 @@ const tituloPagina = computed(() => {
   if (nome === "configuracoes") return "Configurações";
   if (nome === "usuarios") return "Usuários";
   if (nome === "avaliacoes") return "Avaliações de Atendimento";
+  if (nome === "documentos") return "Documentos";
+  if (nome === "documentos-carregar") return "Carregar documento";
   return "Oráculo";
 });
 

+ 6 - 8
src/layout/SidebarSistema.vue

@@ -5,7 +5,6 @@ import ListaConversas from "./ListaConversas.vue";
 import { useAuth } from "../composables/useAuth.js";
 import { useChat } from "../composables/useChat.js";
 import { useSidebar } from "../composables/useSidebar.js";
-import { usePainelDocumentos } from "../composables/usePainelDocumentos.js";
 
 const router = useRouter();
 const route = useRoute();
@@ -13,7 +12,6 @@ const route = useRoute();
 const { user } = useAuth();
 const { activeConversationId, newConversation } = useChat();
 const { sidebarRecolhida, classeRotulo, classeItemSidebar, classeIcon } = useSidebar();
-const { painelAberto, alternarPainel } = usePainelDocumentos();
 
 const rotaAtiva = computed(() => String(route.name ?? ""));
 
@@ -162,12 +160,12 @@ async function novaConversa() {
 
           <button
             type="button"
-            :class="classeItemSidebar(painelAberto === 'update')"
+            :class="classeItemSidebar(rotaAtiva === 'documentos-carregar')"
             :title="sidebarRecolhida ? 'Carregar documento' : ''"
-            @click="alternarPainel('update')"
+            @click="irPara('documentos-carregar')"
           >
             <svg
-              :class="classeIcon(painelAberto === 'update')"
+              :class="classeIcon(rotaAtiva === 'documentos-carregar')"
               viewBox="0 0 24 24"
               fill="none"
               stroke="currentColor"
@@ -182,12 +180,12 @@ async function novaConversa() {
 
           <button
             type="button"
-            :class="classeItemSidebar(painelAberto === 'documentos')"
+            :class="classeItemSidebar(rotaAtiva === 'documentos')"
             :title="sidebarRecolhida ? 'Documentos' : ''"
-            @click="alternarPainel('documentos')"
+            @click="irPara('documentos')"
           >
             <svg
-              :class="classeIcon(painelAberto === 'documentos')"
+              :class="classeIcon(rotaAtiva === 'documentos')"
               viewBox="0 0 24 24"
               fill="none"
               stroke="currentColor"

+ 11 - 1
src/router/index.js

@@ -6,6 +6,8 @@ import ConfiguracoesView from "../views/configuracoes/ConfiguracoesView.vue";
 import UsuariosView from "../views/usuarios/UsuariosView.vue";
 import AvaliacoesView from "../views/atendimentos/AvaliacoesView.vue";
 import AtendimentoView from "../views/atendimentos/AtendimentoView.vue";
+import DocumentosView from "../views/documentos/DocumentosView.vue";
+import CarregarDocumentoView from "../views/documentos/CarregarDocumentoView.vue";
 import LoginView from "../views/auth/LoginView.vue";
 import { useAuth } from "../composables/useAuth.js";
 
@@ -20,6 +22,8 @@ export const router = createRouter({
     { path: "/usuarios", name: "usuarios", component: UsuariosView, meta: { requiresAuth: true } },
     { path: "/atendimentos/avaliacoes", name: "avaliacoes", component: AvaliacoesView, meta: { requiresAuth: true } },
     { path: "/atendimentos/:id(\\d+)", name: "atendimento", component: AtendimentoView, meta: { requiresAuth: true } },
+    { path: "/documentos", name: "documentos", component: DocumentosView, meta: { requiresAuth: true } },
+    { path: "/documentos/carregar", name: "documentos-carregar", component: CarregarDocumentoView, meta: { requiresAuth: true } },
     { path: "/:pathMatch(.*)*", redirect: "/" }
   ]
 });
@@ -38,7 +42,13 @@ router.beforeEach((to) => {
     };
   }
 
-  if (to.name === "usuarios" || to.name === "avaliacoes" || to.name === "atendimento") {
+  if (
+    to.name === "usuarios" ||
+    to.name === "avaliacoes" ||
+    to.name === "atendimento" ||
+    to.name === "documentos" ||
+    to.name === "documentos-carregar"
+  ) {
     const { user } = useAuth();
 
     if (String(user.value?.Nivel) !== "3") {

+ 26 - 0
src/views/documentos/CarregarDocumentoView.vue

@@ -0,0 +1,26 @@
+<script setup>
+import { useRouter } from "vue-router";
+import LayoutSistema from "../../layout/LayoutSistema.vue";
+import DocumentUpload from "../../components/DocumentUpload.vue";
+
+const router = useRouter();
+
+function aoIngerir() {
+  router.push({ name: "documentos" });
+}
+</script>
+
+<template>
+  <LayoutSistema>
+    <div class="mx-auto w-full max-w-[960px] space-y-6">
+      <div>
+        <h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Carregar documento</h1>
+        <p class="text-sm text-gray-500 dark:text-gray-400">
+          Adicione conhecimento à base via arquivo, URL ou texto colado.
+        </p>
+      </div>
+
+      <DocumentUpload @ingested="aoIngerir" />
+    </div>
+  </LayoutSistema>
+</template>

+ 43 - 0
src/views/documentos/DocumentosView.vue

@@ -0,0 +1,43 @@
+<script setup>
+import LayoutSistema from "../../layout/LayoutSistema.vue";
+import SearchBox from "../../components/SearchBox.vue";
+import DocumentList from "../../components/DocumentList.vue";
+import { useSearch } from "../../composables/useSearch.js";
+import { useDocumentos } from "../../composables/useDocumentos.js";
+
+const {
+  results: searchResults,
+  loading: searchLoading,
+  error: searchError,
+  run: runSearch,
+} = useSearch();
+
+const { docs, loadDocs } = useDocumentos();
+</script>
+
+<template>
+  <LayoutSistema>
+    <div class="w-full space-y-6">
+      <div>
+        <h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Documentos</h1>
+        <p class="text-sm text-gray-500 dark:text-gray-400">
+          Pesquise, revise e remova documentos ingeridos na base de conhecimento.
+        </p>
+      </div>
+
+      <SearchBox
+        :results="searchResults"
+        :loading="searchLoading"
+        :error="searchError"
+        @search="runSearch"
+      />
+
+      <DocumentList
+        :items="docs.items"
+        :loading="docs.loading"
+        :error="docs.error"
+        @refresh="loadDocs"
+      />
+    </div>
+  </LayoutSistema>
+</template>

+ 0 - 0
teste curl