feat(ui): enhance chat UI with new components and styling

refactor(chat): improve message display and actions
feat(chat): add high contrast mode toggle
style(chat): update spacing and sizing for better readability
feat(chat): add new Section and Stack layout components
docs: add UI style guide documentation
fix(chat): improve browser connection handling in scraper
feat(settings): expand default sources and improve UI
refactor: consolidate chat page layout and components
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-20 15:13:54 -03:00
1 parent c06e7ae5cf
commit 4006a72eec
20 files changed
+741 -235

No files matched your search

+64 -87
View File
@@ -1,17 +1,14 @@
'use client';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown } from "lucide-react";
import { ChatInput } from "@/components/chat/chat-input";
import { ChatMessage } from "@/components/chat/chat-message";
import { SystemPanel } from "@/components/chat/system-panel";
import { PromptGeneratorDialog } from "@/components/chat/prompt-generator";
import { SidebarList } from "@/components/chat/sidebar-list";
import { useChat } from "@/hooks/use-chat";
import { useLocalModels } from "@/hooks/use-local-models";
@@ -36,7 +33,7 @@ import defaultFormatPrompt from "@/data/prompts/default-format.md";
export default function ChatPage() {
const router = useRouter();
const { messages, setMessages, sendMessage, isLoading, stop, clearChat } = useChat();
const { models } = useLocalModels();
const { models, refresh } = useLocalModels();
const { theme, setTheme } = useTheme();
const {
@@ -56,6 +53,8 @@ export default function ChatPage() {
const settings = useSettingsStore();
const [selectedModel, setSelectedModel] = useState("");
const [mounted, setMounted] = useState(false);
const initializedRef = useRef(false);
// Initialize with default format prompt
const [systemPrompt, setSystemPrompt] = useState(defaultFormatPrompt || "Você é um assistente útil e prestativo.");
const [isChatsSidebarCollapsed, setIsChatsSidebarCollapsed] = useState(false);
@@ -71,10 +70,22 @@ export default function ChatPage() {
// Auto-select first model
useEffect(() => {
if (models.length > 0 && !selectedModel) {
setSelectedModel(models[0].name);
setMounted(true);
}, []);
useEffect(() => {
if (initializedRef.current) return;
const stored = settings.selectedModel;
const first = models.length > 0 ? models[0].name : "";
const next = stored || first || "";
if (next) {
setSelectedModel(next);
if (!stored) {
settings.setSelectedModel(next);
}
}
}, [models, selectedModel]);
initializedRef.current = true;
}, [settings.selectedModel, models]);
// Auto-scroll para o final durante streaming
useEffect(() => {
@@ -372,7 +383,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="rounded-lg text-muted-foreground hover:text-foreground"
>
{theme === "dark" ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
{mounted ? (theme === "dark" ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />) : null}
</Button>
<Button
variant="ghost"
@@ -436,17 +447,27 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
</div>
</div>
<div className="flex-1 max-w-[300px]">
<Select value={selectedModel} onValueChange={setSelectedModel}>
<SelectTrigger className="h-9">
<SelectValue placeholder="Selecione um modelo..." />
</SelectTrigger>
<SelectContent>
{models.map(m => (
<SelectItem key={m.name} value={m.name}>{m.name}</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex-1 max-w-[420px] flex items-center gap-2">
<div className="flex-1">
<Select value={selectedModel} onValueChange={(v) => { setSelectedModel(v); settings.setSelectedModel(v); }}>
<SelectTrigger className="h-9">
<SelectValue placeholder="Selecione um modelo..." />
</SelectTrigger>
<SelectContent>
{models.map(m => (
<SelectItem key={m.name} value={m.name}>
<span className="flex items-center justify-between w-full">
<span className="truncate">{m.name}</span>
<span className="ml-3 text-xs text-muted-foreground">{m.size}</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button variant="outline" size="icon" className="h-9 w-9" onClick={refresh} title="Atualizar modelos">
<Loader2 className="w-4 h-4" />
</Button>
</div>
</div>
@@ -460,7 +481,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
<p>Inicie uma conversa com {selectedModel}</p>
</div>
) : (
<div className="flex flex-col pb-4 max-w-4xl mx-auto w-full">
<div className="flex flex-col pb-4 max-w-3xl mx-auto w-full space-y-6">
{messages.map((msg, i) => {
// Verificar se a mensagem anterior teve busca web
const hasWebSources = i > 0 &&
@@ -471,6 +492,19 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
return (
<div key={i}>
<ChatMessage message={msg} />
{(msg.role === 'user') && (
(webSearch.status === 'searching' ||
webSearch.status === 'scraping' ||
webSearch.status === 'completed' ||
webSearch.status === 'error') && (
<SearchProgress
status={webSearch.status}
query={webSearch.currentQuery}
sources={webSearch.scrapedSources}
error={webSearch.error}
/>
)
)}
{/* Mostrar fontes após resposta do assistente que usou web search */}
{msg.role === 'assistant' && hasWebSources && (
<div className="px-6 pb-4">
@@ -507,18 +541,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
</div>
);
})}
{/* Web Search Progress - mostrar antes da resposta da IA */}
{(webSearch.status === 'searching' ||
webSearch.status === 'scraping' ||
webSearch.status === 'completed' ||
webSearch.status === 'error') && (
<SearchProgress
status={webSearch.status}
query={webSearch.currentQuery}
sources={webSearch.scrapedSources}
error={webSearch.error}
/>
)}
{isLoading && thinkingStep === 'complete' && (
<div className="px-6 py-4 text-xs text-muted-foreground animate-pulse">
Gerando resposta...
@@ -530,8 +553,8 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
<div ref={messagesEndRef} className="h-1" />
</div>
{/* Thinking Indicator */}
{thinkingStep && thinkingStep !== 'complete' && (
{/* Thinking Indicator - aparece apenas quando não há web search ativo */}
{thinkingStep && thinkingStep !== 'complete' && (webSearch.status === 'idle') && (
<ThinkingIndicator
currentStep={thinkingStep}
searchQuery={webSearch.currentQuery}
@@ -593,62 +616,16 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
isLoading={isLoading}
webSearchEnabled={webSearch.isEnabled}
onWebSearchToggle={webSearch.setEnabled}
categories={settings.webSearch.categories}
onToggleCategory={(id, enabled) => {
const cat = settings.webSearch.categories.find(c => c.id === id);
if (!cat) return;
settings.updateCategory({ ...cat, enabled });
}}
/>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
{/* Right Sidebar (Settings & Monitor) */}
<ResizablePanel defaultSize={20} minSize={15} maxSize={30} className="bg-muted/10">
<Tabs defaultValue="params" className="h-full flex flex-col overflow-hidden">
<div className="border-b px-4">
<TabsList className="w-full justify-start h-12 bg-transparent p-0 gap-4">
<TabsTrigger
value="params"
className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none px-0"
>
Parâmetros
</TabsTrigger>
<TabsTrigger
value="system"
className="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none px-0"
>
Sistema
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="params" className="flex-1 p-4 space-y-6 overflow-y-auto overflow-x-hidden m-0">
<div className="space-y-6">
{/* System Prompt */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">System Prompt</label>
<PromptGeneratorDialog
defaultModel={selectedModel}
onPromptGenerated={setSystemPrompt}
/>
</div>
<Textarea
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
className="min-h-[300px] resize-none font-mono text-sm"
placeholder="Defina como a IA deve se comportar..."
/>
<p className="text-xs text-muted-foreground">
Instruções globais para o comportamento do modelo.
</p>
</div>
</div>
</TabsContent>
<TabsContent value="system" className="flex-1 m-0 overflow-y-auto overflow-x-hidden">
<SystemPanel />
</TabsContent>
</Tabs>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);