From a7cbd1a3b4d93e1282ad419e8227e7d357418044 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sun, 22 Feb 2026 14:06:01 -0300 Subject: [PATCH] chore: expurgo de blobs excedentes e realinhamento de estado --- ...o_e_download_hugging_face_96c33a7f.plan.md | 569 ++++ .github/workflows/release.yml | 56 + .gitignore | 11 + LICENSE | 9 +- app/chat/page.tsx | 436 ++- app/globals.css | 10 + app/layout.tsx | 19 + app/page.tsx | 130 + app/settings/page.tsx | 2599 ++++++++++------- app/setup/page.tsx | 36 +- app/tasks/page.tsx | 2 +- components/chat/debug-console.tsx | 46 + components/chat/message-step-thinking.tsx | 25 +- components/chat/model-selector.tsx | 317 ++ components/chat/scraping-urls-monitor.tsx | 107 + components/chat/search-queries-monitor.tsx | 70 + components/global-download-status-bar.tsx | 80 + components/modals/install-modal.tsx | 41 +- .../notifications/welcome-notification.tsx | 8 + components/settings/GpuDashboard.tsx | 8 + components/settings/docker-commands-modal.tsx | 169 ++ components/settings/docker-settings-panel.tsx | 899 ++++++ .../settings/runtime-settings-panel.tsx | 244 ++ components/settings/search-settings-panel.tsx | 209 ++ .../settings/searxng-install-wizard.tsx | 881 ++++++ components/settings/searxng-onboarding.tsx | 186 ++ components/setup/installation-terminal.tsx | 152 + components/setup/model-download-step.tsx | 425 +++ components/setup/ollama-install-step.tsx | 638 ++++ components/setup/setup-complete-step.tsx | 173 ++ components/setup/setup-wizard.tsx | 120 + components/setup/system-check-step.tsx | 136 + components/titlebar.tsx | 6 +- components/ui/alert.tsx | 46 + components/ui/dialog.tsx | 10 +- components/ui/popover.tsx | 31 + .../ui/service-interruption-overlay.tsx | 67 + components/ui/table.tsx | 117 + contexts/download-context.tsx | 346 +++ contexts/ollama-health-context.tsx | 92 + docker-compose.searxng.yml | 31 + hooks/use-app-updater.ts | 16 + hooks/use-auto-labeling-model.ts | 22 +- hooks/use-batch-verify.ts | 25 +- hooks/use-chat-storage.ts | 59 +- hooks/use-chat.ts | 19 +- hooks/use-deep-research.ts | 165 +- hooks/use-model-selection.ts | 102 + hooks/use-ollama-check.ts | 206 +- hooks/use-ollama-download.ts | 169 +- hooks/use-operating-system.ts | 8 + hooks/use-prompt-generator.ts | 24 +- hooks/use-query-generator.ts | 27 +- hooks/use-setup-check.ts | 19 + hooks/use-setup-wizard.ts | 366 +++ hooks/use-system-health.ts | 3 + hooks/use-web-search-events.ts | 161 + hooks/use-web-search.ts | 45 +- lib/context-unifier.ts | 27 +- lib/contextual-analyzer.ts | 72 +- lib/docker-problems.ts | 626 ++++ lib/download-utils.ts | 12 +- lib/knowledge-base-processor.ts | 77 + lib/model-context.ts | 12 +- lib/model-registry.ts | 142 + lib/query-enricher.ts | 27 +- lib/search-logger.ts | 8 + lib/terminal-logger.ts | 8 + lib/types.ts | 31 + lib/web-search-fallback.ts | 80 +- next.config.ts | 36 + package.json | 9 +- pnpm-lock.yaml | 114 +- searxng-settings.yml | 67 + services/engine-circuit-breaker.ts | 191 -- services/webSearch.ts | 166 +- src-tauri/Cargo.lock | 807 +++-- src-tauri/Cargo.toml | 38 +- src-tauri/build.rs | 445 ++- .../__pycache__/scraper.cpython-314.pyc | Bin 0 -> 13307 bytes src-tauri/scripts/requirements.txt | 4 + src-tauri/scripts/scraper.py | 800 +++-- src-tauri/scripts/scraper_nodriver.py | 337 +++ src-tauri/src/brave_search.rs | 110 + src-tauri/src/content_store.rs | 222 ++ src-tauri/src/crypto.rs | 60 + src-tauri/src/db.rs | 317 +- src-tauri/src/docker_fix.rs | 285 ++ src-tauri/src/docker_installer.rs | 600 ++++ src-tauri/src/docker_requirements.rs | 538 ++++ src-tauri/src/docker_validator.rs | 251 ++ src-tauri/src/domain_strategy.rs | 108 + src-tauri/src/embeddings.rs | 7 + src-tauri/src/event_emitter.rs | 73 + src-tauri/src/huggingface_download.rs | 448 +++ src-tauri/src/lib.rs | 2092 +++++++++++-- src-tauri/src/llama_cpp_state.rs | 108 + src-tauri/src/local_models.rs | 174 ++ src-tauri/src/models_dir.rs | 79 + src-tauri/src/ollama_client.rs | 250 +- src-tauri/src/python_scraper.rs | 545 ++++ src-tauri/src/scheduler_loop.rs | 6 +- src-tauri/src/scraper_logger.rs | 115 + src-tauri/src/scraper_state.rs | 32 + src-tauri/src/search_engine.rs | 117 + src-tauri/src/search_orchestrator.rs | 88 + src-tauri/src/searxng_manager.rs | 284 ++ src-tauri/src/searxng_search.rs | 59 + src-tauri/src/setup.rs | 603 ++++ src-tauri/src/setup_manager.rs | 401 +++ src-tauri/src/task_executor.rs | 45 +- src-tauri/src/web_scraper.rs | 1619 +++------- src-tauri/tauri.conf.json | 18 +- store/settings-store.ts | 191 +- 114 files changed, 20716 insertions(+), 4258 deletions(-) create mode 100644 .cursor/plans/integração_llama.cpp_nativo_e_download_hugging_face_96c33a7f.plan.md create mode 100644 components/chat/model-selector.tsx create mode 100644 components/chat/scraping-urls-monitor.tsx create mode 100644 components/chat/search-queries-monitor.tsx create mode 100644 components/global-download-status-bar.tsx create mode 100644 components/settings/docker-commands-modal.tsx create mode 100644 components/settings/docker-settings-panel.tsx create mode 100644 components/settings/runtime-settings-panel.tsx create mode 100644 components/settings/search-settings-panel.tsx create mode 100644 components/settings/searxng-install-wizard.tsx create mode 100644 components/settings/searxng-onboarding.tsx create mode 100644 components/setup/installation-terminal.tsx create mode 100644 components/setup/model-download-step.tsx create mode 100644 components/setup/ollama-install-step.tsx create mode 100644 components/setup/setup-complete-step.tsx create mode 100644 components/setup/setup-wizard.tsx create mode 100644 components/setup/system-check-step.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/service-interruption-overlay.tsx create mode 100644 components/ui/table.tsx create mode 100644 contexts/download-context.tsx create mode 100644 contexts/ollama-health-context.tsx create mode 100644 docker-compose.searxng.yml create mode 100644 hooks/use-model-selection.ts create mode 100644 hooks/use-setup-wizard.ts create mode 100644 hooks/use-web-search-events.ts create mode 100644 lib/docker-problems.ts create mode 100644 lib/model-registry.ts create mode 100644 searxng-settings.yml delete mode 100644 services/engine-circuit-breaker.ts create mode 100644 src-tauri/scripts/__pycache__/scraper.cpython-314.pyc create mode 100644 src-tauri/scripts/scraper_nodriver.py create mode 100644 src-tauri/src/brave_search.rs create mode 100644 src-tauri/src/content_store.rs create mode 100644 src-tauri/src/crypto.rs create mode 100644 src-tauri/src/docker_fix.rs create mode 100644 src-tauri/src/docker_installer.rs create mode 100644 src-tauri/src/docker_requirements.rs create mode 100644 src-tauri/src/docker_validator.rs create mode 100644 src-tauri/src/domain_strategy.rs create mode 100644 src-tauri/src/event_emitter.rs create mode 100644 src-tauri/src/huggingface_download.rs create mode 100644 src-tauri/src/llama_cpp_state.rs create mode 100644 src-tauri/src/local_models.rs create mode 100644 src-tauri/src/models_dir.rs create mode 100644 src-tauri/src/python_scraper.rs create mode 100644 src-tauri/src/scraper_logger.rs create mode 100644 src-tauri/src/scraper_state.rs create mode 100644 src-tauri/src/search_engine.rs create mode 100644 src-tauri/src/search_orchestrator.rs create mode 100644 src-tauri/src/searxng_manager.rs create mode 100644 src-tauri/src/searxng_search.rs create mode 100644 src-tauri/src/setup.rs create mode 100644 src-tauri/src/setup_manager.rs diff --git a/.cursor/plans/integração_llama.cpp_nativo_e_download_hugging_face_96c33a7f.plan.md b/.cursor/plans/integração_llama.cpp_nativo_e_download_hugging_face_96c33a7f.plan.md new file mode 100644 index 0000000..6099819 --- /dev/null +++ b/.cursor/plans/integração_llama.cpp_nativo_e_download_hugging_face_96c33a7f.plan.md @@ -0,0 +1,569 @@ +--- +name: Integração llama.cpp nativo e download Hugging Face +overview: Implementar download direto de modelos GGUF do Hugging Face, integração com llama.cpp nativo via Rust, listagem de modelos locais e UI experimental para alternar entre Ollama e llama.cpp nativo. +todos: + - id: hf-download-module + content: Criar módulo huggingface_download.rs com download_gguf_model() e streaming de progresso + status: pending + - id: hf-list-models + content: Implementar list_huggingface_gguf_models() com cache de metadados (TTL 1h) + status: pending + - id: hf-validation + content: Adicionar validação SHA256 obrigatória quando disponível, retry logic com resume e rate limiting (429) + status: pending + - id: hf-rate-limiting + content: Implementar tratamento de rate limiting com Retry-After header e backoff exponencial + status: pending + - id: llama-cpp-deps + content: Adicionar llama_cpp 0.4 no Cargo.toml com features condicionais por OS + feature flag dev-no-llama + status: pending + - id: llama-deps-parsing + content: Adicionar dependências gguf 0.1 e lru 0.12 para parsing de metadados e cache LRU + status: pending + - id: llama-state + content: Criar LlamaCppState com LRU cache (limite 3 modelos) para prevenir memory leaks + status: pending + - id: models-dir-structure + content: Implementar get_models_dir() com estrutura hierárquica cross-platform (repo_owner/repo_name/quantization) + status: pending + - id: llama-chat-stream + content: Implementar chat_stream_native() com streaming de tokens via llama-cpp-rs + status: pending + - id: llama-params + content: Adicionar configuração de parâmetros (n_gpu_layers, n_ctx, threads) no settings store + status: pending + - id: local-scan + content: Implementar list_local_gguf_models() com scan recursivo e parsing de metadados via crate gguf + status: pending + - id: local-cache + content: Adicionar cache de metadados de modelos locais para evitar re-scan frequente + status: pending + - id: quantization-regex + content: "Implementar regex corrigido para extração de quantização: [._-](Q\\d+_?[KM]?_?[MS]?|[FI]P?\\d+)[._-]" + status: pending + - id: vram-detection + content: Adicionar detecção de VRAM disponível via sysinfo antes de carregar modelos grandes + status: pending + - id: concurrent-downloads + content: Implementar semáforo para limitar downloads paralelos a 2 simultâneos + status: pending + - id: ui-runtime-toggle + content: Criar toggle de runtime (Ollama/llama.cpp) nas configurações com indicador visual + status: pending + - id: ui-hf-selector + content: Implementar seletor de modelos Hugging Face com busca e filtros de quantização + status: pending + - id: ui-integration + content: Integrar runtime selection no use-chat.ts para alternar entre chat_stream e chat_stream_native + status: pending +isProject: false +--- + +# Plano de Implementação: Integração llama.cpp e Download Hugging Face + +## Contexto Arquitetural + +O projeto atualmente usa Ollama como runtime de modelos via HTTP (`http://localhost:11434`). O chat streaming é feito através de `chat_stream` que chama `OllamaClient`. Modelos são baixados via `pull_model` que usa a API do Ollama. Existe suporte básico para instalar modelos GGUF locais via `install_gguf_model`. + +**Decisão arquitetural:** Implementar runtime dual (Ollama + llama.cpp nativo) com abstração no backend Rust para permitir alternância transparente no frontend. + +## Estrutura de Armazenamento + +- **Modelos Ollama:** `~/.ollama/models` (Linux/Mac) ou `%USERPROFILE%\.ollama\models` (Windows) +- **Modelos llama.cpp nativo:** Estrutura hierárquica em `{app_data_dir}/models/gguf/{repo_owner}/{repo_name}/{quantization}/model.gguf` + - Exemplo: `~/.local/share/OllaHub/models/gguf/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/Q4_K_M/mistral-7b-instruct-v0.2.Q4_K_M.gguf` +- **Cache de metadados:** `{app_data_dir}/cache/huggingface_models.json` +- **Cross-platform paths:** + - Windows: `%APPDATA%\Roaming\com.ollahub.app\models\gguf` + - Linux: `~/.local/share/com.ollahub.app/models/gguf` + - macOS: `~/Library/Application Support/com.ollahub.app/models/gguf` + +## Fase 1: Backend - Download Direto Hugging Face + +### 1.1 Módulo de Download (`src-tauri/src/huggingface_download.rs`) + +**Contrato técnico:** + +```rust +pub struct HuggingFaceModel { + pub id: String, + pub filename: String, + pub size: u64, + pub quantization: String, // Q4_K_M, Q5_K_S, etc. + pub sha256: Option, +} + +pub async fn download_gguf_model( + app_handle: AppHandle, + model_id: String, + filename: String, + quantization_filter: Option, + window: Option, +) -> Result +``` + +**Implementação:** + +- Usar `reqwest` com streaming (já presente no `Cargo.toml`) +- API Hugging Face: `https://huggingface.co/api/models/{model_id}/tree/main` +- Download direto: `https://huggingface.co/{model_id}/resolve/main/{filename}` +- Emitir eventos `download-progress` (reutilizar estrutura existente de `pull_model_impl_window`) +- **Validação SHA256 obrigatória** quando disponível no Hugging Face (usar `sha2` já presente) +- Retry logic com backoff exponencial (3 tentativas) +- **Rate limiting:** Tratar 429 Too Many Requests com `Retry-After` header +- **Resume de download:** Verificar arquivo parcial e continuar de onde parou + +**Arquivos:** + +- Criar `src-tauri/src/huggingface_download.rs` +- Adicionar comando `download_gguf_model` em `lib.rs` + +### 1.2 Busca de Modelos (`list_huggingface_gguf_models`) + +**Contrato:** + +```rust +pub async fn list_huggingface_gguf_models( + query: Option, + quantization: Option, +) -> Result, String> +``` + +**Implementação:** + +- Usar API pública do Hugging Face: `https://huggingface.co/api/models?search={query}&filter=gguf` +- Filtrar por arquivos `.gguf` no tree +- Cache local com TTL de 1 hora (evitar requests repetidos) +- **Extrair quantização do filename** (regex corrigido: `[._-](Q\d+_?[KM]?_?[MS]?|[FI]P?\d+)[._-]`) + - Suporta: `Q4_K_M`, `Q5_0`, `F16`, `FP32`, etc. + +**Arquivos:** + +- Adicionar função em `huggingface_download.rs` +- Comando `list_huggingface_gguf_models` em `lib.rs` + +### 1.3 Cache de Metadados + +**Estrutura:** + +- Arquivo JSON em `{app_data_dir}/cache/huggingface_models.json` +- Estrutura: `{ "models": [...], "last_updated": "ISO8601" }` +- Atualizar cache apenas se `last_updated` > 1 hora + +## Fase 2: Backend - Integração llama.cpp + +### 2.1 Dependências (`Cargo.toml`) + +**Correção crítica:** Usar `llama_cpp` (edgenai) versão 0.4 em vez de `llama-cpp-rs` 0.2 + +```toml +[features] +default = ["llama-cpp"] +llama-cpp = ["dep:llama_cpp"] +dev-no-llama = [] # Skip llama.cpp em dev builds (builds 30-45 min podem ser lentos) + +[dependencies] +llama_cpp = { version = "0.4", optional = true } + +# Features condicionais por OS (Metal/CUDA são mutuamente exclusivos) +[target.'cfg(target_os = "macos")'.dependencies] +llama_cpp = { version = "0.4", features = ["metal"], optional = true } + +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +llama_cpp = { version = "0.4", features = ["cuda"], optional = true } + +[target.'cfg(target_os = "windows")'.dependencies] +llama_cpp = { version = "0.4", features = ["cuda"], optional = true } + +# Dependência para parsing de metadados GGUF +gguf = "0.1" +lru = "0.12" # Para LRU cache de modelos carregados +``` + +**Build considerations:** + +- `llama_cpp` compila `llama.cpp` via build script +- **Tempo de build:** 30-45 min em máquinas mais fracas (não 10-15 min) +- Feature flag `dev-no-llama` permite builds rápidos em desenvolvimento +- Requer CMake/Ninja instalados (documentar no README) + +### 2.2 Gerenciamento de Estado (`AppState`) + +**Estrutura (com LRU cache para prevenir memory leaks):** + +```rust +use lru::LruCache; + +pub struct LlamaCppState { + models: Arc>>, // Limite de 3 modelos + default_params: LlamaParams, +} + +impl LlamaCppState { + pub fn new() -> Self { + Self { + models: Arc::new(Mutex::new(LruCache::new(3.try_into().unwrap()))), + default_params: LlamaParams::default(), + } + } +} + +pub struct LlamaModel { + path: PathBuf, + context: Option, // Lazy loading + metadata: ModelMetadata, +} +``` + +**Arquivos:** + +- Criar `src-tauri/src/llama_cpp_state.rs` +- Registrar state em `main()` via `.manage()` + +### 2.3 Comando de Chat Streaming (`chat_stream_native`) + +**Contrato:** + +```rust +#[command] +async fn chat_stream_native( + window: Window, + app_handle: AppHandle, + session_id: Option, + messages: Vec, + model_path: String, + system_prompt: Option, + params: Option, +) -> Result +``` + +**Implementação:** + +- Carregar modelo lazy (verificar se já está em `AppState`) +- Usar `llama-cpp-rs` para streaming de tokens +- Emitir eventos `chat-token` (mesmo formato que `chat_stream` atual) +- Reutilizar lógica de salvamento em DB existente +- Parâmetros: `n_gpu_layers`, `n_ctx`, `threads` (configuráveis via settings) + +**Arquivos:** + +- Criar `src-tauri/src/llama_cpp_chat.rs` +- Comando em `lib.rs` +- Integrar com `ProcessingState` existente + +### 2.4 Configuração de Parâmetros + +**Settings store (frontend):** + +- Adicionar `llamaCppParams` em `settings-store.ts`: + ```typescript + llamaCppParams: { + nGpuLayers: number; + nCtx: number; + threads: number; + } + ``` + + +**Backend:** + +- Struct `LlamaParams` serializável +- Valores padrão: `n_gpu_layers: 0` (CPU), `n_ctx: 4096`, `threads: auto` + +## Fase 3: Backend - Listagem Local + +### 3.1 Scan de Arquivos GGUF (`list_local_gguf_models`) + +**Contrato:** + +```rust +#[command] +fn list_local_gguf_models(app_handle: AppHandle) -> Result, String> +``` + +**Implementação:** + +- Scan recursivo em `{app_data_dir}/models/gguf/` (estrutura hierárquica) +- Usar `walkdir` (já presente) +- **Parsing de metadados GGUF usando crate `gguf`:** + ```rust + use gguf::GGUFFile; + + pub fn extract_gguf_metadata(path: &Path) -> Result { + let file = std::fs::File::open(path).map_err(|e| e.to_string())?; + let gguf = GGUFFile::parse(file).map_err(|e| e.to_string())?; + + Ok(GgufMetadata { + architecture: gguf.header.metadata.get("general.architecture"), + quantization: gguf.header.metadata.get("general.quantization_version"), + parameter_count: gguf.header.metadata.get("general.parameter_count"), + }) + } + ``` + +- Extrair metadados: + - Tamanho do arquivo + - Quantização do filename (regex corrigido) + - **Arquitetura via parsing de header GGUF** (não inferir do filename) +- Cache de metadados para evitar re-scan frequente + +**Estrutura de retorno:** + +```rust +pub struct LocalGgufModel { + pub path: String, + pub name: String, + pub size: u64, + pub quantization: Option, + pub architecture: Option, +} +``` + +**Arquivos:** + +- Função em `lib.rs` ou módulo separado `src-tauri/src/local_models.rs` + +### 3.2 Detecção Automática + +- Ao baixar modelo do Hugging Face, adicionar à lista local automaticamente +- Verificar integridade (tamanho, extensão) antes de listar + +## Fase 4: Frontend - UI Experimental + +### 4.1 Settings Store - Runtime Selection + +**Modificações em `store/settings-store.ts`:** + +```typescript +runtime: 'ollama' | 'llama-cpp'; +llamaCppParams: { nGpuLayers: number; nCtx: number; threads: number }; +setRuntime: (runtime: 'ollama' | 'llama-cpp') => void; +setLlamaCppParams: (params: Partial) => void; +``` + +### 4.2 Toggle de Runtime (`components/settings/`) + +**Componente:** + +- Toggle switch "Usar llama.cpp nativo" +- **Indicador visual melhorado** com ícone + tooltip: + ```tsx + + {runtime === 'ollama' ? ( + <>🦙 Ollama + ) : ( + <>⚡ llama.cpp (nativo) + )} + + + + {runtime === 'ollama' + ? 'Usando Ollama como runtime (requer servidor externo)' + : 'Usando llama.cpp nativo (mais rápido, sem servidor externo)'} + + + ``` + +- Desabilitar se nenhum modelo GGUF local disponível + +**Arquivos:** + +- Criar ou modificar `components/settings/runtime-settings-panel.tsx` + +### 4.3 Seletor de Modelos Hugging Face + +**Componente:** + +- Input de busca +- Lista de modelos com filtros de quantização (Q4, Q5, Q8) +- Botão de download com progresso +- Integrar com `DownloadContext` existente + +**Arquivos:** + +- Criar `components/chat/huggingface-model-selector.tsx` +- Integrar em `components/chat/model-selector.tsx` + +### 4.4 Página de Comparação + +**Componente experimental:** + +- Side-by-side: Ollama vs llama.cpp +- Mesmo prompt, comparar latência e qualidade +- Métricas: tokens/s, tempo total, uso de VRAM + +**Arquivos:** + +- Criar `app/benchmark/page.tsx` (opcional, pode ser feature flag) + +### 4.5 Integração no Chat + +**Modificações em `hooks/use-chat.ts`:** + +- Verificar `runtime` do settings store +- Chamar `chat_stream` ou `chat_stream_native` conforme runtime +- Manter compatibilidade com código existente + +**Arquivos:** + +- Modificar `hooks/use-chat.ts` +- Atualizar `app/chat/page.tsx` se necessário + +## Decisões Técnicas + +### Abordagem Ingênua vs Sênior + +| Aspecto | Ingênua | Sênior (Escolhida) | + +|---------|---------|-------------------| + +| Download | Polling de status | Streaming com eventos Tauri | + +| Estado de Modelos | Sem cache | **LRU cache com limite de 3 modelos** | + +| Integração Chat | Duplicar código | Abstração via enum Runtime | + +| Validação | Apenas tamanho | **SHA256 obrigatório quando disponível** + tamanho + **parsing header GGUF** | + +| Build llama.cpp | Sempre compilar | **Feature flags condicionais por OS + dev-no-llama** | + +| Rate Limiting | Sem tratamento | **Backoff + Retry-After header** | + +| Parsing Metadata | Inferir do filename | **Crate `gguf` para parsing correto** | + +### Trade-offs + +- **Performance:** llama.cpp nativo pode ser 10-30% mais rápido que Ollama (sem overhead HTTP) +- **Manutenção:** Duplicar lógica de chat aumenta complexidade; abstração Runtime reduz duplicação +- **VRAM:** llama.cpp permite controle fino de `n_gpu_layers`; Ollama gerencia automaticamente + +### Modos de Falha + +1. **Gargalo VRAM:** Modelos grandes podem não carregar; implementar fallback para CPU + + - **Solução:** Usar `sysinfo` crate para detectar VRAM disponível antes de carregar + +2. **Download interrompido:** Retry logic com resume (verificar se arquivo parcial existe) +3. **Build llama.cpp falha:** Feature flag `dev-no-llama` para desabilitar em desenvolvimento +4. **Latência de rede:** Cache agressivo de metadados do Hugging Face +5. **Rate limiting Hugging Face:** Tratar 429 com `Retry-After` header +6. **Concurrent downloads:** Limitar a 2 downloads paralelos com semáforo +7. **Memory leaks:** LRU cache com limite de 3 modelos previne crescimento indefinido + +### Antipadrões a Evitar + +- Não fazer polling de status de download (usar eventos) +- Não carregar todos os modelos na inicialização (lazy loading) +- Não duplicar lógica de streaming (abstrair Runtime) +- Não hardcodar paths (usar `app_data_dir`) + +## Ordem de Implementação (Reordenada) + +1. **Estrutura de diretórios e AppState** (fundação) + + - Criar `get_models_dir()` com paths cross-platform + - Implementar `LlamaCppState` com LRU cache + +2. **Listagem local de GGUF** (valida estrutura) + + - `list_local_gguf_models()` com parsing via crate `gguf` + - Cache de metadados + +3. **Download Hugging Face** (adiciona modelos) + + - `download_gguf_model()` com SHA256 obrigatório + - Rate limiting e retry logic + +4. **Parsing de metadados GGUF** (enriquece listagem) + + - Integrar crate `gguf` para extrair arquitetura + +5. **Integração llama.cpp** (runtime) + + - `chat_stream_native()` com lazy loading + - Detecção de VRAM via `sysinfo` + +6. **UI experimental** (camada final) + + - Toggle de runtime com indicador melhorado + - Integração no `use-chat.ts` + +## Validação + +**Comandos de teste (Windows PowerShell):** + +```powershell +# Verificar modelos baixados +Get-ChildItem "$env:APPDATA\OllaHub\models\gguf" -Recurse -Filter "*.gguf" + +# Testar download (via Tauri command) +# Usar DevTools do app para invocar: download_gguf_model +``` + +**Comandos de teste (Linux Bash):** + +```bash +# Verificar modelos baixados +find ~/.local/share/com.ollahub.app/models/gguf -name "*.gguf" + +# Testar build llama.cpp (com feature flag) +cargo build --features llama-cpp # Build completo +cargo build --no-default-features --features dev-no-llama # Build rápido (dev) + +# Verificar VRAM disponível (se NVIDIA) +nvidia-smi --query-gpu=memory.free --format=csv +``` + +## Correções Críticas Aplicadas + +### 1. Crate llama.cpp + +- ✅ Alterado de `llama-cpp-rs = "0.2"` para `llama_cpp = "0.4"` (edgenai, mais estável) + +### 2. Feature Flags + +- ✅ Features condicionais por OS (Metal/CUDA mutuamente exclusivos) +- ✅ Feature flag `dev-no-llama` para builds rápidos em desenvolvimento + +### 3. Parsing de Metadados + +- ✅ Adicionada dependência `gguf = "0.1"` para parsing correto de headers GGUF +- ✅ Removida inferência de arquitetura do filename + +### 4. Memory Management + +- ✅ Substituído `HashMap` por `LruCache` com limite de 3 modelos + +### 5. Validação SHA256 + +- ✅ Tornada obrigatória quando disponível no Hugging Face + +### 6. Rate Limiting + +- ✅ Implementado tratamento de 429 com `Retry-After` header + +### 7. Estrutura de Diretórios + +- ✅ Hierárquica: `{repo_owner}/{repo_name}/{quantization}/model.gguf` + +### 8. Regex de Quantização + +- ✅ Corrigido: `[._-](Q\d+_?[KM]?_?[MS]?|[FI]P?\d+)[._-]` + +### 9. Cross-Platform Paths + +- ✅ Validação de `app_data_dir` para Windows/Linux/macOS + +### 10. Indicador de Runtime + +- ✅ Melhorado com ícone + tooltip explicativo + +### 11. Ordem de Implementação + +- ✅ Reordenada considerando dependências técnicas + +### 12. Riscos Adicionais + +- ✅ VRAM detection via `sysinfo` +- ✅ Limite de downloads paralelos (semáforo) +- ✅ Documentação de requisitos (CMake/Ninja) \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95b99eb..6024c69 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -96,6 +96,62 @@ jobs: fi echo "Frontend build successful - 'out' directory exists" +<<<<<<< HEAD + - name: Prepare bundle resources (Windows only) + if: matrix.platform == 'windows-latest' + shell: powershell + run: | + # Criar diretórios + New-Item -ItemType Directory -Force -Path "src-tauri/bundle/ollama" | Out-Null + New-Item -ItemType Directory -Force -Path "src-tauri/bundle/models/embeddings" | Out-Null + + # Baixar instalador do Ollama (se não existir) + $ollamaPath = "src-tauri/bundle/ollama/OllamaSetup.exe" + if (!(Test-Path $ollamaPath)) { + Write-Host "Downloading Ollama installer..." + Invoke-WebRequest -Uri "https://ollama.com/download/OllamaSetup.exe" -OutFile $ollamaPath -UseBasicParsing + } + + # Baixar modelos de embeddings (se não existirem) + $modelPath = "src-tauri/bundle/models/embeddings/all-MiniLM-L6-v2.onnx" + if (!(Test-Path $modelPath)) { + Write-Host "Downloading ONNX embedding model (~90MB, this may take a while)..." + Invoke-WebRequest -Uri "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx" -OutFile $modelPath -UseBasicParsing + } + + $tokenizerPath = "src-tauri/bundle/models/embeddings/tokenizer.json" + if (!(Test-Path $tokenizerPath)) { + Write-Host "Downloading tokenizer..." + Invoke-WebRequest -Uri "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json" -OutFile $tokenizerPath -UseBasicParsing + } + + # Baixar e extrair ONNX Runtime DLL + $ortDllPath = "src-tauri/bundle/models/embeddings/onnxruntime.dll" + if (!(Test-Path $ortDllPath)) { + $ortZipPath = "src-tauri/bundle/models/embeddings/onnxruntime.zip" + if (!(Test-Path $ortZipPath)) { + Write-Host "Downloading ONNX Runtime library (~50MB)..." + Invoke-WebRequest -Uri "https://github.com/microsoft/onnxruntime/releases/download/v1.20.1/onnxruntime-win-x64-1.20.1.zip" -OutFile $ortZipPath -UseBasicParsing + } + + Write-Host "Extracting ONNX Runtime DLL..." + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [System.IO.Compression.ZipFile]::OpenRead($ortZipPath) + foreach ($entry in $zip.Entries) { + if ($entry.Name -eq "onnxruntime.dll") { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $ortDllPath, $true) + Write-Host "Extracted onnxruntime.dll" + break + } + } + $zip.Dispose() + Remove-Item $ortZipPath -ErrorAction SilentlyContinue + } + + Write-Host "Bundle resources prepared successfully" + +======= +>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 - name: Build Tauri app run: | cd src-tauri diff --git a/.gitignore b/.gitignore index 2146da4..6920664 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,14 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts ~/.tauri/ollahub.key + +# tauri build artifacts +src-tauri/target/ +src-tauri/bundle/ +*.exe +*.msi +*.AppImage +*.deb +*.dmg +OllamaSetup.exe +src-tauri/bundle/ollama/ diff --git a/LICENSE b/LICENSE index a86aaf8..eb2dd65 100644 --- a/LICENSE +++ b/LICENSE @@ -17,4 +17,11 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 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 \ No newline at end of file +Website: https://evandro.dev.br + + + + + + + diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 39b39dd..6d6bdba 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -5,6 +5,7 @@ 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, RefreshCw, Cpu } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { ModelSelector } from "@/components/chat/model-selector"; import { ChatInput } from "@/components/chat/chat-input"; import { ChatMessage } from "@/components/chat/chat-message"; import { ReasoningChain } from "@/components/chat/reasoning-chain"; @@ -13,6 +14,7 @@ import { TitleBar } from "@/components/titlebar"; import { SidebarList } from "@/components/chat/sidebar-list"; import { useChat } from "@/hooks/use-chat"; import { useLocalModels } from "@/hooks/use-local-models"; +import { useModelSelection } from "@/hooks/use-model-selection"; import { useChatStorage } from "@/hooks/use-chat-storage"; import { useAutoLabelingModel } from "@/hooks/use-auto-labeling-model"; import { useWebSearch } from "@/hooks/use-web-search"; @@ -24,9 +26,10 @@ import { useDeepResearch } from "@/hooks/use-deep-research"; import { DEEP_RESEARCH_PROMPTS } from "@/data/prompts/deep-research"; import { useSettingsStore } from "@/store/settings-store"; import type { ScrapedContent } from "@/services/webSearch"; -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useRef, useCallback, startTransition } from "react"; import { useTheme } from "next-themes"; import { useRouter } from "next/navigation"; +import { invoke } from "@tauri-apps/api/core"; import { ImperativePanelHandle } from "react-resizable-panels"; import { ModelDownloadDialog } from "@/components/chat/model-download-dialog"; import { chatLog } from "@/lib/terminal-logger"; @@ -43,6 +46,7 @@ export default function ChatPage() { const { messages, setMessages, sendMessage, isLoading, stop, clearChat } = useChat(); const { models, refresh } = useLocalModels(); const { theme, setTheme } = useTheme(); + const { isSetupCompleted } = useSettingsStore(); const { sessions, @@ -112,9 +116,68 @@ export default function ChatPage() { })); }, [setMessages]); - const [selectedModel, setSelectedModel] = useState(""); + const { selectedModel, setSelectedModel } = useModelSelection(); + + // Sincronizar eventos de busca com thinking messages + useEffect(() => { + if (webSearch.activeQueries.length > 0 || webSearch.activeUrls.length > 0) { + updateThinkingMessage('web-research', { + activeQueries: webSearch.activeQueries, + activeUrls: webSearch.activeUrls, + }); + } + }, [webSearch.activeQueries, webSearch.activeUrls, updateThinkingMessage]); const [mounted, setMounted] = useState(false); const initializedRef = useRef(false); + + // Refs para evitar loops e execuções múltiplas + const hasCheckedIntegrityRef = useRef(false); + const isRedirectingRef = useRef(false); + + // Guarda de roteamento: verificar se setup foi completado + useEffect(() => { + if (!isSetupCompleted && !isRedirectingRef.current) { + isRedirectingRef.current = true; + startTransition(() => { + router.replace("/setup"); // Mudar de push para replace para consistência + }); + } + }, [isSetupCompleted, router]); + + // Guarda de integridade: verificar se Ollama está funcional + // Executa SEMPRE que o componente montar, independente do status do setup + useEffect(() => { + if (hasCheckedIntegrityRef.current) return; // Proteção contra múltiplas execuções + if (isRedirectingRef.current) return; // Evitar redirecionamentos múltiplos + + const checkIntegrity = async () => { + try { + const isOllamaIntact = await invoke('verify_ollama_integrity_command'); + + if (!isOllamaIntact && !isRedirectingRef.current) { + isRedirectingRef.current = true; + // Ollama não está funcional - redirecionar para setup + console.warn("Integridade do Ollama falhou no chat. Redirecionando para Setup."); + startTransition(() => { + router.replace('/setup'); + }); + } + } catch (e) { + // Em caso de erro na verificação, redirecionar para setup (fallback seguro) + if (!isRedirectingRef.current) { + isRedirectingRef.current = true; + console.error("Falha ao verificar integridade do Ollama no chat:", e); + startTransition(() => { + router.replace('/setup'); + }); + } + } finally { + hasCheckedIntegrityRef.current = true; // Marcar como executado + } + }; + + checkIntegrity(); + }, [router]); // Remover isSetupCompleted das dependências - executar sempre // Initialize with default format prompt const [systemPrompt] = useState(defaultFormatPrompt || "Você é um assistente útil e prestativo."); const [isChatsSidebarCollapsed, setIsChatsSidebarCollapsed] = useState(true); // Iniciar colapsada @@ -137,25 +200,10 @@ export default function ChatPage() { } }, [searchQuery, currentSessionId]); - // Auto-select first model useEffect(() => { setMounted(true); }, []); - useEffect(() => { - if (initializedRef.current) return; - const stored = settings.selectedModel; - const first = models.length > 0 ? models[0].name : ""; - const next = stored || first || ""; - if (next) { - setSelectedModel(next); - if (!stored) { - settings.setSelectedModel(next); - } - } - initializedRef.current = true; - }, [settings, models]); - // Auto-scroll para o final durante streaming useEffect(() => { if (messagesEndRef.current && (isLoading || messages.length > 0)) { @@ -425,7 +473,10 @@ export default function ChatPage() { // Executar busca progressiva para cada query do plano const searchPromises = searchPlan.map(async (query, idx) => { - chatLog.info(` → Starting progressive search ${idx + 1}/${searchPlan.length}: "${query}"`); + const queryStartTime = Date.now(); + chatLog.info(`\n[WebResearch] ========== QUERY ${idx + 1}/${searchPlan.length} ==========`); + chatLog.info(`[WebResearch] Query: "${query}"`); + chatLog.info(`[WebResearch] Timestamp: ${new Date().toISOString()}`); // Atualizar progresso updateThinkingMessage('web-research', { @@ -438,6 +489,22 @@ export default function ChatPage() { const context = deepResearch.state.context; const enrichedQueries = deepResearch.state.enrichedQueries; + if (context) { + chatLog.info(`[WebResearch] Using contextual analysis:`, { + intent: context.intent, + entities: context.entities.length, + topics: context.topics.length, + }); + } + + if (enrichedQueries) { + chatLog.info(`[WebResearch] Using enriched queries:`, { + literal: enrichedQueries.literal.length, + semantic: enrichedQueries.semantic.length, + related: enrichedQueries.related.length, + }); + } + // Executar busca progressiva com semantic search se disponível const fallbackResult = await executeProgressiveSearch( query, @@ -461,22 +528,47 @@ export default function ChatPage() { } ); - // Log do resultado - chatLog.info(` ✓ Progressive search ${idx + 1} complete:`); - chatLog.info(` - Success: ${fallbackResult.success}`); - chatLog.info(` - Total results: ${fallbackResult.scrapedSources.length}`); - chatLog.info(` - Total analyzed: ${fallbackResult.totalResultsAnalyzed}`); - chatLog.info(` - Attempts: ${fallbackResult.attempts.length}`); - chatLog.info(` - Used fallback: ${fallbackResult.usedFallback}`); + const queryDuration = Date.now() - queryStartTime; - // Log detalhado de cada resultado - fallbackResult.scrapedSources.forEach((result, resultIdx) => { - chatLog.info(` → Result ${resultIdx + 1}: ${result.title}`); - chatLog.info(` URL: ${result.url}`); - const contentLength = result.markdown?.length || 0; - chatLog.info(` Content length: ${contentLength} chars`); + // Log detalhado do resultado + chatLog.info(`\n[WebResearch] ========== QUERY ${idx + 1} RESULTS ==========`); + chatLog.info(`[WebResearch] Duration: ${queryDuration}ms`); + chatLog.info(`[WebResearch] Success: ${fallbackResult.success}`); + chatLog.info(`[WebResearch] Total results: ${fallbackResult.scrapedSources.length}`); + chatLog.info(`[WebResearch] Total analyzed: ${fallbackResult.totalResultsAnalyzed}`); + chatLog.info(`[WebResearch] Attempts: ${fallbackResult.attempts.length}`); + chatLog.info(`[WebResearch] Used fallback: ${fallbackResult.usedFallback}`); + + // Log detalhado de cada tentativa + fallbackResult.attempts.forEach((attempt, attemptIdx) => { + chatLog.info(`\n Attempt ${attemptIdx + 1}:`); + chatLog.info(` Round: ${attempt.round}`); + chatLog.info(` Strategy: ${attempt.strategy || 'N/A'}`); + chatLog.info(` Query: "${attempt.query}"`); + chatLog.info(` Results: ${attempt.results.length}`); + chatLog.info(` Relevance score: ${attempt.relevanceScore.toFixed(3)}`); + chatLog.info(` Duration: ${attempt.duration}ms`); }); + // Log detalhado de cada resultado extraído + if (fallbackResult.scrapedSources.length > 0) { + chatLog.info(`\n[WebResearch] Extracted Sources:`); + fallbackResult.scrapedSources.forEach((result, resultIdx) => { + chatLog.info(`\n [${resultIdx + 1}] ${result.title}`); + chatLog.info(` URL: ${result.url}`); + const contentLength = result.markdown?.length || result.content?.length || 0; + const snippetLength = result.snippet?.length || 0; + chatLog.info(` Markdown length: ${contentLength} chars`); + chatLog.info(` Snippet length: ${snippetLength} chars`); + chatLog.info(` Cached: ${result.cached ? 'Yes' : 'No'}`); + if (result.markdown) { + const preview = result.markdown.substring(0, 200); + chatLog.info(` Preview: ${preview}${contentLength > 200 ? '...' : ''}`); + } + }); + } + chatLog.info(`[WebResearch] ============================================\n`); + // Adicionar resultados à Knowledge Base if (fallbackResult.scrapedSources.length > 0) { deepResearch.addToKnowledgeBase(query, fallbackResult.scrapedSources); @@ -516,13 +608,14 @@ export default function ChatPage() { chatLog.info(`All searches complete. Knowledge Base size: ${deepResearch.state.knowledgeBase.length}`); // Coletar todas as fontes para exibição - scrapedSources = deepResearch.state.knowledgeBase.map(entry => ({ - url: entry.sourceUrl, - title: entry.title, - content: entry.content, - markdown: entry.content, - snippet: entry.content.substring(0, 200) - })); + scrapedSources = deepResearch.state.knowledgeBase.map(entry => ({ + url: entry.sourceUrl, + title: entry.title, + content: entry.content, + markdown: entry.content, + snippet: entry.content.substring(0, 200), + cached: false + })); // Adicionar mensagem: Fontes encontradas if (scrapedSources.length > 0) { @@ -535,56 +628,84 @@ export default function ChatPage() { if (deepResearch.state.knowledgeBase.length > 0) { // Passo 3: Validação e Contexto - chatLog.info('\nStep 3: Validation'); + chatLog.info('\n[DeepResearch] ========== STEP 3: VALIDATION =========='); deepResearch.setStep('aggregating'); // Adicionar mensagem: Processamento addThinkingMessage('processing', 'Processando contexto...', 'running'); + const validationStart = Date.now(); validationReport = await deepResearch.validate(selectedModel, content); - chatLog.info(`Validation Report Length: ${validationReport.length} chars`); + const validationDuration = Date.now() - validationStart; + + chatLog.info(`[DeepResearch] Validation duration: ${validationDuration}ms`); + chatLog.info(`[DeepResearch] Validation report length: ${validationReport.length} chars`); + if (validationReport) { + const preview = validationReport.substring(0, 500); + chatLog.info(`[DeepResearch] Validation preview: ${preview}${validationReport.length > 500 ? '...' : ''}`); + } // Passo 4: Obter contexto curado (usando versão otimizada) - chatLog.info('\nStep 4: Getting Curated Context (Optimized)'); + chatLog.info('\n[DeepResearch] ========== STEP 4: CONTEXT CONDENSATION =========='); // Calcular tokens disponíveis dinamicamente const { getModelContextInfo } = await import('@/lib/model-context'); const contextInfo = await getModelContextInfo(selectedModel, systemPrompt, messages); const availableTokens = contextInfo.recommendedContextWindow; - chatLog.info(`Model Context Window: ${contextInfo.maxContextWindow} tokens`); - chatLog.info(`Available Tokens for KB: ${availableTokens} tokens`); + chatLog.info(`[DeepResearch] Model: ${selectedModel}`); + chatLog.info(`[DeepResearch] Max context window: ${contextInfo.maxContextWindow} tokens`); + chatLog.info(`[DeepResearch] Available tokens for KB: ${availableTokens} tokens`); + const condensationStart = Date.now(); const optimizedResult = deepResearch.getCuratedContextOptimized(content, availableTokens); + const condensationDuration = Date.now() - condensationStart; knowledgeBaseContext = optimizedResult.context; - chatLog.info(`Knowledge Base Context Length: ${knowledgeBaseContext.length} chars`); - chatLog.info(`Condensation Method: ${optimizedResult.result.method}`); - chatLog.info(`Chunks Used: ${optimizedResult.result.chunksUsed} / ${optimizedResult.result.chunksTotal}`); - chatLog.info(`Compression Ratio: ${(optimizedResult.result.compressionRatio * 100).toFixed(1)}%`); + chatLog.info(`\n[DeepResearch] ========== CONDENSATION SUMMARY ==========`); + chatLog.info(`[DeepResearch] Duration: ${condensationDuration}ms`); + chatLog.info(`[DeepResearch] Final context length: ${knowledgeBaseContext.length} chars`); + chatLog.info(`[DeepResearch] Method: ${optimizedResult.result.method}`); + chatLog.info(`[DeepResearch] Chunks used: ${optimizedResult.result.chunksUsed} / ${optimizedResult.result.chunksTotal}`); + chatLog.info(`[DeepResearch] Compression ratio: ${(optimizedResult.result.compressionRatio * 100).toFixed(1)}%`); + chatLog.info(`[DeepResearch] Original tokens: ${optimizedResult.result.originalTokens}`); + chatLog.info(`[DeepResearch] Final tokens: ${optimizedResult.result.totalTokens}`); + + // Log detalhado por fonte + if (optimizedResult.result.sources.length > 0) { + chatLog.info(`[DeepResearch] Chunks per source:`); + optimizedResult.result.sources.forEach((source, idx) => { + chatLog.info(` [${idx + 1}] ${source.title}: ${source.chunksUsed} chunks`); + chatLog.info(` URL: ${source.url}`); + }); + } // Log preview do contexto se não for muito longo if (knowledgeBaseContext.length > 0) { + chatLog.info(`\n[DeepResearch] Context preview:`); if (knowledgeBaseContext.length > 2000) { - chatLog.info(`Context Preview (first 2000 chars):\n${knowledgeBaseContext.substring(0, 2000)}...`); + chatLog.info(`${knowledgeBaseContext.substring(0, 2000)}...`); + chatLog.info(`[DeepResearch] (truncated, ${knowledgeBaseContext.length} chars total)`); } else { - chatLog.info(`Full Context:\n${knowledgeBaseContext}`); + chatLog.info(knowledgeBaseContext); } // Validar contexto const { validateCondensedContext } = await import('@/lib/knowledge-base-processor'); const validation = validateCondensedContext(optimizedResult.result, content); + chatLog.info(`\n[DeepResearch] ========== CONTEXT VALIDATION ==========`); if (!validation.isValid) { - chatLog.error(`Context validation failed: ${validation.warnings.join(', ')}`); + chatLog.error(`[DeepResearch] Validation failed: ${validation.warnings.join(', ')}`); } else if (validation.warnings.length > 0) { validation.warnings.forEach(warning => { - chatLog.warn(`⚠️ ${warning}`); + chatLog.warn(`[DeepResearch] ⚠️ ${warning}`); }); } else { - chatLog.info('✓ Context validation passed'); + chatLog.info(`[DeepResearch] ✓ Context validation passed`); } + chatLog.info(`[DeepResearch] ============================================\n`); } else { - chatLog.warn('⚠️ Knowledge Base Context is empty!'); + chatLog.warn('[DeepResearch] ⚠️ Knowledge Base Context is empty!'); } @@ -623,17 +744,21 @@ export default function ChatPage() { } catch (error) { // Tratar erro graciosamente sem quebrar o fluxo const errorMsg = error instanceof Error ? error.message : String(error); - chatLog.warn(`⚠️ Error in Deep Research pipeline (continuing with partial results): ${errorMsg}`); + chatLog.warn(`⚠️ Error in Deep Research pipeline (using local model fallback): ${errorMsg}`); // Atualizar mensagem de processo com aviso (não erro fatal) updateThinkingMessage('web-research', { - status: 'completed', // Marcar como completed mesmo com erro parcial - details: `Alguns erros ocorreram durante a pesquisa: ${errorMsg}. Continuando com resultados parciais...`, + status: 'completed', // Marcar como completed mesmo com erro + details: `Erro na pesquisa web (${errorMsg}). Usando modelo local como fallback.`, duration: Date.now() - webResearchStart, }); + // Garantir que o estado de erro seja registrado para ativar fallback + // O erro já será detectado pelo hasWebSearchError através do estado do hook + chatLog.info('Web search error registered - will use local model fallback'); + // Não definir erro global - permitir que o fluxo continue - // O sistema continuará com conhecimento interno se necessário + // O sistema continuará com conhecimento interno (fallback automático) } } else { if (!webSearch.isEnabled) { @@ -694,8 +819,9 @@ export default function ChatPage() { let enhancedSystemPrompt = systemPrompt; // Se tivermos uma Knowledge Base, usar o prompt STRICT_GENERATION - // Verificar se houve fallback (nenhum resultado relevante encontrado) + // Verificar se houve fallback (nenhum resultado relevante encontrado ou erro na pesquisa) const usedFallback = knowledgeBaseContext.length === 0 || knowledgeBaseContext.length < 100; + const hasWebSearchError = webSearch.status === 'error' || webSearch.error !== null; if (knowledgeBaseContext && knowledgeBaseContext.length > 100) { const strictPrompt = DEEP_RESEARCH_PROMPTS.STRICT_GENERATION @@ -704,25 +830,36 @@ export default function ChatPage() { .replace('{{userQuery}}', content); enhancedSystemPrompt = strictPrompt; - } else if (usedFallback) { + } else if (usedFallback || hasWebSearchError) { // Fallback: usar prompt sem contexto web, mas informando que não encontrou fontes + // Isso garante que o modelo local sempre gere uma resposta mesmo sem dados web + const errorContext = hasWebSearchError && webSearch.error + ? `\n**Motivo:** ${webSearch.error}` + : ''; + enhancedSystemPrompt = `${systemPrompt} -## ⚠️ AVISO IMPORTANTE +## ⚠️ FALLBACK PARA CONHECIMENTO INTERNO -Não foi possível encontrar informações relevantes nas fontes web consultadas após múltiplas tentativas de busca. +Não foi possível acessar fontes web para esta consulta após múltiplas tentativas de busca.${errorContext} -**INSTRUÇÕES:** -- Responda usando APENAS seu conhecimento interno (treinamento) -- Seja honesto e diga: "Não encontrei essa informação nas fontes consultadas." -- Se tiver conhecimento sobre o tópico, compartilhe, mas deixe claro que não há fontes externas verificadas -- Não invente informações ou cite fontes que não foram consultadas +**INSTRUÇÕES CRÍTICAS:** +- Você DEVE responder usando APENAS seu conhecimento interno (treinamento do modelo) +- Seja transparente: informe que não foi possível acessar fontes web, mas que você responderá com base no seu conhecimento +- Se tiver conhecimento sobre o tópico, compartilhe-o de forma útil e detalhada +- Seja honesto sobre limitações: se não souber algo, diga claramente +- NÃO invente informações ou cite fontes que não foram consultadas +- NÃO mencione "fontes consultadas" ou "pesquisa realizada" - apenas responda com seu conhecimento + +**IMPORTANTE:** Este é um fallback legítimo. O usuário espera uma resposta útil mesmo sem dados web. Use seu conhecimento para ajudar da melhor forma possível. ## DATA E HORA ATUAL DO SISTEMA **DATA/HORA FORMATADA:** ${currentDateTime} +**DATA/HORA NUMÉRICA:** ${currentDateExplicit} +**ISO 8601:** ${currentDateISO} -Use esta data exata para referências temporais.`; +Use esta data exata para referências temporais quando relevante.`; } // Fallback para lógica antiga se não tiver Knowledge Base else if (knowledgeBaseContext) { @@ -792,6 +929,9 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente let finalUserContent = content; if (knowledgeBaseContext && knowledgeBaseContext.length > 100) { finalUserContent = `[KNOWLEDGE BASE - ÚNICA FONTE DE VERDADE]\n${knowledgeBaseContext}\n[/KNOWLEDGE BASE]\n\nResponda a pergunta usando APENAS os dados acima. Se a informação não estiver na Knowledge Base, diga "Não encontrei essa informação nas fontes consultadas."\n\nPergunta: ${content}`; + } else if (usedFallback || hasWebSearchError) { + // Quando usar fallback, garantir que o modelo saiba que deve usar conhecimento interno + finalUserContent = `Pergunta do usuário: ${content}\n\nNota: Não foi possível acessar fontes web para esta consulta. Responda usando seu conhecimento interno de forma útil e detalhada.`; } chatLog.info(`Final User Content Length: ${finalUserContent.length} chars`); @@ -822,6 +962,75 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente chatLog.info('✅ Response generation complete'); chatLog.info('========== QUERY COMPLETE ==========\n'); +<<<<<<< HEAD + // Capturar dados antes do setMessages para garantir que estão no escopo correto + const currentScrapedSources = scrapedSources.length > 0 + ? scrapedSources + : deepResearch.state.knowledgeBase.map(entry => ({ + url: entry.sourceUrl, + title: entry.title, + content: entry.content, + markdown: entry.content, + cached: false, + })); + + // Coletar thinking steps das mensagens (apenas os relacionados a esta query) + const allThinkingSteps: ThinkingMessageMetadata[] = []; + const currentMessages = messages; // Capturar referência atual + currentMessages.forEach((msg, idx) => { + // Coletar thinking steps das últimas mensagens (relacionadas a esta query) + if (idx >= currentMessages.length - 10) { // Últimas 10 mensagens + if (msg.metadata && typeof msg.metadata === 'object' && 'type' in msg.metadata && msg.metadata.type === 'thinking') { + allThinkingSteps.push(msg.metadata as ThinkingMessageMetadata); + } + } + }); + + // Coletar sites pesquisados com detalhes completos + const sitesResearched = currentScrapedSources.map((source, idx) => { + // Tentar encontrar informações de duração e status dos logs + const sourceLog = deepResearch.state.logs.find(log => { + if (!log.parsedOutput || typeof log.parsedOutput !== 'object') return false; + const parsed = log.parsedOutput as Record; + return 'url' in parsed && parsed.url === source.url; + }); + + let duration: number | undefined = undefined; + if (sourceLog?.parsedOutput && typeof sourceLog.parsedOutput === 'object') { + const parsed = sourceLog.parsedOutput as Record; + if ('duration' in parsed && typeof parsed.duration === 'number') { + duration = parsed.duration; + } + } + + return { + url: source.url, + title: source.title || 'Sem título', + status: source.cached ? 'cached' : 'scraped', + source: source.cached ? 'cache' : 'nodriver', + contentLength: source.content?.length || source.markdown?.length || 0, + scrapedAt: sourceLog?.timestamp || Date.now(), + duration: duration, + }; + }); + + // Coletar dados de raciocínio dos logs do deep research + const reasoningSteps = deepResearch.state.logs + .filter(log => log.stage === 'generation' || log.stage === 'validation') + .map(log => ({ + stage: log.stage, + input: log.input, + output: log.rawOutput || JSON.stringify(log.parsedOutput), + timestamp: log.timestamp, + })); + + // Estimar token usage (aproximado) + const estimatedInputTokens = Math.ceil((enhancedSystemPrompt.length + finalUserContent.length) / 4); + const lastAssistantMsg = currentMessages.find(m => m.role === 'assistant'); + const estimatedOutputTokens = Math.ceil((lastAssistantMsg?.content?.length || 0) / 4); + +======= +>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 // Construir dados de debug para a última mensagem do assistente setMessages(prev => { const newMessages = [...prev]; @@ -834,6 +1043,16 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente latency: Date.now() - responseStart, systemPrompt: enhancedSystemPrompt, userQuery: content, +<<<<<<< HEAD + contextUsed: prev.filter(m => m.role !== 'system' && m.role !== 'assistant'), + webResearch: { + queries: deepResearch.state.plan || [], + enrichedQueries: deepResearch.state.enrichedQueries || undefined, + sources: currentScrapedSources, + sitesResearched: sitesResearched, + logs: deepResearch.state.logs || [], + plan: deepResearch.state.plan || [], +======= contextUsed: messages.filter(m => m.role !== 'system'), webResearch: { queries: deepResearch.state.plan || [], @@ -841,15 +1060,39 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente sources: scrapedSources, logs: deepResearch.state.logs, plan: deepResearch.state.plan, +>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 knowledgeBase: deepResearch.state.knowledgeBase.map(entry => ({ sourceUrl: entry.sourceUrl, title: entry.title, content: entry.content, })), +<<<<<<< HEAD + activeQueries: webSearch.activeQueries.length > 0 ? webSearch.activeQueries : undefined, + activeUrls: webSearch.activeUrls.length > 0 ? webSearch.activeUrls : undefined, + }, + deepResearchState: deepResearch.state, + thinkingSteps: allThinkingSteps.length > 0 ? allThinkingSteps : undefined, + reasoning: reasoningSteps.length > 0 ? { + steps: reasoningSteps.map(r => `${r.stage}: ${r.input.substring(0, 100)}...`), + intermediateResults: reasoningSteps.map(r => ({ + stage: r.stage, + output: r.output, + timestamp: r.timestamp, + })), + } : undefined, + finalResponse: lastMsg.content || '', + rawResponse: lastMsg.content || '', + tokenUsage: { + input: estimatedInputTokens, + output: estimatedOutputTokens, + total: estimatedInputTokens + estimatedOutputTokens, + }, +======= }, deepResearchState: deepResearch.state, finalResponse: lastMsg.content, rawResponse: lastMsg.content, +>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 }; // Armazenar debug data usando o índice da mensagem @@ -1030,7 +1273,7 @@ Ao responder sobre fatos atuais ou notícias, inicie mencionando explicitamente {/* Main Content Area */} -
+
- - - +
); } - diff --git a/app/globals.css b/app/globals.css index c200a2f..44a5283 100644 --- a/app/globals.css +++ b/app/globals.css @@ -376,6 +376,16 @@ background: transparent; } +/* Utility class para ocultar scrollbar */ +.scrollbar-hide { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.scrollbar-hide::-webkit-scrollbar { + display: none; /* Chrome, Safari and Opera */ +} + /* Reasoning Chain animations */ @keyframes reasoning-fade-in { from { diff --git a/app/layout.tsx b/app/layout.tsx index 33ca57d..e96ce4d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -4,6 +4,13 @@ import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; import { AppUpdaterProvider } from "@/components/app-updater-provider"; import { KeyboardShortcuts } from "@/components/keyboard-shortcuts"; +<<<<<<< HEAD +import { DownloadProvider } from "@/contexts/download-context"; +import { GlobalDownloadStatusBar } from "@/components/global-download-status-bar"; +import { OllamaHealthProvider } from "@/contexts/ollama-health-context"; +import { ServiceInterruptionOverlay } from "@/components/ui/service-interruption-overlay"; +======= +>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 const geistSans = Geist({ variable: "--font-geist-sans", @@ -40,9 +47,21 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > +<<<<<<< HEAD + + + + {children} + + + + + +======= {children} +>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 diff --git a/app/page.tsx b/app/page.tsx index 863531a..2309203 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,15 @@ 'use client'; +<<<<<<< HEAD +import { TitleBar } from "@/components/titlebar"; +import { Loader2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useEffect, useState, startTransition, useRef } from "react"; +import { useSettingsStore } from "@/store/settings-store"; +import { invoke } from "@tauri-apps/api/core"; + +export default function Home() { +======= import { useSetupCheck } from "@/hooks/use-setup-check"; import { Hero } from "@/components/landing/hero"; import { InstallModal } from "@/components/modals/install-modal"; @@ -11,8 +21,111 @@ import { useEffect, startTransition } from "react"; export default function Home() { const { status, ollamaStatus, checkingStep, recheck } = useSetupCheck(); +>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 const router = useRouter(); + const { setSetupCompleted } = useSettingsStore(); + const [isHydrated, setIsHydrated] = useState(false); + const [isChecking, setIsChecking] = useState(true); + + // Refs para evitar loops e execuções múltiplas + const hasCheckedRef = useRef(false); + const isRedirectingRef = useRef(false); +<<<<<<< HEAD + // Hydration check: aguardar Zustand carregar do localStorage + useEffect(() => { + setIsHydrated(true); + }, []); + + // Guarda de roteamento: verificação de integridade do Ollama + flag persistente + // Prioridade: Integridade do Sistema > Flag de Setup + useEffect(() => { + if (!isHydrated) return; + if (hasCheckedRef.current) return; // Proteção contra múltiplas execuções + + const performCheck = async () => { + if (isRedirectingRef.current) return; // Evitar múltiplos redirecionamentos + + setIsChecking(true); + + try { + // 1. Verificação Real do Sistema (Fonte da Verdade) + const isOllamaIntact = await invoke('verify_ollama_integrity_command'); + + // Ler isSetupCompleted diretamente do store (não usar como dependência) + const currentSetupStatus = useSettingsStore.getState().isSetupCompleted; + + if (!isOllamaIntact) { + // Se o motor não existe, força o setup, não importa o que o storage diga + console.warn("Integridade do Ollama falhou. Redirecionando para Setup."); + + if (!isRedirectingRef.current) { + isRedirectingRef.current = true; + // Resetar flag de setup se Ollama não está íntegro (ANTES de redirecionar) + setSetupCompleted(false); + + // Pequeno delay para garantir que o estado seja persistido no localStorage + await new Promise(resolve => setTimeout(resolve, 100)); + + startTransition(() => { + router.replace('/setup'); + }); + } + return; + } + + // 2. Verificação de Fluxo de UI (apenas se Ollama está OK) + if (!isRedirectingRef.current) { + isRedirectingRef.current = true; + + if (currentSetupStatus) { + startTransition(() => { + router.replace('/chat'); + }); + } else { + startTransition(() => { + router.replace('/setup'); + }); + } + } + } catch (e) { + console.error("Falha crítica no check de inicialização", e); + // Fallback seguro: sempre ir para setup em caso de erro + if (!isRedirectingRef.current) { + isRedirectingRef.current = true; + setSetupCompleted(false); + + // Pequeno delay para garantir que o estado seja persistido no localStorage + await new Promise(resolve => setTimeout(resolve, 100)); + + startTransition(() => { + router.replace('/setup'); + }); + } + } finally { + setIsChecking(false); + hasCheckedRef.current = true; // Marcar como executado + } + }; + + performCheck(); + }, [isHydrated, router, setSetupCompleted]); + + // Enquanto não hidratou ou está verificando, retornar skeleton simples + if (!isHydrated || isChecking) { + return ( +
+ +
+
+ +
+ + {!isHydrated ? 'Carregando...' : 'Verificando sistema...'} + + Aguarde um momento... +
+======= // Redirecionar automaticamente se tudo estiver pronto useEffect(() => { if (status === 'ready') { @@ -54,13 +167,29 @@ export default function Home() {
{checkingStep} Aguarde um momento... +>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5
); } + // Após verificação, mostrar loading de redirecionamento (não deve aparecer por muito tempo) return ( +<<<<<<< HEAD +
+ +
+
+ +
+ Redirecionando... + Aguarde um momento... +
+
+
+
+=======
{/* Notificação de boas-vindas */} @@ -108,5 +237,6 @@ export default function Home() { )}
+>>>>>>> 593efd42e091a845dea82ee6646e027bce1e18c5 ); } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index a93a9db..9923615 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState, useEffect, lazy, Suspense } 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'; @@ -22,15 +23,22 @@ import { } from '@/components/ui/dialog'; import { useSettingsStore } from '@/store/settings-store'; import { DomainTagsInput } from '@/components/settings/domain-tags-input'; +import { SearxngOnboarding } from '@/components/settings/searxng-onboarding'; +import { SearxngInstallWizard } from '@/components/settings/searxng-install-wizard'; +import { DockerSettingsPanel } from '@/components/settings/docker-settings-panel'; const HardwareDashboard = lazy(() => import('@/components/settings/HardwareDashboard').then(m => ({ default: m.HardwareDashboard }))); +import { RuntimeSettingsPanel } from '@/components/settings/runtime-settings-panel'; 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 { listen } from '@tauri-apps/api/event'; +import { CheckCircle2, XCircle, Loader2, Download, Trash2, Copy, ExternalLink, Plus, X, BookOpen, GraduationCap, Newspaper, Code, DollarSign, Edit, RotateCcw, Terminal, Power, ArrowLeft, Sparkles, RefreshCw, Settings, Square } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; import { useRouter } from 'next/navigation'; import { SourcesConfig, SourceCategory } from '@/lib/types'; import { TitleBar } from '@/components/titlebar'; import { useAppUpdater } from '@/hooks/use-app-updater'; import { getVersion } from '@tauri-apps/api/app'; +import { useAppUpdater } from '@/hooks/use-app-updater'; +import { getVersion } from '@tauri-apps/api/app'; interface Model { name: string; @@ -43,6 +51,8 @@ export default function SettingsPage() { const { toast } = useToast(); const router = useRouter(); const [isCheckingConnection, setIsCheckingConnection] = useState(false); + const [showSearxngOnboarding, setShowSearxngOnboarding] = useState(false); + const [showSearxngWizard, setShowSearxngWizard] = useState(false); const [connectionStatus, setConnectionStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [availableModels, setAvailableModels] = useState([]); const [isLoadingModels, setIsLoadingModels] = useState(false); @@ -56,7 +66,11 @@ export default function SettingsPage() { const [isLoadingLogs, setIsLoadingLogs] = useState(false); const [appVersion, setAppVersion] = useState(''); const [activeTab, setActiveTab] = useState('overview'); - + + const updater = useAppUpdater(); + const [appVersion, setAppVersion] = useState(''); + const [activeTab, setActiveTab] = useState('overview'); + const updater = useAppUpdater(); // Load models, storage path, and sources config on mount @@ -64,7 +78,12 @@ export default function SettingsPage() { loadModels(); loadStoragePath(); settings.fetchSources(); - + + // Load app version + getVersion() + .then(setAppVersion) + .catch(() => setAppVersion('0.1.0')); + // Load app version getVersion() .then(setAppVersion) @@ -86,20 +105,56 @@ export default function SettingsPage() { }; loadLogs(); - const interval = setInterval(loadLogs, 5000); // Refresh every 5 seconds - return () => clearInterval(interval); + const logsInterval = setInterval(loadLogs, 5000); // Refresh logs every 5 seconds + + // Periodic model check (fallback) + const modelsInterval = setInterval(loadModels, 30000); // Refresh models every 30 seconds + + return () => { + clearInterval(logsInterval); + clearInterval(modelsInterval); + }; + }, []); + + // Listen for model download completion + useEffect(() => { + let unlisten: (() => void) | undefined; + + const setupListener = async () => { + unlisten = await listen('download-progress', (event) => { + try { + const payload = JSON.parse(event.payload); + if (payload.status === 'success') { + loadModels(); + toast({ + title: 'Novo modelo detectado', + description: 'A lista de modelos foi atualizada.', + }); + } + } catch (e) { + console.error('Error parsing download progress:', e); + } + }); + }; + + setupListener(); + + return () => { + if (unlisten) unlisten(); + }; }, []); const loadModels = async () => { setIsLoadingModels(true); try { - const response = await fetch(`${settings.ollamaUrl}/api/tags`); - if (response.ok) { - const data = await response.json(); - setAvailableModels(data.models || []); - } else { - setAvailableModels([]); - } + const models = await invoke>('get_ollama_tags'); + // Mapear para o formato esperado pela interface Model + const mappedModels: Model[] = (models || []).map(model => ({ + name: model.name, + size: model.size ?? 0, + modified_at: model.modified_at ?? '', + })); + setAvailableModels(mappedModels); } catch (error) { console.error('Failed to load models:', error); setAvailableModels([]); @@ -121,28 +176,17 @@ export default function SettingsPage() { const checkConnection = async () => { setIsCheckingConnection(true); setConnectionStatus('idle'); - + try { - const response = await fetch(`${settings.ollamaUrl}/api/tags`, { - method: 'HEAD', + // Verificar conexão tentando listar modelos + await invoke('get_ollama_tags'); + setConnectionStatus('success'); + toast({ + title: 'Conexão bem-sucedida', + description: 'Ollama está acessível neste endpoint.', }); - - if (response.ok) { - setConnectionStatus('success'); - toast({ - title: 'Conexão bem-sucedida', - description: 'Ollama está acessível neste endpoint.', - }); - // Reload models after successful connection - loadModels(); - } else { - setConnectionStatus('error'); - toast({ - title: 'Erro de conexão', - description: `Status: ${response.status}`, - variant: 'destructive', - }); - } + // Reload models after successful connection + loadModels(); } catch (error) { setConnectionStatus('error'); toast({ @@ -231,1066 +275,1523 @@ export default function SettingsPage() { return (
-
+
-
-
-
-

Configurações

-

- Gerencie as preferências do OllaHub -

+
+
+
+

Configurações

+

+ Gerencie as preferências do OllaHub +

+
+ +
- -
-
- - - - Visão Geral - - - General - - - Web Search - - - Sources - - - Tasks - - - System - - + + + + Visão Geral + + + General + + + Web Search + + + Sources + + + SearXNG + + + Tasks + + + System + + - {/* Tab: Visão Geral (Hardware Dashboard) */} - - - - Dashboard de Hardware - - Monitoramento em tempo real do sistema (atualizado a cada 1 segundo) - - - - {activeTab === 'overview' && ( - -
- {[1, 2, 3, 4].map((i) => ( - - -
-
-
-
- - -
-
- - - ))} + {/* Tab: Visão Geral (Hardware Dashboard) */} + + + + Dashboard de Hardware + + Monitoramento em tempo real do sistema (atualizado a cada 1 segundo) + + + + {activeTab === 'overview' && ( + +
+ {[1, 2, 3, 4].map((i) => ( + + +
+
+
+
+ + +
+
+ + + ))} +
+
+ }> + + + )} + {activeTab === 'overview' && ( + +
+ {[1, 2, 3, 4].map((i) => ( + + +
+
+
+
+ + +
+
+ + + ))} +
+
+ }> + + + )} +
+ + + + {/* Tab: General */} + + + + + + Conexão Ollama + + Configure o endpoint do Ollama local + + + +
+ +
+ settings.setOllamaUrl(e.target.value)} + placeholder="http://localhost:11434" + className="flex-1 min-w-0" + /> +
- }> - - - )} -
-
-
- {/* Tab: General */} - - - - Conexão Ollama - - Configure o endpoint do Ollama local - - - -
- -
- settings.setOllamaUrl(e.target.value)} - placeholder="http://localhost:11434" - className="flex-1 min-w-0" - /> - -
-
- -
- - {isLoadingModels ? ( -
- - Carregando modelos... -
- ) : availableModels.length > 0 ? ( - - + - - - {model.name} - - + + + - {model.name.length > 40 && ( - -

{model.name}

+ {settings.selectedModel && ( + +

{settings.selectedModel}

)}
- ))} - - -
- ) : ( - settings.setSelectedModel(e.target.value)} - placeholder="Digite o nome do modelo..." - /> - )} -
-
-
- - - - System Prompt - - Defina a personalidade e comportamento padrão da IA - - - -