Files
tfm_ainventory/frontend/components/admin/AiManager.tsx
Daniel Bedeleanu 12b2ef26cf fix: implement Phase 2 medium-impact spacing reductions
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.
2026-04-19 18:36:44 +03:00

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>
);
}