feat(settings): add SEC EDGAR configuration controls
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Settings,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Loader2,
|
||||
@@ -10,8 +9,6 @@ import {
|
||||
Save,
|
||||
KeyRound,
|
||||
Globe,
|
||||
ShieldCheck,
|
||||
ShieldAlert,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { agentSettingsBridge } from '../../lib/agentSettingsBridge';
|
||||
@@ -37,6 +34,7 @@ interface FormState {
|
||||
remoteBaseUrl: string;
|
||||
defaultRemoteModel: string;
|
||||
taskDefaults: Record<TaskProfile, AgentTaskRoute>;
|
||||
secEdgarUserAgent: string;
|
||||
remoteApiKey: string;
|
||||
}
|
||||
|
||||
@@ -44,6 +42,8 @@ interface ValidationState {
|
||||
baseUrl: ValidationStatus;
|
||||
baseUrlError?: string;
|
||||
defaultModel: ValidationStatus;
|
||||
secEdgarUserAgent: ValidationStatus;
|
||||
secEdgarUserAgentError?: string;
|
||||
apiKey: ValidationStatus;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,24 @@ const validateUrl = (url: string): { valid: boolean; error?: string } => {
|
||||
}
|
||||
};
|
||||
|
||||
const validateSecEdgarUserAgent = (
|
||||
value: string,
|
||||
): { valid: boolean; error?: string } => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
if (!trimmed.includes(' ') || !trimmed.includes('@')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Use a format like `MosaicIQ admin@example.com`',
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
status,
|
||||
onStatusChange,
|
||||
@@ -77,12 +95,14 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
remoteBaseUrl: '',
|
||||
defaultRemoteModel: '',
|
||||
taskDefaults: mergeTaskDefaults({}, ''),
|
||||
secEdgarUserAgent: '',
|
||||
remoteApiKey: '',
|
||||
});
|
||||
const [initialState, setInitialState] = useState<FormState | null>(null);
|
||||
const [validation, setValidation] = useState<ValidationState>({
|
||||
baseUrl: 'idle',
|
||||
defaultModel: 'idle',
|
||||
secEdgarUserAgent: 'idle',
|
||||
apiKey: 'idle',
|
||||
});
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
@@ -103,6 +123,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
remoteBaseUrl: status.remoteBaseUrl,
|
||||
defaultRemoteModel: status.defaultRemoteModel,
|
||||
taskDefaults: mergeTaskDefaults(status.taskDefaults, status.defaultRemoteModel),
|
||||
secEdgarUserAgent: status.secEdgarUserAgent,
|
||||
remoteApiKey: '',
|
||||
};
|
||||
|
||||
@@ -114,6 +135,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
setValidation({
|
||||
baseUrl: 'idle',
|
||||
defaultModel: 'idle',
|
||||
secEdgarUserAgent: 'idle',
|
||||
apiKey: 'idle',
|
||||
});
|
||||
}, [status]);
|
||||
@@ -127,7 +149,8 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
formState.remoteBaseUrl !== initialState.remoteBaseUrl ||
|
||||
formState.defaultRemoteModel !== initialState.defaultRemoteModel ||
|
||||
JSON.stringify(formState.taskDefaults) !== JSON.stringify(initialState.taskDefaults) ||
|
||||
(formState.remoteApiKey && formState.remoteApiKey.length > 0);
|
||||
formState.secEdgarUserAgent !== initialState.secEdgarUserAgent ||
|
||||
Boolean(formState.remoteApiKey && formState.remoteApiKey.length > 0);
|
||||
|
||||
setHasUnsavedChanges(hasChanges);
|
||||
}, [formState, initialState]);
|
||||
@@ -155,6 +178,23 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
}
|
||||
}, [formState.defaultRemoteModel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formState.secEdgarUserAgent.trim()) {
|
||||
const result = validateSecEdgarUserAgent(formState.secEdgarUserAgent);
|
||||
setValidation((prev) => ({
|
||||
...prev,
|
||||
secEdgarUserAgent: result.valid ? 'valid' : 'invalid',
|
||||
secEdgarUserAgentError: result.error,
|
||||
}));
|
||||
} else {
|
||||
setValidation((prev) => ({
|
||||
...prev,
|
||||
secEdgarUserAgent: 'idle',
|
||||
secEdgarUserAgentError: undefined,
|
||||
}));
|
||||
}
|
||||
}, [formState.secEdgarUserAgent]);
|
||||
|
||||
// Validate API key
|
||||
useEffect(() => {
|
||||
if (formState.remoteApiKey) {
|
||||
@@ -180,6 +220,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
remoteBaseUrl: formState.remoteBaseUrl,
|
||||
defaultRemoteModel: formState.defaultRemoteModel,
|
||||
taskDefaults: formState.taskDefaults,
|
||||
secEdgarUserAgent: formState.secEdgarUserAgent,
|
||||
};
|
||||
|
||||
const setTaskRoute = useCallback(
|
||||
@@ -279,19 +320,24 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
|
||||
const handleDefaultRemoteModelChange = (nextValue: string) => {
|
||||
const previousValue = formState.defaultRemoteModel;
|
||||
setFormState((prev) => ({ ...prev, defaultRemoteModel: nextValue }));
|
||||
setTaskDefaults((current) => {
|
||||
const next = { ...current };
|
||||
setFormState((current) => {
|
||||
const nextTaskDefaults = { ...current.taskDefaults };
|
||||
for (const profile of TASK_PROFILES) {
|
||||
if (next[profile].model.trim() === previousValue.trim()) {
|
||||
next[profile] = { model: nextValue };
|
||||
if (nextTaskDefaults[profile].model.trim() === previousValue.trim()) {
|
||||
nextTaskDefaults[profile] = { model: nextValue };
|
||||
}
|
||||
}
|
||||
return next;
|
||||
return {
|
||||
...current,
|
||||
defaultRemoteModel: nextValue,
|
||||
taskDefaults: nextTaskDefaults,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const isFormValid = validation.baseUrl === 'valid' || validation.baseUrl === 'idle';
|
||||
const isFormValid =
|
||||
(validation.baseUrl === 'valid' || validation.baseUrl === 'idle') &&
|
||||
(validation.secEdgarUserAgent === 'valid' || validation.secEdgarUserAgent === 'idle');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -501,7 +547,7 @@ export const AgentSettingsForm: React.FC<AgentSettingsFormProps> = ({
|
||||
validationStatus={validation.apiKey}
|
||||
helperText={
|
||||
status.hasRemoteApiKey && !formState.remoteApiKey
|
||||
? `Current key ending in ••••${status.remoteApiKey?.slice(-4) || '****'}`
|
||||
? 'A remote API key is currently stored.'
|
||||
: 'Required for API requests'
|
||||
}
|
||||
disabled={isBusy}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface ModelOption {
|
||||
}
|
||||
|
||||
export interface ModelSelectorProps {
|
||||
id?: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
@@ -29,6 +30,7 @@ const DEFAULT_MODEL_OPTIONS: ModelOption[] = [
|
||||
];
|
||||
|
||||
export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Select or enter a model',
|
||||
@@ -97,15 +99,16 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
>
|
||||
{!isCustomMode ? (
|
||||
<button
|
||||
id={id}
|
||||
type="button"
|
||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||
disabled={disabled}
|
||||
className="w-full rounded border border-[#2a2a2a] bg-[#111111] px-3 py-2 text-left text-sm font-mono text-[#e0e0e0] outline-none transition-colors hover:border-[#3a3a3a] focus:border-[#58a6ff] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="w-full border border-[#2a2a2a] bg-[#111111] px-3 py-2 text-left text-sm font-mono text-[#e0e0e0] outline-none transition-all hover:border-[#3a3a3a] focus:border-[#58a6ff] disabled:cursor-not-allowed disabled:opacity-50 border-l-2 focus:border-l-[#58a6ff] hover:border-l-[#3a3a3a]"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="flex-1 min-w-0 truncate">
|
||||
{selectedOption ? (
|
||||
<span>
|
||||
{selectedOption.label}
|
||||
@@ -117,13 +120,14 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
<span className="text-[#666666]">{placeholder}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-[#666666]" aria-hidden="true">
|
||||
<span className="text-[#666666] flex-shrink-0" aria-hidden="true">
|
||||
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<input
|
||||
id={id}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value}
|
||||
@@ -135,29 +139,33 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Enter custom model name"
|
||||
className="w-full rounded border border-[#58a6ff] bg-[#111111] px-3 py-2 text-sm font-mono text-[#e0e0e0] outline-none focus:ring-2 focus:ring-[#58a6ff] focus:ring-offset-2 focus:ring-offset-[#111111]"
|
||||
className="w-full border border-[#58a6ff] bg-[#111111] px-3 py-2 text-sm font-mono text-[#e0e0e0] outline-none focus:border-l-[#58a6ff] border-l-2 transition-all"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isOpen && !isCustomMode && (
|
||||
<div className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-[#2a2a2a] bg-[#111111] shadow-lg">
|
||||
<div className="border-b border-[#2a2a2a] p-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#666666]" />
|
||||
<div className="absolute z-10 mt-1 max-h-60 w-full overflow-auto border border-[#2a2a2a] bg-[#111111] shadow-lg">
|
||||
<div className="border-b border-[#1a1a1a] p-3">
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[#666666] group-focus-within:text-[#58a6ff] transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search models..."
|
||||
className="w-full rounded border border-[#2a2a2a] bg-[#0d0d0d] py-2 pl-9 pr-3 text-sm font-mono text-[#e0e0e0] outline-none focus:border-[#58a6ff]"
|
||||
className="w-full bg-transparent py-1.5 pl-8 pr-2 text-sm font-mono text-[#e0e0e0] outline-none border-l-2 border-[#1a1a1a] focus:border-l-[#58a6ff] transition-colors"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ul role="listbox" className="py-1">
|
||||
{filteredOptions.length === 0 ? (
|
||||
<li className="px-3 py-2 text-sm font-mono text-[#888888]">
|
||||
No models found
|
||||
<li className="px-3 py-4 text-center">
|
||||
<div className="flex items-center justify-center gap-2 text-xs font-mono text-[#666666]">
|
||||
<div className="h-px w-8 bg-[#2a2a2a]" />
|
||||
<span>No models found</span>
|
||||
<div className="h-px w-8 bg-[#2a2a2a]" />
|
||||
</div>
|
||||
</li>
|
||||
) : (
|
||||
filteredOptions.map((option) => (
|
||||
@@ -166,16 +174,16 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
role="option"
|
||||
aria-selected={value === option.value}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
className={`cursor-pointer px-3 py-2 text-sm font-mono transition-colors ${
|
||||
className={`cursor-pointer px-3 py-2 text-sm font-mono transition-colors border-l-2 ${
|
||||
value === option.value
|
||||
? 'bg-[#1d4c7d] text-[#8dc3ff]'
|
||||
: 'text-[#e0e0e0] hover:bg-[#1d1d1d]'
|
||||
? 'border-l-[#58a6ff] bg-[#1d4c7d]/30 text-[#8dc3ff]'
|
||||
: 'border-l-transparent text-[#e0e0e0] hover:border-l-[#2a2a2a] hover:bg-[#1a1a1a]'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{option.label}</span>
|
||||
<span className="truncate">{option.label}</span>
|
||||
{option.provider && (
|
||||
<span className="text-xs text-[#666666]">{option.provider}</span>
|
||||
<span className="text-xs text-[#666666] ml-2">{option.provider}</span>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
@@ -183,13 +191,13 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
)}
|
||||
{allowCustom && (
|
||||
<>
|
||||
<li className="my-1 border-t border-[#2a2a2a]" role="separator" />
|
||||
<li className="my-1 border-t border-[#1a1a1a]" role="separator" />
|
||||
<li
|
||||
role="option"
|
||||
onClick={handleCustomMode}
|
||||
className="cursor-pointer px-3 py-2 text-sm font-mono text-[#888888] transition-colors hover:bg-[#1d1d1d] hover:text-[#e0e0e0]"
|
||||
className="cursor-pointer px-3 py-2 text-sm font-mono text-[#888888] transition-colors hover:text-[#e0e0e0] hover:bg-[#1a1a1a]"
|
||||
>
|
||||
Custom model...
|
||||
+ Custom model...
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
|
||||
208
MosaicIQ/src/components/Settings/SecEdgarSettingsCard.tsx
Normal file
208
MosaicIQ/src/components/Settings/SecEdgarSettingsCard.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { CheckCircle, Globe, Loader2, Save, XCircle } from 'lucide-react';
|
||||
import { agentSettingsBridge } from '../../lib/agentSettingsBridge';
|
||||
import { AgentConfigStatus } from '../../types/agentSettings';
|
||||
import { ValidatedInput, ValidationStatus } from './ValidatedInput';
|
||||
import { HelpIcon } from './Tooltip';
|
||||
|
||||
interface SecEdgarSettingsCardProps {
|
||||
status: AgentConfigStatus | null;
|
||||
onStatusChange: (status: AgentConfigStatus) => void;
|
||||
}
|
||||
|
||||
const validateSecEdgarUserAgent = (
|
||||
value: string,
|
||||
): { valid: boolean; error?: string } => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
if (!trimmed.includes(' ') || !trimmed.includes('@')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Use a format like `MosaicIQ admin@example.com`',
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
export const SecEdgarSettingsCard: React.FC<SecEdgarSettingsCardProps> = ({
|
||||
status,
|
||||
onStatusChange,
|
||||
}) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [initialValue, setInitialValue] = useState('');
|
||||
const [validation, setValidation] = useState<{
|
||||
status: ValidationStatus;
|
||||
error?: string;
|
||||
}>({
|
||||
status: 'idle',
|
||||
});
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(status.secEdgarUserAgent);
|
||||
setInitialValue(status.secEdgarUserAgent);
|
||||
setValidation({ status: 'idle' });
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value.trim()) {
|
||||
setValidation({ status: 'idle' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = validateSecEdgarUserAgent(value);
|
||||
setValidation({
|
||||
status: result.valid ? 'valid' : 'invalid',
|
||||
error: result.error,
|
||||
});
|
||||
}, [value]);
|
||||
|
||||
const hasChanges = value !== initialValue;
|
||||
const isValid = validation.status === 'valid' || validation.status === 'idle';
|
||||
|
||||
const helperText = useMemo(() => {
|
||||
if (status?.hasSecEdgarUserAgent) {
|
||||
return 'Stored in app settings and used before env fallback.';
|
||||
}
|
||||
return 'Leave blank to fall back to the `SEC_EDGAR_USER_AGENT` environment variable.';
|
||||
}, [status]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!status || !isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBusy(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const nextStatus = await agentSettingsBridge.saveRuntimeConfig({
|
||||
remoteEnabled: status.remoteEnabled,
|
||||
remoteBaseUrl: status.remoteBaseUrl,
|
||||
defaultRemoteModel: status.defaultRemoteModel,
|
||||
taskDefaults: status.taskDefaults,
|
||||
secEdgarUserAgent: value,
|
||||
});
|
||||
onStatusChange(nextStatus);
|
||||
setInitialValue(value);
|
||||
setSuccess('SEC EDGAR setting saved successfully');
|
||||
} catch (saveError) {
|
||||
setError(
|
||||
saveError instanceof Error
|
||||
? saveError.message
|
||||
: 'Failed to save SEC EDGAR setting.',
|
||||
);
|
||||
} finally {
|
||||
setIsBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
|
||||
<div className="mb-6 flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">
|
||||
SEC EDGAR
|
||||
</h2>
|
||||
<HelpIcon tooltip="Identity string sent with SEC EDGAR requests. The SEC asks automated clients to provide a descriptive product name and contact email." />
|
||||
</div>
|
||||
<p className="mt-1.5 text-xs font-mono leading-6 text-[#888888]">
|
||||
Configure how MosaicIQ identifies itself when calling SEC EDGAR.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded border border-[#2a2a2a] bg-[#111111] px-3 py-2 text-right">
|
||||
<div className="flex items-center gap-2 text-[11px] font-mono text-[#888888]">
|
||||
<Globe className="h-3.5 w-3.5" />
|
||||
<span>{status?.hasSecEdgarUserAgent ? 'Stored in app' : 'Env fallback / unset'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ValidatedInput
|
||||
label="SEC EDGAR User Agent"
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder="MosaicIQ admin@example.com"
|
||||
validationStatus={validation.status}
|
||||
errorMessage={validation.error}
|
||||
helperText={helperText}
|
||||
disabled={isBusy}
|
||||
/>
|
||||
|
||||
<div className="mt-5 flex justify-end gap-3">
|
||||
{hasChanges && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setValue(initialValue)}
|
||||
className="rounded border border-[#2a2a2a] bg-[#151515] px-4 py-2 text-xs font-mono text-[#888888] transition-colors hover:border-[#3a3a3a] hover:text-[#e0e0e0]"
|
||||
disabled={isBusy}
|
||||
>
|
||||
Discard Changes
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={isBusy || !hasChanges || !isValid || !status}
|
||||
className="flex items-center gap-2 rounded border border-[#58a6ff] bg-[#0f1f31] px-4 py-2 text-xs font-mono text-[#58a6ff] transition-colors hover:bg-[#1a2d3d] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{isBusy ? (
|
||||
<>
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
Save SEC Setting
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{success && (
|
||||
<div className="mt-4 flex items-center gap-3 rounded-lg border border-[#214f31] bg-[#102417] px-4 py-3 text-sm font-mono text-[#9ee6b3]">
|
||||
<CheckCircle className="h-5 w-5" />
|
||||
<div className="flex-1">{success}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSuccess(null)}
|
||||
className="text-[#9ee6b3] opacity-60 transition-opacity hover:opacity-100"
|
||||
aria-label="Dismiss success message"
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 flex items-center gap-3 rounded-lg border border-[#5c2b2b] bg-[#211313] px-4 py-3 text-sm font-mono text-[#ffb4b4]">
|
||||
<XCircle className="h-5 w-5" />
|
||||
<div className="flex-1">{error}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setError(null)}
|
||||
className="text-[#ffb4b4] opacity-60 transition-opacity hover:opacity-100"
|
||||
aria-label="Dismiss error message"
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
X,
|
||||
XCircle,
|
||||
Loader2,
|
||||
Globe,
|
||||
Wifi,
|
||||
WifiOff,
|
||||
KeyRound,
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { AgentConfigStatus } from '../../types/agentSettings';
|
||||
import { AgentSettingsForm } from './AgentSettingsForm';
|
||||
import { SecEdgarSettingsCard } from './SecEdgarSettingsCard';
|
||||
import { ToastContainer, type Toast, type ToastType } from './Toast';
|
||||
|
||||
type SettingsSectionId = 'general' | 'ai' | 'workspace' | 'about';
|
||||
@@ -154,6 +154,12 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const activeElement = document.activeElement as HTMLElement | null;
|
||||
const isEditingField =
|
||||
activeElement?.tagName === 'INPUT' ||
|
||||
activeElement?.tagName === 'TEXTAREA' ||
|
||||
activeElement?.isContentEditable === true;
|
||||
|
||||
// Cmd/Ctrl + S to save (prevent default)
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
@@ -170,12 +176,13 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
document.getElementById('settings-search')?.focus();
|
||||
}
|
||||
|
||||
if (isEditingField) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ? to show shortcuts
|
||||
if (e.key === '?' && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement?.tagName !== 'INPUT' && activeElement?.tagName !== 'TEXTAREA') {
|
||||
setShowKeyboardShortcuts((prev) => !prev);
|
||||
}
|
||||
setShowKeyboardShortcuts((prev) => !prev);
|
||||
}
|
||||
|
||||
// Escape to close shortcuts modal
|
||||
@@ -199,66 +206,70 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
const renderContent = () => {
|
||||
if (activeSection === 'general') {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
|
||||
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">
|
||||
<div className="space-y-8">
|
||||
<section>
|
||||
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0] mb-3">
|
||||
Settings are centralized here
|
||||
</h2>
|
||||
<p className="mt-3 max-w-2xl text-sm font-mono leading-7 text-[#888888]">
|
||||
<p className="max-w-2xl text-sm font-mono leading-7 text-[#888888]">
|
||||
This page is the dedicated control surface for MosaicIQ configuration. New settings
|
||||
categories should be added as submenu entries here instead of modal dialogs or one-off
|
||||
controls elsewhere in the shell.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 lg:grid-cols-3">
|
||||
{statusSummary.map((item) => (
|
||||
<div
|
||||
key={item.label}
|
||||
className="rounded-lg border border-[#1a1a1a] bg-[#0f0f0f] p-5 transition-shadow hover:shadow-md"
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-[#888888]" aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
<span className="text-xs font-mono text-[#888888]">{item.label}</span>
|
||||
<section>
|
||||
<div className="flex items-center gap-3 border-b border-[#1a1a1a] pb-3 mb-4">
|
||||
<div className="h-px flex-1 bg-[#1a1a1a]" />
|
||||
<span className="text-xs font-mono text-[#666666] uppercase tracking-wider">Status Overview</span>
|
||||
<div className="h-px flex-1 bg-[#1a1a1a]" />
|
||||
</div>
|
||||
<div className="grid gap-6 sm:grid-cols-3">
|
||||
{statusSummary.map((item) => (
|
||||
<div key={item.label} className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-[#666666]">
|
||||
<span className="text-sm">{item.icon}</span>
|
||||
<span className="text-xs font-mono uppercase tracking-wide">{item.label}</span>
|
||||
</div>
|
||||
<div className="text-sm font-mono text-[#e0e0e0]">{item.value}</div>
|
||||
</div>
|
||||
<div className="text-sm font-mono text-[#e0e0e0]">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
|
||||
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">
|
||||
<section>
|
||||
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0] mb-4">
|
||||
Settings roadmap
|
||||
</h2>
|
||||
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-[#888888]" aria-hidden="true">
|
||||
<Bot className="h-5 w-5" />
|
||||
</span>
|
||||
<span className="text-sm font-mono text-[#e0e0e0]">AI & Models</span>
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4 p-4 border-l-2 border-[#1a1a1a] hover:border-[#58a6ff] transition-colors">
|
||||
<span className="text-[#666666] mt-0.5">
|
||||
<Bot className="h-5 w-5" />
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-mono text-[#e0e0e0] mb-1">AI & Models</h3>
|
||||
<p className="text-sm font-mono leading-6 text-[#888888]">
|
||||
Active now. Provider routing, default models, and credential storage are managed
|
||||
in this section.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-mono leading-7 text-[#888888]">
|
||||
Active now. Provider routing, default models, and credential storage are managed
|
||||
in this section.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-[#888888]" aria-hidden="true">
|
||||
<FolderOpen className="h-5 w-5" />
|
||||
</span>
|
||||
<span className="text-sm font-mono text-[#e0e0e0]">Workspace</span>
|
||||
<div className="flex gap-4 p-4 border-l-2 border-[#1a1a1a] hover:border-[#58a6ff] transition-colors">
|
||||
<span className="text-[#666666] mt-0.5">
|
||||
<FolderOpen className="h-5 w-5" />
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-mono text-[#e0e0e0] mb-1">Workspace</h3>
|
||||
<p className="text-sm font-mono leading-6 text-[#888888]">
|
||||
Reserved for terminal defaults, tab naming conventions, and shell preferences as
|
||||
those controls are added.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-mono leading-7 text-[#888888]">
|
||||
Reserved for terminal defaults, tab naming conventions, and shell preferences as
|
||||
those controls are added.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SecEdgarSettingsCard status={status} onStatusChange={onStatusChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -269,41 +280,43 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
|
||||
if (activeSection === 'workspace') {
|
||||
return (
|
||||
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
|
||||
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">Workspace settings</h2>
|
||||
<p className="mt-3 max-w-2xl text-sm font-mono leading-7 text-[#888888]">
|
||||
<section>
|
||||
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0] mb-3">Workspace settings</h2>
|
||||
<p className="max-w-2xl text-sm font-mono leading-7 text-[#888888] mb-6">
|
||||
This submenu is in place for shell-wide controls such as sidebar defaults, startup
|
||||
behavior, and terminal preferences. Future workspace configuration should be added here
|
||||
rather than inside the terminal view.
|
||||
</p>
|
||||
<div className="mt-6 grid gap-4 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-[#888888]" aria-hidden="true">
|
||||
<Zap className="h-5 w-5" />
|
||||
</span>
|
||||
<span className="text-sm font-mono text-[#e0e0e0]">Planned controls</span>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-4 p-4 border-l-2 border-[#1a1a1a] hover:border-[#58a6ff] transition-colors">
|
||||
<span className="text-[#666666] mt-0.5">
|
||||
<Zap className="h-5 w-5" />
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-mono text-[#e0e0e0] mb-1">Planned controls</h3>
|
||||
<p className="text-sm font-mono leading-6 text-[#888888]">
|
||||
Default workspace names, sidebar visibility at launch, terminal input behavior, and
|
||||
session retention rules.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-mono leading-7 text-[#888888]">
|
||||
Default workspace names, sidebar visibility at launch, terminal input behavior, and
|
||||
session retention rules.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="text-[#888888]" aria-hidden="true">
|
||||
<ClipboardList className="h-5 w-5" />
|
||||
</span>
|
||||
<span className="text-sm font-mono text-[#e0e0e0]">Implementation note</span>
|
||||
|
||||
<div className="flex gap-4 p-4 border-l-2 border-[#1a1a1a] hover:border-[#58a6ff] transition-colors">
|
||||
<span className="text-[#666666] mt-0.5">
|
||||
<ClipboardList className="h-5 w-5" />
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-mono text-[#e0e0e0] mb-1">Implementation note</h3>
|
||||
<p className="text-sm font-mono leading-6 text-[#888888]">
|
||||
Keep future settings grouped by submenu and avoid reintroducing feature-specific
|
||||
controls in headers, toolbars, or modal overlays.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-mono leading-7 text-[#888888]">
|
||||
Keep future settings grouped by submenu and avoid reintroducing feature-specific
|
||||
controls in headers, toolbars, or modal overlays.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-lg border border-[#3d3420] bg-[#1d170c] px-4 py-3">
|
||||
<div className="mt-8 p-4 border-l-2 border-[#3d3420] bg-[#1d170c]/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="h-5 w-5 text-[#e7bb62]" aria-hidden="true" />
|
||||
<div>
|
||||
@@ -319,9 +332,9 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-lg bg-[#0f0f0f] p-6 shadow-sm">
|
||||
<h2 className="text-base font-mono font-semibold text-[#e0e0e0]">About this settings page</h2>
|
||||
<div className="mt-4 space-y-4 text-sm font-mono leading-7 text-[#888888]">
|
||||
<section>
|
||||
<h2 className="text-sm font-mono font-semibold text-[#e0e0e0] mb-3">About this settings page</h2>
|
||||
<div className="space-y-4 text-sm font-mono leading-7 text-[#888888]">
|
||||
<p>
|
||||
The settings page is designed as a durable home for configuration instead of scattering
|
||||
controls across the product shell.
|
||||
@@ -332,7 +345,7 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
</p>
|
||||
<p>
|
||||
Current shell shortcut: use the bottom-left cog or press{' '}
|
||||
<kbd className="rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-1 text-xs text-[#e0e0e0]">
|
||||
<kbd className="mx-1 px-1.5 py-0.5 text-xs text-[#e0e0e0] bg-[#1a1a1a] rounded">
|
||||
<Command className="inline h-3 w-3" /> + ,
|
||||
</kbd>{' '}
|
||||
to open settings.
|
||||
@@ -340,39 +353,39 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Keyboard shortcuts help */}
|
||||
<div className="mt-6 rounded-lg border border-[#1a1a1a] bg-[#0a0a0a] p-5">
|
||||
<div className="mt-6 pt-6 border-t border-[#1a1a1a]">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-sm font-mono font-semibold text-[#e0e0e0]">Keyboard shortcuts</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowKeyboardShortcuts(true)}
|
||||
className="rounded border border-[#2a2a2a] bg-[#111111] px-3 py-1.5 text-xs font-mono text-[#888888] transition-colors hover:border-[#58a6ff] hover:text-[#58a6ff]"
|
||||
className="px-3 py-1.5 text-xs font-mono text-[#888888] border border-[#2a2a2a] hover:border-[#58a6ff] hover:text-[#58a6ff] transition-colors rounded"
|
||||
>
|
||||
View all
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="flex items-center justify-between text-xs font-mono">
|
||||
<span className="text-[#888888]">Save settings</span>
|
||||
<kbd className="flex items-center gap-1 rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-0.5 text-[#e0e0e0]">
|
||||
<kbd className="flex items-center gap-1 px-2 py-0.5 text-[#e0e0e0] bg-[#1a1a1a] rounded">
|
||||
<Command className="h-3 w-3" />S
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs font-mono">
|
||||
<span className="text-[#888888]">Search settings</span>
|
||||
<kbd className="flex items-center gap-1 rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-0.5 text-[#e0e0e0]">
|
||||
<kbd className="flex items-center gap-1 px-2 py-0.5 text-[#e0e0e0] bg-[#1a1a1a] rounded">
|
||||
<Command className="h-3 w-3" />K
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs font-mono">
|
||||
<span className="text-[#888888]">Show shortcuts</span>
|
||||
<kbd className="rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-0.5 text-[#e0e0e0]">
|
||||
<kbd className="px-2 py-0.5 text-[#e0e0e0] bg-[#1a1a1a] rounded">
|
||||
?
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs font-mono">
|
||||
<span className="text-[#888888]">Navigate sections</span>
|
||||
<kbd className="rounded border border-[#2a2a2a] bg-[#0d0d0d] px-2 py-0.5 text-[#e0e0e0]">
|
||||
<kbd className="px-2 py-0.5 text-[#e0e0e0] bg-[#1a1a1a] rounded">
|
||||
1-4
|
||||
</kbd>
|
||||
</div>
|
||||
@@ -436,26 +449,30 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
{/* Sidebar */}
|
||||
<aside className="border-b border-[#1a1a1a] bg-[#0d0d0d] lg:w-[280px] lg:border-b-0 lg:border-r">
|
||||
{/* Search */}
|
||||
<div className="border-b border-[#1a1a1a] p-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#666666]" />
|
||||
<div className="border-b border-[#1a1a1a] px-4 py-3">
|
||||
<div className="relative group">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#666666] group-focus-within:text-[#58a6ff] transition-colors" />
|
||||
<input
|
||||
id="settings-search"
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search settings... (⌘K)"
|
||||
className="w-full rounded border border-[#2a2a2a] bg-[#111111] px-3 py-2 pl-9 text-sm font-mono text-[#e0e0e0] outline-none transition-colors focus:border-[#58a6ff]"
|
||||
className="w-full border-l-2 border-[#1a1a1a] bg-transparent px-3 py-2 pl-9 text-sm font-mono text-[#e0e0e0] outline-none transition-all focus:border-l-[#58a6ff] focus:bg-[#0a0a0a]"
|
||||
/>
|
||||
</div>
|
||||
{searchQuery && filteredSections.length === 0 && (
|
||||
<p className="mt-2 text-xs font-mono text-[#888888]">No settings found</p>
|
||||
<div className="mt-3 flex items-center gap-2 px-1">
|
||||
<div className="h-px flex-1 bg-[#2a2a2a]" />
|
||||
<span className="text-xs font-mono text-[#666666]">No results</span>
|
||||
<div className="h-px flex-1 bg-[#2a2a2a]" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav
|
||||
className="flex gap-2 overflow-x-auto px-4 py-4 lg:flex-col lg:gap-1 lg:overflow-visible"
|
||||
className="flex gap-1 overflow-x-auto px-4 py-3 lg:flex-col lg:gap-0.5 lg:overflow-visible"
|
||||
role="navigation"
|
||||
aria-label="Settings sections"
|
||||
>
|
||||
@@ -466,25 +483,23 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
key={section.id}
|
||||
type="button"
|
||||
onClick={() => setActiveSection(section.id)}
|
||||
className={`min-w-[180px] rounded border px-4 py-3 text-left transition-all lg:min-w-0 ${
|
||||
className={`min-w-[180px] flex items-center gap-3 px-3 py-2.5 text-left transition-all lg:min-w-0 ${
|
||||
isActive
|
||||
? 'border-[#3a3a3a] bg-[#111111] text-[#e0e0e0] shadow-sm'
|
||||
: 'border-transparent bg-transparent text-[#888888] hover:border-[#2a2a2a] hover:bg-[#111111] hover:text-[#e0e0e0]'
|
||||
? 'bg-[#1a1a1a] text-[#e0e0e0]'
|
||||
: 'text-[#888888] hover:bg-[#111111] hover:text-[#e0e0e0]'
|
||||
}`}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={isActive ? 'text-[#e0e0e0]' : 'text-[#888888]'}>
|
||||
{section.icon}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-mono">{section.label}</span>
|
||||
<span className="text-[10px] text-[#666666]">{index + 1}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] font-mono leading-tight text-[#666666]">
|
||||
{section.description}
|
||||
</div>
|
||||
<span className={`transition-colors ${isActive ? 'text-[#e0e0e0]' : 'text-[#666666]'}`}>
|
||||
{section.icon}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-mono truncate">{section.label}</span>
|
||||
<kbd className="text-[10px] text-[#444444] font-mono">{index + 1}</kbd>
|
||||
</div>
|
||||
<div className="mt-0.5 text-[11px] font-mono leading-tight truncate opacity-70">
|
||||
{section.description}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@@ -509,9 +524,9 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
</nav>
|
||||
|
||||
{/* Status badges */}
|
||||
<div className="mb-6 flex flex-wrap items-center gap-3">
|
||||
<div className="mb-6 flex flex-wrap items-center gap-4">
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-md border px-3 py-1.5 text-[11px] font-mono ${statusBadgeClassName(
|
||||
className={`flex items-center gap-2 px-3 py-1.5 text-[11px] font-mono border-l-2 ${statusBadgeClassName(
|
||||
Boolean(status?.configured),
|
||||
)}`}
|
||||
>
|
||||
@@ -527,19 +542,20 @@ export const SettingsPage: React.FC<SettingsPageProps> = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-md border border-[#2a2a2a] bg-[#111111] px-3 py-1.5 text-[11px] font-mono text-[#888888]">
|
||||
Active section: {currentSectionLabel}
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 text-[11px] font-mono text-[#888888] border-l-2 border-[#2a2a2a]">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#666666]" />
|
||||
<span>Active section: {currentSectionLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Refresh error */}
|
||||
{refreshError ? (
|
||||
<div
|
||||
className="mb-6 rounded-lg border border-[#5c2b2b] bg-[#211313] px-4 py-3 text-sm font-mono text-[#ffb4b4]"
|
||||
className="mb-6 p-4 border-l-2 border-[#5c2b2b] bg-[#211313]/30 text-sm font-mono text-[#ffb4b4]"
|
||||
role="alert"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<XCircle className="h-5 w-5" aria-hidden="true" />
|
||||
<div className="flex items-start gap-3">
|
||||
<XCircle className="h-5 w-5 mt-0.5" aria-hidden="true" />
|
||||
<div className="flex-1">{refreshError}</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -13,9 +13,11 @@ export interface AgentConfigStatus {
|
||||
remoteConfigured: boolean;
|
||||
remoteEnabled: boolean;
|
||||
hasRemoteApiKey: boolean;
|
||||
hasSecEdgarUserAgent: boolean;
|
||||
remoteBaseUrl: string;
|
||||
defaultRemoteModel: string;
|
||||
taskDefaults: Record<TaskProfile, AgentTaskRoute>;
|
||||
secEdgarUserAgent: string;
|
||||
}
|
||||
|
||||
export interface SaveAgentRuntimeConfigRequest {
|
||||
@@ -23,6 +25,7 @@ export interface SaveAgentRuntimeConfigRequest {
|
||||
remoteBaseUrl: string;
|
||||
defaultRemoteModel: string;
|
||||
taskDefaults: Record<TaskProfile, AgentTaskRoute>;
|
||||
secEdgarUserAgent: string;
|
||||
}
|
||||
|
||||
export interface UpdateRemoteApiKeyRequest {
|
||||
|
||||
Reference in New Issue
Block a user