1 line
690 KiB
JSON
1 line
690 KiB
JSON
{"repo": "andriyze/shakerscan", "files": {"ui/src/components/AISettingsPanel.tsx": {"99ee3a0ad73e2b40516a0a8c13d036f3211646ed": {"t": 1785746446.0120826, "n": 34867, "h": "fe392fd83e6ae430", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\nimport { Button, Fieldset, Tabs, ToggleVisual, fieldClasses, useToast } from '@/components/ui'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\n// Point the panel's local field class at the shared field styling so every\n// input/select here matches the rest of the app and gets a real focus ring.\nconst INPUT_CLASS = `mt-1 w-full ${fieldClasses()}`\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n const raw = String(value || '').trim().replace(/\\/+$/, '')\n try {\n const url = new URL(raw)\n url.protocol = url.protocol.toLowerCase()\n url.hostname = url.hostname.toLowerCase()\n return url.toString().replace(/\\/+$/, '')\n } catch {\n return raw\n }\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const toast = useToast()\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [autoFpOnRetestInput, setAutoFpOnRetestInput] = useState(false)\n const [autoFpMinConfidenceInput, setAutoFpMinConfidenceInput] = useState('0.9')\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setAutoFpOnRetestInput(Boolean(settings.auto_fp_on_retest))\n setAutoFpMinConfidenceInput(String(settings.auto_fp_min_confidence ?? 0.9))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n auto_fp_on_retest: autoFpOnRetestInput,\n auto_fp_min_confidence: Math.min(1, Math.max(0, Number.parseFloat(autoFpMinConfidenceInput || '0.9') || 0.9)),\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n const successMessage = result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n setAISettingsMessage(successMessage)\n toast.success(successMessage)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n toast.error(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n if (result.status === 'ok') {\n toast.success(message)\n } else {\n toast.error(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n toast.error(`Probe failed: ${message}`)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI provider & verification</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={fetchAISettings} disabled={loading || aiSaving}>\n Refresh\n </Button>\n <Button size=\"sm\" onClick={handleSaveAISettings} loading={aiSaving} disabled={loading}>\n {aiSaving ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <Tabs\n ariaLabel=\"AI settings detail level\"\n active={settingsMode}\n onChange={(key) => setSettingsMode(key as 'basic' | 'advanced')}\n items={[\n { key: 'basic', label: 'Basic' },\n { key: 'advanced', label: 'Advanced' },\n ]}\n />\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <Fieldset\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <Tabs\n ariaLabel=\"Honey source\"\n active={demoHoneyModeInput}\n onChange={(key) => selectDemoHoneyMode(key as DemoHoneyMode)}\n items={[\n { key: 'hosted', label: 'Hosted Honey' },\n { key: 'local', label: 'Local Honey' },\n { key: 'custom', label: 'Custom' },\n ]}\n />\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n aria-expanded={showDemoNetworking}\n className=\"rounded text-xs text-gray-400 hover:text-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </Fieldset>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <Fieldset\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('verify')}\n loading={testingScope === 'verify'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'verify' ? 'Testing retest AI…' : 'Test AI Verification'}\n </Button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <Fieldset\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"deepseek/deepseek-chat,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('scan')}\n loading={testingScope === 'scan'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'scan' ? 'Testing provider…' : 'Test Shared Provider'}\n </Button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n\n <Fieldset\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </Fieldset>\n\n <Fieldset\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('verify')}\n loading={testingScope === 'verify'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'verify' ? 'Testing retest AI…' : 'Test Retest AI'}\n </Button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n\n <Fieldset\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <ToggleRow\n label=\"Auto-close high-confidence false positives\"\n description=\"When on, a retest that concludes a high-confidence false positive flips the finding from active to false positive automatically (audited and reversible).\"\n hint=\"Off by default: a wrong auto-FP hides a real vulnerability. Leave off to keep a human in the loop via the one-click 'mark false positive' control.\"\n checked={autoFpOnRetestInput}\n onChange={setAutoFpOnRetestInput}\n />\n {autoFpOnRetestInput && (\n <Field\n label=\"Auto-FP minimum confidence\"\n hint=\"Retest confidence required before auto-closing (0–1). Higher than the 0.7 false-positive verdict bar because auto-closing is riskier than labeling.\"\n >\n <input\n type=\"number\"\n min={0.7}\n max={1}\n step={0.05}\n value={autoFpMinConfidenceInput}\n onChange={(e) => setAutoFpMinConfidenceInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n )}\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </Fieldset>\n\n <Fieldset\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </Fieldset>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Keep settings after restart\"\n description=\"When on, saved settings survive service and container restarts. When off, changes last only for the current runtime.\"\n hint=\"Leave off for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n onClick={() => onChange(!checked)}\n className=\"flex w-full items-start justify-between gap-3 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2 text-left transition-colors hover:border-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-950\"\n >\n <span className=\"min-w-0 space-y-0.5\">\n <span className=\"block text-xs text-gray-200\">{label}</span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n {hint && <span className=\"block text-[11px] text-gray-600\">{hint}</span>}\n </span>\n <ToggleVisual checked={checked} />\n </button>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "c8f07b7652e9c03c82cd0da6048b090ee7034659": {"t": 1785764325.9782884, "n": 34867, "h": "fe392fd83e6ae430", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\nimport { Button, Fieldset, Tabs, ToggleVisual, fieldClasses, useToast } from '@/components/ui'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\n// Point the panel's local field class at the shared field styling so every\n// input/select here matches the rest of the app and gets a real focus ring.\nconst INPUT_CLASS = `mt-1 w-full ${fieldClasses()}`\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n const raw = String(value || '').trim().replace(/\\/+$/, '')\n try {\n const url = new URL(raw)\n url.protocol = url.protocol.toLowerCase()\n url.hostname = url.hostname.toLowerCase()\n return url.toString().replace(/\\/+$/, '')\n } catch {\n return raw\n }\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const toast = useToast()\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [autoFpOnRetestInput, setAutoFpOnRetestInput] = useState(false)\n const [autoFpMinConfidenceInput, setAutoFpMinConfidenceInput] = useState('0.9')\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setAutoFpOnRetestInput(Boolean(settings.auto_fp_on_retest))\n setAutoFpMinConfidenceInput(String(settings.auto_fp_min_confidence ?? 0.9))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n auto_fp_on_retest: autoFpOnRetestInput,\n auto_fp_min_confidence: Math.min(1, Math.max(0, Number.parseFloat(autoFpMinConfidenceInput || '0.9') || 0.9)),\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n const successMessage = result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n setAISettingsMessage(successMessage)\n toast.success(successMessage)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n toast.error(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n if (result.status === 'ok') {\n toast.success(message)\n } else {\n toast.error(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n toast.error(`Probe failed: ${message}`)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI provider & verification</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={fetchAISettings} disabled={loading || aiSaving}>\n Refresh\n </Button>\n <Button size=\"sm\" onClick={handleSaveAISettings} loading={aiSaving} disabled={loading}>\n {aiSaving ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <Tabs\n ariaLabel=\"AI settings detail level\"\n active={settingsMode}\n onChange={(key) => setSettingsMode(key as 'basic' | 'advanced')}\n items={[\n { key: 'basic', label: 'Basic' },\n { key: 'advanced', label: 'Advanced' },\n ]}\n />\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <Fieldset\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <Tabs\n ariaLabel=\"Honey source\"\n active={demoHoneyModeInput}\n onChange={(key) => selectDemoHoneyMode(key as DemoHoneyMode)}\n items={[\n { key: 'hosted', label: 'Hosted Honey' },\n { key: 'local', label: 'Local Honey' },\n { key: 'custom', label: 'Custom' },\n ]}\n />\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n aria-expanded={showDemoNetworking}\n className=\"rounded text-xs text-gray-400 hover:text-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </Fieldset>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <Fieldset\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('verify')}\n loading={testingScope === 'verify'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'verify' ? 'Testing retest AI…' : 'Test AI Verification'}\n </Button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <Fieldset\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"deepseek/deepseek-chat,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('scan')}\n loading={testingScope === 'scan'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'scan' ? 'Testing provider…' : 'Test Shared Provider'}\n </Button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n\n <Fieldset\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </Fieldset>\n\n <Fieldset\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('verify')}\n loading={testingScope === 'verify'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'verify' ? 'Testing retest AI…' : 'Test Retest AI'}\n </Button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n\n <Fieldset\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <ToggleRow\n label=\"Auto-close high-confidence false positives\"\n description=\"When on, a retest that concludes a high-confidence false positive flips the finding from active to false positive automatically (audited and reversible).\"\n hint=\"Off by default: a wrong auto-FP hides a real vulnerability. Leave off to keep a human in the loop via the one-click 'mark false positive' control.\"\n checked={autoFpOnRetestInput}\n onChange={setAutoFpOnRetestInput}\n />\n {autoFpOnRetestInput && (\n <Field\n label=\"Auto-FP minimum confidence\"\n hint=\"Retest confidence required before auto-closing (0–1). Higher than the 0.7 false-positive verdict bar because auto-closing is riskier than labeling.\"\n >\n <input\n type=\"number\"\n min={0.7}\n max={1}\n step={0.05}\n value={autoFpMinConfidenceInput}\n onChange={(e) => setAutoFpMinConfidenceInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n )}\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </Fieldset>\n\n <Fieldset\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </Fieldset>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Keep settings after restart\"\n description=\"When on, saved settings survive service and container restarts. When off, changes last only for the current runtime.\"\n hint=\"Leave off for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n onClick={() => onChange(!checked)}\n className=\"flex w-full items-start justify-between gap-3 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2 text-left transition-colors hover:border-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-950\"\n >\n <span className=\"min-w-0 space-y-0.5\">\n <span className=\"block text-xs text-gray-200\">{label}</span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n {hint && <span className=\"block text-[11px] text-gray-600\">{hint}</span>}\n </span>\n <ToggleVisual checked={checked} />\n </button>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "737f3f0af2940b2bad251a6003ea86e1bde28ed2": {"t": 1785764327.3591921, "n": 34875, "h": "93616a84e8c3fc67", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\nimport { Button, Fieldset, Tabs, ToggleVisual, fieldClasses, useToast } from '@/components/ui'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\n// Point the panel's local field class at the shared field styling so every\n// input/select here matches the rest of the app and gets a real focus ring.\nconst INPUT_CLASS = `mt-1 w-full ${fieldClasses()}`\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n const raw = String(value || '').trim().replace(/\\/+$/, '')\n try {\n const url = new URL(raw)\n url.protocol = url.protocol.toLowerCase()\n url.hostname = url.hostname.toLowerCase()\n return url.toString().replace(/\\/+$/, '')\n } catch {\n return raw\n }\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const toast = useToast()\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [autoFpOnRetestInput, setAutoFpOnRetestInput] = useState(false)\n const [autoFpMinConfidenceInput, setAutoFpMinConfidenceInput] = useState('0.9')\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setAutoFpOnRetestInput(Boolean(settings.auto_fp_on_retest))\n setAutoFpMinConfidenceInput(String(settings.auto_fp_min_confidence ?? 0.9))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n auto_fp_on_retest: autoFpOnRetestInput,\n auto_fp_min_confidence: Math.min(1, Math.max(0, Number.parseFloat(autoFpMinConfidenceInput || '0.9') || 0.9)),\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n const successMessage = result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n setAISettingsMessage(successMessage)\n toast.success(successMessage)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n toast.error(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n if (result.status === 'ok') {\n toast.success(message)\n } else {\n toast.error(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n toast.error(`Probe failed: ${message}`)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI provider & verification</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={fetchAISettings} disabled={loading || aiSaving}>\n Refresh\n </Button>\n <Button size=\"sm\" onClick={handleSaveAISettings} loading={aiSaving} disabled={loading}>\n {aiSaving ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <Tabs\n ariaLabel=\"AI settings detail level\"\n active={settingsMode}\n onChange={(key) => setSettingsMode(key as 'basic' | 'advanced')}\n items={[\n { key: 'basic', label: 'Basic' },\n { key: 'advanced', label: 'Advanced' },\n ]}\n />\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <Fieldset\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <Tabs\n ariaLabel=\"Honey source\"\n active={demoHoneyModeInput}\n onChange={(key) => selectDemoHoneyMode(key as DemoHoneyMode)}\n items={[\n { key: 'hosted', label: 'Hosted Honey' },\n { key: 'local', label: 'Local Honey' },\n { key: 'custom', label: 'Custom' },\n ]}\n />\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n aria-expanded={showDemoNetworking}\n className=\"rounded text-xs text-gray-400 hover:text-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </Fieldset>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <Fieldset\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('verify')}\n loading={testingScope === 'verify'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'verify' ? 'Testing retest AI…' : 'Test AI Verification'}\n </Button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <Fieldset\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"deepseek/deepseek-chat,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('scan')}\n loading={testingScope === 'scan'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'scan' ? 'Testing provider…' : 'Test Shared Provider'}\n </Button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n\n <Fieldset\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </Fieldset>\n\n <Fieldset\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('verify')}\n loading={testingScope === 'verify'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'verify' ? 'Testing retest AI…' : 'Test Retest AI'}\n </Button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n\n <Fieldset\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <ToggleRow\n label=\"Auto-close high-confidence false positives\"\n description=\"When on, a retest that concludes a high-confidence false positive flips the finding from active to false positive automatically (audited and reversible).\"\n hint=\"Off by default: a wrong auto-FP hides a real vulnerability. Leave off to keep a human in the loop via the one-click 'mark false positive' control.\"\n checked={autoFpOnRetestInput}\n onChange={setAutoFpOnRetestInput}\n />\n {autoFpOnRetestInput && (\n <Field\n label=\"Auto-FP minimum confidence\"\n hint=\"Retest confidence required before auto-closing (0–1). Higher than the 0.7 false-positive verdict bar because auto-closing is riskier than labeling.\"\n >\n <input\n type=\"number\"\n min={0.7}\n max={1}\n step={0.05}\n value={autoFpMinConfidenceInput}\n onChange={(e) => setAutoFpMinConfidenceInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n )}\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </Fieldset>\n\n <Fieldset\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </Fieldset>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Keep settings after restart\"\n description=\"When on, saved settings survive service and container restarts. When off, changes last only for the current runtime.\"\n hint=\"Leave off for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={label}\n onClick={() => onChange(!checked)}\n className=\"flex w-full items-start justify-between gap-3 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2 text-left transition-colors hover:border-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-950\"\n >\n <span className=\"min-w-0 space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n <ToggleVisual checked={checked} />\n </button>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "85eb9d35e9488f79b44af17e9f89d27342cc86b5": {"t": 1785764329.2410634, "n": 34550, "h": "ac7a167e6da1db82", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\nimport { Button, Fieldset, Tabs, Toggle, fieldClasses, useToast } from '@/components/ui'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\n// Point the panel's local field class at the shared field styling so every\n// input/select here matches the rest of the app and gets a real focus ring.\nconst INPUT_CLASS = `mt-1 ${fieldClasses()}`\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n const raw = String(value || '').trim().replace(/\\/+$/, '')\n try {\n const url = new URL(raw)\n url.protocol = url.protocol.toLowerCase()\n url.hostname = url.hostname.toLowerCase()\n return url.toString().replace(/\\/+$/, '')\n } catch {\n return raw\n }\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const toast = useToast()\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [autoFpOnRetestInput, setAutoFpOnRetestInput] = useState(false)\n const [autoFpMinConfidenceInput, setAutoFpMinConfidenceInput] = useState('0.9')\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setAutoFpOnRetestInput(Boolean(settings.auto_fp_on_retest))\n setAutoFpMinConfidenceInput(String(settings.auto_fp_min_confidence ?? 0.9))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n auto_fp_on_retest: autoFpOnRetestInput,\n auto_fp_min_confidence: Math.min(1, Math.max(0, Number.parseFloat(autoFpMinConfidenceInput || '0.9') || 0.9)),\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n const successMessage = result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n setAISettingsMessage(successMessage)\n toast.success(successMessage)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n toast.error(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n if (result.status === 'ok') {\n toast.success(message)\n } else {\n toast.error(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n toast.error(`Probe failed: ${message}`)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI provider & verification</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={fetchAISettings} disabled={loading || aiSaving}>\n Refresh\n </Button>\n <Button size=\"sm\" onClick={handleSaveAISettings} loading={aiSaving} disabled={loading}>\n {aiSaving ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <Tabs\n ariaLabel=\"AI settings detail level\"\n active={settingsMode}\n onChange={(key) => setSettingsMode(key as 'basic' | 'advanced')}\n items={[\n { key: 'basic', label: 'Basic' },\n { key: 'advanced', label: 'Advanced' },\n ]}\n />\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <Fieldset\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <Tabs\n ariaLabel=\"Honey source\"\n active={demoHoneyModeInput}\n onChange={(key) => selectDemoHoneyMode(key as DemoHoneyMode)}\n items={[\n { key: 'hosted', label: 'Hosted Honey' },\n { key: 'local', label: 'Local Honey' },\n { key: 'custom', label: 'Custom' },\n ]}\n />\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n aria-expanded={showDemoNetworking}\n className=\"rounded text-xs text-gray-400 hover:text-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </Fieldset>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <Fieldset\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('verify')}\n loading={testingScope === 'verify'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'verify' ? 'Testing retest AI…' : 'Test AI Verification'}\n </Button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <Fieldset\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"deepseek/deepseek-chat,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('scan')}\n loading={testingScope === 'scan'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'scan' ? 'Testing provider…' : 'Test Shared Provider'}\n </Button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n\n <Fieldset\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </Fieldset>\n\n <Fieldset\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => handleTestAISettings('verify')}\n loading={testingScope === 'verify'}\n disabled={loading || aiSaving || testingScope !== null}\n >\n {testingScope === 'verify' ? 'Testing retest AI…' : 'Test Retest AI'}\n </Button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </Fieldset>\n\n <Fieldset\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <ToggleRow\n label=\"Auto-close high-confidence false positives\"\n description=\"When on, a retest that concludes a high-confidence false positive flips the finding from active to false positive automatically (audited and reversible).\"\n hint=\"Off by default: a wrong auto-FP hides a real vulnerability. Leave off to keep a human in the loop via the one-click 'mark false positive' control.\"\n checked={autoFpOnRetestInput}\n onChange={setAutoFpOnRetestInput}\n />\n {autoFpOnRetestInput && (\n <Field\n label=\"Auto-FP minimum confidence\"\n hint=\"Retest confidence required before auto-closing (0–1). Higher than the 0.7 false-positive verdict bar because auto-closing is riskier than labeling.\"\n >\n <input\n type=\"number\"\n min={0.7}\n max={1}\n step={0.05}\n value={autoFpMinConfidenceInput}\n onChange={(e) => setAutoFpMinConfidenceInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n )}\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </Fieldset>\n\n <Fieldset\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </Fieldset>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Keep settings after restart\"\n description=\"When on, saved settings survive service and container restarts. When off, changes last only for the current runtime.\"\n hint=\"Leave off for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <div className=\"flex items-start justify-between gap-3 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <span className=\"min-w-0 space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n <Toggle checked={checked} onChange={onChange} label={label} />\n </div>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "1d4484f587d2a6b187347e752cc84fddcbe6be47": {"t": 1785764330.77839, "n": 37725, "h": "30713cc23302b098", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\nimport { useToast } from '@/components/ui'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n const raw = String(value || '').trim().replace(/\\/+$/, '')\n try {\n const url = new URL(raw)\n url.protocol = url.protocol.toLowerCase()\n url.hostname = url.hostname.toLowerCase()\n return url.toString().replace(/\\/+$/, '')\n } catch {\n return raw\n }\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const toast = useToast()\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [autoFpOnRetestInput, setAutoFpOnRetestInput] = useState(false)\n const [autoFpMinConfidenceInput, setAutoFpMinConfidenceInput] = useState('0.9')\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setAutoFpOnRetestInput(Boolean(settings.auto_fp_on_retest))\n setAutoFpMinConfidenceInput(String(settings.auto_fp_min_confidence ?? 0.9))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n auto_fp_on_retest: autoFpOnRetestInput,\n auto_fp_min_confidence: Math.min(1, Math.max(0, Number.parseFloat(autoFpMinConfidenceInput || '0.9') || 0.9)),\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n const successMessage = result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n setAISettingsMessage(successMessage)\n toast.success(successMessage)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n toast.error(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n if (result.status === 'ok') {\n toast.success(message)\n } else {\n toast.error(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n toast.error(`Probe failed: ${message}`)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI provider & verification</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n type=\"button\"\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n type=\"button\"\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <SectionCard\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <div className=\"inline-flex flex-wrap items-center gap-1 rounded border border-gray-700 bg-gray-950 p-1\">\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('hosted')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'hosted' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'hosted'}\n >\n Hosted Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('local')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'local' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'local'}\n >\n Local Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('custom')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'custom'}\n >\n Custom\n </button>\n </div>\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n aria-expanded={showDemoNetworking}\n className=\"rounded text-xs text-gray-400 hover:text-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </SectionCard>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"deepseek/deepseek-chat,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <ToggleRow\n label=\"Auto-close high-confidence false positives\"\n description=\"When on, a retest that concludes a high-confidence false positive flips the finding from active to false positive automatically (audited and reversible).\"\n hint=\"Off by default: a wrong auto-FP hides a real vulnerability. Leave off to keep a human in the loop via the one-click 'mark false positive' control.\"\n checked={autoFpOnRetestInput}\n onChange={setAutoFpOnRetestInput}\n />\n {autoFpOnRetestInput && (\n <Field\n label=\"Auto-FP minimum confidence\"\n hint=\"Retest confidence required before auto-closing (0–1). Higher than the 0.7 false-positive verdict bar because auto-closing is riskier than labeling.\"\n >\n <input\n type=\"number\"\n min={0.7}\n max={1}\n step={0.05}\n value={autoFpMinConfidenceInput}\n onChange={(e) => setAutoFpMinConfidenceInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n )}\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Keep settings after restart\"\n description=\"When on, saved settings survive service and container restarts. When off, changes last only for the current runtime.\"\n hint=\"Leave off for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "36d36628ea546cfa8b4b00d47c9426f10821ff20": {"t": 1785764332.366383, "n": 37705, "h": "7c62b01ef529b1f3", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\nimport { useToast } from '@/components/ui'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n const raw = String(value || '').trim().replace(/\\/+$/, '')\n try {\n const url = new URL(raw)\n url.protocol = url.protocol.toLowerCase()\n url.hostname = url.hostname.toLowerCase()\n return url.toString().replace(/\\/+$/, '')\n } catch {\n return raw\n }\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const toast = useToast()\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [autoFpOnRetestInput, setAutoFpOnRetestInput] = useState(false)\n const [autoFpMinConfidenceInput, setAutoFpMinConfidenceInput] = useState('0.9')\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setAutoFpOnRetestInput(Boolean(settings.auto_fp_on_retest))\n setAutoFpMinConfidenceInput(String(settings.auto_fp_min_confidence ?? 0.9))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n auto_fp_on_retest: autoFpOnRetestInput,\n auto_fp_min_confidence: Math.min(1, Math.max(0, Number.parseFloat(autoFpMinConfidenceInput || '0.9') || 0.9)),\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n const successMessage = result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n setAISettingsMessage(successMessage)\n toast.success(successMessage)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n toast.error(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n if (result.status === 'ok') {\n toast.success(message)\n } else {\n toast.error(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n toast.error(`Probe failed: ${message}`)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n type=\"button\"\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n type=\"button\"\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <SectionCard\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <div className=\"inline-flex flex-wrap items-center gap-1 rounded border border-gray-700 bg-gray-950 p-1\">\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('hosted')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'hosted' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'hosted'}\n >\n Hosted Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('local')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'local' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'local'}\n >\n Local Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('custom')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'custom'}\n >\n Custom\n </button>\n </div>\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n aria-expanded={showDemoNetworking}\n className=\"rounded text-xs text-gray-400 hover:text-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </SectionCard>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"deepseek/deepseek-chat,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <ToggleRow\n label=\"Auto-close high-confidence false positives\"\n description=\"When on, a retest that concludes a high-confidence false positive flips the finding from active to false positive automatically (audited and reversible).\"\n hint=\"Off by default: a wrong auto-FP hides a real vulnerability. Leave off to keep a human in the loop via the one-click 'mark false positive' control.\"\n checked={autoFpOnRetestInput}\n onChange={setAutoFpOnRetestInput}\n />\n {autoFpOnRetestInput && (\n <Field\n label=\"Auto-FP minimum confidence\"\n hint=\"Retest confidence required before auto-closing (0–1). Higher than the 0.7 false-positive verdict bar because auto-closing is riskier than labeling.\"\n >\n <input\n type=\"number\"\n min={0.7}\n max={1}\n step={0.05}\n value={autoFpMinConfidenceInput}\n onChange={(e) => setAutoFpMinConfidenceInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n )}\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "af804bb35dd0146809875f2187bc75c474a3dfa2": {"t": 1785764335.599114, "n": 37703, "h": "90f9c32dd61ff1ef", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\nimport { useToast } from '@/components/ui'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n const raw = String(value || '').trim().replace(/\\/+$/, '')\n try {\n const url = new URL(raw)\n url.protocol = url.protocol.toLowerCase()\n url.hostname = url.hostname.toLowerCase()\n return url.toString().replace(/\\/+$/, '')\n } catch {\n return raw\n }\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const toast = useToast()\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [autoFpOnRetestInput, setAutoFpOnRetestInput] = useState(false)\n const [autoFpMinConfidenceInput, setAutoFpMinConfidenceInput] = useState('0.9')\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setAutoFpOnRetestInput(Boolean(settings.auto_fp_on_retest))\n setAutoFpMinConfidenceInput(String(settings.auto_fp_min_confidence ?? 0.9))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n auto_fp_on_retest: autoFpOnRetestInput,\n auto_fp_min_confidence: Math.min(1, Math.max(0, Number.parseFloat(autoFpMinConfidenceInput || '0.9') || 0.9)),\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n const successMessage = result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n setAISettingsMessage(successMessage)\n toast.success(successMessage)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n toast.error(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n if (result.status === 'ok') {\n toast.success(message)\n } else {\n toast.error(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n toast.error(`Probe failed: ${message}`)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n type=\"button\"\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n type=\"button\"\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <SectionCard\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <div className=\"inline-flex flex-wrap items-center gap-1 rounded border border-gray-700 bg-gray-950 p-1\">\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('hosted')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'hosted' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'hosted'}\n >\n Hosted Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('local')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'local' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'local'}\n >\n Local Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('custom')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'custom'}\n >\n Custom\n </button>\n </div>\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n aria-expanded={showDemoNetworking}\n className=\"rounded text-xs text-gray-400 hover:text-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </SectionCard>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <ToggleRow\n label=\"Auto-close high-confidence false positives\"\n description=\"When on, a retest that concludes a high-confidence false positive flips the finding from active to false positive automatically (audited and reversible).\"\n hint=\"Off by default: a wrong auto-FP hides a real vulnerability. Leave off to keep a human in the loop via the one-click 'mark false positive' control.\"\n checked={autoFpOnRetestInput}\n onChange={setAutoFpOnRetestInput}\n />\n {autoFpOnRetestInput && (\n <Field\n label=\"Auto-FP minimum confidence\"\n hint=\"Retest confidence required before auto-closing (0–1). Higher than the 0.7 false-positive verdict bar because auto-closing is riskier than labeling.\"\n >\n <input\n type=\"number\"\n min={0.7}\n max={1}\n step={0.05}\n value={autoFpMinConfidenceInput}\n onChange={(e) => setAutoFpMinConfidenceInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n )}\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "2c80dae222d017d1dee2b42c01e5af0aca303396": {"t": 1785764337.1636698, "n": 36073, "h": "787079bbe5aa9118", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\nimport { useToast } from '@/components/ui'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n const raw = String(value || '').trim().replace(/\\/+$/, '')\n try {\n const url = new URL(raw)\n url.protocol = url.protocol.toLowerCase()\n url.hostname = url.hostname.toLowerCase()\n return url.toString().replace(/\\/+$/, '')\n } catch {\n return raw\n }\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const toast = useToast()\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n const successMessage = result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n setAISettingsMessage(successMessage)\n toast.success(successMessage)\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n toast.error(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n if (result.status === 'ok') {\n toast.success(message)\n } else {\n toast.error(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n toast.error(`Probe failed: ${message}`)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n type=\"button\"\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n type=\"button\"\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <SectionCard\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <div className=\"inline-flex flex-wrap items-center gap-1 rounded border border-gray-700 bg-gray-950 p-1\">\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('hosted')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'hosted' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'hosted'}\n >\n Hosted Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('local')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'local' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'local'}\n >\n Local Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('custom')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'custom'}\n >\n Custom\n </button>\n </div>\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n aria-expanded={showDemoNetworking}\n className=\"rounded text-xs text-gray-400 hover:text-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </SectionCard>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n autoComplete=\"off\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "163add2ab9c4bb0c876ed6bb0c1a150a16488243": {"t": 1785764339.811915, "n": 34830, "h": "4de83acf1b68d946", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n const raw = String(value || '').trim().replace(/\\/+$/, '')\n try {\n const url = new URL(raw)\n url.protocol = url.protocol.toLowerCase()\n url.hostname = url.hostname.toLowerCase()\n return url.toString().replace(/\\/+$/, '')\n } catch {\n return raw\n }\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <SectionCard\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <div className=\"inline-flex flex-wrap items-center gap-1 rounded border border-gray-700 bg-gray-950 p-1\">\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('hosted')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'hosted' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'hosted'}\n >\n Hosted Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('local')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'local' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'local'}\n >\n Local Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('custom')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'custom'}\n >\n Custom\n </button>\n </div>\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n className=\"text-xs text-gray-400 hover:text-gray-200\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </SectionCard>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "317255ea9df895b91e574dc2ebbbb08d3dda9fe4": {"t": 1785764341.4540758, "n": 34619, "h": "c9ac6e4911b6cf36", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n return String(value || '').trim().replace(/\\/+$/, '')\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoModeEnabledInput ? demoHoneyPublicURLInput : '',\n demo_honey_scanner_url: demoModeEnabledInput ? demoHoneyScannerURLInput : '',\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n {(settingsMode === 'advanced' || demoModeEnabledInput) && (\n <SectionCard\n title=\"Calibration Lab\"\n description=\"Optional Honey regression runner for maintainers. Leave disabled for normal ShakerScan deployments.\"\n >\n <ToggleRow\n label=\"Enable Honey calibration controls\"\n description=\"Adds the hidden Honey runner to AI Gate and allows ShakerScan to queue calibration-only targets.\"\n hint=\"Calibration targets stay hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n {demoModeEnabledInput && (\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <div className=\"inline-flex flex-wrap items-center gap-1 rounded border border-gray-700 bg-gray-950 p-1\">\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('hosted')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'hosted' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'hosted'}\n >\n Hosted Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('local')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'local' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'local'}\n >\n Local Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('custom')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'custom'}\n >\n Custom\n </button>\n </div>\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n className=\"text-xs text-gray-400 hover:text-gray-200\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n )}\n </SectionCard>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "ee484a2e5adf6d847538e06601df15e8bf52c729": {"t": 1785764343.1906328, "n": 34096, "h": "d7c503be3aa1f5ad", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n return String(value || '').trim().replace(/\\/+$/, '')\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('custom')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('')\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || ''\n const scannerURL = settings.demo_honey_scanner_url || ''\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoHoneyPublicURLInput,\n demo_honey_scanner_url: demoHoneyScannerURLInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <SectionCard\n title=\"Demo Lab\"\n description=\"Optional Honey calibration runner for demos and regression checks. Keep off on production-facing deployments.\"\n >\n <ToggleRow\n label=\"Show Honey demo runner in AI Gate\"\n description=\"Adds a Run Demo button on the AI Gate page and allows ShakerScan to queue hidden Honey calibration targets.\"\n hint=\"Demo targets are hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <div className=\"inline-flex flex-wrap items-center gap-1 rounded border border-gray-700 bg-gray-950 p-1\">\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('hosted')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'hosted' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'hosted'}\n >\n Hosted Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('local')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'local' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'local'}\n >\n Local Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('custom')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'custom'}\n >\n Custom\n </button>\n </div>\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput || HONEY_LOCAL_SCANNER_URL}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n className=\"text-xs text-gray-400 hover:text-gray-200\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n </SectionCard>\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "eb8d991e6a1ad45a1d868eb60cdfff0da00b6b67": {"t": 1785764345.164024, "n": 34125, "h": "58fe10dc5feb2bef", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\nconst HONEY_HOSTED_URL = 'https://honey.shakerscan.com'\nconst HONEY_LOCAL_PUBLIC_URL = 'http://localhost:18080'\nconst HONEY_LOCAL_SCANNER_URL = 'http://host.docker.internal:18080'\n\ntype DemoHoneyMode = 'hosted' | 'local' | 'custom'\n\nfunction normalizeUrlValue(value?: string): string {\n return String(value || '').trim().replace(/\\/+$/, '')\n}\n\nfunction deriveDockerReachableUrl(value: string): string {\n const trimmed = normalizeUrlValue(value)\n try {\n const url = new URL(trimmed)\n if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {\n url.hostname = 'host.docker.internal'\n return normalizeUrlValue(url.toString())\n }\n } catch {\n return trimmed\n }\n return trimmed\n}\n\nfunction detectDemoHoneyMode(publicURL: string, scannerURL: string): DemoHoneyMode {\n const publicValue = normalizeUrlValue(publicURL)\n const scannerValue = normalizeUrlValue(scannerURL)\n if (publicValue === HONEY_HOSTED_URL && scannerValue === HONEY_HOSTED_URL) return 'hosted'\n if (publicValue === HONEY_LOCAL_PUBLIC_URL && scannerValue === HONEY_LOCAL_SCANNER_URL) return 'local'\n return 'custom'\n}\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyModeInput, setDemoHoneyModeInput] = useState<DemoHoneyMode>('hosted')\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState(HONEY_HOSTED_URL)\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState(HONEY_HOSTED_URL)\n const [showDemoNetworking, setShowDemoNetworking] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n const publicURL = settings.demo_honey_public_url || HONEY_HOSTED_URL\n const scannerURL = settings.demo_honey_scanner_url || HONEY_HOSTED_URL\n setDemoHoneyPublicURLInput(publicURL)\n setDemoHoneyScannerURLInput(scannerURL)\n setDemoHoneyModeInput(detectDemoHoneyMode(publicURL, scannerURL))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoHoneyPublicURLInput,\n demo_honey_scanner_url: demoHoneyScannerURLInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n const selectDemoHoneyMode = (mode: DemoHoneyMode) => {\n setDemoHoneyModeInput(mode)\n if (mode === 'hosted') {\n setDemoHoneyPublicURLInput(HONEY_HOSTED_URL)\n setDemoHoneyScannerURLInput(HONEY_HOSTED_URL)\n setShowDemoNetworking(false)\n } else if (mode === 'local') {\n setDemoHoneyPublicURLInput(HONEY_LOCAL_PUBLIC_URL)\n setDemoHoneyScannerURLInput(HONEY_LOCAL_SCANNER_URL)\n } else {\n setShowDemoNetworking(true)\n }\n }\n\n const updateDemoPublicURL = (value: string) => {\n setDemoHoneyPublicURLInput(value)\n if (demoHoneyModeInput === 'local') {\n setDemoHoneyScannerURLInput(deriveDockerReachableUrl(value))\n } else {\n setDemoHoneyModeInput('custom')\n setShowDemoNetworking(true)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <SectionCard\n title=\"Demo Lab\"\n description=\"Optional Honey calibration runner for demos and regression checks. Keep off on production-facing deployments.\"\n >\n <ToggleRow\n label=\"Show Honey demo runner in AI Gate\"\n description=\"Adds a Run Demo button on the AI Gate page and allows ShakerScan to queue hidden Honey calibration targets.\"\n hint=\"Demo targets are hidden from the normal AI target list unless Show demo targets is enabled.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n\n <div className=\"space-y-3 rounded border border-gray-800 bg-gray-900/50 p-3\">\n <div>\n <p className=\"text-xs font-medium text-gray-300\">Honey source</p>\n <p className=\"text-[11px] text-gray-500 mt-0.5\">\n Hosted uses the public Honey service. Local uses your dev Honey on port 18080 and rewrites scanner traffic for Docker.\n </p>\n </div>\n <div className=\"inline-flex flex-wrap items-center gap-1 rounded border border-gray-700 bg-gray-950 p-1\">\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('hosted')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'hosted' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'hosted'}\n >\n Hosted Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('local')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'local' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'local'}\n >\n Local Honey\n </button>\n <button\n type=\"button\"\n onClick={() => selectDemoHoneyMode('custom')}\n className={`px-2.5 py-1 text-xs rounded ${\n demoHoneyModeInput === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-800'\n }`}\n aria-pressed={demoHoneyModeInput === 'custom'}\n >\n Custom\n </button>\n </div>\n\n <Field\n label={demoHoneyModeInput === 'local' ? 'Local Honey URL' : 'Honey URL'}\n hint=\"Browser-facing URL used for labels and links.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => updateDemoPublicURL(e.target.value)}\n className={INPUT_CLASS}\n placeholder={demoHoneyModeInput === 'local' ? HONEY_LOCAL_PUBLIC_URL : HONEY_HOSTED_URL}\n />\n </Field>\n\n {demoHoneyModeInput === 'local' && (\n <p className=\"text-[11px] text-gray-500\">\n Docker scanner traffic will use <span className=\"font-mono text-gray-400\">{demoHoneyScannerURLInput}</span>.\n </p>\n )}\n\n <button\n type=\"button\"\n onClick={() => setShowDemoNetworking(!showDemoNetworking)}\n className=\"text-xs text-gray-400 hover:text-gray-200\"\n >\n {showDemoNetworking ? 'Hide networking details' : 'Show networking details'}\n </button>\n\n {showDemoNetworking && (\n <Field\n label=\"Scanner-reachable Honey URL\"\n hint=\"URL that API and worker containers can reach. Local Docker scans usually need host.docker.internal.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => {\n setDemoHoneyScannerURLInput(e.target.value)\n setDemoHoneyModeInput(detectDemoHoneyMode(demoHoneyPublicURLInput, e.target.value))\n }}\n className={INPUT_CLASS}\n placeholder={HONEY_LOCAL_SCANNER_URL}\n />\n </Field>\n )}\n </div>\n </SectionCard>\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "ff5cd366b7d37573574694cbc21ad167d33ffa10": {"t": 1785764346.9194303, "n": 29573, "h": "67c8cc6c51846a09", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [demoModeEnabledInput, setDemoModeEnabledInput] = useState(false)\n const [demoHoneyPublicURLInput, setDemoHoneyPublicURLInput] = useState('https://honey.shakerscan.com')\n const [demoHoneyScannerURLInput, setDemoHoneyScannerURLInput] = useState('https://honey.shakerscan.com')\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setDemoModeEnabledInput(Boolean(settings.demo_mode_enabled))\n setDemoHoneyPublicURLInput(settings.demo_honey_public_url || 'https://honey.shakerscan.com')\n setDemoHoneyScannerURLInput(settings.demo_honey_scanner_url || 'https://honey.shakerscan.com')\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n demo_mode_enabled: demoModeEnabledInput,\n demo_honey_public_url: demoHoneyPublicURLInput,\n demo_honey_scanner_url: demoHoneyScannerURLInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Demo mode: {aiSettings.demo_mode_enabled ? 'enabled' : 'disabled'}\n </div>\n </div>\n )}\n\n <SectionCard\n title=\"Demo & Calibration\"\n description=\"Controls whether Honey demo actions appear in AI Gate. Keep off for production-facing use.\"\n >\n <ToggleRow\n label=\"Enable Honey AI demo controls\"\n description=\"Shows a demo run button on the AI Gate page and allows the demo API to queue hidden Honey targets.\"\n hint=\"Demo targets are excluded from the normal AI target list by default.\"\n checked={demoModeEnabledInput}\n onChange={setDemoModeEnabledInput}\n />\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field\n label=\"Honey public URL\"\n hint=\"Browser-facing Honey URL shown in links and labels.\"\n >\n <input\n type=\"url\"\n value={demoHoneyPublicURLInput}\n onChange={(e) => setDemoHoneyPublicURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://honey.shakerscan.com\"\n />\n </Field>\n <Field\n label=\"Honey scanner URL\"\n hint=\"URL that API/workers can reach. For local Honey from Docker, use http://host.docker.internal:18080.\"\n >\n <input\n type=\"url\"\n value={demoHoneyScannerURLInput}\n onChange={(e) => setDemoHoneyScannerURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"http://host.docker.internal:18080\"\n />\n </Field>\n </div>\n </SectionCard>\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "a5d692b27f84d8687e18b7d3bb83e995171d1f87": {"t": 1785764349.2224607, "n": 27112, "h": "4a60a99f3519fab6", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setScanAPIKeyInput('')\n setClearScanAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: aiURLInput || undefined,\n ai_api_key: scanAPIKeyInput.trim() || undefined,\n ai_model: aiModelInput || undefined,\n ai_fallback_model: aiModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n AI retest: {aiSettings.ai_verify_enabled ? 'enabled' : 'disabled'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <p className=\"text-xs text-gray-500\">\n Retest AI uses the shared provider settings from <span className=\"text-gray-300\">Shared Provider</span>.\n </p>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "fb28e0577c52228f1ed9a9ad550f9f26d620e9ac": {"t": 1785764351.5190024, "n": 30149, "h": "d44895a0489ad964", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [verifyAPIKeyInput, setVerifyAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [clearVerifyAPIKey, setClearVerifyAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyURLInput, setAIVerifyURLInput] = useState('')\n const [aiVerifyModelInput, setAIVerifyModelInput] = useState('')\n const [aiVerifyModelFallbackInput, setAIVerifyModelFallbackInput] = useState('')\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [settingsMode, setSettingsMode] = useState<'basic' | 'advanced'>('basic')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyURLInput(settings.ai_verify_url || '')\n setAIVerifyModelInput(settings.ai_verify_model || '')\n setAIVerifyModelFallbackInput(settings.ai_verify_model_fallback || '')\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setScanAPIKeyInput('')\n setVerifyAPIKeyInput('')\n setClearScanAPIKey(false)\n setClearVerifyAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_url: aiVerifyURLInput,\n ai_verify_model: aiVerifyModelInput,\n ai_verify_model_fallback: aiVerifyModelFallbackInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n if (clearVerifyAPIKey) {\n payload.ai_verify_api_key = ''\n } else if (verifyAPIKeyInput.trim()) {\n payload.ai_verify_api_key = verifyAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: scope === 'scan' ? aiURLInput || undefined : aiVerifyURLInput || undefined,\n ai_api_key: scope === 'scan' ? scanAPIKeyInput.trim() || undefined : verifyAPIKeyInput.trim() || undefined,\n ai_model: scope === 'scan' ? aiModelInput || undefined : aiVerifyModelInput || undefined,\n ai_fallback_model:\n scope === 'scan'\n ? aiModelFallbackInput || undefined\n : aiVerifyModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n const setUnifiedVerificationSeverity = (severity: Severity) => {\n setAIVerifyMinSeverityInput(severity)\n setVerificationMinSeverityInput(severity)\n setAIEscalationMinSeverityInput(severity)\n setAutoRetestMinSeverityInput(severity)\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Retest key: {aiSettings.ai_verify_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"inline-flex items-center gap-1 rounded-lg border border-gray-700 bg-gray-800 p-1\">\n <button\n onClick={() => setSettingsMode('basic')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'basic' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'basic'}\n >\n Basic\n </button>\n <button\n onClick={() => setSettingsMode('advanced')}\n className={`px-2.5 py-1 text-xs rounded ${\n settingsMode === 'advanced' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'\n }`}\n aria-pressed={settingsMode === 'advanced'}\n >\n Advanced\n </button>\n </div>\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n {settingsMode === 'basic' ? (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Quick Setup\"\n description=\"Recommended defaults for most teams: configure one key, enable retest AI, set severity threshold.\"\n >\n <Field\n label=\"AI API Key\"\n hint=\"Primary key used for AI provider access. Advanced mode supports separate retest key/provider.\"\n >\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Uses AI only when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic proof checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"Minimum Severity for AI Verification\"\n hint=\"Findings below this severity are not escalated to AI retest.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setUnifiedVerificationSeverity(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Need provider/model overrides, proof-gated smart mode, scan-time AI classification, or auto-retest tuning?\n Switch to <span className=\"text-gray-300\">Advanced</span>.\n </p>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test AI Verification'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n </div>\n ) : (\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"AI Verify URL\"\n hint=\"Dedicated endpoint for retest AI. Leave empty to use shared AI URL.\"\n >\n <input\n type=\"text\"\n value={aiVerifyURLInput}\n onChange={(e) => setAIVerifyURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"Leave empty to use shared AI URL\"\n />\n </Field>\n <Field label=\"AI Verify Model\" hint=\"Primary model for retest AI verification.\">\n <input\n type=\"text\"\n value={aiVerifyModelInput}\n onChange={(e) => setAIVerifyModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5\"\n />\n </Field>\n <Field\n label=\"Verify Fallback Models (comma-separated)\"\n hint=\"Fallback model list for retest AI verification.\"\n >\n <input\n type=\"text\"\n value={aiVerifyModelFallbackInput}\n onChange={(e) => setAIVerifyModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"openai/gpt-4o-mini,anthropic/claude-3-5-sonnet\"\n />\n </Field>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field label=\"AI Verify API Key\" hint=\"Optional dedicated key for retest AI. Falls back to shared key.\">\n <input\n type=\"password\"\n value={verifyAPIKeyInput}\n onChange={(e) => setVerifyAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_verify_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear retest AI API key\"\n description=\"Removes the stored retest key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearVerifyAPIKey}\n onChange={setClearVerifyAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n )}\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "90d4f0eb7a071b890818abaa6990691dea0d8b2f": {"t": 1785764354.0799954, "n": 26184, "h": "b7cfe20f8a2b665c", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [verifyAPIKeyInput, setVerifyAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [clearVerifyAPIKey, setClearVerifyAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyURLInput, setAIVerifyURLInput] = useState('')\n const [aiVerifyModelInput, setAIVerifyModelInput] = useState('')\n const [aiVerifyModelFallbackInput, setAIVerifyModelFallbackInput] = useState('')\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(false)\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyURLInput(settings.ai_verify_url || '')\n setAIVerifyModelInput(settings.ai_verify_model || '')\n setAIVerifyModelFallbackInput(settings.ai_verify_model_fallback || '')\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(Boolean(settings.proof_required_for_smart))\n setScanAPIKeyInput('')\n setVerifyAPIKeyInput('')\n setClearScanAPIKey(false)\n setClearVerifyAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_url: aiVerifyURLInput,\n ai_verify_model: aiVerifyModelInput,\n ai_verify_model_fallback: aiVerifyModelFallbackInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n if (clearVerifyAPIKey) {\n payload.ai_verify_api_key = ''\n } else if (verifyAPIKeyInput.trim()) {\n payload.ai_verify_api_key = verifyAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: scope === 'scan' ? aiURLInput || undefined : aiVerifyURLInput || undefined,\n ai_api_key: scope === 'scan' ? scanAPIKeyInput.trim() || undefined : verifyAPIKeyInput.trim() || undefined,\n ai_model: scope === 'scan' ? aiModelInput || undefined : aiVerifyModelInput || undefined,\n ai_fallback_model:\n scope === 'scan'\n ? aiModelFallbackInput || undefined\n : aiVerifyModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan-time triage, authoritative retest verification, and smart-scan reporting behavior.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Retest key: {aiSettings.ai_verify_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Smart proof filter: {aiSettings.proof_required_for_smart ? 'on' : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"rounded border border-blue-500/30 bg-blue-500/10 px-3 py-2\">\n <p className=\"text-xs font-medium text-blue-200\">Pipeline flow</p>\n <p className=\"text-xs text-blue-100/90 mt-1\">\n Scanner tools run first. Optional scan-time AI helps with triage. Retests run deterministic proofs first, then\n optional AI escalation for hard cases.\n </p>\n </div>\n\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field\n label=\"AI URL\"\n hint=\"OpenAI-compatible endpoint used for scan-time AI, and as fallback for retest AI if verify URL is empty.\"\n >\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\" hint=\"Primary model for scan-time AI calls.\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Fallback Models (comma-separated)\"\n hint=\"Tried in order when the primary model fails or returns incompatible output.\"\n >\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field\n label=\"Mask Host\"\n hint=\"Host used to redact sensitive domains/URLs in prompts sent to AI providers.\"\n >\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\" hint=\"Shared provider key used unless a dedicated retest key is configured.\">\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear shared provider API key\"\n description=\"Removes the currently stored shared key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearScanAPIKey}\n onChange={setClearScanAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during report generation. This does not replace retest verification.\"\n >\n <ToggleRow\n label=\"Enable scan-time AI classification\"\n description=\"Uses AI during scan result processing to help classify findings.\"\n hint=\"When off, scan findings rely only on scanner/tool logic.\"\n checked={aiScanClassificationEnabledInput}\n onChange={setAIScanClassificationEnabledInput}\n />\n <Field\n label=\"Scan Classification Min Severity\"\n hint=\"Only findings at or above this severity are sent to scan-time AI classification.\"\n >\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"AI settings used for post-scan retest verification and exploit validation.\"\n >\n <ToggleRow\n label=\"Enable AI verification for retests\"\n description=\"Allows AI escalation when deterministic retest cannot confidently conclude.\"\n hint=\"Deterministic checks still run first.\"\n checked={aiVerifyEnabledInput}\n onChange={setAIVerifyEnabledInput}\n />\n <Field\n label=\"AI Verify URL\"\n hint=\"Dedicated endpoint for retest AI. Leave empty to use shared AI URL.\"\n >\n <input\n type=\"text\"\n value={aiVerifyURLInput}\n onChange={(e) => setAIVerifyURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"Leave empty to use shared AI URL\"\n />\n </Field>\n <Field label=\"AI Verify Model\" hint=\"Primary model for retest AI verification.\">\n <input\n type=\"text\"\n value={aiVerifyModelInput}\n onChange={(e) => setAIVerifyModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5\"\n />\n </Field>\n <Field\n label=\"Verify Fallback Models (comma-separated)\"\n hint=\"Fallback model list for retest AI verification.\"\n >\n <input\n type=\"text\"\n value={aiVerifyModelFallbackInput}\n onChange={(e) => setAIVerifyModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"openai/gpt-4o-mini,anthropic/claude-3-5-sonnet\"\n />\n </Field>\n <Field\n label=\"Verification Min Severity\"\n hint=\"Only retest findings at or above this severity can be escalated to AI.\"\n >\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field label=\"AI Verify API Key\" hint=\"Optional dedicated key for retest AI. Falls back to shared key.\">\n <input\n type=\"password\"\n value={verifyAPIKeyInput}\n onChange={(e) => setVerifyAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_verify_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <ToggleRow\n label=\"Clear retest AI API key\"\n description=\"Removes the stored retest key when you click Save.\"\n hint=\"Only applies on save.\"\n checked={clearVerifyAPIKey}\n onChange={setClearVerifyAPIKey}\n />\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gating and escalation thresholds used by smart scan and retest workflows.\"\n >\n <ToggleRow\n label=\"Proof required for smart scan reports\"\n description=\"When on, only findings with verification evidence are kept in primary smart-scan output.\"\n hint=\"Can reduce visible findings and attack chains when proof collection fails.\"\n checked={proofRequiredForSmartInput}\n onChange={setProofRequiredForSmartInput}\n />\n <Field\n label=\"Verification Min Severity (scan-time + retest)\"\n hint=\"Baseline minimum severity for verification workflows across scan-time checks and retests.\"\n >\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"AI Escalation Min Severity\"\n hint=\"Minimum severity required before deterministic retest can escalate to AI.\"\n >\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans can look quieter because unverified findings are filtered out of\n the primary report.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Queues retests automatically after scan completion.\"\n >\n <ToggleRow\n label=\"Auto-queue retests after each scan\"\n description=\"Creates retest jobs for eligible findings when a scan completes.\"\n hint=\"Useful for continuous validation with less manual work.\"\n checked={autoRetestEnabledInput}\n onChange={setAutoRetestEnabledInput}\n />\n <Field\n label=\"Auto Retest Min Severity\"\n hint=\"Only findings at or above this severity are auto-queued for retest.\"\n >\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field\n label=\"Auto Retest Max Findings Per Scan\"\n hint=\"Hard cap on number of findings auto-queued for retest from one scan.\"\n >\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n\n <div className=\"pt-2 border-t border-gray-800\">\n <ToggleRow\n label=\"Persist changes to local .env\"\n description=\"When on, saved settings survive container restarts. When off, changes are runtime-only.\"\n hint=\"Use runtime-only for temporary experiments.\"\n checked={persistAIToEnv}\n onChange={setPersistAIToEnv}\n />\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n hint,\n children,\n}: {\n label: string\n hint?: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n {children}\n </label>\n )\n}\n\nfunction ToggleRow({\n label,\n description,\n hint,\n checked,\n onChange,\n}: {\n label: string\n description: string\n hint?: string\n checked: boolean\n onChange: (checked: boolean) => void\n}) {\n return (\n <label className=\"flex items-start gap-2 rounded border border-gray-800 bg-gray-900/50 px-2.5 py-2\">\n <input\n type=\"checkbox\"\n checked={checked}\n onChange={(e) => onChange(e.target.checked)}\n className={`${CHECKBOX_CLASS} mt-0.5`}\n />\n <span className=\"space-y-0.5\">\n <span className=\"text-xs text-gray-200 inline-flex items-center gap-1\">\n {label}\n {hint && <HelpHint text={hint} />}\n </span>\n <span className=\"block text-[11px] text-gray-500\">{description}</span>\n </span>\n </label>\n )\n}\n\nfunction HelpHint({ text }: { text: string }) {\n return (\n <span\n className=\"inline-flex h-4 w-4 items-center justify-center rounded-full border border-gray-600 text-[10px] font-semibold text-gray-300 cursor-help\"\n title={text}\n aria-label={text}\n >\n ?\n </span>\n )\n}\n"}, "dc8472508a244c2510b45970aad68ccd714f7b6c": {"t": 1785764355.59712, "n": 23067, "h": "926315ae578c7581", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nconst SEVERITY_OPTIONS: Severity[] = ['critical', 'high', 'medium', 'low', 'info']\nconst INPUT_CLASS =\n 'mt-1 w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500'\nconst CHECKBOX_CLASS =\n 'h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [verifyAPIKeyInput, setVerifyAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [clearVerifyAPIKey, setClearVerifyAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyURLInput, setAIVerifyURLInput] = useState('')\n const [aiVerifyModelInput, setAIVerifyModelInput] = useState('')\n const [aiVerifyModelFallbackInput, setAIVerifyModelFallbackInput] = useState('')\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(true)\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyURLInput(settings.ai_verify_url || '')\n setAIVerifyModelInput(settings.ai_verify_model || '')\n setAIVerifyModelFallbackInput(settings.ai_verify_model_fallback || '')\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(settings.proof_required_for_smart !== false)\n setScanAPIKeyInput('')\n setVerifyAPIKeyInput('')\n setClearScanAPIKey(false)\n setClearVerifyAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_url: aiVerifyURLInput,\n ai_verify_model: aiVerifyModelInput,\n ai_verify_model_fallback: aiVerifyModelFallbackInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n if (clearVerifyAPIKey) {\n payload.ai_verify_api_key = ''\n } else if (verifyAPIKeyInput.trim()) {\n payload.ai_verify_api_key = verifyAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n\n try {\n const result = await testAISettings({\n scope,\n ai_url: scope === 'scan' ? aiURLInput || undefined : aiVerifyURLInput || undefined,\n ai_api_key: scope === 'scan' ? scanAPIKeyInput.trim() || undefined : verifyAPIKeyInput.trim() || undefined,\n ai_model: scope === 'scan' ? aiModelInput || undefined : aiVerifyModelInput || undefined,\n ai_fallback_model:\n scope === 'scan'\n ? aiModelFallbackInput || undefined\n : aiVerifyModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message =\n result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n if (loading && !aiSettings) {\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 text-sm text-gray-400\">\n Loading AI settings...\n </div>\n )\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-200\">AI & Verification Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Retest verification is authoritative. Scan-time AI classification is optional triage assistance.\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save'}\n </button>\n </div>\n </div>\n\n {aiSettings && (\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-2 text-xs\">\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Retest key: {aiSettings.ai_verify_api_key_configured ? 'configured' : 'not set'}\n </div>\n <div className=\"bg-gray-800/70 border border-gray-700 rounded px-2 py-1.5 text-gray-300\">\n Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `on (${aiSettings.ai_classify_min_severity}+)` : 'off'}\n </div>\n </div>\n )}\n\n <div className=\"grid grid-cols-1 xl:grid-cols-2 gap-4\">\n <SectionCard\n title=\"Shared Provider\"\n description=\"Default provider used for scan-time classification and as fallback for retest verification.\"\n >\n <Field label=\"AI URL\">\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </Field>\n <Field label=\"AI Model\">\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"gpt-4o-mini\"\n />\n </Field>\n <Field label=\"Fallback Models (comma-separated)\">\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </Field>\n <Field label=\"Mask Host\">\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"example.com\"\n />\n </Field>\n <Field label=\"AI API Key\">\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearScanAPIKey}\n onChange={(e) => setClearScanAPIKey(e.target.checked)}\n className={CHECKBOX_CLASS}\n />\n Clear shared provider API key\n </label>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Scan-Time Classification\"\n description=\"Optional AI triage during scan reporting. Does not replace retest verification.\"\n >\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiScanClassificationEnabledInput}\n onChange={(e) => setAIScanClassificationEnabledInput(e.target.checked)}\n className={CHECKBOX_CLASS}\n />\n Enable scan-time AI classification\n </label>\n <Field label=\"Scan Classification Min Severity\">\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className={`${INPUT_CLASS} disabled:opacity-50`}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-gray-500\">\n Recommendation: keep this at <code>medium</code> or higher for lower noise.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Retest Verification (Authoritative)\"\n description=\"Used for retest AI decisions and exploit validation workflow.\"\n >\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiVerifyEnabledInput}\n onChange={(e) => setAIVerifyEnabledInput(e.target.checked)}\n className={CHECKBOX_CLASS}\n />\n Enable AI verification for retests\n </label>\n <Field label=\"AI Verify URL\">\n <input\n type=\"text\"\n value={aiVerifyURLInput}\n onChange={(e) => setAIVerifyURLInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"Leave empty to use shared AI URL\"\n />\n </Field>\n <Field label=\"AI Verify Model\">\n <input\n type=\"text\"\n value={aiVerifyModelInput}\n onChange={(e) => setAIVerifyModelInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"moonshotai/kimi-k2.5\"\n />\n </Field>\n <Field label=\"Verify Fallback Models (comma-separated)\">\n <input\n type=\"text\"\n value={aiVerifyModelFallbackInput}\n onChange={(e) => setAIVerifyModelFallbackInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder=\"openai/gpt-4o-mini,anthropic/claude-3-5-sonnet\"\n />\n </Field>\n <Field label=\"Verification Min Severity\">\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field label=\"AI Verify API Key\">\n <input\n type=\"password\"\n value={verifyAPIKeyInput}\n onChange={(e) => setVerifyAPIKeyInput(e.target.value)}\n className={INPUT_CLASS}\n placeholder={aiSettings?.ai_verify_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </Field>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearVerifyAPIKey}\n onChange={(e) => setClearVerifyAPIKey(e.target.checked)}\n className={CHECKBOX_CLASS}\n />\n Clear retest AI API key\n </label>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </SectionCard>\n\n <SectionCard\n title=\"Verification Policy\"\n description=\"Controls proof-gated reporting and how strict chain-visible findings are.\"\n >\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={proofRequiredForSmartInput}\n onChange={(e) => setProofRequiredForSmartInput(e.target.checked)}\n className={CHECKBOX_CLASS}\n />\n Proof required for smart scan reports (no exploit = no report)\n </label>\n <Field label=\"Verification Min Severity (scan-time + retest)\">\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field label=\"AI Escalation Min Severity\">\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <p className=\"text-xs text-yellow-400/90 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1.5\">\n If proof-required is enabled, smart scans may show fewer findings and fewer attack chains unless exploit\n evidence is collected.\n </p>\n </SectionCard>\n\n <SectionCard\n title=\"Auto Retest Policy\"\n description=\"Automatically queue retests after scan completion.\"\n >\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={autoRetestEnabledInput}\n onChange={(e) => setAutoRetestEnabledInput(e.target.checked)}\n className={CHECKBOX_CLASS}\n />\n Auto-queue retests after each scan\n </label>\n <Field label=\"Auto Retest Min Severity\">\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className={INPUT_CLASS}\n >\n {SEVERITY_OPTIONS.map((sev) => (\n <option key={sev} value={sev}>\n {sev}\n </option>\n ))}\n </select>\n </Field>\n <Field label=\"Auto Retest Max Findings Per Scan\">\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className={INPUT_CLASS}\n />\n </Field>\n </SectionCard>\n </div>\n\n <div className=\"pt-2 border-t border-gray-800\">\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={persistAIToEnv}\n onChange={(e) => setPersistAIToEnv(e.target.checked)}\n className={CHECKBOX_CLASS}\n />\n Persist changes to local <code>.env</code>\n </label>\n </div>\n\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n\nfunction SectionCard({\n title,\n description,\n children,\n}: {\n title: string\n description: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"bg-gray-950/60 border border-gray-800 rounded-lg p-3 space-y-2\">\n <div>\n <h3 className=\"text-xs font-medium text-gray-200 uppercase tracking-wide\">{title}</h3>\n <p className=\"text-xs text-gray-500 mt-1\">{description}</p>\n </div>\n {children}\n </section>\n )\n}\n\nfunction Field({\n label,\n children,\n}: {\n label: string\n children: React.ReactNode\n}) {\n return (\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">{label}</span>\n {children}\n </label>\n )\n}\n"}, "5aa488830b1c845080554d1b7868f16898a0b2b0": {"t": 1785764357.1959023, "n": 24762, "h": "c414de7f51aa8bdd", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [verifyAPIKeyInput, setVerifyAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [clearVerifyAPIKey, setClearVerifyAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyURLInput, setAIVerifyURLInput] = useState('')\n const [aiVerifyModelInput, setAIVerifyModelInput] = useState('')\n const [aiVerifyModelFallbackInput, setAIVerifyModelFallbackInput] = useState('')\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(true)\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyURLInput(settings.ai_verify_url || '')\n setAIVerifyModelInput(settings.ai_verify_model || '')\n setAIVerifyModelFallbackInput(settings.ai_verify_model_fallback || '')\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput(settings.verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput(settings.ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput(settings.proof_required_for_smart !== false)\n setScanAPIKeyInput('')\n setVerifyAPIKeyInput('')\n setClearScanAPIKey(false)\n setClearVerifyAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_url: aiVerifyURLInput,\n ai_verify_model: aiVerifyModelInput,\n ai_verify_model_fallback: aiVerifyModelFallbackInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n if (clearVerifyAPIKey) {\n payload.ai_verify_api_key = ''\n } else if (verifyAPIKeyInput.trim()) {\n payload.ai_verify_api_key = verifyAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n try {\n const result = await testAISettings({\n scope,\n ai_url: scope === 'scan' ? aiURLInput || undefined : aiVerifyURLInput || undefined,\n ai_api_key: scope === 'scan' ? scanAPIKeyInput.trim() || undefined : verifyAPIKeyInput.trim() || undefined,\n ai_model: scope === 'scan' ? aiModelInput || undefined : aiVerifyModelInput || undefined,\n ai_fallback_model:\n scope === 'scan'\n ? aiModelFallbackInput || undefined\n : aiVerifyModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message = result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-400\">AI Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Retest AI verification is authoritative. Scan-time AI classification is optional pre-triage.\n Changes apply to new jobs immediately.\n </p>\n </div>\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Shared Provider + Optional Scan Classification</h3>\n <p className=\"text-xs text-gray-500\">\n Used as the shared provider defaults. Scan-time AI classification runs only when enabled below.\n </p>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI URL</span>\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Model</span>\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Mask Host</span>\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"example.com\"\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiScanClassificationEnabledInput}\n onChange={(e) => setAIScanClassificationEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Enable scan-time AI classification\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Scan Classification Min Severity</span>\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500 disabled:opacity-50\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI API Key</span>\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearScanAPIKey}\n onChange={(e) => setClearScanAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear shared provider API key\n </label>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing shared provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Retest AI Verification (Authoritative)</h3>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiVerifyEnabledInput}\n onChange={(e) => setAIVerifyEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Enable AI verification for retests\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify URL</span>\n <input\n type=\"text\"\n value={aiVerifyURLInput}\n onChange={(e) => setAIVerifyURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"Leave empty to fall back to shared AI URL\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify Model</span>\n <input\n type=\"text\"\n value={aiVerifyModelInput}\n onChange={(e) => setAIVerifyModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"claude-sonnet-4-5-20250929\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verify Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiVerifyModelFallbackInput}\n onChange={(e) => setAIVerifyModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"openai/gpt-4o-mini,anthropic/claude-3-5-sonnet\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verification Min Severity</span>\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify API Key</span>\n <input\n type=\"password\"\n value={verifyAPIKeyInput}\n onChange={(e) => setVerifyAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_verify_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearVerifyAPIKey}\n onChange={(e) => setClearVerifyAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear retest AI API key\n </label>\n <div className=\"pt-2 border-t border-gray-800 mt-2 space-y-2\">\n <h4 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Auto Retest Policy</h4>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={autoRetestEnabledInput}\n onChange={(e) => setAutoRetestEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Auto-queue retests after each scan\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Auto Retest Min Severity</span>\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Auto Retest Max Findings Per Scan</span>\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n />\n </label>\n </div>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n <div className=\"pt-2 border-t border-gray-800 mt-2 space-y-2\">\n <h4 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Verification Policy</h4>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={proofRequiredForSmartInput}\n onChange={(e) => setProofRequiredForSmartInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Proof required for smart scan reports (no exploit = no report)\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verification Min Severity (scan-time + retest)</span>\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Escalation Min Severity</span>\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n </div>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={persistAIToEnv}\n onChange={(e) => setPersistAIToEnv(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Persist changes to local <code>.env</code>\n </label>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save AI Settings'}\n </button>\n </div>\n\n {aiSettings && (\n <p className=\"text-xs text-gray-500\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'} · Retest key:{' '}\n {aiSettings.ai_verify_api_key_configured ? 'configured' : 'not set'} · Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `enabled (${aiSettings.ai_classify_min_severity}+)` : 'disabled'}\n </p>\n )}\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n"}, "4d20dab13f3fe174473880fde9b5a1e1db6881ce": {"t": 1785764358.6754692, "n": 24879, "h": "91077273da9f5d8b", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [verifyAPIKeyInput, setVerifyAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [clearVerifyAPIKey, setClearVerifyAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyURLInput, setAIVerifyURLInput] = useState('')\n const [aiVerifyModelInput, setAIVerifyModelInput] = useState('')\n const [aiVerifyModelFallbackInput, setAIVerifyModelFallbackInput] = useState('')\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [verificationMinSeverityInput, setVerificationMinSeverityInput] = useState<Severity>('medium')\n const [aiEscalationMinSeverityInput, setAIEscalationMinSeverityInput] = useState<Severity>('high')\n const [proofRequiredForSmartInput, setProofRequiredForSmartInput] = useState(true)\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyURLInput(settings.ai_verify_url || '')\n setAIVerifyModelInput(settings.ai_verify_model || '')\n setAIVerifyModelFallbackInput(settings.ai_verify_model_fallback || '')\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setVerificationMinSeverityInput((settings as Record<string, any>).verification_min_severity || settings.auto_retest_min_severity || 'medium')\n setAIEscalationMinSeverityInput((settings as Record<string, any>).ai_escalation_min_severity || settings.ai_verify_min_severity || 'high')\n setProofRequiredForSmartInput((settings as Record<string, any>).proof_required_for_smart !== false)\n setScanAPIKeyInput('')\n setVerifyAPIKeyInput('')\n setClearScanAPIKey(false)\n setClearVerifyAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_url: aiVerifyURLInput,\n ai_verify_model: aiVerifyModelInput,\n ai_verify_model_fallback: aiVerifyModelFallbackInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n verification_min_severity: verificationMinSeverityInput,\n ai_escalation_min_severity: aiEscalationMinSeverityInput,\n proof_required_for_smart: proofRequiredForSmartInput,\n persist_to_env: persistAIToEnv,\n } as AISettingsUpdate & Record<string, any>\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n if (clearVerifyAPIKey) {\n payload.ai_verify_api_key = ''\n } else if (verifyAPIKeyInput.trim()) {\n payload.ai_verify_api_key = verifyAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n try {\n const result = await testAISettings({\n scope,\n ai_url: scope === 'scan' ? aiURLInput || undefined : aiVerifyURLInput || undefined,\n ai_api_key: scope === 'scan' ? scanAPIKeyInput.trim() || undefined : verifyAPIKeyInput.trim() || undefined,\n ai_model: scope === 'scan' ? aiModelInput || undefined : aiVerifyModelInput || undefined,\n ai_fallback_model:\n scope === 'scan'\n ? aiModelFallbackInput || undefined\n : aiVerifyModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message = result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-400\">AI Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Retest AI verification is authoritative. Scan-time AI classification is optional pre-triage.\n Changes apply to new jobs immediately.\n </p>\n </div>\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Shared Provider + Optional Scan Classification</h3>\n <p className=\"text-xs text-gray-500\">\n Used as the shared provider defaults. Scan-time AI classification runs only when enabled below.\n </p>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI URL</span>\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Model</span>\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Mask Host</span>\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"example.com\"\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiScanClassificationEnabledInput}\n onChange={(e) => setAIScanClassificationEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Enable scan-time AI classification\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Scan Classification Min Severity</span>\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500 disabled:opacity-50\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI API Key</span>\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearScanAPIKey}\n onChange={(e) => setClearScanAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear shared provider API key\n </label>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing shared provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Retest AI Verification (Authoritative)</h3>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiVerifyEnabledInput}\n onChange={(e) => setAIVerifyEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Enable AI verification for retests\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify URL</span>\n <input\n type=\"text\"\n value={aiVerifyURLInput}\n onChange={(e) => setAIVerifyURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"Leave empty to fall back to shared AI URL\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify Model</span>\n <input\n type=\"text\"\n value={aiVerifyModelInput}\n onChange={(e) => setAIVerifyModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"claude-sonnet-4-5-20250929\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verify Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiVerifyModelFallbackInput}\n onChange={(e) => setAIVerifyModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"openai/gpt-4o-mini,anthropic/claude-3-5-sonnet\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verification Min Severity</span>\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify API Key</span>\n <input\n type=\"password\"\n value={verifyAPIKeyInput}\n onChange={(e) => setVerifyAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_verify_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearVerifyAPIKey}\n onChange={(e) => setClearVerifyAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear retest AI API key\n </label>\n <div className=\"pt-2 border-t border-gray-800 mt-2 space-y-2\">\n <h4 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Auto Retest Policy</h4>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={autoRetestEnabledInput}\n onChange={(e) => setAutoRetestEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Auto-queue retests after each scan\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Auto Retest Min Severity</span>\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Auto Retest Max Findings Per Scan</span>\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n />\n </label>\n </div>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n <div className=\"pt-2 border-t border-gray-800 mt-2 space-y-2\">\n <h4 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Verification Policy</h4>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={proofRequiredForSmartInput}\n onChange={(e) => setProofRequiredForSmartInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Proof required for smart scan reports (no exploit = no report)\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verification Min Severity (scan-time + retest)</span>\n <select\n value={verificationMinSeverityInput}\n onChange={(e) => setVerificationMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Escalation Min Severity</span>\n <select\n value={aiEscalationMinSeverityInput}\n onChange={(e) => setAIEscalationMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n </div>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={persistAIToEnv}\n onChange={(e) => setPersistAIToEnv(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Persist changes to local <code>.env</code>\n </label>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save AI Settings'}\n </button>\n </div>\n\n {aiSettings && (\n <p className=\"text-xs text-gray-500\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'} · Retest key:{' '}\n {aiSettings.ai_verify_api_key_configured ? 'configured' : 'not set'} · Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `enabled (${aiSettings.ai_classify_min_severity}+)` : 'disabled'}\n </p>\n )}\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n"}, "dbe37be761e3ffd134f7ec26d58ce93714244995": {"t": 1785764360.1948383, "n": 21695, "h": "a890468e45e16d24", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [verifyAPIKeyInput, setVerifyAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [clearVerifyAPIKey, setClearVerifyAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiScanClassificationEnabledInput, setAIScanClassificationEnabledInput] = useState(false)\n const [aiClassifyMinSeverityInput, setAIClassifyMinSeverityInput] = useState<Severity>('high')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyURLInput, setAIVerifyURLInput] = useState('')\n const [aiVerifyModelInput, setAIVerifyModelInput] = useState('')\n const [aiVerifyModelFallbackInput, setAIVerifyModelFallbackInput] = useState('')\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIScanClassificationEnabledInput(Boolean(settings.ai_scan_classification_enabled))\n setAIClassifyMinSeverityInput(settings.ai_classify_min_severity || settings.ai_verify_min_severity || 'high')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyURLInput(settings.ai_verify_url || '')\n setAIVerifyModelInput(settings.ai_verify_model || '')\n setAIVerifyModelFallbackInput(settings.ai_verify_model_fallback || '')\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setScanAPIKeyInput('')\n setVerifyAPIKeyInput('')\n setClearScanAPIKey(false)\n setClearVerifyAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_scan_classification_enabled: aiScanClassificationEnabledInput,\n ai_classify_min_severity: aiClassifyMinSeverityInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_url: aiVerifyURLInput,\n ai_verify_model: aiVerifyModelInput,\n ai_verify_model_fallback: aiVerifyModelFallbackInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n if (clearVerifyAPIKey) {\n payload.ai_verify_api_key = ''\n } else if (verifyAPIKeyInput.trim()) {\n payload.ai_verify_api_key = verifyAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n try {\n const result = await testAISettings({\n scope,\n ai_url: scope === 'scan' ? aiURLInput || undefined : aiVerifyURLInput || undefined,\n ai_api_key: scope === 'scan' ? scanAPIKeyInput.trim() || undefined : verifyAPIKeyInput.trim() || undefined,\n ai_model: scope === 'scan' ? aiModelInput || undefined : aiVerifyModelInput || undefined,\n ai_fallback_model:\n scope === 'scan'\n ? aiModelFallbackInput || undefined\n : aiVerifyModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message = result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-400\">AI Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Retest AI verification is authoritative. Scan-time AI classification is optional pre-triage.\n Changes apply to new jobs immediately.\n </p>\n </div>\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Shared Provider + Optional Scan Classification</h3>\n <p className=\"text-xs text-gray-500\">\n Used as the shared provider defaults. Scan-time AI classification runs only when enabled below.\n </p>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI URL</span>\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Model</span>\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Mask Host</span>\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"example.com\"\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiScanClassificationEnabledInput}\n onChange={(e) => setAIScanClassificationEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Enable scan-time AI classification\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Scan Classification Min Severity</span>\n <select\n value={aiClassifyMinSeverityInput}\n onChange={(e) => setAIClassifyMinSeverityInput(e.target.value as Severity)}\n disabled={!aiScanClassificationEnabledInput}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500 disabled:opacity-50\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI API Key</span>\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearScanAPIKey}\n onChange={(e) => setClearScanAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear shared provider API key\n </label>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing shared provider...' : 'Test Shared Provider'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Retest AI Verification (Authoritative)</h3>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiVerifyEnabledInput}\n onChange={(e) => setAIVerifyEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Enable AI verification for retests\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify URL</span>\n <input\n type=\"text\"\n value={aiVerifyURLInput}\n onChange={(e) => setAIVerifyURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"Leave empty to fall back to shared AI URL\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify Model</span>\n <input\n type=\"text\"\n value={aiVerifyModelInput}\n onChange={(e) => setAIVerifyModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"claude-sonnet-4-5-20250929\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verify Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiVerifyModelFallbackInput}\n onChange={(e) => setAIVerifyModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"openai/gpt-4o-mini,anthropic/claude-3-5-sonnet\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verification Min Severity</span>\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify API Key</span>\n <input\n type=\"password\"\n value={verifyAPIKeyInput}\n onChange={(e) => setVerifyAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_verify_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearVerifyAPIKey}\n onChange={(e) => setClearVerifyAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear retest AI API key\n </label>\n <div className=\"pt-2 border-t border-gray-800 mt-2 space-y-2\">\n <h4 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Auto Retest Policy</h4>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={autoRetestEnabledInput}\n onChange={(e) => setAutoRetestEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Auto-queue retests after each scan\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Auto Retest Min Severity</span>\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Auto Retest Max Findings Per Scan</span>\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n />\n </label>\n </div>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={persistAIToEnv}\n onChange={(e) => setPersistAIToEnv(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Persist changes to local <code>.env</code>\n </label>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save AI Settings'}\n </button>\n </div>\n\n {aiSettings && (\n <p className=\"text-xs text-gray-500\">\n Shared key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'} · Retest key:{' '}\n {aiSettings.ai_verify_api_key_configured ? 'configured' : 'not set'} · Scan classification:{' '}\n {aiSettings.ai_scan_classification_enabled ? `enabled (${aiSettings.ai_classify_min_severity}+)` : 'disabled'}\n </p>\n )}\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n"}, "364c9421c5be23b87ba33a62e41ed468c917fdc7": {"t": 1785764361.586232, "n": 19396, "h": "d34f42a9f76953bb", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [verifyAPIKeyInput, setVerifyAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [clearVerifyAPIKey, setClearVerifyAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyURLInput, setAIVerifyURLInput] = useState('')\n const [aiVerifyModelInput, setAIVerifyModelInput] = useState('')\n const [aiVerifyModelFallbackInput, setAIVerifyModelFallbackInput] = useState('')\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [autoRetestEnabledInput, setAutoRetestEnabledInput] = useState(true)\n const [autoRetestMinSeverityInput, setAutoRetestMinSeverityInput] = useState<Severity>('medium')\n const [autoRetestMaxPerScanInput, setAutoRetestMaxPerScanInput] = useState('25')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyURLInput(settings.ai_verify_url || '')\n setAIVerifyModelInput(settings.ai_verify_model || '')\n setAIVerifyModelFallbackInput(settings.ai_verify_model_fallback || '')\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setAutoRetestEnabledInput(Boolean(settings.auto_retest_on_scan_complete))\n setAutoRetestMinSeverityInput(settings.auto_retest_min_severity || 'medium')\n setAutoRetestMaxPerScanInput(String(settings.auto_retest_max_per_scan ?? 25))\n setScanAPIKeyInput('')\n setVerifyAPIKeyInput('')\n setClearScanAPIKey(false)\n setClearVerifyAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_url: aiVerifyURLInput,\n ai_verify_model: aiVerifyModelInput,\n ai_verify_model_fallback: aiVerifyModelFallbackInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n auto_retest_on_scan_complete: autoRetestEnabledInput,\n auto_retest_min_severity: autoRetestMinSeverityInput,\n auto_retest_max_per_scan: Math.max(0, Number.parseInt(autoRetestMaxPerScanInput || '0', 10) || 0),\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n if (clearVerifyAPIKey) {\n payload.ai_verify_api_key = ''\n } else if (verifyAPIKeyInput.trim()) {\n payload.ai_verify_api_key = verifyAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n try {\n const result = await testAISettings({\n scope,\n ai_url: scope === 'scan' ? aiURLInput || undefined : aiVerifyURLInput || undefined,\n ai_api_key: scope === 'scan' ? scanAPIKeyInput.trim() || undefined : verifyAPIKeyInput.trim() || undefined,\n ai_model: scope === 'scan' ? aiModelInput || undefined : aiVerifyModelInput || undefined,\n ai_fallback_model:\n scope === 'scan'\n ? aiModelFallbackInput || undefined\n : aiVerifyModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message = result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-400\">AI Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan AI and retest AI verification. Changes apply to new jobs immediately.\n </p>\n </div>\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Scan AI</h3>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI URL</span>\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Model</span>\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Mask Host</span>\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"example.com\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI API Key</span>\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearScanAPIKey}\n onChange={(e) => setClearScanAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear scan AI API key\n </label>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing scan AI...' : 'Test Scan AI'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Retest AI Verification</h3>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiVerifyEnabledInput}\n onChange={(e) => setAIVerifyEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Enable AI verification for retests\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify URL</span>\n <input\n type=\"text\"\n value={aiVerifyURLInput}\n onChange={(e) => setAIVerifyURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"Leave empty to fall back to Scan AI URL\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify Model</span>\n <input\n type=\"text\"\n value={aiVerifyModelInput}\n onChange={(e) => setAIVerifyModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"claude-sonnet-4-5-20250929\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verify Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiVerifyModelFallbackInput}\n onChange={(e) => setAIVerifyModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"openai/gpt-4o-mini,anthropic/claude-3-5-sonnet\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Min Severity</span>\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify API Key</span>\n <input\n type=\"password\"\n value={verifyAPIKeyInput}\n onChange={(e) => setVerifyAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_verify_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearVerifyAPIKey}\n onChange={(e) => setClearVerifyAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear retest AI API key\n </label>\n <div className=\"pt-2 border-t border-gray-800 mt-2 space-y-2\">\n <h4 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Auto Retest Policy</h4>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={autoRetestEnabledInput}\n onChange={(e) => setAutoRetestEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Auto-queue retests after each scan\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Auto Retest Min Severity</span>\n <select\n value={autoRetestMinSeverityInput}\n onChange={(e) => setAutoRetestMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Auto Retest Max Findings Per Scan</span>\n <input\n type=\"number\"\n min={0}\n max={500}\n value={autoRetestMaxPerScanInput}\n onChange={(e) => setAutoRetestMaxPerScanInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n />\n </label>\n </div>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={persistAIToEnv}\n onChange={(e) => setPersistAIToEnv(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Persist changes to local <code>.env</code>\n </label>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save AI Settings'}\n </button>\n </div>\n\n {aiSettings && (\n <p className=\"text-xs text-gray-500\">\n Scan key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'} · Retest key:{' '}\n {aiSettings.ai_verify_api_key_configured ? 'configured' : 'not set'}\n </p>\n )}\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n"}, "044b667c9221a1325b6ccc40d2201d8ef6c05b5e": {"t": 1785764363.3685148, "n": 16681, "h": "5e61c77e717dd31f", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n testAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [verifyAPIKeyInput, setVerifyAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [clearVerifyAPIKey, setClearVerifyAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiModelFallbackInput, setAIModelFallbackInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyURLInput, setAIVerifyURLInput] = useState('')\n const [aiVerifyModelInput, setAIVerifyModelInput] = useState('')\n const [aiVerifyModelFallbackInput, setAIVerifyModelFallbackInput] = useState('')\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n const [testingScope, setTestingScope] = useState<'scan' | 'verify' | null>(null)\n const [scanProbeMessage, setScanProbeMessage] = useState<string | null>(null)\n const [verifyProbeMessage, setVerifyProbeMessage] = useState<string | null>(null)\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIModelFallbackInput(settings.ai_model_fallback || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyURLInput(settings.ai_verify_url || '')\n setAIVerifyModelInput(settings.ai_verify_model || '')\n setAIVerifyModelFallbackInput(settings.ai_verify_model_fallback || '')\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setScanAPIKeyInput('')\n setVerifyAPIKeyInput('')\n setClearScanAPIKey(false)\n setClearVerifyAPIKey(false)\n setScanProbeMessage(null)\n setVerifyProbeMessage(null)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_model_fallback: aiModelFallbackInput,\n ai_mask_host: aiMaskHostInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_url: aiVerifyURLInput,\n ai_verify_model: aiVerifyModelInput,\n ai_verify_model_fallback: aiVerifyModelFallbackInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n if (clearVerifyAPIKey) {\n payload.ai_verify_api_key = ''\n } else if (verifyAPIKeyInput.trim()) {\n payload.ai_verify_api_key = verifyAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n const handleTestAISettings = async (scope: 'scan' | 'verify') => {\n if (aiSaving || testingScope) return\n setAISettingsError(null)\n if (scope === 'scan') {\n setScanProbeMessage(null)\n } else {\n setVerifyProbeMessage(null)\n }\n setTestingScope(scope)\n try {\n const result = await testAISettings({\n scope,\n ai_url: scope === 'scan' ? aiURLInput || undefined : aiVerifyURLInput || undefined,\n ai_api_key: scope === 'scan' ? scanAPIKeyInput.trim() || undefined : verifyAPIKeyInput.trim() || undefined,\n ai_model: scope === 'scan' ? aiModelInput || undefined : aiVerifyModelInput || undefined,\n ai_fallback_model:\n scope === 'scan'\n ? aiModelFallbackInput || undefined\n : aiVerifyModelFallbackInput || undefined,\n })\n\n const usedModel = String(result.probe?.provider_meta?.model_used || '').trim()\n const latency = result.probe?.latency_ms\n const detail = usedModel ? `model ${usedModel}` : 'provider responded'\n const latencyLabel = typeof latency === 'number' ? ` in ${latency}ms` : ''\n const message = result.status === 'ok'\n ? `Probe succeeded (${detail}${latencyLabel}).`\n : `Probe failed: ${result.probe?.error || 'unknown error'}`\n\n if (scope === 'scan') {\n setScanProbeMessage(message)\n } else {\n setVerifyProbeMessage(message)\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to test AI settings'\n if (scope === 'scan') {\n setScanProbeMessage(`Probe failed: ${message}`)\n } else {\n setVerifyProbeMessage(`Probe failed: ${message}`)\n }\n setAISettingsError(message)\n } finally {\n setTestingScope(null)\n }\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-400\">AI Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan AI and retest AI verification. Changes apply to new jobs immediately.\n </p>\n </div>\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Scan AI</h3>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI URL</span>\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Model</span>\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiModelFallbackInput}\n onChange={(e) => setAIModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"moonshotai/kimi-k2.5,openai/gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Mask Host</span>\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"example.com\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI API Key</span>\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearScanAPIKey}\n onChange={(e) => setClearScanAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear scan AI API key\n </label>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('scan')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'scan' ? 'Testing scan AI...' : 'Test Scan AI'}\n </button>\n {scanProbeMessage && (\n <span className={`text-xs ${scanProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {scanProbeMessage}\n </span>\n )}\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Retest AI Verification</h3>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiVerifyEnabledInput}\n onChange={(e) => setAIVerifyEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Enable AI verification for retests\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify URL</span>\n <input\n type=\"text\"\n value={aiVerifyURLInput}\n onChange={(e) => setAIVerifyURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"Leave empty to fall back to Scan AI URL\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify Model</span>\n <input\n type=\"text\"\n value={aiVerifyModelInput}\n onChange={(e) => setAIVerifyModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"claude-sonnet-4-5-20250929\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Verify Fallback Models (comma-separated)</span>\n <input\n type=\"text\"\n value={aiVerifyModelFallbackInput}\n onChange={(e) => setAIVerifyModelFallbackInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"openai/gpt-4o-mini,anthropic/claude-3-5-sonnet\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Min Severity</span>\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify API Key</span>\n <input\n type=\"password\"\n value={verifyAPIKeyInput}\n onChange={(e) => setVerifyAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_verify_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearVerifyAPIKey}\n onChange={(e) => setClearVerifyAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear retest AI API key\n </label>\n <div className=\"flex items-center gap-2 pt-1\">\n <button\n onClick={() => handleTestAISettings('verify')}\n disabled={loading || aiSaving || testingScope !== null}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-xs\"\n >\n {testingScope === 'verify' ? 'Testing retest AI...' : 'Test Retest AI'}\n </button>\n {verifyProbeMessage && (\n <span className={`text-xs ${verifyProbeMessage.startsWith('Probe succeeded') ? 'text-green-400' : 'text-red-400'}`}>\n {verifyProbeMessage}\n </span>\n )}\n </div>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={persistAIToEnv}\n onChange={(e) => setPersistAIToEnv(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Persist changes to local <code>.env</code>\n </label>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save AI Settings'}\n </button>\n </div>\n\n {aiSettings && (\n <p className=\"text-xs text-gray-500\">\n Scan key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'} · Retest key:{' '}\n {aiSettings.ai_verify_api_key_configured ? 'configured' : 'not set'}\n </p>\n )}\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n"}, "e878caf430484e9e2eeea134441e6a6b9b0d39e7": {"t": 1785764366.0494452, "n": 11670, "h": "fb97d496d8d89f56", "c": "'use client'\n\nimport { useEffect, useState } from 'react'\nimport {\n getAISettings,\n updateAISettings,\n type AISettings,\n type AISettingsUpdate,\n} from '@/lib/api'\n\ntype Severity = 'critical' | 'high' | 'medium' | 'low' | 'info'\n\nexport default function AISettingsPanel() {\n const [aiSettings, setAISettings] = useState<AISettings | null>(null)\n const [aiSettingsError, setAISettingsError] = useState<string | null>(null)\n const [aiSettingsMessage, setAISettingsMessage] = useState<string | null>(null)\n const [aiSaving, setAISaving] = useState(false)\n const [loading, setLoading] = useState(true)\n const [scanAPIKeyInput, setScanAPIKeyInput] = useState('')\n const [verifyAPIKeyInput, setVerifyAPIKeyInput] = useState('')\n const [clearScanAPIKey, setClearScanAPIKey] = useState(false)\n const [clearVerifyAPIKey, setClearVerifyAPIKey] = useState(false)\n const [persistAIToEnv, setPersistAIToEnv] = useState(false)\n const [aiURLInput, setAIURLInput] = useState('')\n const [aiModelInput, setAIModelInput] = useState('')\n const [aiMaskHostInput, setAIMaskHostInput] = useState('')\n const [aiVerifyEnabledInput, setAIVerifyEnabledInput] = useState(false)\n const [aiVerifyURLInput, setAIVerifyURLInput] = useState('')\n const [aiVerifyModelInput, setAIVerifyModelInput] = useState('')\n const [aiVerifyMinSeverityInput, setAIVerifyMinSeverityInput] = useState<Severity>('high')\n\n const applyAISettingsToForm = (settings: AISettings) => {\n setAIURLInput(settings.ai_url || '')\n setAIModelInput(settings.ai_model || '')\n setAIMaskHostInput(settings.ai_mask_host || '')\n setAIVerifyEnabledInput(Boolean(settings.ai_verify_enabled))\n setAIVerifyURLInput(settings.ai_verify_url || '')\n setAIVerifyModelInput(settings.ai_verify_model || '')\n setAIVerifyMinSeverityInput(settings.ai_verify_min_severity || 'high')\n setScanAPIKeyInput('')\n setVerifyAPIKeyInput('')\n setClearScanAPIKey(false)\n setClearVerifyAPIKey(false)\n }\n\n const fetchAISettings = async () => {\n try {\n const settings = await getAISettings()\n setAISettings(settings)\n setAISettingsError(null)\n applyAISettingsToForm(settings)\n } catch {\n setAISettingsError('AI settings unavailable')\n } finally {\n setLoading(false)\n }\n }\n\n useEffect(() => {\n fetchAISettings()\n }, [])\n\n const handleSaveAISettings = async () => {\n if (aiSaving) return\n setAISettingsError(null)\n setAISettingsMessage(null)\n setAISaving(true)\n\n try {\n const payload: AISettingsUpdate = {\n ai_url: aiURLInput,\n ai_model: aiModelInput,\n ai_mask_host: aiMaskHostInput,\n ai_verify_enabled: aiVerifyEnabledInput,\n ai_verify_url: aiVerifyURLInput,\n ai_verify_model: aiVerifyModelInput,\n ai_verify_min_severity: aiVerifyMinSeverityInput,\n persist_to_env: persistAIToEnv,\n }\n\n if (clearScanAPIKey) {\n payload.ai_api_key = ''\n } else if (scanAPIKeyInput.trim()) {\n payload.ai_api_key = scanAPIKeyInput.trim()\n }\n\n if (clearVerifyAPIKey) {\n payload.ai_verify_api_key = ''\n } else if (verifyAPIKeyInput.trim()) {\n payload.ai_verify_api_key = verifyAPIKeyInput.trim()\n }\n\n const result = await updateAISettings(payload)\n setAISettings(result.settings)\n applyAISettingsToForm(result.settings)\n setAISettingsMessage(\n result.persisted_to_env\n ? result.persist_message || 'AI settings updated and persisted to .env'\n : 'AI runtime settings updated'\n )\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to update AI settings'\n setAISettingsError(message)\n } finally {\n setAISaving(false)\n }\n }\n\n return (\n <div className=\"bg-gray-900 rounded-lg border border-gray-800 p-4 space-y-4\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h2 className=\"text-sm font-medium text-gray-400\">AI Settings</h2>\n <p className=\"text-xs text-gray-500 mt-1\">\n Configure scan AI and retest AI verification. Changes apply to new jobs immediately.\n </p>\n </div>\n <button\n onClick={fetchAISettings}\n disabled={loading || aiSaving}\n className=\"px-3 py-1.5 bg-gray-800 hover:bg-gray-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n Refresh\n </button>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Scan AI</h3>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI URL</span>\n <input\n type=\"text\"\n value={aiURLInput}\n onChange={(e) => setAIURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"https://api.openai.com/v1/chat/completions\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Model</span>\n <input\n type=\"text\"\n value={aiModelInput}\n onChange={(e) => setAIModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"gpt-4o-mini\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Mask Host</span>\n <input\n type=\"text\"\n value={aiMaskHostInput}\n onChange={(e) => setAIMaskHostInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"example.com\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI API Key</span>\n <input\n type=\"password\"\n value={scanAPIKeyInput}\n onChange={(e) => setScanAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearScanAPIKey}\n onChange={(e) => setClearScanAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear scan AI API key\n </label>\n </div>\n\n <div className=\"space-y-2\">\n <h3 className=\"text-xs font-medium text-gray-300 uppercase tracking-wide\">Retest AI Verification</h3>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={aiVerifyEnabledInput}\n onChange={(e) => setAIVerifyEnabledInput(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Enable AI verification for retests\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify URL</span>\n <input\n type=\"text\"\n value={aiVerifyURLInput}\n onChange={(e) => setAIVerifyURLInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"Leave empty to fall back to Scan AI URL\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify Model</span>\n <input\n type=\"text\"\n value={aiVerifyModelInput}\n onChange={(e) => setAIVerifyModelInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder=\"claude-sonnet-4-5-20250929\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">Min Severity</span>\n <select\n value={aiVerifyMinSeverityInput}\n onChange={(e) => setAIVerifyMinSeverityInput(e.target.value as Severity)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n >\n <option value=\"critical\">critical</option>\n <option value=\"high\">high</option>\n <option value=\"medium\">medium</option>\n <option value=\"low\">low</option>\n <option value=\"info\">info</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-gray-400\">AI Verify API Key</span>\n <input\n type=\"password\"\n value={verifyAPIKeyInput}\n onChange={(e) => setVerifyAPIKeyInput(e.target.value)}\n className=\"mt-1 w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-white focus:outline-none focus:border-blue-500\"\n placeholder={aiSettings?.ai_verify_api_key_configured ? 'Configured (enter to replace)' : 'sk-...'}\n />\n </label>\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-400\">\n <input\n type=\"checkbox\"\n checked={clearVerifyAPIKey}\n onChange={(e) => setClearVerifyAPIKey(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Clear retest AI API key\n </label>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-3\">\n <label className=\"inline-flex items-center gap-2 text-xs text-gray-300\">\n <input\n type=\"checkbox\"\n checked={persistAIToEnv}\n onChange={(e) => setPersistAIToEnv(e.target.checked)}\n className=\"h-4 w-4 rounded border-gray-700 bg-gray-800 text-blue-600 focus:ring-blue-500\"\n />\n Persist changes to local <code>.env</code>\n </label>\n <button\n onClick={handleSaveAISettings}\n disabled={aiSaving || loading}\n className=\"px-3 py-1.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded text-sm\"\n >\n {aiSaving ? 'Saving...' : 'Save AI Settings'}\n </button>\n </div>\n\n {aiSettings && (\n <p className=\"text-xs text-gray-500\">\n Scan key: {aiSettings.ai_api_key_configured ? 'configured' : 'not set'} · Retest key:{' '}\n {aiSettings.ai_verify_api_key_configured ? 'configured' : 'not set'}\n </p>\n )}\n {aiSettingsMessage && <p className=\"text-xs text-green-400\">{aiSettingsMessage}</p>}\n {aiSettingsError && <p className=\"text-xs text-red-400\">{aiSettingsError}</p>}\n </div>\n )\n}\n"}}}} |