Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/api/ai/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ Action types:
- Scrape: {"actionType": "Scrape", "url": "https://example.com"}
- Search: {"actionType": "Search", "query": "search query", "limit": 10}
- Condition: {"actionType": "Condition", "condition": "{{@nodeId:Label.field}} === 'value'"}
- Create Chat (v0): {"actionType": "Create Chat", "message": "Create a line graph showing DAU over time", "system": "You are an expert coder"} - Use v0 for generating UI components, visualizations (charts, graphs, dashboards), landing pages, or any React/Next.js code. PREFER v0 over Generate Text/Image for any visual output like charts, graphs, or UI.
- Send Message (v0): {"actionType": "Send Message", "chatId": "{{@nodeId:Label.chatId}}", "message": "Add dark mode"} - Use this to continue a v0 chat conversation
CRITICAL ABOUT CONDITION NODES:
- Condition nodes evaluate a boolean expression
Expand Down
28 changes: 27 additions & 1 deletion components/settings/integration-form-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ type IntegrationFormData = {
const INTEGRATION_TYPES: IntegrationType[] = [
"ai-gateway",
"database",
"firecrawl",
"linear",
"resend",
"slack",
"firecrawl",
"v0",
];

const INTEGRATION_LABELS: Record<IntegrationType, string> = {
Expand All @@ -55,6 +56,7 @@ const INTEGRATION_LABELS: Record<IntegrationType, string> = {
database: "Database",
"ai-gateway": "AI Gateway",
firecrawl: "Firecrawl",
v0: "v0",
};

export function IntegrationFormDialog({
Expand Down Expand Up @@ -290,6 +292,30 @@ export function IntegrationFormDialog({
</p>
</div>
);
case "v0":
return (
<div className="space-y-2">
<Label htmlFor="apiKey">API Key</Label>
<Input
id="apiKey"
onChange={(e) => updateConfig("apiKey", e.target.value)}
placeholder="v0_..."
type="password"
value={formData.config.apiKey || ""}
/>
<p className="text-muted-foreground text-xs">
Get your API key from{" "}
<a
className="underline hover:text-foreground"
href="https://v0.dev/chat/settings/keys"
rel="noopener noreferrer"
target="_blank"
>
v0.dev/chat/settings/keys
</a>
</p>
</div>
);
default:
return null;
}
Expand Down
1 change: 1 addition & 0 deletions components/settings/integrations-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const INTEGRATION_TYPE_LABELS: Record<IntegrationType, string> = {
database: "Database",
"ai-gateway": "AI Gateway",
firecrawl: "Firecrawl",
v0: "v0",
};

type IntegrationsManagerProps = {
Expand Down
13 changes: 13 additions & 0 deletions components/ui/template-autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@ const getCommonFields = (node: WorkflowNode) => {
if (actionType === "Search") {
return [{ field: "web", description: "Array of search results" }];
}
if (actionType === "Create Chat") {
return [
{ field: "chatId", description: "v0 chat ID" },
{ field: "url", description: "v0 chat URL" },
{ field: "demoUrl", description: "Demo preview URL" },
];
}
if (actionType === "Send Message") {
return [
{ field: "chatId", description: "v0 chat ID" },
{ field: "demoUrl", description: "Demo preview URL" },
];
}
if (node.data.type === "trigger") {
const triggerType = node.data.config?.triggerType as string | undefined;
const webhookSchema = node.data.config?.webhookSchema as string | undefined;
Expand Down
7 changes: 7 additions & 0 deletions components/workflow/nodes/action-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ const getIntegrationFromActionType = (actionType: string): string => {
Scrape: "Firecrawl",
Search: "Firecrawl",
Condition: "Condition",
"Create Chat": "v0",
"Send Message": "v0",
};
return integrationMap[actionType] || "System";
};
Expand Down Expand Up @@ -106,6 +108,8 @@ const requiresIntegration = (actionType: string): boolean => {
"Database Query",
"Scrape",
"Search",
"Create Chat",
"Send Message",
];
return requiresIntegrationActions.includes(actionType);
};
Expand Down Expand Up @@ -139,6 +143,9 @@ const getProviderLogo = (actionType: string) => {
return <Code className="size-12 text-green-300" strokeWidth={1.5} />;
case "Condition":
return <GitBranch className="size-12 text-pink-300" strokeWidth={1.5} />;
case "Create Chat":
case "Send Message":
return <IntegrationIcon className="size-12" integration="v0" />;
default:
return <Zap className="size-12 text-amber-300" strokeWidth={1.5} />;
}
Expand Down
6 changes: 6 additions & 0 deletions components/workflow/utils/code-generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { searchCodegenTemplate } from "@/plugins/firecrawl/codegen/search";
import { createTicketCodegenTemplate } from "@/plugins/linear/codegen/create-ticket";
import { sendEmailCodegenTemplate } from "@/plugins/resend/codegen/send-email";
import { sendSlackMessageCodegenTemplate } from "@/plugins/slack/codegen/send-slack-message";
import { createChatCodegenTemplate } from "@/plugins/v0/codegen/create-chat";
import { sendMessageCodegenTemplate } from "@/plugins/v0/codegen/send-message";

// Generate code snippet for a single node
export const generateNodeCode = (node: {
Expand Down Expand Up @@ -84,6 +86,10 @@ export async function POST(request: NextRequest) {
return scrapeCodegenTemplate;
case "Search":
return searchCodegenTemplate;
case "Create Chat":
return createChatCodegenTemplate;
case "Send Message":
return sendMessageCodegenTemplate;
default:
return `async function actionStep(input: Record<string, unknown>) {
"use step";
Expand Down
3 changes: 2 additions & 1 deletion lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ export type IntegrationType =
| "slack"
| "database"
| "ai-gateway"
| "firecrawl";
| "firecrawl"
| "v0";

export type IntegrationConfig = {
apiKey?: string;
Expand Down
12 changes: 12 additions & 0 deletions lib/credential-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type WorkflowCredentials = {
AI_GATEWAY_API_KEY?: string;
DATABASE_URL?: string;
FIRECRAWL_API_KEY?: string;
V0_API_KEY?: string;
};

function mapResendConfig(config: IntegrationConfig): WorkflowCredentials {
Expand Down Expand Up @@ -82,6 +83,14 @@ function mapFirecrawlConfig(config: IntegrationConfig): WorkflowCredentials {
return creds;
}

function mapV0Config(config: IntegrationConfig): WorkflowCredentials {
const creds: WorkflowCredentials = {};
if (config.apiKey) {
creds.V0_API_KEY = config.apiKey;
}
return creds;
}

/**
* Map integration config to WorkflowCredentials format
*/
Expand All @@ -107,6 +116,9 @@ function mapIntegrationConfig(
if (integrationType === "firecrawl") {
return mapFirecrawlConfig(config);
}
if (integrationType === "v0") {
return mapV0Config(config);
}
return {};
}

Expand Down
4 changes: 3 additions & 1 deletion lib/db/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ export type IntegrationType =
| "slack"
| "database"
| "ai-gateway"
| "firecrawl";
| "firecrawl"
| "v0";

export type IntegrationConfig = {
// Resend
Expand All @@ -115,6 +116,7 @@ export type IntegrationConfig = {
openaiApiKey?: string;
// Firecrawl
firecrawlApiKey?: string;
// v0 (uses apiKey)
};

export type DecryptedIntegration = {
Expand Down
8 changes: 7 additions & 1 deletion lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@ export const integrations = pgTable("integrations", {
type: text("type")
.notNull()
.$type<
"resend" | "linear" | "slack" | "database" | "ai-gateway" | "firecrawl"
| "resend"
| "linear"
| "slack"
| "database"
| "ai-gateway"
| "firecrawl"
| "v0"
>(),
// biome-ignore lint/suspicious/noExplicitAny: JSONB type - encrypted credentials stored as JSON
config: jsonb("config").notNull().$type<any>(),
Expand Down
29 changes: 29 additions & 0 deletions lib/workflow-codegen-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { searchCodegenTemplate } from "../plugins/firecrawl/codegen/search";
import { createTicketCodegenTemplate } from "../plugins/linear/codegen/create-ticket";
import { sendEmailCodegenTemplate } from "../plugins/resend/codegen/send-email";
import { sendSlackMessageCodegenTemplate } from "../plugins/slack/codegen/send-slack-message";
import { createChatCodegenTemplate } from "../plugins/v0/codegen/create-chat";
import { sendMessageCodegenTemplate } from "../plugins/v0/codegen/send-message";
// Import codegen templates directly
import conditionTemplate from "./codegen-templates/condition";
import databaseQueryTemplate from "./codegen-templates/database-query";
Expand Down Expand Up @@ -42,6 +44,8 @@ function loadStepImplementation(actionType: string): string | null {
Search: searchCodegenTemplate,
"HTTP Request": httpRequestTemplate,
Condition: conditionTemplate,
"Create Chat": createChatCodegenTemplate,
"Send Message": sendMessageCodegenTemplate,
};

const template = templateMap[actionType];
Expand Down Expand Up @@ -592,6 +596,29 @@ export function generateWorkflowSDKCode(

return params;
}

function buildV0CreateChatParams(config: Record<string, unknown>): string[] {
imports.add("import { createClient } from 'v0-sdk';");
const params = [
`message: \`${convertTemplateToJS((config.message as string) || "")}\``,
"apiKey: process.env.V0_API_KEY!",
];
if (config.system) {
params.push(
`system: \`${convertTemplateToJS((config.system as string) || "")}\``
);
}
return params;
}

function buildV0SendMessageParams(config: Record<string, unknown>): string[] {
imports.add("import { createClient } from 'v0-sdk';");
return [
`chatId: \`${convertTemplateToJS((config.chatId as string) || "")}\``,
`message: \`${convertTemplateToJS((config.message as string) || "")}\``,
"apiKey: process.env.V0_API_KEY!",
];
}
function buildStepInputParams(
actionType: string,
config: Record<string, unknown>
Expand All @@ -607,6 +634,8 @@ export function generateWorkflowSDKCode(
Condition: () => buildConditionParams(config),
Scrape: () => buildFirecrawlParams(actionType, config),
Search: () => buildFirecrawlParams(actionType, config),
"Create Chat": () => buildV0CreateChatParams(config),
"Send Message": () => buildV0SendMessageParams(config),
};

const builder = paramBuilders[actionType];
Expand Down
8 changes: 8 additions & 0 deletions lib/workflow-codegen-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,14 @@ export function getStepInfo(actionType: string): {
functionName: "firecrawlSearchStep",
importPath: "./steps/firecrawl",
},
"Create Chat": {
functionName: "createChatStep",
importPath: "./steps/v0",
},
"Send Message": {
functionName: "sendMessageStep",
importPath: "./steps/v0",
},
};

return (
Expand Down
61 changes: 61 additions & 0 deletions lib/workflow-codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,57 @@ export function generateWorkflowCode(
return lines;
}

function generateV0CreateChatActionCode(
node: WorkflowNode,
indent: string,
varName: string
): string[] {
const stepInfo = getStepInfo("Create Chat");
imports.add(
`import { ${stepInfo.functionName} } from '${stepInfo.importPath}';`
);

const config = node.data.config || {};
const message = (config.message as string) || "";
const system = (config.system as string) || "";

const lines = [
`${indent}const ${varName} = await ${stepInfo.functionName}({`,
`${indent} message: ${formatTemplateValue(message)},`,
];

if (system) {
lines.push(`${indent} system: ${formatTemplateValue(system)},`);
}

lines.push(`${indent}});`);
return lines;
}

function generateV0SendMessageActionCode(
node: WorkflowNode,
indent: string,
varName: string
): string[] {
const stepInfo = getStepInfo("Send Message");
imports.add(
`import { ${stepInfo.functionName} } from '${stepInfo.importPath}';`
);

const config = node.data.config || {};
const chatId = (config.chatId as string) || "";
const message = (config.message as string) || "";

const lines = [
`${indent}const ${varName} = await ${stepInfo.functionName}({`,
`${indent} chatId: ${formatTemplateValue(chatId)},`,
`${indent} message: ${formatTemplateValue(message)},`,
`${indent}});`,
];

return lines;
}

// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Action type routing requires many conditionals
function generateActionNodeCode(
node: WorkflowNode,
Expand Down Expand Up @@ -638,6 +689,16 @@ export function generateWorkflowCode(
lines.push(
...wrapActionCall(generateFirecrawlActionCode(node, indent, varName))
);
} else if (actionType === "Create Chat") {
lines.push(
...wrapActionCall(generateV0CreateChatActionCode(node, indent, varName))
);
} else if (actionType === "Send Message") {
lines.push(
...wrapActionCall(
generateV0SendMessageActionCode(node, indent, varName)
)
);
} else if (actionType === "Database Query") {
lines.push(
...wrapActionCall(generateDatabaseActionCode(node, indent, varName))
Expand Down
14 changes: 14 additions & 0 deletions lib/workflow-executor.workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,20 @@ async function executeActionStep(input: {
// biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type
return await firecrawlSearchStep(stepInput as any);
}
if (actionType === "Create Chat") {
const { createChatStep } = await import(
"../plugins/v0/steps/create-chat/step"
);
// biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type
return await createChatStep(stepInput as any);
}
if (actionType === "Send Message") {
const { sendMessageStep } = await import(
"../plugins/v0/steps/send-message/step"
);
// biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type
return await sendMessageStep(stepInput as any);
}

// Fallback for unknown action types
return {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"server-only": "^0.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"v0-sdk": "^0.15.1",
"vaul": "^1.1.2",
"workflow": "4.0.1-beta.17",
"zod": "^4.1.12"
Expand Down
Loading