|
@@ -1,5 +1,5 @@
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { computed, onMounted, reactive, ref } from "vue";
|
|
|
|
|
|
|
+import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
import BotaoAlterarTema from "../components/BotaoAlterarTema.vue";
|
|
import BotaoAlterarTema from "../components/BotaoAlterarTema.vue";
|
|
|
import DocumentList from "../components/DocumentList.vue";
|
|
import DocumentList from "../components/DocumentList.vue";
|
|
@@ -23,6 +23,8 @@ const docs = reactive({
|
|
|
|
|
|
|
|
const painelAberto = ref("");
|
|
const painelAberto = ref("");
|
|
|
const rotaAtiva = computed(() => String(route.name ?? ""));
|
|
const rotaAtiva = computed(() => String(route.name ?? ""));
|
|
|
|
|
+const classeBotaoSidebar =
|
|
|
|
|
+ "w-full rounded-lg border px-3 py-2.5 text-center text-sm font-medium transition-colors !border-primary !bg-none !bg-primary !text-primary-foreground hover:!bg-[var(--primary-600)] hover:!border-[var(--primary-600)]";
|
|
|
const tituloPagina = computed(() => {
|
|
const tituloPagina = computed(() => {
|
|
|
const nome = rotaAtiva.value;
|
|
const nome = rotaAtiva.value;
|
|
|
if (nome === "home") return "Nova conversa";
|
|
if (nome === "home") return "Nova conversa";
|
|
@@ -31,6 +33,12 @@ const tituloPagina = computed(() => {
|
|
|
return "Oráculo";
|
|
return "Oráculo";
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+const tituloModal = computed(() => {
|
|
|
|
|
+ if (painelAberto.value === "update") return "Update";
|
|
|
|
|
+ if (painelAberto.value === "documentos") return "Documentos";
|
|
|
|
|
+ return "";
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
function irPara(nome) {
|
|
function irPara(nome) {
|
|
|
router.push({ name: nome });
|
|
router.push({ name: nome });
|
|
|
}
|
|
}
|
|
@@ -40,6 +48,10 @@ function alternarPainel(nome) {
|
|
|
painelAberto.value = painelAberto.value === n ? "" : n;
|
|
painelAberto.value = painelAberto.value === n ? "" : n;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function fecharModal() {
|
|
|
|
|
+ painelAberto.value = "";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function novaConversa() {
|
|
async function novaConversa() {
|
|
|
newConversation();
|
|
newConversation();
|
|
|
await router.push({ name: "home" });
|
|
await router.push({ name: "home" });
|
|
@@ -75,198 +87,162 @@ async function onIngested({ text, source, file } = {}) {
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
await loadDocs();
|
|
await loadDocs();
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+function onKeyDown(e) {
|
|
|
|
|
+ if (!painelAberto.value) return;
|
|
|
|
|
+ if (e?.key === "Escape") fecharModal();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ window.addEventListener("keydown", onKeyDown);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
|
+ window.removeEventListener("keydown", onKeyDown);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+let overflowAntes = "";
|
|
|
|
|
+watch(
|
|
|
|
|
+ () => painelAberto.value,
|
|
|
|
|
+ (v) => {
|
|
|
|
|
+ const aberto = Boolean(v);
|
|
|
|
|
+ if (aberto) {
|
|
|
|
|
+ overflowAntes = document.body.style.overflow || "";
|
|
|
|
|
+ document.body.style.overflow = "hidden";
|
|
|
|
|
+ } else {
|
|
|
|
|
+ document.body.style.overflow = overflowAntes;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+);
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
|
- <div class="app">
|
|
|
|
|
- <div class="appShell layoutShell">
|
|
|
|
|
- <div class="layoutOverlay" aria-hidden="true" />
|
|
|
|
|
-
|
|
|
|
|
- <header class="layoutHeader">
|
|
|
|
|
- <div class="layoutHeaderLeft">
|
|
|
|
|
- <div class="brandTitle">ORÁCULO</div>
|
|
|
|
|
- <div class="brandSub">Assistente interno com base de conhecimento</div>
|
|
|
|
|
|
|
+ <div class="h-screen overflow-hidden bg-background text-foreground">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="[--layout-header-height:5rem] relative grid h-full min-h-0 grid-cols-[340px_1fr] grid-rows-[var(--layout-header-height)_1fr] overflow-hidden max-[980px]:grid-cols-1 max-[980px]:grid-rows-[var(--layout-header-height)_1fr_auto]"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ aria-hidden="true"
|
|
|
|
|
+ class="pointer-events-none absolute inset-0 z-0 bg-[radial-gradient(900px_520px_at_20%_10%,rgba(37,99,235,0.12),transparent_60%),radial-gradient(700px_520px_at_85%_35%,rgba(162,102,255,0.10),transparent_60%)] dark:hidden"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div
|
|
|
|
|
+ aria-hidden="true"
|
|
|
|
|
+ class="pointer-events-none absolute inset-0 z-0 hidden bg-[radial-gradient(900px_520px_at_20%_10%,rgba(91,140,255,0.18),transparent_60%),radial-gradient(700px_520px_at_85%_35%,rgba(162,102,255,0.14),transparent_60%)] dark:block"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <header
|
|
|
|
|
+ class="col-span-full row-start-1 relative z-10 grid grid-cols-[340px_1fr] items-center gap-[18px] border-b border-gray-200 bg-primary px-[18px] py-[14px] text-primary-foreground dark:border-gray-700 dark:bg-primary max-[980px]:flex"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="min-w-0">
|
|
|
|
|
+ <div class="text-[16px] font-extrabold tracking-[0.14em]">ORÁCULO</div>
|
|
|
|
|
+ <div class="mt-1.5 text-xs text-primary-foreground/80">Assistente interno com base de conhecimento</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="layoutHeaderRight">
|
|
|
|
|
|
|
+ <div class="min-w-0 flex items-center justify-between gap-3 max-[980px]:flex-1">
|
|
|
<div class="min-w-0">
|
|
<div class="min-w-0">
|
|
|
- <div class="text-base font-medium truncate ">{{ tituloPagina }}</div>
|
|
|
|
|
|
|
+ <div class="truncate text-base font-medium">{{ tituloPagina }}</div>
|
|
|
</div>
|
|
</div>
|
|
|
<BotaoAlterarTema />
|
|
<BotaoAlterarTema />
|
|
|
</div>
|
|
</div>
|
|
|
</header>
|
|
</header>
|
|
|
|
|
|
|
|
- <aside class="sidebar layoutSidebar">
|
|
|
|
|
|
|
+ <aside
|
|
|
|
|
+ class="col-start-1 row-start-2 relative z-10 flex min-h-0 flex-col gap-[14px] overflow-hidden border-r border-gray-200 bg-gray-100/50 p-[18px] shadow-sm backdrop-blur-md dark:border-gray-700 dark:bg-secondary/50 max-[980px]:row-start-3 max-[980px]:border-r-0 max-[980px]:border-b-0 max-[980px]:border-t"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="grid flex-1 min-h-0 gap-[14px] overflow-auto pr-0.5">
|
|
|
|
|
+ <div class="rounded-xl border border-gray-200 bg-background/60 p-4 text-sm text-gray-500 dark:border-gray-700 dark:bg-background/10 dark:text-gray-400">
|
|
|
|
|
+
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div class="sidebarScroll">
|
|
|
|
|
- <div class="panel sidebarNav">
|
|
|
|
|
|
|
+ <div class="rounded-xl border border-gray-200 bg-background/60 p-2 shadow-sm backdrop-blur-md dark:border-gray-700">
|
|
|
|
|
+ <div class="grid content-end gap-1.5">
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
|
- class="sidebarNavButton"
|
|
|
|
|
- :class="{ active: rotaAtiva === 'home' }"
|
|
|
|
|
|
|
+ :class="[classeBotaoSidebar, rotaAtiva === 'home' ? '!border-ring' : '']"
|
|
|
@click="novaConversa"
|
|
@click="novaConversa"
|
|
|
>
|
|
>
|
|
|
Nova conversa
|
|
Nova conversa
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
|
- class="sidebarNavButton"
|
|
|
|
|
- :class="{ active: rotaAtiva === 'conversas-pesquisar' }"
|
|
|
|
|
|
|
+ :class="[classeBotaoSidebar, rotaAtiva === 'conversas-pesquisar' ? '!border-ring' : '']"
|
|
|
@click="irPara('conversas-pesquisar')"
|
|
@click="irPara('conversas-pesquisar')"
|
|
|
>
|
|
>
|
|
|
Pesquisar conversas
|
|
Pesquisar conversas
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
|
- class="sidebarNavButton"
|
|
|
|
|
- :class="{ active: rotaAtiva === 'conversas-historico' }"
|
|
|
|
|
|
|
+ :class="[classeBotaoSidebar, rotaAtiva === 'conversas-historico' ? '!border-ring' : '']"
|
|
|
@click="irPara('conversas-historico')"
|
|
@click="irPara('conversas-historico')"
|
|
|
>
|
|
>
|
|
|
Histórico
|
|
Histórico
|
|
|
</button>
|
|
</button>
|
|
|
|
|
|
|
|
- <div class="sidebarNavDivider" />
|
|
|
|
|
|
|
+ <div class="my-1 h-px bg-gray-200 dark:bg-gray-700" />
|
|
|
|
|
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
|
- class="sidebarNavButton"
|
|
|
|
|
- :class="{ active: painelAberto === 'ingestao' }"
|
|
|
|
|
- @click="alternarPainel('ingestao')"
|
|
|
|
|
|
|
+ :class="[classeBotaoSidebar, painelAberto === 'update' ? '!border-ring' : '']"
|
|
|
|
|
+ @click="alternarPainel('update')"
|
|
|
>
|
|
>
|
|
|
- Ingestão
|
|
|
|
|
|
|
+ Update
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
|
- class="sidebarNavButton"
|
|
|
|
|
- :class="{ active: painelAberto === 'documentos' }"
|
|
|
|
|
|
|
+ :class="[classeBotaoSidebar, painelAberto === 'documentos' ? '!border-ring' : '']"
|
|
|
@click="alternarPainel('documentos')"
|
|
@click="alternarPainel('documentos')"
|
|
|
>
|
|
>
|
|
|
Documentos
|
|
Documentos
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- <DocumentUpload v-if="painelAberto === 'ingestao'" @ingested="onIngested" />
|
|
|
|
|
- <template v-if="painelAberto === 'documentos'">
|
|
|
|
|
- <SearchBox :results="searchResults" :loading="searchLoading" :error="searchError" @search="runSearch" />
|
|
|
|
|
- <DocumentList :items="docs.items" :loading="docs.loading" :error="docs.error" @refresh="loadDocs" />
|
|
|
|
|
- </template>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="sidebarFooter muted">
|
|
|
|
|
|
|
+ <div class="pr-8 text-[11px] leading-snug text-gray-400 dark:text-gray-500">
|
|
|
Backend: http://localhost:3001 · Qdrant: http://localhost:6333 · Ollama: http://localhost:11434
|
|
Backend: http://localhost:3001 · Qdrant: http://localhost:6333 · Ollama: http://localhost:11434
|
|
|
</div>
|
|
</div>
|
|
|
</aside>
|
|
</aside>
|
|
|
|
|
|
|
|
- <main class="main layoutMain !flex !flex-col !p-0 !overflow-hidden">
|
|
|
|
|
- <div class="flex-1 overflow-auto p-4 md:p-6">
|
|
|
|
|
|
|
+ <Teleport to="body">
|
|
|
|
|
+ <div v-if="painelAberto" class="fixed inset-0 z-[9999]">
|
|
|
|
|
+ <div class="absolute inset-0 bg-black/40 dark:bg-black/60" @click="fecharModal" />
|
|
|
|
|
+ <div class="absolute inset-0 flex items-center justify-center p-4">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="w-full max-w-[980px] overflow-hidden rounded-2xl border border-gray-200 bg-background text-foreground shadow-lg dark:border-gray-700"
|
|
|
|
|
+ @click.stop
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="flex items-center justify-between gap-3 border-b border-gray-200 px-4 py-3 dark:border-gray-700">
|
|
|
|
|
+ <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ tituloModal }}</div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="rounded-lg border border-gray-200 bg-background/60 px-3 py-1.5 text-sm text-gray-500 transition-colors hover:border-gray-500 dark:border-gray-700 dark:bg-background/10 dark:text-gray-400 dark:hover:border-gray-400"
|
|
|
|
|
+ @click="fecharModal"
|
|
|
|
|
+ >
|
|
|
|
|
+ Fechar
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="max-h-[calc(100vh-8rem)] overflow-auto p-4">
|
|
|
|
|
+ <DocumentUpload v-if="painelAberto === 'update'" @ingested="onIngested" />
|
|
|
|
|
+ <template v-else-if="painelAberto === 'documentos'">
|
|
|
|
|
+ <SearchBox :results="searchResults" :loading="searchLoading" :error="searchError" @search="runSearch" />
|
|
|
|
|
+ <div class="h-3" />
|
|
|
|
|
+ <DocumentList :items="docs.items" :loading="docs.loading" :error="docs.error" @refresh="loadDocs" />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Teleport>
|
|
|
|
|
+
|
|
|
|
|
+ <main class="col-start-2 row-start-2 relative z-10 flex min-h-0 flex-col overflow-hidden p-0 max-[980px]:col-start-1 max-[980px]:row-start-2">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="flex-1 min-h-0 overflow-auto p-4 md:p-6"
|
|
|
|
|
+ :class="rotaAtiva === 'home' ? 'bg-black/10' : ''"
|
|
|
|
|
+ >
|
|
|
<slot />
|
|
<slot />
|
|
|
</div>
|
|
</div>
|
|
|
</main>
|
|
</main>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
-
|
|
|
|
|
-<style scoped>
|
|
|
|
|
-.layoutShell {
|
|
|
|
|
- --layout-header-height: 5rem;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- grid-template-rows: var(--layout-header-height) 1fr;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.layoutOverlay {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- inset: 0;
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
- z-index: 0;
|
|
|
|
|
- background: radial-gradient(900px 520px at 20% 10%, rgba(37, 99, 235, 0.12), transparent 60%),
|
|
|
|
|
- radial-gradient(700px 520px at 85% 35%, rgba(162, 102, 255, 0.1), transparent 60%);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-:global(html.dark) .layoutOverlay {
|
|
|
|
|
- background: radial-gradient(900px 520px at 20% 10%, rgba(91, 140, 255, 0.18), transparent 60%),
|
|
|
|
|
- radial-gradient(700px 520px at 85% 35%, rgba(162, 102, 255, 0.14), transparent 60%);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.layoutHeader {
|
|
|
|
|
- grid-column: 1 / -1;
|
|
|
|
|
- grid-row: 1;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- z-index: 10;
|
|
|
|
|
- display: grid;
|
|
|
|
|
- grid-template-columns: 340px 1fr;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 18px;
|
|
|
|
|
- padding: 14px 18px;
|
|
|
|
|
- background: var(--primary);
|
|
|
|
|
- color: var(--primary-foreground);
|
|
|
|
|
- border-bottom: 1px solid var(--border);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.layoutHeaderLeft {
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.layoutHeaderRight {
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- gap: 14px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.layoutSidebar {
|
|
|
|
|
- grid-column: 1;
|
|
|
|
|
- grid-row: 2;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- z-index: 10;
|
|
|
|
|
- grid-template-rows: 1fr auto;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.layoutMain {
|
|
|
|
|
- grid-column: 2;
|
|
|
|
|
- grid-row: 2;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- z-index: 10;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-@media (max-width: 980px) {
|
|
|
|
|
- .layoutHeader {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .layoutHeaderRight {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.sidebarNav {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- align-content: end;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- padding: 6px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.sidebarNavButton {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 95%;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- background: var(--primary);
|
|
|
|
|
- color: var(--primary-foreground);
|
|
|
|
|
- border-color: var(--primary);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.sidebarNavButton:hover:not(:disabled) {
|
|
|
|
|
- background: var(--primary-600);
|
|
|
|
|
- border-color: var(--primary-600);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.sidebarNavButton.active {
|
|
|
|
|
- border-color: var(--ring);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.sidebarNavDivider {
|
|
|
|
|
- height: 1px;
|
|
|
|
|
- background: var(--border);
|
|
|
|
|
- margin: 4px 0;
|
|
|
|
|
-}
|
|
|
|
|
-</style>
|
|
|