365 lines
14 KiB
TypeScript
365 lines
14 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import {
|
||
Server,
|
||
Check,
|
||
AlertCircle,
|
||
Loader2,
|
||
ExternalLink,
|
||
Copy,
|
||
Terminal,
|
||
Cpu,
|
||
HardDrive
|
||
} from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
interface OllamaModel {
|
||
name: string;
|
||
size: string;
|
||
quantization: string;
|
||
}
|
||
|
||
const recommendedModels: OllamaModel[] = [
|
||
{ name: "llama3.2:3b", size: "2 GB", quantization: "Q4_0" },
|
||
{ name: "mistral:7b", size: "4.1 GB", quantization: "Q4_0" },
|
||
{ name: "qwen2.5:7b", size: "4.7 GB", quantization: "Q4_K_M" },
|
||
{ name: "llama3.1:8b", size: "4.7 GB", quantization: "Q4_0" },
|
||
{ name: "gemma2:9b", size: "5.4 GB", quantization: "Q4_0" },
|
||
];
|
||
|
||
export default function OllamaSetupPage() {
|
||
const [endpoint, setEndpoint] = useState("http://localhost:11434");
|
||
const [selectedModel, setSelectedModel] = useState("");
|
||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||
const [testing, setTesting] = useState(false);
|
||
const [connectionStatus, setConnectionStatus] = useState<"idle" | "success" | "error">("idle");
|
||
const [errorMessage, setErrorMessage] = useState("");
|
||
const [copied, setCopied] = useState<string | null>(null);
|
||
|
||
const testConnection = async () => {
|
||
setTesting(true);
|
||
setConnectionStatus("idle");
|
||
setErrorMessage("");
|
||
|
||
try {
|
||
// Test connection to Ollama
|
||
const res = await fetch(`${endpoint}/api/tags`);
|
||
if (!res.ok) throw new Error("Failed to connect to Ollama");
|
||
|
||
const data = await res.json();
|
||
const models = data.models?.map((m: any) => m.name) || [];
|
||
setAvailableModels(models);
|
||
setConnectionStatus("success");
|
||
|
||
// Auto-select first model if available
|
||
if (models.length > 0 && !selectedModel) {
|
||
setSelectedModel(models[0]);
|
||
}
|
||
} catch (error: any) {
|
||
setConnectionStatus("error");
|
||
setErrorMessage(error.message || "Failed to connect to Ollama");
|
||
} finally {
|
||
setTesting(false);
|
||
}
|
||
};
|
||
|
||
const copyToClipboard = (text: string, id: string) => {
|
||
navigator.clipboard.writeText(text);
|
||
setCopied(id);
|
||
setTimeout(() => setCopied(null), 2000);
|
||
};
|
||
|
||
const saveSettings = () => {
|
||
// Save to localStorage or user settings
|
||
const settings = { ollamaEndpoint: endpoint, ollamaModel: selectedModel };
|
||
localStorage.setItem("ollamaSettings", JSON.stringify(settings));
|
||
|
||
// Also save to user account if logged in
|
||
const token = localStorage.getItem("token");
|
||
if (token) {
|
||
fetch("http://localhost:8000/api/auth/settings", {
|
||
method: "PUT",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
body: JSON.stringify({
|
||
ollama_endpoint: endpoint,
|
||
ollama_model: selectedModel,
|
||
}),
|
||
});
|
||
}
|
||
|
||
alert("Settings saved successfully!");
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-b from-[#1a1a1a] to-[#262626] py-16">
|
||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
{/* Header */}
|
||
<div className="text-center mb-12">
|
||
<Badge className="mb-4 bg-orange-500/20 text-orange-400 border-orange-500/30">
|
||
Self-Hosted
|
||
</Badge>
|
||
<h1 className="text-4xl font-bold text-white mb-4">
|
||
Configure Your Ollama Server
|
||
</h1>
|
||
<p className="text-xl text-zinc-400 max-w-2xl mx-auto">
|
||
Connect your own Ollama instance for unlimited, free translations using local AI models.
|
||
</p>
|
||
</div>
|
||
|
||
{/* What is Ollama */}
|
||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6 mb-8">
|
||
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||
<Server className="h-5 w-5 text-orange-400" />
|
||
What is Ollama?
|
||
</h2>
|
||
<p className="text-zinc-400 mb-4">
|
||
Ollama is a free, open-source tool that lets you run large language models locally on your computer.
|
||
With Ollama, you can translate documents without sending data to external servers, ensuring complete privacy.
|
||
</p>
|
||
<div className="flex flex-wrap gap-4">
|
||
<a
|
||
href="https://ollama.ai"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="inline-flex items-center gap-2 text-teal-400 hover:text-teal-300"
|
||
>
|
||
Visit Ollama Website
|
||
<ExternalLink className="h-4 w-4" />
|
||
</a>
|
||
<a
|
||
href="https://github.com/ollama/ollama"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="inline-flex items-center gap-2 text-teal-400 hover:text-teal-300"
|
||
>
|
||
GitHub Repository
|
||
<ExternalLink className="h-4 w-4" />
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Installation Guide */}
|
||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6 mb-8">
|
||
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||
<Terminal className="h-5 w-5 text-blue-400" />
|
||
Quick Installation Guide
|
||
</h2>
|
||
|
||
<div className="space-y-6">
|
||
{/* Step 1 */}
|
||
<div>
|
||
<h3 className="text-white font-medium mb-2">1. Install Ollama</h3>
|
||
<div className="space-y-2">
|
||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800">
|
||
<div>
|
||
<span className="text-zinc-400">macOS / Linux:</span>
|
||
<code className="ml-2 text-teal-400">curl -fsSL https://ollama.ai/install.sh | sh</code>
|
||
</div>
|
||
<button
|
||
onClick={() => copyToClipboard("curl -fsSL https://ollama.ai/install.sh | sh", "install")}
|
||
className="p-2 rounded hover:bg-zinc-700"
|
||
>
|
||
{copied === "install" ? (
|
||
<Check className="h-4 w-4 text-green-400" />
|
||
) : (
|
||
<Copy className="h-4 w-4 text-zinc-400" />
|
||
)}
|
||
</button>
|
||
</div>
|
||
<p className="text-sm text-zinc-500">
|
||
Windows: Download from <a href="https://ollama.ai/download" className="text-teal-400 hover:underline" target="_blank">ollama.ai/download</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Step 2 */}
|
||
<div>
|
||
<h3 className="text-white font-medium mb-2">2. Pull a Translation Model</h3>
|
||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800">
|
||
<code className="text-teal-400">ollama pull llama3.2:3b</code>
|
||
<button
|
||
onClick={() => copyToClipboard("ollama pull llama3.2:3b", "pull")}
|
||
className="p-2 rounded hover:bg-zinc-700"
|
||
>
|
||
{copied === "pull" ? (
|
||
<Check className="h-4 w-4 text-green-400" />
|
||
) : (
|
||
<Copy className="h-4 w-4 text-zinc-400" />
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Step 3 */}
|
||
<div>
|
||
<h3 className="text-white font-medium mb-2">3. Start Ollama Server</h3>
|
||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800">
|
||
<code className="text-teal-400">ollama serve</code>
|
||
<button
|
||
onClick={() => copyToClipboard("ollama serve", "serve")}
|
||
className="p-2 rounded hover:bg-zinc-700"
|
||
>
|
||
{copied === "serve" ? (
|
||
<Check className="h-4 w-4 text-green-400" />
|
||
) : (
|
||
<Copy className="h-4 w-4 text-zinc-400" />
|
||
)}
|
||
</button>
|
||
</div>
|
||
<p className="text-sm text-zinc-500 mt-2">
|
||
On macOS/Windows with the desktop app, Ollama runs automatically in the background.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Recommended Models */}
|
||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6 mb-8">
|
||
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||
<Cpu className="h-5 w-5 text-purple-400" />
|
||
Recommended Models for Translation
|
||
</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{recommendedModels.map((model) => (
|
||
<div
|
||
key={model.name}
|
||
className="flex items-center justify-between p-3 rounded-lg bg-zinc-800"
|
||
>
|
||
<div>
|
||
<span className="text-white font-medium">{model.name}</span>
|
||
<div className="flex items-center gap-2 mt-1">
|
||
<Badge variant="outline" className="text-xs border-zinc-700 text-zinc-400">
|
||
<HardDrive className="h-3 w-3 mr-1" />
|
||
{model.size}
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => copyToClipboard(`ollama pull ${model.name}`, model.name)}
|
||
className="px-3 py-1.5 rounded bg-zinc-700 hover:bg-zinc-600 text-sm text-white"
|
||
>
|
||
{copied === model.name ? "Copied!" : "Copy command"}
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<p className="text-sm text-zinc-500 mt-4">
|
||
💡 Tip: For best results with limited RAM (8GB), use <code className="text-teal-400">llama3.2:3b</code>.
|
||
With 16GB+ RAM, try <code className="text-teal-400">mistral:7b</code> or larger.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Configuration */}
|
||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-6 mb-8">
|
||
<h2 className="text-lg font-semibold text-white mb-4">Configure Connection</h2>
|
||
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="endpoint" className="text-zinc-300">Ollama Server URL</Label>
|
||
<div className="flex gap-2">
|
||
<Input
|
||
id="endpoint"
|
||
type="url"
|
||
value={endpoint}
|
||
onChange={(e) => setEndpoint(e.target.value)}
|
||
placeholder="http://localhost:11434"
|
||
className="bg-zinc-800 border-zinc-700 text-white"
|
||
/>
|
||
<Button
|
||
onClick={testConnection}
|
||
disabled={testing}
|
||
className="bg-teal-500 hover:bg-teal-600 text-white whitespace-nowrap"
|
||
>
|
||
{testing ? (
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
) : (
|
||
"Test Connection"
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Connection Status */}
|
||
{connectionStatus === "success" && (
|
||
<div className="p-3 rounded-lg bg-green-500/10 border border-green-500/20 flex items-center gap-2">
|
||
<Check className="h-5 w-5 text-green-400" />
|
||
<span className="text-green-400">Connected successfully! Found {availableModels.length} model(s).</span>
|
||
</div>
|
||
)}
|
||
|
||
{connectionStatus === "error" && (
|
||
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center gap-2">
|
||
<AlertCircle className="h-5 w-5 text-red-400" />
|
||
<span className="text-red-400">{errorMessage}</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* Model Selection */}
|
||
{availableModels.length > 0 && (
|
||
<div className="space-y-2">
|
||
<Label className="text-zinc-300">Select Model</Label>
|
||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||
{availableModels.map((model) => (
|
||
<button
|
||
key={model}
|
||
onClick={() => setSelectedModel(model)}
|
||
className={cn(
|
||
"p-3 rounded-lg border text-left transition-colors",
|
||
selectedModel === model
|
||
? "border-teal-500 bg-teal-500/10 text-teal-400"
|
||
: "border-zinc-700 bg-zinc-800 text-zinc-300 hover:border-zinc-600"
|
||
)}
|
||
>
|
||
{model}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Save Button */}
|
||
{connectionStatus === "success" && selectedModel && (
|
||
<Button
|
||
onClick={saveSettings}
|
||
className="w-full bg-teal-500 hover:bg-teal-600 text-white"
|
||
>
|
||
Save Configuration
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Benefits */}
|
||
<div className="rounded-xl border border-zinc-800 bg-gradient-to-r from-teal-500/10 to-purple-500/10 p-6">
|
||
<h2 className="text-lg font-semibold text-white mb-4">Why Self-Host?</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div className="text-center">
|
||
<div className="text-3xl mb-2">🔒</div>
|
||
<h3 className="font-medium text-white mb-1">Complete Privacy</h3>
|
||
<p className="text-sm text-zinc-400">Your documents never leave your computer</p>
|
||
</div>
|
||
<div className="text-center">
|
||
<div className="text-3xl mb-2">♾️</div>
|
||
<h3 className="font-medium text-white mb-1">Unlimited Usage</h3>
|
||
<p className="text-sm text-zinc-400">No monthly limits or quotas</p>
|
||
</div>
|
||
<div className="text-center">
|
||
<div className="text-3xl mb-2">💰</div>
|
||
<h3 className="font-medium text-white mb-1">Free Forever</h3>
|
||
<p className="text-sm text-zinc-400">No subscription or API costs</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|