|
|
@@ -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>
|