Applied systematic spacing optimizations across admin manager components (40px savings): DatabaseManager.tsx: - Input field padding: py-3 → py-2 (3 inputs: retention_count, schedule_hour, schedule_freq_days) - Form field spacing: space-y-2 → space-y-1.5 (Max Backups field group) - Grid gaps: gap-4 → gap-3 (storage policy fields) - Item list spacing: space-y-2 → space-y-1.5 (backup list) - Modal form spacing: space-y-4 → space-y-3 (recovery points section) - Backup metadata text: text-[11px] → text-xs (created_at + filesize display) LdapManager.tsx: - Input field padding: py-2.5 → py-2 (4 inputs: server_uri, base_dn, user_template, groups_dn) - Form field spacing: space-y-1.5 → space-y-1 (all label+input groups) - Button padding: py-2.5 → py-2 (test connection), py-3 → py-2.5 (save policy) IdentityManager.tsx: - Input field padding: py-3 → py-2 (username, password, role in edit modal) - Form field spacing: space-y-4 → space-y-3 (edit modal form) - Form field labels: space-y-1.5 → space-y-1 (all 3 fields) - User list spacing: space-y-2 → space-y-1.5 (user items) AiManager.tsx: - Grid gaps: gap-4 → gap-3 (provider options grid) - Input field padding: py-3 → py-2 (Gemini/Claude API key inputs) - Form field spacing: space-y-2 → space-y-1.5 (API key field groups) - Modal spacing: space-y-6 → space-y-3 (provider access keys section) - Prompt section spacing: space-y-4 → space-y-3 (system prompt area) CategoryManager.tsx: - Grid gaps: gap-3 → gap-2.5 (category items grid) - Input field padding: py-3.5 → py-2.5, py-4 → py-2.5 (name + description inputs) - Form field spacing: space-y-6 → space-y-3, space-y-1.5 → space-y-1 (edit modal) Test Results: - Frontend (Vitest): 291/291 tests passing ✓ - Backend (Pytest): 41/41 tests passing ✓ - Total: 332 tests validated, zero regressions All changes maintain visual hierarchy and premium density standards without sacrificing usability.
188 lines
9.0 KiB
TypeScript
188 lines
9.0 KiB
TypeScript
import React from 'react';
|
|
import { Brain, Cpu, Zap, Lock, RotateCcw, Wifi, FileText, Shield } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface AiManagerProps {
|
|
aiConfig: any;
|
|
handleUpdateAiProvider: (provider: string) => void;
|
|
aiKeys: { gemini: string; claude: string };
|
|
setAiKeys: (keys: any) => void;
|
|
isSavingKeys: boolean;
|
|
onSaveAiKeys: () => void;
|
|
isTestingKeys: { gemini: boolean; claude: boolean };
|
|
onTestAiKey: (provider: 'gemini' | 'claude') => void;
|
|
aiPrompt: string;
|
|
setAiPrompt: (prompt: string) => void;
|
|
isSavingPrompt: boolean;
|
|
onUpdatePrompt: () => void;
|
|
}
|
|
|
|
export default function AiManager({
|
|
aiConfig,
|
|
handleUpdateAiProvider,
|
|
aiKeys,
|
|
setAiKeys,
|
|
isSavingKeys,
|
|
onSaveAiKeys,
|
|
isTestingKeys,
|
|
onTestAiKey,
|
|
aiPrompt,
|
|
setAiPrompt,
|
|
isSavingPrompt,
|
|
onUpdatePrompt
|
|
}: AiManagerProps) {
|
|
return (
|
|
<section data-testid="ai-config" className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-6 transition-all group/ai">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
|
|
<Brain size={20} />
|
|
</div>
|
|
<h2 className="text-xl font-normal text-white tracking-tight">AI Intelligence</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid sm:grid-cols-2 gap-3">
|
|
{aiConfig?.providers?.map((p: any) => (
|
|
<button
|
|
key={p.id}
|
|
data-testid="provider-option"
|
|
onClick={() => handleUpdateAiProvider(p.id)}
|
|
className={cn(
|
|
"p-4 rounded-2xl border transition-all text-left flex items-center justify-between group",
|
|
p.active
|
|
? "bg-primary border-primary shadow-xl shadow-primary/10"
|
|
: "bg-background/60 border-slate-800 hover:border-primary/40"
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className={cn(
|
|
"w-8 h-8 rounded-lg flex items-center justify-center transition-colors",
|
|
p.active ? "bg-white/20 text-white" : "bg-primary/10 text-primary group-hover:bg-primary/20"
|
|
)}>
|
|
{p.id === 'gemini' ? <Cpu size={16} /> : <Zap size={16} />}
|
|
</div>
|
|
<div>
|
|
<p className={cn("text-xs font-normal tracking-tight", p.active ? "text-white" : "text-secondary")}>{p.name}</p>
|
|
<p className={cn(
|
|
"text-xs font-normal mt-1 px-0.5 rounded",
|
|
p.active
|
|
? (p.configured ? "text-emerald-200" : "text-rose-100")
|
|
: (p.configured ? "text-emerald-500" : "text-rose-500")
|
|
)}>
|
|
{p.configured ? "● Key Configured" : "○ Missing Api Key"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{p.active && (
|
|
<div className="bg-white/20 px-2.5 py-1 rounded-full text-xs font-normal text-white tracking-tight">
|
|
Active
|
|
</div>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="bg-primary/5 border border-primary/10 rounded-[2rem] p-6 space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-primary/10 rounded-lg text-primary"><Lock size={14} /></div>
|
|
<h3 className="text-sm font-normal text-secondary tracking-tight">Provider Access Keys</h3>
|
|
</div>
|
|
<button
|
|
onClick={onSaveAiKeys}
|
|
disabled={isSavingKeys}
|
|
data-testid="save-settings-button"
|
|
className="px-6 py-2.5 bg-primary hover:bg-primary text-white rounded-xl text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/10 tracking-tight border border-primary/30 flex items-center gap-2"
|
|
>
|
|
{isSavingKeys ? <RotateCcw size={14} className="animate-spin" /> : <Lock size={14} />}
|
|
{isSavingKeys ? "Storing..." : "Store API Keys"}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-6">
|
|
<div className="space-y-1.5">
|
|
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Gemini Api Key</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
data-testid="ai-api-key-input"
|
|
type="password"
|
|
value={aiKeys.gemini}
|
|
onChange={(e) => setAiKeys({...aiKeys, gemini: e.target.value})}
|
|
placeholder={aiConfig?.providers?.find((p: any) => p.id === 'gemini')?.masked_key || "Enter Gemini Key..."}
|
|
className="flex-1 bg-background/80 border border-slate-800 rounded-xl py-2 px-4 text-xs text-white outline-none focus:border-primary/50 transition-all placeholder:text-secondary"
|
|
/>
|
|
<button
|
|
onClick={() => onTestAiKey('gemini')}
|
|
disabled={isTestingKeys.gemini}
|
|
className={cn(
|
|
"px-4 py-2 rounded-xl text-xs font-normal tracking-tight transition-all border shadow-lg flex items-center gap-1.5",
|
|
isTestingKeys.gemini
|
|
? "bg-slate-800 border-slate-700 text-muted cursor-wait"
|
|
: "bg-primary border-primary text-white hover:bg-primary"
|
|
)}
|
|
>
|
|
{isTestingKeys.gemini ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />}
|
|
{isTestingKeys.gemini ? "..." : "Test"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<label className="text-sm font-normal text-secondary tracking-tight ml-1">Claude Api Key</label>
|
|
<div className="flex gap-2">
|
|
<input
|
|
data-testid="ai-api-key-input"
|
|
type="password"
|
|
value={aiKeys.claude}
|
|
onChange={(e) => setAiKeys({...aiKeys, claude: e.target.value})}
|
|
placeholder={aiConfig?.providers?.find((p: any) => p.id === 'claude')?.masked_key || "Enter Claude Key..."}
|
|
className="flex-1 bg-background/80 border border-slate-800 rounded-xl py-2 px-4 text-xs text-white outline-none focus:border-primary/50 transition-all placeholder:text-secondary"
|
|
/>
|
|
<button
|
|
onClick={() => onTestAiKey('claude')}
|
|
disabled={isTestingKeys.claude}
|
|
className={cn(
|
|
"px-4 py-2 rounded-xl text-xs font-normal tracking-tight transition-all border shadow-lg flex items-center gap-1.5",
|
|
isTestingKeys.claude
|
|
? "bg-slate-800 border-slate-700 text-muted cursor-wait"
|
|
: "bg-primary border-primary text-white hover:bg-primary"
|
|
)}
|
|
>
|
|
{isTestingKeys.claude ? <RotateCcw size={12} className="animate-spin" /> : <Wifi size={12} />}
|
|
{isTestingKeys.claude ? "..." : "Test"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div className="space-y-1.5">
|
|
<div className="flex items-center justify-between px-1">
|
|
<label className="text-sm font-normal text-secondary tracking-tight">System Prompt (Vision Extraction)</label>
|
|
<button
|
|
onClick={onUpdatePrompt}
|
|
disabled={isSavingPrompt}
|
|
className="px-6 py-2 bg-primary hover:bg-primary text-white rounded-xl text-sm font-normal transition-all active:scale-95 shadow-xl shadow-primary/10 tracking-tight border border-primary/30 flex items-center gap-2"
|
|
>
|
|
{isSavingPrompt ? <RotateCcw size={14} className="animate-spin" /> : <FileText size={14} />}
|
|
{isSavingPrompt ? "Saving..." : "Save Prompt"}
|
|
</button>
|
|
</div>
|
|
<textarea
|
|
value={aiPrompt}
|
|
onChange={(e) => setAiPrompt(e.target.value)}
|
|
className="w-full bg-background/80 border border-slate-800 rounded-2xl p-6 text-xs font-mono font-normal text-secondary leading-relaxed outline-none focus:border-purple-500/50 transition-all min-h-[200px] custom-scrollbar shadow-inner"
|
|
/>
|
|
</div>
|
|
<div className="bg-primary/5 border border-purple-500/10 rounded-2xl p-4 flex gap-4 items-start">
|
|
<div className="p-2 bg-primary/10 rounded-lg text-purple-400 shrink-0"><Shield size={14} /></div>
|
|
<p className="text-xs font-medium text-secondary leading-relaxed">
|
|
This prompt instructs the Vision AI core on label interpretation. Ensure it defines explicit mapping for technical attributes like <span className="text-purple-400">Item</span>, <span className="text-purple-400">Type</span>, and <span className="text-purple-400">Part Number</span> to avoid extraction null-pointers and ensure inventory data integrity.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|