Files

1 line
690 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}