Skip to content

Commit 656de2c

Browse files
committed
feat(canvas): "Set up a workflow" suggestion on the new-task surfaces
Workflow builds were only discoverable via + > New canvas. Add a canvas-kind starter card to the channel task suggestions, shown on both new-task surfaces: - Channel home: selecting it prefills the composer AND arms canvas mode, so the submit runs canvas generation (whose unified prompt handles workflow intent) instead of creating a plain task. - New-task screen: TaskInput hands canvas suggestions to the surface, which creates a canvas in the channel and opens it with the starter prompt dropped into the hero composer (one-shot prefill handoff), ready to edit. Generated-By: PostHog Code Task-Id: 3b50883f-cab5-443a-8b92-3d0c948688da
1 parent 4288c7f commit 656de2c

11 files changed

Lines changed: 121 additions & 7 deletions

File tree

packages/shared/src/analytics-events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ export type ChannelActionType =
866866
| "edit_context_open"
867867
| "new_task_open"
868868
| "new_task_suggestion"
869+
| "new_task_canvas_suggestion"
869870
| "view_context"
870871
| "view_history"
871872
| "view_artifacts"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// One-shot handoff of a starter instruction into a just-created canvas's hero
2+
// composer. A surface that creates a canvas on behalf of a suggestion (e.g. the
3+
// new-task screen's "Set up a workflow" card) stashes the prompt here BEFORE
4+
// navigating; the hero takes (and clears) it on mount. Ephemeral by design —
5+
// nothing is persisted, and an entry survives at most one navigation.
6+
const pending = new Map<string, string>();
7+
8+
export function setPendingCanvasPrefill(
9+
dashboardId: string,
10+
instruction: string,
11+
): void {
12+
pending.set(dashboardId, instruction);
13+
}
14+
15+
// Returns the stashed instruction for this canvas and removes it, so a
16+
// remount (or another canvas) can never replay it.
17+
export function takePendingCanvasPrefill(dashboardId: string): string | null {
18+
const instruction = pending.get(dashboardId) ?? null;
19+
pending.delete(dashboardId);
20+
return instruction;
21+
}

packages/ui/src/features/canvas/channelTaskSuggestions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Cube,
77
CurrencyDollar,
88
Flask,
9+
Lightning,
910
Wrench,
1011
} from "@phosphor-icons/react";
1112
import type { SuggestedPrompt } from "@posthog/ui/features/task-detail/components/SuggestedPromptCard";
@@ -18,6 +19,18 @@ import type { SuggestedPrompt } from "@posthog/ui/features/task-detail/component
1819
// (icon badge + title + description); the icon/color follow the same
1920
// `var(--<color>-N)` token scheme.
2021
export const CHANNEL_TASK_SUGGESTIONS: SuggestedPrompt[] = [
22+
{
23+
label: "Set up a workflow",
24+
description: "Automate an action, with a live board tracking it",
25+
icon: Lightning,
26+
color: "amber",
27+
// Workflows build through canvas generation (the canvas is the workflow's
28+
// observability board), so this suggestion arms the canvas path rather
29+
// than creating a plain task.
30+
canvas: true,
31+
prompt:
32+
"Set up a workflow that runs automatically — send an email, post to Slack, or fire a webhook when something happens — and build a live dashboard tracking it.\n\n\nUser input:\n- What should happen, and when (e.g. send a welcome email after signup):",
33+
},
2134
{
2235
label: "Debug a user issue",
2336
description: "Trace a specific user's events, replays, and errors",

packages/ui/src/features/canvas/components/ChannelHomeComposer.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,14 @@ import {
4848
import type { PendingKickoff } from "./ChannelFeedView";
4949

5050
export interface ChannelHomeComposerHandle {
51-
/** Drop a starter prompt into the editor and apply its mode, if any. */
52-
applySuggestion: (prompt: string, mode?: string) => void;
51+
/** Drop a starter prompt into the editor and apply its mode, if any. A
52+
* `canvas` suggestion also arms canvas mode, so the submit runs canvas
53+
* generation (canvas + workflow builds) instead of creating a plain task. */
54+
applySuggestion: (
55+
prompt: string,
56+
mode?: string,
57+
opts?: { canvas?: boolean },
58+
) => void;
5359
}
5460

5561
interface ChannelHomeComposerProps {
@@ -351,7 +357,11 @@ export const ChannelHomeComposer = forwardRef<
351357
useImperativeHandle(
352358
ref,
353359
() => ({
354-
applySuggestion: (prompt: string, mode?: string) => {
360+
applySuggestion: (
361+
prompt: string,
362+
mode?: string,
363+
opts?: { canvas?: boolean },
364+
) => {
355365
// Pending content (not setContent) preserves the multi-line template's
356366
// line breaks and focuses at the end; mirrors the new-task screen.
357367
useDraftStore.getState().actions.setPendingContent(sessionId, {
@@ -360,6 +370,9 @@ export const ChannelHomeComposer = forwardRef<
360370
if (mode && isValidConfigValue(modeOption, mode)) {
361371
setConfigOption(modeOption.id, mode);
362372
}
373+
// Canvas suggestions (canvas + workflow builds) flip the composer into
374+
// canvas mode so the submit runs canvas generation for this prompt.
375+
setCanvasArmed(opts?.canvas ?? false);
363376
},
364377
}),
365378
[sessionId, modeOption, setConfigOption],

packages/ui/src/features/canvas/components/WebsiteChannelHome.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) {
123123
const closeThread = useThreadPanelStore((s) => s.closeThread);
124124

125125
const handleSuggestionSelect = useCallback(
126-
(prompt: string, mode?: string) => {
127-
composerRef.current?.applySuggestion(prompt, mode);
126+
(prompt: string, mode?: string, opts?: { canvas?: boolean }) => {
127+
composerRef.current?.applySuggestion(prompt, mode, opts);
128128
},
129129
[],
130130
);
@@ -256,7 +256,9 @@ export function WebsiteChannelHome({ channelId }: { channelId: string }) {
256256
key={suggestion.label}
257257
suggestion={suggestion}
258258
onSelect={() =>
259-
handleSuggestionSelect(suggestion.prompt, suggestion.mode)
259+
handleSuggestionSelect(suggestion.prompt, suggestion.mode, {
260+
canvas: suggestion.canvas,
261+
})
260262
}
261263
/>
262264
))}

packages/ui/src/features/canvas/components/WebsiteNewTask.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ vi.mock("@posthog/ui/features/canvas/hooks/useChannels", () => ({
3939
vi.mock("@posthog/ui/features/canvas/hooks/useChannelTasks", () => ({
4040
useChannelTaskMutations: () => ({ fileTask: vi.fn() }),
4141
}));
42+
// Needs a TRPCProvider at render; the canvas-suggestion path isn't under test.
43+
vi.mock("@posthog/ui/features/canvas/hooks/useDashboards", () => ({
44+
useCreateAndOpenDashboard: () => vi.fn(),
45+
}));
4246
vi.mock("@posthog/ui/features/canvas/hooks/useFolderInstructions", () => ({
4347
useFolderInstructions,
4448
}));

packages/ui/src/features/canvas/components/WebsiteNewTask.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ChannelBreadcrumb } from "@posthog/ui/features/canvas/components/Channe
55
import { ChannelContextPanel } from "@posthog/ui/features/canvas/components/ChannelContextPanel";
66
import { useChannels } from "@posthog/ui/features/canvas/hooks/useChannels";
77
import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks";
8+
import { useCreateAndOpenDashboard } from "@posthog/ui/features/canvas/hooks/useDashboards";
89
import { useFolderInstructions } from "@posthog/ui/features/canvas/hooks/useFolderInstructions";
910
import { TaskInput } from "@posthog/ui/features/task-detail/components/TaskInput";
1011
import { taskDetailQuery } from "@posthog/ui/features/tasks/queries";
@@ -68,6 +69,23 @@ export function WebsiteNewTask({ channelId }: { channelId: string }) {
6869
}
6970
}, [channelId, contextPanelOpen]);
7071

72+
// Canvas suggestions (e.g. "Set up a workflow") don't fill this composer —
73+
// it creates plain tasks — they open a fresh canvas in the channel with the
74+
// starter prompt dropped into its hero composer, ready to edit/send.
75+
const createAndOpenCanvas = useCreateAndOpenDashboard(channelId);
76+
const onCanvasSuggestionSelect = useCallback(
77+
(suggestion: { label: string; prompt: string }) => {
78+
track(ANALYTICS_EVENTS.CHANNEL_ACTION, {
79+
action_type: "new_task_canvas_suggestion",
80+
surface: "new_task",
81+
channel_id: channelId,
82+
suggestion_label: suggestion.label,
83+
});
84+
void createAndOpenCanvas({ prefillInstruction: suggestion.prompt });
85+
},
86+
[channelId, createAndOpenCanvas],
87+
);
88+
7189
const onTaskCreated = useCallback(
7290
(task: Task) => {
7391
// Seed the detail cache so the destination route resolves instantly
@@ -120,6 +138,7 @@ export function WebsiteNewTask({ channelId }: { channelId: string }) {
120138
suggestion_label: label,
121139
})
122140
}
141+
onCanvasSuggestionSelect={onCanvasSuggestionSelect}
123142
onContextChipClick={
124143
channelContext ? handleContextChipClick : undefined
125144
}

packages/ui/src/features/canvas/freeform/CanvasGenerateHero.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ShapesIcon } from "@phosphor-icons/react";
2+
import { takePendingCanvasPrefill } from "@posthog/ui/features/canvas/canvasPrefill";
23
import { CANVAS_GENERATE_SUGGESTIONS } from "@posthog/ui/features/canvas/freeform/canvasGenerateSuggestions";
34
import { FreeformGenerateBar } from "@posthog/ui/features/canvas/freeform/FreeformGenerateBar";
45
import type { EditorHandle } from "@posthog/ui/features/message-editor/types";
56
import { SuggestedPromptCard } from "@posthog/ui/features/task-detail/components/SuggestedPromptCard";
67
import { DotPatternBackground } from "@posthog/ui/primitives/DotPatternBackground";
78
import { Flex, Text } from "@radix-ui/themes";
8-
import { useRef } from "react";
9+
import { useEffect, useRef } from "react";
910

1011
// The empty-canvas landing state: a centered composer with starter-prompt
1112
// suggestions below it. Once the user submits, the canvas record records a
@@ -35,6 +36,17 @@ export function CanvasGenerateHero({
3536
// Lets a suggestion card drop its prompt straight into the editor.
3637
const editorRef = useRef<EditorHandle>(null);
3738

39+
// A surface that created this canvas on behalf of a suggestion (e.g. the
40+
// new-task screen's "Set up a workflow" card) stashes the starter prompt
41+
// before navigating here; drop it into the composer ready to edit/send.
42+
useEffect(() => {
43+
const prefill = takePendingCanvasPrefill(dashboardId);
44+
if (prefill) {
45+
editorRef.current?.setContent(prefill);
46+
editorRef.current?.focus();
47+
}
48+
}, [dashboardId]);
49+
3850
return (
3951
<Flex
4052
direction="column"

packages/ui/src/features/canvas/hooks/useDashboards.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
} from "@posthog/core/canvas/dashboardSchemas";
66
import type { FreeformVersion } from "@posthog/core/canvas/freeformSchemas";
77
import { useHostTRPC } from "@posthog/host-router/react";
8+
import { setPendingCanvasPrefill } from "@posthog/ui/features/canvas/canvasPrefill";
89
import { useDashboardEditStore } from "@posthog/ui/features/canvas/stores/dashboardEditStore";
910
import { toast } from "@posthog/ui/primitives/toast";
1011
import { logger } from "@posthog/ui/shell/logger";
@@ -221,6 +222,9 @@ export function useCreateAndOpenDashboard(
221222
templateId?: string;
222223
name?: string;
223224
channelId?: string;
225+
/** Starter instruction dropped into the new canvas's hero composer, ready to
226+
* edit/send (stashed before navigating so the hero finds it on mount). */
227+
prefillInstruction?: string;
224228
}) => Promise<void> {
225229
const navigate = useNavigate();
226230
const { createDashboard } = useDashboardMutations();
@@ -235,6 +239,9 @@ export function useCreateAndOpenDashboard(
235239
try {
236240
const record = await createDashboard(targetChannelId, name, templateId);
237241
setEditing(record.id, true);
242+
if (opts?.prefillInstruction) {
243+
setPendingCanvasPrefill(record.id, opts.prefillInstruction);
244+
}
238245
await navigate({
239246
to: "/website/$channelId/dashboards/$dashboardId",
240247
params: { channelId: targetChannelId, dashboardId: record.id },

packages/ui/src/features/task-detail/components/SuggestedPromptCard.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export interface SuggestedPrompt {
1010
color: string;
1111
/** Task mode to apply when this suggestion is selected, if it implies one. */
1212
mode?: ExecutionMode;
13+
/** Routes into canvas generation (canvas + workflow builds) instead of a
14+
* plain task — the surface arms its canvas path when selecting this. */
15+
canvas?: boolean;
1316
}
1417

1518
export interface SuggestedPromptCardProps {

0 commit comments

Comments
 (0)