| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797 |
- <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 { listDocuments } from "../api/chat.js";
- import { useChat } from "../composables/useChat.js";
- import { useSearch } from "../composables/useSearch.js";
- import { useAuth } from "../composables/useAuth.js";
- const router = useRouter();
- const route = useRoute();
- const {
- conversations,
- activeConversationId,
- conversationsLoading,
- setActiveConversation,
- deleteConversation,
- renameConversation,
- newConversation,
- } = useChat();
- const { user, logout } = useAuth();
- const {
- results: searchResults,
- loading: searchLoading,
- error: searchError,
- run: runSearch,
- } = useSearch();
- const docs = reactive({
- items: [],
- loading: false,
- error: "",
- });
- const logoutLoading = ref(false);
- const hoveredConversaId = ref(null);
- const editingConversaId = ref(null);
- const editingTitulo = ref("");
- const sidebarRecolhida = ref(false);
- const painelAberto = ref("");
- const menuUsuarioAberto = ref(false);
- const menuUsuarioRef = ref(null);
- const rotaAtiva = computed(() => String(route.name ?? ""));
- const usuarioNome = computed(() => user.value?.Nome || user.value?.Login || "Usuario");
- const usuarioSetor = computed(() => user.value?.Setor || user.value?.Nivel || "");
- const isNivel1 = computed(() => String(user.value?.Nivel) === "1");
- const layoutStyle = computed(() => ({
- "--layout-sidebar-width": sidebarRecolhida.value ? "84px" : "280px",
- }));
- const classeItemSidebarBase = computed(() =>
- sidebarRecolhida.value
- ? "w-full flex items-center justify-center gap-0 rounded-xl p-2.5 text-sm font-medium transition-colors !bg-none !bg-transparent !border-0 !transform-none hover:!transform-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
- : "w-full flex items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm font-medium transition-colors !bg-none !bg-transparent !border-0 !transform-none hover:!transform-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
- );
- const classeItemSidebarAtivo =
- "text-gray-900 !bg-gray-200/70 hover:!bg-gray-300/70 dark:text-white dark:!bg-white/10 dark:hover:!bg-white/10";
- const classeItemSidebarInativo =
- "text-gray-600 hover:text-gray-900 hover:!bg-gray-300/70 dark:text-white/70 dark:hover:text-white dark:hover:!bg-white/10";
- const classeIconAtivo = "text-gray-900 dark:text-white";
- const classeIconInativo = "text-gray-500 dark:text-white/60";
- const classeRotulo = computed(() =>
- sidebarRecolhida.value ? "sr-only" : "min-w-0 truncate",
- );
- function classeItemSidebar(ativo) {
- return [
- classeItemSidebarBase.value,
- ativo ? classeItemSidebarAtivo : classeItemSidebarInativo,
- ];
- }
- function classeIcon(ativo) {
- return ["h-5 w-5 shrink-0", ativo ? classeIconAtivo : classeIconInativo];
- }
- const AVATAR_CORES = [
- "bg-blue-500", "bg-violet-500", "bg-emerald-500",
- "bg-amber-500", "bg-rose-500", "bg-cyan-500", "bg-indigo-500"
- ];
- const avatarIniciais = computed(() => {
- const nome = usuarioNome.value.trim();
- const partes = nome.split(/\s+/);
- if (partes.length >= 2) return (partes[0][0] + partes[1][0]).toUpperCase();
- return nome.slice(0, 2).toUpperCase();
- });
- const avatarCor = computed(() => {
- let hash = 0;
- for (const c of usuarioNome.value) hash = (hash * 31 + c.charCodeAt(0)) & 0xffffffff;
- return AVATAR_CORES[Math.abs(hash) % AVATAR_CORES.length];
- });
- 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";
- if (nome === "configuracoes") return "Configurações";
- if (nome === "usuarios") return "Usuários";
- return "Oráculo";
- });
- const conversacoesAgrupadas = computed(() => {
- const hoje = new Date().setHours(0, 0, 0, 0);
- const ontem = hoje - 86400000;
- const semana = hoje - 6 * 86400000;
- const grupos = [
- { label: "Hoje", items: [] },
- { label: "Ontem", items: [] },
- { label: "Últimos 7 dias",items: [] },
- { label: "Mais antigas", items: [] },
- ];
- for (const c of conversations.value) {
- const t = c.updatedAt ?? 0;
- if (t >= hoje) grupos[0].items.push(c);
- else if (t >= ontem) grupos[1].items.push(c);
- else if (t >= semana) grupos[2].items.push(c);
- else grupos[3].items.push(c);
- }
- return grupos.filter((g) => g.items.length > 0);
- });
- const tituloModal = computed(() => {
- if (painelAberto.value === "update") return "Carregar documento";
- 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 = "";
- }
- function alternarMenuUsuario() {
- menuUsuarioAberto.value = !menuUsuarioAberto.value;
- }
- function fecharMenuUsuario() {
- menuUsuarioAberto.value = false;
- }
- async function novaConversa() {
- newConversation();
- await router.push({ name: "home" });
- }
- async function abrirConversa(id) {
- await setActiveConversation(id);
- await router.push({ name: "home" });
- }
- function iniciarEdicao(conversa) {
- editingConversaId.value = conversa.id;
- editingTitulo.value = conversa.title;
- }
- async function confirmarEdicao(id) {
- if (editingTitulo.value.trim()) await renameConversation(id, editingTitulo.value);
- editingConversaId.value = null;
- editingTitulo.value = "";
- }
- function cancelarEdicao() {
- editingConversaId.value = null;
- editingTitulo.value = "";
- }
- async function encerrarSessao() {
- if (logoutLoading.value) return;
- logoutLoading.value = true;
- try {
- await logout();
- await router.replace({ name: "login" });
- } finally {
- logoutLoading.value = false;
- }
- }
- 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() {
- await loadDocs();
- }
- onMounted(async () => {
- await loadDocs();
- });
- onMounted(() => {
- const v = window.localStorage.getItem("layout.sidebarRecolhida");
- if (v != null) sidebarRecolhida.value = v === "1" || v === "true";
- });
- watch(
- () => sidebarRecolhida.value,
- (v) => {
- window.localStorage.setItem("layout.sidebarRecolhida", v ? "1" : "0");
- },
- );
- function onKeyDown(e) {
- if (e?.key !== "Escape") return;
- if (menuUsuarioAberto.value) fecharMenuUsuario();
- else if (painelAberto.value) fecharModal();
- }
- function onDocumentClick(e) {
- if (!menuUsuarioAberto.value) return;
- if (!menuUsuarioRef.value?.contains(e.target)) fecharMenuUsuario();
- }
- onMounted(() => {
- window.addEventListener("keydown", onKeyDown);
- document.addEventListener("click", onDocumentClick);
- });
- onBeforeUnmount(() => {
- window.removeEventListener("keydown", onKeyDown);
- document.removeEventListener("click", onDocumentClick);
- });
- 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-[var(--layout-sidebar-width)_1fr] grid-rows-[var(--layout-header-height)_1fr] overflow-hidden transition-[grid-template-columns] duration-200 ease-in-out max-[980px]:grid-cols-1 max-[980px]:grid-rows-[var(--layout-header-height)_1fr_auto]"
- :style="layoutStyle"
- >
- <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-30 grid grid-cols-[var(--layout-sidebar-width)_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 overflow-hidden">
- <div
- class="truncate whitespace-nowrap text-[16px] font-extrabold tracking-[0.14em]"
- >
- ORÁCULO
- </div>
- <div
- v-if="!sidebarRecolhida"
- 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-4 max-[980px]:flex-1"
- >
- <div class="min-w-0">
- <div class="truncate text-base font-medium">{{ tituloPagina }}</div>
- </div>
- <div class="flex items-center gap-2">
- <BotaoAlterarTema />
- <div ref="menuUsuarioRef" class="relative">
- <button
- type="button"
- class="flex items-center gap-2.5 rounded-xl px-1.5 py-1.5 !text-primary-foreground !border-0 !bg-none !bg-transparent !transform-none motion-safe:transition-colors hover:!bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background md:pr-2.5"
- :aria-expanded="menuUsuarioAberto"
- aria-haspopup="menu"
- aria-label="Menu do usuário"
- @click="alternarMenuUsuario"
- >
- <div
- class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-xs font-bold text-white ring-1 ring-white/20"
- :class="avatarCor"
- >
- {{ avatarIniciais }}
- </div>
- <div class="hidden min-w-0 text-left md:block">
- <div class="truncate text-sm font-semibold leading-tight">{{ usuarioNome }}</div>
- <div
- v-if="usuarioSetor"
- class="truncate text-xs text-primary-foreground/70 leading-tight"
- >
- {{ usuarioSetor }}
- </div>
- </div>
- <svg
- class="hidden h-4 w-4 shrink-0 text-primary-foreground/70 transition-transform md:block"
- :class="menuUsuarioAberto ? 'rotate-180' : ''"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="1.8"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="m6 9 6 6 6-6"
- />
- </svg>
- </button>
- <transition
- enter-active-class="transition duration-150 ease-out"
- enter-from-class="opacity-0 -translate-y-1 scale-95"
- enter-to-class="opacity-100 translate-y-0 scale-100"
- leave-active-class="transition duration-100 ease-in"
- leave-from-class="opacity-100 translate-y-0 scale-100"
- leave-to-class="opacity-0 -translate-y-1 scale-95"
- >
- <div
- v-if="menuUsuarioAberto"
- role="menu"
- class="absolute right-0 top-full z-20 mt-2 w-60 origin-top-right overflow-hidden rounded-xl border border-border bg-background text-foreground shadow-lg"
- >
- <div class="flex items-center gap-3 border-b border-border px-3 py-3">
- <div
- class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-sm font-bold text-white"
- :class="avatarCor"
- >
- {{ avatarIniciais }}
- </div>
- <div class="min-w-0">
- <div class="truncate text-sm font-semibold leading-tight">{{ usuarioNome }}</div>
- <div
- v-if="usuarioSetor"
- class="truncate text-xs text-foreground/60 leading-tight"
- >
- {{ usuarioSetor }}
- </div>
- </div>
- </div>
- <div class="p-1.5">
- <button
- type="button"
- role="menuitem"
- class="flex w-full items-center gap-2.5 rounded-lg !border-0 !bg-none !bg-transparent px-2.5 py-2 text-left text-sm font-medium !text-foreground !transform-none motion-safe:transition-colors hover:!bg-black/5 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring dark:hover:!bg-white/10"
- @click="fecharMenuUsuario(); irPara('configuracoes')"
- >
- <svg
- class="h-5 w-5 shrink-0 text-foreground/60"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="1.8"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 0 1 1.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.894.149c-.424.07-.764.383-.929.78-.165.398-.143.854.107 1.204l.527.738c.32.447.27 1.06-.12 1.45l-.774.773a1.125 1.125 0 0 1-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.398.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.27-1.45-.12l-.773-.774a1.125 1.125 0 0 1-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.108-1.204l-.526-.738a1.125 1.125 0 0 1 .12-1.45l.773-.773a1.125 1.125 0 0 1 1.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894Z"
- />
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
- />
- </svg>
- <span>Configurações</span>
- </button>
- <button
- type="button"
- role="menuitem"
- class="flex w-full items-center gap-2.5 rounded-lg !border-0 !bg-none !bg-transparent px-2.5 py-2 text-left text-sm font-medium !text-red-600 !transform-none motion-safe:transition-colors hover:!bg-red-500/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 dark:!text-red-400"
- :disabled="logoutLoading"
- @click="encerrarSessao"
- >
- <svg
- class="h-5 w-5 shrink-0"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="1.8"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9"
- />
- </svg>
- <span>{{ logoutLoading ? "Saindo..." : "Sair" }}</span>
- </button>
- </div>
- </div>
- </transition>
- </div>
- </div>
- </div>
- </header>
- <aside
- class="col-start-1 row-start-2 relative z-10 flex min-h-0 min-w-0 flex-col gap-3 overflow-hidden border-r border-gray-200 bg-transparent p-2 dark:border-gray-700 max-[980px]:row-start-3 max-[980px]:border-r-0 max-[980px]:border-t"
- >
- <div
- class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-[28px] border border-gray-200 bg-white/70 p-3 shadow-sm backdrop-blur-md dark:border-white/10 dark:bg-[#101827] dark:shadow-[0_18px_50px_rgba(0,0,0,0.35)]"
- >
- <div
- :class="
- sidebarRecolhida
- ? 'flex items-center justify-center px-1 pb-1'
- : 'flex items-center justify-end px-1 pb-1'
- "
- >
- <button
- type="button"
- class="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-gray-200 bg-white/60 text-gray-600 transition-colors hover:bg-white/80 hover:text-gray-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background dark:border-white/10 dark:bg-white/5 dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
- :aria-label="
- sidebarRecolhida ? 'Expandir sidebar' : 'Recolher sidebar'
- "
- :title="sidebarRecolhida ? 'Expandir sidebar' : 'Recolher sidebar'"
- @click="sidebarRecolhida = !sidebarRecolhida"
- >
- <svg
- v-if="sidebarRecolhida"
- class="h-5 w-5"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="1.8"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="m9 6 6 6-6 6"
- />
- </svg>
- <svg
- v-else
- class="h-5 w-5"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="1.8"
- >
- <path
- stroke-linecap="round"
- stroke-linejoin="round"
- d="m15 6-6 6 6 6"
- />
- </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"
- >
- Chat
- </div>
- <nav class="grid gap-1">
- <button
- type="button"
- :class="classeItemSidebar(rotaAtiva === 'home' && !activeConversationId)"
- :title="sidebarRecolhida ? 'Nova conversa' : ''"
- @click="novaConversa"
- >
- <svg
- :class="classeIcon(rotaAtiva === 'home' && !activeConversationId)"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="1.8"
- >
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
- </svg>
- <span :class="classeRotulo">Nova conversa</span>
- </button>
- </nav>
- <!-- Lista de conversas recentes (apenas sidebar expandida) -->
- <div
- v-if="!sidebarRecolhida"
- class="mt-0.5 -mx-0.5 px-0.5"
- >
- <div v-if="conversationsLoading" class="px-3 py-2 text-xs text-gray-400 dark:text-white/30">
- Carregando...
- </div>
- <template v-for="grupo in conversacoesAgrupadas" :key="grupo.label">
- <div class="px-3 pt-2 pb-0.5 text-[10px] font-medium text-gray-400 dark:text-white/30 select-none">
- {{ grupo.label }}
- </div>
- <div class="grid gap-1">
- <button
- v-for="c in grupo.items"
- :key="c.id"
- type="button"
- class="relative w-full flex items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors overflow-hidden !bg-none !border-0 !transform-none hover:!transform-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
- :class="String(activeConversationId) === c.id
- ? 'text-gray-900 bg-gray-200/70 dark:text-white dark:bg-white/10'
- : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100/80 dark:text-white/70 dark:hover:text-white dark:hover:bg-white/10'"
- @mouseenter="hoveredConversaId = c.id"
- @mouseleave="hoveredConversaId = null"
- @click="editingConversaId !== c.id && abrirConversa(c.id)"
- >
- <template v-if="editingConversaId === c.id">
- <input
- type="text"
- v-model="editingTitulo"
- class="flex-1 min-w-0 bg-transparent text-sm outline-none border-b border-gray-400 dark:border-white/40 text-gray-900 dark:text-white"
- @keydown.enter.prevent="confirmarEdicao(c.id)"
- @keydown.escape.prevent="cancelarEdicao"
- @click.stop
- autofocus
- />
- <div class="flex items-center gap-0.5 shrink-0" @click.stop>
- <button
- type="button"
- class="rounded p-0.5 text-gray-500 hover:text-gray-900 dark:text-white/50 dark:hover:text-white transition-colors"
- title="Confirmar"
- @click.stop="confirmarEdicao(c.id)"
- >
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
- </svg>
- </button>
- <button
- type="button"
- class="rounded p-0.5 text-gray-500 hover:text-gray-900 dark:text-white/50 dark:hover:text-white transition-colors"
- title="Cancelar"
- @click.stop="cancelarEdicao"
- >
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
- </svg>
- </button>
- </div>
- </template>
- <template v-else>
- <span class="flex-1 min-w-0 truncate leading-snug">{{ c.title }}</span>
- <div
- v-show="hoveredConversaId === c.id"
- class="absolute right-0 top-0 h-full flex items-center gap-0.5 pr-2 pl-16"
- :class="String(activeConversationId) === c.id
- ? 'bg-gradient-to-l from-gray-200 from-60% dark:from-[#131f30] to-transparent'
- : 'bg-gradient-to-l from-gray-100 from-60% dark:from-[#101827] to-transparent'"
- @click.stop
- >
- <button
- type="button"
- class="rounded p-0.5 text-gray-400 hover:text-gray-700 dark:text-white/40 dark:hover:text-white transition-colors"
- title="Renomear"
- @click.stop="iniciarEdicao(c)"
- >
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125" />
- </svg>
- </button>
- <button
- type="button"
- class="rounded p-0.5 text-gray-400 hover:text-red-600 dark:text-white/40 dark:hover:text-red-400 transition-colors"
- title="Excluir"
- @click.stop="deleteConversation(c.id)"
- >
- <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
- </svg>
- </button>
- </div>
- </template>
- </button>
- </div>
- </template>
- <div
- v-if="!conversationsLoading && conversacoesAgrupadas.length === 0"
- class="px-3 py-2 text-xs text-gray-400 dark:text-white/30 italic"
- >
- Nenhuma conversa ainda
- </div>
- </div>
- <!-- 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">
- <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"
- >
- Biblioteca
- </div>
- <button
- type="button"
- :class="classeItemSidebar(painelAberto === 'update')"
- :title="sidebarRecolhida ? 'Carregar documento' : ''"
- @click="alternarPainel('update')"
- >
- <svg
- :class="classeIcon(painelAberto === 'update')"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="1.8"
- >
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v12" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 7.5 12 3l4.5 4.5" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M4 21h16" />
- </svg>
- <span :class="classeRotulo">Carregar documento</span>
- </button>
- <button
- type="button"
- :class="classeItemSidebar(painelAberto === 'documentos')"
- :title="sidebarRecolhida ? 'Documentos' : ''"
- @click="alternarPainel('documentos')"
- >
- <svg
- :class="classeIcon(painelAberto === 'documentos')"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="1.8"
- >
- <path stroke-linecap="round" stroke-linejoin="round" d="M7 3h7l3 3v15a.75.75 0 0 1-.75.75H7.75A.75.75 0 0 1 7 21V3Z" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M14 3v4h4" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6M9 15.5h6" />
- </svg>
- <span :class="classeRotulo">Documentos</span>
- </button>
- <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
- </template>
- <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"
- >
- Sistema
- </div>
- <button
- type="button"
- :class="classeItemSidebar(rotaAtiva === 'configuracoes')"
- :title="sidebarRecolhida ? 'Configurações' : ''"
- @click="irPara('configuracoes')"
- >
- <svg
- :class="classeIcon(rotaAtiva === 'configuracoes')"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- stroke-width="1.8"
- >
- <path stroke-linecap="round" stroke-linejoin="round" d="M4 21v-7" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M4 10V3" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 21v-9" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 8V3" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M20 21v-5" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M20 12V3" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M1 14h6" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 8h6" />
- <path stroke-linecap="round" stroke-linejoin="round" d="M17 16h6" />
- </svg>
- <span :class="classeRotulo">Configurações</span>
- </button>
- </div>
- </div>
-
- </aside>
- <Teleport to="body">
- <Transition name="modal">
- <div v-if="painelAberto" class="fixed inset-0 z-[9999]">
- <div
- class="modal-backdrop 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="modal-panel w-full max-w-[980px] overflow-hidden rounded-2xl border border-gray-200 bg-background text-foreground shadow-xl dark:border-gray-700"
- @click.stop
- >
- <div
- class="flex items-center justify-between gap-3 border-b border-gray-200 px-5 py-4 dark:border-gray-700"
- >
- <div
- class="text-base font-semibold text-gray-900 dark:text-gray-100"
- >
- {{ tituloModal }}
- </div>
- <button
- type="button"
- class="rounded-lg border border-transparent p-1.5 text-gray-500 transition-colors hover:border-gray-200 hover:bg-gray-100 dark:text-gray-400 dark:hover:border-gray-700 dark:hover:bg-white/5"
- @click="fecharModal"
- >
- <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
- </svg>
- </button>
- </div>
- <div class="max-h-[calc(100vh-8rem)] overflow-auto p-5">
- <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-4" />
- <DocumentList
- :items="docs.items"
- :loading="docs.loading"
- :error="docs.error"
- @refresh="loadDocs"
- />
- </template>
- </div>
- </div>
- </div>
- </div>
- </Transition>
- </Teleport>
- <main
- class="col-start-2 row-start-2 relative z-10 flex min-h-0 min-w-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">
- <slot />
- </div>
- </main>
- </div>
- </div>
- </template>
- <style scoped>
- .modal-enter-active,
- .modal-leave-active {
- transition: opacity 0.2s ease;
- }
- .modal-enter-from,
- .modal-leave-to {
- opacity: 0;
- }
- .modal-enter-active .modal-panel,
- .modal-leave-active .modal-panel {
- transition: transform 0.2s ease, opacity 0.2s ease;
- }
- .modal-enter-from .modal-panel,
- .modal-leave-to .modal-panel {
- transform: scale(0.96) translateY(8px);
- opacity: 0;
- }
- </style>
|