Skip to content

Commit 97a8d55

Browse files
authored
Dashboard quick starters open a chat, not a workflow (#4740)
1 parent 5df99d0 commit 97a8d55

12 files changed

Lines changed: 263 additions & 335 deletions

web/src/components/chat/composer/MediaChatComposer.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import { usePromptHistory } from "../hooks/usePromptHistory";
8484
import { useMessageQueue } from "../../../hooks/useMessageQueue";
8585
import { createMediaComposerStyles } from "./MediaChatComposer.styles";
8686
import useModelPreferencesStore from "../../../stores/ModelPreferencesStore";
87+
import { useChatDraftStore } from "../../../stores/ChatDraftStore";
8788
import { StopGenerationButton } from "./StopGenerationButton";
8889
import PermissionSelector from "./PermissionSelector";
8990
import { useElapsedTime } from "../../../hooks/useElapsedTime";
@@ -130,6 +131,10 @@ export interface MediaChatComposerProps {
130131
/** Pure chat panel: hide the mode picker and force "chat" mode. Used by the
131132
* app builder / video / 3d editor agent panels which are hardcoded to chat. */
132133
hideModePicker?: boolean;
134+
/** Thread this composer writes to. A surface that opened the thread with a
135+
* prompt in mind (the dashboard quick starters) seeds it through
136+
* ChatDraftStore; the seed lands in the textarea once, unsent. */
137+
threadId?: string | null;
133138
}
134139

135140
/**
@@ -165,7 +170,8 @@ const MediaChatComposer: React.FC<MediaChatComposerProps> = ({
165170
trailingActions,
166171
leadingActions,
167172
placeholder: placeholderOverride,
168-
hideModePicker = false
173+
hideModePicker = false,
174+
threadId
169175
}) => {
170176
const theme = useTheme();
171177
const styles = useMemo(() => createMediaComposerStyles(theme), [theme]);
@@ -327,6 +333,19 @@ const MediaChatComposer: React.FC<MediaChatComposerProps> = ({
327333
}
328334
}, [autoFocus, autoFocusEnabled]);
329335

336+
// A seeded prompt is consumed once, and only into an empty box — the user's
337+
// own half-written text always wins.
338+
const takeDraft = useChatDraftStore((s) => s.takeDraft);
339+
useEffect(() => {
340+
if (!threadId) {
341+
return;
342+
}
343+
const draft = takeDraft(threadId);
344+
if (draft) {
345+
setPrompt((current) => (current ? current : draft));
346+
}
347+
}, [threadId, takeDraft]);
348+
330349
// Close any open model / option dialogs whenever the mode changes. The
331350
// image- and video-model dialogs are intentionally shared between the
332351
// `image`/`image_edit` and `video`/`image_to_video` chip clusters

web/src/components/chat/containers/ChatInputSection.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ type ChatInputSectionProps = {
7070
placeholder?: string;
7171
/** Pure chat panel: hide the media mode picker, force chat mode. */
7272
hideModePicker?: boolean;
73+
/** Thread this composer writes to; used to pick up a seeded prompt. */
74+
threadId?: string | null;
7375
};
7476

7577
const ChatInputSection = ({
@@ -86,7 +88,8 @@ const ChatInputSection = ({
8688
variant = "media",
8789
composerToolbar,
8890
placeholder,
89-
hideModePicker
91+
hideModePicker,
92+
threadId
9093
}: ChatInputSectionProps) => {
9194
const isLoading = status === "loading";
9295
const isStreaming = status === "streaming";
@@ -119,6 +122,7 @@ const ChatInputSection = ({
119122
requireToolSupport={requireToolSupport}
120123
placeholder={placeholder}
121124
hideModePicker={hideModePicker}
125+
threadId={threadId}
122126
/>
123127
)}
124128
</div>

web/src/components/chat/containers/ChatView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ const ChatView = ({
306306
composerToolbar={composerToolbar}
307307
placeholder={composerPlaceholder}
308308
hideModePicker={hideModePicker}
309+
threadId={effectiveThreadId}
309310
/>
310311
</div>
311312
{showTodoSidebar && <TodoSidebar todos={todos} />}

web/src/components/portal/Portal.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import DashboardTemplates, {
1919
import DashboardTutorials from "./DashboardTutorials";
2020
import DashboardWorkflows from "./DashboardWorkflows";
2121
import DashboardFooter from "./DashboardFooter";
22-
import { useCreateStarterWorkflow } from "../../hooks/useCreateStarterWorkflow";
22+
import { useStartTrackChat } from "../../hooks/useStartTrackChat";
2323
import { WELCOME_TRACKS, type WelcomeTrackId } from "./welcomeTracks";
2424
import { Box, SPACING, getSpacingPx } from "../ui_primitives";
2525

@@ -56,13 +56,13 @@ const Portal: React.FC = () => {
5656
const { sortedWorkflows, isLoadingWorkflows } = useDashboardData();
5757
const { handleCreateNewWorkflow } = useWorkflowActions();
5858

59-
const createStarterWorkflow = useCreateStarterWorkflow();
59+
const startTrackChat = useStartTrackChat();
6060
const hasConfiguredProvider = useHasConfiguredProvider();
6161

6262
const handlePickTrack = useCallback(
6363
(trackId: WelcomeTrackId) => {
64-
// A starter workflow needs a model to run; route key-less users through
65-
// the shared provider onboarding first so their first Run doesn't fail.
64+
// The first send needs a model; route key-less users through the shared
65+
// provider onboarding first so that send doesn't fail.
6666
if (!hasConfiguredProvider) {
6767
setPendingTrack(trackId);
6868
const track = WELCOME_TRACKS.find((t) => t.id === trackId);
@@ -76,23 +76,23 @@ const Portal: React.FC = () => {
7676
});
7777
return;
7878
}
79-
createStarterWorkflow(trackId);
79+
void startTrackChat(trackId);
8080
},
81-
[hasConfiguredProvider, createStarterWorkflow]
81+
[hasConfiguredProvider, startTrackChat]
8282
);
8383

8484
// Picking a track without a provider parks it here; once one is connected the
85-
// starter opens on its own, so the user finishes the thing they asked for
85+
// chat opens on its own, so the user finishes the thing they asked for
8686
// rather than landing back on the dashboard.
87-
const createStarter = useRef(createStarterWorkflow);
88-
createStarter.current = createStarterWorkflow;
87+
const startChat = useRef(startTrackChat);
88+
startChat.current = startTrackChat;
8989
useEffect(() => {
9090
if (!pendingTrack || !hasConfiguredProvider) {
9191
return;
9292
}
9393
const trackId = pendingTrack;
9494
setPendingTrack(null);
95-
createStarter.current(trackId);
95+
void startChat.current(trackId);
9696
}, [pendingTrack, hasConfiguredProvider]);
9797

9898
const handleOpenWorkflow = useCallback(

web/src/components/portal/WelcomeFlow.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,9 @@ const WelcomeFlow: React.FC<WelcomeFlowProps> = ({
198198
</div>
199199
<h1 className="welcome-heading">What do you want to make today?</h1>
200200
<p className="welcome-sub">
201-
Pick one. We&apos;ll drop a starter workflow onto the canvas so you can run
202-
it and make it your own. You can always change your mind, or skip and
203-
explore.
201+
Pick one. We&apos;ll open a chat in that mode with an example prompt ready
202+
to go — press send, or write your own. You can always change your mind,
203+
or skip and explore.
204204
</p>
205205

206206
<div className="welcome-grid">
@@ -226,7 +226,7 @@ const WelcomeFlow: React.FC<WelcomeFlowProps> = ({
226226
</div>
227227
<div className="welcome-card-title">{track.label}</div>
228228
<div className="welcome-card-blurb">{track.blurb}</div>
229-
<span className="welcome-card-node">{track.nodeLabel}</span>
229+
<span className="welcome-card-node">{track.modeLabel}</span>
230230
</button>
231231
);
232232
})}
@@ -239,7 +239,7 @@ const WelcomeFlow: React.FC<WelcomeFlowProps> = ({
239239
</button>
240240
<span className="welcome-footer-divider" />
241241
<span className="welcome-footer-hint">
242-
Each pick is a real, editable workflow.
242+
Each pick opens a chat with the prompt already written.
243243
</span>
244244
</div>
245245
)}

web/src/components/portal/__tests__/welcomeTracks.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
import { WELCOME_TRACKS } from "../welcomeTracks";
55
import type { WelcomeTrack } from "../welcomeTracks";
6+
import type { MediaMode } from "../../../stores/MediaGenerationStore";
67

78
const tracks: WelcomeTrack[] = WELCOME_TRACKS;
89

@@ -20,12 +21,10 @@ describe("WELCOME_TRACKS", () => {
2021
for (const t of tracks) {
2122
expect(t.label.length).toBeGreaterThan(0);
2223
expect(t.blurb.length).toBeGreaterThan(0);
23-
expect(t.nodeLabel.length).toBeGreaterThan(0);
24-
expect(t.workflowName.length).toBeGreaterThan(0);
24+
expect(t.modeLabel.length).toBeGreaterThan(0);
25+
expect(t.threadTitle.length).toBeGreaterThan(0);
2526
expect(t.samplePrompt.length).toBeGreaterThan(0);
26-
expect(t.modelType.length).toBeGreaterThan(0);
27-
expect(t.promptInput.length).toBeGreaterThan(0);
28-
expect(t.outputHandle.length).toBeGreaterThan(0);
27+
expect(t.chatMode.length).toBeGreaterThan(0);
2928
}
3029
});
3130

@@ -35,9 +34,10 @@ describe("WELCOME_TRACKS", () => {
3534
}
3635
});
3736

38-
it("model types use nodetool namespace", () => {
37+
it("every track opens a composer mode the media store knows", () => {
38+
const modes: MediaMode[] = ["chat", "image", "video", "audio"];
3939
for (const t of tracks) {
40-
expect(t.modelType).toMatch(/^nodetool\./);
40+
expect(modes).toContain(t.chatMode);
4141
}
4242
});
4343

Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
/**
22
* Starter tracks for the dashboard welcome flow. Each track maps a creative
3-
* modality to a real three-node starter graph (String -> model node ->
4-
* Preview) that is dropped onto the canvas when the user picks it.
5-
*
6-
* Node type strings, input handles, and output handles are the canonical
7-
* `nodetool.*` node identifiers (see packages/dsl/src/generated). The model
8-
* field on each model node is filled with the user's default model at
9-
* creation time via applyDefaultModels — these tracks intentionally do not
10-
* hardcode a provider.
3+
* modality to a chat tab: picking one opens a new conversation in that
4+
* modality's composer mode with the example prompt already typed, so the
5+
* user's first action is pressing send.
116
*/
127
import type { OnboardingCapability } from "../../stores/ProviderOnboardingStore";
8+
import type { MediaMode } from "../../stores/MediaGenerationStore";
139

1410
export type WelcomeTrackId = "image" | "video" | "audio" | "agent";
1511

@@ -19,88 +15,71 @@ export interface WelcomeTrack {
1915
label: string;
2016
/** One-line description under the title. */
2117
blurb: string;
22-
/** Short node-type label shown as a mono chip on the card. */
23-
nodeLabel: string;
18+
/** Short mode label shown as a mono chip on the card. */
19+
modeLabel: string;
2420
/** Modality accent color used for the card icon tint. */
2521
accent: string;
26-
/** Name given to the created workflow. */
27-
workflowName: string;
28-
/** Example prompt pre-filled into the String node. */
22+
/** Title given to the chat tab this track opens. */
23+
threadTitle: string;
24+
/** Example prompt pre-filled into the composer. */
2925
samplePrompt: string;
30-
/** Node type of the model node wired after the String. */
31-
modelType: string;
32-
/** Input handle on the model node that receives the prompt String. */
33-
promptInput: string;
34-
/** Output handle on the model node that feeds the Preview. */
35-
outputHandle: string;
26+
/** Composer mode the chat opens in. */
27+
chatMode: MediaMode;
3628
/**
37-
* What a provider must be able to do for this track's model node to run.
29+
* What a provider must be able to do for this track's first send to work.
3830
* Narrows provider onboarding to the ones that unblock the track the user
3931
* just picked.
4032
*/
4133
capability: OnboardingCapability;
4234
}
4335

44-
export {
45-
STRING_NODE_TYPE,
46-
PREVIEW_NODE_TYPE
47-
} from "../../constants/nodeTypes";
48-
4936
export const WELCOME_TRACKS: WelcomeTrack[] = [
5037
{
5138
id: "image",
5239
label: "Image",
5340
blurb: "A still frame from a prompt.",
54-
nodeLabel: "TextToImage",
41+
modeLabel: "image",
5542
accent: "#9F7AEA",
56-
workflowName: "Image starter",
43+
threadTitle: "Image",
5744
samplePrompt:
5845
"a brutalist concrete pavilion at dusk, fog rolling in low, single warm light from inside, photograph, medium format",
59-
modelType: "nodetool.image.TextToImage",
60-
promptInput: "prompt",
61-
outputHandle: "output",
46+
chatMode: "image",
6247
capability: "text_to_image"
6348
},
6449
{
6550
id: "video",
6651
label: "Video",
6752
blurb: "A short clip from a prompt.",
68-
nodeLabel: "TextToVideo",
53+
modeLabel: "video",
6954
accent: "#F472B6",
70-
workflowName: "Video starter",
55+
threadTitle: "Video",
7156
samplePrompt:
7257
"slow dolly through an empty library at golden hour, dust in the air, 24fps, cinematic",
73-
modelType: "nodetool.video.TextToVideo",
74-
capability: "text_to_video",
75-
promptInput: "prompt",
76-
outputHandle: "output"
58+
chatMode: "video",
59+
capability: "text_to_video"
7760
},
7861
{
7962
id: "audio",
8063
label: "Audio",
8164
blurb: "A spoken line or sound texture.",
82-
nodeLabel: "TextToSpeech",
65+
modeLabel: "audio",
8366
accent: "#FBBF24",
84-
workflowName: "Audio starter",
67+
threadTitle: "Audio",
8568
samplePrompt:
8669
"a calm narrator: you are listening to nodetool. every model, your keys, your canvas.",
87-
modelType: "nodetool.audio.TextToSpeech",
88-
capability: "text_to_speech",
89-
promptInput: "text",
90-
outputHandle: "audio"
70+
chatMode: "audio",
71+
capability: "text_to_speech"
9172
},
9273
{
9374
id: "agent",
9475
label: "Text · Agent",
9576
blurb: "A reasoning step or written output.",
96-
nodeLabel: "Agent",
77+
modeLabel: "chat",
9778
accent: "#60A5FA",
98-
workflowName: "Agent starter",
79+
threadTitle: "Chat",
9980
samplePrompt:
10081
"draft a 4-line tagline for a creative tool that runs every AI model locally or in the cloud, no hype words",
101-
modelType: "nodetool.agents.Agent",
102-
capability: "generate_message",
103-
promptInput: "prompt",
104-
outputHandle: "text"
82+
chatMode: "chat",
83+
capability: "generate_message"
10584
}
10685
];

0 commit comments

Comments
 (0)