mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
Clear the opposing selection when the user picks a skill or scenario chip, and omit skillId when a scenario plugin is active on submit.
This commit is contained in:
parent
5b22143763
commit
19142b0d11
2 changed files with 149 additions and 2 deletions
|
|
@ -579,6 +579,7 @@ export function HomeView({
|
|||
) {
|
||||
const applyRequestId = activePluginApplyRequestRef.current + 1;
|
||||
activePluginApplyRequestRef.current = applyRequestId;
|
||||
setActiveSkill(null);
|
||||
const shouldResolveImmediately = options?.deferApply !== true;
|
||||
const inputFields = options?.inputFields ?? record.manifest?.od?.inputs ?? [];
|
||||
const optimisticInputs = hydratePluginInputs(
|
||||
|
|
@ -992,9 +993,13 @@ export function HomeView({
|
|||
}
|
||||
|
||||
function useSkill(skill: SkillSummary, nextPrompt: string | null) {
|
||||
setActiveSkill(skill);
|
||||
activePluginApplyRequestRef.current += 1;
|
||||
setActive(null);
|
||||
setPendingChipId(null);
|
||||
setPendingApplyId(null);
|
||||
setFallbackProjectKind(null);
|
||||
setFallbackProjectMetadata(null);
|
||||
setActiveSkill(skill);
|
||||
setError(null);
|
||||
const replacement = nextPrompt ?? localizeSkillPrompt(locale, skill) ?? '';
|
||||
if (replacement.trim().length > 0) {
|
||||
|
|
@ -1248,10 +1253,13 @@ export function HomeView({
|
|||
submittedActive?.inputs ?? null,
|
||||
submittedActive?.projectMetadata ?? fallbackProjectMetadata ?? null,
|
||||
);
|
||||
// Scenario plugins (chips / preset cards) and explicit skill picks are
|
||||
// mutually exclusive routing sources — never send both (#2972).
|
||||
const resolvedSkillId = submittedActive ? null : activeSkill?.id ?? null;
|
||||
onSubmit({
|
||||
prompt: trimmed,
|
||||
pluginId: submittedActive?.record.id ?? DEFAULT_UNSELECTED_SCENARIO_PLUGIN_ID,
|
||||
skillId: activeSkill?.id ?? null,
|
||||
skillId: resolvedSkillId,
|
||||
appliedPluginSnapshotId: submittedActive?.result?.appliedPlugin?.snapshotId ?? null,
|
||||
pluginTitle: submittedActive?.record.title ?? null,
|
||||
taskKind: submittedActive?.result?.appliedPlugin?.taskKind ?? null,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,18 @@ const SKILL: SkillSummary = {
|
|||
aggregatesExamples: false,
|
||||
};
|
||||
|
||||
const DECK_SKILL: SkillSummary = {
|
||||
...SKILL,
|
||||
id: 'deck-lab',
|
||||
name: 'Deck Lab',
|
||||
description: 'Create a focused slide deck.',
|
||||
triggers: ['deck', 'slides'],
|
||||
mode: 'deck',
|
||||
examplePrompt: 'Design a focused investor deck.',
|
||||
};
|
||||
|
||||
const WEB_PROTOTYPE_PLUGIN = makePlugin('example-web-prototype', 'Web Prototype');
|
||||
|
||||
function makePlugin(id: string, title: string): InstalledPluginRecord {
|
||||
return {
|
||||
id,
|
||||
|
|
@ -234,4 +246,131 @@ describe('HomeView context picker', () => {
|
|||
projectKind: 'prototype',
|
||||
}));
|
||||
});
|
||||
|
||||
it('clears an active type chip when the user picks a skill (#2972)', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>(async (url) => {
|
||||
if (typeof url === 'string' && url === '/api/plugins') {
|
||||
return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (typeof url === 'string' && url === '/api/mcp/servers') {
|
||||
return new Response(JSON.stringify({ servers: [], templates: [] }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch ${url}`);
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 0;
|
||||
});
|
||||
const onSubmit = vi.fn();
|
||||
|
||||
render(
|
||||
<HomeView
|
||||
projects={[]}
|
||||
skills={[DECK_SKILL, SKILL]}
|
||||
onSubmit={onSubmit}
|
||||
onOpenProject={() => undefined}
|
||||
onViewAllProjects={() => undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByTestId('home-hero-rail-prototype'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Prototype');
|
||||
});
|
||||
|
||||
const input = screen.getByTestId('home-hero-input');
|
||||
fireEvent.change(input, { target: { value: '@deck' } });
|
||||
fireEvent.mouseDown(screen.getByRole('option', { name: /deck lab/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('home-hero-active-skill')).toBeTruthy();
|
||||
expect(screen.queryByTestId('home-hero-active-type-chip')).toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('home-hero-submit'));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
||||
pluginId: DEFAULT_UNSELECTED_SCENARIO_PLUGIN_ID,
|
||||
skillId: DECK_SKILL.id,
|
||||
projectKind: 'deck',
|
||||
}));
|
||||
expect(onSubmit.mock.calls[0]?.[0]?.pluginId).not.toBe('example-web-prototype');
|
||||
});
|
||||
|
||||
it('clears an active skill when the user picks a type chip (#2972)', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>(async (url) => {
|
||||
if (typeof url === 'string' && url === '/api/plugins') {
|
||||
return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (typeof url === 'string' && url.includes('/apply')) {
|
||||
return new Response(JSON.stringify({
|
||||
appliedPlugin: {
|
||||
snapshotId: 'snap-web-prototype',
|
||||
pluginId: 'example-web-prototype',
|
||||
pluginVersion: '1.0.0',
|
||||
inputs: {},
|
||||
},
|
||||
contextItems: [],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
if (typeof url === 'string' && url === '/api/mcp/servers') {
|
||||
return new Response(JSON.stringify({ servers: [], templates: [] }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch ${url}`);
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 0;
|
||||
});
|
||||
const onSubmit = vi.fn();
|
||||
|
||||
render(
|
||||
<HomeView
|
||||
projects={[]}
|
||||
skills={[SKILL]}
|
||||
onSubmit={onSubmit}
|
||||
onOpenProject={() => undefined}
|
||||
onViewAllProjects={() => undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = await screen.findByTestId('home-hero-input');
|
||||
fireEvent.change(input, { target: { value: '@proto' } });
|
||||
fireEvent.mouseDown(screen.getByRole('option', { name: /prototype lab/i }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('home-hero-active-skill')).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(await screen.findByTestId('home-hero-rail-prototype'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Prototype');
|
||||
expect(screen.queryByTestId('home-hero-active-skill')).toBeNull();
|
||||
});
|
||||
|
||||
fireEvent.change(input, { target: { value: 'Build a pricing-page prototype.' } });
|
||||
fireEvent.click(screen.getByTestId('home-hero-submit'));
|
||||
|
||||
await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
|
||||
pluginId: 'example-web-prototype',
|
||||
skillId: null,
|
||||
projectKind: 'prototype',
|
||||
})));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue