| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- <script setup>
- import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import BotaoAlterarTema from "../components/BotaoAlterarTema.vue";
- import DocumentList from "../components/DocumentList.vue";
- import DocumentUpload from "../components/DocumentUpload.vue";
- import SearchBox from "../components/SearchBox.vue";
- import { ingestDocuments, ingestFile, listDocuments } from "../api/chat.js";
- import { useChat } from "../composables/useChat.js";
- import { useSearch } from "../composables/useSearch.js";
- const router = useRouter();
- const route = useRoute();
- const { newConversation } = useChat();
- const { results: searchResults, loading: searchLoading, error: searchError, run: runSearch } = useSearch();
- const docs = reactive({
- items: [],
- loading: false,
- error: ""
- });
- const painelAberto = ref("");
- 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 nome = rotaAtiva.value;
- if (nome === "home") return "Nova conversa";
- if (nome === "conversas-pesquisar") return "Pesquisar conversas";
- if (nome === "conversas-historico") return "Histórico";
- return "Oráculo";
- });
- const tituloModal = computed(() => {
- if (painelAberto.value === "update") return "Update";
- if (painelAberto.value === "documentos") return "Documentos";
- return "";
- });
- function irPara(nome) {
- router.push({ name: nome });
- }
- function alternarPainel(nome) {
- const n = String(nome ?? "");
- painelAberto.value = painelAberto.value === n ? "" : n;
- }
- function fecharModal() {
- painelAberto.value = "";
- }
- async function novaConversa() {
- newConversation();
- await router.push({ name: "home" });
- }
- async function loadDocs() {
- docs.loading = true;
- docs.error = "";
- try {
- const data = await listDocuments({ limit: 50, offset: 0 });
- docs.items = data.items ?? [];
- } catch (e) {
- docs.error = e?.message || "erro";
- docs.items = [];
- } finally {
- docs.loading = false;
- }
- }
- async function onIngested({ text, source, file } = {}) {
- try {
- if (file) {
- await ingestFile(file, { source });
- } else {
- await ingestDocuments([{ text, source }]);
- }
- await loadDocs();
- } catch (e) {
- docs.error = e?.message || "erro";
- }
- }
- onMounted(async () => {
- 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>
- <template>
- <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 class="min-w-0 flex items-center justify-between gap-3 max-[980px]:flex-1">
- <div class="min-w-0">
- <div class="truncate text-base font-medium">{{ tituloPagina }}</div>
- </div>
- <BotaoAlterarTema />
- </div>
- </header>
- <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="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
- type="button"
- :class="[classeBotaoSidebar, rotaAtiva === 'home' ? '!border-ring' : '']"
- @click="novaConversa"
- >
- Nova conversa
- </button>
- <button
- type="button"
- :class="[classeBotaoSidebar, rotaAtiva === 'conversas-pesquisar' ? '!border-ring' : '']"
- @click="irPara('conversas-pesquisar')"
- >
- Pesquisar conversas
- </button>
- <button
- type="button"
- :class="[classeBotaoSidebar, rotaAtiva === 'conversas-historico' ? '!border-ring' : '']"
- @click="irPara('conversas-historico')"
- >
- Histórico
- </button>
- <div class="my-1 h-px bg-gray-200 dark:bg-gray-700" />
- <button
- type="button"
- :class="[classeBotaoSidebar, painelAberto === 'update' ? '!border-ring' : '']"
- @click="alternarPainel('update')"
- >
- Update
- </button>
- <button
- type="button"
- :class="[classeBotaoSidebar, painelAberto === 'documentos' ? '!border-ring' : '']"
- @click="alternarPainel('documentos')"
- >
- Documentos
- </button>
- </div>
- </div>
- <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
- </div>
- </aside>
- <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 />
- </div>
- </main>
- </div>
- </div>
- </template>
|