Update notification-settings.tsx

This commit is contained in:
MacRimi
2026-03-17 14:56:23 +01:00
parent ac71057a3d
commit feaf7b8abd

View File

@@ -14,7 +14,7 @@ import {
Bell, BellOff, Send, CheckCircle2, XCircle, Loader2, Bell, BellOff, Send, CheckCircle2, XCircle, Loader2,
AlertTriangle, Info, Settings2, Zap, Eye, EyeOff, AlertTriangle, Info, Settings2, Zap, Eye, EyeOff,
Trash2, ChevronDown, ChevronUp, ChevronRight, TestTube2, Mail, Webhook, Trash2, ChevronDown, ChevronUp, ChevronRight, TestTube2, Mail, Webhook,
Copy, Server, Shield Copy, Server, Shield, ExternalLink
} from "lucide-react" } from "lucide-react"
interface ChannelConfig { interface ChannelConfig {
@@ -110,37 +110,43 @@ const AI_PROVIDERS = [
value: "groq", value: "groq",
label: "Groq", label: "Groq",
model: "llama-3.3-70b-versatile", model: "llama-3.3-70b-versatile",
description: "Very fast, generous free tier (30 req/min). Ideal to start." description: "Very fast, generous free tier (30 req/min). Ideal to start.",
keyUrl: "https://console.groq.com/keys"
}, },
{ {
value: "openai", value: "openai",
label: "OpenAI", label: "OpenAI",
model: "gpt-4o-mini", model: "gpt-4o-mini",
description: "Industry standard. Very accurate and widely used." description: "Industry standard. Very accurate and widely used.",
keyUrl: "https://platform.openai.com/api-keys"
}, },
{ {
value: "anthropic", value: "anthropic",
label: "Anthropic (Claude)", label: "Anthropic (Claude)",
model: "claude-3-haiku-20240307", model: "claude-3-haiku-20240307",
description: "Excellent for writing and translation. Fast and economical." description: "Excellent for writing and translation. Fast and economical.",
keyUrl: "https://console.anthropic.com/settings/keys"
}, },
{ {
value: "gemini", value: "gemini",
label: "Google Gemini", label: "Google Gemini",
model: "gemini-1.5-flash", model: "gemini-1.5-flash",
description: "Free tier available, great quality/price ratio." description: "Free tier available, great quality/price ratio.",
keyUrl: "https://aistudio.google.com/app/apikey"
}, },
{ {
value: "ollama", value: "ollama",
label: "Ollama (Local)", label: "Ollama (Local)",
model: "llama3.2", model: "llama3.2",
description: "100% local execution. No costs, total privacy, no internet required." description: "100% local execution. No costs, total privacy, no internet required.",
keyUrl: ""
}, },
{ {
value: "openrouter", value: "openrouter",
label: "OpenRouter", label: "OpenRouter",
model: "meta-llama/llama-3.3-70b-instruct", model: "meta-llama/llama-3.3-70b-instruct",
description: "Aggregator with access to 100+ models using a single API key. Maximum flexibility." description: "Aggregator with access to 100+ models using a single API key. Maximum flexibility.",
keyUrl: "https://openrouter.ai/keys"
}, },
] ]
@@ -1330,12 +1336,12 @@ export function NotificationSettings() {
{config.ai_enabled && ( {config.ai_enabled && (
<> <>
{/* Provider + Info button */} {/* Provider + Info button */}
<div className="space-y-1.5"> <div className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Label className="text-[11px] text-muted-foreground">Provider</Label> <Label className="text-xs sm:text-sm text-muted-foreground">Provider</Label>
<button <button
onClick={() => setShowProviderInfo(true)} onClick={() => setShowProviderInfo(true)}
className="text-[10px] text-blue-400 hover:text-blue-300 transition-colors" className="text-xs text-blue-400 hover:text-blue-300 transition-colors"
> >
+info +info
</button> </button>
@@ -1345,7 +1351,7 @@ export function NotificationSettings() {
onValueChange={v => updateConfig(p => ({ ...p, ai_provider: v }))} onValueChange={v => updateConfig(p => ({ ...p, ai_provider: v }))}
disabled={!editMode} disabled={!editMode}
> >
<SelectTrigger className="h-7 text-xs"> <SelectTrigger className="h-9 text-sm">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -1358,10 +1364,10 @@ export function NotificationSettings() {
{/* Ollama URL (conditional) */} {/* Ollama URL (conditional) */}
{config.ai_provider === "ollama" && ( {config.ai_provider === "ollama" && (
<div className="space-y-1.5"> <div className="space-y-2">
<Label className="text-[11px] text-muted-foreground">Ollama URL</Label> <Label className="text-xs sm:text-sm text-muted-foreground">Ollama URL</Label>
<Input <Input
className="h-7 text-xs font-mono" className="h-9 text-sm font-mono"
placeholder="http://localhost:11434" placeholder="http://localhost:11434"
value={config.ai_ollama_url} value={config.ai_ollama_url}
onChange={e => updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))} onChange={e => updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))}
@@ -1372,32 +1378,44 @@ export function NotificationSettings() {
{/* API Key (not shown for Ollama) */} {/* API Key (not shown for Ollama) */}
{config.ai_provider !== "ollama" && ( {config.ai_provider !== "ollama" && (
<div className="space-y-1.5"> <div className="space-y-2">
<Label className="text-[11px] text-muted-foreground">API Key</Label> <div className="flex items-center gap-2">
<div className="flex items-center gap-1.5"> <Label className="text-xs sm:text-sm text-muted-foreground">API Key</Label>
{AI_PROVIDERS.find(p => p.value === config.ai_provider)?.keyUrl && (
<a
href={AI_PROVIDERS.find(p => p.value === config.ai_provider)?.keyUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 hover:text-blue-300 transition-colors flex items-center gap-1"
>
Get key <ExternalLink className="h-3 w-3" />
</a>
)}
</div>
<div className="flex items-center gap-2">
<Input <Input
type={showSecrets["ai_key"] ? "text" : "password"} type={showSecrets["ai_key"] ? "text" : "password"}
className="h-7 text-xs font-mono" className="h-9 text-sm font-mono"
placeholder="sk-..." placeholder="sk-..."
value={config.ai_api_key} value={config.ai_api_key}
onChange={e => updateConfig(p => ({ ...p, ai_api_key: e.target.value }))} onChange={e => updateConfig(p => ({ ...p, ai_api_key: e.target.value }))}
disabled={!editMode} disabled={!editMode}
/> />
<button <button
className="h-7 w-7 flex items-center justify-center rounded-md border border-border hover:bg-muted transition-colors shrink-0" className="h-9 w-9 flex items-center justify-center rounded-md border border-border hover:bg-muted transition-colors shrink-0"
onClick={() => toggleSecret("ai_key")} onClick={() => toggleSecret("ai_key")}
> >
{showSecrets["ai_key"] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />} {showSecrets["ai_key"] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button> </button>
</div> </div>
</div> </div>
)} )}
{/* Model (optional) */} {/* Model (optional) */}
<div className="space-y-1.5"> <div className="space-y-2">
<Label className="text-[11px] text-muted-foreground">Model (optional)</Label> <Label className="text-xs sm:text-sm text-muted-foreground">Model (optional)</Label>
<Input <Input
className="h-7 text-xs font-mono" className="h-9 text-sm font-mono"
placeholder={AI_PROVIDERS.find(p => p.value === config.ai_provider)?.model || ""} placeholder={AI_PROVIDERS.find(p => p.value === config.ai_provider)?.model || ""}
value={config.ai_model} value={config.ai_model}
onChange={e => updateConfig(p => ({ ...p, ai_model: e.target.value }))} onChange={e => updateConfig(p => ({ ...p, ai_model: e.target.value }))}
@@ -1406,14 +1424,14 @@ export function NotificationSettings() {
</div> </div>
{/* Language selector */} {/* Language selector */}
<div className="space-y-1.5"> <div className="space-y-2">
<Label className="text-[11px] text-muted-foreground">Language</Label> <Label className="text-xs sm:text-sm text-muted-foreground">Language</Label>
<Select <Select
value={config.ai_language} value={config.ai_language}
onValueChange={v => updateConfig(p => ({ ...p, ai_language: v }))} onValueChange={v => updateConfig(p => ({ ...p, ai_language: v }))}
disabled={!editMode} disabled={!editMode}
> >
<SelectTrigger className="h-7 text-xs"> <SelectTrigger className="h-9 text-sm">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -1428,27 +1446,27 @@ export function NotificationSettings() {
<button <button
onClick={handleTestAI} onClick={handleTestAI}
disabled={!editMode || testingAI || (config.ai_provider !== "ollama" && !config.ai_api_key)} disabled={!editMode || testingAI || (config.ai_provider !== "ollama" && !config.ai_api_key)}
className="w-full h-7 flex items-center justify-center gap-1.5 rounded-md text-xs font-medium bg-purple-600 hover:bg-purple-700 text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors" className="w-full h-9 flex items-center justify-center gap-2 rounded-md text-sm font-medium bg-purple-600 hover:bg-purple-700 text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
> >
{testingAI ? ( {testingAI ? (
<><Loader2 className="h-3 w-3 animate-spin" /> Testing...</> <><Loader2 className="h-4 w-4 animate-spin" /> Testing...</>
) : ( ) : (
<><Zap className="h-3 w-3" /> Test Connection</> <><Zap className="h-4 w-4" /> Test Connection</>
)} )}
</button> </button>
{/* Test result */} {/* Test result */}
{aiTestResult && ( {aiTestResult && (
<div className={`flex items-start gap-2 p-2 rounded-md ${ <div className={`flex items-start gap-2 p-3 rounded-md ${
aiTestResult.success aiTestResult.success
? "bg-green-500/10 border border-green-500/20" ? "bg-green-500/10 border border-green-500/20"
: "bg-red-500/10 border border-red-500/20" : "bg-red-500/10 border border-red-500/20"
}`}> }`}>
{aiTestResult.success {aiTestResult.success
? <CheckCircle2 className="h-3.5 w-3.5 text-green-400 shrink-0 mt-0.5" /> ? <CheckCircle2 className="h-4 w-4 text-green-400 shrink-0 mt-0.5" />
: <XCircle className="h-3.5 w-3.5 text-red-400 shrink-0 mt-0.5" /> : <XCircle className="h-4 w-4 text-red-400 shrink-0 mt-0.5" />
} }
<p className={`text-[10px] leading-relaxed ${ <p className={`text-xs sm:text-sm leading-relaxed ${
aiTestResult.success ? "text-green-400/90" : "text-red-400/90" aiTestResult.success ? "text-green-400/90" : "text-red-400/90"
}`}> }`}>
{aiTestResult.message} {aiTestResult.message}
@@ -1458,12 +1476,12 @@ export function NotificationSettings() {
)} )}
{/* Per-channel detail level */} {/* Per-channel detail level */}
<div className="space-y-2 pt-2 border-t border-border/50"> <div className="space-y-3 pt-3 border-t border-border/50">
<Label className="text-[11px] text-muted-foreground">Detail Level per Channel</Label> <Label className="text-xs sm:text-sm text-muted-foreground">Detail Level per Channel</Label>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-3">
{CHANNEL_TYPES.map(ch => ( {CHANNEL_TYPES.map(ch => (
<div key={ch} className="flex items-center justify-between gap-2 px-2 py-1 rounded bg-muted/30"> <div key={ch} className="flex items-center justify-between gap-2 px-3 py-2 rounded bg-muted/30">
<span className="text-[10px] text-muted-foreground capitalize">{ch}</span> <span className="text-xs sm:text-sm text-muted-foreground capitalize">{ch}</span>
<Select <Select
value={config.channel_ai_detail?.[ch] || "standard"} value={config.channel_ai_detail?.[ch] || "standard"}
onValueChange={v => updateConfig(p => ({ onValueChange={v => updateConfig(p => ({
@@ -1472,12 +1490,12 @@ export function NotificationSettings() {
}))} }))}
disabled={!editMode} disabled={!editMode}
> >
<SelectTrigger className="h-5 w-[80px] text-[10px] px-1.5"> <SelectTrigger className="h-7 w-[90px] text-xs px-2">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{AI_DETAIL_LEVELS.map(l => ( {AI_DETAIL_LEVELS.map(l => (
<SelectItem key={l.value} value={l.value} className="text-[10px]"> <SelectItem key={l.value} value={l.value} className="text-xs">
{l.label} {l.label}
</SelectItem> </SelectItem>
))} ))}
@@ -1488,9 +1506,9 @@ export function NotificationSettings() {
</div> </div>
</div> </div>
<div className="flex items-start gap-2 p-2 rounded-md bg-purple-500/10 border border-purple-500/20"> <div className="flex items-start gap-2 p-3 rounded-md bg-purple-500/10 border border-purple-500/20">
<Info className="h-3.5 w-3.5 text-purple-400 shrink-0 mt-0.5" /> <Info className="h-4 w-4 text-purple-400 shrink-0 mt-0.5" />
<p className="text-[10px] text-purple-400/90 leading-relaxed"> <p className="text-xs sm:text-sm text-purple-400/90 leading-relaxed">
AI enhancement translates and formats notifications to your selected language. Each channel can have different detail levels. If the AI service is unavailable, standard templates are used as fallback. AI enhancement translates and formats notifications to your selected language. Each channel can have different detail levels. If the AI service is unavailable, standard templates are used as fallback.
</p> </p>
</div> </div>
@@ -1506,8 +1524,8 @@ export function NotificationSettings() {
{/* ── Footer info ── */} {/* ── Footer info ── */}
<div className="flex items-start gap-2 pt-3 border-t border-border"> <div className="flex items-start gap-2 pt-3 border-t border-border">
<Info className="h-3.5 w-3.5 text-blue-400 shrink-0 mt-0.5" /> <Info className="h-4 w-4 text-blue-400 shrink-0 mt-0.5" />
<p className="text-[11px] text-muted-foreground leading-relaxed"> <p className="text-xs sm:text-sm text-muted-foreground leading-relaxed">
{config.enabled {config.enabled
? "Notifications are active. Each channel sends events based on its own category and event selection." ? "Notifications are active. Each channel sends events based on its own category and event selection."
: "Enable notifications to receive alerts about system events, health status changes, and security incidents via Telegram, Gotify, Discord, or Email."} : "Enable notifications to receive alerts about system events, health status changes, and security incidents via Telegram, Gotify, Discord, or Email."}
@@ -1520,24 +1538,24 @@ export function NotificationSettings() {
<Dialog open={showProviderInfo} onOpenChange={setShowProviderInfo}> <Dialog open={showProviderInfo} onOpenChange={setShowProviderInfo}>
<DialogContent className="max-w-md"> <DialogContent className="max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-base">AI Providers Information</DialogTitle> <DialogTitle className="text-base sm:text-lg">AI Providers Information</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1"> <div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
{AI_PROVIDERS.map(provider => ( {AI_PROVIDERS.map(provider => (
<div <div
key={provider.value} key={provider.value}
className="p-3 rounded-lg bg-muted/50 border border-border hover:border-muted-foreground/40 transition-colors" className="p-4 rounded-lg bg-muted/50 border border-border hover:border-muted-foreground/40 transition-colors"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="font-medium text-sm">{provider.label}</span> <span className="font-medium text-sm sm:text-base">{provider.label}</span>
{provider.value === "ollama" && ( {provider.value === "ollama" && (
<Badge variant="outline" className="text-[9px] px-1.5 py-0">Local</Badge> <Badge variant="outline" className="text-xs px-2 py-0.5">Local</Badge>
)} )}
</div> </div>
<div className="text-[11px] text-muted-foreground mt-1"> <div className="text-xs sm:text-sm text-muted-foreground mt-1.5">
Default model: <code className="text-[10px] bg-muted px-1 py-0.5 rounded font-mono">{provider.model}</code> Default model: <code className="text-xs bg-muted px-1.5 py-0.5 rounded font-mono">{provider.model}</code>
</div> </div>
<p className="text-[11px] text-muted-foreground mt-2 leading-relaxed"> <p className="text-xs sm:text-sm text-muted-foreground mt-2 leading-relaxed">
{provider.description} {provider.description}
</p> </p>
</div> </div>