14 Commits
Author SHA1 Message Date
evandro f0b7db8c12 docs(readme): redesenha o README com hero em SVG [skip ci] 2026-10-24 04:57:52 -03:00
evandro 2835957e40 docs: redesign do README (padrão TwitterBR-MOD) — dados atualizados
Build and Release / build (appimage, ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 6m25s
Build and Release / build (dmg, macos-latest, aarch64-apple-darwin) (push) Canceled after 0s
Build and Release / build (dmg, macos-latest, x86_64-apple-darwin) (push) Canceled after 0s
Build and Release / build (msi, windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
Build and Release / create-release (push) Canceled after 0s
2026-10-13 21:50:58 -03:00
Evandro Fonseca Junior 3e57b78aa7 Merge pull request #7 from evandrodevbr:develop
Build and Release / build (appimage, ubuntu-latest, x86_64-unknown-linux-gnu) (push) Failing after 6m18s
Build and Release / build (dmg, macos-latest, aarch64-apple-darwin) (push) Canceled after 0s
Build and Release / build (dmg, macos-latest, x86_64-apple-darwin) (push) Canceled after 0s
Build and Release / build (msi, windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
Build and Release / create-release (push) Canceled after 0s
chore: add 'develop' branch to release workflow triggers
2025-11-27 23:04:26 -03:00
Evandro Fonseca Junior cde8e5ac00 chore: add 'develop' branch to release workflow triggers
- Included 'develop' branch in the GitHub Actions release workflow to enable automated releases from the development branch, enhancing CI/CD processes.
2025-11-27 23:03:58 -03:00
Evandro Fonseca Junior 6fdbd4bfe6 chore: update pnpm workspace configuration
- Added root package to the pnpm workspace configuration to streamline dependency management across the project.
2025-11-27 23:02:59 -03:00
Evandro Fonseca Junior d01d26e74f Merge pull request #6 from evandrodevbr:develop
chore: streamline pnpm installation in release workflow
2025-11-27 22:59:06 -03:00
Evandro Fonseca Junior e51a54c568 Merge remote-tracking branch 'origin/master' into develop 2025-11-27 22:58:59 -03:00
Evandro Fonseca Junior a319a9cf8d chore: streamline pnpm installation in release workflow
- Removed duplicate pnpm installation steps in the GitHub Actions release workflow.
- Ensured consistent setup of pnpm across jobs to optimize workflow efficiency.
2025-11-27 22:58:29 -03:00
Evandro Fonseca Junior 1daa6c77ee Merge pull request #5 from evandrodevbr:develop
remove: delete the document "Estabilizar Scraper Rust (Browser Resurrection, Send e Retry)" as it is no longer needed for the project. This document contained analysis, context, and implementation plans for improving the scraper's resilience and functionality.
2025-11-27 22:54:28 -03:00
Evandro Fonseca Junior 3e5727ffc7 remove: delete the document "Estabilizar Scraper Rust (Browser Resurrection, Send e Retry)" as it is no longer needed for the project. This document contained analysis, context, and implementation plans for improving the scraper's resilience and functionality. 2025-11-27 22:53:55 -03:00
Evandro Fonseca Junior 162a6f37f7 Merge pull request #4 from evandrodevbr:app-automatic-att
feat: integrate automatic update checks and enhance settings UI
2025-11-27 22:48:06 -03:00
Evandro Fonseca Junior 6c366eecb0 feat: integrate automatic update checks and enhance settings UI
- Added functionality for automatic update checks in the settings page, allowing users to enable or disable this feature.
- Integrated app version display and update availability notifications.
- Updated settings store to manage auto-check updates state and handle version migration.
- Enhanced layout with a new card for update settings, improving user experience and accessibility.
2025-11-27 22:47:48 -03:00
Evandro Fonseca Junior 6cdc8d4294 Merge pull request #3 from evandrodevbr:optimizations
Optimizations
2025-11-27 21:53:56 -03:00
Evandro Fonseca Junior 7d169d8a61 Merge pull request #1 from evandrodevbr/develop
feat: enhance chat functionality with markdown support and default prompts
2025-11-19 00:40:26 -03:00
24 changed files with 1395 additions and 141 deletions

No files matched your search

+186
View File
@@ -0,0 +1,186 @@
name: Build and Release
on:
push:
branches:
- main
- master
- develop
workflow_dispatch:
env:
TAURI_VERSION: "2.9.4"
jobs:
build:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: 'windows-latest'
target: 'x86_64-pc-windows-msvc'
bundle: 'msi'
- platform: 'macos-latest'
target: 'aarch64-apple-darwin'
bundle: 'dmg'
- platform: 'macos-latest'
target: 'x86_64-apple-darwin'
bundle: 'dmg'
- platform: 'ubuntu-latest'
target: 'x86_64-unknown-linux-gnu'
bundle: 'appimage'
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: ${{ matrix.target }}
- name: Install dependencies (Ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile
- name: Generate changelog
run: pnpm generate-changelog
- name: Build Tauri app
run: |
cd src-tauri
cargo build --release --target ${{ matrix.target }}
- name: Build Tauri bundle
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY }}
run: |
cd src-tauri
if [ -n "$TAURI_PRIVATE_KEY" ]; then
echo "$TAURI_PRIVATE_KEY" > /tmp/private.key
cargo tauri build --target ${{ matrix.target }} --config '{"updater":{"active":true}}'
rm /tmp/private.key
else
cargo tauri build --target ${{ matrix.target }}
fi
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}-${{ matrix.target }}
path: |
src-tauri/target/${{ matrix.target }}/release/bundle/**/*.${{ matrix.bundle }}
src-tauri/target/${{ matrix.target }}/release/bundle/**/*.exe
src-tauri/target/${{ matrix.target }}/release/bundle/**/*.AppImage
src-tauri/target/${{ matrix.target }}/release/bundle/**/*.deb
retention-days: 7
create-release:
needs: build
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 8
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Get version
id: get_version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
- name: Generate changelog for release
run: pnpm generate-changelog
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Extract release notes
id: extract_notes
run: |
if [ -f "data/changelog.json" ]; then
NOTES=$(node -e "
const fs = require('fs');
try {
const changelog = JSON.parse(fs.readFileSync('data/changelog.json', 'utf-8'));
const latestRelease = changelog.releases[0];
if (latestRelease) {
let notes = '## ' + (latestRelease.version === 'unreleased' ? 'Próxima Versão' : 'v' + latestRelease.version) + '\\n\\n';
notes += '**Data:** ' + latestRelease.date + '\\n\\n';
notes += '### Mudanças\\n\\n';
latestRelease.commits.forEach(commit => {
const icon = commit.type === 'feat' ? '✨' : '🐛';
notes += '- ' + icon + ' **' + (commit.type === 'feat' ? 'Nova Funcionalidade' : 'Correção') + ':** ' + commit.message + '\\n';
});
console.log(notes);
} else {
console.log('## Release ${{ steps.get_version.outputs.version }}');
}
} catch (e) {
console.log('## Release ${{ steps.get_version.outputs.version }}');
}
")
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "notes=## Release ${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.get_version.outputs.tag }}
name: Release ${{ steps.get_version.outputs.tag }}
body: |
${{ steps.extract_notes.outputs.notes }}
## Downloads
Baixe o instalador para sua plataforma nos assets abaixo.
draft: false
files: artifacts/**/*
generate_release_notes: false
+1
View File
@@ -39,3 +39,4 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
~/.tauri/ollahub.key
@@ -1,114 +0,0 @@
## 📋 Análise
**Solicitação**: Corrigir falhas de conexão do browser headless e o erro de compilação `Send`, tornar o scraper resiliente com retry e ampliar resultados de busca.
**Objetivo real**: Garantir estabilidade contínua do backend de scraping, mesmo quando o Chrome headless cai, evitando violações de `Send` e melhorando a cobertura de fontes.
## 🔍 Contexto Identificado
### Arquivos Examinados
- `src-tauri/src/lib.rs:1176–1193` get_or_create_browser: cria/reutiliza `Browser` singleton.
- `src-tauri/src/lib.rs:1247–1277` search_web_metadata: comando Tauri que exige `Future + Send`.
- `src-tauri/src/web_scraper.rs:136–240` search_duckduckgo_metadata: parsing HTML, previously segurava `Html` atravessando `await`.
- `src-tauri/src/web_scraper.rs:469–538` search_and_scrape_with_config: coordena scraping paralelo e coleta resultados.
- `src-tauri/src/web_scraper.rs:605–736` fetch_and_convert_sync: cria aba, navega, extrai HTML e usa Readability.
- `src-tauri/src/lib.rs:1306–1399` force_kill_browser: encerra processos Chrome headless.
### Padrões Detectados
- **Singleton Browser** via `State<BrowserState>` com `Arc<Browser>`.
- **Concorrência controlada** com `Semaphore` e `tokio::spawn_blocking` para operações de Chrome.
- **Fallbacks**: limpeza de URL, filtros anti-ads, Readability + fallback por parágrafos.
### Dependências Mapeadas
```
web_scraper (modificado)
├─ headless_chrome::Browser/Tab
├─ reqwest (HTTP)
├─ scraper (Html, Selector) [não Send]
└─ readability, html2text
lib (modificado)
└─ tauri commands (Future + Send obrigatório)
```
## 🎯 Abordagens Viáveis
### Abordagem A: Verificação ativa de Browser + Retry cooperativo
**Conceito**: Validar liveness do `Browser` antes de uso; se cair durante scraping, recriar e repetir uma passada de URLs com limite de tentativas.
```
request → get_or_create_browser (is_alive?) → spawn scraping
↳ erro conn fechada → reset + recreate → retry restante (1x)
```
**Vantagens**: ✓ Simples de integrar, ✓ Minimiza impacto, ✓ Boa resiliência
**Desvantagens**: ✗ Retry global pode repetir algumas URLs
**Complexidade**: O(n) | Tempo: 3–4h | Risco: Médio
### Abordagem B: Pool/Auto-heal por task
**Conceito**: Cada task recupera o browser ao falhar e re-executa só sua URL.
```
spawn_blocking(url) → new_tab → erro conn → sinaliza reset → reobtem browser → reprocessa url
```
**Vantagens**: ✓ Isolamento por URL
**Desvantagens**: ✗ Coordenação de estado entre threads, ✗ Maior complexidade
**Complexidade**: O(n) | Tempo: 5–6h | Risco: Médio-Alto
## 🏆 Recomendação
**Abordagem A** porque:
- Alinha com o padrão singleton já existente.
- Menor acoplamento entre tarefas e estado global.
- Implementação direta com menor risco de condições de corrida.
## ⚠️ Riscos & Mitigações
| Risco | Prob | Impacto | Mitigação |
|-------|------|---------|-----------|
| Loop de retry infinito | Baixa | Médio | Limitar a 1–2 tentativas, backoff curto |
| Recriação concorrente do Browser | Média | Médio | Guardar lock no `BrowserState` e checar antes de recriar |
| Demasiadas abas simultâneas | Média | Médio | Reduzir `max_concurrent_tabs` ao detectar falha |
| DDG bloqueios 429 | Média | Baixo | Rotação User-Agent e backoff |
## 📝 Plano de Execução
1. Ajustes `Send` (consolidação)
- Isolar parsing `scraper::Html` em escopo síncrono que retorna `Vec<SearchResultMetadata>` (já aplicado em parte na função; revisar para todos os caminhos).
- Garantir que nenhum `Html`/`Selector` vive além do `await` nos métodos `search_*`.
- Verificar pontos: `src-tauri/src/web_scraper.rs:136–240` e demais que usem `Html`.
2. Browser Resurrection
- Adicionar `fn is_browser_alive(browser: &Browser) -> bool` em `web_scraper.rs` ou `lib.rs` (ex.: tentar `new_tab()` e descartar; ou `get_tabs()`; se falhar → false).
- Alterar `get_or_create_browser` (`src-tauri/src/lib.rs:1176–1193`) para: se `Some(browser)` e `!is_alive(browser)` → `reset` e `create_browser()`.
- Em `force_kill_browser` (`src-tauri/src/lib.rs:1306–1399`): após kill, setar `BrowserState` para `None` via lock (ou orientar UI a chamar `reset_browser` antes do kill), garantindo recriação no próximo uso.
3. Retry resiliente no Scraper
- Envolver `fetch_and_convert_sync` com camada de retry (máx. 2 tentativas) na orquestração async (`search_and_scrape_with_config` em `src-tauri/src/web_scraper.rs:469–538`).
- Se erro contiver "underlying connection is closed"/timeout global do browser:
- Executar `reset_browser` e `get_or_create_browser` para novo `Arc<Browser)`.
- Reprocessar as URLs que falharam apenas uma vez com o novo browser.
- Reduzir temporariamente `max_concurrent_tabs` para 3 ao detectar queda, evitando sobrecarga imediata.
4. Paginação no DuckDuckGo
- Em `search_duckduckgo` (`src-tauri/src/web_scraper.rs:78–133`): suportar paginação via parâmetro `s=<offset>` até atingir `limit` (30–50).
- Extrair links por página, deduplicar, respeitar `excluded_domains`/ads.
5. Telemetria e Logs
- Logar eventos de recreação de browser, tentativas de retry e motivos.
- Contadores: quedas de browser, tentativas, tempo médio por URL.
6. Testes/Validação
- Caso 1: Forçar `force_kill_browser` durante scraping → validar que a próxima chamada recria o browser e conclui.
- Caso 2: Simular DDG com >= 40 resultados → paginar e coletar sem violar `Send`.
- Caso 3: Stress com `max_concurrent_tabs=5` → se cair, reduzir e recuperar.
## Key Changes (Resumo de Implementação)
- `lib.rs:1176–1193`: `get_or_create_browser` verifica liveness; recria se morto.
- `lib.rs:1306–1399`: sincronia com `reset_browser` ao matar processos.
- `web_scraper.rs:469–538`: retry global (1x) e recriação de browser ao detectar conexão fechada; ajuste dinamicamente `Semaphore` na segunda passada.
- `web_scraper.rs:78–133`: paginação DDG com `s=` offset.
- `web_scraper.rs:136–240`: parsing isolado; garantir que `Html` é dropado antes do `await`.
## Entregáveis
- Código robusto em `web_scraper.rs` e `lib.rs` com:
- Correção total do `Send`.
- Resurreição automática do browser.
- Retry controlado por erro de conexão.
- Paginação para 30–50 resultados.
- Logs melhorados e validação manual automática.
## ❓ Aguardando Aprovação
- [ ] Posso prosseguir com as mudanças propostas?
- [ ] Preferências de `max_concurrent_tabs` e `limit` padrão (sugestão: 5 e 40)?
- [ ] Deseja manter `force_kill_browser` como utilitário avançado, com alerta na UI para reiniciar o scraper?
+66 -21
View File
@@ -1,36 +1,81 @@
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). <div align="center">
## Getting Started <img src="assets/readme/hero.svg" width="100%" alt="OllaHub: desktop chat for your local Ollama models, with web search and MCP tools">
First, run the development server: ![Version 0.1.0](https://img.shields.io/badge/version-0.1.0-555?style=flat-square)
![Tauri 2](https://img.shields.io/badge/Tauri-2-24C8DB?style=flat-square&logo=tauri&logoColor=white)
![Next.js 16](https://img.shields.io/badge/Next.js-16.0.3-000000?style=flat-square&logo=nextdotjs&logoColor=white)
![React 19](https://img.shields.io/badge/React-19-61DAFB?style=flat-square&logo=react&logoColor=black)
![Platforms](https://img.shields.io/badge/platforms-Windows_%7C_macOS_%7C_Linux-1447e6?style=flat-square)
[Quick start](#quick-start) · [Docs](docs/) · [Issues](https://git.evandro.dev.br/evandro/OllaHub/issues)
</div>
## What it is
OllaHub is a desktop chat app for the models you run with [Ollama](https://ollama.com). On first run it scans your hardware, recommends a model and downloads it. After that it chats with streaming, keeps history in a local SQLite database, and can add context from web search, MCP tools and scheduled tasks. The UI is Next.js inside a Tauri 2 window, and a Rust backend does the work.
OllaHub is a client, not a model runtime. It talks to the Ollama API (default `http://localhost:11434`, configurable in Settings), tries to start Ollama when it is installed but stopped, and can download the Ollama installer when it is missing.
## Why use it
- **Local by default.** Prompts go to your own Ollama, and chats live in `ollahub.db` in the app data directory, with FTS5 full-text search. Besides downloads (models, the embedding model, updates), web search is what reaches the internet.
- **Web answers ranked on your machine.** Headless Chrome scrapes Google, Bing, Yahoo, DuckDuckGo and Startpage (order configurable), then local embeddings (`all-MiniLM-L6-v2` on ONNX Runtime) rank and prune the pages, with a BM25-like fallback when the embedding model is unavailable. A deep-research mode runs planning → searching → aggregating → validating → formulating.
- **More than a chat box.** MCP servers run as child processes configured in `mcp_config.json`, their tools can be called from the chat, and cron tasks (search and summarize, notification, custom prompt) keep running in the background.
**When not to use it:** if you want a finished, tested product (version `0.1.0`, no test suite), a model runtime of its own, or a server you can run without a desktop.
## How it works
- **Frontend** (`app/`, `components/`, `hooks/`, `lib/`, `store/`): Next.js 16 with `output: 'export'`, React 19, Tailwind 4, Radix UI and Zustand. It calls the backend through Tauri `invoke` and events.
- **Backend** (`src-tauri/src/`): `lib.rs` holds the commands for chat (`chat_stream`), models, the Ollama lifecycle, sessions, MCP, search, tasks, export and the installer. `db.rs` handles SQLite, `web_scraper.rs` the search engines, `embeddings.rs` the ranking, and `scheduler.rs` the cron tasks stored in `tasks.json`.
- **Models:** list, delete and pull with progress, install a local `.gguf` file, and get a recommendation for your hardware.
- **System monitor:** CPU and RAM stats plus GPU VRAM, usage and temperature. GPU detection uses `wmic` and `nvidia-smi` on Windows and `lspci` on Linux.
- **Updates:** the Tauri updater checks automatically. Its endpoint in `src-tauri/tauri.conf.json` points to a GitHub Releases `latest.json`.
More notes: [`docs/updater-setup.md`](docs/updater-setup.md) (signing keys and updater) and [`docs/style-guide.md`](docs/style-guide.md) (UI spacing, radii, accessibility).
## Quick start
No release is published on this repository yet, so build it locally. You need Node.js 20, pnpm 8, a stable Rust toolchain and the [Tauri 2 prerequisites](https://tauri.app/start/prerequisites/) (the versions come from the CI workflow). On Ubuntu the workflow also installs `libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev patchelf`. Ollama is needed at runtime.
```bash ```bash
npm run dev git clone https://git.evandro.dev.br/evandro/OllaHub.git
# or cd OllaHub
yarn dev pnpm install
# or
pnpm dev pnpm tauri dev # starts `pnpm dev` (Next.js on http://localhost:3000) inside the Tauri window
# or pnpm tauri build # runs `pnpm generate-changelog && pnpm build`, then bundles the installer
bun dev pnpm lint # ESLint
pnpm bump-version # patch | minor | major, optional --commit
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. The workflow [`.github/workflows/release.yml`](.github/workflows/release.yml) builds `msi` (Windows), `dmg` (macOS, Apple Silicon and Intel) and `AppImage` (Linux) on pushes to `main`, `master` and `develop`, and publishes release `v<version>` on pushes to `main` or `master`.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. > [!WARNING]
> **Two things the app does on its own.**
> 1. Opening the chat page pulls the small helper model **`qwen2.5:0.5b`** if it is not installed yet, used for automatic chat titles. Expect an extra download on first use.
> 2. **Closing the window only hides it.** The close request is intercepted (`prevent_close`) so scheduled tasks keep running, but there is no tray icon to bring the window back yet; the tray step is still pending in [`IMPLEMENTATION_GUIDE.md`](IMPLEMENTATION_GUIDE.md).
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. ## Project status
## Learn More Early: version `0.1.0`, 47 commits between 2025-10-27 and 2025-11-27, no automated tests (`package.json` defines `dev`, `build`, `start`, `lint`, `generate-changelog` and `bump-version`). Known limitations:
To learn more about Next.js, take a look at the following resources: - **No tray icon yet:** closing the window hides it (see the warning above).
- **"Start with the system" does nothing yet:** the setting only toggles a stored flag; the code has a `TODO` until `tauri-plugin-autostart` is configured.
- **RAG inside `chat_stream` is not wired:** the `enable_rag` branch is commented out as `TODO`; the frontend assembles web context before the request.
- **Embeddings outside Windows:** the ONNX Runtime library is downloaded automatically only on Windows. On macOS and Linux, provide it in the app data folder or on the system path, otherwise ranking falls back to BM25-like scoring.
- **Unused leftovers:** `src-tauri/scripts/scraper.py` is not called by the Rust backend, and the 188 model files in `data/models/` are not referenced by the app code.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. ## License
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! No published license: all rights reserved.
## Deploy on Vercel Built with [Tauri](https://tauri.app), [Next.js](https://nextjs.org), [Radix UI](https://www.radix-ui.com), [Tailwind CSS](https://tailwindcss.com), [Zustand](https://zustand-demo.pmnd.rs), [Lucide](https://lucide.dev), [headless_chrome](https://github.com/rust-headless-chrome/rust-headless-chrome), [ONNX Runtime](https://onnxruntime.ai) and the [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) model.
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. <div align="center">
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. <sub>Made by <a href="https://evandro.dev.br">Evandro Fonseca Junior</a> · <a href="https://git.evandro.dev.br/evandro">git.evandro.dev.br</a></sub>
</div>
+2
View File
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider"; import { ThemeProvider } from "@/components/theme-provider";
import { AppUpdaterProvider } from "@/components/app-updater-provider";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -35,6 +36,7 @@ export default function RootLayout({
disableTransitionOnChange disableTransitionOnChange
> >
{children} {children}
<AppUpdaterProvider />
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>
+90 -1
View File
@@ -24,11 +24,13 @@ import { useSettingsStore } from '@/store/settings-store';
import { DomainTagsInput } from '@/components/settings/domain-tags-input'; import { DomainTagsInput } from '@/components/settings/domain-tags-input';
import { HardwareDashboard } from '@/components/settings/HardwareDashboard'; import { HardwareDashboard } from '@/components/settings/HardwareDashboard';
import { invoke } from '@tauri-apps/api/core'; 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 } from 'lucide-react'; 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'; import { useToast } from '@/hooks/use-toast';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { SourcesConfig, SourceCategory } from '@/lib/types'; import { SourcesConfig, SourceCategory } from '@/lib/types';
import { TitleBar } from '@/components/titlebar'; import { TitleBar } from '@/components/titlebar';
import { useAppUpdater } from '@/hooks/use-app-updater';
import { getVersion } from '@tauri-apps/api/app';
interface Model { interface Model {
name: string; name: string;
@@ -52,12 +54,20 @@ export default function SettingsPage() {
const [isSavingSources, setIsSavingSources] = useState(false); const [isSavingSources, setIsSavingSources] = useState(false);
const [recentLogs, setRecentLogs] = useState<string[]>([]); const [recentLogs, setRecentLogs] = useState<string[]>([]);
const [isLoadingLogs, setIsLoadingLogs] = useState(false); const [isLoadingLogs, setIsLoadingLogs] = useState(false);
const [appVersion, setAppVersion] = useState<string>('');
const updater = useAppUpdater();
// Load models, storage path, and sources config on mount // Load models, storage path, and sources config on mount
useEffect(() => { useEffect(() => {
loadModels(); loadModels();
loadStoragePath(); loadStoragePath();
settings.fetchSources(); settings.fetchSources();
// Load app version
getVersion()
.then(setAppVersion)
.catch(() => setAppVersion('0.1.0'));
}, [settings.ollamaUrl]); }, [settings.ollamaUrl]);
// Load logs periodically // Load logs periodically
@@ -1095,6 +1105,85 @@ export default function SettingsPage() {
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Atualizações</CardTitle>
<CardDescription>
Configure verificação automática de atualizações
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="auto-check-updates">Verificar Atualizações Automaticamente</Label>
<p className="text-xs text-muted-foreground">
O app verificará novas versões periodicamente
</p>
</div>
<Switch
id="auto-check-updates"
checked={settings.autoCheckUpdates}
onCheckedChange={settings.setAutoCheckUpdates}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Versão Atual</span>
<span className="font-mono font-medium">
{appVersion || 'Carregando...'}
</span>
</div>
{updater.updateAvailable && (
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Nova Versão Disponível</span>
<span className="font-mono font-medium text-primary">
{updater.updateInfo?.version}
</span>
</div>
)}
</div>
<Button
onClick={async () => {
await updater.checkUpdate();
if (updater.updateAvailable) {
toast({
title: 'Atualização disponível',
description: `Versão ${updater.updateInfo?.version} está disponível para download.`,
});
} else {
toast({
title: 'Você está atualizado',
description: 'Não há novas atualizações disponíveis.',
});
}
}}
variant="outline"
className="w-full"
disabled={updater.isChecking}
>
{updater.isChecking ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Verificando...
</>
) : (
<>
<RefreshCw className="w-4 h-4 mr-2" />
Verificar Atualizações
</>
)}
</Button>
{updater.error && (
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive">{updater.error}</p>
</div>
)}
</CardContent>
</Card>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
+59
View File
@@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="420" viewBox="0 0 1200 420" role="img" aria-labelledby="title desc">
<title id="title">OllaHub</title>
<desc id="desc">OllaHub: a desktop window with a chat sidebar, a local Ollama model download in progress, an answer backed by numbered web sources, and Web and MCP toggles in the message box.</desc>
<rect width="1200" height="420" rx="24" fill="#09090B"/>
<g id="title-block" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
<text x="64" y="170" font-size="64" font-weight="700" fill="#FAFAFA">OllaHub</text>
<text x="64" y="226" font-size="28" fill="#FAFAFA">Desktop chat for your local Ollama</text>
<text x="64" y="264" font-size="28" fill="#FAFAFA">models, with web search and MCP.</text>
<text x="64" y="322" font-size="20" fill="#9F9FA9" font-family="ui-monospace, 'SF Mono', Consolas, monospace">Tauri 2 · Rust · Next.js 16 · React 19</text>
</g>
<g id="window" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
<rect x="608" y="52" width="528" height="316" rx="14" fill="#09090B" stroke="#27272A" stroke-width="2"/>
<path d="M622 52 h500 a14 14 0 0 1 14 14 v26 h-528 v-26 a14 14 0 0 1 14 -14 z" fill="#18181B"/>
<text x="628" y="79" font-size="18" fill="#9F9FA9">OllaHub</text>
<g fill="none" stroke="#9F9FA9" stroke-width="2" stroke-linecap="round">
<path d="M1052 72 h14"/>
<rect x="1078" y="65" width="14" height="14" rx="2"/>
<path d="M1106 65 l14 14 M1120 65 l-14 14"/>
</g>
<path d="M608 92 h528" stroke="#27272A" stroke-width="2"/>
<path d="M608 92 h144 v276 h-130 a14 14 0 0 1 -14 -14 z" fill="#18181B"/>
<rect x="622" y="108" width="116" height="34" rx="8" fill="#1447E6"/>
<text x="680" y="131" font-size="18" fill="#FAFAFA" text-anchor="middle">New chat</text>
<rect x="622" y="166" width="100" height="10" rx="5" fill="#27272A"/>
<rect x="622" y="192" width="84" height="10" rx="5" fill="#27272A"/>
<rect x="622" y="218" width="94" height="10" rx="5" fill="#27272A"/>
<rect x="622" y="244" width="72" height="10" rx="5" fill="#27272A"/>
<text x="772" y="128" font-size="18" fill="#FAFAFA" font-family="ui-monospace, 'SF Mono', Consolas, monospace">qwen2.5:0.5b</text>
<rect x="772" y="140" width="344" height="10" rx="5" fill="#27272A"/>
<rect x="772" y="140" width="220" height="10" rx="5" fill="#1447E6"/>
<rect x="772" y="170" width="344" height="116" rx="12" fill="#201E25"/>
<rect x="790" y="190" width="290" height="10" rx="5" fill="#9F9FA9"/>
<rect x="790" y="212" width="300" height="10" rx="5" fill="#9F9FA9"/>
<rect x="790" y="234" width="180" height="10" rx="5" fill="#9F9FA9"/>
<text x="790" y="272" font-size="18" fill="#9F9FA9">Sources</text>
<g font-size="18" fill="#FAFAFA" text-anchor="middle">
<rect x="866" y="254" width="26" height="26" rx="13" fill="#27272A"/>
<text x="879" y="273">1</text>
<rect x="898" y="254" width="26" height="26" rx="13" fill="#27272A"/>
<text x="911" y="273">2</text>
<rect x="930" y="254" width="26" height="26" rx="13" fill="#27272A"/>
<text x="943" y="273">3</text>
</g>
<rect x="772" y="302" width="344" height="40" rx="20" fill="#18181B" stroke="#27272A" stroke-width="2"/>
<rect x="782" y="309" width="60" height="26" rx="13" fill="#27272A"/>
<text x="812" y="328" font-size="18" fill="#FAFAFA" text-anchor="middle">Web</text>
<rect x="850" y="309" width="62" height="26" rx="13" fill="#27272A"/>
<text x="881" y="328" font-size="18" fill="#FAFAFA" text-anchor="middle">MCP</text>
<circle cx="1096" cy="322" r="13" fill="#1447E6"/>
<path d="M1096 328 v-12 M1090 321 l6 -6 l6 6" fill="none" stroke="#FAFAFA" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+40
View File
@@ -0,0 +1,40 @@
'use client';
import { useEffect, useState } from 'react';
import { UpdateDialog } from '@/components/modals/update-dialog';
import { useAppUpdater } from '@/hooks/use-app-updater';
import { useSettingsStore } from '@/store/settings-store';
export function AppUpdaterProvider() {
const { updateAvailable, checkUpdate } = useAppUpdater();
const [isOpen, setIsOpen] = useState(false);
const [hasChecked, setHasChecked] = useState(false);
const autoCheckEnabled = useSettingsStore((state) => state.autoCheckUpdates ?? true);
useEffect(() => {
if (!autoCheckEnabled || hasChecked) return;
// Verificar após um delay para não interferir no carregamento inicial
const timer = setTimeout(() => {
checkUpdate().then(() => {
setHasChecked(true);
});
}, 5000); // 5 segundos após o app carregar
return () => clearTimeout(timer);
}, [autoCheckEnabled, hasChecked, checkUpdate]);
useEffect(() => {
if (updateAvailable && !isOpen) {
setIsOpen(true);
}
}, [updateAvailable, isOpen]);
return (
<UpdateDialog
open={isOpen}
onOpenChange={setIsOpen}
/>
);
}
+153
View File
@@ -0,0 +1,153 @@
'use client';
import { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { Download, Loader2, X, Sparkles } from 'lucide-react';
import { useAppUpdater } from '@/hooks/use-app-updater';
import { useSettingsStore } from '@/store/settings-store';
interface UpdateDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function UpdateDialog({ open, onOpenChange }: UpdateDialogProps) {
const {
currentVersion,
updateAvailable,
updateInfo,
isChecking,
isDownloading,
downloadProgress,
error,
installUpdate,
} = useAppUpdater();
const [dontShowAgain, setDontShowAgain] = useState(false);
const setAutoCheckUpdates = useSettingsStore((state) => state.setAutoCheckUpdates);
const handleInstall = async () => {
if (dontShowAgain) {
setAutoCheckUpdates(false);
}
await installUpdate();
};
const handleClose = () => {
if (dontShowAgain) {
setAutoCheckUpdates(false);
}
onOpenChange(false);
};
if (!updateAvailable && !isChecking) {
return null;
}
return (
<Dialog open={open && updateAvailable} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" />
Atualização Disponível
</DialogTitle>
<DialogDescription>
Uma nova versão do OllaHub está disponível para download.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Versão Atual</span>
<span className="font-mono font-medium">{currentVersion}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Nova Versão</span>
<span className="font-mono font-medium text-primary">
{updateInfo?.version}
</span>
</div>
</div>
{updateInfo?.body && (
<div className="p-3 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground line-clamp-3">
{updateInfo.body}
</p>
</div>
)}
{isDownloading && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Baixando atualização...</span>
<span className="font-medium">{downloadProgress}%</span>
</div>
<Progress value={downloadProgress} className="h-2" />
</div>
)}
{error && (
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive">{error}</p>
</div>
)}
<div className="flex items-center gap-2">
<Checkbox
id="dont-show-updates"
checked={dontShowAgain}
onCheckedChange={(checked) => setDontShowAgain(checked === true)}
/>
<Label
htmlFor="dont-show-updates"
className="text-sm font-normal cursor-pointer"
>
Não verificar atualizações automaticamente
</Label>
</div>
</div>
<div className="flex gap-2 justify-end">
<Button
variant="outline"
onClick={handleClose}
disabled={isDownloading}
>
<X className="h-4 w-4 mr-2" />
Depois
</Button>
<Button
onClick={handleInstall}
disabled={isDownloading || isChecking}
>
{isDownloading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Baixando...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
Atualizar Agora
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
+30
View File
@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
+118
View File
@@ -0,0 +1,118 @@
# Configuração do Sistema de Atualização Automática
Este documento explica como configurar o sistema de atualização automática do OllaHub.
## Pré-requisitos
1. Repositório GitHub configurado
2. Rust e Cargo instalados
3. Tauri CLI instalado
## Passo 1: Gerar Chaves de Assinatura
Execute o script para gerar as chaves:
```bash
chmod +x scripts/generate-updater-keys.sh
./scripts/generate-updater-keys.sh
```
Ou manualmente:
```bash
mkdir -p ~/.tauri
cargo tauri signer generate -w ~/.tauri/ollahub.key
```
## Passo 2: Configurar Chave Pública
1. Copie a chave pública gerada (está em `~/.tauri/ollahub.key.pub`)
2. Edite `src-tauri/tauri.conf.json`
3. Substitua `YOUR_PUBLIC_KEY_HERE` pela chave pública
4. Atualize o endpoint com seu repositório GitHub:
```json
{
"updater": {
"active": true,
"endpoints": [
"https://api.github.com/repos/SEU_USUARIO/SEU_REPOSITORIO/releases/latest"
],
"dialog": true,
"pubkey": "SUA_CHAVE_PUBLICA_AQUI"
}
}
```
## Passo 3: Configurar GitHub Secrets
1. Vá em **Settings** > **Secrets and variables** > **Actions**
2. Clique em **New repository secret**
3. Nome: `TAURI_UPDATER_PRIVATE_KEY`
4. Valor: Cole o conteúdo completo do arquivo `~/.tauri/ollahub.key`
## Passo 4: Atualizar Workflow (se necessário)
O workflow `.github/workflows/release.yml` já está configurado para:
- Buildar para Windows, macOS e Linux
- Assinar binários com a chave privada
- Criar release automaticamente quando há push em `main`/`master`
## Como Funciona
1. **Build Automático**: Quando você faz push para `main`/`master`, o GitHub Actions:
- Builda o app para todas as plataformas
- Assina os binários
- Cria uma release no GitHub
2. **Verificação de Atualizações**: O app verifica automaticamente:
- 5 segundos após iniciar (primeira verificação)
- A cada 6 horas (verificação periódica)
- Manualmente via botão nas configurações
3. **Instalação**: Quando há atualização disponível:
- Usuário recebe notificação
- Pode escolher atualizar agora ou depois
- Download e instalação são automáticos
- App reinicia automaticamente após instalação
## Bump de Versão
Para criar uma nova versão:
```bash
# Patch (0.1.0 -> 0.1.1)
pnpm run bump-version patch --commit
# Minor (0.1.0 -> 0.2.0)
pnpm run bump-version minor --commit
# Major (0.1.0 -> 1.0.0)
pnpm run bump-version major --commit
```
Depois, faça push:
```bash
git push && git push --tags
```
O GitHub Actions criará automaticamente a release.
## Troubleshooting
### Erro: "Failed to check for updates"
- Verifique se o endpoint no `tauri.conf.json` está correto
- Verifique se há releases públicas no GitHub
- Verifique a conexão com a internet
### Erro: "Signature verification failed"
- Verifique se a chave pública no `tauri.conf.json` está correta
- Verifique se a chave privada no GitHub Secret está correta
- Regere as chaves se necessário
### Release não é criada automaticamente
- Verifique se o push foi feito para `main` ou `master`
- Verifique os logs do GitHub Actions
- Verifique se as permissões do workflow estão corretas
+142
View File
@@ -0,0 +1,142 @@
import { useState, useEffect, useCallback } from 'react';
import { getVersion } from '@tauri-apps/api/app';
import { useSettingsStore } from '@/store/settings-store';
import { check as checkForUpdate } from '@tauri-apps/plugin-updater';
export interface UpdateInfo {
version: string;
date?: string;
body?: string;
}
export interface AppUpdaterState {
currentVersion: string;
updateAvailable: boolean;
updateInfo: UpdateInfo | null;
isChecking: boolean;
isDownloading: boolean;
downloadProgress: number;
error: string | null;
checkUpdate: () => Promise<void>;
installUpdate: () => Promise<void>;
}
export function useAppUpdater(): AppUpdaterState {
const [currentVersion, setCurrentVersion] = useState<string>('');
const [updateAvailable, setUpdateAvailable] = useState(false);
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
const [isChecking, setIsChecking] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const autoCheckEnabled = useSettingsStore((state) => state.autoCheckUpdates ?? true);
// Obter versão atual
useEffect(() => {
const fetchVersion = async () => {
try {
const version = await getVersion();
setCurrentVersion(version);
} catch (err) {
console.error('Failed to get app version:', err);
setCurrentVersion('0.1.0');
}
};
fetchVersion();
}, []);
// Verificar atualizações
const checkUpdate = useCallback(async () => {
setIsChecking(true);
setError(null);
try {
const update = await checkForUpdate();
if (update?.available) {
setUpdateAvailable(true);
setUpdateInfo({
version: update.version,
date: update.date,
body: update.body,
});
} else {
setUpdateAvailable(false);
setUpdateInfo(null);
}
} catch (err) {
console.error('Failed to check for updates:', err);
setError(err instanceof Error ? err.message : 'Erro ao verificar atualizações');
setUpdateAvailable(false);
} finally {
setIsChecking(false);
}
}, []);
// Instalar atualização
const installUpdate = useCallback(async () => {
if (!updateAvailable) return;
setIsDownloading(true);
setError(null);
setDownloadProgress(0);
try {
const update = await checkForUpdate();
if (update?.available) {
// Escutar progresso do download
const unlisten = await update.on('download-progress', (progress: any) => {
setDownloadProgress(progress.progress || 0);
});
// Baixar e instalar atualização
await update.downloadAndInstall();
unlisten();
// Reiniciar aplicativo após instalação
await update.installAndRestart();
}
} catch (err) {
console.error('Failed to install update:', err);
setError(err instanceof Error ? err.message : 'Erro ao instalar atualização');
setIsDownloading(false);
}
}, [updateAvailable]);
// Verificação automática periódica (a cada 6 horas)
useEffect(() => {
if (!autoCheckEnabled) return;
// Verificar imediatamente ao montar (com delay para não interferir no carregamento)
const initialTimer = setTimeout(() => {
checkUpdate();
}, 5000); // 5 segundos após o app carregar
// Verificar periodicamente
const interval = setInterval(() => {
checkUpdate();
}, 6 * 60 * 60 * 1000); // 6 horas
return () => {
clearTimeout(initialTimer);
clearInterval(interval);
};
}, [autoCheckEnabled, checkUpdate]);
return {
currentVersion,
updateAvailable,
updateInfo,
isChecking,
isDownloading,
downloadProgress,
error,
checkUpdate,
installUpdate,
};
}
+4 -1
View File
@@ -6,7 +6,9 @@
"dev": "next dev --webpack", "dev": "next dev --webpack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "eslint" "lint": "eslint",
"generate-changelog": "node scripts/generate-changelog.js",
"bump-version": "node scripts/bump-version.js"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
@@ -23,6 +25,7 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@tauri-apps/api": "^2.9.0", "@tauri-apps/api": "^2.9.0",
"@tauri-apps/plugin-updater": "^2.0.0",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
+10
View File
@@ -50,6 +50,9 @@ importers:
'@tauri-apps/api': '@tauri-apps/api':
specifier: ^2.9.0 specifier: ^2.9.0
version: 2.9.0 version: 2.9.0
'@tauri-apps/plugin-updater':
specifier: ^2.0.0
version: 2.9.0
'@types/react-syntax-highlighter': '@types/react-syntax-highlighter':
specifier: ^15.5.13 specifier: ^15.5.13
version: 15.5.13 version: 15.5.13
@@ -1153,6 +1156,9 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
hasBin: true hasBin: true
'@tauri-apps/[email protected]':
resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==}
'@tybys/[email protected]': '@tybys/[email protected]':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -4228,6 +4234,10 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.9.4 '@tauri-apps/cli-win32-ia32-msvc': 2.9.4
'@tauri-apps/cli-win32-x64-msvc': 2.9.4 '@tauri-apps/cli-win32-x64-msvc': 2.9.4
'@tauri-apps/[email protected]':
dependencies:
'@tauri-apps/api': 2.9.0
'@tybys/[email protected]': '@tybys/[email protected]':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
+3
View File
@@ -1,3 +1,6 @@
packages:
- '.'
onlyBuiltDependencies: onlyBuiltDependencies:
- sharp - sharp
- unrs-resolver - unrs-resolver
+71
View File
@@ -0,0 +1,71 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const versionType = process.argv[2] || 'patch'; // patch, minor, major
const shouldCommit = process.argv.includes('--commit');
// Validar tipo de versão
if (!['patch', 'minor', 'major'].includes(versionType)) {
console.error('❌ Tipo de versão inválido. Use: patch, minor ou major');
process.exit(1);
}
// Ler package.json
const packageJsonPath = path.join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
// Bump version
const [major, minor, patch] = packageJson.version.split('.').map(Number);
let newVersion;
switch (versionType) {
case 'major':
newVersion = `${major + 1}.0.0`;
break;
case 'minor':
newVersion = `${major}.${minor + 1}.0`;
break;
case 'patch':
default:
newVersion = `${major}.${minor}.${patch + 1}`;
break;
}
console.log(`📦 Atualizando versão de ${packageJson.version} para ${newVersion}...`);
// Atualizar package.json
packageJson.version = newVersion;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
console.log('✅ package.json atualizado');
// Atualizar tauri.conf.json
const tauriConfPath = path.join(__dirname, '..', 'src-tauri', 'tauri.conf.json');
const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf-8'));
tauriConf.version = newVersion;
fs.writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + '\n');
console.log('✅ tauri.conf.json atualizado');
// Atualizar Cargo.toml
const cargoTomlPath = path.join(__dirname, '..', 'src-tauri', 'Cargo.toml');
let cargoToml = fs.readFileSync(cargoTomlPath, 'utf-8');
cargoToml = cargoToml.replace(/^version = ".*"$/m, `version = "${newVersion}"`);
fs.writeFileSync(cargoTomlPath, cargoToml);
console.log('✅ Cargo.toml atualizado');
// Criar commit e tag (opcional)
if (shouldCommit) {
try {
execSync(`git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml`, { stdio: 'inherit' });
execSync(`git commit -m "chore: bump version to ${newVersion}"`, { stdio: 'inherit' });
execSync(`git tag v${newVersion}`, { stdio: 'inherit' });
console.log(`✅ Commit e tag v${newVersion} criados`);
console.log(`\n📝 Próximo passo: git push && git push --tags`);
} catch (error) {
console.error('❌ Erro ao criar commit/tag:', error.message);
process.exit(1);
}
}
console.log(`\n✅ Versão atualizada para ${newVersion}`);
+205
View File
@@ -0,0 +1,205 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Ler versão do package.json
const packageJsonPath = path.join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const currentVersion = packageJson.version;
// Verificar se estamos em um repositório Git
let isGitRepo = false;
try {
execSync('git rev-parse --git-dir', { stdio: 'ignore', cwd: path.join(__dirname, '..') });
isGitRepo = true;
} catch (error) {
console.warn('⚠️ Não é um repositório Git. Gerando changelog vazio.');
}
// Função para sanitizar mensagens de commit
function sanitizeMessage(message) {
return message
.replace(/\n/g, ' ')
.replace(/\r/g, '')
.replace(/"/g, '\\"')
.trim();
}
// Função para extrair tipo e mensagem do commit
function parseCommitMessage(fullMessage) {
const featMatch = fullMessage.match(/^feat(\(.+?\))?:\s*(.+)$/i);
const fixMatch = fullMessage.match(/^fix(\(.+?\))?:\s*(.+)$/i);
if (featMatch) {
return { type: 'feat', message: featMatch[2] || featMatch[0] };
}
if (fixMatch) {
return { type: 'fix', message: fixMatch[2] || fixMatch[0] };
}
return null;
}
// Função para obter tags de versão
function getVersionTags() {
try {
const tags = execSync('git tag --sort=-version:refname', {
encoding: 'utf-8',
cwd: path.join(__dirname, '..'),
})
.trim()
.split('\n')
.filter(Boolean)
.filter(tag => /^v?\d+\.\d+\.\d+/.test(tag));
return tags;
} catch (error) {
return [];
}
}
// Função para obter commits entre duas tags ou desde o início
function getCommitsBetweenTags(fromTag, toTag) {
try {
let range = '';
if (fromTag && toTag) {
range = `${fromTag}..${toTag}`;
} else if (toTag) {
range = toTag;
} else {
range = 'HEAD';
}
const logFormat = '%H|%an|%ad|%s';
const dateFormat = '--date=iso-strict';
const output = execSync(
`git log ${range} --pretty=format:"${logFormat}" --date=${dateFormat}`,
{
encoding: 'utf-8',
cwd: path.join(__dirname, '..'),
}
);
return output
.trim()
.split('\n')
.filter(Boolean)
.map(line => {
const [hash, author, date, message] = line.split('|');
const parsed = parseCommitMessage(message);
if (!parsed) return null;
return {
hash: hash.substring(0, 7),
type: parsed.type,
message: sanitizeMessage(parsed.message),
author: author.trim(),
date: date.trim(),
};
})
.filter(Boolean);
} catch (error) {
return [];
}
}
// Função principal para gerar changelog
function generateChangelog() {
const changelogPath = path.join(__dirname, '..', 'data', 'changelog.json');
const dataDir = path.dirname(changelogPath);
// Criar diretório data se não existir
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
if (!isGitRepo) {
// Gerar changelog vazio se não for repositório Git
const emptyChangelog = {
version: currentVersion,
generatedAt: new Date().toISOString(),
releases: [],
};
fs.writeFileSync(changelogPath, JSON.stringify(emptyChangelog, null, 2), 'utf-8');
console.log('✅ Changelog vazio gerado (não é repositório Git)');
return;
}
const releases = [];
const versionTags = getVersionTags();
if (versionTags.length > 0) {
// Agrupar por tags de versão
for (let i = 0; i < versionTags.length; i++) {
const currentTag = versionTags[i];
const nextTag = i < versionTags.length - 1 ? versionTags[i + 1] : null;
const commits = getCommitsBetweenTags(nextTag, currentTag);
if (commits.length > 0) {
// Extrair versão da tag (remover 'v' prefix se existir)
const version = currentTag.replace(/^v/, '');
const releaseDate = commits[0]?.date || new Date().toISOString();
releases.push({
version,
date: releaseDate.split('T')[0],
commits,
});
}
}
// Adicionar commits não versionados (após a última tag)
const unreleasedCommits = getCommitsBetweenTags(versionTags[0], 'HEAD');
if (unreleasedCommits.length > 0) {
releases.unshift({
version: 'unreleased',
date: unreleasedCommits[0]?.date.split('T')[0] || new Date().toISOString().split('T')[0],
commits: unreleasedCommits,
});
}
} else {
// Sem tags, agrupar por data (últimos 30 dias ou todos se menos)
const allCommits = getCommitsBetweenTags(null, 'HEAD');
if (allCommits.length > 0) {
// Agrupar por data
const commitsByDate = {};
allCommits.forEach(commit => {
const date = commit.date.split('T')[0];
if (!commitsByDate[date]) {
commitsByDate[date] = [];
}
commitsByDate[date].push(commit);
});
// Criar releases por data (ordenado por data decrescente)
Object.keys(commitsByDate)
.sort((a, b) => b.localeCompare(a))
.forEach(date => {
releases.push({
version: 'unreleased',
date,
commits: commitsByDate[date],
});
});
}
}
const changelog = {
version: currentVersion,
generatedAt: new Date().toISOString(),
releases,
};
fs.writeFileSync(changelogPath, JSON.stringify(changelog, null, 2), 'utf-8');
console.log(`✅ Changelog gerado: ${releases.length} release(s) encontrada(s)`);
console.log(` Total de commits: ${releases.reduce((sum, r) => sum + r.commits.length, 0)}`);
}
// Executar
try {
generateChangelog();
} catch (error) {
console.error('❌ Erro ao gerar changelog:', error.message);
process.exit(1);
}
+175 -1
View File
@@ -129,6 +129,7 @@ dependencies = [
"tauri-plugin-log", "tauri-plugin-log",
"tauri-plugin-notification", "tauri-plugin-notification",
"tauri-plugin-shell", "tauri-plugin-shell",
"tauri-plugin-updater",
"tokenizers", "tokenizers",
"tokio", "tokio",
"tokio-cron-scheduler", "tokio-cron-scheduler",
@@ -136,7 +137,16 @@ dependencies = [
"urlencoding", "urlencoding",
"uuid", "uuid",
"walkdir", "walkdir",
"zip", "zip 0.6.6",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
] ]
[[package]] [[package]]
@@ -1095,6 +1105,17 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]] [[package]]
name = "derive_builder" name = "derive_builder"
version = "0.20.2" version = "0.20.2"
@@ -1822,8 +1843,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.1+wasi-snapshot-preview1", "wasi 0.11.1+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1833,9 +1856,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"r-efi", "r-efi",
"wasip2", "wasip2",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -2280,6 +2305,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
"webpki-roots 1.0.4",
] ]
[[package]] [[package]]
@@ -2801,6 +2827,12 @@ dependencies = [
"value-bag", "value-bag",
] ]
[[package]]
name = "lru-slab"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]] [[package]]
name = "mac" name = "mac"
version = "0.1.1" version = "0.1.1"
@@ -2938,6 +2970,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minisign-verify"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -3447,6 +3485,18 @@ dependencies = [
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.10.0",
"objc2 0.6.3",
"objc2-app-kit",
"objc2-foundation 0.3.2",
]
[[package]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.2.2" version = "0.2.2"
@@ -3655,6 +3705,20 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2 0.6.3",
"objc2-foundation 0.3.2",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.17",
]
[[package]] [[package]]
name = "pango" name = "pango"
version = "0.18.3" version = "0.18.3"
@@ -4129,6 +4193,61 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "quinn"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tracing",
"web-time",
]
[[package]]
name = "quinn-proto"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.2",
"ring",
"rustc-hash",
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.17",
"tinyvec",
"tracing",
"web-time",
]
[[package]]
name = "quinn-udp"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.6.1",
"tracing",
"windows-sys 0.60.2",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.42" version = "1.0.42"
@@ -4471,6 +4590,8 @@ dependencies = [
"native-tls", "native-tls",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"quinn",
"rustls",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
@@ -4478,6 +4599,7 @@ dependencies = [
"sync_wrapper 1.0.2", "sync_wrapper 1.0.2",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-rustls",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
@@ -4487,6 +4609,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams", "wasm-streams",
"web-sys", "web-sys",
"webpki-roots 1.0.4",
] ]
[[package]] [[package]]
@@ -4585,6 +4708,12 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@@ -4637,6 +4766,7 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
dependencies = [ dependencies = [
"web-time",
"zeroize", "zeroize",
] ]
@@ -5669,6 +5799,38 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tauri-plugin-updater"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b"
dependencies = [
"base64 0.22.1",
"dirs 6.0.0",
"flate2",
"futures-util",
"http 1.3.1",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.12.24",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.17",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip 4.6.1",
]
[[package]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.9.1" version = "2.9.1"
@@ -7668,6 +7830,18 @@ dependencies = [
"zstd", "zstd",
] ]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.12.0",
"memchr",
]
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.11.2+zstd.1.5.2" version = "0.11.2+zstd.1.5.2"
+1
View File
@@ -44,6 +44,7 @@ futures-util = "0.3"
regex = "1.10" regex = "1.10"
walkdir = "2.5" walkdir = "2.5"
tauri-plugin-autostart = "2" tauri-plugin-autostart = "2"
tauri-plugin-updater = "2"
dirs = "5.0" dirs = "5.0"
rfd = "0.14" rfd = "0.14"
rusqlite = { version = "0.31", features = ["bundled"] } rusqlite = { version = "0.31", features = ["bundled"] }
+2 -1
View File
@@ -13,6 +13,7 @@
"core:window:allow-maximize", "core:window:allow-maximize",
"core:window:allow-unmaximize", "core:window:allow-unmaximize",
"core:window:allow-close", "core:window:allow-close",
"core:window:allow-is-maximized" "core:window:allow-is-maximized",
"updater:default"
] ]
} }
+3
View File
@@ -3613,6 +3613,9 @@ pub fn run() {
// Plugin de notificações // Plugin de notificações
app.handle().plugin(tauri_plugin_notification::init())?; app.handle().plugin(tauri_plugin_notification::init())?;
// Plugin de atualização automática
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
// Modificar comportamento de fechar janela (ocultar ao invés de fechar) // Modificar comportamento de fechar janela (ocultar ao invés de fechar)
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
let window_clone = window.clone(); let window_clone = window.clone();
+11 -1
View File
@@ -7,7 +7,7 @@
"frontendDist": "../out", "frontendDist": "../out",
"devUrl": "http://localhost:3000", "devUrl": "http://localhost:3000",
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build" "beforeBuildCommand": "pnpm generate-changelog && pnpm build"
}, },
"app": { "app": {
"windows": [ "windows": [
@@ -35,5 +35,15 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
] ]
},
"plugins": {
"updater": {
"active": true,
"endpoints": [
"https://github.com/evandrodevbr/OllaHub/releases/latest/download/latest.json"
],
"dialog": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDgxMjY3NThDMUQyREQ5QUYKUldTdjJTMGRqSFVtZ1k1ZWFHSEcxNExDZzgrNVh6M0Z6VXIyaTU5dDAvZnpQb3p0UVpuRW1Od3YK"
}
} }
} }
+22 -1
View File
@@ -56,6 +56,9 @@ export interface SettingsState {
keywords: string[]; keywords: string[];
}; };
// Updates
autoCheckUpdates: boolean;
// Query Preprocessing // Query Preprocessing
queryPreprocessing: { queryPreprocessing: {
enabled: boolean; enabled: boolean;
@@ -96,6 +99,9 @@ export interface SettingsState {
addNotificationKeyword: (keyword: string) => void; addNotificationKeyword: (keyword: string) => void;
removeNotificationKeyword: (keyword: string) => void; removeNotificationKeyword: (keyword: string) => void;
// Updates Actions
setAutoCheckUpdates: (enabled: boolean) => void;
// Query Preprocessing Actions // Query Preprocessing Actions
setQueryPreprocessingEnabled: (enabled: boolean) => void; setQueryPreprocessingEnabled: (enabled: boolean) => void;
setQueryPreprocessingMinLength: (min: number) => void; setQueryPreprocessingMinLength: (min: number) => void;
@@ -221,6 +227,7 @@ const initialState = {
enabled: true, enabled: true,
keywords: [], keywords: [],
}, },
autoCheckUpdates: true,
queryPreprocessing: { queryPreprocessing: {
enabled: true, enabled: true,
minLength: 3, minLength: 3,
@@ -407,6 +414,10 @@ export const useSettingsStore = create<SettingsState>()(
}, },
})), })),
// Updates Actions
setAutoCheckUpdates: (enabled) =>
set({ autoCheckUpdates: enabled }),
// Query Preprocessing Actions // Query Preprocessing Actions
setQueryPreprocessingEnabled: (enabled) => setQueryPreprocessingEnabled: (enabled) =>
set((state) => ({ set((state) => ({
@@ -479,7 +490,17 @@ export const useSettingsStore = create<SettingsState>()(
}), }),
{ {
name: 'ollahub-settings', name: 'ollahub-settings',
version: 1, version: 3,
migrate: (persistedState: any, version: number) => {
// Migração da versão 1 para 3
if (version < 3) {
return {
...persistedState,
autoCheckUpdates: persistedState.autoCheckUpdates ?? true,
};
}
return persistedState;
},
} }
) )
); );
+1
View File
@@ -0,0 +1 @@
dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDgxMjY3NThDMUQyREQ5QUYKUldTdjJTMGRqSFVtZ1k1ZWFHSEcxNExDZzgrNVh6M0Z6VXIyaTU5dDAvZnpQb3p0UVpuRW1Od3YK