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.
No files matched your search
@@ -0,0 +1,20 @@
|
||||
OllaHub Community License (v1.0)
|
||||
Copyright (c) 2024 Evandro Fonseca Junior (evandrodevbr) Website: https://evandro.dev.br
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. GRANT OF LICENSE Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, distribute, and sublicense copies of the Software, subject strictly to the conditions and limitations set forth below.
|
||||
|
||||
2. ATTRIBUTION REQUIREMENT (CREDITWARE) This license explicitly requires correct attribution as a condition of use. a) Source Code & Distribution: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. b) Public Display: Any redistribution, fork, derivative work, video content, article, social media post, or public demonstration involving this Software MUST include visible and clear credits to the original author (Evandro Fonseca Junior) and a direct link to https://evandro.dev.br. c) UI Integrity: Removal, obscuring, or modification of copyright notices, author links, metadata, or "About" screens within the Software's user interface is strictly prohibited.
|
||||
|
||||
3. NON-COMMERCIAL & ANTI-MONETIZATION RESTRICTIONS This license distinguishes between "Internal Use" and "Commercial Distribution." a) Prohibition of Sale: You may NOT sell, rent, lease, or sub-license the Software for a fee. You may not bundle this Software within a paid product or service without express written permission from the Copyright Holder. b) No Monetization Barriers: You may NOT use the Software as a mechanism for monetization, including but not limited to "freemium" models, paywalls, or "pay-for-speed" download gates. c) Permitted Internal Use: The use of this Software internally within a commercial organization for productivity, research, or development purposes (e.g., employees using the tool to perform their daily tasks) is EXPLICITLY PERMITTED and encouraged, provided the Software itself is not the product being sold.
|
||||
|
||||
4. DONATION POLICY The Software is developed and maintained through voluntary contributions. a) Official Channels Only: No third-party entity is authorized to solicit or collect donations on behalf of OllaHub or its author. b) Direct Support: All financial contributions must be directed exclusively to the official channels listed at https://evandro.dev.br. Soliciting donations for a fork or redistribution of this Software without significant and distinct added value is prohibited.
|
||||
|
||||
5. DISCLAIMER OF WARRANTY & LIABILITY THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
6. SPECIFIC DISCLAIMER: AI & WEB SCRAPING The Software interacts with Large Language Models (LLMs) and performs web scraping operations. By using this Software, you acknowledge and agree that: a) The authors are not responsible for the accuracy, hallucinations, or bias of content generated by LLMs. b) You are solely responsible for ensuring that your use of the web scraping features complies with the Terms of Service (ToS) of third-party websites and applicable local laws. c) You are responsible for maintaining data privacy and security when processing sensitive information.
|
||||
|
||||
7. COMMERCIAL LICENSING For usage scenarios that fall outside these terms (e.g., white-labeling, redistribution in paid suites, or custom enterprise agreements), please contact the author for a commercial license:
|
||||
|
||||
Website: https://evandro.dev.br
|
||||
@@ -1,36 +1,358 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# OllaHub
|
||||
|
||||
Local-first LLM workspace with deep research capabilities and native OS integration.
|
||||
|
||||
[](https://www.rust-lang.org/)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](LICENSE)
|
||||
[](https://github.com/evandrodevbr/OllaHub)
|
||||
|
||||
## Overview
|
||||
|
||||
OllaHub is a desktop application that orchestrates local Large Language Models (LLMs) through Ollama, providing a private, offline-capable workspace for research, analysis, and conversation. The application executes complex research workflows by decomposing queries, performing parallel web scraping, and synthesizing context from multiple sources before generating responses.
|
||||
|
||||
The architecture prioritizes data privacy by keeping all processing local, using SQLite with full-text search for efficient storage, and integrating ONNX Runtime for semantic relevance ranking without external API dependencies.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Deep Research Pipeline
|
||||
|
||||
The application implements a multi-stage research workflow:
|
||||
|
||||
1. **Query Decomposition**: Uses LLM to break complex queries into 3-5 atomic, search-optimized sub-queries
|
||||
2. **Parallel Web Scraping**: Executes multiple searches concurrently using headless Chrome and DuckDuckGo metadata API
|
||||
3. **Knowledge Base Aggregation**: Collects and deduplicates content from multiple sources
|
||||
4. **Context Condensation**: Applies relevance scoring and summarization to fit context within token limits
|
||||
5. **Validation**: Verifies knowledge base sufficiency before response generation
|
||||
|
||||
The pipeline includes safety checks to ensure decomposed queries remain relevant to the original intent, with automatic fallback to the original query if decomposition fails.
|
||||
|
||||
### Local-First Architecture
|
||||
|
||||
- **Ollama Integration**: Direct subprocess management of Ollama for model execution
|
||||
- **ONNX Embeddings**: Local semantic similarity using all-MiniLM-L6-v2 (384-dimensional embeddings)
|
||||
- **SQLite with FTS5**: Full-text search with virtual tables for session titles and message content
|
||||
- **Hybrid Storage**: Dual persistence (SQLite + JSON) with automatic migration and fallback
|
||||
|
||||
### Hybrid Storage System
|
||||
|
||||
The application uses SQLite as the primary storage backend with JSON as a fallback:
|
||||
|
||||
- **SQLite (Primary)**: WAL mode for concurrent reads/writes, optimized cache settings, FTS5 virtual tables for search
|
||||
- **JSON (Legacy)**: Maintained for backward compatibility and export functionality
|
||||
- **Automatic Migration**: Seamless transition from JSON to SQLite with data preservation
|
||||
|
||||
### Reactive UI
|
||||
|
||||
Built with React 19 and Next.js 16, the interface uses:
|
||||
|
||||
- **Tailwind CSS**: Utility-first styling with design tokens
|
||||
- **Radix UI / Shadcn**: Accessible, headless component primitives
|
||||
- **Zustand**: Lightweight state management for settings and UI state
|
||||
- **Framer Motion**: Smooth animations for state transitions
|
||||
- **Resizable Panels**: Customizable layout with persistent preferences
|
||||
|
||||
### MCP (Model Context Protocol) Integration
|
||||
|
||||
Supports external tool integration through MCP servers:
|
||||
|
||||
- **JSON-RPC Communication**: Bidirectional communication with MCP servers via stdin/stdout
|
||||
- **Tool Discovery**: Automatic enumeration of available tools from running servers
|
||||
- **Process Management**: Lifecycle management (start, stop, restart) with health monitoring
|
||||
- **Tool Execution**: Runtime invocation of MCP tools with result injection into LLM context
|
||||
|
||||
### System Monitoring
|
||||
|
||||
Real-time hardware monitoring and resource management:
|
||||
|
||||
- **GPU Detection**: NVIDIA and AMD GPU identification with utilization tracking
|
||||
- **CPU/Memory Stats**: System-wide resource usage via `sysinfo` crate
|
||||
- **Model Recommendations**: Hardware-aware model suggestions based on available VRAM/RAM
|
||||
|
||||
### Task Scheduler
|
||||
|
||||
Cron-based task execution system:
|
||||
|
||||
- **Recurring Tasks**: Schedule periodic operations (e.g., knowledge base updates)
|
||||
- **Task Queue**: Priority-based execution with retry logic
|
||||
- **State Persistence**: Task state survives application restarts
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Frontend["Frontend (Next.js)"]
|
||||
UI[React Components]
|
||||
Hooks[Custom Hooks]
|
||||
Store[Zustand Store]
|
||||
end
|
||||
|
||||
subgraph Bridge["Tauri Bridge"]
|
||||
Commands[Tauri Commands]
|
||||
Events[Event System]
|
||||
end
|
||||
|
||||
subgraph Backend["Rust Core"]
|
||||
Orchestrator[Orchestrator]
|
||||
Scraper[Web Scraper]
|
||||
DB[(SQLite + FTS5)]
|
||||
Embeddings[ONNX Embeddings]
|
||||
Scheduler[Task Scheduler]
|
||||
MCP[MCP Manager]
|
||||
end
|
||||
|
||||
subgraph External["External Services"]
|
||||
Ollama[Ollama Subprocess]
|
||||
Chrome[Headless Chrome]
|
||||
MCP_Servers[MCP Servers]
|
||||
end
|
||||
|
||||
UI --> Hooks
|
||||
Hooks --> Store
|
||||
Store --> Commands
|
||||
Commands --> Orchestrator
|
||||
Orchestrator --> Scraper
|
||||
Orchestrator --> DB
|
||||
Orchestrator --> Embeddings
|
||||
Orchestrator --> Scheduler
|
||||
Orchestrator --> MCP
|
||||
Scraper --> Chrome
|
||||
Orchestrator --> Ollama
|
||||
MCP --> MCP_Servers
|
||||
Events --> UI
|
||||
|
||||
style Frontend fill:#2563eb,color:#fff
|
||||
style Bridge fill:#7c3aed,color:#fff
|
||||
style Backend fill:#dc2626,color:#fff
|
||||
style External fill:#059669,color:#fff
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **User Query** → Frontend captures input and triggers preprocessing
|
||||
2. **Query Preprocessing** → Intent classification, entity extraction, query enrichment
|
||||
3. **Deep Research** → Query decomposition → Parallel search → Scraping → Knowledge base aggregation
|
||||
4. **Context Condensation** → Relevance scoring (ONNX embeddings) → Summarization → Token optimization
|
||||
5. **LLM Generation** → Ollama subprocess with streaming response
|
||||
6. **Storage** → SQLite persistence with FTS5 indexing for future retrieval
|
||||
|
||||
## Tech Stack
|
||||
|
||||
### Core
|
||||
|
||||
| Layer | Technology | Version | Purpose |
|
||||
|-------|-----------|---------|---------|
|
||||
| **Desktop Framework** | Tauri | 2.9.2 | Native desktop app with Rust backend |
|
||||
| **Backend Language** | Rust | 1.77.2 | System-level operations, performance-critical paths |
|
||||
| **Frontend Framework** | Next.js | 16.0.3 | React-based UI with SSR capabilities |
|
||||
| **UI Library** | React | 19.2.0 | Component-based interface |
|
||||
| **Language** | TypeScript | 5.0 | Type-safe frontend development |
|
||||
|
||||
### Frontend
|
||||
|
||||
| Category | Technology | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| **State Management** | Zustand | 5.0.8 | Settings and UI state |
|
||||
| **Styling** | Tailwind CSS | 4.0 | Utility-first CSS framework |
|
||||
| **Components** | Radix UI | Latest | Accessible primitives (dialogs, dropdowns, tooltips) |
|
||||
| **Components** | Shadcn/ui | Custom | Pre-built component library |
|
||||
| **Animations** | Framer Motion | 12.23.24 | Smooth transitions |
|
||||
| **Markdown** | react-markdown | 10.1.0 | Message rendering with GFM support |
|
||||
| **Syntax Highlighting** | react-syntax-highlighter | 16.1.0 | Code block rendering |
|
||||
|
||||
### Backend
|
||||
|
||||
| Category | Technology | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| **Database** | SQLite (rusqlite) | 0.31 | Local data persistence with FTS5 |
|
||||
| **Web Scraping** | headless_chrome | 1.0 | Browser automation for content extraction |
|
||||
| **HTML Parsing** | scraper | 0.18 | DOM parsing and content extraction |
|
||||
| **Text Processing** | readability | 0.3 | Main content extraction from HTML |
|
||||
| **HTTP Client** | reqwest | 0.12 | API requests with streaming support |
|
||||
| **Async Runtime** | tokio | 1.0 | Async I/O and concurrency |
|
||||
| **Task Scheduling** | tokio-cron-scheduler | 0.9 | Cron-based task execution |
|
||||
| **System Info** | sysinfo | 0.36.1 | Hardware monitoring |
|
||||
| **Embeddings** | ONNX Runtime (ort) | 2.0.0-rc.10 | Local semantic similarity |
|
||||
| **Tokenization** | tokenizers | 0.21 | Text tokenization for embeddings |
|
||||
| **Array Operations** | ndarray | 0.16 | Numerical computations |
|
||||
|
||||
### AI/ML
|
||||
|
||||
| Component | Technology | Purpose |
|
||||
|-----------|-----------|---------|
|
||||
| **LLM Runtime** | Ollama | Local model execution |
|
||||
| **Embeddings Model** | all-MiniLM-L6-v2 | 384-dim semantic embeddings |
|
||||
| **Embeddings Runtime** | ONNX Runtime | Model inference |
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js**: 20.x or later
|
||||
- **Rust**: 1.77.2 or later
|
||||
- **pnpm**: Latest version (`npm install -g pnpm`)
|
||||
- **Ollama**: [Install Ollama](https://ollama.ai) and ensure it's running on `http://localhost:11434`
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
git clone https://github.com/evandrodevbr/OllaHub.git
|
||||
cd OllaHub
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
3. Run in development mode:
|
||||
```bash
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
This command will:
|
||||
- Start the Next.js development server on `http://localhost:3000`
|
||||
- Build the Rust backend
|
||||
- Launch the Tauri application window
|
||||
|
||||
## Learn More
|
||||
### Building for Production
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
```bash
|
||||
pnpm tauri build
|
||||
```
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
The built application will be in `src-tauri/target/release/bundle/`:
|
||||
- **Windows**: `.msi` installer
|
||||
- **macOS**: `.dmg` or `.app`
|
||||
- **Linux**: `.deb`, `.AppImage`, or `.rpm`
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
### Environment Configuration
|
||||
|
||||
## Deploy on Vercel
|
||||
The application uses Tauri's built-in configuration. No environment variables are required for basic operation. MCP server configurations are stored in the application data directory and managed through the UI.
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
## Project Structure
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
```
|
||||
OllaHub/
|
||||
├── app/ # Next.js app directory (pages)
|
||||
│ ├── chat/ # Chat interface
|
||||
│ ├── settings/ # Application settings
|
||||
│ ├── setup/ # Initial setup wizard
|
||||
│ └── tasks/ # Task management
|
||||
├── components/ # React components
|
||||
│ ├── chat/ # Chat-specific components
|
||||
│ ├── settings/ # Settings UI components
|
||||
│ ├── setup/ # Setup wizard components
|
||||
│ └── ui/ # Reusable UI primitives (Shadcn)
|
||||
├── hooks/ # React custom hooks
|
||||
│ ├── use-chat.ts # Chat state management
|
||||
│ ├── use-deep-research.ts # Research pipeline orchestration
|
||||
│ ├── use-mcp-servers.ts # MCP server management
|
||||
│ └── ...
|
||||
├── lib/ # Utility functions
|
||||
│ ├── knowledge-base-processor.ts # Context condensation
|
||||
│ ├── contextual-analyzer.ts # Query analysis
|
||||
│ ├── query-enricher.ts # Query expansion
|
||||
│ └── ...
|
||||
├── services/ # Service layer
|
||||
│ └── webSearch.ts # Web search abstraction
|
||||
├── store/ # Zustand stores
|
||||
│ └── settings-store.ts # Application settings
|
||||
├── src-tauri/ # Rust backend
|
||||
│ ├── src/
|
||||
│ │ ├── lib.rs # Main Tauri commands
|
||||
│ │ ├── db.rs # SQLite database layer
|
||||
│ │ ├── embeddings.rs # ONNX embeddings module
|
||||
│ │ ├── web_scraper.rs # Web scraping logic
|
||||
│ │ ├── scheduler.rs # Task scheduling
|
||||
│ │ └── ...
|
||||
│ └── Cargo.toml # Rust dependencies
|
||||
└── data/ # Static data
|
||||
├── models/ # Model metadata JSON files
|
||||
└── prompts/ # LLM prompt templates
|
||||
```
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### Backend (Rust)
|
||||
|
||||
- **Command Pattern**: Tauri commands encapsulate backend operations
|
||||
- **State Management**: Arc<Mutex<>> for shared state (browser instances, MCP processes)
|
||||
- **Error Handling**: Result<T, E> with custom error types and fallback strategies
|
||||
- **Async/Await**: Tokio runtime for concurrent operations (scraping, MCP communication)
|
||||
|
||||
### Frontend (TypeScript)
|
||||
|
||||
- **Custom Hooks**: Encapsulate complex logic (deep research, chat management)
|
||||
- **State Machines**: Step-based workflows (idle → planning → searching → aggregating)
|
||||
- **Optimistic Updates**: UI updates before backend confirmation
|
||||
- **Streaming**: Real-time token rendering via Tauri events
|
||||
|
||||
### Data Layer
|
||||
|
||||
- **Repository Pattern**: Database abstraction in `db.rs`
|
||||
- **Migration Strategy**: Dual storage (SQLite + JSON) with automatic fallback
|
||||
- **Full-Text Search**: FTS5 virtual tables with trigger-based synchronization
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Planned Features
|
||||
|
||||
- **Vector Store Integration**: Replace SQLite BLOB embeddings with dedicated vector database (LanceDB or Qdrant)
|
||||
- **Cloud Sync**: Optional encrypted synchronization of chat sessions across devices
|
||||
- **Plugin System**: Extensible architecture for custom research workflows
|
||||
- **Advanced RAG**: Multi-modal retrieval with image and document support
|
||||
- **Model Fine-tuning UI**: Interface for training custom models on user data
|
||||
- **Collaborative Features**: Shared workspaces and team collaboration
|
||||
|
||||
### Technical Improvements
|
||||
|
||||
- **Performance**: Optimize SQLite queries with prepared statements caching
|
||||
- **Memory Management**: Implement LRU cache for embeddings model
|
||||
- **Error Recovery**: Enhanced retry logic with exponential backoff
|
||||
- **Testing**: Comprehensive unit and integration test coverage
|
||||
- **Documentation**: API documentation with code examples
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome. Please follow these guidelines:
|
||||
|
||||
1. **Code Style**: Follow existing patterns (Rust: `rustfmt`, TypeScript: ESLint)
|
||||
2. **Commits**: Use conventional commits (feat:, fix:, docs:, etc.)
|
||||
3. **Pull Requests**: Include description of changes and test results
|
||||
4. **Issues**: Use GitHub Issues for bug reports and feature requests
|
||||
|
||||
## License
|
||||
|
||||
OllaHub is released under a custom license that combines freeware, creditware, and non-commercial restrictions. See the [LICENSE](LICENSE) file for full terms.
|
||||
|
||||
### Quick Summary
|
||||
|
||||
**You may:**
|
||||
- Use OllaHub freely for personal, educational, and internal professional use
|
||||
- Modify and distribute the Software (with proper attribution)
|
||||
- Fork the project and create derivative works (with attribution)
|
||||
|
||||
**You must:**
|
||||
- Give credit to **Evandro Fonseca Junior** and link to [evandro.dev.br](https://evandro.dev.br)
|
||||
- Keep copyright notices and attribution intact
|
||||
- Not sell the Software or charge for access without written permission
|
||||
|
||||
**You may not:**
|
||||
- Sell the Software or bundle it in paid products (without permission)
|
||||
- Use it as a monetization mechanism
|
||||
- Remove attribution or copyright notices
|
||||
- Collect donations on behalf of OllaHub
|
||||
|
||||
For commercial licensing inquiries, visit [evandro.dev.br](https://evandro.dev.br).
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [Ollama](https://ollama.ai) for local LLM execution
|
||||
- [Tauri](https://tauri.app) for desktop framework
|
||||
- [Radix UI](https://www.radix-ui.com) for accessible components
|
||||
- [Shadcn](https://ui.shadcn.com) for component library
|
||||
- [ONNX Runtime](https://onnxruntime.ai) for embeddings inference
|
||||
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -8,7 +8,9 @@ import { markdownRenderers } from "./markdown-renderers";
|
||||
import { MessageActions } from "./message-actions";
|
||||
import { MessageBackground } from "./message-background";
|
||||
import { MessageStepThinking } from "./message-step-thinking";
|
||||
import { DebugConsole, type DebugData } from "./debug-console";
|
||||
import type { ThinkingMessageMetadata } from "@/hooks/use-chat";
|
||||
import { useSettingsStore } from "@/store/settings-store";
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message;
|
||||
@@ -16,16 +18,17 @@ interface ChatMessageProps {
|
||||
highlightTerm?: string;
|
||||
highlightIndex?: number;
|
||||
messageIndex?: number;
|
||||
debugData?: DebugData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Componente de mensagem do chat otimizado com React.memo
|
||||
*
|
||||
* Durante streaming, usa renderização simplificada (texto puro) para evitar
|
||||
* recálculo custoso da árvore DOM e syntax highlight a cada token.
|
||||
* Markdown completo é aplicado apenas quando a mensagem está finalizada.
|
||||
* Renderiza markdown em tempo real durante streaming para melhor UX.
|
||||
* Usa useMemo para otimizar re-renders e manter performance.
|
||||
*/
|
||||
function ChatMessageComponent({ message, isStreaming = false, highlightTerm, highlightIndex = 0, messageIndex }: ChatMessageProps) {
|
||||
function ChatMessageComponent({ message, isStreaming = false, highlightTerm, highlightIndex = 0, messageIndex, debugData }: ChatMessageProps) {
|
||||
const debugMode = useSettingsStore(state => state.debugMode);
|
||||
// Verificar se é mensagem de processo de pensamento
|
||||
const isThinkingMessage = message.metadata &&
|
||||
typeof message.metadata === 'object' &&
|
||||
@@ -61,6 +64,9 @@ function ChatMessageComponent({ message, isStreaming = false, highlightTerm, hig
|
||||
|
||||
return content || message.content;
|
||||
}, [message.content]);
|
||||
|
||||
// Memoizar conteúdo markdown para otimizar re-renders durante streaming
|
||||
const markdownContent = useMemo(() => processedContent, [processedContent]);
|
||||
|
||||
// Processar highlight separadamente (após limpeza de conteúdo)
|
||||
// Desabilitar durante streaming para performance
|
||||
@@ -114,91 +120,101 @@ function ChatMessageComponent({ message, isStreaming = false, highlightTerm, hig
|
||||
}, [processedContent, highlightTerm, highlightIndex, message.role, isStreaming]);
|
||||
|
||||
return (
|
||||
<MessageBackground
|
||||
role={isUser ? 'user' : 'assistant'}
|
||||
state={selected ? 'selected' : 'normal'}
|
||||
className={cn(
|
||||
"cursor-default transition-colors rounded-xl",
|
||||
selected && "bg-muted/40",
|
||||
isUser ? "mb-2" : "mb-8"
|
||||
)}
|
||||
onClick={() => setSelected((v) => !v)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<div className={cn(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center shadow-sm ring-1 ring-border/50 backdrop-blur-sm",
|
||||
isUser
|
||||
? "bg-background text-muted-foreground"
|
||||
: "bg-primary/10 text-primary ring-primary/20"
|
||||
)}>
|
||||
{isUser ? <User className="w-4 h-4" /> : <Bot className="w-5 h-5" />}
|
||||
<>
|
||||
<MessageBackground
|
||||
role={isUser ? 'user' : 'assistant'}
|
||||
state={selected ? 'selected' : 'normal'}
|
||||
className={cn(
|
||||
"cursor-default transition-colors rounded-xl",
|
||||
selected && "bg-muted/40",
|
||||
isUser ? "mb-2" : "mb-8"
|
||||
)}
|
||||
onClick={() => setSelected((v) => !v)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<div className={cn(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center shadow-sm ring-1 ring-border/50 backdrop-blur-sm",
|
||||
isUser
|
||||
? "bg-background text-muted-foreground"
|
||||
: "bg-primary/10 text-primary ring-primary/20"
|
||||
)}>
|
||||
{isUser ? <User className="w-4 h-4" /> : <Bot className="w-5 h-5" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden min-w-0 pt-1" data-message-index={messageIndex}>
|
||||
{isUser ? (
|
||||
// Estilo específico para mensagem do usuário (mais destaque, fonte maior)
|
||||
<div
|
||||
className="text-lg sm:text-xl font-medium tracking-tight text-foreground/90 leading-relaxed break-words whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={highlightedContent ? { __html: highlightedContent } : undefined}
|
||||
>
|
||||
{!highlightedContent && processedContent}
|
||||
</div>
|
||||
) : (
|
||||
// Estilo Assistant
|
||||
isEmpty && isStreaming ? (
|
||||
<div className="flex items-center gap-2 text-muted-foreground py-1">
|
||||
<div className="w-2 h-2 bg-primary rounded-full animate-pulse" />
|
||||
<span className="text-sm font-medium">Gerando resposta...</span>
|
||||
</div>
|
||||
) : isStreaming ? (
|
||||
// Renderização LEVE durante streaming: texto simples com formatação básica
|
||||
// Evita recálculo de ReactMarkdown e syntax highlight a cada token
|
||||
<div className={cn(
|
||||
"prose prose-neutral dark:prose-invert max-w-none break-words leading-7 text-base text-foreground/90",
|
||||
"prose-headings:font-semibold prose-headings:tracking-tight",
|
||||
"relative"
|
||||
)}>
|
||||
<div className="whitespace-pre-wrap font-sans">
|
||||
{processedContent}
|
||||
|
||||
<div className="flex-1 overflow-hidden min-w-0 pt-1" data-message-index={messageIndex}>
|
||||
{isUser ? (
|
||||
// Estilo específico para mensagem do usuário (mais destaque, fonte maior)
|
||||
<div
|
||||
className="text-lg sm:text-xl font-medium tracking-tight text-foreground/90 leading-relaxed break-words whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={highlightedContent ? { __html: highlightedContent } : undefined}
|
||||
>
|
||||
{!highlightedContent && processedContent}
|
||||
</div>
|
||||
) : (
|
||||
// Estilo Assistant
|
||||
isEmpty && isStreaming ? (
|
||||
<div className="flex items-center gap-2 text-muted-foreground py-1">
|
||||
<div className="w-2 h-2 bg-primary rounded-full animate-pulse" />
|
||||
<span className="text-sm font-medium">Gerando resposta...</span>
|
||||
</div>
|
||||
<span className="inline-block w-2 h-4 bg-primary ml-1 animate-pulse rounded-full align-middle" />
|
||||
</div>
|
||||
) : (
|
||||
// Renderização COMPLETA quando mensagem finalizada: ReactMarkdown com syntax highlight
|
||||
<div className={cn(
|
||||
"prose prose-neutral dark:prose-invert max-w-none break-words leading-7 text-base text-foreground/90",
|
||||
"prose-headings:font-semibold prose-headings:tracking-tight prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg",
|
||||
"prose-p:leading-7 prose-li:leading-7",
|
||||
"prose-pre:bg-muted/50 prose-pre:border prose-pre:border-border/50 prose-pre:rounded-xl",
|
||||
"prose-mark:bg-yellow-200/50 prose-mark:dark:bg-yellow-800/50 prose-mark:px-0.5 prose-mark:rounded"
|
||||
)}>
|
||||
{highlightedContent ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: highlightedContent }} />
|
||||
) : (
|
||||
) : isStreaming ? (
|
||||
// Renderização de markdown em tempo real durante streaming
|
||||
<div className={cn(
|
||||
"prose prose-neutral dark:prose-invert max-w-none break-words leading-7 text-base text-foreground/90",
|
||||
"prose-headings:font-semibold prose-headings:tracking-tight prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg",
|
||||
"prose-p:leading-7 prose-li:leading-7",
|
||||
"prose-pre:bg-muted/50 prose-pre:border prose-pre:border-border/50 prose-pre:rounded-xl",
|
||||
"prose-mark:bg-yellow-200/50 prose-mark:dark:bg-yellow-800/50 prose-mark:px-0.5 prose-mark:rounded",
|
||||
"relative"
|
||||
)}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={markdownRenderers}
|
||||
>
|
||||
{processedContent}
|
||||
{markdownContent}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isUser && !isStreaming && (
|
||||
<div className={cn(
|
||||
"absolute -bottom-8 left-12 flex items-center gap-2 opacity-0 transition-opacity duration-200",
|
||||
"group-hover:opacity-100",
|
||||
selected && "opacity-100"
|
||||
)}>
|
||||
<MessageActions content={message.content} role={message.role} />
|
||||
<span className="inline-block w-2 h-4 bg-primary ml-1 animate-pulse rounded-full align-middle" />
|
||||
</div>
|
||||
) : (
|
||||
// Renderização COMPLETA quando mensagem finalizada: ReactMarkdown com syntax highlight
|
||||
<div className={cn(
|
||||
"prose prose-neutral dark:prose-invert max-w-none break-words leading-7 text-base text-foreground/90",
|
||||
"prose-headings:font-semibold prose-headings:tracking-tight prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg",
|
||||
"prose-p:leading-7 prose-li:leading-7",
|
||||
"prose-pre:bg-muted/50 prose-pre:border prose-pre:border-border/50 prose-pre:rounded-xl",
|
||||
"prose-mark:bg-yellow-200/50 prose-mark:dark:bg-yellow-800/50 prose-mark:px-0.5 prose-mark:rounded"
|
||||
)}>
|
||||
{highlightedContent ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: highlightedContent }} />
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={markdownRenderers}
|
||||
>
|
||||
{processedContent}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isUser && !isStreaming && (
|
||||
<div className={cn(
|
||||
"absolute -bottom-8 left-12 flex items-center gap-2 opacity-0 transition-opacity duration-200",
|
||||
"group-hover:opacity-100",
|
||||
selected && "opacity-100"
|
||||
)}>
|
||||
<MessageActions content={message.content} role={message.role} />
|
||||
</div>
|
||||
)}
|
||||
</MessageBackground>
|
||||
{!isUser && !isStreaming && debugMode && debugData && (
|
||||
<DebugConsole data={debugData} />
|
||||
)}
|
||||
</MessageBackground>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -227,7 +243,8 @@ export const ChatMessage = React.memo(ChatMessageComponent, (prevProps, nextProp
|
||||
prevProps.message.role === nextProps.message.role &&
|
||||
prevProps.highlightTerm === nextProps.highlightTerm &&
|
||||
prevProps.highlightIndex === nextProps.highlightIndex &&
|
||||
prevProps.messageIndex === nextProps.messageIndex
|
||||
prevProps.messageIndex === nextProps.messageIndex &&
|
||||
prevProps.debugData === nextProps.debugData
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Copy,
|
||||
Check,
|
||||
Brain,
|
||||
Search,
|
||||
Settings,
|
||||
FileText,
|
||||
Globe,
|
||||
Clock,
|
||||
Database,
|
||||
Code2
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Message } from '@/hooks/use-chat';
|
||||
import type { DeepResearchState } from '@/hooks/use-deep-research';
|
||||
import type { ScrapedContent } from '@/services/webSearch';
|
||||
|
||||
export interface DebugData {
|
||||
model: string;
|
||||
timestamp: number;
|
||||
latency?: number;
|
||||
systemPrompt: string;
|
||||
userQuery: string;
|
||||
contextUsed: Message[];
|
||||
webResearch?: {
|
||||
queries: string[];
|
||||
enrichedQueries?: {
|
||||
literal: string[];
|
||||
semantic: string[];
|
||||
related: string[];
|
||||
expanded: string[];
|
||||
contextual: string[];
|
||||
};
|
||||
sources: ScrapedContent[];
|
||||
logs: Array<{
|
||||
stage: string;
|
||||
timestamp: number;
|
||||
input: string;
|
||||
rawOutput?: string;
|
||||
parsedOutput?: any;
|
||||
error?: string;
|
||||
}>;
|
||||
plan?: string[];
|
||||
knowledgeBase?: Array<{
|
||||
sourceUrl: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}>;
|
||||
};
|
||||
deepResearchState?: DeepResearchState;
|
||||
finalResponse: string;
|
||||
rawResponse?: string;
|
||||
tokenUsage?: {
|
||||
input: number;
|
||||
output: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface DebugConsoleProps {
|
||||
data: DebugData;
|
||||
}
|
||||
|
||||
export function DebugConsole({ data }: DebugConsoleProps) {
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const toggleSection = (section: string) => {
|
||||
setExpandedSections(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(section)) {
|
||||
next.delete(section);
|
||||
} else {
|
||||
next.add(section);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const copyFullDump = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(JSON.stringify(data, null, 2));
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTimestamp = (ts: number) => {
|
||||
return new Date(ts).toLocaleString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const formatDuration = (ms?: number) => {
|
||||
if (!ms) return 'N/A';
|
||||
if (ms < 1000) return `${ms}ms`;
|
||||
return `${(ms / 1000).toFixed(2)}s`;
|
||||
};
|
||||
|
||||
const isExpanded = (section: string) => expandedSections.has(section);
|
||||
|
||||
return (
|
||||
<div className="mt-6 border-t border-border/50 pt-4">
|
||||
<div className="bg-muted/30 rounded-lg border border-border/50 overflow-hidden font-mono">
|
||||
{/* Header Bar */}
|
||||
<div className="bg-muted/50 border-b border-border/50 px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code2 className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-semibold text-foreground">Debug Console</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs font-mono">
|
||||
{data.model || 'N/A'}
|
||||
</Badge>
|
||||
{data.latency !== undefined && (
|
||||
<Badge variant="outline" className="text-xs font-mono">
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
{formatDuration(data.latency)}
|
||||
</Badge>
|
||||
)}
|
||||
{data.tokenUsage && (
|
||||
<Badge variant="outline" className="text-xs font-mono">
|
||||
Tokens: {data.tokenUsage.input}/{data.tokenUsage.output}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={copyFullDump}
|
||||
className="h-7 px-2 text-xs"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="w-3 h-3 mr-1" />
|
||||
Copiado
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-3 h-3 mr-1" />
|
||||
Copiar Dump (JSON)
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Sections */}
|
||||
<div className="divide-y divide-border/50">
|
||||
{/* Section A: Contexto & Memória */}
|
||||
<Section
|
||||
id="context"
|
||||
title="🧠 Contexto & Memória (Input)"
|
||||
icon={<Brain className="w-4 h-4" />}
|
||||
isExpanded={isExpanded('context')}
|
||||
onToggle={() => toggleSection('context')}
|
||||
>
|
||||
<div className="space-y-4 p-4">
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold mb-2 text-muted-foreground uppercase tracking-wide">
|
||||
System Prompt
|
||||
</h4>
|
||||
<pre className="text-xs bg-background/50 p-3 rounded border border-border/30 overflow-x-auto whitespace-pre-wrap break-words">
|
||||
{data.systemPrompt}
|
||||
</pre>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
Injetado em: {formatTimestamp(data.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold mb-2 text-muted-foreground uppercase tracking-wide">
|
||||
Histórico da Sessão ({data.contextUsed.length} mensagens)
|
||||
</h4>
|
||||
<details className="text-xs">
|
||||
<summary className="cursor-pointer text-muted-foreground hover:text-foreground mb-2">
|
||||
Expandir JSON
|
||||
</summary>
|
||||
<pre className="bg-background/50 p-3 rounded border border-border/30 overflow-x-auto">
|
||||
{JSON.stringify(data.contextUsed, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Section B: Deep Research */}
|
||||
{data.webResearch && (
|
||||
<Section
|
||||
id="research"
|
||||
title="🔎 Deep Research (Processamento)"
|
||||
icon={<Search className="w-4 h-4" />}
|
||||
isExpanded={isExpanded('research')}
|
||||
onToggle={() => toggleSection('research')}
|
||||
>
|
||||
<div className="space-y-4 p-4">
|
||||
{/* Pipeline Status */}
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold mb-3 text-muted-foreground uppercase tracking-wide">
|
||||
Status do Pipeline
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{/* Decomposition */}
|
||||
{data.webResearch.plan && data.webResearch.plan.length > 0 && (
|
||||
<div className="border-l-2 border-primary/50 pl-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full" />
|
||||
<span className="text-xs font-medium">Decomposition</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground ml-4 space-y-1">
|
||||
{data.webResearch.plan.map((query, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2">
|
||||
<span className="text-muted-foreground/50">{idx + 1}.</span>
|
||||
<span>{query}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Query Expansion */}
|
||||
{data.webResearch.enrichedQueries && (
|
||||
<div className="border-l-2 border-blue-500/50 pl-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full" />
|
||||
<span className="text-xs font-medium">Query Expansion</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground ml-4 space-y-2">
|
||||
{data.webResearch.queries.map((original, idx) => {
|
||||
const eq = data.webResearch?.enrichedQueries;
|
||||
const allEnriched = eq ? [
|
||||
...(eq.literal || []),
|
||||
...(eq.semantic || []),
|
||||
...(eq.related || []),
|
||||
...(eq.expanded || []),
|
||||
...(eq.contextual || []),
|
||||
] : [];
|
||||
return (
|
||||
<div key={idx} className="space-y-1">
|
||||
<div className="text-muted-foreground/70">
|
||||
<span className="font-medium">Original:</span> {original}
|
||||
</div>
|
||||
{allEnriched.length > 0 && (
|
||||
<div className="text-muted-foreground space-y-1">
|
||||
<span className="font-medium">Enriquecidas ({allEnriched.length}):</span>
|
||||
<div className="ml-2 space-y-0.5">
|
||||
{allEnriched.slice(0, 5).map((q, qIdx) => (
|
||||
<div key={qIdx} className="text-[10px]">• {q}</div>
|
||||
))}
|
||||
{allEnriched.length > 5 && (
|
||||
<div className="text-[10px] text-muted-foreground/70">
|
||||
... e mais {allEnriched.length - 5}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Web Crawling */}
|
||||
{data.webResearch.sources && data.webResearch.sources.length > 0 && (
|
||||
<div className="border-l-2 border-yellow-500/50 pl-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-2 h-2 bg-yellow-500 rounded-full" />
|
||||
<span className="text-xs font-medium">
|
||||
Web Crawling ({data.webResearch.sources.length} fontes)
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground ml-4 space-y-2 max-h-60 overflow-y-auto">
|
||||
{data.webResearch.sources.map((source, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 p-2 bg-background/30 rounded border border-border/20">
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{source.content && source.content.length > 0 ? (
|
||||
<span className="text-green-500">✓</span>
|
||||
) : (
|
||||
<span className="text-red-500">✗</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium truncate">{source.title || source.url}</span>
|
||||
{source.cached && (
|
||||
<Badge variant="outline" className="text-[10px] px-1">
|
||||
Cached
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<a
|
||||
href={source.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[10px] text-primary hover:underline truncate block"
|
||||
>
|
||||
{source.url}
|
||||
</a>
|
||||
{source.content && (
|
||||
<p className="text-[10px] text-muted-foreground mt-1 line-clamp-2">
|
||||
{source.content.substring(0, 150)}...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Knowledge Base */}
|
||||
{data.webResearch.knowledgeBase && data.webResearch.knowledgeBase.length > 0 && (
|
||||
<div className="border-l-2 border-purple-500/50 pl-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-2 h-2 bg-purple-500 rounded-full" />
|
||||
<span className="text-xs font-medium">Knowledge Base</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground ml-4">
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
{data.webResearch.knowledgeBase.length} chunks extraídos
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Research Logs */}
|
||||
{data.webResearch.logs && data.webResearch.logs.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold mb-2 text-muted-foreground uppercase tracking-wide">
|
||||
Logs Internos ({data.webResearch.logs.length} entradas)
|
||||
</h4>
|
||||
<div className="space-y-2 max-h-60 overflow-y-auto">
|
||||
{data.webResearch.logs.map((log, idx) => (
|
||||
<div key={idx} className="text-xs bg-background/30 p-2 rounded border border-border/20">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge variant="outline" className="text-[10px] px-1">
|
||||
{log.stage}
|
||||
</Badge>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{formatTimestamp(log.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
<div className="mb-1">
|
||||
<span className="font-medium">Input:</span> {log.input}
|
||||
</div>
|
||||
{log.rawOutput && (
|
||||
<div className="mb-1">
|
||||
<span className="font-medium">Output:</span>{' '}
|
||||
<span className="text-[10px]">{log.rawOutput.substring(0, 200)}...</span>
|
||||
</div>
|
||||
)}
|
||||
{log.error && (
|
||||
<div className="text-red-500">
|
||||
<span className="font-medium">Erro:</span> {log.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Section C: Dados Processados */}
|
||||
{data.deepResearchState && (
|
||||
<Section
|
||||
id="processed"
|
||||
title="⚙️ Dados Processados (Raciocínio)"
|
||||
icon={<Settings className="w-4 h-4" />}
|
||||
isExpanded={isExpanded('processed')}
|
||||
onToggle={() => toggleSection('processed')}
|
||||
>
|
||||
<div className="space-y-4 p-4">
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold mb-2 text-muted-foreground uppercase tracking-wide">
|
||||
Estado do Deep Research
|
||||
</h4>
|
||||
<div className="text-xs space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">Step:</span>
|
||||
<Badge variant="outline">{data.deepResearchState.step}</Badge>
|
||||
</div>
|
||||
{data.deepResearchState.validationReport && (
|
||||
<div>
|
||||
<span className="text-muted-foreground font-medium">Validation Report:</span>
|
||||
<pre className="mt-1 bg-background/50 p-2 rounded border border-border/30 overflow-x-auto whitespace-pre-wrap break-words text-[10px]">
|
||||
{data.deepResearchState.validationReport}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{data.deepResearchState.error && (
|
||||
<div className="text-red-500">
|
||||
<span className="font-medium">Erro:</span> {data.deepResearchState.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{/* Section D: Resposta Raw */}
|
||||
<Section
|
||||
id="response"
|
||||
title="📤 Resposta Raw (Output)"
|
||||
icon={<FileText className="w-4 h-4" />}
|
||||
isExpanded={isExpanded('response')}
|
||||
onToggle={() => toggleSection('response')}
|
||||
>
|
||||
<div className="p-4">
|
||||
<pre className="text-xs bg-background/50 p-3 rounded border border-border/30 overflow-x-auto whitespace-pre-wrap break-words max-h-96 overflow-y-auto">
|
||||
{data.rawResponse || data.finalResponse}
|
||||
</pre>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SectionProps {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Section({ id, title, icon, isExpanded, onToggle, children }: SectionProps) {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="w-full px-4 py-3 flex items-center justify-between hover:bg-muted/40 transition-colors text-left"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-4 h-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="bg-background/20">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export function KeyboardShortcuts() {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// CTRL+SHIFT+D para abrir devtools
|
||||
if (e.ctrlKey && e.shiftKey && (e.key === 'd' || e.key === 'D')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
invoke('toggle_devtools').catch(console.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear F5 (refresh)
|
||||
if (e.key === 'F5') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear CTRL+R (refresh)
|
||||
if (e.ctrlKey && (e.key === 'r' || e.key === 'R') && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear CTRL+SHIFT+R (hard refresh)
|
||||
if (e.ctrlKey && e.shiftKey && (e.key === 'r' || e.key === 'R')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear CTRL+SHIFT+I (devtools)
|
||||
if (e.ctrlKey && e.shiftKey && (e.key === 'i' || e.key === 'I')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear CTRL+SHIFT+J (console)
|
||||
if (e.ctrlKey && e.shiftKey && (e.key === 'j' || e.key === 'J')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear CTRL+U (view source)
|
||||
if (e.ctrlKey && (e.key === 'u' || e.key === 'U') && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear CTRL+SHIFT+DEL (clear storage)
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'Delete') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear CTRL+W (fechar aba - mas permitir fechar janela com o comportamento customizado do Tauri)
|
||||
// Não bloqueamos CTRL+W porque o Tauri já trata isso no on_window_event
|
||||
|
||||
// Bloquear CTRL+N (nova janela)
|
||||
if (e.ctrlKey && (e.key === 'n' || e.key === 'N') && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear CTRL+SHIFT+N (nova janela anônima)
|
||||
if (e.ctrlKey && e.shiftKey && (e.key === 'n' || e.key === 'N')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
// Bloquear CTRL+T (nova aba)
|
||||
if (e.ctrlKey && (e.key === 't' || e.key === 'T') && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Adicionar listener com capture para interceptar antes de outros handlers
|
||||
window.addEventListener('keydown', handleKeyDown, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -43,11 +43,33 @@ export function HardwareDashboard() {
|
||||
|
||||
if (isLoading || !stats) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
Carregando estatísticas do sistema...
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Skeleton Cards */}
|
||||
{[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>
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="h-4 w-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
<span>Carregando estatísticas do sistema...</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RECOMMENDED_MODELS, ModelRecommendation } from "@/lib/recommendation";
|
||||
import { useState, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { startTransition } from "react";
|
||||
|
||||
interface ModelSelectorProps {
|
||||
recommendation: ModelRecommendation;
|
||||
@@ -30,6 +31,7 @@ export function ModelSelector({ recommendation, onComplete }: ModelSelectorProps
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
const [downloadStatus, setDownloadStatus] = useState("");
|
||||
const [downloadInfo, setDownloadInfo] = useState<DownloadInfo | null>(null);
|
||||
const [isNavigating, setIsNavigating] = useState(false);
|
||||
|
||||
const selectedModel = RECOMMENDED_MODELS.find(m => m.id === selectedModelId) || RECOMMENDED_MODELS[0];
|
||||
|
||||
@@ -80,10 +82,11 @@ export function ModelSelector({ recommendation, onComplete }: ModelSelectorProps
|
||||
|
||||
// Se chegou a 100% ou success, marcar como instalado após um pequeno delay
|
||||
if (info.percent === 100 || info.status === "success") {
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
setIsInstalled(true);
|
||||
setIsDownloading(false);
|
||||
checkInstallation(selectedModelId);
|
||||
// Verificar instalação de forma assíncrona sem bloquear
|
||||
checkInstallation(selectedModelId).catch(console.error);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@@ -103,6 +106,14 @@ export function ModelSelector({ recommendation, onComplete }: ModelSelectorProps
|
||||
}
|
||||
};
|
||||
|
||||
const handleNavigate = () => {
|
||||
setIsNavigating(true);
|
||||
// Usar startTransition para navegação não-bloqueante
|
||||
startTransition(() => {
|
||||
onComplete();
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
setIsDownloading(true);
|
||||
setDownloadProgress(0);
|
||||
@@ -192,9 +203,22 @@ export function ModelSelector({ recommendation, onComplete }: ModelSelectorProps
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{isInstalled ? (
|
||||
<Button className="w-full h-12 text-lg" onClick={onComplete}>
|
||||
Ir para o Chat
|
||||
<Play className="ml-2 w-5 h-5" />
|
||||
<Button
|
||||
className="w-full h-12 text-lg"
|
||||
onClick={handleNavigate}
|
||||
disabled={isNavigating}
|
||||
>
|
||||
{isNavigating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
Inicializando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Ir para o Chat
|
||||
<Play className="ml-2 w-5 h-5" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -85,6 +85,12 @@ export function TitleBar() {
|
||||
className="flex items-center gap-2 px-4 h-full flex-1"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<img
|
||||
src="/icon.png"
|
||||
alt="OllaHub"
|
||||
className="w-5 h-5 shrink-0"
|
||||
draggable={false}
|
||||
/>
|
||||
<span className="text-sm font-medium text-sidebar-foreground">
|
||||
OllaHub
|
||||
</span>
|
||||
|
||||
@@ -10,11 +10,8 @@ export function useHardware() {
|
||||
async function fetchSpecs() {
|
||||
try {
|
||||
const data = await invoke<SystemSpecs>('get_system_specs');
|
||||
// Simulate a small delay for the "scanning" effect
|
||||
setTimeout(() => {
|
||||
setSpecs(data);
|
||||
setLoading(false);
|
||||
}, 1500);
|
||||
setSpecs(data);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to get system specs:", error);
|
||||
setLoading(false);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useOllamaCheck, type OllamaStatus } from './use-ollama-check';
|
||||
|
||||
export type SetupStatus = 'checking' | 'ready' | 'needs_setup' | 'needs_ollama';
|
||||
|
||||
export function useSetupCheck() {
|
||||
const { status: ollamaStatus, check: checkOllama } = useOllamaCheck();
|
||||
const [setupStatus, setSetupStatus] = useState<SetupStatus>('checking');
|
||||
const [checkingStep, setCheckingStep] = useState<string>('Verificando Ollama...');
|
||||
const [hasModels, setHasModels] = useState(false);
|
||||
const [modelsCount, setModelsCount] = useState(0);
|
||||
|
||||
const checkModels = async (): Promise<boolean> => {
|
||||
try {
|
||||
setCheckingStep('Verificando modelos instalados...');
|
||||
const models = await invoke<Array<{ name: string; size: string; id: string; modified_at: string }>>('list_local_models');
|
||||
const count = models.length;
|
||||
setModelsCount(count);
|
||||
const hasAny = count > 0;
|
||||
setHasModels(hasAny);
|
||||
return hasAny;
|
||||
} catch (error) {
|
||||
console.error('Failed to check models:', error);
|
||||
// Em caso de erro, assumir que não há modelos (seguro)
|
||||
setHasModels(false);
|
||||
setModelsCount(0);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const determineSetupStatus = async (ollamaStatus: OllamaStatus): Promise<SetupStatus> => {
|
||||
// Se Ollama não está instalado, precisa instalar
|
||||
if (ollamaStatus === 'not_installed') {
|
||||
return 'needs_ollama';
|
||||
}
|
||||
|
||||
// Se Ollama está instalado mas parado, tentar iniciar primeiro
|
||||
if (ollamaStatus === 'installed_stopped') {
|
||||
try {
|
||||
setCheckingStep('Iniciando Ollama...');
|
||||
await invoke('auto_start_ollama');
|
||||
// Aguardar um pouco para o servidor iniciar
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Verificar novamente se está rodando
|
||||
const running = await invoke<boolean>('check_ollama_running');
|
||||
if (!running) {
|
||||
// Ainda não está rodando, precisa de intervenção do usuário
|
||||
return 'needs_setup';
|
||||
}
|
||||
// Agora está rodando, verificar modelos
|
||||
const hasAnyModels = await checkModels();
|
||||
return hasAnyModels ? 'ready' : 'needs_setup';
|
||||
} catch (error) {
|
||||
console.error('Failed to auto-start Ollama:', error);
|
||||
return 'needs_setup';
|
||||
}
|
||||
}
|
||||
|
||||
// Se Ollama está rodando, verificar modelos
|
||||
if (ollamaStatus === 'running') {
|
||||
const hasAnyModels = await checkModels();
|
||||
return hasAnyModels ? 'ready' : 'needs_setup';
|
||||
}
|
||||
|
||||
// Estado checking ou qualquer outro caso
|
||||
return 'checking';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const performCheck = async () => {
|
||||
if (ollamaStatus === 'checking') {
|
||||
setSetupStatus('checking');
|
||||
setCheckingStep('Verificando Ollama...');
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await determineSetupStatus(ollamaStatus);
|
||||
setSetupStatus(status);
|
||||
|
||||
if (status === 'ready') {
|
||||
setCheckingStep('Tudo pronto!');
|
||||
} else if (status === 'needs_setup') {
|
||||
setCheckingStep('Configuração necessária');
|
||||
} else if (status === 'needs_ollama') {
|
||||
setCheckingStep('Ollama não instalado');
|
||||
}
|
||||
};
|
||||
|
||||
performCheck();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ollamaStatus]);
|
||||
|
||||
const recheck = async () => {
|
||||
setSetupStatus('checking');
|
||||
setCheckingStep('Verificando Ollama...');
|
||||
await checkOllama();
|
||||
// O useEffect vai processar o novo status
|
||||
};
|
||||
|
||||
return {
|
||||
status: setupStatus,
|
||||
ollamaStatus,
|
||||
hasModels,
|
||||
modelsCount,
|
||||
checkingStep,
|
||||
recheck,
|
||||
};
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 136 KiB |
@@ -6,7 +6,7 @@ use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{command, Window, Emitter, Manager, AppHandle, State};
|
||||
use tauri::{command, Window, Emitter, Manager, AppHandle, State, WebviewWindow};
|
||||
use sysinfo::System;
|
||||
use chrono::{DateTime, Utc};
|
||||
use zip::write::{FileOptions, ZipWriter};
|
||||
@@ -328,6 +328,23 @@ fn get_mcp_config_path(app_handle: &AppHandle) -> Result<PathBuf, String> {
|
||||
Ok(app_data_dir.join("mcp_config.json"))
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn toggle_devtools(window: WebviewWindow) -> Result<(), String> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if window.is_devtools_open() {
|
||||
window.close_devtools();
|
||||
} else {
|
||||
window.open_devtools();
|
||||
}
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
// Em release, não fazer nada
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn save_chat_session(
|
||||
app_handle: AppHandle,
|
||||
@@ -3697,6 +3714,7 @@ pub fn run() {
|
||||
delete_model,
|
||||
save_chat_session,
|
||||
load_chat_sessions,
|
||||
toggle_devtools,
|
||||
search_chat_sessions,
|
||||
load_chat_history,
|
||||
load_chat_history_paginated,
|
||||
|
||||
@@ -210,6 +210,7 @@ pub fn detect_all_gpus() -> Vec<GpuInfo> {
|
||||
#[cfg(target_os = "windows")]
|
||||
fn detect_gpus_windows() -> Vec<GpuInfo> {
|
||||
use std::process::Command;
|
||||
use std::os::windows::process::CommandExt;
|
||||
let mut gpus = Vec::new();
|
||||
|
||||
log::info!("Tentando detectar GPUs via wmic...");
|
||||
@@ -217,6 +218,7 @@ fn detect_gpus_windows() -> Vec<GpuInfo> {
|
||||
// Tentar formato CSV primeiro (mais confiável)
|
||||
if let Ok(output) = Command::new("wmic")
|
||||
.args(&["path", "win32_VideoController", "get", "name,AdapterRAM,PNPDeviceID", "/format:csv"])
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||
.output()
|
||||
{
|
||||
// wmic pode retornar UTF-16LE no Windows, precisamos converter
|
||||
@@ -282,6 +284,7 @@ fn detect_gpus_windows() -> Vec<GpuInfo> {
|
||||
// Fallback para formato list
|
||||
if let Ok(output) = Command::new("wmic")
|
||||
.args(&["path", "win32_VideoController", "get", "name,AdapterRAM,PNPDeviceID", "/format:list"])
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||
.output()
|
||||
{
|
||||
if let Ok(stdout) = String::from_utf8(output.stdout) {
|
||||
@@ -337,11 +340,13 @@ fn detect_gpus_windows() -> Vec<GpuInfo> {
|
||||
#[cfg(target_os = "windows")]
|
||||
fn detect_gpus_nvidia_smi() -> Result<Vec<GpuInfo>, String> {
|
||||
use std::process::Command;
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
log::info!("Executando nvidia-smi...");
|
||||
|
||||
let output = Command::new("nvidia-smi")
|
||||
.args(&["--query-gpu=name,memory.total", "--format=csv,noheader,nounits"])
|
||||
.creation_flags(0x08000000) // CREATE_NO_WINDOW
|
||||
.output()
|
||||
.map_err(|e| format!("nvidia-smi não encontrado: {}", e))?;
|
||||
|
||||
@@ -678,13 +683,19 @@ fn get_nvidia_gpu_stats(gpu: &GpuInfo) -> Option<GpuStats> {
|
||||
// Query nvidia-smi para obter todas as métricas
|
||||
let query = "name,memory.used,memory.total,utilization.gpu,utilization.memory,temperature.gpu,temperature.max,power.draw,power.limit,fan.speed,driver_version";
|
||||
|
||||
let output = Command::new("nvidia-smi")
|
||||
.args(&[
|
||||
"--query-gpu", query,
|
||||
"--format=csv,noheader,nounits"
|
||||
])
|
||||
.output()
|
||||
.ok()?;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
let mut cmd = Command::new("nvidia-smi");
|
||||
cmd.args(&[
|
||||
"--query-gpu", query,
|
||||
"--format=csv,noheader,nounits"
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||
|
||||
let output = cmd.output().ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
log::warn!("nvidia-smi falhou ao coletar stats");
|
||||
@@ -757,9 +768,16 @@ fn get_nvidia_gpu_stats(gpu: &GpuInfo) -> Option<GpuStats> {
|
||||
fn count_nvidia_gpu_processes() -> Result<usize, String> {
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("nvidia-smi")
|
||||
.args(&["--query-compute-apps=pid", "--format=csv,noheader"])
|
||||
.output()
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
let mut cmd = Command::new("nvidia-smi");
|
||||
cmd.args(&["--query-compute-apps=pid", "--format=csv,noheader"]);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW
|
||||
|
||||
let output = cmd.output()
|
||||
.map_err(|e| format!("nvidia-smi não encontrado: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false,
|
||||
"transparent": false
|
||||
"transparent": false,
|
||||
"devtools": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
@@ -29,11 +30,12 @@
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
"icons/icon.ico",
|
||||
"icons/32x32.ico",
|
||||
"icons/128x128.ico",
|
||||
"icons/256x256.ico",
|
||||
"icons/512x512.ico",
|
||||
"icons/icon.icns"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
|
||||
@@ -59,6 +59,9 @@ export interface SettingsState {
|
||||
// Updates
|
||||
autoCheckUpdates: boolean;
|
||||
|
||||
// Debug
|
||||
debugMode: boolean;
|
||||
|
||||
// Query Preprocessing
|
||||
queryPreprocessing: {
|
||||
enabled: boolean;
|
||||
@@ -102,6 +105,9 @@ export interface SettingsState {
|
||||
// Updates Actions
|
||||
setAutoCheckUpdates: (enabled: boolean) => void;
|
||||
|
||||
// Debug Actions
|
||||
setDebugMode: (enabled: boolean) => void;
|
||||
|
||||
// Query Preprocessing Actions
|
||||
setQueryPreprocessingEnabled: (enabled: boolean) => void;
|
||||
setQueryPreprocessingMinLength: (min: number) => void;
|
||||
@@ -161,7 +167,7 @@ const defaultCategories: SearchCategory[] = [
|
||||
'pubmed.ncbi.nlm.nih.gov',
|
||||
'researchgate.net',
|
||||
],
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'news',
|
||||
@@ -197,7 +203,7 @@ const defaultCategories: SearchCategory[] = [
|
||||
'investing.com',
|
||||
'yahoo.com/finance',
|
||||
],
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -228,6 +234,7 @@ const initialState = {
|
||||
keywords: [],
|
||||
},
|
||||
autoCheckUpdates: true,
|
||||
debugMode: false,
|
||||
queryPreprocessing: {
|
||||
enabled: true,
|
||||
minLength: 3,
|
||||
@@ -418,6 +425,10 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
setAutoCheckUpdates: (enabled) =>
|
||||
set({ autoCheckUpdates: enabled }),
|
||||
|
||||
// Debug Actions
|
||||
setDebugMode: (enabled) =>
|
||||
set({ debugMode: enabled }),
|
||||
|
||||
// Query Preprocessing Actions
|
||||
setQueryPreprocessingEnabled: (enabled) =>
|
||||
set((state) => ({
|
||||
@@ -490,7 +501,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
}),
|
||||
{
|
||||
name: 'ollahub-settings',
|
||||
version: 3,
|
||||
version: 4,
|
||||
migrate: (persistedState: any, version: number) => {
|
||||
// Migração da versão 1 para 3
|
||||
if (version < 3) {
|
||||
@@ -499,6 +510,13 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
autoCheckUpdates: persistedState.autoCheckUpdates ?? true,
|
||||
};
|
||||
}
|
||||
// Migração da versão 3 para 4
|
||||
if (version < 4) {
|
||||
return {
|
||||
...persistedState,
|
||||
debugMode: persistedState.debugMode ?? false,
|
||||
};
|
||||
}
|
||||
return persistedState;
|
||||
},
|
||||
}
|
||||
|
||||