Fold the assistant's panels away, and name the default it will use
Quick prompts and the assistant model panel both start collapsed now: the prompt box is what the copilot is opened for, and everything else is a detour until asked for. A saved session still reopens with whatever was last expanded, the same as the other collapsible sections. The assistant model panel gains the toggle it was already written for - modelPickerOpen and toggleModelPicker existed, were persisted in the session snapshot, and were never wired to the template, so the panel could not be folded at all. Ticking Use default configuration used to leave the panel empty: it said a default applied without saying which. The backend has been publishing defaultProvider, defaultModel and defaultPhaseModels alongside the retriever URLs all along, so nothing new was needed there - only the showing. The effective selection also appears in the section header, so the collapsed panel still answers what the run will use, and the per-phase lines list only the phases actually overridden rather than three rows repeating the main model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
37bf2cb853
commit
9e42ca154a
|
|
@ -242,6 +242,20 @@
|
|||
gap: 8px;
|
||||
}
|
||||
|
||||
/* What the backend will use when nothing is chosen here. */
|
||||
.assistant-defaults {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #dbe4ee;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.assistant-defaults .assistant-meta {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.assistant-add-credential {
|
||||
justify-self: start;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,10 +43,23 @@
|
|||
<div class="assistant-starters-head">
|
||||
<div>
|
||||
<p class="assistant-label">Assistant model</p>
|
||||
<!-- Says what the run will use, so the panel does not have to be opened to find out. -->
|
||||
@if (effectiveModelSummary(); as summary) {
|
||||
<p class="assistant-copy">{{ summary }}</p>
|
||||
} @else {
|
||||
<p class="assistant-copy">Provider credentials are managed by the backend.</p>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="assistant-section-toggle"
|
||||
(click)="toggleModelPicker()"
|
||||
[attr.aria-label]="modelPickerOpen() ? 'Collapse assistant model' : 'Expand assistant model'">
|
||||
{{ modelPickerOpen() ? '−' : '+' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (modelPickerOpen()) {
|
||||
<mat-checkbox
|
||||
[checked]="useDefaultConfiguration()"
|
||||
[disabled]="configurationLocked() || assistantBusy()"
|
||||
|
|
@ -58,6 +71,19 @@
|
|||
<p class="assistant-meta">The LLM configuration is locked for this session.</p>
|
||||
}
|
||||
|
||||
@if (useDefaultConfiguration()) {
|
||||
<div class="assistant-defaults">
|
||||
@if (defaultConfigurationSummary(); as summary) {
|
||||
<p class="assistant-meta"><strong>{{ summary }}</strong></p>
|
||||
} @else {
|
||||
<p class="assistant-meta">The backend has not published a default provider and model.</p>
|
||||
}
|
||||
@for (phase of defaultPhaseModelSummary(); track phase.label) {
|
||||
<p class="assistant-meta">{{ phase.label }}: {{ phase.model }}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!useDefaultConfiguration()) {
|
||||
<div class="assistant-settings-fields">
|
||||
<mat-form-field appearance="outline">
|
||||
|
|
@ -188,6 +214,7 @@
|
|||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</section>
|
||||
|
||||
@if (assistantErrorMessage()) {
|
||||
|
|
|
|||
|
|
@ -259,3 +259,145 @@ describe('FlowAssistant credentials', () => {
|
|||
expect(provider.options).toEqual([{ label: 'Gemini', value: 'Gemini' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FlowAssistant quick prompts', () => {
|
||||
function build() {
|
||||
const assistant = {
|
||||
getConfig: vi.fn(() => of({
|
||||
defaultProvider: '',
|
||||
defaultModel: '',
|
||||
availableProvidersRetrieverUrl: '/retriever/LLM/providers',
|
||||
availableModelsRetrieverUrl: '/retriever/LLM/models?provider={provider}',
|
||||
defaultPhaseModels: {},
|
||||
providerCatalogUrl: '/llm/providers'
|
||||
})),
|
||||
listProviders: vi.fn(() => of([])),
|
||||
listModels: vi.fn(() => of([])),
|
||||
areModelsOpen: vi.fn(() => of(false)),
|
||||
listProviderCatalog: vi.fn(() => of([]))
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: AssistantService, useValue: assistant },
|
||||
{ provide: NodeSettingsDialogService, useValue: { open: vi.fn((_input: unknown) => Promise.resolve(null)) } },
|
||||
{ provide: VaultService, useValue: { listSecrets: vi.fn(() => of([])) } },
|
||||
{
|
||||
provide: EditorStateHolder,
|
||||
useValue: { currentFlow: vi.fn(() => null), activeFlowData: vi.fn(() => null) }
|
||||
},
|
||||
{ provide: Authorization, useValue: { currentUser: vi.fn(() => null) } },
|
||||
{
|
||||
provide: AssistantSessionStore,
|
||||
useValue: {
|
||||
flowKey: vi.fn(() => 'flow-1'),
|
||||
getSnapshot: vi.fn(() => null),
|
||||
setSnapshot: vi.fn(),
|
||||
clearSnapshot: vi.fn(),
|
||||
cloneSnapshot: vi.fn((snapshot: unknown) => snapshot)
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return TestBed.createComponent(FlowAssistant).componentInstance as any;
|
||||
}
|
||||
|
||||
afterEach(() => TestBed.resetTestingModule());
|
||||
|
||||
it('starts collapsed, like the other collapsible sections', () => {
|
||||
expect(build().quickPromptsOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it('starts with the assistant model panel collapsed too', () => {
|
||||
// The signal and its toggle already existed; the template never used them, so the panel was
|
||||
// permanently expanded and there was no way to fold it away.
|
||||
expect(build().modelPickerOpen()).toBe(false);
|
||||
});
|
||||
|
||||
it('stays collapsed when a flow with no saved session is opened', async () => {
|
||||
const component = build();
|
||||
component.quickPromptsOpen.set(true);
|
||||
|
||||
await component.restoreSessionForFlow('flow-2');
|
||||
|
||||
expect(component.quickPromptsOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FlowAssistant default configuration', () => {
|
||||
function build(config: Record<string, unknown>) {
|
||||
const assistant = {
|
||||
getConfig: vi.fn(() => of(config)),
|
||||
listProviders: vi.fn(() => of([])),
|
||||
listModels: vi.fn(() => of([])),
|
||||
areModelsOpen: vi.fn(() => of(false)),
|
||||
listProviderCatalog: vi.fn(() => of([]))
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: AssistantService, useValue: assistant },
|
||||
{ provide: NodeSettingsDialogService, useValue: { open: vi.fn((_input: unknown) => Promise.resolve(null)) } },
|
||||
{ provide: VaultService, useValue: { listSecrets: vi.fn(() => of([])) } },
|
||||
{
|
||||
provide: EditorStateHolder,
|
||||
useValue: { currentFlow: vi.fn(() => null), activeFlowData: vi.fn(() => null) }
|
||||
},
|
||||
{ provide: Authorization, useValue: { currentUser: vi.fn(() => null) } },
|
||||
{
|
||||
provide: AssistantSessionStore,
|
||||
useValue: {
|
||||
flowKey: vi.fn(() => 'flow-1'),
|
||||
getSnapshot: vi.fn(() => null),
|
||||
setSnapshot: vi.fn(),
|
||||
clearSnapshot: vi.fn(),
|
||||
cloneSnapshot: vi.fn((snapshot: unknown) => snapshot)
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const component = TestBed.createComponent(FlowAssistant).componentInstance as any;
|
||||
component.assistantConfig.set(config);
|
||||
return component;
|
||||
}
|
||||
|
||||
afterEach(() => TestBed.resetTestingModule());
|
||||
|
||||
it('names the backend default while Use default configuration is on', () => {
|
||||
// Ticking the box said a default applied without ever saying which one.
|
||||
const component = build({ defaultProvider: 'InternalOllama', defaultModel: 'llama3.2:3b', defaultPhaseModels: {} });
|
||||
|
||||
expect(component.useDefaultConfiguration()).toBe(true);
|
||||
expect(component.defaultConfigurationSummary()).toBe('InternalOllama \u00b7 llama3.2:3b');
|
||||
expect(component.effectiveModelSummary()).toBe('Default: InternalOllama \u00b7 llama3.2:3b');
|
||||
});
|
||||
|
||||
it('lists only the phases the backend actually overrides', () => {
|
||||
const component = build({
|
||||
defaultProvider: 'InternalOllama',
|
||||
defaultModel: 'llama3.2:3b',
|
||||
defaultPhaseModels: { planningModel: 'qwen2.5:7b', jsonModel: ' ', repairModel: undefined }
|
||||
});
|
||||
|
||||
expect(component.defaultPhaseModelSummary()).toEqual([{ label: 'Planning', model: 'qwen2.5:7b' }]);
|
||||
});
|
||||
|
||||
it('says nothing rather than something empty when no default was published', () => {
|
||||
const component = build({ defaultProvider: '', defaultModel: '', defaultPhaseModels: {} });
|
||||
|
||||
expect(component.defaultConfigurationSummary()).toBeNull();
|
||||
expect(component.effectiveModelSummary()).toBeNull();
|
||||
});
|
||||
|
||||
it('summarises the chosen provider and model once the default is turned off', () => {
|
||||
const component = build({ defaultProvider: 'InternalOllama', defaultModel: 'llama3.2:3b', defaultPhaseModels: {} });
|
||||
|
||||
component.useDefaultConfiguration.set(false);
|
||||
component.selectedProvider.set('Gemini');
|
||||
component.selectedModel.set('gemini-2.5-flash');
|
||||
|
||||
expect(component.effectiveModelSummary()).toBe('Gemini \u00b7 gemini-2.5-flash');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -84,7 +84,8 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
readonly credentialSaving = signal(false);
|
||||
readonly advancedModelsOpen = signal(false);
|
||||
readonly modelPickerOpen = signal(false);
|
||||
readonly quickPromptsOpen = signal(true);
|
||||
/** Closed on arrival, like the other collapsible sections: the prompt box is what you came for. */
|
||||
readonly quickPromptsOpen = signal(false);
|
||||
readonly assistantErrorMessage = signal<string | null>(null);
|
||||
readonly lastFailedPrompt = signal<string | null>(null);
|
||||
readonly lastSubmittedPrompt = signal('');
|
||||
|
|
@ -205,6 +206,37 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
readonly currentFlow = this.editorState.currentFlow;
|
||||
readonly currentDraft = computed(() => this.sessionState()?.currentDraftFlow ?? null);
|
||||
readonly configurationLocked = computed(() => !!this.sessionState()?.id);
|
||||
/**
|
||||
* What the backend says it will use when nothing is chosen here. It has always been in the
|
||||
* config payload alongside the retriever URLs; the panel just never showed it, so ticking Use
|
||||
* default configuration told you a default applied without ever saying which.
|
||||
*/
|
||||
readonly defaultConfigurationSummary = computed(() => {
|
||||
const config = this.assistantConfig();
|
||||
const parts = [config?.defaultProvider?.trim(), config?.defaultModel?.trim()].filter((part) => !!part);
|
||||
return parts.length ? parts.join(' \u00b7 ') : null;
|
||||
});
|
||||
|
||||
/** The phases the backend overrides, if any. Silence here means they all use the main model. */
|
||||
readonly defaultPhaseModelSummary = computed(() => {
|
||||
const phases = this.assistantConfig()?.defaultPhaseModels;
|
||||
return [
|
||||
{ label: 'Planning', model: phases?.planningModel?.trim() },
|
||||
{ label: 'JSON', model: phases?.jsonModel?.trim() },
|
||||
{ label: 'Repair', model: phases?.repairModel?.trim() }
|
||||
].filter((entry): entry is { label: string; model: string } => !!entry.model);
|
||||
});
|
||||
|
||||
/** What the run will use, readable with the panel collapsed. */
|
||||
readonly effectiveModelSummary = computed(() => {
|
||||
if (this.useDefaultConfiguration()) {
|
||||
const summary = this.defaultConfigurationSummary();
|
||||
return summary ? `Default: ${summary}` : null;
|
||||
}
|
||||
const parts = [this.selectedProvider().trim(), this.selectedModel().trim()].filter((part) => !!part);
|
||||
return parts.length ? parts.join(' \u00b7 ') : null;
|
||||
});
|
||||
|
||||
readonly providerNeedsCredential = computed(() =>
|
||||
!!this.selectedProvider().trim() && !this.isInternalProvider(this.selectedProvider())
|
||||
);
|
||||
|
|
@ -892,7 +924,7 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
if (!snapshot) {
|
||||
this.prompt.set('');
|
||||
this.modelPickerOpen.set(false);
|
||||
this.quickPromptsOpen.set(true);
|
||||
this.quickPromptsOpen.set(false);
|
||||
this.sessionState.set(null);
|
||||
this.localMessages.set([]);
|
||||
this.assistantErrorMessage.set(null);
|
||||
|
|
|
|||
Loading…
Reference in New Issue