Procházet zdrojové kódy

ajustes layout do sistema

leonardo před 2 měsíci
rodič
revize
3beed229ad

+ 33 - 0
src/composables/useConversaEdicao.js

@@ -0,0 +1,33 @@
+import { ref } from "vue";
+import { useChat } from "./useChat.js";
+
+export function useConversaEdicao() {
+  const { renameConversation } = useChat();
+
+  const editingConversaId = ref(null);
+  const editingTitulo = ref("");
+
+  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 = "";
+  }
+
+  return {
+    editingConversaId,
+    editingTitulo,
+    iniciarEdicao,
+    confirmarEdicao,
+    cancelarEdicao,
+  };
+}

+ 38 - 0
src/composables/useDocumentos.js

@@ -0,0 +1,38 @@
+import { onMounted, reactive } from "vue";
+import { listDocuments } from "../api/chat.js";
+
+export function useDocumentos() {
+  const docs = reactive({
+    items: [],
+    loading: false,
+    error: "",
+  });
+
+  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();
+  });
+
+  return {
+    docs,
+    loadDocs,
+    onIngested,
+  };
+}

+ 42 - 0
src/composables/useMenuUsuario.js

@@ -0,0 +1,42 @@
+import { onBeforeUnmount, onMounted, ref } from "vue";
+
+export function useMenuUsuario({ onEscape } = {}) {
+  const menuUsuarioAberto = ref(false);
+  const menuUsuarioRef = ref(null);
+
+  function alternarMenuUsuario() {
+    menuUsuarioAberto.value = !menuUsuarioAberto.value;
+  }
+
+  function fecharMenuUsuario() {
+    menuUsuarioAberto.value = false;
+  }
+
+  function onKeyDown(e) {
+    if (e?.key !== "Escape") return;
+    if (menuUsuarioAberto.value) fecharMenuUsuario();
+    else onEscape?.();
+  }
+
+  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);
+  });
+
+  return {
+    menuUsuarioAberto,
+    menuUsuarioRef,
+    alternarMenuUsuario,
+    fecharMenuUsuario,
+  };
+}

+ 45 - 0
src/composables/usePainelDocumentos.js

@@ -0,0 +1,45 @@
+import { computed, ref, watch } from "vue";
+
+let singleton;
+
+export function usePainelDocumentos() {
+  if (singleton) return singleton;
+
+  const painelAberto = ref("");
+  let overflowAntes = "";
+
+  const tituloModal = computed(() => {
+    if (painelAberto.value === "update") return "Carregar documento";
+    if (painelAberto.value === "documentos") return "Documentos";
+    return "";
+  });
+
+  function alternarPainel(nome) {
+    const n = String(nome ?? "");
+    painelAberto.value = painelAberto.value === n ? "" : n;
+  }
+
+  function fecharModal() {
+    painelAberto.value = "";
+  }
+
+  watch(
+    () => painelAberto.value,
+    (v) => {
+      if (v) {
+        overflowAntes = document.body.style.overflow || "";
+        document.body.style.overflow = "hidden";
+      } else {
+        document.body.style.overflow = overflowAntes;
+      }
+    },
+  );
+
+  singleton = {
+    painelAberto,
+    tituloModal,
+    alternarPainel,
+    fecharModal,
+  };
+  return singleton;
+}

+ 55 - 0
src/composables/useSidebar.js

@@ -0,0 +1,55 @@
+import { computed, ref, watch } from "vue";
+
+let singleton;
+
+export function useSidebar() {
+  if (singleton) return singleton;
+
+  const sidebarRecolhida = ref(false);
+
+  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 armazenado = window.localStorage.getItem("layout.sidebarRecolhida");
+  if (armazenado != null) sidebarRecolhida.value = armazenado === "1" || armazenado === "true";
+
+  watch(
+    () => sidebarRecolhida.value,
+    (v) => { window.localStorage.setItem("layout.sidebarRecolhida", v ? "1" : "0"); },
+  );
+
+  singleton = {
+    sidebarRecolhida,
+    layoutStyle,
+    classeRotulo,
+    classeItemSidebar,
+    classeIcon,
+  };
+  return singleton;
+}

+ 206 - 0
src/layout/HeaderSistema.vue

@@ -0,0 +1,206 @@
+<script setup>
+import { computed, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import BotaoAlterarTema from "../components/BotaoAlterarTema.vue";
+import { avatarIniciais as _avatarIniciais, avatarCor as _avatarCor } from "../utils/avatar.js";
+import { useAuth } from "../composables/useAuth.js";
+import { useSidebar } from "../composables/useSidebar.js";
+import { usePainelDocumentos } from "../composables/usePainelDocumentos.js";
+import { useMenuUsuario } from "../composables/useMenuUsuario.js";
+
+const router = useRouter();
+const route = useRoute();
+
+const { user, logout } = useAuth();
+const { sidebarRecolhida } = useSidebar();
+const { fecharModal } = usePainelDocumentos();
+const { menuUsuarioAberto, menuUsuarioRef, alternarMenuUsuario, fecharMenuUsuario } = useMenuUsuario({ onEscape: fecharModal });
+
+const logoutLoading = ref(false);
+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 avatarIniciais = computed(() => _avatarIniciais(usuarioNome.value));
+const avatarCor = computed(() => _avatarCor(usuarioNome.value));
+
+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";
+});
+
+function irPara(nome) {
+  router.push({ name: nome });
+}
+
+async function encerrarSessao() {
+  if (logoutLoading.value) return;
+  logoutLoading.value = true;
+  try {
+    await logout();
+    await router.replace({ name: "login" });
+  } finally {
+    logoutLoading.value = false;
+  }
+}
+</script>
+
+<template>
+  <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>
+</template>

+ 8 - 832
src/layout/LayoutSistema.vue

@@ -1,260 +1,10 @@
 <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 SkeletonConversation from "../components/skeletons/SkeletonConversation.vue";
-import { avatarIniciais as _avatarIniciais, avatarCor as _avatarCor } from "../utils/avatar.js";
-import { listDocuments } from "../api/chat.js";
-import { useChat } from "../composables/useChat.js";
-import { useSearch } from "../composables/useSearch.js";
-import { useAuth } from "../composables/useAuth.js";
+import HeaderSistema from "./HeaderSistema.vue";
+import SidebarSistema from "./SidebarSistema.vue";
+import ModalDocumentos from "./ModalDocumentos.vue";
+import { useSidebar } from "../composables/useSidebar.js";
 
-const router = useRouter();
-const route = useRoute();
-const {
-  conversations,
-  activeConversationId,
-  conversationsLoading,
-  conversationsError,
-  conversationsHasMore,
-  setActiveConversation,
-  deleteConversation,
-  renameConversation,
-  newConversation,
-  loadMoreConversations,
-} = 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 avatarIniciais = computed(() => _avatarIniciais(usuarioNome.value));
-const avatarCor = computed(() => _avatarCor(usuarioNome.value));
-
-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;
-    }
-  },
-);
+const { layoutStyle } = useSidebar();
 </script>
 
 <template>
@@ -272,538 +22,11 @@ watch(
         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>
-
-            <button
-              type="button"
-              :class="classeItemSidebar(rotaAtiva === 'conversas-historico')"
-              :title="sidebarRecolhida ? 'Histórico' : ''"
-              @click="irPara('conversas-historico')"
-            >
-              <svg
-                :class="classeIcon(rotaAtiva === 'conversas-historico')"
-                viewBox="0 0 24 24"
-                fill="none"
-                stroke="currentColor"
-                stroke-width="1.8"
-              >
-                <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6l4 2" />
-                <path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-3.2-6.9" />
-              </svg>
-              <span :class="classeRotulo">Histórico</span>
-            </button>
-
-            <button
-              type="button"
-              :class="classeItemSidebar(rotaAtiva === 'conversas-pesquisar')"
-              :title="sidebarRecolhida ? 'Pesquisar' : ''"
-              @click="irPara('conversas-pesquisar')"
-            >
-              <svg
-                :class="classeIcon(rotaAtiva === 'conversas-pesquisar')"
-                viewBox="0 0 24 24"
-                fill="none"
-                stroke="currentColor"
-                stroke-width="1.8"
-              >
-                <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
-              </svg>
-              <span :class="classeRotulo">Pesquisar</span>
-            </button>
-          </nav>
-
-          <!-- Lista de conversas recentes (apenas sidebar expandida) -->
-          <div
-            v-if="!sidebarRecolhida"
-            class="conv-scroll mt-0.5 -mx-0.5 px-0.5 flex-1 min-h-0 overflow-y-auto"
-          >
-            <div v-if="conversationsLoading" class="py-1">
-              <SkeletonConversation :count="5" />
-            </div>
-            <div v-else-if="conversationsError" class="px-3 py-2 text-xs text-red-400 dark:text-red-400/70">
-              {{ conversationsError }}
-            </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" :title="c.title">{{ 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>
-
-            <button
-              v-if="conversationsHasMore"
-              type="button"
-              class="mt-1 w-full rounded-xl px-3 py-2 text-left text-xs text-gray-400 transition-colors hover:bg-gray-100/80 hover:text-gray-700 dark:text-white/30 dark:hover:bg-white/10 dark:hover:text-white/70"
-              :disabled="conversationsLoading"
-              @click="loadMoreConversations"
-            >
-              {{ conversationsLoading ? "Carregando..." : "Carregar mais" }}
-            </button>
-          </div>
+      <HeaderSistema />
 
-          <!-- 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>
+      <SidebarSistema />
 
-              <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" @click="fecharModal">
-              <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>
+      <ModalDocumentos />
 
       <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"
@@ -815,50 +38,3 @@ watch(
     </div>
   </div>
 </template>
-
-<style scoped>
-.conv-scroll {
-  scrollbar-width: thin;
-  scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
-}
-.conv-scroll::-webkit-scrollbar {
-  width: 3px;
-}
-.conv-scroll::-webkit-scrollbar-track {
-  background: transparent;
-}
-.conv-scroll::-webkit-scrollbar-thumb {
-  background: rgba(0, 0, 0, 0.15);
-  border-radius: 999px;
-}
-.conv-scroll::-webkit-scrollbar-thumb:hover {
-  background: rgba(0, 0, 0, 0.25);
-}
-:is(.dark) .conv-scroll {
-  scrollbar-color: rgba(255, 255, 255, 0.12) transparent;
-}
-:is(.dark) .conv-scroll::-webkit-scrollbar-thumb {
-  background: rgba(255, 255, 255, 0.12);
-}
-:is(.dark) .conv-scroll::-webkit-scrollbar-thumb:hover {
-  background: rgba(255, 255, 255, 0.22);
-}
-
-.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>

+ 190 - 0
src/layout/ListaConversas.vue

@@ -0,0 +1,190 @@
+<script setup>
+import { computed, ref } from "vue";
+import { useRouter } from "vue-router";
+import SkeletonConversation from "../components/skeletons/SkeletonConversation.vue";
+import { useChat } from "../composables/useChat.js";
+import { useConversaEdicao } from "../composables/useConversaEdicao.js";
+
+const router = useRouter();
+
+const {
+  conversations,
+  activeConversationId,
+  conversationsLoading,
+  conversationsError,
+  conversationsHasMore,
+  setActiveConversation,
+  deleteConversation,
+  loadMoreConversations,
+} = useChat();
+
+const { editingConversaId, editingTitulo, iniciarEdicao, confirmarEdicao, cancelarEdicao } = useConversaEdicao();
+
+const hoveredConversaId = ref(null);
+
+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);
+});
+
+async function abrirConversa(id) {
+  await setActiveConversation(id);
+  await router.push({ name: "home" });
+}
+</script>
+
+<template>
+  <div class="conv-scroll mt-0.5 -mx-0.5 px-0.5 flex-1 min-h-0 overflow-y-auto">
+    <div v-if="conversationsLoading" class="py-1">
+      <SkeletonConversation :count="5" />
+    </div>
+    <div v-else-if="conversationsError" class="px-3 py-2 text-xs text-red-400 dark:text-red-400/70">
+      {{ conversationsError }}
+    </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" :title="c.title">{{ 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>
+
+    <button
+      v-if="conversationsHasMore"
+      type="button"
+      class="mt-1 w-full rounded-xl px-3 py-2 text-left text-xs text-gray-400 transition-colors hover:bg-gray-100/80 hover:text-gray-700 dark:text-white/30 dark:hover:bg-white/10 dark:hover:text-white/70"
+      :disabled="conversationsLoading"
+      @click="loadMoreConversations"
+    >
+      {{ conversationsLoading ? "Carregando..." : "Carregar mais" }}
+    </button>
+  </div>
+</template>
+
+<style scoped>
+.conv-scroll {
+  scrollbar-width: thin;
+  scrollbar-color: rgba(0, 0, 0, 0.15) transparent;
+}
+.conv-scroll::-webkit-scrollbar {
+  width: 3px;
+}
+.conv-scroll::-webkit-scrollbar-track {
+  background: transparent;
+}
+.conv-scroll::-webkit-scrollbar-thumb {
+  background: rgba(0, 0, 0, 0.15);
+  border-radius: 999px;
+}
+.conv-scroll::-webkit-scrollbar-thumb:hover {
+  background: rgba(0, 0, 0, 0.25);
+}
+:is(.dark) .conv-scroll {
+  scrollbar-color: rgba(255, 255, 255, 0.12) transparent;
+}
+:is(.dark) .conv-scroll::-webkit-scrollbar-thumb {
+  background: rgba(255, 255, 255, 0.12);
+}
+:is(.dark) .conv-scroll::-webkit-scrollbar-thumb:hover {
+  background: rgba(255, 255, 255, 0.22);
+}
+</style>

+ 99 - 0
src/layout/ModalDocumentos.vue

@@ -0,0 +1,99 @@
+<script setup>
+import DocumentList from "../components/DocumentList.vue";
+import DocumentUpload from "../components/DocumentUpload.vue";
+import SearchBox from "../components/SearchBox.vue";
+import { useSearch } from "../composables/useSearch.js";
+import { usePainelDocumentos } from "../composables/usePainelDocumentos.js";
+import { useDocumentos } from "../composables/useDocumentos.js";
+
+const { painelAberto, tituloModal, fecharModal } = usePainelDocumentos();
+
+const {
+  results: searchResults,
+  loading: searchLoading,
+  error: searchError,
+  run: runSearch,
+} = useSearch();
+
+const { docs, loadDocs, onIngested } = useDocumentos();
+</script>
+
+<template>
+  <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" @click="fecharModal">
+          <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>
+</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>

+ 239 - 0
src/layout/SidebarSistema.vue

@@ -0,0 +1,239 @@
+<script setup>
+import { computed } from "vue";
+import { useRoute, useRouter } from "vue-router";
+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();
+
+const { user } = useAuth();
+const { activeConversationId, newConversation } = useChat();
+const { sidebarRecolhida, classeRotulo, classeItemSidebar, classeIcon } = useSidebar();
+const { painelAberto, alternarPainel } = usePainelDocumentos();
+
+const rotaAtiva = computed(() => String(route.name ?? ""));
+const isNivel1 = computed(() => String(user.value?.Nivel) === "1");
+
+function irPara(nome) {
+  router.push({ name: nome });
+}
+
+async function novaConversa() {
+  newConversation();
+  await router.push({ name: "home" });
+}
+</script>
+
+<template>
+  <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>
+
+        <button
+          type="button"
+          :class="classeItemSidebar(rotaAtiva === 'conversas-historico')"
+          :title="sidebarRecolhida ? 'Histórico' : ''"
+          @click="irPara('conversas-historico')"
+        >
+          <svg
+            :class="classeIcon(rotaAtiva === 'conversas-historico')"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="1.8"
+          >
+            <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6l4 2" />
+            <path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-3.2-6.9" />
+          </svg>
+          <span :class="classeRotulo">Histórico</span>
+        </button>
+
+        <button
+          type="button"
+          :class="classeItemSidebar(rotaAtiva === 'conversas-pesquisar')"
+          :title="sidebarRecolhida ? 'Pesquisar' : ''"
+          @click="irPara('conversas-pesquisar')"
+        >
+          <svg
+            :class="classeIcon(rotaAtiva === 'conversas-pesquisar')"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="1.8"
+          >
+            <path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
+          </svg>
+          <span :class="classeRotulo">Pesquisar</span>
+        </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">
+          <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>
+</template>