shubhjn commited on
Commit
d0916ba
·
1 Parent(s): 385b747

fix some Buggs

Browse files
app/actions/business.ts CHANGED
@@ -1,6 +1,7 @@
1
  "use server";
2
 
3
  import { auth } from "@/auth";
 
4
  import { db } from "@/db";
5
  import { businesses } from "@/db/schema";
6
  import { eq, inArray, and } from "drizzle-orm";
@@ -10,10 +11,12 @@ export async function deleteBusiness(id: string) {
10
  const session = await auth();
11
  if (!session?.user?.id) throw new Error("Unauthorized");
12
 
 
 
13
  await db.delete(businesses).where(
14
  and(
15
  eq(businesses.id, id),
16
- eq(businesses.userId, session.user.id)
17
  )
18
  );
19
  revalidatePath("/dashboard");
@@ -24,10 +27,12 @@ export async function bulkDeleteBusinesses(ids: string[]) {
24
  const session = await auth();
25
  if (!session?.user?.id) throw new Error("Unauthorized");
26
 
 
 
27
  await db.delete(businesses).where(
28
  and(
29
  inArray(businesses.id, ids),
30
- eq(businesses.userId, session.user.id)
31
  )
32
  );
33
  revalidatePath("/dashboard");
 
1
  "use server";
2
 
3
  import { auth } from "@/auth";
4
+ import { getEffectiveUserId } from "@/lib/auth-utils";
5
  import { db } from "@/db";
6
  import { businesses } from "@/db/schema";
7
  import { eq, inArray, and } from "drizzle-orm";
 
11
  const session = await auth();
12
  if (!session?.user?.id) throw new Error("Unauthorized");
13
 
14
+ const userId = await getEffectiveUserId(session.user.id);
15
+
16
  await db.delete(businesses).where(
17
  and(
18
  eq(businesses.id, id),
19
+ eq(businesses.userId, userId)
20
  )
21
  );
22
  revalidatePath("/dashboard");
 
27
  const session = await auth();
28
  if (!session?.user?.id) throw new Error("Unauthorized");
29
 
30
+ const userId = await getEffectiveUserId(session.user.id);
31
+
32
  await db.delete(businesses).where(
33
  and(
34
  inArray(businesses.id, ids),
35
+ eq(businesses.userId, userId)
36
  )
37
  );
38
  revalidatePath("/dashboard");
app/actions/workflow-actions.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server";
2
+
3
+ import { generateWorkflowFromPrompt } from "@/lib/gemini";
4
+ import { Node, Edge } from "reactflow";
5
+
6
+ // Define return type
7
+ interface GenerateWorkflowResponse {
8
+ success: boolean;
9
+ data?: {
10
+ nodes: Node[];
11
+ edges: Edge[];
12
+ };
13
+ error?: string;
14
+ }
15
+
16
+ export async function generateWorkflowAction(prompt: string): Promise<GenerateWorkflowResponse> {
17
+ if (!prompt) {
18
+ return { success: false, error: "Prompt is required" };
19
+ }
20
+
21
+ try {
22
+ const workflowData = await generateWorkflowFromPrompt(prompt);
23
+
24
+ // Ensure nodes and edges are present and valid (basic check)
25
+ if (!Array.isArray(workflowData.nodes) || !Array.isArray(workflowData.edges)) {
26
+ return { success: false, error: "AI generated invalid workflow structure" };
27
+ }
28
+
29
+ return { success: true, data: workflowData };
30
+ } catch (error: unknown) {
31
+ let errorMessage = "Failed to generate workflow";
32
+
33
+ if (error instanceof Error) {
34
+ // Log clean error for rate limits
35
+ if (error.message.includes("429") || error.message.includes("Quota exceeded")) {
36
+ console.warn("AI Workflow Action Rate Limit:", error.message);
37
+ errorMessage = "AI usage limit exceeded. Please try again later.";
38
+ } else {
39
+ console.error("AI Workflow Action Error:", error);
40
+ errorMessage = error.message;
41
+ }
42
+ } else {
43
+ console.error("AI Workflow Action Unknown Error:", error);
44
+ }
45
+
46
+ return { success: false, error: errorMessage };
47
+ }
48
+ }
app/api/search/route.ts CHANGED
@@ -1,6 +1,7 @@
1
 
2
  import { NextResponse } from "next/server";
3
  import { auth } from "@/lib/auth";
 
4
  import { db } from "@/db";
5
  import { businesses, emailTemplates, automationWorkflows } from "@/db/schema";
6
  import { ilike, or, eq, and } from "drizzle-orm";
@@ -19,7 +20,7 @@ export async function GET(req: Request) {
19
  return NextResponse.json({ results: [] });
20
  }
21
 
22
- const userId = session.user.id;
23
  const searchPattern = `%${query}%`;
24
 
25
  // Parallelize queries for better performance
 
1
 
2
  import { NextResponse } from "next/server";
3
  import { auth } from "@/lib/auth";
4
+ import { getEffectiveUserId } from "@/lib/auth-utils";
5
  import { db } from "@/db";
6
  import { businesses, emailTemplates, automationWorkflows } from "@/db/schema";
7
  import { ilike, or, eq, and } from "drizzle-orm";
 
20
  return NextResponse.json({ results: [] });
21
  }
22
 
23
+ const userId = await getEffectiveUserId(session.user.id);
24
  const searchPattern = `%${query}%`;
25
 
26
  // Parallelize queries for better performance
app/api/settings/route.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
 
3
  import { db } from "@/db";
4
  import { users } from "@/db/schema";
5
  import { eq } from "drizzle-orm";
@@ -22,7 +23,7 @@ export async function GET() {
22
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
23
  }
24
 
25
- const userId = session.user.id;
26
 
27
  // Fetch user with API keys
28
  const [user] = await db
@@ -81,7 +82,7 @@ export async function PATCH(request: Request) {
81
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
82
  }
83
 
84
- const userId = session.user.id;
85
  const body = await request.json();
86
 
87
  const updateData: UpdateUserData = {
 
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
3
+ import { getEffectiveUserId } from "@/lib/auth-utils";
4
  import { db } from "@/db";
5
  import { users } from "@/db/schema";
6
  import { eq } from "drizzle-orm";
 
23
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
24
  }
25
 
26
+ const userId = await getEffectiveUserId(session.user.id);
27
 
28
  // Fetch user with API keys
29
  const [user] = await db
 
82
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
83
  }
84
 
85
+ const userId = await getEffectiveUserId(session.user.id);
86
  const body = await request.json();
87
 
88
  const updateData: UpdateUserData = {
app/api/tasks/route.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
3
  import { db } from "@/db";
4
- import { scrapingJobs, automationWorkflows } from "@/db/schema";
5
  import { eq, desc, and } from "drizzle-orm";
6
 
7
  export async function GET() {
@@ -12,6 +12,19 @@ export async function GET() {
12
  }
13
 
14
  const userId = session.user.id;
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  // Fetch scraping jobs
17
  const jobs = await db
@@ -27,7 +40,7 @@ export async function GET() {
27
  })
28
  .from(scrapingJobs)
29
  .leftJoin(automationWorkflows, eq(scrapingJobs.workflowId, automationWorkflows.id))
30
- .where(eq(scrapingJobs.userId, userId))
31
  .orderBy(desc(scrapingJobs.createdAt))
32
  .limit(100); // Limit results for performance
33
 
@@ -41,7 +54,7 @@ export async function GET() {
41
  createdAt: automationWorkflows.createdAt,
42
  })
43
  .from(automationWorkflows)
44
- .where(eq(automationWorkflows.userId, userId))
45
  .orderBy(desc(automationWorkflows.createdAt))
46
  .limit(100);
47
 
@@ -95,6 +108,19 @@ export async function DELETE(request: Request) {
95
  const id = searchParams.get("id");
96
  const type = searchParams.get("type");
97
  const userId = session.user.id;
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  if (!id || !type) {
100
  return NextResponse.json({ error: "ID and type required" }, { status: 400 });
@@ -103,11 +129,11 @@ export async function DELETE(request: Request) {
103
  if (type === "workflow") {
104
  await db
105
  .delete(automationWorkflows)
106
- .where(and(eq(automationWorkflows.id, id), eq(automationWorkflows.userId, userId)));
107
  } else {
108
  await db
109
  .delete(scrapingJobs)
110
- .where(and(eq(scrapingJobs.id, id), eq(scrapingJobs.userId, userId)));
111
  }
112
 
113
  return NextResponse.json({ success: true });
 
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
3
  import { db } from "@/db";
4
+ import { scrapingJobs, automationWorkflows, users } from "@/db/schema";
5
  import { eq, desc, and } from "drizzle-orm";
6
 
7
  export async function GET() {
 
12
  }
13
 
14
  const userId = session.user.id;
15
+ let queryUserId = userId;
16
+
17
+ if (userId === "admin-user") {
18
+ const adminEmail = process.env.ADMIN_EMAIL;
19
+ if (adminEmail) {
20
+ const existingUserByEmail = await db.query.users.findFirst({
21
+ where: eq(users.email, adminEmail)
22
+ });
23
+ if (existingUserByEmail) {
24
+ queryUserId = existingUserByEmail.id;
25
+ }
26
+ }
27
+ }
28
 
29
  // Fetch scraping jobs
30
  const jobs = await db
 
40
  })
41
  .from(scrapingJobs)
42
  .leftJoin(automationWorkflows, eq(scrapingJobs.workflowId, automationWorkflows.id))
43
+ .where(eq(scrapingJobs.userId, queryUserId))
44
  .orderBy(desc(scrapingJobs.createdAt))
45
  .limit(100); // Limit results for performance
46
 
 
54
  createdAt: automationWorkflows.createdAt,
55
  })
56
  .from(automationWorkflows)
57
+ .where(eq(automationWorkflows.userId, queryUserId))
58
  .orderBy(desc(automationWorkflows.createdAt))
59
  .limit(100);
60
 
 
108
  const id = searchParams.get("id");
109
  const type = searchParams.get("type");
110
  const userId = session.user.id;
111
+ let queryUserId = userId;
112
+
113
+ if (userId === "admin-user") {
114
+ const adminEmail = process.env.ADMIN_EMAIL;
115
+ if (adminEmail) {
116
+ const existingUserByEmail = await db.query.users.findFirst({
117
+ where: eq(users.email, adminEmail)
118
+ });
119
+ if (existingUserByEmail) {
120
+ queryUserId = existingUserByEmail.id;
121
+ }
122
+ }
123
+ }
124
 
125
  if (!id || !type) {
126
  return NextResponse.json({ error: "ID and type required" }, { status: 400 });
 
129
  if (type === "workflow") {
130
  await db
131
  .delete(automationWorkflows)
132
+ .where(and(eq(automationWorkflows.id, id), eq(automationWorkflows.userId, queryUserId)));
133
  } else {
134
  await db
135
  .delete(scrapingJobs)
136
+ .where(and(eq(scrapingJobs.id, id), eq(scrapingJobs.userId, queryUserId)));
137
  }
138
 
139
  return NextResponse.json({ success: true });
app/api/templates/[templateId]/route.ts CHANGED
@@ -1,6 +1,7 @@
1
 
2
  import { NextResponse } from "next/server";
3
  import { auth } from "@/lib/auth";
 
4
  import { db } from "@/db";
5
  import { emailTemplates } from "@/db/schema";
6
  import { eq, and } from "drizzle-orm";
@@ -16,7 +17,7 @@ export async function PATCH(
16
  }
17
 
18
  const { templateId } = await params;
19
- const userId = session.user.id;
20
  const body = await request.json();
21
  const { name, subject, body: emailBody, isDefault } = body;
22
 
@@ -72,7 +73,7 @@ export async function DELETE(
72
  }
73
 
74
  const { templateId } = await params;
75
- const userId = session.user.id;
76
 
77
  if (!templateId) {
78
  return NextResponse.json({ error: "Template ID required" }, { status: 400 });
 
1
 
2
  import { NextResponse } from "next/server";
3
  import { auth } from "@/lib/auth";
4
+ import { getEffectiveUserId } from "@/lib/auth-utils";
5
  import { db } from "@/db";
6
  import { emailTemplates } from "@/db/schema";
7
  import { eq, and } from "drizzle-orm";
 
17
  }
18
 
19
  const { templateId } = await params;
20
+ const userId = await getEffectiveUserId(session.user.id);
21
  const body = await request.json();
22
  const { name, subject, body: emailBody, isDefault } = body;
23
 
 
73
  }
74
 
75
  const { templateId } = await params;
76
+ const userId = await getEffectiveUserId(session.user.id);
77
 
78
  if (!templateId) {
79
  return NextResponse.json({ error: "Template ID required" }, { status: 400 });
app/api/templates/generate/route.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
 
3
  import { db } from "@/db";
4
  import { users } from "@/db/schema";
5
  import { eq } from "drizzle-orm";
@@ -12,7 +13,7 @@ export async function POST(request: Request) {
12
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
13
  }
14
 
15
- const userId = session.user.id;
16
  const { businessType, purpose } = await request.json();
17
 
18
  if (!businessType || !purpose) {
 
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
3
+ import { getEffectiveUserId } from "@/lib/auth-utils";
4
  import { db } from "@/db";
5
  import { users } from "@/db/schema";
6
  import { eq } from "drizzle-orm";
 
13
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
14
  }
15
 
16
+ const userId = await getEffectiveUserId(session.user.id);
17
  const { businessType, purpose } = await request.json();
18
 
19
  if (!businessType || !purpose) {
app/api/workflows/[id]/route.ts CHANGED
@@ -1,9 +1,59 @@
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
3
  import { db } from "@/db";
4
- import { automationWorkflows } from "@/db/schema";
5
  import { eq, and } from "drizzle-orm";
6
  import { SessionUser } from "@/types";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  export async function PATCH(
9
  request: Request,
@@ -19,14 +69,38 @@ export async function PATCH(
19
  const userId = (session.user as SessionUser).id;
20
  const body = await request.json();
21
 
22
- // Toggle isActive or update priority
23
- const updates: Partial<{ isActive: boolean; priority: string; updatedAt: Date }> = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  updatedAt: new Date(),
25
  };
26
 
27
- if (typeof body.isActive === "boolean") {
28
- updates.isActive = body.isActive;
29
- }
 
 
30
 
31
  if (body.priority && ["low", "medium", "high"].includes(body.priority)) {
32
  updates.priority = body.priority;
@@ -39,7 +113,7 @@ export async function PATCH(
39
  .where(
40
  and(
41
  eq(automationWorkflows.id, id),
42
- eq(automationWorkflows.userId, userId)
43
  )
44
  );
45
 
@@ -69,12 +143,26 @@ export async function DELETE(
69
  const { id } = await params;
70
  const userId = (session.user as SessionUser).id;
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  await db
73
  .delete(automationWorkflows)
74
  .where(
75
  and(
76
  eq(automationWorkflows.id, id),
77
- eq(automationWorkflows.userId, userId)
78
  )
79
  );
80
 
 
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
3
  import { db } from "@/db";
4
+ import { automationWorkflows, users } from "@/db/schema";
5
  import { eq, and } from "drizzle-orm";
6
  import { SessionUser } from "@/types";
7
+ import { Edge, Node } from "reactflow";
8
+
9
+ export async function GET(
10
+ request: Request,
11
+ { params }: { params: Promise<{ id: string }> }
12
+ ) {
13
+ try {
14
+ const session = await auth();
15
+ if (!session?.user) {
16
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
17
+ }
18
+
19
+ const { id } = await params;
20
+ const userId = (session.user as SessionUser).id;
21
+
22
+ let queryUserId = userId;
23
+
24
+ if (userId === "admin-user") {
25
+ const adminEmail = process.env.ADMIN_EMAIL;
26
+ if (adminEmail) {
27
+ const existingUserByEmail = await db.query.users.findFirst({
28
+ where: eq(users.email, adminEmail)
29
+ });
30
+ if (existingUserByEmail) {
31
+ queryUserId = existingUserByEmail.id;
32
+ }
33
+ }
34
+ }
35
+
36
+ const workflow = await db.query.automationWorkflows.findFirst({
37
+ where: and(
38
+ eq(automationWorkflows.id, id),
39
+ eq(automationWorkflows.userId, queryUserId)
40
+ )
41
+ });
42
+
43
+ if (!workflow) {
44
+ return NextResponse.json({ error: "Workflow not found" }, { status: 404 });
45
+ }
46
+
47
+ return NextResponse.json({ workflow });
48
+
49
+ } catch (error) {
50
+ console.error("Error fetching workflow:", error);
51
+ return NextResponse.json(
52
+ { error: "Failed to fetch workflow" },
53
+ { status: 500 }
54
+ );
55
+ }
56
+ }
57
 
58
  export async function PATCH(
59
  request: Request,
 
69
  const userId = (session.user as SessionUser).id;
70
  const body = await request.json();
71
 
72
+ let queryUserId = userId;
73
+
74
+ if (userId === "admin-user") {
75
+ const adminEmail = process.env.ADMIN_EMAIL;
76
+ if (adminEmail) {
77
+ const existingUserByEmail = await db.query.users.findFirst({
78
+ where: eq(users.email, adminEmail)
79
+ });
80
+ if (existingUserByEmail) {
81
+ queryUserId = existingUserByEmail.id;
82
+ }
83
+ }
84
+ }
85
+
86
+
87
+ const updates: Partial<{
88
+ name: string;
89
+ isActive: boolean;
90
+ priority: string;
91
+ nodes: Node[];
92
+ edges: Edge[];
93
+ targetBusinessType: string;
94
+ updatedAt: Date
95
+ }> = {
96
  updatedAt: new Date(),
97
  };
98
 
99
+ if (body.name) updates.name = body.name;
100
+ if (typeof body.isActive === "boolean") updates.isActive = body.isActive;
101
+ if (body.nodes) updates.nodes = body.nodes;
102
+ if (body.edges) updates.edges = body.edges;
103
+ if (body.targetBusinessType) updates.targetBusinessType = body.targetBusinessType;
104
 
105
  if (body.priority && ["low", "medium", "high"].includes(body.priority)) {
106
  updates.priority = body.priority;
 
113
  .where(
114
  and(
115
  eq(automationWorkflows.id, id),
116
+ eq(automationWorkflows.userId, queryUserId)
117
  )
118
  );
119
 
 
143
  const { id } = await params;
144
  const userId = (session.user as SessionUser).id;
145
 
146
+ let queryUserId = userId;
147
+
148
+ if (userId === "admin-user") {
149
+ const adminEmail = process.env.ADMIN_EMAIL;
150
+ if (adminEmail) {
151
+ const existingUserByEmail = await db.query.users.findFirst({
152
+ where: eq(users.email, adminEmail)
153
+ });
154
+ if (existingUserByEmail) {
155
+ queryUserId = existingUserByEmail.id;
156
+ }
157
+ }
158
+ }
159
+
160
  await db
161
  .delete(automationWorkflows)
162
  .where(
163
  and(
164
  eq(automationWorkflows.id, id),
165
+ eq(automationWorkflows.userId, queryUserId)
166
  )
167
  );
168
 
app/api/workflows/execute/route.ts CHANGED
@@ -1,5 +1,6 @@
1
  import { auth } from "@/auth";
2
  import { executeWorkflowLoop } from "@/lib/workflow-executor";
 
3
  import { NextResponse } from "next/server";
4
 
5
  export const POST = async (req: Request) => {
@@ -17,7 +18,8 @@ export const POST = async (req: Request) => {
17
  return new NextResponse("Workflow ID is required", { status: 400 });
18
  }
19
 
20
- const result = await executeWorkflowLoop(workflowId, session.user.id);
 
21
 
22
  return NextResponse.json(result);
23
  } catch (error) {
 
1
  import { auth } from "@/auth";
2
  import { executeWorkflowLoop } from "@/lib/workflow-executor";
3
+ import { getEffectiveUserId } from "@/lib/auth-utils";
4
  import { NextResponse } from "next/server";
5
 
6
  export const POST = async (req: Request) => {
 
18
  return new NextResponse("Workflow ID is required", { status: 400 });
19
  }
20
 
21
+ const userId = await getEffectiveUserId(session.user.id);
22
+ const result = await executeWorkflowLoop(workflowId, userId);
23
 
24
  return NextResponse.json(result);
25
  } catch (error) {
app/api/workflows/route.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
3
  import { db } from "@/db";
4
- import { automationWorkflows } from "@/db/schema";
5
  import { eq, and } from "drizzle-orm";
6
  import { SessionUser } from "@/types";
7
 
@@ -14,10 +14,24 @@ export async function GET() {
14
 
15
  const userId = (session.user as SessionUser).id;
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  const workflows = await db
18
  .select()
19
  .from(automationWorkflows)
20
- .where(eq(automationWorkflows.userId, userId))
21
  .orderBy(automationWorkflows.createdAt);
22
 
23
  return NextResponse.json({ workflows });
@@ -48,10 +62,44 @@ export async function POST(request: Request) {
48
  isActive,
49
  } = body;
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  const [workflow] = await db
52
  .insert(automationWorkflows)
53
  .values({
54
- userId,
55
  name,
56
  targetBusinessType: targetBusinessType || "",
57
  keywords: keywords || [],
 
1
  import { NextResponse } from "next/server";
2
  import { auth } from "@/lib/auth";
3
  import { db } from "@/db";
4
+ import { automationWorkflows, users } from "@/db/schema";
5
  import { eq, and } from "drizzle-orm";
6
  import { SessionUser } from "@/types";
7
 
 
14
 
15
  const userId = (session.user as SessionUser).id;
16
 
17
+ let queryUserId = userId;
18
+
19
+ if (userId === "admin-user") {
20
+ const adminEmail = process.env.ADMIN_EMAIL;
21
+ if (adminEmail) {
22
+ const existingUserByEmail = await db.query.users.findFirst({
23
+ where: eq(users.email, adminEmail)
24
+ });
25
+ if (existingUserByEmail) {
26
+ queryUserId = existingUserByEmail.id;
27
+ }
28
+ }
29
+ }
30
+
31
  const workflows = await db
32
  .select()
33
  .from(automationWorkflows)
34
+ .where(eq(automationWorkflows.userId, queryUserId))
35
  .orderBy(automationWorkflows.createdAt);
36
 
37
  return NextResponse.json({ workflows });
 
62
  isActive,
63
  } = body;
64
 
65
+ // Fail-safe: Ensure admin user exists if this is the admin
66
+ let finalUserId = userId;
67
+
68
+ if (userId === "admin-user") {
69
+ // 1. Try to find by ID first
70
+ const existingUserById = await db.query.users.findFirst({
71
+ where: eq(users.id, userId)
72
+ });
73
+
74
+ if (!existingUserById) {
75
+ const adminEmail = process.env.ADMIN_EMAIL;
76
+ if (adminEmail) {
77
+ // 2. Try to find by Email
78
+ const existingUserByEmail = await db.query.users.findFirst({
79
+ where: eq(users.email, adminEmail)
80
+ });
81
+
82
+ if (existingUserByEmail) {
83
+ // Use the existing user's ID
84
+ finalUserId = existingUserByEmail.id;
85
+ } else {
86
+ // Create new admin user if absolutely disconnected
87
+ await db.insert(users).values({
88
+ id: userId,
89
+ name: "Admin",
90
+ email: adminEmail,
91
+ createdAt: new Date(),
92
+ updatedAt: new Date()
93
+ });
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
  const [workflow] = await db
100
  .insert(automationWorkflows)
101
  .values({
102
+ userId: finalUserId,
103
  name,
104
  targetBusinessType: targetBusinessType || "",
105
  keywords: keywords || [],
app/dashboard/workflows/builder/[id]/page.tsx CHANGED
@@ -91,7 +91,7 @@ export default function WorkflowBuilderPage() {
91
  }
92
 
93
  return (
94
- <div className="flex flex-col h-screen w-full bg-background">
95
  {/* Header for Back button and Name (Immersive Mode) */}
96
  <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between p-4 border-b shrink-0 gap-4">
97
  <div className="flex items-center gap-4 w-full sm:w-auto">
@@ -124,7 +124,7 @@ export default function WorkflowBuilderPage() {
124
  </div>
125
 
126
  {/* Editor Area */}
127
- <div className="flex-1 overflow-hidden">
128
  <NodeEditor
129
  initialNodes={(workflow.nodes as Node<NodeData>[]) || []}
130
  initialEdges={(workflow.edges as Edge[]) || []}
 
91
  }
92
 
93
  return (
94
+ <div className="flex flex-col h-screen w-full bg-background overflow-y-auto">
95
  {/* Header for Back button and Name (Immersive Mode) */}
96
  <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between p-4 border-b shrink-0 gap-4">
97
  <div className="flex items-center gap-4 w-full sm:w-auto">
 
124
  </div>
125
 
126
  {/* Editor Area */}
127
+ <div className="flex-1 overflow-hidden h-screen">
128
  <NodeEditor
129
  initialNodes={(workflow.nodes as Node<NodeData>[]) || []}
130
  initialEdges={(workflow.edges as Edge[]) || []}
app/dashboard/workflows/page.tsx CHANGED
@@ -35,7 +35,7 @@ export default function WorkflowsPage() {
35
  </div>
36
 
37
  <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
38
- {loading && workflows.length === 0 ? (
39
  // Show skeleton cards while loading
40
  Array.from({ length: 6 }).map((_, i) => (
41
  <Card key={i} className="animate-pulse">
 
35
  </div>
36
 
37
  <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
38
+ {loading ||workflows.length === 0 ? (
39
  // Show skeleton cards while loading
40
  Array.from({ length: 6 }).map((_, i) => (
41
  <Card key={i} className="animate-pulse">
app/globals.css CHANGED
@@ -1,4 +1,5 @@
1
  @import "tailwindcss";
 
2
 
3
  @import './animations.css';
4
 
@@ -38,35 +39,33 @@
38
 
39
  @layer base {
40
  :root {
41
- /* Rich Dark Theme by Default for "Futuristic" feel */
42
- --background: 240 10% 3.9%;
43
- --foreground: 0 0% 98%;
44
 
45
- /* Glass Effect Basics */
46
- --card: 240 10% 3.9%; /* Base fallback */
47
- --card-foreground: 0 0% 98%;
48
 
49
- --popover: 240 10% 3.9%;
50
- --popover-foreground: 0 0% 98%;
51
 
52
- /* Vibrant Primary - Electric Violet/Blue */
53
  --primary: 263.4 70% 50.4%;
54
  --primary-foreground: 210 40% 98%;
55
 
56
- --secondary: 240 3.7% 15.9%;
57
- --secondary-foreground: 0 0% 98%;
58
 
59
- --muted: 240 3.7% 15.9%;
60
- --muted-foreground: 240 5% 64.9%;
61
 
62
- --accent: 240 3.7% 15.9%;
63
- --accent-foreground: 0 0% 98%;
64
 
65
- --destructive: 0 62.8% 30.6%;
66
  --destructive-foreground: 0 0% 98%;
67
 
68
- --border: 240 3.7% 15.9%;
69
- --input: 240 3.7% 15.9%;
70
  --ring: 263.4 70% 50.4%;
71
 
72
  --radius: 0.75rem;
 
1
  @import "tailwindcss";
2
+ @custom-variant dark (&:where(.dark, .dark *));
3
 
4
  @import './animations.css';
5
 
 
39
 
40
  @layer base {
41
  :root {
42
+ /* Light Mode Values */
43
+ --background: 0 0% 100%;
44
+ --foreground: 240 10% 3.9%;
45
 
46
+ --card: 0 0% 100%;
47
+ --card-foreground: 240 10% 3.9%;
 
48
 
49
+ --popover: 0 0% 100%;
50
+ --popover-foreground: 240 10% 3.9%;
51
 
 
52
  --primary: 263.4 70% 50.4%;
53
  --primary-foreground: 210 40% 98%;
54
 
55
+ --secondary: 240 4.8% 95.9%;
56
+ --secondary-foreground: 240 5.9% 10%;
57
 
58
+ --muted: 240 4.8% 95.9%;
59
+ --muted-foreground: 240 3.8% 46.1%;
60
 
61
+ --accent: 240 4.8% 95.9%;
62
+ --accent-foreground: 240 5.9% 10%;
63
 
64
+ --destructive: 0 84.2% 60.2%;
65
  --destructive-foreground: 0 0% 98%;
66
 
67
+ --border: 240 5.9% 90%;
68
+ --input: 240 5.9% 90%;
69
  --ring: 263.4 70% 50.4%;
70
 
71
  --radius: 0.75rem;
components/dashboard/user-nav.tsx CHANGED
@@ -8,7 +8,6 @@ import {
8
  DropdownMenuItem,
9
  DropdownMenuLabel,
10
  DropdownMenuSeparator,
11
- DropdownMenuShortcut,
12
  DropdownMenuTrigger,
13
  } from "@/components/ui/dropdown-menu";
14
  import { Button } from "@/components/ui/button";
@@ -23,7 +22,7 @@ export function UserNav() {
23
  return (
24
  <DropdownMenu>
25
  <DropdownMenuTrigger asChild>
26
- <Button variant="ghost" className="relative h-8 w-8 rounded-full cursor-pointer">
27
  <Avatar className="h-8 w-8">
28
  <AvatarImage src={user?.image || ""} alt={user?.name || ""} />
29
  <AvatarFallback>{user?.name?.charAt(0) || "U"}</AvatarFallback>
@@ -55,7 +54,7 @@ export function UserNav() {
55
  </DropdownMenuItem>
56
  </DropdownMenuGroup>
57
  <DropdownMenuSeparator />
58
- <DropdownMenuItem onClick={() => signOut()} className="cursor-pointer">
59
  <LogOut className="mr-2 h-4 w-4" />
60
  <span>Log out</span>
61
  </DropdownMenuItem>
 
8
  DropdownMenuItem,
9
  DropdownMenuLabel,
10
  DropdownMenuSeparator,
 
11
  DropdownMenuTrigger,
12
  } from "@/components/ui/dropdown-menu";
13
  import { Button } from "@/components/ui/button";
 
22
  return (
23
  <DropdownMenu>
24
  <DropdownMenuTrigger asChild>
25
+ <Button variant="ghost" className="relative h-8 w-8 hover:scale-110 rounded-full cursor-pointer">
26
  <Avatar className="h-8 w-8">
27
  <AvatarImage src={user?.image || ""} alt={user?.name || ""} />
28
  <AvatarFallback>{user?.name?.charAt(0) || "U"}</AvatarFallback>
 
54
  </DropdownMenuItem>
55
  </DropdownMenuGroup>
56
  <DropdownMenuSeparator />
57
+ <DropdownMenuItem onClick={() => signOut()} className="cursor-pointer text-destructive">
58
  <LogOut className="mr-2 h-4 w-4" />
59
  <span>Log out</span>
60
  </DropdownMenuItem>
components/node-editor/ai-workflow-dialog.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState } from "react";
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogFooter,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from "@/components/ui/dialog";
11
+ import { Button } from "@/components/ui/button";
12
+ import { Textarea } from "@/components/ui/textarea";
13
+ import { generateWorkflowAction } from "@/app/actions/workflow-actions";
14
+ import { Loader2, Sparkles } from "lucide-react";
15
+ import { Node, Edge } from "reactflow";
16
+ import { NodeData } from "./node-editor";
17
+
18
+ interface AiWorkflowDialogProps {
19
+ open: boolean;
20
+ onOpenChange: (open: boolean) => void;
21
+ onGenerate: (nodes: Node<NodeData>[], edges: Edge[]) => void;
22
+ }
23
+
24
+ export function AiWorkflowDialog({
25
+ open,
26
+ onOpenChange,
27
+ onGenerate,
28
+ }: AiWorkflowDialogProps) {
29
+ const [prompt, setPrompt] = useState("");
30
+ const [isGenerating, setIsGenerating] = useState(false);
31
+ const [error, setError] = useState<string | null>(null);
32
+
33
+ const handleGenerate = async () => {
34
+ if (!prompt.trim()) return;
35
+
36
+ setIsGenerating(true);
37
+ setError(null);
38
+
39
+ try {
40
+ const result = await generateWorkflowAction(prompt);
41
+
42
+ if (result.success && result.data) {
43
+ onGenerate(result.data.nodes, result.data.edges);
44
+ onOpenChange(false);
45
+ setPrompt("");
46
+ } else {
47
+ throw new Error(result.error || "Failed to generate workflow");
48
+ }
49
+ } catch (err: unknown) {
50
+ if (err instanceof Error) {
51
+ setError(err.message);
52
+ } else {
53
+ setError("Something went wrong");
54
+ }
55
+ } finally {
56
+ setIsGenerating(false);
57
+ }
58
+ };
59
+
60
+ return (
61
+ <Dialog open={open} onOpenChange={onOpenChange}>
62
+ <DialogContent className="sm:max-w-[500px]">
63
+ <DialogHeader>
64
+ <DialogTitle className="flex items-center gap-2">
65
+ <Sparkles className="h-5 w-5 text-indigo-500" />
66
+ Generate Workflow with AI
67
+ </DialogTitle>
68
+ <DialogDescription>
69
+ Describe the workflow you want to build, and AI will generate it for you.
70
+ </DialogDescription>
71
+ </DialogHeader>
72
+
73
+ <div className="grid gap-4 py-4">
74
+ <Textarea
75
+ placeholder="E.g., Start with a webhook, wait 2 days, then send a welcome email if the user hasn't visited."
76
+ value={prompt}
77
+ onChange={(e) => setPrompt(e.target.value)}
78
+ className="h-32"
79
+ />
80
+ {error && <p className="text-sm text-red-500">{error}</p>}
81
+ </div>
82
+
83
+ <DialogFooter>
84
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
85
+ Cancel
86
+ </Button>
87
+ <Button
88
+ onClick={handleGenerate}
89
+ disabled={!prompt.trim() || isGenerating}
90
+ className="gap-2"
91
+ >
92
+ {isGenerating ? (
93
+ <>
94
+ <Loader2 className="h-4 w-4 animate-spin" />
95
+ Generating...
96
+ </>
97
+ ) : (
98
+ <>
99
+ <Sparkles className="h-4 w-4" />
100
+ Generate
101
+ </>
102
+ )}
103
+ </Button>
104
+ </DialogFooter>
105
+ </DialogContent>
106
+ </Dialog>
107
+ );
108
+ }
components/node-editor/node-editor.tsx CHANGED
@@ -18,7 +18,8 @@ import ReactFlow, {
18
  import "reactflow/dist/style.css";
19
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
20
  import { Button } from "@/components/ui/button";
21
- import { Plus, Save, Play, Copy, Trash2, Undo, Redo, FileText, Loader2, Download, Upload, HelpCircle, BookOpen, Hand, MousePointer2 } from "lucide-react";
 
22
  import { ImportWorkflowDialog } from "./import-workflow-dialog";
23
  import { NodeConfigDialog } from "./node-config-dialog";
24
  import { WorkflowNode } from "./workflow-node";
@@ -106,8 +107,10 @@ export function NodeEditor({
106
  const [selectedNodes, setSelectedNodes] = useState<Node<NodeData>[]>([]);
107
  const [isConfigOpen, setIsConfigOpen] = useState(false);
108
  const [isTemplatesOpen, setIsTemplatesOpen] = useState(false);
 
109
  const [isGuideOpen, setIsGuideOpen] = useState(false);
110
  const [isImportOpen, setIsImportOpen] = useState(false);
 
111
  const [contextMenu, setContextMenu] = useState<{
112
  x: number;
113
  y: number;
@@ -441,6 +444,16 @@ export function NodeEditor({
441
  setIsTemplatesOpen(false);
442
  }, [setNodes, setEdges, saveToHistory]);
443
 
 
 
 
 
 
 
 
 
 
 
444
  // Close context menu when clicking outside
445
  useEffect(() => {
446
  const handleClick = () => setContextMenu(null);
@@ -459,6 +472,22 @@ export function NodeEditor({
459
  <CardTitle className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
460
  <span>Workflow Editor</span>
461
  <div className="flex flex-wrap gap-2 w-full sm:w-auto">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  <Tooltip>
463
  <TooltipTrigger asChild>
464
  <Button
@@ -778,6 +807,11 @@ export function NodeEditor({
778
  setIsImportOpen(false);
779
  }}
780
  />
 
 
 
 
 
781
  </div>
782
  </TooltipProvider>
783
  );
 
18
  import "reactflow/dist/style.css";
19
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
20
  import { Button } from "@/components/ui/button";
21
+ import { Plus, Save, Play, Copy, Trash2, Undo, Redo, FileText, Loader2, Download, Upload, HelpCircle, BookOpen, Hand, MousePointer2, Sparkles } from "lucide-react";
22
+ import { AiWorkflowDialog } from "./ai-workflow-dialog";
23
  import { ImportWorkflowDialog } from "./import-workflow-dialog";
24
  import { NodeConfigDialog } from "./node-config-dialog";
25
  import { WorkflowNode } from "./workflow-node";
 
107
  const [selectedNodes, setSelectedNodes] = useState<Node<NodeData>[]>([]);
108
  const [isConfigOpen, setIsConfigOpen] = useState(false);
109
  const [isTemplatesOpen, setIsTemplatesOpen] = useState(false);
110
+
111
  const [isGuideOpen, setIsGuideOpen] = useState(false);
112
  const [isImportOpen, setIsImportOpen] = useState(false);
113
+ const [isAiDialogOpen, setIsAiDialogOpen] = useState(false);
114
  const [contextMenu, setContextMenu] = useState<{
115
  x: number;
116
  y: number;
 
444
  setIsTemplatesOpen(false);
445
  }, [setNodes, setEdges, saveToHistory]);
446
 
447
+ const handleAiGenerate = useCallback((generatedNodes: Node<NodeData>[], generatedEdges: Edge[]) => {
448
+ setNodes(generatedNodes);
449
+ setEdges(generatedEdges);
450
+ saveToHistory();
451
+ toast({
452
+ title: "Workflow Generated",
453
+ description: "AI successfully created the workflow structure.",
454
+ });
455
+ }, [setNodes, setEdges, saveToHistory, toast]);
456
+
457
  // Close context menu when clicking outside
458
  useEffect(() => {
459
  const handleClick = () => setContextMenu(null);
 
472
  <CardTitle className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
473
  <span>Workflow Editor</span>
474
  <div className="flex flex-wrap gap-2 w-full sm:w-auto">
475
+ <Tooltip>
476
+ <TooltipTrigger asChild>
477
+ <Button
478
+ onClick={() => setIsAiDialogOpen(true)}
479
+ variant="outline"
480
+ className="gap-2 text-indigo-600 border-indigo-200 hover:bg-indigo-50"
481
+ >
482
+ <Sparkles className="h-4 w-4" />
483
+ AI Generate
484
+ </Button>
485
+ </TooltipTrigger>
486
+ <TooltipContent>Generate workflow with AI</TooltipContent>
487
+ </Tooltip>
488
+
489
+ <div className="w-px h-6 bg-border mx-1 self-center" />
490
+
491
  <Tooltip>
492
  <TooltipTrigger asChild>
493
  <Button
 
807
  setIsImportOpen(false);
808
  }}
809
  />
810
+ <AiWorkflowDialog
811
+ open={isAiDialogOpen}
812
+ onOpenChange={setIsAiDialogOpen}
813
+ onGenerate={handleAiGenerate}
814
+ />
815
  </div>
816
  </TooltipProvider>
817
  );
components/notification-bell.tsx CHANGED
@@ -44,7 +44,7 @@ export function NotificationBell() {
44
  <Button
45
  variant="ghost"
46
  size="icon"
47
- className="relative"
48
  >
49
  <Bell className={cn(
50
  "h-5 w-5",
 
44
  <Button
45
  variant="ghost"
46
  size="icon"
47
+ className="relative hover:scale-110"
48
  >
49
  <Bell className={cn(
50
  "h-5 w-5",
components/theme-switch.tsx CHANGED
@@ -12,7 +12,7 @@ export function ThemeSwitch() {
12
  variant="ghost"
13
  size="icon"
14
  onClick={(e) => toggleTheme(e)}
15
- className="relative cursor-pointer transition-colors hover:bg-accent"
16
  aria-label="Toggle theme"
17
  >
18
  <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
 
12
  variant="ghost"
13
  size="icon"
14
  onClick={(e) => toggleTheme(e)}
15
+ className="relative cursor-pointer hover:scale-110 transition-colors hover:bg-accent"
16
  aria-label="Toggle theme"
17
  >
18
  <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
hooks/use-toast.ts CHANGED
@@ -1,8 +1,9 @@
1
  import { toast as sonnerToast } from "sonner";
2
 
 
 
3
  export function useToast() {
4
- return {
5
- toast: ({ title, description, variant = "default" }: { title: string; description?: string; variant?: "default" | "destructive" }) => {
6
  if (variant === "destructive") {
7
  sonnerToast.error(title, {
8
  description,
@@ -12,6 +13,7 @@ export function useToast() {
12
  description,
13
  });
14
  }
15
- },
16
- };
 
17
  }
 
1
  import { toast as sonnerToast } from "sonner";
2
 
3
+ import { useCallback } from "react";
4
+
5
  export function useToast() {
6
+ const toast = useCallback(({ title, description, variant = "default" }: { title: string; description?: string; variant?: "default" | "destructive" }) => {
 
7
  if (variant === "destructive") {
8
  sonnerToast.error(title, {
9
  description,
 
13
  description,
14
  });
15
  }
16
+ }, []);
17
+
18
+ return { toast };
19
  }
lib/auth-utils.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { db } from "@/db";
2
+ import { users } from "@/db/schema";
3
+ import { eq } from "drizzle-orm";
4
+
5
+ /**
6
+ * Resolves the effective User ID for the current session.
7
+ * If the user is the hardcoded "admin-user" from credentials auth,
8
+ * it attempts to resolve their real database ID via email.
9
+ */
10
+ export async function getEffectiveUserId(sessionUserId: string): Promise<string> {
11
+ if (sessionUserId === "admin-user") {
12
+ const adminEmail = process.env.ADMIN_EMAIL;
13
+ if (adminEmail) {
14
+ const user = await db.query.users.findFirst({
15
+ where: eq(users.email, adminEmail)
16
+ });
17
+ if (user) {
18
+ return user.id;
19
+ }
20
+ }
21
+ }
22
+ return sessionUserId;
23
+ }
lib/auth.ts CHANGED
@@ -44,8 +44,30 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
44
  credentials.email === adminEmail &&
45
  credentials.password === adminPassword
46
  ) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  return {
48
- id: "admin-user",
49
  name: "Admin",
50
  email: adminEmail,
51
  role: "admin",
 
44
  credentials.email === adminEmail &&
45
  credentials.password === adminPassword
46
  ) {
47
+ // Ensure admin user exists in DB
48
+ const adminId = "admin-user";
49
+
50
+ try {
51
+ const existingAdmin = await db.query.users.findFirst({
52
+ where: eq(users.id, adminId)
53
+ });
54
+
55
+ if (!existingAdmin) {
56
+ await db.insert(users).values({
57
+ id: adminId,
58
+ name: "Admin",
59
+ email: adminEmail!,
60
+ createdAt: new Date(),
61
+ updatedAt: new Date()
62
+ });
63
+ }
64
+ } catch (error) {
65
+ console.error("Failed to ensure admin user exists:", error);
66
+ // Fallback to memory-only, but this might cause FK issues as seen
67
+ }
68
+
69
  return {
70
+ id: adminId,
71
  name: "Admin",
72
  email: adminEmail,
73
  role: "admin",
lib/gemini.ts CHANGED
@@ -1,4 +1,7 @@
 
1
  import { GoogleGenerativeAI } from "@google/generative-ai";
 
 
2
 
3
  export async function generateEmailTemplate(
4
  businessType: string,
@@ -117,3 +120,118 @@ export async function generateAIContent(prompt: string, apiKey?: string): Promis
117
  throw error;
118
  }
119
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
  import { GoogleGenerativeAI } from "@google/generative-ai";
3
+ import { Node, Edge } from "reactflow";
4
+
5
 
6
  export async function generateEmailTemplate(
7
  businessType: string,
 
120
  throw error;
121
  }
122
  }
123
+
124
+ /**
125
+ * Generate a workflow JSON from a natural language prompt
126
+ */
127
+
128
+ export async function generateWorkflowFromPrompt(
129
+ prompt: string,
130
+ apiKey?: string
131
+ ): Promise<{ nodes: Node[]; edges: Edge[] }> {
132
+ try {
133
+ const key = apiKey || process.env.GEMINI_API_KEY;
134
+ if (!key) {
135
+ throw new Error("Gemini API key not configured");
136
+ }
137
+
138
+ const genAI = new GoogleGenerativeAI(key);
139
+ // Models to try in order
140
+ const models = ["gemini-3-flash-preview", "gemini-2.0-flash", "gemini-1.5-flash", "gemini-1.5-pro"];
141
+ let lastError;
142
+
143
+ for (const modelName of models) {
144
+ try {
145
+ const model = genAI.getGenerativeModel({ model: modelName });
146
+
147
+ const systemPrompt = `
148
+ You are an expert automation architect. Your task is to generate a JSON object representing a workflow for a node-based automation editor (ReactFlow).
149
+
150
+ Available Node Types:
151
+ - start: Entry point (always required)
152
+ - condition: Evaluates a condition (true/false paths)
153
+ - template: Sends an email template
154
+ - delay: Waits for a specified duration
155
+ - custom: Executes custom code
156
+ - gemini: AI processing task
157
+ - apiRequest: Makes an HTTP request
158
+ - agent: AI Agent with Excel capabilities
159
+ - webhook: Listens for external events
160
+ - schedule: Triggers on a schedule (cron)
161
+ - merge: Merges multiple paths
162
+ - splitInBatches: Loops through items
163
+ - filter: Filters data
164
+ - set: Sets variables
165
+ - scraper: Scrapes a website
166
+
167
+ Schema Requirement:
168
+ Return a JSON object with two arrays: "nodes" and "edges".
169
+
170
+ Node Structure:
171
+ {
172
+ "id": "string (unique, e.g., 'start-1', 'email-2')",
173
+ "type": "string (one of the available types)",
174
+ "data": {
175
+ "label": "string (descriptive name)",
176
+ "type": "string (must match the outer type)",
177
+ "config": {} (optional configuration based on node type)
178
+ },
179
+ "position": { "x": number, "y": number }
180
+ }
181
+
182
+ Edge Structure:
183
+ {
184
+ "id": "string (unique, e.g., 'e1-2')",
185
+ "source": "string (source node id)",
186
+ "target": "string (target node id)",
187
+ "label": "string (optional, e.g., 'True' for condition)"
188
+ }
189
+
190
+ Layout Rules:
191
+ - Arrange nodes logically.
192
+ - Use a vertical flow (y increases downwards).
193
+ - Center the 'start' node at x: 250, y: 0.
194
+ - Space nodes sufficiently (e.g., y + 150 for each step).
195
+ - For conditions, branch out left and right.
196
+
197
+ User Prompt: "${prompt}"
198
+
199
+ Response Format:
200
+ ONLY return the raw JSON object. Do not include markdown formatting (like \`\`\`json).
201
+ `;
202
+
203
+ const result = await model.generateContent(systemPrompt);
204
+ const response = result.response;
205
+ let text = response.text();
206
+
207
+ // Clean up potential markdown formatting
208
+ text = text.replace(/```json/g, "").replace(/```/g, "").trim();
209
+
210
+ try {
211
+ const workflowData = JSON.parse(text);
212
+ if (!workflowData.nodes || !workflowData.edges) {
213
+ throw new Error("Invalid workflow format generated");
214
+ }
215
+ return workflowData;
216
+ } catch {
217
+ console.warn(`Failed to parse workflow from ${modelName}:`, text);
218
+ // If specific parsing fails, maybe try another model?
219
+ // Or just re-throw to be caught by the outer loop?
220
+ throw new Error("Failed to parse generated workflow");
221
+ }
222
+
223
+ } catch (error) {
224
+ console.warn(`Gemini workflow generation failed with model ${modelName}:`, error);
225
+ lastError = error;
226
+ // Continue to next model
227
+ }
228
+ }
229
+
230
+ // If all models fail
231
+ throw lastError || new Error("Failed to generate workflow with all available Gemini models");
232
+
233
+ } catch (error) {
234
+ console.error("Error generating workflow from prompt:", error);
235
+ throw error;
236
+ }
237
+ }