diff --git a/app/api/ollama/models/route.ts b/app/api/ollama/models/route.ts index 9bd465d..0e71db7 100644 --- a/app/api/ollama/models/route.ts +++ b/app/api/ollama/models/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { listModelsViaSdk, listModelsViaHttp } from "@/lib/ollama"; +import { listModelsViaSdk, listModelsViaHttp, ensureOllamaAvailable } from "@/lib/ollama"; import { estimateCpuVram, inferDevice, @@ -31,6 +31,8 @@ function normalizeModel(model: any): ModelInfo { export async function GET() { try { let models: any[] = []; + // Tentar garantir disponibilidade rapidamente (não bloquear longamente a UI) + await ensureOllamaAvailable({ timeoutMs: 3000 }).catch(() => {}); try { models = await listModelsViaSdk(); } catch { diff --git a/app/api/ollama/pull/route.ts b/app/api/ollama/pull/route.ts index 7407e1a..9bf743e 100644 --- a/app/api/ollama/pull/route.ts +++ b/app/api/ollama/pull/route.ts @@ -1,5 +1,5 @@ import { NextRequest } from "next/server"; -import { pullModel } from "@/lib/ollama"; +import { pullModel, ensureOllamaAvailable } from "@/lib/ollama"; export const dynamic = "force-dynamic"; @@ -14,6 +14,18 @@ export async function POST(req: NextRequest) { const stream = new ReadableStream({ async start(controller) { try { + // Garantir que o servidor esteja disponível antes do pull + const ok = await ensureOllamaAvailable({ timeoutMs: 20000 }); + if (!ok) { + controller.enqueue( + new TextEncoder().encode( + JSON.stringify({ error: "Ollama indisponível" }) + "\n", + ), + ); + controller.close(); + return; + } + const response = await pullModel(model); for await (const chunk of response) { diff --git a/components/chat/ChatContainer.tsx b/components/chat/ChatContainer.tsx index 544d3ae..1dc5672 100644 --- a/components/chat/ChatContainer.tsx +++ b/components/chat/ChatContainer.tsx @@ -435,20 +435,16 @@ export function ChatContainer({

- {selectedModelData - ? "Chat with" - : "Select a model to start chatting"} + {selectedModelData ? "Chat with" : "Select a model to start chatting"}

- {selectedModelData && ( - - )} +
{ + try { + await listModelsViaHttp(); + return true; + } catch { + return false; + } +} + +async function waitUntilHealthy(timeoutMs: number, intervalMs = 800): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (await isOllamaHealthy()) return true; + await new Promise((r) => setTimeout(r, intervalMs)); + } + return false; +} + +export async function ensureOllamaAvailable(options?: { timeoutMs?: number }): Promise { + const timeoutMs = options?.timeoutMs ?? 20000; + if (await isOllamaHealthy()) return true; + + // Windows: tentar iniciar via PowerShell script (WSL-first fallback Windows) + if (os.platform() === "win32") { + try { + const child = spawn( + "powershell", + [ + "-ExecutionPolicy", + "Bypass", + "-File", + "scripts/ensure-ollama.ps1", + ], + { stdio: "ignore", detached: true } + ); + child.unref(); + } catch (e) { + // Ignorar, tentaremos apenas aguardar + } + } + + // Aguardar saúde + return await waitUntilHealthy(timeoutMs); +} + export async function chatWithStream( model: string, messages: Array<{ role: string; content: string }>, diff --git a/package.json b/package.json index 06b92cc..40d5a61 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "predev": "tsx scripts/init-db.ts", + "predev": "powershell -ExecutionPolicy Bypass -File scripts/ensure-ollama.ps1 && tsx scripts/init-db.ts", "prebuild": "tsx scripts/init-db.ts", "dev": "next dev", "build": "next build", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 67b4e10..f2d9d9f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ ignoredBuiltDependencies: + - esbuild - sharp onlyBuiltDependencies: diff --git a/scripts/ensure-ollama.ps1 b/scripts/ensure-ollama.ps1 new file mode 100644 index 0000000..47e4d69 --- /dev/null +++ b/scripts/ensure-ollama.ps1 @@ -0,0 +1,123 @@ +# Ensures Ollama is running on localhost using WSL (Ubuntu) when available, +# with a Windows-native fallback. Designed to be idempotent. + +param() + +$ErrorActionPreference = 'SilentlyContinue' + +function Write-Info($msg) { Write-Host "[ensure-ollama] $msg" } +function Write-Warn($msg) { Write-Warning "[ensure-ollama] $msg" } + +function Get-EnvOrDefault([string]$name, [string]$default) { + $val = [Environment]::GetEnvironmentVariable($name) + if ([string]::IsNullOrWhiteSpace($val)) { return $default } + return $val +} + +function Test-Port([int]$Port) { + try { + $res = Test-NetConnection -ComputerName localhost -Port $Port -WarningAction SilentlyContinue -ErrorAction SilentlyContinue + return [bool]$res.TcpTestSucceeded + } catch { return $false } +} + +function Wait-Healthy([int]$Port, [int]$TimeoutSeconds) { + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + while ((Get-Date) -lt $deadline) { + if (Test-Port -Port $Port) { return $true } + Start-Sleep -Seconds 1 + } + return $false +} + +function Has-WSL() { + return $null -ne (Get-Command wsl.exe -ErrorAction SilentlyContinue) +} + +function WSL-HasDistro([string]$Distro) { + try { + $list = & wsl.exe -l -q 2>$null + return $list -split "`r?`n" | Where-Object { $_ -eq $Distro } | ForEach-Object { $true } | Select-Object -First 1 + } catch { return $false } +} + +function Invoke-WSL([string]$Distro, [string]$Cmd) { + & wsl.exe -d $Distro -- bash -lc $Cmd +} + +function Ensure-Ollama-WSL([string]$Distro) { + Write-Info "Verificando/instalando Ollama na WSL ($Distro)" + $installCmd = 'command -v ollama >/dev/null 2>&1 || (curl -fsSL https://ollama.com/install.sh | sh)' + Invoke-WSL -Distro $Distro -Cmd $installCmd | Out-Null +} + +function Start-Ollama-WSL([string]$Distro) { + Write-Info "Iniciando ollama serve em background (WSL:$Distro)" + $startCmd = 'mkdir -p "$HOME/.ollama"; nohup ollama serve > "$HOME/.ollama/ollama.log" 2>&1 & disown || true' + Invoke-WSL -Distro $Distro -Cmd $startCmd | Out-Null +} + +function Has-Ollama-OnWindows() { + return $null -ne (Get-Command ollama -ErrorAction SilentlyContinue) +} + +function Start-Ollama-Windows() { + try { + $ollama = (Get-Command ollama -ErrorAction Stop).Source + Write-Info "Iniciando ollama serve no Windows (hidden)" + Start-Process -FilePath $ollama -ArgumentList 'serve' -WindowStyle Hidden | Out-Null + return $true + } catch { + Write-Warn "Ollama não encontrado no PATH do Windows." + return $false + } +} + +# Main +$port = [int](Get-EnvOrDefault 'OLLAMA_PORT' '11434') +$timeout = [int](Get-EnvOrDefault 'OLLAMA_START_TIMEOUT' '20') +$distro = Get-EnvOrDefault 'OLLAMA_WSL_DISTRO' 'Ubuntu' + +if (Test-Port -Port $port) { + Write-Info "Porta $port já está respondendo. Nada a fazer." + exit 0 +} + +if (Has-WSL) { + if (-not (WSL-HasDistro -Distro $distro)) { + Write-Warn "Distro '$distro' não encontrada. Tentando WSL padrão..." + $distro = 'Ubuntu' + } + + Write-Info "Tentando iniciar Ollama via WSL ($distro)" + Ensure-Ollama-WSL -Distro $distro + Start-Ollama-WSL -Distro $distro + + if (Wait-Healthy -Port $port -TimeoutSeconds $timeout) { + Write-Info "Ollama disponível em localhost:$port (via WSL)." + exit 0 + } else { + Write-Warn "Timeout aguardando Ollama (WSL). Prosseguindo com fallback Windows." + } +} + +# Fallback Windows +if (-not (Has-Ollama-OnWindows)) { + Write-Warn "Ollama não encontrado no Windows e WSL não atendeu no tempo." + Write-Warn "Instale o Ollama no Windows ou habilite WSL/Ubuntu." + exit 0 +} + +if (Start-Ollama-Windows) { + if (Wait-Healthy -Port $port -TimeoutSeconds $timeout) { + Write-Info "Ollama disponível em localhost:$port (Windows)." + exit 0 + } else { + Write-Warn "Timeout aguardando Ollama (Windows)." + exit 0 + } +} + +exit 0 + + diff --git a/tsconfig.json b/tsconfig.json index 3a13f90..af21167 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -19,7 +23,9 @@ } ], "paths": { - "@/*": ["./*"] + "@/*": [ + "./*" + ] } }, "include": [ @@ -28,7 +34,11 @@ "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", - "**/*.mts" + "**/*.mts", + ".next\\dev/types/**/*.ts", + ".next\\dev/types/**/*.ts" ], - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] }