diff --git a/bun.lockb b/bun.lockb
index 2563ba1..0bfe108 100644
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/design-system.md b/design-system.md
new file mode 100644
index 0000000..883f61d
--- /dev/null
+++ b/design-system.md
@@ -0,0 +1,385 @@
+# Design System — HGTX Commander
+
+> Referência técnica de tokens e padrões — light + dark.
+> **Fonte da verdade (código):** `src/index.css` + `tailwind.config.ts`.
+> Tema aplicado via `next-themes` (`class` no ``, `storageKey: hgtx-commander-theme`).
+
+## Cor de marca (azul HGTX)
+
+Azul institucional usado em botões primários, links, anéis de foco e destaques do Commander.
+
+| Papel | Light | Dark |
+|-------|-------|------|
+| **Primária** | `hsl(210 100% 33%)` · **#0054A8** | `hsl(210 90% 52%)` · **#1685F3** |
+| Hover | `hsl(210 100% 28%)` · #00478F | `hsl(210 95% 46%)` · #0675E5 |
+| Pressed | `hsl(210 100% 24%)` · #003D7A | `hsl(210 100% 40%)` · #0066CC |
+| Fundo sutil | `hsl(210 55% 96%)` · #EFF5FB | `hsl(210 50% 14%)` · #122033 |
+| Focus ring | `rgba(0, 85, 164, 0.28)` | `rgba(51, 136, 255, 0.35)` |
+
+Referência histórica de marca: ~`#0055A4` / `#004080` — no código o token `--primary` (light) resolve para **#0054A8**.
+
+**Tailwind / shadcn:** `bg-primary`, `text-primary`, `border-primary`, `ring-ring` (usa `--ring`, alinhado ao primário).
+
+---
+
+## Fonte
+
+Inter — Google Fonts
+
+Pesos: 400, 500, 600, 700
+
+Import: `https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap`
+
+Token: `--font-sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif`
+
+---
+
+## Cores
+
+Valores em **HSL sem vírgula** (formato shadcn: `hsl(var(--token))`). Hex entre parênteses para referência visual.
+
+### Primária e estados
+
+| Token CSS | Light (HSL · HEX) | Dark (HSL · HEX) | Classe Tailwind | Uso |
+|-----------|-------------------|------------------|-----------------|-----|
+| `--primary` | 210 100% 33% · #0054A8 | 210 90% 52% · #1685F3 | `bg-primary` `text-primary` | CTAs, enviar, links ativos |
+| `--primary-foreground` | 0 0% 100% · #FFFFFF | 0 0% 100% · #FFFFFF | `text-primary-foreground` | Texto sobre primário |
+| `--primary-hover` | 210 100% 28% · #00478F | 210 95% 46% · #0675E5 | `bg-primary-hover` | Hover botões primários |
+| `--primary-pressed` | 210 100% 24% · #003D7A | 210 100% 40% · #0066CC | `bg-primary-pressed` | Active / pressed |
+| `--primary-light` | 210 55% 96% · #EFF5FB | 210 50% 14% · #122033 | `bg-primary-light` | Fundo de destaque suave |
+| `--ring` | 210 100% 33% | 210 90% 52% | `ring-ring` | Focus visível (inputs) |
+| `--focus-ring` | rgba(0, 85, 164, 0.28) | rgba(51, 136, 255, 0.35) | sombra manual | Focus em componentes custom |
+
+### Semânticas (feedback)
+
+| Token CSS | Light (HSL · HEX) | Dark (HSL · HEX) | Classe Tailwind | Uso |
+|-----------|-------------------|------------------|-----------------|-----|
+| `--success` | 134 61% 41% · #29A847 | 141 71% 48% · #23D160 | `bg-success` `text-success` | Online, confirmação |
+| `--success-foreground` | 0 0% 100% | 216 48% 8% | `text-success-foreground` | Texto sobre success |
+| `--destructive` | 0 84% 60% | 0 84% 60% | `bg-destructive` | Erro, excluir |
+| `--destructive-foreground` | 0 0% 100% | 0 0% 100% | `text-destructive-foreground` | Texto sobre destructive |
+| `--warning` | 38 92% 50% | 38 92% 50% | `bg-warning` `text-warning` | Alertas, badge “Pessoal” |
+| `--warning-foreground` | 209 11% 15% | 216 48% 8% | `text-warning-foreground` | Texto sobre warning |
+
+### Superfícies (page, card, sidebar)
+
+| Token CSS | Light (HSL · HEX) | Dark (HSL · HEX) | Classe Tailwind | Uso |
+|-----------|-------------------|------------------|-----------------|-----|
+| `--background` | 0 0% 100% · #FFFFFF | 216 48% 8% · #0B121E | `bg-background` | Fundo principal / chat |
+| `--foreground` | 209 11% 15% · #22262A | 0 0% 100% · #FFFFFF | `text-foreground` | Texto principal |
+| `--card` | 0 0% 100% · #FFFFFF | 216 42% 11% · #101A28 | `bg-card` | Cards, modais, composer |
+| `--card-foreground` | 209 11% 15% | 0 0% 100% | `text-card-foreground` | Texto em cards |
+| `--popover` | 0 0% 100% | 216 42% 11% | `bg-popover` | Dropdowns, selects |
+| `--sidebar-background` | 210 17% 98% · #F8FAFB | 216 50% 9% · #0B1522 | `bg-sidebar` | Sidebar Commander / módulos |
+| `--sidebar-foreground` | 209 11% 15% | 0 0% 100% | `text-sidebar-foreground` | Texto na sidebar |
+| `--sidebar-border` | 220 13% 91% · #E5E7EB | 212 12% 21% · #2F363F | `border-sidebar-border` | Divisor da sidebar |
+| `--sidebar-accent` | 210 70% 94% · #E3EEFC | 210 45% 16% · #16283D | `bg-sidebar-accent` | Item de nav ativo / hover |
+| `--sidebar-accent-foreground` | 210 100% 33% | 210 85% 62% · #5BA3EB | `text-sidebar-accent-foreground` | Texto no item ativo |
+| `--muted` | 210 17% 96% · #F3F6F8 | 216 38% 13% · #15202E | `bg-muted` | Fundos secundários |
+| `--accent` | 210 70% 94% | 210 45% 16% | `bg-accent` | Hover suave, chips |
+| `--accent-foreground` | 210 100% 33% | 210 85% 62% | `text-accent-foreground` | Texto em accent |
+
+### Tokens extras (chat e UI)
+
+| Token CSS | Light (HSL · HEX) | Dark (HSL · HEX) | Uso |
+|-----------|-------------------|------------------|-----|
+| `--bubble-bot` | 209 36% 96% | 216 36% 14% | Bolha do assistente |
+| `--nav-active` | 210 70% 94% | 210 45% 16% | Nav ativo (alias visual do accent) |
+| `--input-bg` | 0 0% 100% | 216 40% 10% | Fundo de campos (referência) |
+| `--secondary` | 210 6% 60% | 216 38% 13% | Neutro secundário (evitar texto em badge dark) |
+| `--secondary-foreground` | 209 11% 15% | 0 0% 100% | Texto sobre secondary |
+
+### Bordas e inputs (shadcn)
+
+No shadcn, `--input` é a **cor da borda** de Input / Select / Textarea, não o fundo do campo.
+
+| Token CSS | Light (HSL · HEX) | Dark (HSL · HEX) | Classe Tailwind | Uso |
+|-----------|-------------------|------------------|-----------------|-----|
+| `--border` | 220 13% 91% · #E5E7EB | 216 18% 26% · #36404E | `border-border` | Divisores, cards |
+| `--input` | 220 9% 83% · #D0D2D8 | 216 16% 30% · #404A59 | `border-input` | Borda de inputs |
+| `--border-subtle` | 220 13% 95% | 216 22% 20% | custom | Bordas muito suaves |
+| `--btn-secondary-border` | 220 9% 83% | 216 16% 30% | custom | Outline secundário |
+
+### Texto secundário
+
+| Token CSS | Light (HSL · HEX) | Dark (HSL · HEX) | Classe Tailwind | Uso |
+|-----------|-------------------|------------------|-----------------|-----|
+| `--muted-foreground` | 209 7% 46% · #6D767E | 212 9% 58% · #8B949E | `text-muted-foreground` | Subtítulos, hints |
+| `--placeholder` | 220 9% 66% | 212 10% 31% | `text-placeholder` / `placeholder:` | Placeholders |
+
+### Gradientes e glow (efeitos)
+
+| Token | Light | Dark |
+|-------|-------|------|
+| `--gradient-cyber` | `135deg`, hsl(210 100% 33%) → hsl(210 90% 42%) | hsl(210 90% 52%) → hsl(210 100% 40%) |
+| `--gradient-subtle` | #FFF → hsl(210 17% 98%) | hsl(216 42% 11%) → hsl(216 48% 8%) |
+| `--glow-cyan` / `--glow-blue` | sombra azul suave 12–15% opac. | sombra azul 28–35% opac. |
+
+Classes utilitárias: `.gradient-text`, `.cyber-glow`, `.glass-effect` em `src/index.css`.
+
+---
+
+## Tipografia
+
+> Mesma escala em light e dark.
+
+| Nível | Tamanho | Peso | Line Height | Classe Semântica |
+|-------|---------|------|-------------|------------------|
+| Display / Commander | 32px (`--fs-display`) | 700 | 1.25 (`--lh-tight`) | text-display |
+| Heading 1 — Modal | 24px (`--fs-h1`) | 600 | 1.25 | text-h1 |
+| Heading 2 — Seção | 18px (`--fs-h2`) | 600 | 1.5 (`--lh-body`) | text-h2 |
+| Heading 3 | 15px (`--fs-h3`) | — | — | text-h3 |
+| Body | 14px (`--fs-body`) | 400 | 1.5 | text-body |
+| Small / Timestamp | 12px (`--fs-small`) | 400 | — | text-small |
+| Caption | 11px (`--fs-caption`) | 500 | — | text-caption |
+| Nav link | 13px | 500 (`--fw-medium`) | — | text-nav-link |
+| Nav brand | 15px | 700 (`--fw-bold`) | — | text-nav-brand |
+| Brand lockup (tagline) | 10px | 600 | — | text-brand-tag |
+| Brand lockup (nome) | 16px | 700 | — | text-brand-name |
+| Hero título (catálogo) | clamp(28px, 4vw, 42px) | 700 | 1.25 | text-hero |
+| Section title | 28px | 700 | — | text-section-title |
+
+Tokens de peso: `--fw-regular` 400, `--fw-medium` 500, `--fw-semibold` 600, `--fw-bold` 700.
+Tokens de line-height: `--lh-tight` 1.25, `--lh-body` 1.5, `--lh-relaxed` 1.65.
+
+---
+
+## Espaçamento
+
+Base unit: **4px**
+
+| Token | Valor | Classe Semântica |
+|-------|-------|------------------|
+| --space-1 | 4px | p-1 / m-1 / gap-1 |
+| --space-2 | 8px | p-2 / m-2 / gap-2 |
+| --space-3 | 12px | p-3 / m-3 / gap-3 |
+| --space-4 | 16px | p-4 / m-4 / gap-4 |
+| --space-5 | 20px | p-5 / m-5 / gap-5 |
+| --space-6 | 24px | p-6 / m-6 / gap-6 |
+| --space-7 | 32px | p-7 / m-7 / gap-7 |
+| --space-8 | 48px | p-8 / m-8 / gap-8 |
+| --space-9 | 64px | p-9 / m-9 / gap-9 |
+| --section-y | 64px | py-section |
+| --section-x | 40px | px-section |
+| --nav-h | 56px | h-nav |
+| --container | 1200px | max-w-container |
+
+---
+
+## Border Radius
+
+| Token | Valor | Classe Semântica | Uso |
+|-------|-------|------------------|-----|
+| --radius-sm | 6px | rounded-sm | Chat items, detalhes |
+| --radius-md | 8px | rounded-md | Botões, inputs, nav items, badges |
+| --radius-lg | 12px | rounded-lg | Cards, bolhas, demo wrap |
+| --radius-xl | 16px | rounded-xl | Modal Personalidade |
+| --radius-pill | 999px | rounded-pill | Chat bar (composer) |
+| 50% | — | rounded-full | Botão send circular |
+
+---
+
+## Sombras
+
+| Token CSS | Light | Dark | Classe Tailwind | Uso |
+|-----------|-------|------|-----------------|-----|
+| `--shadow-card` | `0 4px 12px rgba(0,0,0,0.06)` | `0 4px 16px rgba(0,0,0,0.35)` | `shadow-card` | Cards elevados |
+| `--shadow-modal` | `0 16px 40px rgba(0,0,0,0.12)` | `0 24px 48px rgba(0,0,0,0.55)` | `shadow-modal` | Modais, dialogs |
+
+Glow Online (badge): `box-shadow: 0 0 8px hsl(var(--success) / 0.5)`.
+
+---
+
+## Componentes
+
+> Use referências a tokens — valores resolvem para light/dark conforme o tema ativo.
+
+### Botão Primary (HGTX — Nova Sessão / Salvar Perfil)
+```
+bg-primary hover:bg-primary-hover active:bg-primary-pressed
+text-white rounded-md px-[18px] py-[10px]
+text-[13px] font-semibold inline-flex items-center gap-2
+border-none cursor-pointer transition-[background] duration-200
+disabled:opacity-45 disabled:cursor-not-allowed
+```
+
+### Botão Primary (catálogo / hero)
+```
+bg-primary hover:bg-primary-hover active:bg-primary-pressed active:translate-y-px
+text-white rounded-md px-6 py-3 text-sm font-semibold
+border-none cursor-pointer transition-all duration-200
+```
+
+### Botão Outline (Nova Pasta)
+```
+bg-transparent text-primary border border-btn-secondary
+rounded-md px-[18px] py-[10px] text-[13px] font-medium
+inline-flex items-center gap-2 cursor-pointer
+hover:border-secondary transition-[border-color] duration-200
+```
+
+### Botão Secondary (catálogo)
+```
+bg-transparent text-primary border border-btn-secondary
+rounded-md px-6 py-3 text-sm font-medium
+hover:border-secondary hover:bg-black/[0.03] (light) / hover:bg-white/[0.04] (dark)
+transition-all duration-200 cursor-pointer
+```
+
+### Botão Ghost (Cancelar)
+```
+bg-transparent text-secondary border-none rounded-md px-[14px] py-2
+text-[13px] cursor-pointer
+hover:text-primary hover:bg-black/[0.04] (light) / hover:bg-white/[0.05] (dark)
+transition-all duration-200
+```
+
+### Botão Send (circular)
+```
+w-10 h-10 rounded-full bg-primary text-white border-none
+flex items-center justify-center text-base cursor-pointer
+```
+
+### Nav Sticky (catálogo DS)
+```
+sticky top-0 z-[100] h-nav flex items-center justify-between px-section
+bg-nav backdrop-blur-[12px] border-b border-divider
+```
+
+### Nav CTA
+```
+bg-primary hover:bg-primary-hover text-white rounded-md
+px-[18px] py-2 text-[13px] font-semibold border-none cursor-pointer
+```
+
+### Nav Item (sidebar)
+```
+flex items-center gap-3 px-3 py-2 rounded-md text-[13px] text-secondary
+```
+
+### Nav Item Ativo
+```
+bg-nav-active text-primary
+```
+
+### Nav Chat Item (lista conversas)
+```
+flex justify-between items-center px-3 py-2 text-xs text-secondary rounded-sm
+/* timestamp */ text-[11px] text-placeholder
+```
+
+### Input / Select / Textarea
+```
+font-sans text-sm px-[14px] py-[10px] rounded-md
+border border-divider bg-input text-primary outline-none w-full
+placeholder:text-placeholder
+transition-[border-color,box-shadow] duration-200
+focus:border-primary focus:shadow-[0_0_0_3px_var(--color-focus-ring)]
+disabled:opacity-50 disabled:cursor-not-allowed
+```
+
+### Textarea (modal)
+```
+[classes input] min-h-[120px] resize-y
+```
+
+### Input Search (com ícone)
+```
+relative /* wrap */
+[input] pl-9
+/* ícone */ absolute left-3 top-1/2 -translate-y-1/2 text-placeholder text-sm
+```
+
+### Chat Composer Bar
+```
+flex items-center gap-3 py-2 pr-2 pl-5
+border border-divider rounded-pill bg-input max-w-[560px]
+/* input interno */ flex-1 border-none bg-transparent text-sm text-primary outline-none placeholder:text-placeholder
+/* + btn-send */
+```
+
+### Badge Online
+```
+inline-flex items-center gap-2 text-small font-medium text-secondary
+/* dot */ w-2 h-2 rounded-full bg-success shadow-[0_0_8px_rgba(35,209,96,0.5)]
+```
+
+### Badge Self-Hosted
+```
+inline-flex items-center gap-2 text-caption text-placeholder
+px-3 py-2 border border-divider rounded-md
+```
+
+### Card Modal (Personalidade)
+```
+max-w-[480px] p-6 bg-surface-card border border-divider rounded-xl shadow-modal
+```
+
+### Card Modal Header
+```
+flex justify-between items-start mb-4
+/* título */ text-h2 font-semibold
+```
+
+### Card Modal Close
+```
+w-8 h-8 border-none rounded-md bg-transparent text-secondary text-lg cursor-pointer
+hover:text-primary hover:bg-black/[0.05] (light) / hover:bg-white/[0.06] (dark)
+```
+
+### Card Modal Actions
+```
+flex justify-end gap-3 mt-5
+/* Cancelar: btn-hgtx-ghost | Salvar: btn-hgtx-primary */
+```
+
+### Bolha Bot (boas-vindas)
+```
+max-w-[420px] px-5 py-4 bg-bubble-bot rounded-lg
+text-sm text-secondary leading-relaxed
+```
+
+### Layout Demo (3 colunas)
+```
+grid grid-cols-[200px_280px_1fr] border border-divider rounded-lg overflow-hidden min-h-[320px]
+/* sidebars */ bg-sidebar p-4 border-r border-divider
+/* main */ bg-page p-5 flex flex-col items-center justify-center
+```
+
+### Brand Lockup (sidebar)
+```
+text-[10px] font-bold uppercase tracking-[0.12em] text-secondary mb-4
+/* nome */ block text-sm text-primary tracking-[0.06em]
+```
+
+### Folder Label (sidebar)
+```
+text-xs font-medium text-secondary py-2 mt-3
+```
+
+---
+
+## Tema — como aplicar
+
+1. **Tokens light** em `:root` e **tokens dark** em `.dark` — ver `src/index.css`.
+2. **App:** `ThemeProvider` (`next-themes`) com `attribute="class"`, `storageKey="hgtx-commander-theme"`, temas `light` | `dark`.
+3. **Tailwind:** cores semânticas em `tailwind.config.ts` → `hsl(var(--primary))`, etc.
+
+Tokens **compartilhados** (não mudam com o tema): `--fs-*`, `--space-*`, `--radius-*`, `--fw-*`, `--lh-*`, `--section-*`, `--nav-h`, `--container`.
+
+Texto sobre botões primários: sempre `--primary-foreground` (`#FFFFFF`).
+
+### Mapeamento rápido (legado → atual)
+
+| Doc / HTML antigo | Token atual |
+|-------------------|-------------|
+| `#0061ff` / `#1a73e8` | `--primary` → #0054A8 (light) / #1685F3 (dark) |
+| `bg-page` | `bg-background` |
+| `bg-sidebar` | `bg-sidebar` (`--sidebar-background`) |
+| `text-secondary` (corpo) | `text-muted-foreground` |
+| `border-divider` | `border-border` |
+
+---
+
+*Atualizado para refletir `src/index.css` (HGTX Commander redesign).*
+*Para uso como referência no Cursor e agentes de código.*
diff --git a/index.html b/index.html
index 1513c1c..5e1cd1a 100644
--- a/index.html
+++ b/index.html
@@ -3,12 +3,12 @@
- HGTX Codex - AI Interface System
-
-
+ HGTX Commander
+
+
-
-
+
+
diff --git a/package.json b/package.json
index 2a3d56c..13638bb 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
"input-otp": "^1.4.2",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.462.0",
- "motion": "^12.36.0",
+ "motion": "^12.38.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
diff --git a/public/hgtx-commander-dark.png b/public/hgtx-commander-dark.png
new file mode 100644
index 0000000..557a2a9
Binary files /dev/null and b/public/hgtx-commander-dark.png differ
diff --git a/public/hgtx-commander-light.png b/public/hgtx-commander-light.png
new file mode 100644
index 0000000..f3642f3
Binary files /dev/null and b/public/hgtx-commander-light.png differ
diff --git a/src/App.tsx b/src/App.tsx
index 8a2211b..a44b80d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,29 +2,14 @@ import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { BrowserRouter, Routes, Route, Navigate, useLocation } from "react-router-dom";
+import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { ThemeProvider } from "next-themes";
-import Index from "./pages/Index";
import NotFound from "./pages/NotFound";
import Redirect from "./pages/Redirect";
import React from "react";
+import CommanderApp from "./modules/commander/App";
import IntelligenceIAApp from "./modules/intelligence-ia/App";
-import FechamentoHgtxApp from "./modules/fechamento-hgtx/App";
-
-const INTELLIGENCE_SCORE_PREFIX = "/intelligence-score";
-const FECHAMENTO_LEGACY_PREFIX = "/fechamento-hgtx";
-const FECHAMENTO_PREFIX = "/fechamento";
-
-/** Redireciona URLs antigas para `/intelligence-score/...` (mesmo sufixo, query e hash). */
-function FechamentoLegacyRedirect() {
- const location = useLocation();
- const fromPrefix = location.pathname.startsWith(FECHAMENTO_LEGACY_PREFIX)
- ? FECHAMENTO_LEGACY_PREFIX
- : FECHAMENTO_PREFIX;
- const tail = location.pathname.slice(fromPrefix.length);
- const to = `${INTELLIGENCE_SCORE_PREFIX}${tail}`;
- return ;
-}
+import IntelligenceScoreApp from "./modules/intelligence-score/App";
const queryClient = new QueryClient();
@@ -35,7 +20,7 @@ const App = () => (
defaultTheme="light"
themes={["light", "dark"]}
enableSystem={false}
- storageKey="hgtx-codex-theme"
+ storageKey="hgtx-commander-theme"
enableColorScheme
>
@@ -43,20 +28,15 @@ const App = () => (
- }
- />
+ } />
+ } />
} />
- } />
- } />
- } />
+ } />
- } />
-
- } />
+ } />
} />
+ } />
@@ -66,12 +46,10 @@ const App = () => (
export default App;
-
-const HGTXLogin = () =>{
-
+const HGTXLogin = () => {
React.useEffect(() => {
window.location.replace(import.meta.env.VITE_BASE_URL_HGTX_CORE);
});
- return(<>>);
-}
\ No newline at end of file
+ return <>>;
+};
diff --git a/src/components/ChatHeader.tsx b/src/components/ChatHeader.tsx
index e17a750..60321ef 100644
--- a/src/components/ChatHeader.tsx
+++ b/src/components/ChatHeader.tsx
@@ -69,7 +69,7 @@ export const ChatHeader = ({
@@ -155,7 +155,7 @@ export const ChatHeader = ({
- {showModelSelector ? "Modelo de linguagem avançado" : "Sistema HGTX Codex"}
+ {showModelSelector ? "Modelo de linguagem avançado" : "HGTX Commander"}
);
diff --git a/src/components/auth/NoAccessScreen.tsx b/src/components/auth/NoAccessScreen.tsx
index 3c0c4d2..f34df09 100644
--- a/src/components/auth/NoAccessScreen.tsx
+++ b/src/components/auth/NoAccessScreen.tsx
@@ -95,7 +95,7 @@ export function NoAccessScreen({ reason, errorMessage }: NoAccessScreenProps) {
Preenchimento automático.
{!estabelecimentoId ? (
- Não foi possível obter o código do estabelecimento. Abra o Fechamento HGTX a partir do Codex com o
+ Não foi possível obter o código do estabelecimento. Abra o Intelligence Score a partir do Commander com o
estabelecimento carregado no transfer.
) : null}
diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx
index 50802ac..ff3bf45 100644
--- a/src/components/chat/ChatInput.tsx
+++ b/src/components/chat/ChatInput.tsx
@@ -4,22 +4,43 @@ import { Textarea } from "@/components/ui/textarea";
import { useState, useRef, useEffect } from "react";
import { useToast } from "@/hooks/use-toast";
import { chatService } from "@/services/chat";
+import { cn } from "@/lib/utils";
interface ChatInputProps {
onSendMessage: (message: string, files?: File[]) => void;
+ draftMessage?: string | null;
+ draftTrigger?: number;
+ onDraftApplied?: () => void;
}
-export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
+const MOBILE_TEXTAREA_HEIGHT = 88;
+const DEFAULT_TEXTAREA_HEIGHT = 72;
+
+export const ChatInput = ({
+ onSendMessage,
+ draftMessage,
+ draftTrigger,
+ onDraftApplied,
+}: ChatInputProps) => {
const [message, setMessage] = useState("");
const [attachedFiles, setAttachedFiles] = useState([]);
- const [textareaHeight, setTextareaHeight] = useState(60);
+ const [textareaHeight, setTextareaHeight] = useState(DEFAULT_TEXTAREA_HEIGHT);
const [isDragging, setIsDragging] = useState(false);
+ const [isMobile, setIsMobile] = useState(false);
const fileInputRef = useRef(null);
const textareaRef = useRef(null);
const dragStartY = useRef(0);
const dragStartHeight = useRef(0);
const { toast } = useToast();
+ useEffect(() => {
+ const mq = window.matchMedia("(max-width: 767px)");
+ const update = () => setIsMobile(mq.matches);
+ update();
+ mq.addEventListener("change", update);
+ return () => mq.removeEventListener("change", update);
+ }, []);
+
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
@@ -30,9 +51,9 @@ export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
-
- const deltaY = dragStartY.current - e.clientY; // Inverted: moving up = positive
- const newHeight = Math.min(400, Math.max(60, dragStartHeight.current + deltaY));
+
+ const deltaY = dragStartY.current - e.clientY;
+ const newHeight = Math.min(400, Math.max(DEFAULT_TEXTAREA_HEIGHT, dragStartHeight.current + deltaY));
setTextareaHeight(newHeight);
};
@@ -41,16 +62,30 @@ export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
};
if (isDragging) {
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
+ document.addEventListener("mousemove", handleMouseMove);
+ document.addEventListener("mouseup", handleMouseUp);
}
return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
+ document.removeEventListener("mousemove", handleMouseMove);
+ document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, textareaHeight]);
+ useEffect(() => {
+ if (!draftMessage || draftTrigger == null) return;
+
+ setMessage(draftMessage);
+ requestAnimationFrame(() => {
+ const textarea = textareaRef.current;
+ if (!textarea) return;
+ textarea.focus();
+ const end = draftMessage.length;
+ textarea.setSelectionRange(end, end);
+ });
+ onDraftApplied?.();
+ }, [draftMessage, draftTrigger, onDraftApplied]);
+
const handleSend = () => {
if (message.trim() || attachedFiles.length > 0) {
onSendMessage(message, attachedFiles);
@@ -70,7 +105,6 @@ export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
const files = Array.from(e.target.files || []);
const newFiles = [...attachedFiles, ...files];
- // Valida quantidade máxima de anexos (5)
if (newFiles.length > chatService.getMaxAttachments()) {
toast({
title: "Limite de anexos excedido",
@@ -83,10 +117,8 @@ export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
return;
}
- // Valida tipos de arquivo e tamanho
const validFiles: File[] = [];
for (const file of files) {
- // Valida tamanho (max 10MB por arquivo)
if (file.size > 10 * 1024 * 1024) {
toast({
title: "Arquivo muito grande",
@@ -99,7 +131,6 @@ export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
validFiles.push(file);
}
- // Valida formatos permitidos
const allFiles = [...attachedFiles, ...validFiles];
const validation = chatService.validateAttachments(allFiles);
@@ -125,96 +156,108 @@ export const ChatInput = ({ onSendMessage }: ChatInputProps) => {
setAttachedFiles(attachedFiles.filter((_, i) => i !== index));
};
+ const canSend = Boolean(message.trim() || attachedFiles.length > 0);
+ const textareaStyleHeight = isMobile ? MOBILE_TEXTAREA_HEIGHT : textareaHeight;
+
return (
-
-
- {/* Attached Files Preview */}
+
+
{attachedFiles.length > 0 && (
-
+
{attachedFiles.map((file, index) => (
-
-
{file.name}
-
+
+ {file.name}
+
({(file.size / 1024).toFixed(1)} KB)
removeFile(index)}
+ aria-label={`Remover ${file.name}`}
>
-
+
))}
)}
- {/* Message Input and Buttons */}
-
-
- {/* Resize Handle - Hidden on mobile */}
-
-
-
-
- {/* Textarea */}
-
-
+
+
+
- {/* Action Buttons */}
-
-
+
+
+
+
+
+
fileInputRef.current?.click()}
+ title="Anexar arquivo"
+ aria-label="Anexar arquivo"
+ >
+
+
+ {attachedFiles.length > 0 && (
+
+ {attachedFiles.length} anexo{attachedFiles.length > 1 ? "s" : ""}
+
+ )}
+
+
fileInputRef.current?.click()}
- title="Anexar arquivo"
- >
-
-
-
-
+
-
- Pressione Enter para enviar, Shift + Enter para nova linha • Máx. {chatService.getMaxAttachments()} anexos • Formatos: {chatService.getAllowedFileTypesLabel()}
+
+ Pressione Enter para enviar, Shift + Enter para nova linha • Máx. {chatService.getMaxAttachments()}{" "}
+ anexos • Formatos: {chatService.getAllowedFileTypesLabel()}
diff --git a/src/components/chat/ChatSidebar.tsx b/src/components/chat/ChatSidebar.tsx
index 781822e..9c40ccd 100644
--- a/src/components/chat/ChatSidebar.tsx
+++ b/src/components/chat/ChatSidebar.tsx
@@ -1,5 +1,18 @@
import { useState, useEffect } from "react";
-import { Search, Folder, FolderPlus, MessageSquare, MoreVertical, Trash2, Edit, FolderInput, ChevronLeft, ChevronRight, Plus } from "lucide-react";
+import {
+ Search,
+ Folder,
+ FolderPlus,
+ MessageSquare,
+ MoreVertical,
+ Trash2,
+ Edit,
+ ChevronLeft,
+ ChevronRight,
+ ChevronDown,
+ Plus,
+ Inbox,
+} from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
@@ -11,6 +24,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
@@ -33,6 +47,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
+import { cn } from "@/lib/utils";
interface ChatSidebarProps {
isCollapsed: boolean;
@@ -42,7 +57,19 @@ interface ChatSidebarProps {
currentChatId?: string;
}
-export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelectChat, currentChatId }: ChatSidebarProps) => {
+const SectionLabel = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+);
+
+export const ChatSidebar = ({
+ isCollapsed,
+ onToggleCollapse,
+ onNewChat,
+ onSelectChat,
+ currentChatId,
+}: ChatSidebarProps) => {
const { toast } = useToast();
const [searchQuery, setSearchQuery] = useState("");
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
@@ -56,49 +83,36 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect
const [deletingChat, setDeletingChat] = useState
(null);
const [isLoading, setIsLoading] = useState(false);
- // Estado carregado do banco de dados via chatService
const [chats, setChats] = useState([]);
const [folders, setFolders] = useState([]);
+ /** Pastas fechadas por padrão; estado preservado ao recarregar lista */
const [expandedFolders, setExpandedFolders] = useState>(new Set());
- // Carrega chats e pastas do banco de dados quando o componente monta
useEffect(() => {
loadChatsAndFolders();
- // Escuta evento customizado para recarregar quando chat é atualizado
const handleChatUpdate = () => {
loadChatsAndFolders();
};
- window.addEventListener('chatUpdated', handleChatUpdate);
-
- // Cleanup
- return () => {
- window.removeEventListener('chatUpdated', handleChatUpdate);
- };
+ window.addEventListener("chatUpdated", handleChatUpdate);
+ return () => window.removeEventListener("chatUpdated", handleChatUpdate);
}, []);
const loadChatsAndFolders = async () => {
setIsLoading(true);
try {
const data = await chatService.getChatsAndFolders();
-
- console.log('Dados carregados:', data);
-
setChats(data.chats);
setFolders(data.folders);
-
- // Expande todas as pastas por padrão
- setExpandedFolders(new Set(data.folders.map(f => f.id)));
- } catch (error: any) {
- console.error('Erro ao carregar chats e pastas:', error);
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "Não foi possível carregar chats e pastas.";
+ console.error("Erro ao carregar chats e pastas:", error);
toast({
title: "Erro ao carregar dados",
- description: error.message || "Não foi possível carregar chats e pastas.",
+ description: message,
variant: "destructive",
});
-
- // Define arrays vazios em caso de erro
setChats([]);
setFolders([]);
} finally {
@@ -107,48 +121,36 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect
};
const handleCreateFolder = async () => {
- if (newFolderName.trim()) {
- try {
- await chatService.createFolder(newFolderName);
- await loadChatsAndFolders();
- setNewFolderName("");
- setIsCreateFolderOpen(false);
-
- toast({
- title: "Pasta criada",
- description: `Pasta "${newFolderName}" criada com sucesso.`,
- });
- } catch (error: any) {
- console.error('Erro ao criar pasta:', error);
- toast({
- title: "Erro ao criar pasta",
- description: error.message || "Não foi possível criar a pasta. Tente novamente.",
- variant: "destructive",
- });
- }
+ if (!newFolderName.trim()) return;
+ const name = newFolderName.trim();
+ try {
+ await chatService.createFolder(name);
+ await loadChatsAndFolders();
+ setNewFolderName("");
+ setIsCreateFolderOpen(false);
+ toast({ title: "Pasta criada", description: `Pasta "${name}" criada com sucesso.` });
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "Não foi possível criar a pasta.";
+ toast({ title: "Erro ao criar pasta", description: message, variant: "destructive" });
}
};
const handleDeleteFolder = async () => {
- if (deletingFolder) {
- try {
- await chatService.deleteFolder(deletingFolder.id);
- await loadChatsAndFolders();
- setDeletingFolder(null);
- setIsDeleteFolderOpen(false);
-
- toast({
- title: "Pasta excluída",
- description: "A pasta foi excluída com sucesso.",
- });
- } catch (error: any) {
- console.error('Erro ao excluir pasta:', error);
- toast({
- title: "Erro ao excluir pasta",
- description: error.message || "Não foi possível excluir a pasta. Tente novamente.",
- variant: "destructive",
- });
- }
+ if (!deletingFolder) return;
+ try {
+ await chatService.deleteFolder(deletingFolder.id);
+ setExpandedFolders((prev) => {
+ const next = new Set(prev);
+ next.delete(deletingFolder.id);
+ return next;
+ });
+ await loadChatsAndFolders();
+ setDeletingFolder(null);
+ setIsDeleteFolderOpen(false);
+ toast({ title: "Pasta excluída", description: "A pasta foi excluída com sucesso." });
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "Não foi possível excluir a pasta.";
+ toast({ title: "Erro ao excluir pasta", description: message, variant: "destructive" });
}
};
@@ -164,79 +166,52 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect
};
const handleRenameFolder = async () => {
- if (renamingFolder && renamedFolderName.trim()) {
- try {
- await chatService.renameFolder(renamingFolder.id, renamedFolderName);
- await loadChatsAndFolders();
- setRenamingFolder(null);
- setRenamedFolderName("");
- setIsRenameFolderOpen(false);
-
- toast({
- title: "Pasta renomeada",
- description: `Pasta renomeada para "${renamedFolderName}" com sucesso.`,
- });
- } catch (error: any) {
- console.error('Erro ao renomear pasta:', error);
- toast({
- title: "Erro ao renomear pasta",
- description: error.message || "Não foi possível renomear a pasta. Tente novamente.",
- variant: "destructive",
- });
- }
+ if (!renamingFolder || !renamedFolderName.trim()) return;
+ try {
+ await chatService.renameFolder(renamingFolder.id, renamedFolderName);
+ await loadChatsAndFolders();
+ setRenamingFolder(null);
+ setRenamedFolderName("");
+ setIsRenameFolderOpen(false);
+ toast({ title: "Pasta renomeada", description: `Pasta renomeada para "${renamedFolderName}".` });
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "Não foi possível renomear a pasta.";
+ toast({ title: "Erro ao renomear pasta", description: message, variant: "destructive" });
}
};
const toggleFolder = (folderId: string) => {
- const newExpanded = new Set(expandedFolders);
- if (newExpanded.has(folderId)) {
- newExpanded.delete(folderId);
- } else {
- newExpanded.add(folderId);
- }
- setExpandedFolders(newExpanded);
+ setExpandedFolders((prev) => {
+ const next = new Set(prev);
+ if (next.has(folderId)) next.delete(folderId);
+ else next.add(folderId);
+ return next;
+ });
};
const moveToFolder = async (chatId: string, folderId: string) => {
try {
await chatService.moveChatToFolder(chatId, folderId);
await loadChatsAndFolders();
-
- const folder = folders.find(f => f.id === folderId);
- toast({
- title: "Chat movido",
- description: `Movido para a pasta "${folder?.name}".`,
- });
- } catch (error: any) {
- console.error('Erro ao mover chat:', error);
- toast({
- title: "Erro ao mover chat",
- description: error.message || "Não foi possível mover o chat. Tente novamente.",
- variant: "destructive",
- });
+ const folder = folders.find((f) => f.id === folderId);
+ toast({ title: "Chat movido", description: `Movido para "${folder?.name}".` });
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "Não foi possível mover o chat.";
+ toast({ title: "Erro ao mover chat", description: message, variant: "destructive" });
}
};
const handleDeleteChat = async () => {
- if (deletingChat) {
- try {
- await chatService.deleteChat(deletingChat.id);
- await loadChatsAndFolders();
- setDeletingChat(null);
- setIsDeleteChatOpen(false);
-
- toast({
- title: "Chat excluído",
- description: "A conversa foi excluída com sucesso.",
- });
- } catch (error: any) {
- console.error('Erro ao excluir chat:', error);
- toast({
- title: "Erro ao excluir chat",
- description: error.message || "Não foi possível excluir o chat. Tente novamente.",
- variant: "destructive",
- });
- }
+ if (!deletingChat) return;
+ try {
+ await chatService.deleteChat(deletingChat.id);
+ await loadChatsAndFolders();
+ setDeletingChat(null);
+ setIsDeleteChatOpen(false);
+ toast({ title: "Chat excluído", description: "A conversa foi excluída com sucesso." });
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "Não foi possível excluir o chat.";
+ toast({ title: "Erro ao excluir chat", description: message, variant: "destructive" });
}
};
@@ -246,287 +221,318 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect
};
const handleSelectChat = (chat: ChatRecord) => {
- if (onSelectChat) {
- onSelectChat(chat);
- }
+ onSelectChat?.(chat);
};
- // Filtrar chats pela busca
- const filteredChats = chats.filter((chat) => {
- const searchLower = searchQuery.toLowerCase();
- const titleMatch = chat.title.toLowerCase().includes(searchLower);
- return titleMatch;
- });
+ const filteredChats = chats.filter((chat) =>
+ chat.title.toLowerCase().includes(searchQuery.toLowerCase())
+ );
- // Separar chats sem pasta (folder_id é null)
const chatsWithoutFolder = filteredChats.filter((c) => c.folder_id === null);
- // Agrupar chats por pasta (quando folder_id === folder.id)
- const chatsByFolder = folders.reduce((acc, folder) => {
- acc[folder.id] = filteredChats.filter((c) => c.folder_id === folder.id);
- return acc;
- }, {} as Record);
+ const chatsByFolder = folders.reduce(
+ (acc, folder) => {
+ acc[folder.id] = filteredChats.filter((c) => c.folder_id === folder.id);
+ return acc;
+ },
+ {} as Record
+ );
+
+ const visibleFolders = folders.filter(
+ (folder) =>
+ (chatsByFolder[folder.id]?.length ?? 0) > 0 ||
+ !searchQuery.trim() ||
+ folder.name.toLowerCase().includes(searchQuery.toLowerCase())
+ );
if (isCollapsed) {
return (
-
+
-
+
);
}
return (
-
- {/* Header with collapse button */}
-
+
+ {/* Cabeçalho */}
+
Conversas
-
+
- {/* Search */}
-
+ {/* Busca + ações */}
+
-
+
setSearchQuery(e.target.value)}
placeholder="Pesquisar conversas..."
- className="pl-9 bg-muted/50"
+ className="h-9 bg-background/80 pl-9 text-sm"
/>
-
-
- Novo Chat
-
-
-
-
-
-
- Nova Pasta
-
-
-
-
- Criar Nova Pasta
-
- Organize suas conversas em pastas personalizadas.
-
-
-
-
-
Nome da Pasta
+
+
+
+ Novo chat
+
+
+
+
+
+ Pasta
+
+
+
+
+ Nova pasta
+ Organize suas conversas em pastas.
+
+
+ Nome
setNewFolderName(e.target.value)}
- placeholder="Ex: Projetos, Estudos..."
+ placeholder="Ex: Projetos, Clientes..."
onKeyDown={(e) => e.key === "Enter" && handleCreateFolder()}
/>
-
-
- setIsCreateFolderOpen(false)}>
- Cancelar
-
-
- Criar Pasta
-
-
-
-
-
- {/* Rename Folder Dialog */}
-
-
-
- Renomear Pasta
-
- Digite o novo nome para a pasta "{renamingFolder?.name}".
-
-
-
-
- Novo Nome
- setRenamedFolderName(e.target.value)}
- placeholder="Ex: Projetos, Estudos..."
- onKeyDown={(e) => e.key === "Enter" && handleRenameFolder()}
- autoFocus
- />
-
-
-
- setIsRenameFolderOpen(false)}>
- Cancelar
-
-
- Renomear
-
-
-
-
-
- {/* Delete Folder Alert */}
-
-
-
- Excluir Pasta?
-
- Esta ação não pode ser desfeita. A pasta "{deletingFolder?.name}" será excluída.
-
-
-
- Cancelar
-
- Excluir
-
-
-
-
+
+ setIsCreateFolderOpen(false)}>
+ Cancelar
+
+
+ Criar
+
+
+
+
+
- {/* Conversations List */}
-
-
+
+
+
+ Renomear pasta
+
+ Novo nome para "{renamingFolder?.name}".
+
+
+
+ Nome
+ setRenamedFolderName(e.target.value)}
+ onKeyDown={(e) => e.key === "Enter" && handleRenameFolder()}
+ autoFocus
+ />
+
+
+ setIsRenameFolderOpen(false)}>
+ Cancelar
+
+
+ Salvar
+
+
+
+
+
+
+
+
+ Excluir pasta?
+
+ A pasta "{deletingFolder?.name}" será excluída. As conversas dentro dela
+ ficarão sem pasta.
+
+
+
+ Cancelar
+
+ Excluir
+
+
+
+
+
+ {/* Lista */}
+
+
{isLoading ? (
-
-
+
) : (
<>
- {/* Folders */}
- {folders.map((folder) => (
-
-
-
toggleFolder(folder.id)}
- className="flex-1 flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-muted/50 transition-colors group"
- >
-
-
-
-
- {folder.name}
-
-
- {chatsByFolder[folder.id]?.length || 0}
-
-
+ {visibleFolders.length > 0 && (
+
+ {visibleFolders.map((folder) => {
+ const folderChats = chatsByFolder[folder.id] ?? [];
+ const isExpanded = expandedFolders.has(folder.id);
-
-
-
-
-
-
-
- openRenameFolder(folder)}
- >
-
- Renomear Pasta
-
- openDeleteFolder(folder)}
- >
-
- Excluir Pasta
-
-
-
-
+ return (
+
+
+ toggleFolder(folder.id)}
+ className="flex min-w-0 flex-1 items-center gap-1.5 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-sidebar-accent/50"
+ aria-expanded={isExpanded}
+ >
+
+
+
+ {folder.name}
+
+
+ {folderChats.length}
+
+
+
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ openRenameFolder(folder)}>
+
+ Renomear
+
+ openDeleteFolder(folder)}
+ >
+
+ Excluir pasta
+
+
+
+
- {expandedFolders.has(folder.id) && chatsByFolder[folder.id]?.length > 0 && (
-
- {chatsByFolder[folder.id].map((chat) => (
- handleSelectChat(chat)}
- folders={folders}
- onMoveToFolder={moveToFolder}
- onDelete={() => openDeleteChat(chat)}
- />
- ))}
-
- )}
-
- ))}
+ {isExpanded && (
+
+ {folderChats.length > 0 ? (
+ folderChats.map((chat) => (
+
handleSelectChat(chat)}
+ folders={folders}
+ onMoveToFolder={moveToFolder}
+ onDelete={() => openDeleteChat(chat)}
+ nested
+ />
+ ))
+ ) : (
+ Pasta vazia
+ )}
+
+ )}
+
+ );
+ })}
+
+ )}
- {/* Chats without folder */}
{chatsWithoutFolder.length > 0 && (
-
-
- Sem Pasta
+
0 && "mt-3 border-t border-border/40 pt-3"
+ )}
+ >
+ Conversas
+
+ {chatsWithoutFolder.map((chat) => (
+ handleSelectChat(chat)}
+ folders={folders}
+ onMoveToFolder={moveToFolder}
+ onDelete={() => openDeleteChat(chat)}
+ />
+ ))}
- {chatsWithoutFolder.map((chat) => (
- handleSelectChat(chat)}
- folders={folders}
- onMoveToFolder={moveToFolder}
- onDelete={() => openDeleteChat(chat)}
- />
- ))}
+
+ )}
+
+ {!isLoading && chats.length === 0 && (
+
+
+
+
+
Nenhuma conversa
+
+ Crie um novo chat para começar
+
)}
- {/* Empty state */}
- {!isLoading && chats.length === 0 && (
-
-
-
- Nenhuma conversa ainda
+ {!isLoading &&
+ chats.length > 0 &&
+ filteredChats.length === 0 &&
+ searchQuery.trim() && (
+
+ Nenhum resultado para "{searchQuery}"
-
- Clique em "Novo Chat" para começar
-
-
- )}
+ )}
>
)}
- {/* Delete Chat Alert */}
-
+
- Excluir Conversa?
+ Excluir conversa?
- Esta ação não pode ser desfeita. A conversa "{deletingChat?.title}" será permanentemente excluída.
+ "{deletingChat?.title}" será excluída permanentemente.
Cancelar
-
+
Excluir
@@ -543,6 +549,7 @@ interface ChatItemProps {
folders: FolderRecord[];
onMoveToFolder: (chatId: string, folderId: string) => void;
onDelete: () => void;
+ nested?: boolean;
}
const ChatItem = ({
@@ -552,8 +559,8 @@ const ChatItem = ({
folders,
onMoveToFolder,
onDelete,
+ nested = false,
}: ChatItemProps) => {
- // Formata timestamp relativo
const timeAgo = formatDistanceToNow(new Date(chat.updated_at), {
addSuffix: true,
locale: ptBR,
@@ -561,19 +568,34 @@ const ChatItem = ({
return (
{
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onSelect();
+ }
+ }}
+ className={cn(
+ "group flex w-full max-w-full min-w-0 cursor-pointer items-center gap-2 overflow-hidden rounded-md px-2 py-1.5 transition-colors",
+ nested && "py-1",
+ isSelected
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
+ : "hover:bg-sidebar-accent/50"
+ )}
>
-
-
-
+
+
+
{chat.title}
-
+
{timeAgo}
@@ -583,29 +605,32 @@ const ChatItem = ({
-
+
-
- {/* Opções para mover para pastas */}
- {folders.filter(f => f.id !== chat.folder_id).map((folder) => (
- onMoveToFolder(chat.id, folder.id)}
- >
-
- Mover para {folder.name}
-
- ))}
-
-
-
+
+ {folders.filter((f) => f.id !== chat.folder_id).length > 0 && (
+ <>
+ {folders
+ .filter((f) => f.id !== chat.folder_id)
+ .map((folder) => (
+ onMoveToFolder(chat.id, folder.id)}
+ >
+
+ Mover para {folder.name}
+
+ ))}
+
+ >
+ )}
+
+
Excluir
diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx
index 9c0ac7a..136bfc7 100644
--- a/src/components/chat/ChatView.tsx
+++ b/src/components/chat/ChatView.tsx
@@ -3,6 +3,13 @@ import { ChatHeader } from "@/components/ChatHeader";
import { ChatMessage } from "./ChatMessage";
import { ChatInput } from "./ChatInput";
import { ChatSidebar } from "./ChatSidebar";
+import { ChatWelcome } from "./ChatWelcome";
+import {
+ createWelcomeMessage,
+ isWelcomeMessage,
+ withoutWelcomeMessages,
+} from "./welcome-message";
+import type { Message } from "./ChatView.types";
import { ScrollArea } from "@/components/ui/scroll-area";
import { chatService, ChatRecord, MessageRecord } from "@/services/chat";
import { useToast } from "@/hooks/use-toast";
@@ -11,18 +18,6 @@ import { getModelId } from "@/config/models";
// Texto padrão da personalidade (usado para novos chats)
const DEFAULT_SYSTEM_PROMPT = "Você é um assistente útil e prestativo.";
-interface Message {
- id: string;
- role: "user" | "assistant";
- content: string;
- model?: string;
- attachments?: Array<{
- name: string;
- type: string;
- size: number;
- }>;
-}
-
export const ChatView = () => {
const { toast } = useToast();
const [isChatSidebarCollapsed, setIsChatSidebarCollapsed] = useState(false);
@@ -31,16 +26,17 @@ export const ChatView = () => {
const [currentChatId, setCurrentChatId] = useState("0");
// Inicia com texto padrão - será atualizado ao carregar chat existente
const [systemPrompt, setSystemPrompt] = useState(DEFAULT_SYSTEM_PROMPT);
- const [messages, setMessages] = useState([
- {
- id: "1",
- role: "assistant",
- content: "Olá! Sou o assistente HGTX Codex. Como posso ajudá-lo hoje?",
- model: "GPT-4o",
- },
- ]);
+ const [messages, setMessages] = useState([createWelcomeMessage()]);
const [isLoading, setIsLoading] = useState(false);
+ const [inputDraft, setInputDraft] = useState<{
+ text: string;
+ at: number;
+ } | null>(null);
+
+ const handleSelectSuggestion = (prompt: string) => {
+ setInputDraft({ text: prompt, at: Date.now() });
+ };
// NOTA: Salvamento automático desabilitado - mensagens já são salvas na API
// quando enviadas via handleSendMessage
@@ -82,14 +78,7 @@ export const ChatView = () => {
// Reseta a personalidade para o texto padrão
setSystemPrompt(DEFAULT_SYSTEM_PROMPT);
- setMessages([
- {
- id: "1",
- role: "assistant",
- content: "Olá! Sou o assistente HGTX Codex. Como posso ajudá-lo hoje?",
- model: selectedModel,
- },
- ]);
+ setMessages([createWelcomeMessage()]);
// Dispara evento para atualizar a sidebar
window.dispatchEvent(new Event('chatUpdated'));
@@ -136,14 +125,7 @@ export const ChatView = () => {
});
// Inicia com mensagem padrão em caso de erro
- setMessages([
- {
- id: "1",
- role: "assistant",
- content: "Olá! Sou o assistente HGTX Codex. Como posso ajudá-lo hoje?",
- model: selectedModel,
- },
- ]);
+ setMessages([createWelcomeMessage()]);
} finally {
setIsLoading(false);
}
@@ -162,7 +144,7 @@ export const ChatView = () => {
})),
};
- setMessages((prev) => [...prev, newMessage]);
+ setMessages((prev) => [...withoutWelcomeMessages(prev), newMessage]);
setIsLoading(true);
try {
@@ -234,7 +216,7 @@ export const ChatView = () => {
return (
{/* Chat Sidebar - Hidden on mobile */}
-
+
setIsChatSidebarCollapsed(!isChatSidebarCollapsed)}
@@ -256,15 +238,22 @@ export const ChatView = () => {
- {messages.map((message) => (
-
- ))}
+ {messages.map((message) =>
+ isWelcomeMessage(message) ? (
+
+ ) : (
+
+ )
+ )}
{isLoading && (
@@ -281,7 +270,12 @@ export const ChatView = () => {
-
+
setInputDraft(null)}
+ />
);
diff --git a/src/components/chat/ChatView.types.ts b/src/components/chat/ChatView.types.ts
new file mode 100644
index 0000000..53b01f1
--- /dev/null
+++ b/src/components/chat/ChatView.types.ts
@@ -0,0 +1,11 @@
+export interface Message {
+ id: string;
+ role: "user" | "assistant";
+ content: string;
+ model?: string;
+ attachments?: Array<{
+ name: string;
+ type: string;
+ size: number;
+ }>;
+}
diff --git a/src/components/chat/ChatWelcome.tsx b/src/components/chat/ChatWelcome.tsx
new file mode 100644
index 0000000..bd84477
--- /dev/null
+++ b/src/components/chat/ChatWelcome.tsx
@@ -0,0 +1,85 @@
+import { DiaTextReveal } from "@/components/ui/dia-text-reveal";
+import { AnimatedShinyText } from "@/components/ui/animated-shiny-text";
+import {
+ CHAT_STARTER_SUGGESTIONS,
+ COMMANDER_REVEAL_COLORS,
+} from "./welcome-message";
+import { cn } from "@/lib/utils";
+import { Sparkles } from "lucide-react";
+
+interface ChatWelcomeProps {
+ className?: string;
+ onSelectSuggestion?: (prompt: string) => void;
+}
+
+export function ChatWelcome({ className, onSelectSuggestion }: ChatWelcomeProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {onSelectSuggestion && (
+
+
+
+
+ Ideias para começar
+
+
+
+
+ {CHAT_STARTER_SUGGESTIONS.map((suggestion) => (
+ onSelectSuggestion(suggestion.prompt)}
+ title={suggestion.prompt}
+ className={cn(
+ "max-w-full rounded-full border border-border/80 bg-background/70 px-4 py-2.5 text-left text-sm font-medium text-foreground",
+ "shadow-sm transition-all duration-200",
+ "hover:border-primary/40 hover:bg-sidebar-accent/60 hover:shadow-md",
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
+ "active:scale-[0.98]"
+ )}
+ >
+ {suggestion.label}
+
+ ))}
+
+
+
+ Toque em uma sugestão para preencher a mensagem — você pode editar antes de enviar.
+
+
+ )}
+
+ );
+}
diff --git a/src/components/chat/welcome-message.ts b/src/components/chat/welcome-message.ts
new file mode 100644
index 0000000..635c87d
--- /dev/null
+++ b/src/components/chat/welcome-message.ts
@@ -0,0 +1,73 @@
+import type { Message } from "./ChatView.types";
+
+export const CHAT_WELCOME_MESSAGE_ID = "welcome";
+
+/** Paleta alinhada ao azul HGTX Commander */
+export const COMMANDER_REVEAL_COLORS = [
+ "#003366",
+ "#0055a4",
+ "#0066b3",
+ "#4d9de0",
+ "#a8c8f0",
+];
+
+export function createWelcomeMessage(): Message {
+ return {
+ id: CHAT_WELCOME_MESSAGE_ID,
+ role: "assistant",
+ content: "",
+ };
+}
+
+export function isWelcomeMessage(message: Message): boolean {
+ return message.id === CHAT_WELCOME_MESSAGE_ID;
+}
+
+export function withoutWelcomeMessages(messages: Message[]): Message[] {
+ return messages.filter((m) => !isWelcomeMessage(m));
+}
+
+export type ChatStarterSuggestion = {
+ id: string;
+ label: string;
+ prompt: string;
+};
+
+export const CHAT_STARTER_SUGGESTIONS: ChatStarterSuggestion[] = [
+ {
+ id: "executive-summary",
+ label: "Resumo com pendências",
+ prompt:
+ "Vou colar um texto ou documento abaixo. Produza um resumo executivo em tópicos, separe fatos de interpretações e liste pendências ou riscos que exijam decisão.",
+ },
+ {
+ id: "meeting-minutes",
+ label: "Ata da reunião",
+ prompt:
+ "Com base no conteúdo que enviarei, redija uma ata executiva: contexto em uma linha, decisões tomadas, responsáveis, prazos e pontos que ficaram em aberto.",
+ },
+ {
+ id: "audio-structure",
+ label: "Organizar transcrição",
+ prompt:
+ "Tenho uma transcrição de áudio para colar em seguida. Organize por tópicos cronológicos, destaque compromissos assumidos e sugira follow-ups que não foram fechados na conversa.",
+ },
+ {
+ id: "structured-reasoning",
+ label: "Linha de raciocínio",
+ prompt:
+ "Preciso estruturar um raciocínio sobre o tema que descreverei. Monte hipóteses, argumentos a favor e contra, e indique quais informações ainda faltam para fechar uma conclusão sólida.",
+ },
+ {
+ id: "compare-options",
+ label: "Comparar alternativas",
+ prompt:
+ "Vou descrever duas ou mais alternativas em seguida. Compare critérios objetivos, trade-offs e riscos de cada uma, e indique em que cenário cada opção faz mais sentido — sem favorecer uma resposta só pelo tom.",
+ },
+ {
+ id: "professional-draft",
+ label: "Comunicado profissional",
+ prompt:
+ "Com os pontos que enviarei abaixo, redija um e-mail ou comunicado interno: tom profissional e direto, assunto sugerido, corpo estruturado e uma ação clara no encerramento.",
+ },
+];
diff --git a/src/components/fechamento/UnidadeGate.tsx b/src/components/fechamento/UnidadeGate.tsx
index 31bb150..295402d 100644
--- a/src/components/fechamento/UnidadeGate.tsx
+++ b/src/components/fechamento/UnidadeGate.tsx
@@ -106,7 +106,7 @@ export function UnidadeGate({ children }: UnidadeGateProps) {
O código do estabelecimento não foi enviado pelo ambiente (TransferArea). Abra o módulo
- HGTX Intelligence Score a partir do Codex com o estabelecimento carregado no transfer.
+ HGTX Intelligence Score a partir do Commander com o estabelecimento carregado no transfer.
diff --git a/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx b/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx
index 7860ad3..0f30120 100644
--- a/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx
+++ b/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx
@@ -96,9 +96,9 @@ interface ChatMessage {
tipo_resposta?: "chat" | "agente" | null;
}
-const LS_READER_FONT = "codex-parecer-reader-font-step";
-const LS_READER_WIDTH = "codex-parecer-reader-comfortable-width";
-const LS_MAIN_COMFORTABLE = "codex-parecer-main-comfortable-width";
+const LS_READER_FONT = "commander-parecer-reader-font-step";
+const LS_READER_WIDTH = "commander-parecer-reader-comfortable-width";
+const LS_MAIN_COMFORTABLE = "commander-parecer-main-comfortable-width";
/** Velocidade do auto-scroll (px/s): nível 1 mais lento … 5 mais rápido (progressão mais perceptível). */
const READER_AUTO_SCROLL_SPEEDS = [42, 68, 98, 138, 195] as const;
@@ -588,7 +588,7 @@ export function ParecerJuridicoDetailView() {
.then((res) => {
toast.success(res.message);
setDeleteOpen(false);
- navigate("/codex/parecer-juridico");
+ navigate("/commander/parecer-juridico");
})
.catch((e) => {
toast.error(e?.message ?? "Erro ao excluir parecer.");
@@ -672,7 +672,7 @@ export function ParecerJuridicoDetailView() {
if (!id?.trim()) {
return (
-
navigate("/codex/parecer-juridico")} className="w-fit gap-2">
+ navigate("/commander/parecer-juridico")} className="w-fit gap-2">
Voltar
@@ -697,7 +697,7 @@ export function ParecerJuridicoDetailView() {
if (error && !parecer && !hasPreview) {
return (
-
navigate("/codex/parecer-juridico")} className="w-fit gap-2">
+ navigate("/commander/parecer-juridico")} className="w-fit gap-2">
Voltar
@@ -721,7 +721,7 @@ export function ParecerJuridicoDetailView() {
-
navigate("/codex/parecer-juridico")} className="shrink-0">
+ navigate("/commander/parecer-juridico")} className="shrink-0">
{titulo}
diff --git a/src/components/parecer-juridico/ParecerJuridicoFormView.tsx b/src/components/parecer-juridico/ParecerJuridicoFormView.tsx
index 80d01ef..5f5af87 100644
--- a/src/components/parecer-juridico/ParecerJuridicoFormView.tsx
+++ b/src/components/parecer-juridico/ParecerJuridicoFormView.tsx
@@ -119,7 +119,7 @@ export function ParecerJuridicoFormView() {
.then((data) => {
setIsGenerating(false);
playSuccessSound();
- navigate(`/codex/parecer-juridico/${data.id}`, {
+ navigate(`/commander/parecer-juridico/${data.id}`, {
state: { titulo: data.titulo, conteudoMarkdown: data.conteudo_gerado ?? "" },
});
})
@@ -136,11 +136,11 @@ export function ParecerJuridicoFormView() {
{isGenerating && (
)}
-
-
+
+
-
navigate("/codex/parecer-juridico")} className="shrink-0">
+ navigate("/commander/parecer-juridico")} className="shrink-0">
@@ -151,8 +151,8 @@ export function ParecerJuridicoFormView() {
-
-
+
+
Título do parecer
diff --git a/src/components/parecer-juridico/ParecerJuridicoView.tsx b/src/components/parecer-juridico/ParecerJuridicoView.tsx
index 5573063..75b63b0 100644
--- a/src/components/parecer-juridico/ParecerJuridicoView.tsx
+++ b/src/components/parecer-juridico/ParecerJuridicoView.tsx
@@ -105,7 +105,7 @@ export const ParecerJuridicoView = () => {
};
const handleEditar = (item: ParecerListItem) => {
- navigate(`/codex/parecer-juridico/${item.id}`);
+ navigate(`/commander/parecer-juridico/${item.id}`);
};
return (
@@ -117,7 +117,7 @@ export const ParecerJuridicoView = () => {
Parecer Jurídico
-
navigate("/codex/parecer-juridico/novo")} className="gap-1 md:gap-2 flex-1 md:flex-none text-xs md:text-sm">
+ navigate("/commander/parecer-juridico/novo")} className="gap-1 md:gap-2 flex-1 md:flex-none text-xs md:text-sm">
Novo Parecer
Novo
diff --git a/src/components/prompts/PromptAssistantButton.tsx b/src/components/prompts/PromptAssistantButton.tsx
new file mode 100644
index 0000000..e197c7b
--- /dev/null
+++ b/src/components/prompts/PromptAssistantButton.tsx
@@ -0,0 +1,92 @@
+import { Sparkles } from "lucide-react";
+import { useTheme } from "next-themes";
+import { AnimatedGradientText } from "@/components/ui/animated-gradient-text";
+import { cn } from "@/lib/utils";
+
+type PromptAssistantButtonProps = {
+ onClick?: () => void;
+ disabled?: boolean;
+ loading?: boolean;
+ className?: string;
+ asBadge?: boolean;
+};
+
+function useAssistantPalette() {
+ const { resolvedTheme } = useTheme();
+ const isDark = resolvedTheme === "dark";
+
+ return {
+ isDark,
+ textFrom: isDark ? "hsl(210, 55%, 88%)" : "hsl(210, 100%, 28%)",
+ textTo: isDark ? "hsl(210, 70%, 72%)" : "hsl(210, 75%, 40%)",
+ };
+}
+
+const shellClass = (opts: { asBadge?: boolean; disabled?: boolean; interactive?: boolean }) =>
+ cn(
+ "inline-flex items-center gap-2.5 rounded-full border transition-all duration-300",
+ "border-primary/20 bg-gradient-to-br from-primary/[0.07] via-card to-accent/30",
+ "shadow-[0_1px_2px_hsl(var(--foreground)/0.04),0_4px_16px_hsl(var(--primary)/0.06)]",
+ opts.asBadge ? "px-3.5 py-1.5" : "min-h-10 px-4 py-2",
+ opts.interactive &&
+ !opts.disabled &&
+ "hover:border-primary/35 hover:from-primary/[0.1] hover:shadow-[0_2px_8px_hsl(var(--foreground)/0.05),0_8px_24px_hsl(var(--primary)/0.12)] hover:-translate-y-px active:translate-y-0",
+ opts.disabled && "opacity-60"
+ );
+
+export function PromptAssistantButton({
+ onClick,
+ disabled,
+ loading,
+ className,
+ asBadge = false,
+}: PromptAssistantButtonProps) {
+ const palette = useAssistantPalette();
+
+ const label = loading ? (
+ Gerando em segundo plano...
+ ) : (
+
+ Assistente de criação de prompt
+
+ );
+
+ const content = (
+ <>
+
+
+
+ {label}
+ >
+ );
+
+ if (asBadge) {
+ return {content} ;
+ }
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/src/components/prompts/PromptsFormView.tsx b/src/components/prompts/PromptsFormView.tsx
index be29be4..7f6508b 100644
--- a/src/components/prompts/PromptsFormView.tsx
+++ b/src/components/prompts/PromptsFormView.tsx
@@ -8,11 +8,8 @@ import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
-import { ShimmerButton } from "@/components/ui/shimmer-button";
-import { SparklesText } from "@/components/ui/sparkles-text";
import { InteractiveHoverButton } from "@/components/ui/interactive-hover-button";
-import { BorderBeam } from "@/components/ui/border-beam";
-import { AnimatedShinyText } from "@/components/ui/animated-shiny-text";
+import { PromptAssistantButton } from "@/components/prompts/PromptAssistantButton";
import { Confetti, type ConfettiRef } from "@/components/ui/confetti";
import { cn } from "@/lib/utils";
import { usePrompts } from "@/contexts/PromptsContext";
@@ -85,7 +82,7 @@ export function PromptsFormView() {
useEffect(() => {
if (id && !prompt && prompts.length > 0) {
- navigate("/codex/prompts", { replace: true });
+ navigate("/commander/prompts", { replace: true });
}
}, [id, prompt, prompts.length, navigate]);
@@ -143,7 +140,7 @@ export function PromptsFormView() {
setPrompts((prev) => [...prev, { id: res.id, titulo: res.titulo, area: finalAreaName, area_id: finalAreaId, descricao: finalDescricao || undefined, conteudo: res.conteudo ?? "" }]);
toast.success("Prompt criado.");
}
- navigate("/codex/prompts");
+ navigate("/commander/prompts");
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? (e as { message: string }).message : "Erro ao salvar.";
toast.error(message);
@@ -224,7 +221,7 @@ export function PromptsFormView() {
/>
-
navigate("/codex/prompts")}>
+ navigate("/commander/prompts")}>
Voltar
@@ -277,27 +274,11 @@ export function PromptsFormView() {
title="Gerando em andamento"
/>
)}
- setIsAssistantOpen(true)}
disabled={isAssistantWorkingInBackground}
- background="hsl(var(--card))"
- shimmerColor="hsl(var(--foreground) / 0.14)"
- className={cn(
- "gap-2 px-5 py-2.5 text-sm font-medium text-card-foreground border-border shadow-md hover:shadow-lg transition-shadow",
- isAssistantWorkingInBackground && "opacity-70 pointer-events-none"
- )}
- >
- {isAssistantWorkingInBackground ? (
-
- Gerando em segundo plano...
-
- ) : (
-
- ✨ Assistente de Criação de Prompt
-
- )}
-
+ loading={isAssistantWorkingInBackground}
+ />
- setIsAssistantOpen(false)} disabled={isAssistantLoading}>
+ setIsAssistantOpen(false)} disabled={isAssistantLoading}>
Fechar
{
const openEditFromDetails = () => {
if (!selectedPrompt) return;
setIsDetailsDialogOpen(false);
- navigate(`/codex/prompts/${selectedPrompt.id}`);
+ navigate(`/commander/prompts/${selectedPrompt.id}`);
};
const openDeleteDialog = (prompt: Prompt) => {
@@ -168,7 +168,7 @@ export const PromptsView = () => {
Prompts
-
navigate("/codex/prompts/novo")} className="gap-1 md:gap-2 flex-1 md:flex-none text-xs md:text-sm">
+ navigate("/commander/prompts/novo")} className="gap-1 md:gap-2 flex-1 md:flex-none text-xs md:text-sm">
Novo prompt
Novo
@@ -231,7 +231,7 @@ export const PromptsView = () => {
-
navigate("/codex/prompts/novo")}>
+ navigate("/commander/prompts/novo")}>
Novo prompt
@@ -287,7 +287,7 @@ export const PromptsView = () => {
navigate(`/codex/prompts/${prompt.id}`)}
+ onClick={() => navigate(`/commander/prompts/${prompt.id}`)}
title="Editar"
className="h-7 w-7 md:h-9 md:w-9 hover:bg-slate-300 dark:hover:bg-slate-700 hover:text-foreground"
>
diff --git a/src/components/ui/animated-gradient-text.tsx b/src/components/ui/animated-gradient-text.tsx
index 39c6690..a09b322 100644
--- a/src/components/ui/animated-gradient-text.tsx
+++ b/src/components/ui/animated-gradient-text.tsx
@@ -1,8 +1,8 @@
-import { type ComponentPropsWithoutRef } from "react"
+import { type ComponentPropsWithoutRef, type CSSProperties } from "react"
import { cn } from "@/lib/utils"
-export interface AnimatedGradientTextProps extends ComponentPropsWithoutRef<"div"> {
+export interface AnimatedGradientTextProps extends ComponentPropsWithoutRef<"span"> {
speed?: number
colorFrom?: string
colorTo?: string
@@ -12,23 +12,24 @@ export function AnimatedGradientText({
children,
className,
speed = 1,
- colorFrom = "#ffaa40",
- colorTo = "#9c40ff",
+ colorFrom = "#0055a4",
+ colorTo = "#0d8ecf",
+ style,
...props
}: AnimatedGradientTextProps) {
+ const bgSize = `${speed * 300}%`
+
return (
{children}
diff --git a/src/components/ui/dia-text-reveal.tsx b/src/components/ui/dia-text-reveal.tsx
new file mode 100644
index 0000000..fa77bce
--- /dev/null
+++ b/src/components/ui/dia-text-reveal.tsx
@@ -0,0 +1,261 @@
+import { useEffect, useRef, useState } from "react"
+import {
+ animate,
+ motion,
+ useInView,
+ useMotionValue,
+ useReducedMotion,
+ useTransform,
+ type HTMLMotionProps,
+} from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+const DEFAULT_COLORS = ["#c679c4", "#fa3d1d", "#ffb005", "#e1e1fe", "#0358f7"]
+const BAND_HALF = 17
+const SWEEP_START = -BAND_HALF
+const SWEEP_END = 100 + BAND_HALF
+
+const sweepEase = (t: number) =>
+ t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2
+
+function buildGradient(pos: number, colors: string[], textColor: string) {
+ const bandStart = pos - BAND_HALF
+ const bandEnd = pos + BAND_HALF
+
+ if (bandStart >= 100) {
+ return `linear-gradient(90deg, ${textColor}, ${textColor})`
+ }
+ const n = colors.length
+ const parts: string[] = []
+
+ if (bandStart > 0)
+ parts.push(`${textColor} 0%`, `${textColor} ${bandStart.toFixed(2)}%`)
+
+ colors.forEach((c, i) => {
+ const pct = n === 1 ? pos : bandStart + (i / (n - 1)) * BAND_HALF * 2
+ parts.push(`${c} ${pct.toFixed(2)}%`)
+ })
+
+ if (bandEnd < 100)
+ parts.push(`transparent ${bandEnd.toFixed(2)}%`, `transparent 100%`)
+
+ return `linear-gradient(90deg, ${parts.join(", ")})`
+}
+
+function measureWidths(el: HTMLElement, texts: string[]) {
+ const ghost = el.cloneNode() as HTMLElement
+ Object.assign(ghost.style, {
+ position: "absolute",
+ visibility: "hidden",
+ pointerEvents: "none",
+ width: "auto",
+ whiteSpace: "nowrap",
+ })
+ el.parentElement!.appendChild(ghost)
+ const widths = texts.map((t) => {
+ ghost.textContent = t
+ return ghost.getBoundingClientRect().width
+ })
+ ghost.remove()
+ return widths
+}
+
+/**
+ * Props for {@link DiaTextReveal}.
+ */
+export interface DiaTextRevealProps extends Omit<
+ HTMLMotionProps<"span">,
+ "ref" | "children" | "style" | "animate" | "transition" | "color"
+> {
+ /**
+ * Text to reveal. Pass multiple strings to rotate when {@link DiaTextRevealProps.repeat} is `true`.
+ */
+ text: string | string[]
+ /**
+ * Colors sampled across the moving gradient band. Defaults to a built-in palette.
+ */
+ colors?: string[]
+ /**
+ * CSS color for revealed text after the sweep and for leading/trailing regions during the animation.
+ * @defaultValue `"var(--foreground)"`
+ */
+ textColor?: string
+ /**
+ * Duration of one sweep pass, in seconds.
+ * @defaultValue `1.5`
+ */
+ duration?: number
+ /**
+ * Delay before the sweep starts, in seconds.
+ * @defaultValue `0`
+ */
+ delay?: number
+ /**
+ * When `text` is an array, replay the sweep and advance to the next string after each completion.
+ * @defaultValue `false`
+ */
+ repeat?: boolean
+ /**
+ * Pause between cycles when {@link DiaTextRevealProps.repeat} is `true`, in seconds.
+ * @defaultValue `0.5`
+ */
+ repeatDelay?: number
+ /**
+ * If `true`, the animation starts only after the element enters the viewport.
+ * @defaultValue `true`
+ */
+ startOnView?: boolean
+ /**
+ * Passed to `useInView`: if `true`, in-view detection fires at most once (no replay on scroll-back).
+ * @defaultValue `true`
+ */
+ once?: boolean
+ /**
+ * Additional class names for the animated `span` (e.g. typography utilities).
+ */
+ className?: string
+ /**
+ * When `text` has multiple entries, use the widest string’s width for layout instead of animating width per line.
+ * @defaultValue `false`
+ */
+ fixedWidth?: boolean
+}
+
+export function DiaTextReveal({
+ text,
+ colors = DEFAULT_COLORS,
+ textColor = "var(--foreground)",
+ duration = 1.5,
+ delay = 0,
+ repeat = false,
+ repeatDelay = 0.5,
+ startOnView = true,
+ once = true,
+ className,
+ fixedWidth = false,
+ ...props
+}: DiaTextRevealProps) {
+ const texts = Array.isArray(text) ? text : [text]
+ const isMulti = texts.length > 1
+ const prefersReducedMotion = useReducedMotion()
+
+ const spanRef = useRef(null)
+ const optsRef = useRef({
+ colors,
+ textColor,
+ duration,
+ delay,
+ repeat,
+ repeatDelay,
+ texts,
+ })
+ optsRef.current = {
+ colors,
+ textColor,
+ duration,
+ delay,
+ repeat,
+ repeatDelay,
+ texts,
+ }
+
+ const indexRef = useRef(0)
+ const hasPlayedRef = useRef(false)
+ const timerRef = useRef>(undefined)
+ const playRef = useRef<() => void>(null!)
+ const stopRef = useRef<(() => void) | null>(null)
+
+ const [activeIndex, setActiveIndex] = useState(0)
+ const [measuredWidths, setMeasuredWidths] = useState([])
+
+ const sweepPos = useMotionValue(SWEEP_START)
+
+ const backgroundImage = useTransform(sweepPos, (pos) =>
+ buildGradient(pos, optsRef.current.colors, optsRef.current.textColor)
+ )
+
+ const isInView = useInView(spanRef, { once, amount: 0.1 })
+
+ useEffect(() => {
+ const el = spanRef.current
+ if (!el || !isMulti) return
+ setMeasuredWidths(measureWidths(el, texts))
+ }, [Array.isArray(text) ? text.join("\0") : text])
+
+ playRef.current = () => {
+ const { duration, delay, repeat, repeatDelay, texts } = optsRef.current
+
+ sweepPos.set(SWEEP_START)
+
+ const controls = animate(sweepPos, SWEEP_END, {
+ duration,
+ delay,
+ ease: sweepEase,
+ onComplete() {
+ if (!repeat) return
+ timerRef.current = setTimeout(() => {
+ const next = (indexRef.current + 1) % texts.length
+ indexRef.current = next
+ setActiveIndex(next)
+ playRef.current()
+ }, repeatDelay * 1000)
+ },
+ })
+
+ stopRef.current = () => controls.stop()
+ }
+
+ useEffect(() => {
+ if (prefersReducedMotion) {
+ sweepPos.set(SWEEP_END)
+ return
+ }
+ if (startOnView && !isInView) return
+ if (once && hasPlayedRef.current) return
+ hasPlayedRef.current = true
+ playRef.current()
+
+ return () => {
+ stopRef.current?.()
+ clearTimeout(timerRef.current)
+ }
+ }, [isInView, startOnView, once, prefersReducedMotion, sweepPos])
+
+ const fixedW =
+ isMulti && fixedWidth && measuredWidths.length > 0
+ ? Math.max(...measuredWidths)
+ : undefined
+
+ const animatedW =
+ isMulti && !fixedWidth && measuredWidths[activeIndex] != null
+ ? measuredWidths[activeIndex]
+ : undefined
+
+ return (
+
+ {texts[activeIndex]}
+
+ )
+}
diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
index 8d29b27..5cf0816 100644
--- a/src/components/ui/scroll-area.tsx
+++ b/src/components/ui/scroll-area.tsx
@@ -8,7 +8,9 @@ const ScrollArea = React.forwardRef<
React.ComponentPropsWithoutRef
>(({ className, children, ...props }, ref) => (
- {children}
+
+ {children}
+
@@ -36,3 +38,4 @@ const ScrollBar = React.forwardRef<
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };
+
diff --git a/src/index.css b/src/index.css
index d78de1d..780c67c 100644
--- a/src/index.css
+++ b/src/index.css
@@ -6,62 +6,123 @@
@layer base {
:root {
- /* HGTX Codex - Light Theme */
- --background: 210 40% 98%;
- --foreground: 222 47% 11%;
+ /* ─── HGTX Commander — Design System Tokens (compartilhados) ─── */
+
+ /* Tipografia */
+ --fs-display: 32px;
+ --fs-h1: 24px;
+ --fs-h2: 18px;
+ --fs-h3: 15px;
+ --fs-body: 14px;
+ --fs-small: 12px;
+ --fs-caption: 11px;
+ --lh-tight: 1.25;
+ --lh-body: 1.5;
+ --lh-relaxed: 1.65;
+ --fw-regular: 400;
+ --fw-medium: 500;
+ --fw-semibold: 600;
+ --fw-bold: 700;
+
+ /* Espaçamento */
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 20px;
+ --space-6: 24px;
+ --space-7: 32px;
+ --space-8: 48px;
+ --space-9: 64px;
+ --section-y: 64px;
+ --section-x: 40px;
+ --nav-h: 56px;
+ --container: 1200px;
+
+ /* Border radius */
+ --radius-sm: 6px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-xl: 16px;
+ --radius-pill: 999px;
+
+ /* ─── Light Theme ─── */
+ --background: 0 0% 100%;
+ --foreground: 209 11% 15%;
--card: 0 0% 100%;
- --card-foreground: 222 47% 11%;
+ --card-foreground: 209 11% 15%;
--popover: 0 0% 100%;
- --popover-foreground: 222 47% 11%;
+ --popover-foreground: 209 11% 15%;
- --primary: 190 100% 45%;
+ /* Azul primário vivo (referência ~#004080 / #0055a4) */
+ --primary: 210 100% 33%;
--primary-foreground: 0 0% 100%;
+ --primary-hover: 210 100% 28%;
+ --primary-pressed: 210 100% 24%;
+ --primary-light: 210 55% 96%;
- --secondary: 210 100% 45%;
- --secondary-foreground: 0 0% 100%;
+ /* Secondary neutro para botões */
+ --secondary: 210 6% 60%;
+ --secondary-foreground: 209 11% 15%;
- --muted: 210 40% 96%;
- --muted-foreground: 215 16% 47%;
+ /* Muted — bg suave */
+ --muted: 210 17% 96%;
+ --muted-foreground: 209 7% 46%;
- --accent: 190 100% 50%;
- --accent-foreground: 0 0% 100%;
+ /* Accent = fundo nav ativo */
+ --accent: 210 70% 94%;
+ --accent-foreground: 210 100% 33%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
- --success: 142 71% 45%;
+ /* Sucesso: #28a745 */
+ --success: 134 61% 41%;
--success-foreground: 0 0% 100%;
--warning: 38 92% 50%;
- --warning-foreground: 222 47% 11%;
+ --warning-foreground: 209 11% 15%;
- --border: 214 32% 91%;
- --input: 214 32% 91%;
- --ring: 190 100% 45%;
+ /* Borda: #e5e7eb */
+ --border: 220 13% 91%;
+ /* --input no shadcn = cor da BORDA de Input/Select/Textarea */
+ --input: 220 9% 83%;
+ --ring: 210 100% 33%;
- --radius: 0.75rem;
+ --radius: 0.5rem;
- /* Sidebar specific - Light */
- --sidebar-background: 210 40% 99%;
- --sidebar-foreground: 222 47% 11%;
- --sidebar-primary: 190 100% 45%;
+ /* Sidebar — Light */
+ --sidebar-background: 210 17% 98%;
+ --sidebar-foreground: 209 11% 15%;
+ --sidebar-primary: 210 100% 33%;
--sidebar-primary-foreground: 0 0% 100%;
- --sidebar-accent: 210 40% 95%;
- --sidebar-accent-foreground: 222 47% 11%;
- --sidebar-border: 214 32% 88%;
- --sidebar-ring: 190 100% 45%;
+ --sidebar-accent: 210 70% 94%;
+ --sidebar-accent-foreground: 210 100% 33%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 210 100% 33%;
- /* Custom gradients - Light */
- --gradient-cyber: linear-gradient(135deg, hsl(190 100% 45%) 0%, hsl(210 100% 45%) 100%);
- --gradient-subtle: linear-gradient(180deg, hsl(210 40% 98%) 0%, hsl(210 40% 95%) 100%);
- --glow-cyan: 0 0 40px hsl(190 100% 50% / 0.15);
- --glow-blue: 0 0 30px hsl(210 100% 50% / 0.12);
+ /* Tokens extras — Light */
+ --bubble-bot: 209 36% 96%;
+ --nav-active: 210 70% 94%;
+ --input-bg: 0 0% 100%;
+ --placeholder: 220 9% 66%;
+ --btn-secondary-border: 220 9% 83%;
+ --border-subtle: 220 13% 95%;
+ --shadow-card: 0 4px 12px rgba(0, 0, 0, 0.06);
+ --shadow-modal: 0 16px 40px rgba(0, 0, 0, 0.12);
+ --focus-ring: rgba(0, 85, 164, 0.28);
+
+ /* Gradientes — Light */
+ --gradient-cyber: linear-gradient(135deg, hsl(210 100% 33%) 0%, hsl(210 90% 42%) 100%);
+ --gradient-subtle: linear-gradient(180deg, hsl(0 0% 100%) 0%, hsl(210 17% 98%) 100%);
+ --glow-cyan: 0 0 40px hsl(210 100% 33% / 0.15);
+ --glow-blue: 0 0 30px hsl(210 90% 42% / 0.12);
- /* Animation speeds */
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+
--color-1: oklch(66.2% 0.225 25.9);
--color-2: oklch(60.4% 0.26 302);
--color-3: oklch(69.6% 0.165 251);
@@ -70,62 +131,85 @@
}
.dark {
- /* HGTX Codex - Dark Theme */
- --background: 222 47% 5%;
- --foreground: 200 100% 95%;
+ /* ─── Dark Theme ─── */
+ /* Fundo principal = mesma família da sidebar (216 50% 9%), levemente mais profundo */
+ --background: 216 48% 8%;
+ --foreground: 0 0% 100%;
- --card: 222 40% 8%;
- --card-foreground: 200 100% 95%;
+ /* Cards/painéis um tom acima do fundo para hierarquia sutil */
+ --card: 216 42% 11%;
+ --card-foreground: 0 0% 100%;
- --popover: 222 45% 10%;
- --popover-foreground: 200 100% 95%;
+ --popover: 216 42% 11%;
+ --popover-foreground: 0 0% 100%;
- --primary: 190 100% 50%;
- --primary-foreground: 222 47% 5%;
+ /* Azul primário dark — vibrante em fundo escuro */
+ --primary: 210 90% 52%;
+ --primary-foreground: 0 0% 100%;
+ --primary-hover: 210 95% 46%;
+ --primary-pressed: 210 100% 40%;
+ --primary-light: 210 50% 14%;
- --secondary: 210 100% 50%;
- --secondary-foreground: 222 47% 5%;
+ --secondary: 216 38% 13%;
+ --secondary-foreground: 0 0% 100%;
- --muted: 222 30% 15%;
- --muted-foreground: 200 20% 65%;
+ --muted: 216 38% 13%;
+ --muted-foreground: 212 9% 58%;
- --accent: 190 100% 60%;
- --accent-foreground: 222 47% 5%;
+ /* Accent dark */
+ --accent: 210 45% 16%;
+ --accent-foreground: 210 85% 62%;
--destructive: 0 84% 60%;
- --destructive-foreground: 200 100% 95%;
+ --destructive-foreground: 0 0% 100%;
- --success: 142 71% 45%;
- --success-foreground: 222 47% 5%;
+ /* Sucesso dark: #23d160 */
+ --success: 141 71% 48%;
+ --success-foreground: 216 48% 8%;
--warning: 38 92% 50%;
- --warning-foreground: 222 47% 5%;
+ --warning-foreground: 216 48% 8%;
- --border: 222 30% 18%;
- --input: 222 30% 18%;
- --ring: 190 100% 50%;
+ /* Bordas dark — tom suave (evita “borda preta” em inputs/selects) */
+ --border: 216 18% 26%;
+ /* --input no shadcn = cor da BORDA de Input/Select/Textarea (não o fundo) */
+ --input: 216 16% 30%;
+ --ring: 210 90% 52%;
- /* Sidebar specific - Dark */
- --sidebar-background: 222 45% 7%;
- --sidebar-foreground: 200 100% 90%;
- --sidebar-primary: 190 100% 50%;
- --sidebar-primary-foreground: 222 47% 5%;
- --sidebar-accent: 222 35% 12%;
- --sidebar-accent-foreground: 190 100% 60%;
- --sidebar-border: 222 30% 15%;
- --sidebar-ring: 190 100% 50%;
+ /* Sidebar — Dark */
+ --sidebar-background: 216 50% 9%;
+ --sidebar-foreground: 0 0% 100%;
+ --sidebar-primary: 210 90% 52%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 210 45% 16%;
+ --sidebar-accent-foreground: 210 85% 62%;
+ --sidebar-border: 212 12% 21%;
+ --sidebar-ring: 210 90% 52%;
+
+ /* Tokens extras — Dark */
+ --bubble-bot: 216 36% 14%;
+ --nav-active: 210 45% 16%;
+ --input-bg: 216 40% 10%;
+ --placeholder: 212 10% 31%;
+ --btn-secondary-border: 216 16% 30%;
+ --border-subtle: 216 22% 20%;
+ --shadow-card: 0 4px 16px rgba(0, 0, 0, 0.35);
+ --shadow-modal: 0 24px 48px rgba(0, 0, 0, 0.55);
+ --focus-ring: rgba(51, 136, 255, 0.35);
+
+ /* Gradientes — Dark */
+ --gradient-cyber: linear-gradient(135deg, hsl(210 90% 52%) 0%, hsl(210 100% 40%) 100%);
+ --gradient-subtle: linear-gradient(180deg, hsl(216 42% 11%) 0%, hsl(216 48% 8%) 100%);
+ --glow-cyan: 0 0 40px hsl(210 90% 52% / 0.35);
+ --glow-blue: 0 0 30px hsl(210 100% 40% / 0.28);
- /* Custom gradients - Dark */
- --gradient-cyber: linear-gradient(135deg, hsl(190 100% 50%) 0%, hsl(210 100% 50%) 100%);
- --gradient-subtle: linear-gradient(180deg, hsl(222 45% 8%) 0%, hsl(222 47% 5%) 100%);
- --glow-cyan: 0 0 40px hsl(190 100% 50% / 0.3);
- --glow-blue: 0 0 30px hsl(210 100% 50% / 0.25);
--color-1: oklch(66.2% 0.225 25.9);
--color-2: oklch(60.4% 0.26 302);
--color-3: oklch(69.6% 0.165 251);
--color-4: oklch(80.2% 0.134 225);
--color-5: oklch(90.7% 0.231 133);
}
+
.theme {
--animate-shiny-text: shiny-text 8s infinite;
--animate-gradient: gradient 8s linear infinite;
@@ -144,12 +228,21 @@
@apply border-border transition-colors;
}
+ html,
+ body,
+ #root {
+ height: 100%;
+ }
+
body {
@apply bg-background text-foreground font-sans antialiased;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
- /* Scrollbar styling */
+ #root {
+ @apply min-h-0;
+ }
+
::-webkit-scrollbar {
width: 8px;
height: 8px;
@@ -179,15 +272,22 @@
.cyber-border {
@apply border border-primary/30 rounded-lg;
- box-shadow: 0 0 20px hsl(190 100% 50% / 0.1);
+ box-shadow: 0 0 20px hsl(210 100% 33% / 0.1);
}
.gradient-text {
- @apply bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent;
+ @apply bg-gradient-to-r from-primary to-primary/70 bg-clip-text text-transparent;
+ }
+
+ .animated-gradient-text {
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
}
.metric-card {
@apply bg-card rounded-xl p-5 border border-border shadow-sm transition-all duration-300 hover:shadow-md hover:border-primary/30;
+ box-shadow: var(--shadow-card);
}
.nav-item {
@@ -246,6 +346,16 @@
}
}
+@keyframes gradient-text-shift {
+ 0%,
+ 100% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+}
+
@theme inline {
@keyframes shiny-text {
0%, 90%, 100% {
diff --git a/src/modules/commander/App.tsx b/src/modules/commander/App.tsx
new file mode 100644
index 0000000..c34095e
--- /dev/null
+++ b/src/modules/commander/App.tsx
@@ -0,0 +1,47 @@
+import { Navigate, Route, Routes } from "react-router-dom";
+import React from "react";
+import { GlobalFunctions } from "@/GlobalFunctions";
+import { PromptsProvider } from "@/contexts/PromptsContext";
+import { MainLayout } from "./components/layout/MainLayout";
+import { ChatView } from "@/components/chat/ChatView";
+import { ImageView } from "@/components/images/ImageView";
+import { TranscriptionView } from "@/components/audio/TranscriptionView";
+import { GenerationView } from "@/components/audio/GenerationView";
+import { PromptsView } from "@/components/prompts/PromptsView";
+import { PromptsFormView } from "@/components/prompts/PromptsFormView";
+import { AreasView } from "@/components/areas/AreasView";
+import { ParecerJuridicoView } from "@/components/parecer-juridico/ParecerJuridicoView";
+import { ParecerJuridicoFormView } from "@/components/parecer-juridico/ParecerJuridicoFormView";
+import { ParecerJuridicoDetailView } from "@/components/parecer-juridico/ParecerJuridicoDetailView";
+
+const CommanderApp = () => {
+ React.useEffect(() => {
+ if (!GlobalFunctions.isUsuarioLogado()) {
+ window.location.replace(import.meta.env.VITE_BASE_URL_HGTX_CORE);
+ }
+ }, []);
+
+ return (
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ );
+};
+
+export default CommanderApp;
diff --git a/src/modules/commander/components/layout/CommanderLogo.tsx b/src/modules/commander/components/layout/CommanderLogo.tsx
new file mode 100644
index 0000000..f2f898f
--- /dev/null
+++ b/src/modules/commander/components/layout/CommanderLogo.tsx
@@ -0,0 +1,32 @@
+import { cn } from "@/lib/utils";
+
+interface CommanderLogoProps {
+ className?: string;
+ /** Classes do container (expandido: proporção 3:2 da arte; recolhido: quadrado) */
+ heightClass?: string;
+}
+
+export const CommanderLogo = ({
+ className,
+ heightClass = "h-36 w-full",
+}: CommanderLogoProps) => {
+ const imgClass = cn(
+ "absolute inset-0 h-full w-full object-contain object-left",
+ className
+ );
+
+ return (
+
+
+
+
+ );
+};
diff --git a/src/modules/commander/components/layout/MainLayout.tsx b/src/modules/commander/components/layout/MainLayout.tsx
new file mode 100644
index 0000000..78c8c37
--- /dev/null
+++ b/src/modules/commander/components/layout/MainLayout.tsx
@@ -0,0 +1,210 @@
+import { useState, type ComponentType } from "react";
+import { NavLink } from "react-router-dom";
+import {
+ MessageSquare,
+ Image,
+ AudioLines,
+ Mic,
+ ArrowLeft,
+ ChevronLeft,
+ ChevronRight,
+ Bot,
+ FileText,
+ Layers,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Separator } from "@/components/ui/separator";
+import { ThemeToggle } from "@/components/ThemeToggle";
+import { GlobalFunctions } from "@/GlobalFunctions";
+import { CommanderLogo } from "./CommanderLogo";
+import { cn } from "@/lib/utils";
+
+type NavItem = {
+ path: string;
+ label: string;
+ icon: ComponentType<{ className?: string }>;
+};
+
+const navSections: { title: string; items: NavItem[] }[] = [
+ {
+ title: "I.A",
+ items: [
+ { path: "bate-papo", label: "Bate-papo", icon: MessageSquare },
+ { path: "imagens", label: "Imagens", icon: Image },
+ { path: "transcricao-audio", label: "Transcrição de Áudio", icon: AudioLines },
+ { path: "geracao-audio", label: "Geração de Áudio", icon: Mic },
+ ],
+ },
+ {
+ title: "Parecer Jurídico",
+ items: [
+ { path: "areas", label: "Áreas", icon: Layers },
+ { path: "parecer-juridico", label: "Parecer Jurídico", icon: Bot },
+ { path: "prompts", label: "Prompts", icon: FileText },
+ ],
+ },
+];
+
+const navItems = navSections.flatMap((section) => section.items);
+
+const navLinkClass = (isActive: boolean, collapsed?: boolean) =>
+ cn(
+ "flex items-center rounded-lg text-sm font-medium transition-all duration-200 w-full",
+ collapsed
+ ? "justify-center p-3"
+ : "gap-3 px-3 py-2.5",
+ isActive
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
+ : "text-sidebar-foreground hover:bg-sidebar-accent/60 hover:text-foreground"
+ );
+
+interface MainLayoutProps {
+ children: React.ReactNode;
+}
+
+export const MainLayout = ({ children }: MainLayoutProps) => {
+ const [collapsed, setCollapsed] = useState(false);
+
+ return (
+
+ {/* Sidebar desktop expandido */}
+ {!collapsed ? (
+
+ {/* Logo + recolher lado a lado */}
+
+
+
+
+
setCollapsed(true)}
+ className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
+ title="Recolher menu"
+ >
+
+
+
+
+ {/* Navegação */}
+
+ {navSections.map((section) => (
+
+
+ {section.title}
+
+ {section.items.map((item) => (
+
navLinkClass(isActive)}
+ >
+
+ {item.label}
+
+ ))}
+
+ ))}
+
+
+
+
+ {/* Rodapé */}
+
+
+ Tema
+
+
+
+
+ Voltar
+
+
+
+ ) : (
+ /* Sidebar desktop recolhido */
+
+
+
setCollapsed(false)}
+ className="w-full text-muted-foreground hover:text-foreground"
+ title="Expandir"
+ >
+
+
+
+
+
+
+
+
+ {navItems.map((item) => (
+ navLinkClass(isActive, true)}
+ title={item.label}
+ >
+
+
+ ))}
+
+
+
+
+
+
+ )}
+
+ {/* Conteúdo principal */}
+
+ {children}
+
+
+ {/* Navegação mobile (bottom bar) */}
+
+ {navItems.map((item) => (
+
+ cn(
+ "flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-all",
+ isActive ? "text-primary" : "text-sidebar-foreground/70"
+ )
+ }
+ >
+ {({ isActive }) => (
+ <>
+
+ {item.label}
+ >
+ )}
+
+ ))}
+
+
+ );
+};
diff --git a/src/modules/intelligence-ia/components/layout/AppSidebar.tsx b/src/modules/intelligence-ia/components/layout/AppSidebar.tsx
index dfdb86c..7eaca3c 100644
--- a/src/modules/intelligence-ia/components/layout/AppSidebar.tsx
+++ b/src/modules/intelligence-ia/components/layout/AppSidebar.tsx
@@ -10,6 +10,7 @@ import {
DollarSign,
UserCircle,
} from "lucide-react";
+import { ThemeToggle } from "@/components/ThemeToggle";
import { cn } from "@/lib/utils";
const navItems = [
@@ -27,12 +28,12 @@ export function AppSidebar() {
<>
{/* Logo */}
-
+
{!collapsed && (
-
AI Agent
+
AI Agent
Painel Admin
)}
@@ -61,16 +62,36 @@ export function AppSidebar() {
))}
- {/* Collapse button - desktop only */}
-
+
+
+ Tema
+
+
+ {collapsed && (
+
+
+
+ )}
+
setCollapsed(!collapsed)}
- className="nav-item flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 w-full justify-center lg:justify-start text-muted-foreground hover:text-foreground hover:bg-sidebar-accent"
+ className="nav-item hidden w-full items-center justify-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground transition-all duration-200 hover:bg-sidebar-accent/60 hover:text-foreground lg:flex lg:justify-start"
>
{!collapsed && Recolher }
diff --git a/src/modules/intelligence-ia/pages/Financas.tsx b/src/modules/intelligence-ia/pages/Financas.tsx
index c6556a7..15c61e3 100644
--- a/src/modules/intelligence-ia/pages/Financas.tsx
+++ b/src/modules/intelligence-ia/pages/Financas.tsx
@@ -674,7 +674,7 @@ export default function Financas() {
variant="outline"
className={
expense.tipo === "corporativo"
- ? "bg-secondary/10 text-secondary border-secondary/20"
+ ? "bg-secondary/10 text-secondary border-secondary/20 dark:bg-white/10 dark:text-white dark:border-white/25"
: "bg-warning/10 text-warning border-warning/20"
}
>
diff --git a/src/modules/fechamento-hgtx/App.tsx b/src/modules/intelligence-score/App.tsx
similarity index 75%
rename from src/modules/fechamento-hgtx/App.tsx
rename to src/modules/intelligence-score/App.tsx
index bc89892..052fd54 100644
--- a/src/modules/fechamento-hgtx/App.tsx
+++ b/src/modules/intelligence-score/App.tsx
@@ -1,18 +1,18 @@
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
-import { MainLayout } from "@/modules/fechamento-hgtx/components/layout/MainLayout";
-import DashboardPontos from "@/modules/fechamento-hgtx/pages/DashboardPontos";
-import Fechamentos from "@/modules/fechamento-hgtx/pages/Fechamentos";
-import CompetenciaFechamentos from "@/modules/fechamento-hgtx/pages/CompetenciaFechamentos";
-import FechamentoDetalhes from "@/modules/fechamento-hgtx/pages/FechamentoDetalhes";
-import BancoPontos from "@/modules/fechamento-hgtx/pages/BancoPontos";
-import BancoPontosExtrato from "@/modules/fechamento-hgtx/pages/BancoPontosExtrato";
-import Parceiros from "@/modules/fechamento-hgtx/pages/Parceiros";
-import Usuarios from "@/modules/fechamento-hgtx/pages/Usuarios";
-import Configuracoes from "@/modules/fechamento-hgtx/pages/Configuracoes";
-import MeuPerfil from "@/modules/fechamento-hgtx/pages/MeuPerfil";
-import MeuFechamento from "@/modules/fechamento-hgtx/pages/MeuFechamento";
-import MeuFechamentoBancoPontos from "@/modules/fechamento-hgtx/pages/MeuFechamentoBancoPontos";
-import NotFound from "@/modules/fechamento-hgtx/pages/NotFound";
+import { MainLayout } from "@/modules/intelligence-score/components/layout/MainLayout";
+import DashboardPontos from "@/modules/intelligence-score/pages/DashboardPontos";
+import Fechamentos from "@/modules/intelligence-score/pages/Fechamentos";
+import CompetenciaFechamentos from "@/modules/intelligence-score/pages/CompetenciaFechamentos";
+import FechamentoDetalhes from "@/modules/intelligence-score/pages/FechamentoDetalhes";
+import BancoPontos from "@/modules/intelligence-score/pages/BancoPontos";
+import BancoPontosExtrato from "@/modules/intelligence-score/pages/BancoPontosExtrato";
+import Parceiros from "@/modules/intelligence-score/pages/Parceiros";
+import Usuarios from "@/modules/intelligence-score/pages/Usuarios";
+import Configuracoes from "@/modules/intelligence-score/pages/Configuracoes";
+import MeuPerfil from "@/modules/intelligence-score/pages/MeuPerfil";
+import MeuFechamento from "@/modules/intelligence-score/pages/MeuFechamento";
+import MeuFechamentoBancoPontos from "@/modules/intelligence-score/pages/MeuFechamentoBancoPontos";
+import NotFound from "@/modules/intelligence-score/pages/NotFound";
import { AuthAccessProvider, useAuthAccess } from "@/contexts/AuthAccessContext";
import { AuthGate } from "@/components/auth/AuthGate";
import { UnidadeGate } from "@/components/fechamento/UnidadeGate";
@@ -43,7 +43,7 @@ function RequireFechamentoDetalheRoute({ children }: { children: JSX.Element })
return ;
}
-const FechamentoHgtxApp = () => {
+const IntelligenceScoreApp = () => {
return (
@@ -135,4 +135,5 @@ const FechamentoHgtxApp = () => {
);
};
-export default FechamentoHgtxApp;
+export default IntelligenceScoreApp;
+
diff --git a/src/modules/fechamento-hgtx/components/layout/AppSidebar.tsx b/src/modules/intelligence-score/components/layout/AppSidebar.tsx
similarity index 91%
rename from src/modules/fechamento-hgtx/components/layout/AppSidebar.tsx
rename to src/modules/intelligence-score/components/layout/AppSidebar.tsx
index fe5569a..367e846 100644
--- a/src/modules/fechamento-hgtx/components/layout/AppSidebar.tsx
+++ b/src/modules/intelligence-score/components/layout/AppSidebar.tsx
@@ -16,6 +16,7 @@ import {
X,
} from "lucide-react";
import { Button } from "@/components/ui/button";
+import { ThemeToggle } from "@/components/ThemeToggle";
import { cn } from "@/lib/utils";
import { GlobalFunctions } from "@/GlobalFunctions";
import { useAuthAccess } from "@/contexts/AuthAccessContext";
@@ -113,11 +114,11 @@ export function AppSidebar() {
{!collapsed ? (
<>
-
+
-
HGTX Intelligence Score
+
HGTX Intelligence Score
{painelLabel}
@@ -146,7 +147,7 @@ export function AppSidebar() {
>
-
+
>
@@ -191,7 +192,22 @@ export function AppSidebar() {
})}
-
+
+ {collapsed ? (
+
+
+
+ ) : (
+
+ Tema
+
+
+ )}
);
}
+
diff --git a/src/modules/fechamento-hgtx/pages/BancoPontos.tsx b/src/modules/intelligence-score/pages/BancoPontos.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/BancoPontos.tsx
rename to src/modules/intelligence-score/pages/BancoPontos.tsx
diff --git a/src/modules/fechamento-hgtx/pages/BancoPontosExtrato.tsx b/src/modules/intelligence-score/pages/BancoPontosExtrato.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/BancoPontosExtrato.tsx
rename to src/modules/intelligence-score/pages/BancoPontosExtrato.tsx
diff --git a/src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx b/src/modules/intelligence-score/pages/CompetenciaFechamentos.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/CompetenciaFechamentos.tsx
rename to src/modules/intelligence-score/pages/CompetenciaFechamentos.tsx
diff --git a/src/modules/fechamento-hgtx/pages/Configuracoes.tsx b/src/modules/intelligence-score/pages/Configuracoes.tsx
similarity index 99%
rename from src/modules/fechamento-hgtx/pages/Configuracoes.tsx
rename to src/modules/intelligence-score/pages/Configuracoes.tsx
index 1b27d48..e9d816a 100644
--- a/src/modules/fechamento-hgtx/pages/Configuracoes.tsx
+++ b/src/modules/intelligence-score/pages/Configuracoes.tsx
@@ -432,7 +432,7 @@ export default function Configuracoes() {
) : !codigoEstabelecimento ? (
- Abra o módulo pelo Codex com o estabelecimento no transfer para exibir o código e cadastrar a
+ Abra o módulo pelo Commander com o estabelecimento no transfer para exibir o código e cadastrar a
unidade.
) : (
diff --git a/src/modules/fechamento-hgtx/pages/DashboardPontos.tsx b/src/modules/intelligence-score/pages/DashboardPontos.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/DashboardPontos.tsx
rename to src/modules/intelligence-score/pages/DashboardPontos.tsx
diff --git a/src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx b/src/modules/intelligence-score/pages/FechamentoDetalhes.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/FechamentoDetalhes.tsx
rename to src/modules/intelligence-score/pages/FechamentoDetalhes.tsx
diff --git a/src/modules/fechamento-hgtx/pages/Fechamentos.tsx b/src/modules/intelligence-score/pages/Fechamentos.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/Fechamentos.tsx
rename to src/modules/intelligence-score/pages/Fechamentos.tsx
diff --git a/src/modules/fechamento-hgtx/pages/MeuFechamento.tsx b/src/modules/intelligence-score/pages/MeuFechamento.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/MeuFechamento.tsx
rename to src/modules/intelligence-score/pages/MeuFechamento.tsx
diff --git a/src/modules/fechamento-hgtx/pages/MeuFechamentoBancoPontos.tsx b/src/modules/intelligence-score/pages/MeuFechamentoBancoPontos.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/MeuFechamentoBancoPontos.tsx
rename to src/modules/intelligence-score/pages/MeuFechamentoBancoPontos.tsx
diff --git a/src/modules/fechamento-hgtx/pages/MeuPerfil.tsx b/src/modules/intelligence-score/pages/MeuPerfil.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/MeuPerfil.tsx
rename to src/modules/intelligence-score/pages/MeuPerfil.tsx
diff --git a/src/modules/fechamento-hgtx/pages/NotFound.tsx b/src/modules/intelligence-score/pages/NotFound.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/NotFound.tsx
rename to src/modules/intelligence-score/pages/NotFound.tsx
diff --git a/src/modules/fechamento-hgtx/pages/Parceiros.tsx b/src/modules/intelligence-score/pages/Parceiros.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/Parceiros.tsx
rename to src/modules/intelligence-score/pages/Parceiros.tsx
diff --git a/src/modules/fechamento-hgtx/pages/Usuarios.tsx b/src/modules/intelligence-score/pages/Usuarios.tsx
similarity index 100%
rename from src/modules/fechamento-hgtx/pages/Usuarios.tsx
rename to src/modules/intelligence-score/pages/Usuarios.tsx
diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx
deleted file mode 100644
index 461bc31..0000000
--- a/src/pages/Index.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Layout } from "@/components/Layout";
-import { ChatView } from "@/components/chat/ChatView";
-import { ImageView } from "@/components/images/ImageView";
-import { TranscriptionView } from "@/components/audio/TranscriptionView";
-import { GenerationView } from "@/components/audio/GenerationView";
-import { PromptsView } from "@/components/prompts/PromptsView";
-import { PromptsFormView } from "../components/prompts/PromptsFormView";
-import { AreasView } from "@/components/areas/AreasView";
-import { ParecerJuridicoView } from "@/components/parecer-juridico/ParecerJuridicoView";
-import { ParecerJuridicoFormView } from "@/components/parecer-juridico/ParecerJuridicoFormView";
-import { ParecerJuridicoDetailView } from "@/components/parecer-juridico/ParecerJuridicoDetailView";
-import { PromptsProvider } from "@/contexts/PromptsContext";
-import { Navigate, Route, Routes, useLocation, useNavigate, Outlet } from "react-router-dom";
-import React from "react";
-import { GlobalFunctions } from "@/GlobalFunctions";
-
-type TabType = "chat" | "images" | "transcription" | "generation" | "prompts" | "parecerJuridico" | "areas";
-
-const PATH_TO_TAB: Record = {
- "/codex/bate-papo": "chat",
- "/codex/imagens": "images",
- "/codex/transcricao-audio": "transcription",
- "/codex/geracao-audio": "generation",
- "/codex/parecer-juridico": "parecerJuridico",
- "/codex/areas": "areas",
-};
-
-function getActiveTabFromPath(pathname: string): TabType {
- if (pathname.includes("/prompts")) return "prompts";
- if (pathname.includes("/parecer-juridico")) return "parecerJuridico";
- const tab = PATH_TO_TAB[pathname];
- if (tab) return tab;
- return "chat";
-}
-
-const TAB_TO_PATH: Record = {
- chat: "/codex/bate-papo",
- images: "/codex/imagens",
- transcription: "/codex/transcricao-audio",
- generation: "/codex/geracao-audio",
- prompts: "/codex/prompts",
- parecerJuridico: "/codex/parecer-juridico",
- areas: "/codex/areas",
-};
-
-function CodexLayout() {
- const location = useLocation();
- const navigate = useNavigate();
- const activeTab = getActiveTabFromPath(location.pathname);
-
- const onTabChange = (tab: TabType) => {
- navigate(TAB_TO_PATH[tab]);
- };
-
- return (
-
-
-
- );
-}
-
-const Index = () => {
- React.useEffect(() => {
- if (!GlobalFunctions.isUsuarioLogado()) window.location.replace(import.meta.env.VITE_BASE_URL_HGTX_CORE);
- }, []);
-
- return (
-
- } />
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
- } />
-
- );
-};
-
-export default Index;
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 5a9f6b9..8833bb4 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -25,6 +25,9 @@ export default {
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
+ hover: "hsl(var(--primary-hover))",
+ pressed: "hsl(var(--primary-pressed))",
+ light: "hsl(var(--primary-light))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
@@ -68,11 +71,33 @@ export default {
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
+ "bubble-bot": "hsl(var(--bubble-bot))",
+ "nav-active": "hsl(var(--nav-active))",
+ "input-bg": "hsl(var(--input-bg))",
+ placeholder: "hsl(var(--placeholder))",
+ "btn-secondary-border": "hsl(var(--btn-secondary-border))",
+ "border-subtle": "hsl(var(--border-subtle))",
},
borderRadius: {
- lg: "var(--radius)",
- md: "calc(var(--radius) - 2px)",
- sm: "calc(var(--radius) - 4px)",
+ sm: "var(--radius-sm)",
+ md: "var(--radius-md)",
+ lg: "var(--radius-lg)",
+ xl: "var(--radius-xl)",
+ "2xl": "20px",
+ pill: "var(--radius-pill)",
+ },
+ boxShadow: {
+ card: "var(--shadow-card)",
+ modal: "var(--shadow-modal)",
+ },
+ fontSize: {
+ display: ["32px", { lineHeight: "1.25", fontWeight: "700" }],
+ h1: ["24px", { lineHeight: "1.25", fontWeight: "600" }],
+ h2: ["18px", { lineHeight: "1.5", fontWeight: "600" }],
+ h3: ["15px", { lineHeight: "1.5" }],
+ body: ["14px", { lineHeight: "1.5" }],
+ small: ["12px", { lineHeight: "1.5" }],
+ caption: ["11px", { lineHeight: "1.5" }],
},
keyframes: {
"accordion-down": {
@@ -84,15 +109,25 @@ export default {
to: { height: "0" },
},
"pulse-glow": {
- "0%, 100%": {
- boxShadow: "0 0 20px hsl(190 100% 50% / 0.3)",
+ "0%, 100%": {
+ boxShadow: "0 0 20px hsl(210 100% 33% / 0.3)",
transform: "scale(1)"
},
- "50%": {
- boxShadow: "0 0 40px hsl(190 100% 50% / 0.5)",
+ "50%": {
+ boxShadow: "0 0 40px hsl(210 100% 33% / 0.5)",
transform: "scale(1.02)"
},
},
+ "online-pulse": {
+ "0%, 100%": {
+ boxShadow: "0 0 0 0 hsl(134 61% 41% / 0.7)",
+ opacity: "1",
+ },
+ "50%": {
+ boxShadow: "0 0 0 4px hsl(134 61% 41% / 0)",
+ opacity: "0.85",
+ },
+ },
"slide-in-right": {
"0%": { transform: "translateX(100%)", opacity: "0" },
"100%": { transform: "translateX(0)", opacity: "1" },
@@ -106,6 +141,7 @@ export default {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"pulse-glow": "pulse-glow 2s ease-in-out infinite",
+ "online-pulse": "online-pulse 1.5s ease-in-out infinite",
"slide-in-right": "slide-in-right 0.3s ease-out",
"fade-in": "fade-in 0.3s ease-out",
},