dgx-spark-playbooks/nvidia/txt2kg/assets/frontend/components/settings-modal.tsx
2025-10-06 17:05:41 +00:00

996 lines
44 KiB
TypeScript

"use client"
import React, { useState, useEffect } from "react"
import {
Settings,
Database,
Save,
Eye,
EyeOff,
Search as SearchIcon,
Cpu,
HardDrive,
Server,
RefreshCw,
Check,
X
} from "lucide-react"
import { GraphDBType } from "@/lib/graph-db-service"
import { listFilesInS3 } from "@/utils/s3-storage"
import { useToast } from "@/hooks/use-toast"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
export function SettingsModal() {
const { toast } = useToast()
const [isOpen, setIsOpen] = useState(false)
const [activeTab, setActiveTab] = useState("models")
const [dbUrl, setDbUrl] = useState("")
const [dbUsername, setDbUsername] = useState("")
const [dbPassword, setDbPassword] = useState("")
const [vectorDbHost, setVectorDbHost] = useState("")
const [vectorDbPort, setVectorDbPort] = useState("")
const [showPassword, setShowPassword] = useState(false)
// Graph DB settings
const [graphDbType, setGraphDbType] = useState<GraphDBType>("arangodb")
const [neo4jUrl, setNeo4jUrl] = useState("")
const [neo4jUser, setNeo4jUser] = useState("")
const [neo4jPassword, setNeo4jPassword] = useState("")
const [arangoUrl, setArangoUrl] = useState("http://localhost:8529")
const [arangoDb, setArangoDb] = useState("txt2kg")
const [arangoUser, setArangoUser] = useState("")
const [arangoPassword, setArangoPassword] = useState("")
// Vector DB settings - changed from Milvus to Pinecone
const [pineconeApiKey, setPineconeApiKey] = useState("")
const [pineconeEnvironment, setPineconeEnvironment] = useState("")
const [pineconeIndex, setPineconeIndex] = useState("")
// S3 Storage settings
const [s3Endpoint, setS3Endpoint] = useState("")
const [s3Bucket, setS3Bucket] = useState("")
const [s3AccessKey, setS3AccessKey] = useState("")
const [s3SecretKey, setS3SecretKey] = useState("")
const [isConnecting, setIsConnecting] = useState(false)
const [isS3Connected, setIsS3Connected] = useState(false)
const [s3FileCount, setS3FileCount] = useState(0)
const [s3Error, setS3Error] = useState<string | null>(null)
// Embeddings model settings
const [embeddingsProvider, setEmbeddingsProvider] = useState("local")
const [nvidiaEmbeddingsModel, setNvidiaEmbeddingsModel] = useState("nvidia/llama-3.2-nv-embedqa-1b-v2")
// Ollama model configuration
const [availableOllamaModels, setAvailableOllamaModels] = useState<string[]>([])
const [selectedOllamaModels, setSelectedOllamaModels] = useState<string[]>([])
const [isLoadingOllamaModels, setIsLoadingOllamaModels] = useState(false)
const [ollamaConnectionStatus, setOllamaConnectionStatus] = useState<'idle' | 'connected' | 'error'>('idle')
const [ollamaError, setOllamaError] = useState<string | null>(null)
// Listen for open-settings event
useEffect(() => {
const handleOpenSettings = (event: CustomEvent) => {
const { tab } = event.detail
setIsOpen(true)
if (tab) {
setActiveTab(tab)
}
}
window.addEventListener('open-settings', handleOpenSettings as EventListener)
return () => {
window.removeEventListener('open-settings', handleOpenSettings as EventListener)
}
}, [])
// Automatically fetch Ollama models when modal opens and models tab is active
useEffect(() => {
if (isOpen && activeTab === "models" && ollamaConnectionStatus === 'idle') {
fetchOllamaModels()
}
}, [isOpen, activeTab])
// Load saved settings when modal opens
useEffect(() => {
if (isOpen) {
const storedDbUrl = localStorage.getItem("NEO4J_URL") || ""
const storedDbUsername = localStorage.getItem("NEO4J_USERNAME") || ""
const storedDbPassword = localStorage.getItem("NEO4J_PASSWORD") || ""
const storedVectorDbHost = localStorage.getItem("VECTOR_DB_HOST") || ""
const storedVectorDbPort = localStorage.getItem("VECTOR_DB_PORT") || ""
setDbUrl(storedDbUrl)
setDbUsername(storedDbUsername)
setDbPassword(storedDbPassword)
setVectorDbHost(storedVectorDbHost)
setVectorDbPort(storedVectorDbPort)
// Load embeddings settings
const storedEmbeddingsProvider = localStorage.getItem("embeddings_provider") || "local"
const storedNvidiaModel = localStorage.getItem("nvidia_embeddings_model") || "nvidia/llama-3.2-nv-embedqa-1b-v2"
setEmbeddingsProvider(storedEmbeddingsProvider)
setNvidiaEmbeddingsModel(storedNvidiaModel)
// Load Ollama model configuration
const storedSelectedModels = localStorage.getItem("selected_ollama_models")
if (storedSelectedModels) {
try {
setSelectedOllamaModels(JSON.parse(storedSelectedModels))
} catch (e) {
console.error("Error parsing stored Ollama models:", e)
}
}
// Load S3 settings
const savedS3Endpoint = localStorage.getItem("S3_ENDPOINT") || ""
const savedS3Bucket = localStorage.getItem("S3_BUCKET") || ""
const savedS3AccessKey = localStorage.getItem("S3_ACCESS_KEY") || ""
const savedS3SecretKey = localStorage.getItem("S3_SECRET_KEY") || ""
const s3Connected = localStorage.getItem("S3_CONNECTED") === "true"
setS3Endpoint(savedS3Endpoint)
setS3Bucket(savedS3Bucket)
setS3AccessKey(savedS3AccessKey)
setS3SecretKey(savedS3SecretKey)
setIsS3Connected(s3Connected)
}
// Load graph DB type
const storedGraphDbType = localStorage.getItem("graph_db_type") || "arangodb"
setGraphDbType(storedGraphDbType as GraphDBType)
// Load Neo4j settings
setNeo4jUrl(localStorage.getItem("neo4j_url") || "")
setNeo4jUser(localStorage.getItem("neo4j_user") || "")
setNeo4jPassword(localStorage.getItem("neo4j_password") || "")
// Load ArangoDB settings
setArangoUrl(localStorage.getItem("arango_url") || "http://localhost:8529")
setArangoDb(localStorage.getItem("arango_db") || "txt2kg")
setArangoUser(localStorage.getItem("arango_user") || "")
setArangoPassword(localStorage.getItem("arango_password") || "")
setPineconeApiKey(localStorage.getItem("pinecone_api_key") || "")
setPineconeEnvironment(localStorage.getItem("pinecone_environment") || "")
setPineconeIndex(localStorage.getItem("pinecone_index") || "")
}, [isOpen])
// Save database settings
const saveDbSettings = async (e: React.FormEvent) => {
e.preventDefault()
// Save graph DB type
localStorage.setItem("graph_db_type", graphDbType)
// Save Neo4j settings
localStorage.setItem("neo4j_url", neo4jUrl)
localStorage.setItem("neo4j_user", neo4jUser)
localStorage.setItem("neo4j_password", neo4jPassword)
// Save ArangoDB settings
localStorage.setItem("arango_url", arangoUrl)
localStorage.setItem("arango_db", arangoDb)
localStorage.setItem("arango_user", arangoUser)
localStorage.setItem("arango_password", arangoPassword)
// Sync settings with server
try {
await fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
settings: {
graph_db_type: graphDbType,
neo4j_url: neo4jUrl,
neo4j_user: neo4jUser,
neo4j_password: neo4jPassword,
arango_url: arangoUrl,
arango_db: arangoDb,
arango_user: arangoUser,
arango_password: arangoPassword
}
}),
});
} catch (error) {
console.error('Error syncing settings:', error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to sync settings with server"
});
}
toast({
title: "Success",
description: "Graph database settings saved"
});
setIsOpen(false)
}
// Save vector database settings
const saveVectorDbSettings = async (e: React.FormEvent) => {
e.preventDefault()
localStorage.setItem("pinecone_api_key", pineconeApiKey)
localStorage.setItem("pinecone_environment", pineconeEnvironment)
localStorage.setItem("pinecone_index", pineconeIndex)
// Sync settings with server
try {
await fetch('/api/settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
settings: {
pinecone_api_key: pineconeApiKey,
pinecone_environment: pineconeEnvironment,
pinecone_index: pineconeIndex,
}
}),
});
} catch (error) {
console.error('Error syncing settings:', error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to sync settings with server"
});
}
toast({
title: "Success",
description: "Vector database settings saved"
})
}
// Save S3 settings and check connection
const saveS3Settings = async (e: React.FormEvent) => {
e.preventDefault()
setIsConnecting(true)
setS3Error(null)
try {
// Save S3 settings to localStorage
localStorage.setItem("S3_ENDPOINT", s3Endpoint)
localStorage.setItem("S3_BUCKET", s3Bucket)
localStorage.setItem("S3_ACCESS_KEY", s3AccessKey)
localStorage.setItem("S3_SECRET_KEY", s3SecretKey)
// Set these in window for runtime access
window.process = window.process || {}
window.process.env = window.process.env || {}
window.process.env.S3_ENDPOINT = s3Endpoint
window.process.env.S3_BUCKET = s3Bucket
window.process.env.S3_ACCESS_KEY = s3AccessKey
window.process.env.S3_SECRET_KEY = s3SecretKey
// Try to list files to verify connection
const files = await listFilesInS3()
setS3FileCount(files.length)
setIsS3Connected(true)
// Save connection status to localStorage
localStorage.setItem("S3_CONNECTED", "true")
// Dispatch event to notify other components
window.dispatchEvent(new CustomEvent('s3ConnectionChanged', {
detail: { isConnected: true }
}))
toast({
title: "Success",
description: `Connected to S3 bucket. Found ${files.length} files.`
})
} catch (error) {
console.error("Failed to connect to S3:", error)
setIsS3Connected(false)
// Save connection status to localStorage
localStorage.setItem("S3_CONNECTED", "false")
// Dispatch event to notify other components
window.dispatchEvent(new CustomEvent('s3ConnectionChanged', {
detail: { isConnected: false }
}))
setS3Error(error instanceof Error ? error.message : "Could not connect to S3 storage")
toast({
variant: "destructive",
title: "S3 Connection Failed",
description: error instanceof Error ? error.message : "Unknown error"
})
} finally {
setIsConnecting(false)
}
}
// Fetch available Ollama models
const fetchOllamaModels = async () => {
setIsLoadingOllamaModels(true)
setOllamaError(null)
try {
const response = await fetch('/api/ollama?action=test-connection')
const data = await response.json()
if (data.connected && data.models) {
setAvailableOllamaModels(data.models)
setOllamaConnectionStatus('connected')
// If no models are selected yet, select all by default
if (selectedOllamaModels.length === 0) {
setSelectedOllamaModels(data.models)
}
} else {
setOllamaConnectionStatus('error')
setOllamaError(data.error || 'Failed to connect to Ollama')
setAvailableOllamaModels([])
}
} catch (error) {
console.error('Error fetching Ollama models:', error)
setOllamaConnectionStatus('error')
setOllamaError(error instanceof Error ? error.message : 'Unknown error')
setAvailableOllamaModels([])
} finally {
setIsLoadingOllamaModels(false)
}
}
// Save Ollama model settings
const saveOllamaSettings = (e: React.FormEvent) => {
e.preventDefault()
// Save selected models to localStorage
localStorage.setItem("selected_ollama_models", JSON.stringify(selectedOllamaModels))
// Dispatch event to notify model selector to refresh
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('ollama-models-updated', {
detail: { selectedModels: selectedOllamaModels }
}))
}
toast({
title: "Success",
description: "Ollama model settings saved"
})
}
// Toggle Ollama model selection
const toggleOllamaModel = (modelName: string) => {
setSelectedOllamaModels(prev => {
if (prev.includes(modelName)) {
return prev.filter(m => m !== modelName)
} else {
return [...prev, modelName]
}
})
}
// Save embeddings settings
const saveEmbeddingsSettings = (e: React.FormEvent) => {
e.preventDefault();
// Save embeddings provider to localStorage
localStorage.setItem("embeddings_provider", embeddingsProvider);
// If using NVIDIA API, also save the model
if (embeddingsProvider === "nvidia") {
localStorage.setItem("nvidia_embeddings_model", nvidiaEmbeddingsModel);
}
// Save to environment variables (this works in development; in production needs server-side implementation)
process.env.EMBEDDINGS_PROVIDER = embeddingsProvider;
if (embeddingsProvider === "nvidia") {
process.env.NVIDIA_EMBEDDINGS_MODEL = nvidiaEmbeddingsModel;
}
// Reset the EmbeddingsService instance to pick up new settings
try {
// Import dynamically to avoid issues with circular dependencies
import("@/lib/embeddings").then(({ EmbeddingsService }) => {
EmbeddingsService.reset();
console.log("EmbeddingsService reset successfully");
// Dispatch a custom event to notify components that embeddings settings have changed
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('embeddings-settings-changed'));
}
});
} catch (error) {
console.error("Error resetting EmbeddingsService:", error);
}
toast({
title: "Success",
description: "Embeddings settings saved"
})
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<button className="flex items-center justify-center gap-2 p-2 hover:bg-primary/10 rounded-full transition-colors" title="Settings">
<Settings className="h-5 w-5 text-muted-foreground hover:text-primary transition-colors" />
</button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto bg-background border-border">
<DialogHeader className="pb-6 border-b border-border/10">
<div className="flex items-center gap-3 mb-3">
<div className="w-8 h-8 rounded-lg bg-nvidia-green/15 flex items-center justify-center">
<Settings className="h-4 w-4 text-nvidia-green" />
</div>
<DialogTitle className="text-xl font-semibold text-foreground">
Settings
</DialogTitle>
</div>
<DialogDescription className="text-sm text-muted-foreground leading-relaxed">
Configure your API keys and DB connections
</DialogDescription>
</DialogHeader>
<div className="mt-4">
<div className="mb-4">
<Select value={activeTab} onValueChange={setActiveTab}>
<SelectTrigger className="w-full border-border/60 bg-background text-foreground focus:border-primary/50 focus:ring-primary/20">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="graph">
<div className="flex items-center gap-3">
<Database className="h-4 w-4 text-nvidia-green" />
<span>Graph Database</span>
</div>
</SelectItem>
<SelectItem value="vectordb">
<div className="flex items-center gap-3">
<SearchIcon className="h-4 w-4 text-nvidia-green" />
<span>Vector Database</span>
</div>
</SelectItem>
<SelectItem value="s3">
<div className="flex items-center gap-3">
<HardDrive className="h-4 w-4 text-nvidia-green" />
<span>S3 Storage</span>
</div>
</SelectItem>
<SelectItem value="embeddings">
<div className="flex items-center gap-3">
<Cpu className="h-4 w-4 text-nvidia-green" />
<span>Embeddings</span>
</div>
</SelectItem>
<SelectItem value="models">
<div className="flex items-center gap-3">
<Server className="h-4 w-4 text-nvidia-green" />
<span>Model Management</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{activeTab === "graph" && (
<div className="bg-muted/30 border border-border/40 rounded-xl p-4">
<form onSubmit={saveDbSettings} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<Database className="h-4 w-4 text-nvidia-green" />
Database Type
</label>
<select
value={graphDbType}
onChange={(e) => setGraphDbType(e.target.value as GraphDBType)}
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
>
<option value="neo4j">Neo4j</option>
<option value="arangodb">ArangoDB</option>
</select>
</div>
{graphDbType === "neo4j" && (
<div className="bg-background/50 rounded-lg p-3 space-y-3">
<h4 className="text-sm font-medium text-foreground mb-2">Neo4j Configuration</h4>
<div className="grid grid-cols-1 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Connection URL</label>
<input
type="text"
value={neo4jUrl}
onChange={(e) => setNeo4jUrl(e.target.value)}
placeholder="bolt://localhost:7687"
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Username</label>
<input
type="text"
value={neo4jUser}
onChange={(e) => setNeo4jUser(e.target.value)}
placeholder="neo4j"
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Password</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
value={neo4jPassword}
onChange={(e) => setNeo4jPassword(e.target.value)}
placeholder="password"
className="w-full bg-background border border-border/60 rounded-md p-2 pr-8 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
<button
type="button"
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-3 w-3" />
) : (
<Eye className="h-3 w-3" />
)}
</button>
</div>
</div>
</div>
</div>
</div>
)}
{graphDbType === "arangodb" && (
<div className="bg-background/50 rounded-lg p-3 space-y-3">
<h4 className="text-sm font-medium text-foreground mb-2">ArangoDB Configuration</h4>
<div className="grid grid-cols-1 gap-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Connection URL</label>
<input
type="text"
value={arangoUrl}
onChange={(e) => setArangoUrl(e.target.value)}
placeholder="http://localhost:8529"
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Database Name</label>
<input
type="text"
value={arangoDb}
onChange={(e) => setArangoDb(e.target.value)}
placeholder="txt2kg"
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Username</label>
<input
type="text"
value={arangoUser}
onChange={(e) => setArangoUser(e.target.value)}
placeholder="root"
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Password</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
value={arangoPassword}
onChange={(e) => setArangoPassword(e.target.value)}
placeholder="password"
className="w-full bg-background border border-border/60 rounded-md p-2 pr-8 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
<button
type="button"
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-3 w-3" />
) : (
<Eye className="h-3 w-3" />
)}
</button>
</div>
</div>
</div>
</div>
</div>
)}
<div className="flex justify-end pt-3 border-t border-border/30">
<button
type="submit"
className="flex items-center gap-2 px-4 py-2 rounded-md bg-nvidia-green hover:bg-nvidia-green/90 text-white transition-colors text-sm font-medium shadow-sm"
>
<Save className="h-4 w-4" />
Save Settings
</button>
</div>
</form>
</div>
)}
{activeTab === "vectordb" && (
<div className="bg-muted/30 border border-border/40 rounded-xl p-4">
<form onSubmit={saveVectorDbSettings} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<SearchIcon className="h-4 w-4 text-nvidia-green" />
Pinecone Configuration
</label>
</div>
<div className="bg-background/50 rounded-lg p-3 space-y-3">
<div className="grid grid-cols-1 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">API Key</label>
<input
type="password"
value={pineconeApiKey}
onChange={(e) => setPineconeApiKey(e.target.value)}
placeholder="Enter your Pinecone API key"
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Environment</label>
<input
type="text"
value={pineconeEnvironment}
onChange={(e) => setPineconeEnvironment(e.target.value)}
placeholder="us-west1-gcp"
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Index Name</label>
<input
type="text"
value={pineconeIndex}
onChange={(e) => setPineconeIndex(e.target.value)}
placeholder="knowledge-graph"
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
</div>
</div>
</div>
</div>
<div className="flex justify-end pt-3 border-t border-border/30">
<button
type="submit"
className="flex items-center gap-2 px-4 py-2 rounded-md bg-primary hover:bg-primary/90 text-primary-foreground transition-colors text-sm font-medium shadow-sm"
>
<Save className="h-4 w-4" />
Save Settings
</button>
</div>
</form>
</div>
)}
{activeTab === "s3" && (
<div className="bg-muted/30 border border-border/40 rounded-xl p-4">
<form onSubmit={saveS3Settings} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<HardDrive className="h-4 w-4 text-nvidia-green" />
S3 Storage Configuration
</label>
</div>
<div className="bg-background/50 rounded-lg p-3 space-y-3">
<div className="grid grid-cols-1 gap-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Endpoint URL</label>
<Input
placeholder="http://localhost:9000"
value={s3Endpoint}
onChange={(e) => setS3Endpoint(e.target.value)}
required
className="h-8 text-sm"
/>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Bucket Name</label>
<Input
placeholder="txt2kg"
value={s3Bucket}
onChange={(e) => setS3Bucket(e.target.value)}
required
className="h-8 text-sm"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Access Key</label>
<Input
placeholder="Access Key ID"
value={s3AccessKey}
onChange={(e) => setS3AccessKey(e.target.value)}
required
className="h-8 text-sm"
/>
</div>
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Secret Key</label>
<Input
type="password"
placeholder="Secret Access Key"
value={s3SecretKey}
onChange={(e) => setS3SecretKey(e.target.value)}
required
className="h-8 text-sm"
/>
</div>
</div>
</div>
</div>
{s3Error && (
<div className="text-xs text-destructive bg-destructive/10 p-2 rounded-md border border-destructive/30">
{s3Error}
</div>
)}
{isS3Connected && (
<div className="bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800/50 rounded-lg p-3">
<div className="flex items-center gap-2 text-green-800 dark:text-green-300 text-sm">
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse"></div>
<span className="font-medium">Connected</span>
<span className="text-green-700 dark:text-green-400 ml-2">
{s3FileCount} {s3FileCount === 1 ? 'file' : 'files'} in bucket
</span>
</div>
</div>
)}
<div className="flex justify-end pt-3 border-t border-border/30">
<Button
type="submit"
disabled={isConnecting}
className="flex items-center gap-2 px-4 py-2 rounded-md bg-primary hover:bg-primary/90 text-primary-foreground transition-colors text-sm font-medium shadow-sm"
>
{isConnecting ? (
<>
<div className="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
Connecting...
</>
) : isS3Connected ? (
<>
<Save className="h-4 w-4" />
Update Connection
</>
) : (
<>
<HardDrive className="h-4 w-4" />
Connect to S3
</>
)}
</Button>
</div>
</form>
</div>
)}
{activeTab === "embeddings" && (
<div className="bg-muted/30 border border-border/40 rounded-xl p-4">
<form onSubmit={saveEmbeddingsSettings} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<Cpu className="h-4 w-4 text-nvidia-green" />
Embeddings Provider
</label>
</div>
<div className="bg-background/50 rounded-lg p-3 space-y-3">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Provider Type</label>
<select
value={embeddingsProvider}
onChange={(e) => setEmbeddingsProvider(e.target.value)}
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
>
<option value="local">Local Sentence Transformer</option>
<option value="nvidia">NVIDIA API</option>
</select>
</div>
{embeddingsProvider === "nvidia" && (
<div className="space-y-3 pt-3 border-t border-border/30">
<div>
<label className="text-xs font-medium text-muted-foreground mb-1 block">Model Name</label>
<input
type="text"
value={nvidiaEmbeddingsModel}
onChange={(e) => setNvidiaEmbeddingsModel(e.target.value)}
placeholder="nvidia/llama-3.2-nv-embedqa-1b-v2"
className="w-full bg-background border border-border/60 rounded-md p-2 text-sm text-foreground focus:ring-1 focus:ring-primary/50 focus:border-primary transition-colors"
/>
</div>
<div className="bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800/50 rounded-md p-2">
<p className="text-xs text-amber-800 dark:text-amber-300/90 flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-3 h-3 mt-0.5 flex-shrink-0 text-amber-600 dark:text-amber-400">
<path fillRule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" clipRule="evenodd" />
</svg>
NVIDIA API key is configured via environment variables
</p>
</div>
</div>
)}
</div>
<div className="flex justify-end pt-3 border-t border-border/30">
<button
type="submit"
className="flex items-center gap-2 px-4 py-2 rounded-md bg-primary hover:bg-primary/90 text-primary-foreground transition-colors text-sm font-medium shadow-sm"
>
<Save className="h-4 w-4" />
Save Settings
</button>
</div>
</form>
</div>
)}
{activeTab === "models" && (
<div className="bg-muted/30 border border-border/40 rounded-xl p-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<label className="text-sm font-semibold text-foreground flex items-center gap-2">
<Server className="h-4 w-4 text-nvidia-green" />
Ollama Model Configuration
</label>
<p className="text-xs text-muted-foreground">
Select models for triple extraction dropdown
</p>
</div>
<button
type="button"
onClick={fetchOllamaModels}
disabled={isLoadingOllamaModels}
className="flex items-center gap-2 px-3 py-1.5 text-xs bg-secondary hover:bg-secondary/80 text-secondary-foreground rounded-md transition-colors shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw className={`h-3 w-3 ${isLoadingOllamaModels ? 'animate-spin' : ''}`} />
{isLoadingOllamaModels ? 'Loading...' : 'Refresh'}
</button>
</div>
{ollamaConnectionStatus === 'error' && ollamaError && (
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-3">
<div className="flex items-center gap-2 text-destructive text-xs">
<X className="h-3 w-3 flex-shrink-0" />
<span className="font-medium">Connection Error</span>
</div>
<p className="text-xs text-destructive/90 mt-1">{ollamaError}</p>
</div>
)}
{ollamaConnectionStatus === 'connected' && (
<div className="bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-800/50 rounded-lg p-3">
<div className="flex items-center gap-2 text-green-800 dark:text-green-300 text-xs">
<Check className="h-3 w-3 flex-shrink-0" />
<span className="font-medium">Connected</span>
<span className="text-green-700 dark:text-green-400">
{availableOllamaModels.length} model{availableOllamaModels.length !== 1 ? 's' : ''} found
</span>
</div>
</div>
)}
{availableOllamaModels.length > 0 && (
<div className="bg-background/50 rounded-lg p-3 space-y-3">
<form onSubmit={saveOllamaSettings} className="space-y-3">
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-medium text-muted-foreground">Available Models</label>
<span className="text-xs text-muted-foreground">
{selectedOllamaModels.length} of {availableOllamaModels.length} selected
</span>
</div>
<div className="grid gap-1 max-h-48 overflow-y-auto border border-border/60 rounded-md p-2 bg-background">
{availableOllamaModels.map((model) => (
<label
key={model}
className="flex items-center gap-2 p-2 rounded hover:bg-muted/50 cursor-pointer transition-colors text-sm"
>
<input
type="checkbox"
checked={selectedOllamaModels.includes(model)}
onChange={() => toggleOllamaModel(model)}
className="h-3 w-3 rounded border-input text-primary focus:ring-1 focus:ring-primary/50"
/>
<Server className="h-3 w-3 text-muted-foreground flex-shrink-0" />
<span className="text-xs font-medium text-foreground truncate">{model}</span>
</label>
))}
</div>
</div>
<div className="flex justify-between items-center pt-2 border-t border-border/30">
<div className="flex gap-2 text-xs">
<button
type="button"
onClick={() => setSelectedOllamaModels(availableOllamaModels)}
className="text-primary hover:text-primary/80 transition-colors font-medium"
>
All
</button>
<span className="text-muted-foreground/50">|</span>
<button
type="button"
onClick={() => setSelectedOllamaModels([])}
className="text-primary hover:text-primary/80 transition-colors font-medium"
>
None
</button>
</div>
<button
type="submit"
className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-primary hover:bg-primary/90 text-primary-foreground transition-colors text-xs font-medium shadow-sm"
>
<Save className="h-3 w-3" />
Save Settings
</button>
</div>
</form>
</div>
)}
{availableOllamaModels.length === 0 && ollamaConnectionStatus === 'idle' && (
<div className="text-center py-8 bg-muted/20 rounded-lg border border-border/30">
<Server className="h-8 w-8 mx-auto text-muted-foreground/50 mb-2" />
<p className="text-xs text-muted-foreground">
Click "Refresh" to load available models
</p>
</div>
)}
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
)
}