mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
Split AI settings section owners
This commit is contained in:
@@ -25,61 +25,66 @@ work extends shared components instead of creating new local variants.
|
||||
3. `frontend-modern/src/components/Settings/SettingsPageShell.tsx`
|
||||
4. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts`
|
||||
5. `frontend-modern/src/components/Settings/APIAccessPanel.tsx`
|
||||
6. `frontend-modern/src/components/Settings/AISettings.tsx`
|
||||
7. `frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx`
|
||||
8. `frontend-modern/src/components/Settings/AISettingsDialogs.tsx`
|
||||
9. `frontend-modern/src/components/Settings/aiSettingsModel.ts`
|
||||
10. `frontend-modern/src/components/Settings/useAISettingsState.ts`
|
||||
11. `frontend-modern/src/components/Settings/AuditLogPanel.tsx`
|
||||
12. `frontend-modern/src/components/Settings/useAuditLogPanelState.ts`
|
||||
13. `frontend-modern/src/components/Settings/AuditWebhookPanel.tsx`
|
||||
14. `frontend-modern/src/components/Settings/useAuditWebhookPanelState.ts`
|
||||
15. `frontend-modern/src/components/Settings/CopyCommandBlock.tsx`
|
||||
16. `frontend-modern/src/components/Settings/diagnosticsModel.ts`
|
||||
17. `frontend-modern/src/components/Settings/DiagnosticsPanel.tsx`
|
||||
18. `frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx`
|
||||
19. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`
|
||||
20. `frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx`
|
||||
21. `frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx`
|
||||
22. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx`
|
||||
23. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx`
|
||||
24. `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`
|
||||
25. `frontend-modern/src/components/Settings/SettingsPageShell.tsx`
|
||||
26. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts`
|
||||
27. `frontend-modern/src/components/Settings/SSOProvidersPanel.tsx`
|
||||
28. `frontend-modern/src/components/Settings/useDiagnosticsPanelState.ts`
|
||||
29. `frontend-modern/src/components/Settings/useSSOProvidersState.ts`
|
||||
30. `frontend-modern/src/components/Settings/ssoProvidersModel.ts`
|
||||
31. `frontend-modern/src/components/Settings/UpdateInstallGuide.tsx`
|
||||
32. `frontend-modern/src/components/Settings/updatesSettingsModel.ts`
|
||||
33. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx`
|
||||
34. `frontend-modern/src/components/Settings/ReportingPanel.tsx`
|
||||
35. `frontend-modern/src/components/Settings/reportingPanelModel.ts`
|
||||
36. `frontend-modern/src/components/Settings/useReportingPanelState.ts`
|
||||
37. `frontend-modern/src/utils/reportingPresentation.ts`
|
||||
38. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts`
|
||||
39. `tests/integration/tests/15-settings-shell-consistency.spec.ts`
|
||||
40. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts`
|
||||
41. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts`
|
||||
42. `frontend-modern/src/features/`
|
||||
43. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx`
|
||||
44. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx`
|
||||
45. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx`
|
||||
46. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx`
|
||||
47. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
|
||||
48. `frontend-modern/src/components/Settings/SystemLogsPanel.tsx`
|
||||
49. `frontend-modern/src/components/Settings/useSystemLogsPanelState.ts`
|
||||
50. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx`
|
||||
51. `frontend-modern/src/features/operations/OperationsPageSurface.tsx`
|
||||
52. `frontend-modern/src/features/operations/operationsPageModel.ts`
|
||||
53. `frontend-modern/src/pages/Operations.tsx`
|
||||
54. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts`
|
||||
55. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx`
|
||||
56. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx`
|
||||
57. `frontend-modern/src/components/Settings/networkSettingsModel.ts`
|
||||
58. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts`
|
||||
59. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx`
|
||||
60. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx`
|
||||
6. `frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx`
|
||||
7. `frontend-modern/src/components/Settings/AIModelSelectionSection.tsx`
|
||||
8. `frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx`
|
||||
9. `frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx`
|
||||
10. `frontend-modern/src/components/Settings/AISettings.tsx`
|
||||
11. `frontend-modern/src/components/Settings/AISettingsDialogs.tsx`
|
||||
12. `frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx`
|
||||
13. `frontend-modern/src/components/Settings/aiSettingsModel.ts`
|
||||
14. `frontend-modern/src/components/Settings/APIAccessPanel.tsx`
|
||||
15. `frontend-modern/src/components/Settings/AuditLogPanel.tsx`
|
||||
16. `frontend-modern/src/components/Settings/useAuditLogPanelState.ts`
|
||||
17. `frontend-modern/src/components/Settings/AuditWebhookPanel.tsx`
|
||||
18. `frontend-modern/src/components/Settings/useAuditWebhookPanelState.ts`
|
||||
19. `frontend-modern/src/components/Settings/CopyCommandBlock.tsx`
|
||||
20. `frontend-modern/src/components/Settings/diagnosticsModel.ts`
|
||||
21. `frontend-modern/src/components/Settings/DiagnosticsPanel.tsx`
|
||||
22. `frontend-modern/src/components/Settings/DiagnosticsResultsPanel.tsx`
|
||||
23. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`
|
||||
24. `frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx`
|
||||
25. `frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx`
|
||||
26. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx`
|
||||
27. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx`
|
||||
28. `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`
|
||||
29. `frontend-modern/src/components/Settings/SettingsPageShell.tsx`
|
||||
30. `frontend-modern/src/components/Settings/settingsPanelRegistry.ts`
|
||||
31. `frontend-modern/src/components/Settings/SSOProvidersPanel.tsx`
|
||||
32. `frontend-modern/src/components/Settings/useAISettingsState.ts`
|
||||
33. `frontend-modern/src/components/Settings/useDiagnosticsPanelState.ts`
|
||||
34. `frontend-modern/src/components/Settings/useSSOProvidersState.ts`
|
||||
35. `frontend-modern/src/components/Settings/ssoProvidersModel.ts`
|
||||
36. `frontend-modern/src/components/Settings/UpdateInstallGuide.tsx`
|
||||
37. `frontend-modern/src/components/Settings/updatesSettingsModel.ts`
|
||||
38. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx`
|
||||
39. `frontend-modern/src/components/Settings/ReportingPanel.tsx`
|
||||
40. `frontend-modern/src/components/Settings/reportingPanelModel.ts`
|
||||
41. `frontend-modern/src/components/Settings/useReportingPanelState.ts`
|
||||
42. `frontend-modern/src/utils/reportingPresentation.ts`
|
||||
43. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts`
|
||||
44. `tests/integration/tests/15-settings-shell-consistency.spec.ts`
|
||||
45. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts`
|
||||
46. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts`
|
||||
47. `frontend-modern/src/features/`
|
||||
48. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx`
|
||||
49. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx`
|
||||
50. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx`
|
||||
51. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx`
|
||||
52. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
|
||||
53. `frontend-modern/src/components/Settings/SystemLogsPanel.tsx`
|
||||
54. `frontend-modern/src/components/Settings/useSystemLogsPanelState.ts`
|
||||
55. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx`
|
||||
56. `frontend-modern/src/features/operations/OperationsPageSurface.tsx`
|
||||
57. `frontend-modern/src/features/operations/operationsPageModel.ts`
|
||||
58. `frontend-modern/src/pages/Operations.tsx`
|
||||
59. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts`
|
||||
60. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx`
|
||||
61. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx`
|
||||
62. `frontend-modern/src/components/Settings/networkSettingsModel.ts`
|
||||
63. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts`
|
||||
64. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx`
|
||||
65. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
@@ -369,14 +374,22 @@ well: `frontend-modern/src/components/Settings/settingsHeaderMeta.ts` and
|
||||
representative top-level panels such as
|
||||
`frontend-modern/src/components/Settings/APIAccessPanel.tsx`,
|
||||
`frontend-modern/src/components/Settings/AISettings.tsx`,
|
||||
`frontend-modern/src/components/Settings/AIModelSelectionSection.tsx`,
|
||||
`frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx`,
|
||||
`frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx`,
|
||||
`frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx`,
|
||||
`frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx`,
|
||||
`frontend-modern/src/components/Settings/AISettingsDialogs.tsx`, and
|
||||
`frontend-modern/src/components/Settings/aiSettingsModel.ts` now also define
|
||||
the canonical AI settings runtime boundary. `AISettings.tsx` is the shell,
|
||||
`frontend-modern/src/components/Settings/useAISettingsState.ts` owns the
|
||||
runtime lifecycle and persistence flow, and provider configuration, modal
|
||||
flows, and provider metadata must extend those extracted owners instead of
|
||||
being re-inlined into the shell.
|
||||
runtime lifecycle and persistence flow, model/provider setup now routes
|
||||
through `AIModelSelectionSection.tsx`, discovery, budget, timeout, and
|
||||
permission controls route through `AIRuntimeControlsSection.tsx`, chat
|
||||
maintenance routes through `AIChatMaintenanceSection.tsx`, and readiness plus
|
||||
save/test actions route through `AISettingsStatusAndActions.tsx`. Future AI
|
||||
settings work must extend those section owners instead of re-inlining large
|
||||
runtime subsections into the shell.
|
||||
`frontend-modern/src/components/Settings/AuditLogPanel.tsx`,
|
||||
`frontend-modern/src/components/Settings/AuditWebhookPanel.tsx`,
|
||||
`frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx`,
|
||||
|
||||
@@ -1735,10 +1735,14 @@
|
||||
"frontend-modern/src/components/shared/"
|
||||
],
|
||||
"owned_files": [
|
||||
"frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AIModelSelectionSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AISettings.tsx",
|
||||
"frontend-modern/src/components/Settings/AISettingsDialogs.tsx",
|
||||
"frontend-modern/src/components/Settings/aiSettingsModel.ts",
|
||||
"frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx",
|
||||
"frontend-modern/src/components/Settings/APIAccessPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/AuditLogPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx",
|
||||
@@ -1817,10 +1821,14 @@
|
||||
"label": "settings shell framing proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AIModelSelectionSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AIProviderConfigurationSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AISettings.tsx",
|
||||
"frontend-modern/src/components/Settings/AISettingsDialogs.tsx",
|
||||
"frontend-modern/src/components/Settings/aiSettingsModel.ts",
|
||||
"frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx",
|
||||
"frontend-modern/src/components/Settings/APIAccessPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/AuditLogPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx",
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Component, For, Show } from 'solid-js';
|
||||
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
|
||||
import {
|
||||
getAIChatSessionsEmptyState,
|
||||
getAIChatSessionsLoadingState,
|
||||
} from '@/utils/aiSettingsPresentation';
|
||||
|
||||
interface AIChatMaintenanceSectionProps {
|
||||
state: AISettingsState;
|
||||
}
|
||||
|
||||
export const AIChatMaintenanceSection: Component<AIChatMaintenanceSectionProps> = (props) => {
|
||||
const { state } = props;
|
||||
|
||||
return (
|
||||
<div class="border border-border rounded-md overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full min-h-10 sm:min-h-9 px-3 py-2.5 flex items-center justify-between bg-surface-alt hover:bg-surface-hover transition-colors text-left"
|
||||
onClick={() => {
|
||||
const next = !state.showChatMaintenance();
|
||||
state.setShowChatMaintenance(next);
|
||||
if (next) {
|
||||
state.loadChatSessions();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-base-content">Chat Session Maintenance</span>
|
||||
</div>
|
||||
<svg
|
||||
class={`w-4 h-4 transition-transform ${state.showChatMaintenance() ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<Show when={state.showChatMaintenance()}>
|
||||
<div class="px-3 py-3 bg-surface border-t border-border space-y-3">
|
||||
<p class="text-xs text-muted">
|
||||
Use this panel to summarize, inspect, or revert a specific chat session. It does not
|
||||
change your default Pulse Assistant settings.
|
||||
</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium text-muted">Session</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.loadChatSessions}
|
||||
disabled={state.chatSessionsLoading()}
|
||||
class="inline-flex min-h-10 sm:min-h-9 items-center rounded-md px-2 py-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 disabled:opacity-50"
|
||||
>
|
||||
{state.chatSessionsLoading() ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={state.chatSessionsLoading()}>
|
||||
<div class="text-xs text-muted">{getAIChatSessionsLoadingState().text}</div>
|
||||
</Show>
|
||||
<Show when={!state.chatSessionsLoading()}>
|
||||
<Show when={state.chatSessionsError()}>
|
||||
<div class="text-xs text-red-500">{state.chatSessionsError()}</div>
|
||||
</Show>
|
||||
<Show when={!state.chatSessionsError()}>
|
||||
<Show
|
||||
when={state.chatSessions().length > 0}
|
||||
fallback={<div class="text-xs text-muted">{getAIChatSessionsEmptyState().text}</div>}
|
||||
>
|
||||
<select
|
||||
value={state.selectedSessionId()}
|
||||
onChange={(e) => state.setSelectedSessionId(e.currentTarget.value)}
|
||||
class="w-full min-h-10 sm:min-h-9 px-2 py-2 text-sm border border-border rounded"
|
||||
disabled={state.saving()}
|
||||
>
|
||||
<For each={state.chatSessions()}>
|
||||
{(session) => <option value={session.id}>{state.formatSessionLabel(session)}</option>}
|
||||
</For>
|
||||
</select>
|
||||
<Show when={state.selectedChatSession()}>
|
||||
<p class="text-[10px] text-muted mt-1">
|
||||
Last updated {new Date(state.selectedChatSession()!.updated_at).toLocaleString()}
|
||||
</p>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-wrap gap-2 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.handleSessionSummarize}
|
||||
disabled={!state.selectedSessionId() || state.sessionActionLoading() !== null}
|
||||
class="w-full sm:w-auto min-h-10 sm:min-h-9 px-3 py-2 text-sm font-medium rounded border border-border bg-surface text-base-content hover:bg-surface-hover disabled:opacity-50"
|
||||
>
|
||||
{state.sessionActionLoading() === 'summarize' ? 'Summarizing...' : 'Summarize context'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.handleSessionDiff}
|
||||
disabled={!state.selectedSessionId() || state.sessionActionLoading() !== null}
|
||||
class="w-full sm:w-auto min-h-10 sm:min-h-9 px-3 py-2 text-sm font-medium rounded border border-border bg-surface text-base-content hover:bg-surface-hover disabled:opacity-50"
|
||||
>
|
||||
{state.sessionActionLoading() === 'diff' ? 'Loading...' : 'View file changes'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.handleSessionRevert}
|
||||
disabled={!state.selectedSessionId() || state.sessionActionLoading() !== null}
|
||||
class="w-full sm:w-auto min-h-10 sm:min-h-9 px-3 py-2 text-sm font-medium rounded border border-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900 text-red-700 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-900 disabled:opacity-50"
|
||||
>
|
||||
{state.sessionActionLoading() === 'revert' ? 'Reverting...' : 'Revert changes'}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-[10px] text-muted">These actions apply to the selected chat session only.</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,305 @@
|
||||
import { Component, For, Show } from 'solid-js';
|
||||
import { AIProviderConfigurationSection } from '@/components/Settings/AIProviderConfigurationSection';
|
||||
import {
|
||||
groupModelsByProvider,
|
||||
isAIProviderConfigured,
|
||||
isModelProviderConfigured,
|
||||
} from '@/components/Settings/aiSettingsModel';
|
||||
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
|
||||
import { formField, labelClass, controlClass } from '@/components/shared/Form';
|
||||
import { getAIProviderDisplayName, getProviderFromModelId } from '@/utils/aiProviderPresentation';
|
||||
|
||||
interface AIModelSelectionSectionProps {
|
||||
state: AISettingsState;
|
||||
}
|
||||
|
||||
export const AIModelSelectionSection: Component<AIModelSelectionSectionProps> = (props) => {
|
||||
const { state } = props;
|
||||
const groupedModels = () => Array.from(groupModelsByProvider(state.availableModels()).entries());
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class={formField}>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class={labelClass()}>
|
||||
Default Model
|
||||
{state.modelsLoading() && <span class="ml-2 text-xs text-slate-500">(loading...)</span>}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.loadModels}
|
||||
disabled={state.modelsLoading()}
|
||||
class="inline-flex min-h-10 sm:min-h-9 items-center gap-1 rounded-md px-2 py-1.5 text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 disabled:opacity-50"
|
||||
title="Refresh model list from all configured providers"
|
||||
>
|
||||
<svg
|
||||
class={`w-3 h-3 ${state.modelsLoading() ? 'animate-spin' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<Show
|
||||
when={state.availableModels().length > 0}
|
||||
fallback={
|
||||
<input
|
||||
type="text"
|
||||
value={state.form.model}
|
||||
onInput={(e) => state.setForm('model', e.currentTarget.value)}
|
||||
placeholder="Configure a provider below to see available models"
|
||||
class={controlClass()}
|
||||
disabled={state.saving()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<select
|
||||
value={state.form.model}
|
||||
onChange={(e) => state.setForm('model', e.currentTarget.value)}
|
||||
class={controlClass()}
|
||||
disabled={state.saving()}
|
||||
>
|
||||
<Show
|
||||
when={
|
||||
!state.form.model ||
|
||||
!state.availableModels().some((model) => model.id === state.form.model)
|
||||
}
|
||||
>
|
||||
<option value={state.form.model}>{state.form.model || 'Select a model...'}</option>
|
||||
</Show>
|
||||
<For
|
||||
each={groupedModels().filter(([provider]) =>
|
||||
isAIProviderConfigured(provider, state.settings()),
|
||||
)}
|
||||
>
|
||||
{([provider, models]) => (
|
||||
<optgroup label={getAIProviderDisplayName(provider) || provider}>
|
||||
<For each={models}>
|
||||
{(model) => (
|
||||
<option value={model.id} selected={model.id === state.form.model}>
|
||||
{model.name || model.id.split(':').pop()}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</optgroup>
|
||||
)}
|
||||
</For>
|
||||
<For
|
||||
each={groupedModels().filter(([provider]) =>
|
||||
!isAIProviderConfigured(provider, state.settings()),
|
||||
)}
|
||||
>
|
||||
{([provider, models]) => (
|
||||
<optgroup
|
||||
label={`${getAIProviderDisplayName(provider) || provider} (not configured)`}
|
||||
>
|
||||
<For each={models}>
|
||||
{(model) => (
|
||||
<option
|
||||
value={model.id}
|
||||
selected={model.id === state.form.model}
|
||||
class="text-slate-400"
|
||||
>
|
||||
{model.name || model.id.split(':').pop()}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</optgroup>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</Show>
|
||||
<Show when={state.modelsError()}>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1 flex items-center gap-1">
|
||||
<svg
|
||||
class="w-3.5 h-3.5 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
Failed to load models: {state.modelsError()}. Enter a model ID manually (format:
|
||||
provider:model-name) or click Refresh to retry.
|
||||
</p>
|
||||
</Show>
|
||||
<Show when={state.form.model && !isModelProviderConfigured(state.form.model, state.settings())}>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400 mt-1 flex items-center gap-1">
|
||||
<svg
|
||||
class="w-3.5 h-3.5 flex-shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
This model requires{' '}
|
||||
{getAIProviderDisplayName(getProviderFromModelId(state.form.model)) ||
|
||||
getProviderFromModelId(state.form.model)}{' '}
|
||||
to be configured. Add an API key below or select a different model.
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="border border-border rounded-md overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full min-h-10 sm:min-h-9 px-3 py-2.5 flex items-center justify-between bg-surface-alt hover:bg-surface-hover transition-colors text-left"
|
||||
onClick={() => state.setShowAdvancedModels(!state.showAdvancedModels())}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-base-content">Advanced Model Selection</span>
|
||||
<Show when={state.form.chatModel || state.form.patrolModel}>
|
||||
<span class="px-1.5 py-0.5 text-[10px] font-semibold bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded">
|
||||
Customized
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<svg
|
||||
class={`w-4 h-4 text-slate-500 transition-transform ${state.showAdvancedModels() ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<Show when={state.showAdvancedModels()}>
|
||||
<div class="px-3 py-3 bg-surface border-t border-border space-y-3">
|
||||
<p class="text-xs text-muted">
|
||||
Override the default model for specific tasks. Leave empty to use the default.
|
||||
</p>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-muted mb-0.5">Chat Model (Interactive)</label>
|
||||
<p class="text-[11px] text-muted mb-1">
|
||||
Used for chat and fix execution — a more capable model is recommended.
|
||||
</p>
|
||||
<Show
|
||||
when={state.availableModels().length > 0}
|
||||
fallback={
|
||||
<input
|
||||
type="text"
|
||||
value={state.form.chatModel}
|
||||
onInput={(e) => state.setForm('chatModel', e.currentTarget.value)}
|
||||
placeholder="Use default model"
|
||||
class={controlClass()}
|
||||
disabled={state.saving()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<select
|
||||
value={state.form.chatModel}
|
||||
onChange={(e) => state.setForm('chatModel', e.currentTarget.value)}
|
||||
class={controlClass()}
|
||||
disabled={state.saving()}
|
||||
>
|
||||
<option value="">
|
||||
Use default ({state.form.model?.split(':').pop() || 'not set'})
|
||||
</option>
|
||||
<For each={groupedModels()}>
|
||||
{([provider, models]) => (
|
||||
<optgroup label={getAIProviderDisplayName(provider) || provider}>
|
||||
<For each={models}>
|
||||
{(model) => (
|
||||
<option value={model.id}>{model.name || model.id.split(':').pop()}</option>
|
||||
)}
|
||||
</For>
|
||||
</optgroup>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</Show>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-muted mb-0.5">Patrol Model (Background)</label>
|
||||
<p class="text-[11px] text-muted mb-1">
|
||||
Runs frequently for detection — a smaller, cheaper model keeps costs low.
|
||||
</p>
|
||||
<Show
|
||||
when={state.availableModels().length > 0}
|
||||
fallback={
|
||||
<input
|
||||
type="text"
|
||||
value={state.form.patrolModel}
|
||||
onInput={(e) => state.setForm('patrolModel', e.currentTarget.value)}
|
||||
placeholder="Use default model"
|
||||
class={controlClass()}
|
||||
disabled={state.saving()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<select
|
||||
value={state.form.patrolModel}
|
||||
onChange={(e) => state.setForm('patrolModel', e.currentTarget.value)}
|
||||
class={controlClass()}
|
||||
disabled={state.saving()}
|
||||
>
|
||||
<option value="">
|
||||
Use default ({state.form.model?.split(':').pop() || 'not set'})
|
||||
</option>
|
||||
<For each={groupedModels()}>
|
||||
{([provider, models]) => (
|
||||
<optgroup label={getAIProviderDisplayName(provider) || provider}>
|
||||
<For each={models}>
|
||||
{(model) => (
|
||||
<option value={model.id}>{model.name || model.id.split(':').pop()}</option>
|
||||
)}
|
||||
</For>
|
||||
</optgroup>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class={formField}>
|
||||
<AIProviderConfigurationSection
|
||||
settings={state.settings}
|
||||
form={state.form}
|
||||
setForm={state.setForm}
|
||||
expandedProviders={state.expandedProviders}
|
||||
setExpandedProviders={state.setExpandedProviders}
|
||||
providerHealth={state.providerHealth}
|
||||
preflightRunning={state.preflightRunning}
|
||||
preflightLastCheckedAt={state.preflightLastCheckedAt}
|
||||
providerIssueCount={state.providerIssueCount}
|
||||
testingProvider={state.testingProvider}
|
||||
providerTestResult={state.providerTestResult}
|
||||
saving={state.saving}
|
||||
runProviderPreflight={() => state.runProviderPreflight()}
|
||||
handleTestProvider={state.handleTestProvider}
|
||||
handleClearProvider={state.handleClearProvider}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,281 @@
|
||||
import { Component, Show } from 'solid-js';
|
||||
import type { AIControlLevel } from '@/utils/aiControlLevelPresentation';
|
||||
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
|
||||
import { HelpIcon } from '@/components/shared/HelpIcon';
|
||||
import { Toggle } from '@/components/shared/Toggle';
|
||||
import {
|
||||
getAIControlLevelBadgeClass,
|
||||
getAIControlLevelDescription,
|
||||
getAIControlLevelPanelClass,
|
||||
} from '@/utils/aiControlLevelPresentation';
|
||||
import { trackUpgradeClicked } from '@/utils/upgradeMetrics';
|
||||
|
||||
interface AIRuntimeControlsSectionProps {
|
||||
state: AISettingsState;
|
||||
}
|
||||
|
||||
export const AIRuntimeControlsSection: Component<AIRuntimeControlsSectionProps> = (props) => {
|
||||
const { state } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="rounded-md border border-blue-200 dark:border-blue-800 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full min-h-10 sm:min-h-9 px-3 py-2.5 flex items-center justify-between bg-blue-50 dark:bg-blue-900 hover:bg-blue-100 dark:hover:bg-blue-900 transition-colors text-left"
|
||||
onClick={() => state.setShowDiscoverySettings(!state.showDiscoverySettings())}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-base-content">Discovery Settings</span>
|
||||
<Show when={state.form.discoveryEnabled}>
|
||||
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-blue-100 dark:bg-blue-800 text-blue-700 dark:text-blue-300 rounded">
|
||||
{state.form.discoveryIntervalHours > 0
|
||||
? `${state.form.discoveryIntervalHours}h`
|
||||
: 'Manual'}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={!state.form.discoveryEnabled}>
|
||||
<span class="px-1.5 py-0.5 text-[10px] font-medium bg-surface-hover text-muted rounded">
|
||||
Off
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<svg
|
||||
class={`w-4 h-4 transition-transform ${state.showDiscoverySettings() ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<Show when={state.showDiscoverySettings()}>
|
||||
<div class="px-3 py-3 bg-surface border-t border-border space-y-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<label class="text-xs font-medium text-muted flex items-center gap-1.5">
|
||||
Enable Discovery
|
||||
<HelpIcon
|
||||
inline={{
|
||||
title: 'What is Discovery?',
|
||||
description:
|
||||
'Discovery scans your VMs, containers, and container runtimes to identify what services are running (databases, web servers, etc.), their versions, and how to access them. This information helps Pulse AI give you accurate troubleshooting commands and understand your infrastructure.',
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
</label>
|
||||
<Toggle
|
||||
checked={state.form.discoveryEnabled}
|
||||
onChange={(event) => state.setForm('discoveryEnabled', event.currentTarget.checked)}
|
||||
disabled={state.saving()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show when={state.form.discoveryEnabled}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs font-medium text-muted w-32 flex-shrink-0">Scan Interval</label>
|
||||
<select
|
||||
class="flex-1 px-2 py-1 text-sm border border-border rounded bg-surface"
|
||||
value={state.form.discoveryIntervalHours}
|
||||
onChange={(e) =>
|
||||
state.setForm('discoveryIntervalHours', parseInt(e.currentTarget.value, 10))
|
||||
}
|
||||
disabled={state.saving()}
|
||||
>
|
||||
<option value={0}>Manual only</option>
|
||||
<option value={6}>Every 6 hours</option>
|
||||
<option value={12}>Every 12 hours</option>
|
||||
<option value={24}>Every 24 hours</option>
|
||||
<option value={48}>Every 2 days</option>
|
||||
<option value={168}>Every 7 days</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-[10px] text-muted ml-32 pl-3">
|
||||
{state.form.discoveryIntervalHours === 0
|
||||
? 'Discovery runs only when you click "Update Discovery" on a resource'
|
||||
: 'Discovery will automatically re-scan resources at this interval'}
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<p class="text-[10px] text-muted">
|
||||
Discovery gives Pulse AI workload context, so responses can reference concrete
|
||||
services and commands instead of generic advice.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 p-3 rounded-md border border-border bg-surface-alt">
|
||||
<svg class="w-4 h-4 text-muted flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<label class="text-xs font-medium text-base-content">30-day Budget</label>
|
||||
<div class="relative flex-shrink-0">
|
||||
<span class="absolute left-2 top-1/2 -translate-y-1/2 text-muted text-xs">$</span>
|
||||
<input
|
||||
type="number"
|
||||
class="w-24 min-h-10 sm:min-h-9 pl-5 pr-2 py-2 text-sm border border-border rounded bg-surface"
|
||||
value={state.form.costBudgetUSD30d}
|
||||
onInput={(e) => state.setForm('costBudgetUSD30d', e.currentTarget.value)}
|
||||
min={0}
|
||||
step={1}
|
||||
placeholder="0"
|
||||
disabled={state.saving()}
|
||||
/>
|
||||
</div>
|
||||
<Show when={parseFloat(state.form.costBudgetUSD30d) > 0}>
|
||||
<span class="text-xs">≈ ${(parseFloat(state.form.costBudgetUSD30d) / 30).toFixed(2)}/day</span>
|
||||
</Show>
|
||||
<Show when={!state.form.costBudgetUSD30d || parseFloat(state.form.costBudgetUSD30d) === 0}>
|
||||
<span class="text-[10px] text-muted">Set a budget to receive usage alerts</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 p-3 rounded-md border border-border bg-surface-alt">
|
||||
<svg class="w-4 h-4 text-muted flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<label class="text-xs font-medium text-base-content">Request Timeout</label>
|
||||
<input
|
||||
type="number"
|
||||
class="w-20 min-h-10 sm:min-h-9 px-2 py-2 text-sm border border-border rounded bg-surface"
|
||||
value={state.form.requestTimeoutSeconds}
|
||||
onInput={(e) => {
|
||||
const value = parseInt(e.currentTarget.value, 10);
|
||||
if (!isNaN(value) && value > 0) {
|
||||
state.setForm('requestTimeoutSeconds', value);
|
||||
}
|
||||
}}
|
||||
min={30}
|
||||
max={3600}
|
||||
step={30}
|
||||
disabled={state.saving()}
|
||||
/>
|
||||
<span class="text-xs">seconds</span>
|
||||
<Show when={state.form.requestTimeoutSeconds !== 300}>
|
||||
<span class="text-[10px] text-blue-600 dark:text-blue-400">Custom</span>
|
||||
</Show>
|
||||
<Show when={state.form.requestTimeoutSeconds === 300}>
|
||||
<span class="text-[10px] text-muted">default</span>
|
||||
</Show>
|
||||
</div>
|
||||
<p class="text-[10px] text-muted -mt-4 ml-1">
|
||||
Increase for slower Ollama hardware (default: 300s / 5 min)
|
||||
</p>
|
||||
|
||||
<div class={`space-y-3 p-4 rounded-md border ${getAIControlLevelPanelClass(state.form.controlLevel)}`}>
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-base-content">Pulse Permission Level</span>
|
||||
<Show when={state.form.controlLevel !== 'read_only'}>
|
||||
<span class={`px-1.5 py-0.5 text-[10px] font-medium rounded ${getAIControlLevelBadgeClass(state.form.controlLevel)}`}>
|
||||
{state.form.controlLevel}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-xs font-medium text-muted w-28 flex-shrink-0">Permission</label>
|
||||
<select
|
||||
value={state.form.controlLevel}
|
||||
onChange={(e) => state.setForm('controlLevel', e.currentTarget.value as AIControlLevel)}
|
||||
class="flex-1 min-h-10 sm:min-h-9 px-2 py-2 text-sm border border-border rounded bg-surface"
|
||||
disabled={state.saving()}
|
||||
>
|
||||
<option value="read_only">Read Only - Pulse Assistant can only observe</option>
|
||||
<option value="controlled">Controlled - Pulse Assistant executes with your approval</option>
|
||||
<option value="autonomous">Autonomous - Pulse Assistant executes without approval (Pro)</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="text-[10px] text-muted ml-[7.5rem]">
|
||||
{getAIControlLevelDescription(state.form.controlLevel)}
|
||||
</p>
|
||||
<Show when={state.form.controlLevel === 'autonomous'}>
|
||||
<div class="p-2 bg-amber-100 dark:bg-amber-900 rounded border border-amber-200 dark:border-amber-800 text-[10px] text-amber-800 dark:text-amber-200">
|
||||
<strong>Legal Disclaimer:</strong> Model-driven systems can hallucinate. You are
|
||||
responsible for any damage caused by autonomous actions. See{' '}
|
||||
<a
|
||||
href="https://github.com/rcourtman/Pulse/blob/main/TERMS.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex min-h-10 sm:min-h-9 items-center rounded px-1 underline"
|
||||
>
|
||||
Terms of Service
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={state.form.controlLevel === 'autonomous' && state.autoFixLocked()}>
|
||||
<p class="text-xs text-muted">
|
||||
<a
|
||||
class="text-blue-600 dark:text-blue-400 font-medium hover:underline"
|
||||
href={state.upgradeAutofixUrl()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => trackUpgradeClicked('settings_ai_patrol_autofix', 'ai_autofix')}
|
||||
>
|
||||
Upgrade to Pro
|
||||
</a>{' '}
|
||||
to enable autonomous mode.
|
||||
<Show when={state.canStartTrial()}>
|
||||
{' '}
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.handleStartTrial}
|
||||
disabled={state.startingTrial()}
|
||||
class="text-indigo-500 hover:underline disabled:opacity-50"
|
||||
>
|
||||
Start free trial
|
||||
</button>
|
||||
</Show>
|
||||
</p>
|
||||
</Show>
|
||||
|
||||
<Show when={state.form.controlLevel !== 'read_only'}>
|
||||
<div class="flex items-start gap-3 pt-2 border-t border-blue-200 dark:border-blue-700">
|
||||
<label class="text-xs font-medium text-muted w-28 flex-shrink-0 pt-1">Protected</label>
|
||||
<div class="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={state.form.protectedGuests}
|
||||
onInput={(e) => state.setForm('protectedGuests', e.currentTarget.value)}
|
||||
placeholder="e.g., 100, 101, prod-db"
|
||||
class="w-full min-h-10 sm:min-h-9 px-2 py-2 text-sm border border-border rounded"
|
||||
disabled={state.saving()}
|
||||
/>
|
||||
<p class="text-[10px] text-muted mt-1">
|
||||
Comma-separated VMIDs or names that Pulse Assistant cannot control
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
import { Component, Show } from 'solid-js';
|
||||
import type { AISettingsState } from '@/components/Settings/useAISettingsState';
|
||||
|
||||
interface AISettingsStatusAndActionsProps {
|
||||
state: AISettingsState;
|
||||
}
|
||||
|
||||
export const AISettingsStatusAndActions: Component<AISettingsStatusAndActionsProps> = (props) => {
|
||||
const { state } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={state.settings()}>
|
||||
<div class="p-4 sm:p-6">
|
||||
<div
|
||||
class={`flex items-center gap-2 p-3 rounded-md ${state.settingsReadiness().containerClassName}`}
|
||||
>
|
||||
<div class={`w-2 h-2 rounded-full ${state.settingsReadiness().dotClassName}`} />
|
||||
<div class="flex-1 min-w-0">
|
||||
<span class="text-xs font-medium">{state.settingsReadiness().summary}</span>
|
||||
<Show when={state.settings()?.configured && state.settings()?.model}>
|
||||
<span class="block sm:inline text-xs opacity-75 sm:ml-2">
|
||||
• Default: {state.settings()?.model?.split(':').pop() || state.settings()?.model}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="sticky bottom-0 bg-surface px-4 sm:px-6 py-4 flex flex-col sm:flex-row sm:flex-wrap sm:items-center sm:justify-between gap-3">
|
||||
<Show when={state.settings()?.configured}>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full sm:w-auto min-h-10 sm:min-h-9 px-4 py-2.5 text-sm border border-blue-300 dark:border-blue-700 text-blue-700 dark:text-blue-300 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={state.handleTest}
|
||||
disabled={state.testing() || state.saving() || state.loading()}
|
||||
>
|
||||
{state.testing() ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
</Show>
|
||||
<div class="grid grid-cols-1 sm:flex gap-3 w-full sm:w-auto sm:ml-auto">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full sm:w-auto min-h-10 sm:min-h-9 px-4 py-2.5 border border-border text-base-content rounded-md hover:bg-surface-hover"
|
||||
onClick={() => state.resetForm(state.settings())}
|
||||
disabled={state.saving() || state.loading()}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full sm:w-auto min-h-10 sm:min-h-9 px-4 py-2.5 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={state.saving() || state.loading()}
|
||||
>
|
||||
{state.saving() ? 'Saving...' : 'Save changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+16
@@ -38,9 +38,13 @@ import alertThresholdsSectionPresentationSource from '@/utils/alertThresholdsSec
|
||||
import loginSource from '@/components/Login.tsx?raw';
|
||||
import settingsSource from '../Settings.tsx?raw';
|
||||
import aiSettingsShellSource from '../AISettings.tsx?raw';
|
||||
import aiChatMaintenanceSectionSource from '../AIChatMaintenanceSection.tsx?raw';
|
||||
import aiProviderConfigurationSectionSource from '../AIProviderConfigurationSection.tsx?raw';
|
||||
import aiSettingsDialogsSource from '../AISettingsDialogs.tsx?raw';
|
||||
import aiModelSelectionSectionSource from '../AIModelSelectionSection.tsx?raw';
|
||||
import aiSettingsModelSource from '../aiSettingsModel.ts?raw';
|
||||
import aiRuntimeControlsSectionSource from '../AIRuntimeControlsSection.tsx?raw';
|
||||
import aiSettingsStatusAndActionsSource from '../AISettingsStatusAndActions.tsx?raw';
|
||||
import aiSettingsStateSource from '../useAISettingsState.ts?raw';
|
||||
import reportingPanelSource from '../ReportingPanel.tsx?raw';
|
||||
import reportingPanelModelSource from '../reportingPanelModel.ts?raw';
|
||||
@@ -111,6 +115,10 @@ import workloadTypePresentationSource from '@/utils/workloadTypePresentation.ts?
|
||||
|
||||
const aiSettingsSource = [
|
||||
aiSettingsShellSource,
|
||||
aiModelSelectionSectionSource,
|
||||
aiRuntimeControlsSectionSource,
|
||||
aiChatMaintenanceSectionSource,
|
||||
aiSettingsStatusAndActionsSource,
|
||||
aiProviderConfigurationSectionSource,
|
||||
aiSettingsDialogsSource,
|
||||
aiSettingsModelSource,
|
||||
@@ -414,6 +422,10 @@ describe('monitored-system model guardrails', () => {
|
||||
expect(aiSettingsSource).toContain('getAICredentialsClearErrorMessage');
|
||||
expect(aiSettingsSource).toContain('getAISettingsToggleErrorMessage');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/useAISettingsState');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/AIModelSelectionSection');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/AIRuntimeControlsSection');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/AIChatMaintenanceSection');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/AISettingsStatusAndActions');
|
||||
expect(aiSettingsSource).not.toContain('Loading Pulse Assistant settings...');
|
||||
expect(aiSettingsSource).not.toContain('Loading chat sessions...');
|
||||
expect(aiSettingsSource).not.toContain('No chat sessions yet. Start a chat to create one.');
|
||||
@@ -427,7 +439,11 @@ describe('monitored-system model guardrails', () => {
|
||||
expect(aiSettingsSource).not.toContain('const errorMessages: Record<string, string> =');
|
||||
expect(aiSettingsShellSource).not.toContain('const [loading, setLoading] = createSignal(false);');
|
||||
expect(aiSettingsShellSource).not.toContain('const handleSave = async (event?: Event) =>');
|
||||
expect(aiSettingsShellSource).not.toContain('Chat Session Maintenance');
|
||||
expect(aiSettingsShellSource).not.toContain('Discovery Settings');
|
||||
expect(aiSettingsShellSource).not.toContain('Pulse Permission Level');
|
||||
expect(aiSettingsStateSource).toContain('export const useAISettingsState =');
|
||||
expect(aiSettingsStateSource).toContain('export type AISettingsState =');
|
||||
expect(aiSettingsStateSource).toContain('const [loading, setLoading] = createSignal(false);');
|
||||
expect(aiSettingsStateSource).toContain('const handleSave = async (event?: Event) =>');
|
||||
expect(aiSettingsStateSource).toContain('const handleEnabledToggle = async (newValue: boolean) =>');
|
||||
|
||||
@@ -32,9 +32,13 @@ import billingAdminPanelSource from '../BillingAdminPanel.tsx?raw';
|
||||
import billingAdminPanelStateSource from '../useBillingAdminPanelState.ts?raw';
|
||||
import generalSettingsPanelSource from '../GeneralSettingsPanel.tsx?raw';
|
||||
import aiSettingsPanelSource from '../AISettings.tsx?raw';
|
||||
import aiChatMaintenanceSectionSource from '../AIChatMaintenanceSection.tsx?raw';
|
||||
import aiProviderConfigurationSectionSource from '../AIProviderConfigurationSection.tsx?raw';
|
||||
import aiSettingsDialogsSource from '../AISettingsDialogs.tsx?raw';
|
||||
import aiModelSelectionSectionSource from '../AIModelSelectionSection.tsx?raw';
|
||||
import aiSettingsModelSource from '../aiSettingsModel.ts?raw';
|
||||
import aiRuntimeControlsSectionSource from '../AIRuntimeControlsSection.tsx?raw';
|
||||
import aiSettingsStatusAndActionsSource from '../AISettingsStatusAndActions.tsx?raw';
|
||||
import aiSettingsStateSource from '../useAISettingsState.ts?raw';
|
||||
import diagnosticsModelSource from '../diagnosticsModel.ts?raw';
|
||||
import diagnosticsPanelSource from '../DiagnosticsPanel.tsx?raw';
|
||||
@@ -135,6 +139,10 @@ const extractedModules = [
|
||||
'../InfrastructureStopMonitoringDialog.tsx',
|
||||
'../AIProviderConfigurationSection.tsx',
|
||||
'../AISettingsDialogs.tsx',
|
||||
'../AIChatMaintenanceSection.tsx',
|
||||
'../AIModelSelectionSection.tsx',
|
||||
'../AIRuntimeControlsSection.tsx',
|
||||
'../AISettingsStatusAndActions.tsx',
|
||||
'../aiSettingsModel.ts',
|
||||
'../useAISettingsState.ts',
|
||||
'../diagnosticsModel.ts',
|
||||
@@ -734,13 +742,26 @@ describe('Settings architecture guardrails', () => {
|
||||
});
|
||||
|
||||
it('keeps AI settings sub-surfaces behind extracted runtime owners', () => {
|
||||
expect(aiSettingsPanelSource).toContain('@/components/Settings/AIProviderConfigurationSection');
|
||||
expect(aiSettingsPanelSource).toContain('@/components/Settings/AIModelSelectionSection');
|
||||
expect(aiSettingsPanelSource).toContain('@/components/Settings/AIRuntimeControlsSection');
|
||||
expect(aiSettingsPanelSource).toContain('@/components/Settings/AIChatMaintenanceSection');
|
||||
expect(aiSettingsPanelSource).toContain('@/components/Settings/AISettingsStatusAndActions');
|
||||
expect(aiSettingsPanelSource).toContain('@/components/Settings/AISettingsDialogs');
|
||||
expect(aiSettingsPanelSource).toContain('@/components/Settings/aiSettingsModel');
|
||||
expect(aiSettingsPanelSource).toContain('@/components/Settings/useAISettingsState');
|
||||
expect(aiSettingsPanelSource).not.toContain('const [loading, setLoading] = createSignal(false);');
|
||||
expect(aiSettingsPanelSource).not.toContain('const handleSave = async (event?: Event) =>');
|
||||
expect(aiSettingsPanelSource).not.toContain('AIAPI.getSettings()');
|
||||
expect(aiSettingsPanelSource).not.toContain('Chat Session Maintenance');
|
||||
expect(aiSettingsPanelSource).not.toContain('Discovery Settings');
|
||||
expect(aiSettingsPanelSource).not.toContain('Pulse Permission Level');
|
||||
expect(aiModelSelectionSectionSource).toContain('@/components/Settings/AIProviderConfigurationSection');
|
||||
expect(aiModelSelectionSectionSource).toContain('@/components/Settings/aiSettingsModel');
|
||||
expect(aiModelSelectionSectionSource).toContain('Advanced Model Selection');
|
||||
expect(aiRuntimeControlsSectionSource).toContain('Discovery Settings');
|
||||
expect(aiRuntimeControlsSectionSource).toContain('Pulse Permission Level');
|
||||
expect(aiChatMaintenanceSectionSource).toContain('Chat Session Maintenance');
|
||||
expect(aiSettingsStatusAndActionsSource).toContain('Save changes');
|
||||
expect(aiSettingsStatusAndActionsSource).toContain('Test Connection');
|
||||
expect(aiProviderConfigurationSectionSource).toContain('@/components/Settings/aiSettingsModel');
|
||||
expect(aiSettingsDialogsSource).toContain('@/components/Settings/aiSettingsModel');
|
||||
expect(aiSettingsModelSource).toContain('export const AI_PROVIDER_CONFIGS');
|
||||
|
||||
@@ -879,3 +879,5 @@ export const useAISettingsState = () => {
|
||||
upgradeAutofixUrl,
|
||||
};
|
||||
};
|
||||
|
||||
export type AISettingsState = ReturnType<typeof useAISettingsState>;
|
||||
|
||||
@@ -343,9 +343,13 @@ import runToolCallTraceSource from '@/components/patrol/RunToolCallTrace.tsx?raw
|
||||
import diagnosticsPanelSource from '@/components/Settings/DiagnosticsPanel.tsx?raw';
|
||||
import diagnosticsPresentationSource from '@/utils/diagnosticsPresentation.ts?raw';
|
||||
import aiSettingsShellSource from '@/components/Settings/AISettings.tsx?raw';
|
||||
import aiChatMaintenanceSectionSource from '@/components/Settings/AIChatMaintenanceSection.tsx?raw';
|
||||
import aiProviderConfigurationSectionSource from '@/components/Settings/AIProviderConfigurationSection.tsx?raw';
|
||||
import aiSettingsDialogsSource from '@/components/Settings/AISettingsDialogs.tsx?raw';
|
||||
import aiModelSelectionSectionSource from '@/components/Settings/AIModelSelectionSection.tsx?raw';
|
||||
import aiSettingsModelSource from '@/components/Settings/aiSettingsModel.ts?raw';
|
||||
import aiRuntimeControlsSectionSource from '@/components/Settings/AIRuntimeControlsSection.tsx?raw';
|
||||
import aiSettingsStatusAndActionsSource from '@/components/Settings/AISettingsStatusAndActions.tsx?raw';
|
||||
import aiSettingsStateSource from '@/components/Settings/useAISettingsState.ts?raw';
|
||||
import aiIntelligenceSource from '@/pages/AIIntelligence.tsx?raw';
|
||||
import patrolIntelligenceSurfaceSource from '@/features/patrol/PatrolIntelligenceSurface.tsx?raw';
|
||||
@@ -371,6 +375,10 @@ import infrastructurePageStateSource from '@/features/infrastructure/useInfrastr
|
||||
|
||||
const aiSettingsSource = [
|
||||
aiSettingsShellSource,
|
||||
aiModelSelectionSectionSource,
|
||||
aiRuntimeControlsSectionSource,
|
||||
aiChatMaintenanceSectionSource,
|
||||
aiSettingsStatusAndActionsSource,
|
||||
aiProviderConfigurationSectionSource,
|
||||
aiSettingsDialogsSource,
|
||||
aiSettingsModelSource,
|
||||
@@ -2907,6 +2915,10 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(aiSettingsSource).toContain('getAICredentialsClearErrorMessage');
|
||||
expect(aiSettingsSource).toContain('getAISettingsToggleErrorMessage');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/useAISettingsState');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/AIModelSelectionSection');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/AIRuntimeControlsSection');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/AIChatMaintenanceSection');
|
||||
expect(aiSettingsShellSource).toContain('@/components/Settings/AISettingsStatusAndActions');
|
||||
expect(aiSettingsSource).not.toContain('Loading Pulse Assistant settings...');
|
||||
expect(aiSettingsSource).not.toContain('Loading chat sessions...');
|
||||
expect(aiSettingsSource).not.toContain('No chat sessions yet. Start a chat to create one.');
|
||||
@@ -2923,10 +2935,14 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(aiSettingsSource).not.toContain("'Failed to update Pulse Assistant setting'");
|
||||
expect(aiSettingsShellSource).not.toContain('const [loading, setLoading] = createSignal(false);');
|
||||
expect(aiSettingsShellSource).not.toContain('const handleSave = async (event?: Event) =>');
|
||||
expect(aiSettingsShellSource).not.toContain('Chat Session Maintenance');
|
||||
expect(aiSettingsShellSource).not.toContain('Discovery Settings');
|
||||
expect(aiSettingsShellSource).not.toContain('Pulse Permission Level');
|
||||
expect(aiSettingsSource).not.toContain(
|
||||
"providerTestResult()?.success ? 'text-green-600' : 'text-red-600'",
|
||||
);
|
||||
expect(aiSettingsStateSource).toContain('export const useAISettingsState =');
|
||||
expect(aiSettingsStateSource).toContain('export type AISettingsState =');
|
||||
expect(aiSettingsStateSource).toContain('const [loading, setLoading] = createSignal(false);');
|
||||
expect(aiSettingsStateSource).toContain('const handleSave = async (event?: Event) =>');
|
||||
expect(aiSettingsStateSource).toContain('const handleEnabledToggle = async (newValue: boolean) =>');
|
||||
|
||||
@@ -1856,7 +1856,11 @@ class SubsystemLookupTest(unittest.TestCase):
|
||||
def test_lookup_paths_assigns_remaining_settings_shell_panels_to_frontend_primitives(self) -> None:
|
||||
runtime_files = [
|
||||
"frontend-modern/src/components/Settings/APIAccessPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/AIChatMaintenanceSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AIModelSelectionSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx",
|
||||
"frontend-modern/src/components/Settings/AISettings.tsx",
|
||||
"frontend-modern/src/components/Settings/AISettingsStatusAndActions.tsx",
|
||||
"frontend-modern/src/components/Settings/AuditLogPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx",
|
||||
|
||||
Reference in New Issue
Block a user