feat: enhance OllaHub with new features and UI improvements
- Introduced a comprehensive README.md detailing the application, its architecture, and key features. - Implemented a local-first architecture with deep research capabilities, including query decomposition and parallel web scraping. - Added a reactive UI built with React and Next.js, featuring Tailwind CSS for styling and Zustand for state management. - Integrated a task scheduler for recurring tasks and system monitoring for real-time resource management. - Enhanced settings page with debug mode options and improved hardware dashboard loading states. - Updated chat interface to include debug data visualization and improved user experience during message streaming. - Removed unused favicon and updated application icons for better branding. - Improved navigation handling with non-blocking transitions and state management for setup completion.
This commit is contained in:
1 parent
55749c07e3
commit
c7da6cb8a8
36 files changed
+1458
-293
No files matched your search
+44
-122
@@ -3,7 +3,7 @@
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown, Plus, ScrollText, Copy, Check, Globe, RefreshCw, Cpu } from "lucide-react";
|
||||
import { MessageSquare, Settings, Server, Moon, Sun, PanelLeftClose, PanelLeftOpen, Loader2, ChevronDown, Plus, RefreshCw, Cpu } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { ChatInput } from "@/components/chat/chat-input";
|
||||
import { ChatMessage } from "@/components/chat/chat-message";
|
||||
@@ -33,6 +33,7 @@ import { chatLog } from "@/lib/terminal-logger";
|
||||
import { useQueryPreprocessor, type PreprocessedQuery } from "@/hooks/use-query-preprocessor";
|
||||
import type { Message, ThinkingMessageMetadata, ThinkingStepType, ThinkingStepStatus } from "@/hooks/use-chat";
|
||||
import { executeProgressiveSearch } from "@/lib/web-search-fallback";
|
||||
import type { DebugData } from "@/components/chat/debug-console";
|
||||
// @ts-expect-error - MD file import
|
||||
import defaultFormatPrompt from "@/data/prompts/default-format.md";
|
||||
|
||||
@@ -125,14 +126,9 @@ export default function ChatPage() {
|
||||
const lastSavedMessagesRef = useRef<number>(0);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [showContextDebug, setShowContextDebug] = useState(false);
|
||||
const [showProcessDebug, setShowProcessDebug] = useState(false);
|
||||
const [lastWebContext, setLastWebContext] = useState('');
|
||||
const [lastContextSources, setLastContextSources] = useState<ScrapedContent[]>([]);
|
||||
const [showDownloadDialog, setShowDownloadDialog] = useState(false);
|
||||
const [logsCopied, setLogsCopied] = useState(false);
|
||||
const [lastUserQuery, setLastUserQuery] = useState('');
|
||||
const [searchMatchIndex, setSearchMatchIndex] = useState(0);
|
||||
const [debugDataMap, setDebugDataMap] = useState<Map<number, DebugData>>(new Map());
|
||||
|
||||
// Resetar índice quando busca é limpa ou sessão muda
|
||||
useEffect(() => {
|
||||
@@ -369,13 +365,6 @@ export default function ChatPage() {
|
||||
chatLog.info(`Should Search: ${preprocessed.shouldSearch}`);
|
||||
chatLog.info(`Timestamp: ${new Date().toISOString()}`);
|
||||
|
||||
setShowContextDebug(false);
|
||||
setShowProcessDebug(false);
|
||||
setLastWebContext('');
|
||||
setLastContextSources([]);
|
||||
setLastUserQuery(content); // Store for copy logs
|
||||
setLogsCopied(false);
|
||||
|
||||
// Generate ID if new session (but don't create card yet - wait for title)
|
||||
if (!currentSessionId) {
|
||||
const newId = crypto.randomUUID();
|
||||
@@ -598,8 +587,6 @@ export default function ChatPage() {
|
||||
chatLog.warn('⚠️ Knowledge Base Context is empty!');
|
||||
}
|
||||
|
||||
setLastWebContext(knowledgeBaseContext);
|
||||
setLastContextSources(scrapedSources);
|
||||
|
||||
// Atualizar mensagem: Processamento concluído
|
||||
updateThinkingMessage('processing', {
|
||||
@@ -627,8 +614,6 @@ export default function ChatPage() {
|
||||
deepResearch.addToKnowledgeBase(simpleQuery, results);
|
||||
knowledgeBaseContext = deepResearch.getCuratedContext();
|
||||
scrapedSources = results;
|
||||
setLastWebContext(knowledgeBaseContext);
|
||||
setLastContextSources(scrapedSources);
|
||||
}
|
||||
} else {
|
||||
chatLog.info('Simple query returned NO_SEARCH, skipping web search');
|
||||
@@ -837,6 +822,46 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
|
||||
chatLog.info('✅ Response generation complete');
|
||||
chatLog.info('========== QUERY COMPLETE ==========\n');
|
||||
|
||||
// Construir dados de debug para a última mensagem do assistente
|
||||
setMessages(prev => {
|
||||
const newMessages = [...prev];
|
||||
const lastMsgIndex = newMessages.length - 1;
|
||||
if (lastMsgIndex >= 0 && newMessages[lastMsgIndex].role === 'assistant') {
|
||||
const lastMsg = newMessages[lastMsgIndex];
|
||||
const debugData: DebugData = {
|
||||
model: selectedModel,
|
||||
timestamp: Date.now(),
|
||||
latency: Date.now() - responseStart,
|
||||
systemPrompt: enhancedSystemPrompt,
|
||||
userQuery: content,
|
||||
contextUsed: messages.filter(m => m.role !== 'system'),
|
||||
webResearch: {
|
||||
queries: deepResearch.state.plan || [],
|
||||
enrichedQueries: deepResearch.state.enrichedQueries || undefined,
|
||||
sources: scrapedSources,
|
||||
logs: deepResearch.state.logs,
|
||||
plan: deepResearch.state.plan,
|
||||
knowledgeBase: deepResearch.state.knowledgeBase.map(entry => ({
|
||||
sourceUrl: entry.sourceUrl,
|
||||
title: entry.title,
|
||||
content: entry.content,
|
||||
})),
|
||||
},
|
||||
deepResearchState: deepResearch.state,
|
||||
finalResponse: lastMsg.content,
|
||||
rawResponse: lastMsg.content,
|
||||
};
|
||||
|
||||
// Armazenar debug data usando o índice da mensagem
|
||||
setDebugDataMap(prevMap => {
|
||||
const newMap = new Map(prevMap);
|
||||
newMap.set(lastMsgIndex, debugData);
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
|
||||
// Persistir fontes na mensagem do assistente (metadata)
|
||||
if (scrapedSources.length > 0) {
|
||||
// Aguardar um pouco para garantir que o sendMessage iniciou e criou a mensagem do assistente
|
||||
@@ -998,45 +1023,6 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
|
||||
}
|
||||
};
|
||||
|
||||
// Copy Logs to Clipboard
|
||||
const handleCopyLogs = async () => {
|
||||
const logs = deepResearch.state.logs;
|
||||
const lastAssistantMessage = messages.filter(m => m.role === 'assistant').pop();
|
||||
|
||||
let logText = `# Deep Research Debug Log\n\n`;
|
||||
logText += `## User Query\n${lastUserQuery}\n\n`;
|
||||
logText += `## Model\n${selectedModel}\n\n`;
|
||||
logText += `## Process Logs\n\n`;
|
||||
|
||||
logs.forEach((log, idx) => {
|
||||
logText += `### ${idx + 1}. Stage: ${log.stage.toUpperCase()}\n`;
|
||||
logText += `- **Time**: ${new Date(log.timestamp).toLocaleTimeString()}\n`;
|
||||
logText += `- **Input**: ${log.input.substring(0, 500)}${log.input.length > 500 ? '...' : ''}\n`;
|
||||
if (log.rawOutput) {
|
||||
logText += `- **Raw Output**: ${log.rawOutput.substring(0, 500)}${log.rawOutput.length > 500 ? '...' : ''}\n`;
|
||||
}
|
||||
if (log.parsedOutput) {
|
||||
logText += `- **Parsed Output**: \`\`\`json\n${JSON.stringify(log.parsedOutput, null, 2)}\n\`\`\`\n`;
|
||||
}
|
||||
if (log.error) {
|
||||
logText += `- **Error**: ${log.error}\n`;
|
||||
}
|
||||
logText += '\n';
|
||||
});
|
||||
|
||||
logText += `## AI Response\n`;
|
||||
logText += lastAssistantMessage?.content || '[No response yet]';
|
||||
logText += '\n\n---\n';
|
||||
logText += `Generated at: ${new Date().toISOString()}`;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(logText);
|
||||
setLogsCopied(true);
|
||||
setTimeout(() => setLogsCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy logs:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full bg-background overflow-hidden flex flex-col overflow-x-hidden">
|
||||
@@ -1380,6 +1366,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
|
||||
highlightTerm={searchQuery && searchQuery.trim().length >= 2 ? searchQuery.trim() : undefined}
|
||||
highlightIndex={messageMatchIndex}
|
||||
messageIndex={group.index}
|
||||
debugData={group.index !== undefined ? debugDataMap.get(group.index) : undefined}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -1390,71 +1377,6 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Debug Info (Moved inside scroll) */}
|
||||
{(deepResearch.state.logs.length > 0 || lastWebContext) && (
|
||||
<div className="mt-12 pt-6 border-t border-dashed border-muted/50 opacity-70 hover:opacity-100 transition-opacity">
|
||||
<div className="space-y-4">
|
||||
{/* Process Log Toggle */}
|
||||
{deepResearch.state.logs.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowProcessDebug((prev) => !prev)}
|
||||
className="flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
<ScrollText className="w-3 h-3" />
|
||||
Debug: Processo de Pensamento
|
||||
<ChevronDown className={`w-3 h-3 transition-transform ${showProcessDebug ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{showProcessDebug && (
|
||||
<div className="mt-2 space-y-2 p-4 rounded-lg bg-muted/30 text-xs border border-muted/50 max-h-60 overflow-y-auto">
|
||||
<div className="flex justify-end mb-2">
|
||||
<Button variant="ghost" size="sm" onClick={handleCopyLogs} className="h-6 px-2 text-[10px]">
|
||||
{logsCopied ? <Check className="w-3 h-3 mr-1" /> : <Copy className="w-3 h-3 mr-1" />} Copiar Logs
|
||||
</Button>
|
||||
</div>
|
||||
{deepResearch.state.logs.map((log, idx) => (
|
||||
<div key={idx} className="grid grid-cols-[60px_1fr] gap-2 border-b border-muted/20 pb-2 last:border-0">
|
||||
<span className="text-muted-foreground uppercase text-[10px]">{log.stage}</span>
|
||||
<span>{log.input ? (log.input.length > 50 ? log.input.substring(0, 50) + '...' : log.input) : '-'}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context Debug Toggle */}
|
||||
{lastWebContext && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowContextDebug((prev) => !prev)}
|
||||
className="flex items-center gap-2 text-xs font-medium text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
<Globe className="w-3 h-3" />
|
||||
Debug: Contexto Web ({lastContextSources.length} fontes)
|
||||
<ChevronDown className={`w-3 h-3 transition-transform ${showContextDebug ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{showContextDebug && (
|
||||
<div className="mt-2 p-4 rounded-lg bg-muted/30 text-xs border border-muted/50 space-y-3">
|
||||
<div className="max-h-32 overflow-y-auto space-y-1">
|
||||
{lastContextSources.map((s, i) => (
|
||||
<a key={i} href={s.url} target="_blank" className="block hover:underline text-primary truncate">{s.title || s.url}</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t border-muted/20 pt-2">
|
||||
<p className="mb-1 font-semibold">Preview Contexto:</p>
|
||||
<pre className="max-h-32 overflow-y-auto p-2 bg-background rounded border text-[10px]">{lastWebContext.substring(0, 500)}...</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Spacer for Bottom Input */}
|
||||
<div className="h-32 md:h-40 w-full shrink-0" ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { AppUpdaterProvider } from "@/components/app-updater-provider";
|
||||
import { KeyboardShortcuts } from "@/components/keyboard-shortcuts";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -17,6 +18,10 @@ const geistMono = Geist_Mono({
|
||||
export const metadata: Metadata = {
|
||||
title: "OllaHub",
|
||||
description: "Interface moderna para Ollama",
|
||||
icons: {
|
||||
icon: "/icon.png",
|
||||
apple: "/icon.png",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -35,6 +40,7 @@ export default function RootLayout({
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<KeyboardShortcuts />
|
||||
{children}
|
||||
<AppUpdaterProvider />
|
||||
</ThemeProvider>
|
||||
|
||||
+53
-30
@@ -1,53 +1,63 @@
|
||||
'use client';
|
||||
|
||||
import { useOllamaCheck } from "@/hooks/use-ollama-check";
|
||||
import { useSetupCheck } from "@/hooks/use-setup-check";
|
||||
import { Hero } from "@/components/landing/hero";
|
||||
import { InstallModal } from "@/components/modals/install-modal";
|
||||
import { StoppedCard } from "@/components/landing/stopped-card";
|
||||
import { WelcomeNotification } from "@/components/notifications/welcome-notification";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, startTransition } from "react";
|
||||
|
||||
export default function Home() {
|
||||
const { status, check } = useOllamaCheck();
|
||||
const { status, ollamaStatus, checkingStep, recheck } = useSetupCheck();
|
||||
const router = useRouter();
|
||||
|
||||
// Tentar iniciar Ollama automaticamente quando o componente montar
|
||||
// Redirecionar automaticamente se tudo estiver pronto
|
||||
useEffect(() => {
|
||||
const tryAutoStart = async () => {
|
||||
try {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
await invoke('auto_start_ollama');
|
||||
// Aguardar um pouco e verificar novamente
|
||||
setTimeout(() => {
|
||||
check();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Erro ao tentar iniciar Ollama automaticamente:', error);
|
||||
if (status === 'ready') {
|
||||
const setupComplete = localStorage.getItem("ollahub_setup_complete");
|
||||
|
||||
// Se setup não foi marcado como completo, marcar agora
|
||||
if (!setupComplete) {
|
||||
localStorage.setItem("ollahub_setup_complete", "true");
|
||||
}
|
||||
};
|
||||
|
||||
// Só tentar se estiver instalado mas não rodando
|
||||
if (status === 'installed_stopped') {
|
||||
tryAutoStart();
|
||||
|
||||
// Redirecionar para chat após um breve delay para mostrar feedback
|
||||
const timer = setTimeout(() => {
|
||||
startTransition(() => {
|
||||
router.push("/chat");
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [status, check]);
|
||||
}, [status, router]);
|
||||
|
||||
const handleStart = () => {
|
||||
const setupComplete = localStorage.getItem("ollahub_setup_complete");
|
||||
if (setupComplete) {
|
||||
router.push("/chat");
|
||||
startTransition(() => {
|
||||
router.push("/chat");
|
||||
});
|
||||
} else {
|
||||
router.push("/setup");
|
||||
startTransition(() => {
|
||||
router.push("/setup");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Mostrar loading durante verificação
|
||||
if (status === 'checking') {
|
||||
return (
|
||||
<div className="flex h-screen w-full items-center justify-center bg-background">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<span className="ml-2 text-muted-foreground">Verificando sistema...</span>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-sm font-medium text-foreground">{checkingStep}</span>
|
||||
<span className="text-xs text-muted-foreground">Aguarde um momento...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,24 +67,37 @@ export default function Home() {
|
||||
{/* Notificação de boas-vindas */}
|
||||
<WelcomeNotification />
|
||||
|
||||
{/* Layout for Running State */}
|
||||
{status === 'running' && (
|
||||
{/* Layout for Ready State - Redirecionamento automático */}
|
||||
{status === 'ready' && (
|
||||
<div className="flex h-screen w-full items-center justify-center bg-background">
|
||||
<div className="flex flex-col items-center gap-4 animate-in fade-in duration-300">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<span className="text-sm font-medium text-foreground">Tudo pronto!</span>
|
||||
<span className="text-xs text-muted-foreground">Redirecionando para o chat...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Layout for Needs Setup State */}
|
||||
{status === 'needs_setup' && ollamaStatus === 'running' && (
|
||||
<Hero onStart={handleStart} />
|
||||
)}
|
||||
|
||||
{/* Layout for Stopped State */}
|
||||
{status === 'installed_stopped' && (
|
||||
{status === 'needs_setup' && ollamaStatus === 'installed_stopped' && (
|
||||
<div className="flex flex-col items-center gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tighter">OllaHub</h1>
|
||||
<p className="text-muted-foreground">Detectamos o Ollama, mas ele não está rodando.</p>
|
||||
</div>
|
||||
<StoppedCard onCheckAgain={check} />
|
||||
<StoppedCard onCheckAgain={recheck} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Layout for Not Installed State (Background + Modal) */}
|
||||
{status === 'not_installed' && (
|
||||
{status === 'needs_ollama' && (
|
||||
<>
|
||||
<div className="opacity-20 pointer-events-none blur-sm">
|
||||
<Hero onStart={() => {}} />
|
||||
@@ -82,7 +105,7 @@ export default function Home() {
|
||||
{/* Hero is shown in background, modal is open */}
|
||||
<InstallModal
|
||||
open={true}
|
||||
onCheckAgain={check}
|
||||
onCheckAgain={recheck}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
+60
-4
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, lazy, Suspense } from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { useSettingsStore } from '@/store/settings-store';
|
||||
import { DomainTagsInput } from '@/components/settings/domain-tags-input';
|
||||
import { HardwareDashboard } from '@/components/settings/HardwareDashboard';
|
||||
const HardwareDashboard = lazy(() => import('@/components/settings/HardwareDashboard').then(m => ({ default: m.HardwareDashboard })));
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { CheckCircle2, XCircle, Loader2, Download, Trash2, Copy, ExternalLink, Plus, X, BookOpen, GraduationCap, Newspaper, Code, DollarSign, Edit, RotateCcw, Terminal, Power, ArrowLeft, Sparkles, RefreshCw } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
@@ -55,6 +55,7 @@ export default function SettingsPage() {
|
||||
const [recentLogs, setRecentLogs] = useState<string[]>([]);
|
||||
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
|
||||
const [appVersion, setAppVersion] = useState<string>('');
|
||||
const [activeTab, setActiveTab] = useState<string>('overview');
|
||||
|
||||
const updater = useAppUpdater();
|
||||
|
||||
@@ -252,7 +253,7 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="overview" className="space-y-4 sm:space-y-6">
|
||||
<Tabs defaultValue="overview" value={activeTab} onValueChange={setActiveTab} className="space-y-4 sm:space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-1 sm:gap-2 h-auto">
|
||||
<TabsTrigger value="overview" className="text-xs sm:text-sm px-2 sm:px-3 py-1.5 sm:py-2 truncate">
|
||||
Visão Geral
|
||||
@@ -284,7 +285,30 @@ export default function SettingsPage() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<HardwareDashboard />
|
||||
{activeTab === 'overview' && (
|
||||
<Suspense fallback={
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-4 w-16 bg-muted animate-pulse rounded" />
|
||||
<div className="h-5 w-12 bg-muted animate-pulse rounded" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="h-2 w-full bg-muted animate-pulse rounded" />
|
||||
<div className="h-3 w-24 bg-muted animate-pulse rounded" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}>
|
||||
<HardwareDashboard />
|
||||
</Suspense>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
@@ -1106,6 +1130,38 @@ export default function SettingsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Debug & Desenvolvimento</CardTitle>
|
||||
<CardDescription>
|
||||
Ferramentas de debug e introspecção técnica
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="debug-mode">Modo Debug (God-Mode)</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Exibe console completo de introspecção técnica para cada resposta da IA
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="debug-mode"
|
||||
checked={settings.debugMode}
|
||||
onCheckedChange={settings.setDebugMode}
|
||||
/>
|
||||
</div>
|
||||
{settings.debugMode && (
|
||||
<div className="p-3 bg-muted/50 rounded-lg border border-border/50">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
O console de debug mostrará: System Prompt, Histórico da Sessão, Deep Research Pipeline,
|
||||
Dados Processados e Resposta Raw para cada mensagem do assistente.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Atualizações</CardTitle>
|
||||
|
||||
+8
-1
@@ -6,15 +6,22 @@ import { GpuSelector } from "@/components/setup/gpu-selector";
|
||||
import { ModelSelector } from "@/components/setup/model-selector";
|
||||
import { getRecommendation } from "@/lib/recommendation";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { startTransition, useState } from "react";
|
||||
|
||||
export default function SetupPage() {
|
||||
const { specs, loading, gpus } = useHardware();
|
||||
const router = useRouter();
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
|
||||
const handleComplete = () => {
|
||||
setIsNavigating(true);
|
||||
// Save setup state if needed
|
||||
localStorage.setItem("ollahub_setup_complete", "true");
|
||||
router.push("/chat");
|
||||
|
||||
// Usar startTransition para navegação não-bloqueante
|
||||
startTransition(() => {
|
||||
router.push("/chat");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user