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"
+ ]
}