openvolo 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +175 -0
  3. package/components.json +20 -0
  4. package/dist/cli.js +992 -0
  5. package/drizzle.config.ts +14 -0
  6. package/next.config.mjs +7 -0
  7. package/package.json +91 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/android-chrome-192x192.png +0 -0
  10. package/public/android-chrome-512x512.png +0 -0
  11. package/public/apple-touch-icon.png +0 -0
  12. package/public/assets/openvolo-logo-black.png +0 -0
  13. package/public/assets/openvolo-logo-name.png +0 -0
  14. package/public/assets/openvolo-logo-transparent.png +0 -0
  15. package/public/favicon-16x16.png +0 -0
  16. package/public/favicon-32x32.png +0 -0
  17. package/public/favicon.ico +0 -0
  18. package/public/site.webmanifest +19 -0
  19. package/src/app/api/analytics/agents/route.ts +30 -0
  20. package/src/app/api/analytics/content/route.ts +24 -0
  21. package/src/app/api/analytics/engagement/route.ts +24 -0
  22. package/src/app/api/analytics/overview/route.ts +22 -0
  23. package/src/app/api/analytics/sync-health/route.ts +22 -0
  24. package/src/app/api/contacts/[id]/identities/[identityId]/route.ts +24 -0
  25. package/src/app/api/contacts/[id]/identities/route.ts +61 -0
  26. package/src/app/api/contacts/[id]/route.ts +72 -0
  27. package/src/app/api/contacts/route.ts +91 -0
  28. package/src/app/api/content/[id]/route.ts +61 -0
  29. package/src/app/api/content/route.ts +48 -0
  30. package/src/app/api/platforms/gmail/auth/route.ts +50 -0
  31. package/src/app/api/platforms/gmail/callback/route.ts +126 -0
  32. package/src/app/api/platforms/gmail/route.ts +60 -0
  33. package/src/app/api/platforms/gmail/sync/route.ts +96 -0
  34. package/src/app/api/platforms/linkedin/auth/route.ts +40 -0
  35. package/src/app/api/platforms/linkedin/callback/route.ts +128 -0
  36. package/src/app/api/platforms/linkedin/import/route.ts +40 -0
  37. package/src/app/api/platforms/linkedin/route.ts +60 -0
  38. package/src/app/api/platforms/linkedin/sync/route.ts +85 -0
  39. package/src/app/api/platforms/x/auth/route.ts +52 -0
  40. package/src/app/api/platforms/x/browser-session/route.ts +79 -0
  41. package/src/app/api/platforms/x/callback/route.ts +130 -0
  42. package/src/app/api/platforms/x/compose/route.ts +247 -0
  43. package/src/app/api/platforms/x/engage/route.ts +113 -0
  44. package/src/app/api/platforms/x/enrich/route.ts +79 -0
  45. package/src/app/api/platforms/x/route.ts +63 -0
  46. package/src/app/api/platforms/x/sync/route.ts +142 -0
  47. package/src/app/api/settings/route.ts +43 -0
  48. package/src/app/api/settings/search-api/route.ts +180 -0
  49. package/src/app/api/tasks/[id]/route.ts +60 -0
  50. package/src/app/api/tasks/route.ts +39 -0
  51. package/src/app/api/workflows/[id]/progress/route.ts +45 -0
  52. package/src/app/api/workflows/[id]/route.ts +20 -0
  53. package/src/app/api/workflows/route.ts +30 -0
  54. package/src/app/api/workflows/run-agent/route.ts +44 -0
  55. package/src/app/api/workflows/templates/[id]/activate/route.ts +64 -0
  56. package/src/app/api/workflows/templates/[id]/route.ts +75 -0
  57. package/src/app/api/workflows/templates/route.ts +60 -0
  58. package/src/app/dashboard/analytics/analytics-dashboard.tsx +535 -0
  59. package/src/app/dashboard/analytics/page.tsx +15 -0
  60. package/src/app/dashboard/contacts/[id]/contact-detail-client.tsx +334 -0
  61. package/src/app/dashboard/contacts/[id]/page.tsx +21 -0
  62. package/src/app/dashboard/contacts/contact-list-client.tsx +213 -0
  63. package/src/app/dashboard/contacts/page.tsx +38 -0
  64. package/src/app/dashboard/content/[id]/engagement-actions.tsx +167 -0
  65. package/src/app/dashboard/content/[id]/page.tsx +253 -0
  66. package/src/app/dashboard/content/content-list-client.tsx +428 -0
  67. package/src/app/dashboard/content/page.tsx +39 -0
  68. package/src/app/dashboard/help/page.tsx +1247 -0
  69. package/src/app/dashboard/layout.tsx +19 -0
  70. package/src/app/dashboard/page.tsx +187 -0
  71. package/src/app/dashboard/settings/page.tsx +1664 -0
  72. package/src/app/dashboard/workflows/[id]/page.tsx +90 -0
  73. package/src/app/dashboard/workflows/[id]/workflow-detail-steps.tsx +55 -0
  74. package/src/app/dashboard/workflows/[id]/workflow-run-live.tsx +195 -0
  75. package/src/app/dashboard/workflows/activate-dialog.tsx +251 -0
  76. package/src/app/dashboard/workflows/page.tsx +41 -0
  77. package/src/app/dashboard/workflows/template-gallery.tsx +201 -0
  78. package/src/app/dashboard/workflows/workflow-quick-actions.tsx +121 -0
  79. package/src/app/dashboard/workflows/workflow-view-switcher.tsx +62 -0
  80. package/src/app/globals.css +232 -0
  81. package/src/app/layout.tsx +57 -0
  82. package/src/app/page.tsx +5 -0
  83. package/src/components/add-contact-dialog.tsx +74 -0
  84. package/src/components/add-task-dialog.tsx +153 -0
  85. package/src/components/animated-stat.tsx +53 -0
  86. package/src/components/app-sidebar.tsx +130 -0
  87. package/src/components/charts/area-chart-card.tsx +99 -0
  88. package/src/components/charts/bar-chart-card.tsx +128 -0
  89. package/src/components/charts/chart-skeleton.tsx +43 -0
  90. package/src/components/charts/donut-chart-card.tsx +100 -0
  91. package/src/components/charts/ranked-table-card.tsx +127 -0
  92. package/src/components/charts/stat-cards-row.tsx +45 -0
  93. package/src/components/compose-dialog.tsx +344 -0
  94. package/src/components/contact-form.tsx +218 -0
  95. package/src/components/dashboard-greeting.tsx +27 -0
  96. package/src/components/dashboard-header.tsx +87 -0
  97. package/src/components/empty-state.tsx +32 -0
  98. package/src/components/enrich-button.tsx +107 -0
  99. package/src/components/enrichment-score-badge.tsx +30 -0
  100. package/src/components/funnel-stage-badge.tsx +19 -0
  101. package/src/components/funnel-visualization.tsx +66 -0
  102. package/src/components/identities-section.tsx +219 -0
  103. package/src/components/pagination-controls.tsx +115 -0
  104. package/src/components/platform-connection-card.tsx +292 -0
  105. package/src/components/priority-badge.tsx +17 -0
  106. package/src/components/step-output-renderer.tsx +63 -0
  107. package/src/components/tweet-input.tsx +126 -0
  108. package/src/components/ui/alert-dialog.tsx +196 -0
  109. package/src/components/ui/avatar.tsx +109 -0
  110. package/src/components/ui/badge.tsx +48 -0
  111. package/src/components/ui/button.tsx +64 -0
  112. package/src/components/ui/card.tsx +92 -0
  113. package/src/components/ui/chart.tsx +357 -0
  114. package/src/components/ui/dialog.tsx +158 -0
  115. package/src/components/ui/dropdown-menu.tsx +257 -0
  116. package/src/components/ui/input.tsx +21 -0
  117. package/src/components/ui/label.tsx +24 -0
  118. package/src/components/ui/progress.tsx +31 -0
  119. package/src/components/ui/scroll-area.tsx +58 -0
  120. package/src/components/ui/select.tsx +190 -0
  121. package/src/components/ui/separator.tsx +28 -0
  122. package/src/components/ui/sheet.tsx +143 -0
  123. package/src/components/ui/sidebar.tsx +726 -0
  124. package/src/components/ui/skeleton.tsx +13 -0
  125. package/src/components/ui/table.tsx +116 -0
  126. package/src/components/ui/tabs.tsx +91 -0
  127. package/src/components/ui/textarea.tsx +18 -0
  128. package/src/components/ui/tooltip.tsx +57 -0
  129. package/src/components/workflow-graph-view.tsx +205 -0
  130. package/src/components/workflow-kanban-view.tsx +69 -0
  131. package/src/components/workflow-list-view.tsx +201 -0
  132. package/src/components/workflow-progress-card.tsx +150 -0
  133. package/src/components/workflow-run-card.tsx +144 -0
  134. package/src/components/workflow-step-timeline.tsx +173 -0
  135. package/src/components/workflow-swimlane-view.tsx +87 -0
  136. package/src/hooks/use-mobile.ts +19 -0
  137. package/src/hooks/use-workflow-polling.ts +85 -0
  138. package/src/lib/agents/router.ts +79 -0
  139. package/src/lib/agents/run-agent-workflow.ts +605 -0
  140. package/src/lib/agents/tools/browser-scrape.ts +118 -0
  141. package/src/lib/agents/tools/enrich-contact.ts +128 -0
  142. package/src/lib/agents/tools/search-web.ts +473 -0
  143. package/src/lib/agents/tools/update-progress.ts +40 -0
  144. package/src/lib/agents/tools/url-fetch.ts +152 -0
  145. package/src/lib/agents/types.ts +79 -0
  146. package/src/lib/analytics/utils.ts +33 -0
  147. package/src/lib/auth/claude-auth.ts +134 -0
  148. package/src/lib/auth/crypto.ts +58 -0
  149. package/src/lib/browser/anti-detection.ts +79 -0
  150. package/src/lib/browser/extractors/profile-merger.ts +71 -0
  151. package/src/lib/browser/extractors/profile-parser.ts +133 -0
  152. package/src/lib/browser/platforms/x-scraper.ts +269 -0
  153. package/src/lib/browser/scraper.ts +92 -0
  154. package/src/lib/browser/session.ts +229 -0
  155. package/src/lib/browser/types.ts +80 -0
  156. package/src/lib/db/client.ts +24 -0
  157. package/src/lib/db/enrichment.ts +90 -0
  158. package/src/lib/db/migrate-identities.ts +95 -0
  159. package/src/lib/db/migrate.ts +33 -0
  160. package/src/lib/db/migrations/0000_tired_thanos.sql +296 -0
  161. package/src/lib/db/migrations/meta/0000_snapshot.json +2169 -0
  162. package/src/lib/db/migrations/meta/_journal.json +13 -0
  163. package/src/lib/db/queries/analytics.ts +449 -0
  164. package/src/lib/db/queries/contacts.ts +170 -0
  165. package/src/lib/db/queries/content.ts +215 -0
  166. package/src/lib/db/queries/dashboard.ts +79 -0
  167. package/src/lib/db/queries/engagements.ts +35 -0
  168. package/src/lib/db/queries/identities.ts +51 -0
  169. package/src/lib/db/queries/platform-accounts.ts +53 -0
  170. package/src/lib/db/queries/sync.ts +74 -0
  171. package/src/lib/db/queries/tasks.ts +88 -0
  172. package/src/lib/db/queries/workflow-templates.ts +213 -0
  173. package/src/lib/db/queries/workflows.ts +167 -0
  174. package/src/lib/db/schema.ts +437 -0
  175. package/src/lib/db/seed-templates.ts +221 -0
  176. package/src/lib/db/types.ts +78 -0
  177. package/src/lib/pagination.ts +12 -0
  178. package/src/lib/platforms/adapter.ts +75 -0
  179. package/src/lib/platforms/gmail/adapter.ts +112 -0
  180. package/src/lib/platforms/gmail/auth.ts +137 -0
  181. package/src/lib/platforms/gmail/client.ts +255 -0
  182. package/src/lib/platforms/gmail/mappers.ts +125 -0
  183. package/src/lib/platforms/gmail/oauth-state-store.ts +65 -0
  184. package/src/lib/platforms/index.ts +22 -0
  185. package/src/lib/platforms/linkedin/adapter.ts +164 -0
  186. package/src/lib/platforms/linkedin/auth.ts +124 -0
  187. package/src/lib/platforms/linkedin/client.ts +183 -0
  188. package/src/lib/platforms/linkedin/csv-import.ts +283 -0
  189. package/src/lib/platforms/linkedin/mappers.ts +123 -0
  190. package/src/lib/platforms/linkedin/oauth-state-store.ts +65 -0
  191. package/src/lib/platforms/rate-limiter.ts +88 -0
  192. package/src/lib/platforms/sync-contacts.ts +121 -0
  193. package/src/lib/platforms/sync-content.ts +225 -0
  194. package/src/lib/platforms/sync-gmail-contacts.ts +186 -0
  195. package/src/lib/platforms/sync-gmail-metadata.ts +158 -0
  196. package/src/lib/platforms/sync-linkedin-contacts.ts +148 -0
  197. package/src/lib/platforms/sync-x-profiles.ts +280 -0
  198. package/src/lib/platforms/x/adapter.ts +129 -0
  199. package/src/lib/platforms/x/auth.ts +165 -0
  200. package/src/lib/platforms/x/client.ts +390 -0
  201. package/src/lib/platforms/x/mappers.ts +134 -0
  202. package/src/lib/platforms/x/pkce-store.ts +67 -0
  203. package/src/lib/utils.ts +6 -0
  204. package/src/lib/workflows/format-error.test.ts +177 -0
  205. package/src/lib/workflows/format-error.ts +207 -0
  206. package/src/lib/workflows/run-sync-workflow.ts +141 -0
  207. package/src/lib/workflows/types.ts +71 -0
  208. package/tsconfig.json +42 -0
@@ -0,0 +1,201 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Search,
9
+ Sparkles,
10
+ Trash2,
11
+ Bot,
12
+ Loader2,
13
+ Play,
14
+ DollarSign,
15
+ Clock,
16
+ Hash,
17
+ } from "lucide-react";
18
+ import { ActivateDialog } from "./activate-dialog";
19
+
20
+ interface Template {
21
+ id: string;
22
+ name: string;
23
+ description: string | null;
24
+ templateType: string;
25
+ status: string;
26
+ systemPrompt: string | null;
27
+ targetPersona: string | null;
28
+ estimatedCost: number;
29
+ totalRuns: number;
30
+ lastRunAt: number | null;
31
+ config: string;
32
+ }
33
+
34
+ const TYPE_ICONS: Record<string, typeof Search> = {
35
+ prospecting: Search,
36
+ enrichment: Sparkles,
37
+ pruning: Trash2,
38
+ outreach: Bot,
39
+ engagement: Bot,
40
+ content: Bot,
41
+ nurture: Bot,
42
+ };
43
+
44
+ const TYPE_LABELS: Record<string, string> = {
45
+ prospecting: "Search",
46
+ enrichment: "Enrich",
47
+ pruning: "Prune",
48
+ outreach: "Sequence",
49
+ engagement: "Agent",
50
+ content: "Agent",
51
+ nurture: "Agent",
52
+ };
53
+
54
+ const TYPE_COLORS: Record<string, string> = {
55
+ prospecting: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300",
56
+ enrichment: "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300",
57
+ pruning: "bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300",
58
+ };
59
+
60
+ function formatRelativeTime(unixSeconds: number): string {
61
+ const now = Math.floor(Date.now() / 1000);
62
+ const diff = now - unixSeconds;
63
+ if (diff < 60) return "just now";
64
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
65
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
66
+ return `${Math.floor(diff / 86400)}d ago`;
67
+ }
68
+
69
+ export function TemplateGallery() {
70
+ const [templates, setTemplates] = useState<Template[]>([]);
71
+ const [loading, setLoading] = useState(true);
72
+ const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
73
+ const [filter, setFilter] = useState<string>("all");
74
+
75
+ useEffect(() => {
76
+ fetch("/api/workflows/templates?pageSize=50")
77
+ .then((r) => r.json())
78
+ .then((data) => {
79
+ setTemplates(data.data ?? []);
80
+ setLoading(false);
81
+ })
82
+ .catch(() => setLoading(false));
83
+ }, []);
84
+
85
+ const filteredTemplates =
86
+ filter === "all"
87
+ ? templates
88
+ : templates.filter((t) => {
89
+ if (filter === "search") return t.templateType === "prospecting";
90
+ if (filter === "enrich") return t.templateType === "enrichment";
91
+ if (filter === "prune") return t.templateType === "pruning";
92
+ return true;
93
+ });
94
+
95
+ if (loading) {
96
+ return (
97
+ <div className="flex items-center justify-center py-8">
98
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
99
+ </div>
100
+ );
101
+ }
102
+
103
+ if (templates.length === 0) {
104
+ return null;
105
+ }
106
+
107
+ return (
108
+ <div className="space-y-4">
109
+ <div className="flex items-center justify-between">
110
+ <h2 className="text-lg font-semibold">Agent Templates</h2>
111
+ <div className="flex items-center gap-1">
112
+ {[
113
+ { key: "all", label: "All" },
114
+ { key: "search", label: "Search" },
115
+ { key: "enrich", label: "Enrich" },
116
+ { key: "prune", label: "Prune" },
117
+ ].map((tab) => (
118
+ <Button
119
+ key={tab.key}
120
+ variant={filter === tab.key ? "default" : "ghost"}
121
+ size="sm"
122
+ className="h-7 text-xs"
123
+ onClick={() => setFilter(tab.key)}
124
+ >
125
+ {tab.label}
126
+ </Button>
127
+ ))}
128
+ </div>
129
+ </div>
130
+
131
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
132
+ {filteredTemplates.map((template) => {
133
+ const Icon = TYPE_ICONS[template.templateType] ?? Bot;
134
+ const typeLabel = TYPE_LABELS[template.templateType] ?? "Agent";
135
+ const typeColor = TYPE_COLORS[template.templateType] ?? "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300";
136
+
137
+ return (
138
+ <Card
139
+ key={template.id}
140
+ className="hover:border-primary/50 transition-colors"
141
+ >
142
+ <CardHeader className="pb-3">
143
+ <div className="flex items-start justify-between gap-2">
144
+ <div className="flex items-center gap-2">
145
+ <div className="rounded-md bg-muted p-1.5">
146
+ <Icon className="h-3.5 w-3.5 text-muted-foreground" />
147
+ </div>
148
+ <CardTitle className="text-sm">{template.name}</CardTitle>
149
+ </div>
150
+ <Badge
151
+ variant="outline"
152
+ className={`text-[10px] px-1.5 py-0 shrink-0 ${typeColor}`}
153
+ >
154
+ {typeLabel}
155
+ </Badge>
156
+ </div>
157
+ <CardDescription className="text-xs line-clamp-2 mt-1">
158
+ {template.description}
159
+ </CardDescription>
160
+ </CardHeader>
161
+ <CardContent className="pt-0 space-y-3">
162
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
163
+ <span className="flex items-center gap-1">
164
+ <DollarSign className="h-3 w-3" />
165
+ ~${template.estimatedCost.toFixed(2)}
166
+ </span>
167
+ <span className="flex items-center gap-1">
168
+ <Hash className="h-3 w-3" />
169
+ {template.totalRuns} run{template.totalRuns !== 1 ? "s" : ""}
170
+ </span>
171
+ {template.lastRunAt && (
172
+ <span className="flex items-center gap-1">
173
+ <Clock className="h-3 w-3" />
174
+ {formatRelativeTime(template.lastRunAt)}
175
+ </span>
176
+ )}
177
+ </div>
178
+ <Button
179
+ size="sm"
180
+ className="w-full h-8"
181
+ onClick={() => setActiveTemplate(template)}
182
+ >
183
+ <Play className="mr-1.5 h-3 w-3" />
184
+ Run
185
+ </Button>
186
+ </CardContent>
187
+ </Card>
188
+ );
189
+ })}
190
+ </div>
191
+
192
+ {activeTemplate && (
193
+ <ActivateDialog
194
+ template={activeTemplate}
195
+ open={!!activeTemplate}
196
+ onClose={() => setActiveTemplate(null)}
197
+ />
198
+ )}
199
+ </div>
200
+ );
201
+ }
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuLabel,
12
+ } from "@/components/ui/dropdown-menu";
13
+ import { Loader2, Plus, RefreshCw, Sparkles, Mail } from "lucide-react";
14
+ import { useRouter } from "next/navigation";
15
+
16
+ type SyncAction = {
17
+ label: string;
18
+ platform: string;
19
+ endpoint: string;
20
+ body: Record<string, unknown>;
21
+ icon: typeof RefreshCw;
22
+ };
23
+
24
+ const SYNC_ACTIONS: SyncAction[] = [
25
+ { label: "Sync X Tweets", platform: "x", endpoint: "/api/platforms/x/sync", body: { type: "tweets" }, icon: RefreshCw },
26
+ { label: "Sync X Mentions", platform: "x", endpoint: "/api/platforms/x/sync", body: { type: "mentions" }, icon: RefreshCw },
27
+ { label: "Sync X Contacts", platform: "x", endpoint: "/api/platforms/x/sync", body: { type: "contacts" }, icon: RefreshCw },
28
+ { label: "Sync Gmail Contacts", platform: "gmail", endpoint: "/api/platforms/gmail/sync", body: { type: "contacts" }, icon: Mail },
29
+ { label: "Sync Gmail Metadata", platform: "gmail", endpoint: "/api/platforms/gmail/sync", body: { type: "metadata" }, icon: Mail },
30
+ { label: "Sync LinkedIn", platform: "linkedin", endpoint: "/api/platforms/linkedin/sync", body: { type: "contacts" }, icon: RefreshCw },
31
+ { label: "Enrich X Profiles", platform: "x", endpoint: "/api/platforms/x/enrich", body: {}, icon: Sparkles },
32
+ ];
33
+
34
+ export function WorkflowQuickActions() {
35
+ const [running, setRunning] = useState<string | null>(null);
36
+ const router = useRouter();
37
+
38
+ async function handleAction(action: SyncAction) {
39
+ setRunning(action.label);
40
+ try {
41
+ const res = await fetch(action.endpoint, {
42
+ method: "POST",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify(action.body),
45
+ });
46
+
47
+ if (res.ok) {
48
+ const data = await res.json();
49
+ if (data.workflowRunId) {
50
+ router.push(`/dashboard/workflows/${data.workflowRunId}`);
51
+ return;
52
+ }
53
+ }
54
+ // Refresh page to show new workflow
55
+ router.refresh();
56
+ } catch {
57
+ // Errors will be visible in the workflow run
58
+ router.refresh();
59
+ } finally {
60
+ setRunning(null);
61
+ }
62
+ }
63
+
64
+ return (
65
+ <DropdownMenu>
66
+ <DropdownMenuTrigger asChild>
67
+ <Button variant="outline" size="sm" disabled={!!running}>
68
+ {running ? (
69
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
70
+ ) : (
71
+ <Plus className="mr-2 h-4 w-4" />
72
+ )}
73
+ {running ?? "Quick Action"}
74
+ </Button>
75
+ </DropdownMenuTrigger>
76
+ <DropdownMenuContent align="end" className="w-56">
77
+ <DropdownMenuLabel>Platform Sync</DropdownMenuLabel>
78
+ {SYNC_ACTIONS.filter((a) => a.platform === "x" && a.endpoint.includes("sync")).map((action) => (
79
+ <DropdownMenuItem
80
+ key={action.label}
81
+ onClick={() => handleAction(action)}
82
+ >
83
+ <action.icon className="mr-2 h-4 w-4" />
84
+ {action.label}
85
+ </DropdownMenuItem>
86
+ ))}
87
+ <DropdownMenuSeparator />
88
+ {SYNC_ACTIONS.filter((a) => a.platform === "gmail").map((action) => (
89
+ <DropdownMenuItem
90
+ key={action.label}
91
+ onClick={() => handleAction(action)}
92
+ >
93
+ <action.icon className="mr-2 h-4 w-4" />
94
+ {action.label}
95
+ </DropdownMenuItem>
96
+ ))}
97
+ <DropdownMenuSeparator />
98
+ {SYNC_ACTIONS.filter((a) => a.platform === "linkedin").map((action) => (
99
+ <DropdownMenuItem
100
+ key={action.label}
101
+ onClick={() => handleAction(action)}
102
+ >
103
+ <action.icon className="mr-2 h-4 w-4" />
104
+ {action.label}
105
+ </DropdownMenuItem>
106
+ ))}
107
+ <DropdownMenuSeparator />
108
+ <DropdownMenuLabel>Enrichment</DropdownMenuLabel>
109
+ {SYNC_ACTIONS.filter((a) => a.endpoint.includes("enrich")).map((action) => (
110
+ <DropdownMenuItem
111
+ key={action.label}
112
+ onClick={() => handleAction(action)}
113
+ >
114
+ <action.icon className="mr-2 h-4 w-4" />
115
+ {action.label}
116
+ </DropdownMenuItem>
117
+ ))}
118
+ </DropdownMenuContent>
119
+ </DropdownMenu>
120
+ );
121
+ }
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { Suspense } from "react";
4
+ import { useSearchParams, useRouter, usePathname } from "next/navigation";
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
6
+ import { List, Columns3, Rows3 } from "lucide-react";
7
+ import { WorkflowListView } from "@/components/workflow-list-view";
8
+ import { WorkflowKanbanView } from "@/components/workflow-kanban-view";
9
+ import { WorkflowSwimlaneView } from "@/components/workflow-swimlane-view";
10
+ import type { WorkflowRun } from "@/lib/db/types";
11
+
12
+ function ViewSwitcherInner({ runs }: { runs: WorkflowRun[] }) {
13
+ const searchParams = useSearchParams();
14
+ const router = useRouter();
15
+ const pathname = usePathname();
16
+ const currentView = searchParams.get("view") ?? "list";
17
+
18
+ function handleViewChange(view: string) {
19
+ const params = new URLSearchParams(searchParams.toString());
20
+ params.set("view", view);
21
+ router.replace(`${pathname}?${params.toString()}`, { scroll: false });
22
+ }
23
+
24
+ return (
25
+ <Tabs value={currentView} onValueChange={handleViewChange}>
26
+ <TabsList>
27
+ <TabsTrigger value="list" className="gap-1.5">
28
+ <List className="h-3.5 w-3.5" />
29
+ List
30
+ </TabsTrigger>
31
+ <TabsTrigger value="kanban" className="gap-1.5">
32
+ <Columns3 className="h-3.5 w-3.5" />
33
+ Kanban
34
+ </TabsTrigger>
35
+ <TabsTrigger value="swimlane" className="gap-1.5">
36
+ <Rows3 className="h-3.5 w-3.5" />
37
+ Swimlane
38
+ </TabsTrigger>
39
+ </TabsList>
40
+
41
+ <TabsContent value="list" className="mt-4">
42
+ <WorkflowListView runs={runs} />
43
+ </TabsContent>
44
+
45
+ <TabsContent value="kanban" className="mt-4">
46
+ <WorkflowKanbanView runs={runs} />
47
+ </TabsContent>
48
+
49
+ <TabsContent value="swimlane" className="mt-4">
50
+ <WorkflowSwimlaneView runs={runs} />
51
+ </TabsContent>
52
+ </Tabs>
53
+ );
54
+ }
55
+
56
+ export function WorkflowViewSwitcher({ runs }: { runs: WorkflowRun[] }) {
57
+ return (
58
+ <Suspense fallback={<WorkflowListView runs={runs} />}>
59
+ <ViewSwitcherInner runs={runs} />
60
+ </Suspense>
61
+ );
62
+ }
@@ -0,0 +1,232 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @plugin "@tailwindcss/typography";
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ @theme inline {
8
+ --color-background: var(--background);
9
+ --color-foreground: var(--foreground);
10
+ --font-sans: var(--font-body);
11
+ --font-mono: var(--font-mono);
12
+ --font-display: var(--font-display);
13
+ --color-sidebar-ring: var(--sidebar-ring);
14
+ --color-sidebar-border: var(--sidebar-border);
15
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
16
+ --color-sidebar-accent: var(--sidebar-accent);
17
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
18
+ --color-sidebar-primary: var(--sidebar-primary);
19
+ --color-sidebar-foreground: var(--sidebar-foreground);
20
+ --color-sidebar: var(--sidebar);
21
+ --color-chart-5: var(--chart-5);
22
+ --color-chart-4: var(--chart-4);
23
+ --color-chart-3: var(--chart-3);
24
+ --color-chart-2: var(--chart-2);
25
+ --color-chart-1: var(--chart-1);
26
+ --color-chart-6: var(--chart-6);
27
+ --color-chart-7: var(--chart-7);
28
+ --color-chart-8: var(--chart-8);
29
+ --color-ring: var(--ring);
30
+ --color-input: var(--input);
31
+ --color-border: var(--border);
32
+ --color-destructive: var(--destructive);
33
+ --color-accent-foreground: var(--accent-foreground);
34
+ --color-accent: var(--accent);
35
+ --color-muted-foreground: var(--muted-foreground);
36
+ --color-muted: var(--muted);
37
+ --color-secondary-foreground: var(--secondary-foreground);
38
+ --color-secondary: var(--secondary);
39
+ --color-primary-foreground: var(--primary-foreground);
40
+ --color-primary: var(--primary);
41
+ --color-popover-foreground: var(--popover-foreground);
42
+ --color-popover: var(--popover);
43
+ --color-card-foreground: var(--card-foreground);
44
+ --color-card: var(--card);
45
+ --radius-sm: calc(var(--radius) - 4px);
46
+ --radius-md: calc(var(--radius) - 2px);
47
+ --radius-lg: var(--radius);
48
+ --radius-xl: calc(var(--radius) + 4px);
49
+
50
+ /* Animation keyframes */
51
+ --animate-shimmer: shimmer 2s linear infinite;
52
+ --animate-fade-slide-in: fadeSlideIn 0.5s ease-out forwards;
53
+ }
54
+
55
+ /* ─── Light mode: lavender-tinted neutrals ─── */
56
+ :root {
57
+ --radius: 0.625rem;
58
+ --background: oklch(0.995 0.005 270);
59
+ --foreground: oklch(0.17 0.02 270);
60
+ --card: oklch(1 0.003 270);
61
+ --card-foreground: oklch(0.17 0.02 270);
62
+ --popover: oklch(1 0.003 270);
63
+ --popover-foreground: oklch(0.17 0.02 270);
64
+ /* Primary: brand cyan */
65
+ --primary: oklch(0.55 0.18 195);
66
+ --primary-foreground: oklch(0.99 0 0);
67
+ --secondary: oklch(0.96 0.01 270);
68
+ --secondary-foreground: oklch(0.22 0.02 270);
69
+ --muted: oklch(0.96 0.01 270);
70
+ --muted-foreground: oklch(0.52 0.02 270);
71
+ /* Accent: soft pink */
72
+ --accent: oklch(0.96 0.03 340);
73
+ --accent-foreground: oklch(0.22 0.02 270);
74
+ --destructive: oklch(0.577 0.245 27.325);
75
+ --border: oklch(0.91 0.01 270);
76
+ --input: oklch(0.91 0.01 270);
77
+ --ring: oklch(0.55 0.18 195);
78
+ /* Chart colors: brand gradient stops */
79
+ --chart-1: oklch(0.65 0.15 195);
80
+ --chart-2: oklch(0.65 0.12 280);
81
+ --chart-3: oklch(0.72 0.12 340);
82
+ --chart-4: oklch(0.72 0.13 45);
83
+ --chart-5: oklch(0.60 0.14 270);
84
+ --chart-6: oklch(0.58 0.16 150);
85
+ --chart-7: oklch(0.68 0.10 220);
86
+ --chart-8: oklch(0.75 0.08 100);
87
+ /* Sidebar */
88
+ --sidebar: oklch(0.985 0.008 270);
89
+ --sidebar-foreground: oklch(0.17 0.02 270);
90
+ --sidebar-primary: oklch(0.55 0.18 195);
91
+ --sidebar-primary-foreground: oklch(0.99 0 0);
92
+ --sidebar-accent: oklch(0.95 0.02 270);
93
+ --sidebar-accent-foreground: oklch(0.22 0.02 270);
94
+ --sidebar-border: oklch(0.91 0.01 270);
95
+ --sidebar-ring: oklch(0.55 0.18 195);
96
+ }
97
+
98
+ /* ─── Dark mode: deep indigo-tinted darks ─── */
99
+ .dark {
100
+ --background: oklch(0.15 0.02 270);
101
+ --foreground: oklch(0.96 0.01 270);
102
+ --card: oklch(0.18 0.02 270);
103
+ --card-foreground: oklch(0.96 0.01 270);
104
+ --popover: oklch(0.18 0.02 270);
105
+ --popover-foreground: oklch(0.96 0.01 270);
106
+ --primary: oklch(0.65 0.18 195);
107
+ --primary-foreground: oklch(0.15 0.02 270);
108
+ --secondary: oklch(0.25 0.02 270);
109
+ --secondary-foreground: oklch(0.96 0.01 270);
110
+ --muted: oklch(0.25 0.02 270);
111
+ --muted-foreground: oklch(0.65 0.02 270);
112
+ --accent: oklch(0.25 0.03 340);
113
+ --accent-foreground: oklch(0.96 0.01 270);
114
+ --destructive: oklch(0.577 0.245 27.325);
115
+ --border: oklch(0.28 0.02 270);
116
+ --input: oklch(0.28 0.02 270);
117
+ --ring: oklch(0.65 0.18 195);
118
+ --chart-1: oklch(0.65 0.15 195);
119
+ --chart-2: oklch(0.65 0.12 280);
120
+ --chart-3: oklch(0.72 0.12 340);
121
+ --chart-4: oklch(0.72 0.13 45);
122
+ --chart-5: oklch(0.60 0.14 270);
123
+ --chart-6: oklch(0.58 0.16 150);
124
+ --chart-7: oklch(0.68 0.10 220);
125
+ --chart-8: oklch(0.75 0.08 100);
126
+ --sidebar: oklch(0.17 0.025 270);
127
+ --sidebar-foreground: oklch(0.96 0.01 270);
128
+ --sidebar-primary: oklch(0.65 0.18 195);
129
+ --sidebar-primary-foreground: oklch(0.99 0 0);
130
+ --sidebar-accent: oklch(0.25 0.03 270);
131
+ --sidebar-accent-foreground: oklch(0.96 0.01 270);
132
+ --sidebar-border: oklch(0.28 0.02 270);
133
+ --sidebar-ring: oklch(0.65 0.18 195);
134
+ }
135
+
136
+ @layer base {
137
+ * {
138
+ @apply border-border outline-ring/50;
139
+ }
140
+ body {
141
+ @apply bg-background text-foreground;
142
+ }
143
+ }
144
+
145
+ /* ─── Brand gradient utilities ─── */
146
+ @layer utilities {
147
+ .gradient-brand {
148
+ background: linear-gradient(135deg, oklch(0.75 0.15 195), oklch(0.70 0.12 280), oklch(0.78 0.12 340), oklch(0.78 0.13 45));
149
+ }
150
+ .gradient-brand-subtle {
151
+ background: linear-gradient(135deg, oklch(0.95 0.04 195), oklch(0.94 0.03 280), oklch(0.96 0.03 340), oklch(0.96 0.03 45));
152
+ }
153
+ .dark .gradient-brand-subtle {
154
+ background: linear-gradient(135deg, oklch(0.25 0.04 195), oklch(0.24 0.03 280), oklch(0.26 0.03 340), oklch(0.26 0.03 45));
155
+ }
156
+ .text-gradient-brand {
157
+ background: linear-gradient(135deg, oklch(0.55 0.18 195), oklch(0.50 0.15 280), oklch(0.60 0.15 340));
158
+ -webkit-background-clip: text;
159
+ -webkit-text-fill-color: transparent;
160
+ background-clip: text;
161
+ }
162
+ .border-gradient-brand {
163
+ border-image: linear-gradient(135deg, oklch(0.75 0.15 195), oklch(0.70 0.12 280), oklch(0.78 0.12 340), oklch(0.78 0.13 45)) 1;
164
+ }
165
+
166
+ /* Type scale */
167
+ .text-display {
168
+ font-family: var(--font-display);
169
+ font-weight: 700;
170
+ letter-spacing: -0.02em;
171
+ line-height: 1.1;
172
+ }
173
+ .text-heading-1 {
174
+ font-family: var(--font-display);
175
+ font-weight: 700;
176
+ font-size: 1.875rem;
177
+ letter-spacing: -0.02em;
178
+ line-height: 1.2;
179
+ }
180
+ .text-heading-2 {
181
+ font-family: var(--font-display);
182
+ font-weight: 600;
183
+ font-size: 1.25rem;
184
+ letter-spacing: -0.01em;
185
+ line-height: 1.3;
186
+ }
187
+ .text-heading-3 {
188
+ font-family: var(--font-display);
189
+ font-weight: 600;
190
+ font-size: 1rem;
191
+ line-height: 1.4;
192
+ }
193
+ .text-label {
194
+ font-family: var(--font-display);
195
+ font-weight: 500;
196
+ font-size: 0.8125rem;
197
+ letter-spacing: 0.01em;
198
+ text-transform: uppercase;
199
+ }
200
+ }
201
+
202
+ /* ─── Animation keyframes ─── */
203
+ @keyframes shimmer {
204
+ 0% { background-position: -200% 0; }
205
+ 100% { background-position: 200% 0; }
206
+ }
207
+
208
+ @keyframes fadeSlideIn {
209
+ from {
210
+ opacity: 0;
211
+ transform: translateY(8px);
212
+ }
213
+ to {
214
+ opacity: 1;
215
+ transform: translateY(0);
216
+ }
217
+ }
218
+
219
+ @keyframes stepSlideIn {
220
+ from {
221
+ opacity: 0;
222
+ transform: translateX(-12px);
223
+ }
224
+ to {
225
+ opacity: 1;
226
+ transform: translateX(0);
227
+ }
228
+ }
229
+
230
+ .animate-step-slide-in {
231
+ animation: stepSlideIn 0.3s ease-out both;
232
+ }
@@ -0,0 +1,57 @@
1
+ import type { Metadata } from "next";
2
+ import { Plus_Jakarta_Sans, Inter, JetBrains_Mono } from "next/font/google";
3
+ import { ThemeProvider } from "next-themes";
4
+ import "./globals.css";
5
+
6
+ const plusJakarta = Plus_Jakarta_Sans({
7
+ variable: "--font-display",
8
+ subsets: ["latin"],
9
+ weight: ["400", "500", "600", "700", "800"],
10
+ });
11
+
12
+ const inter = Inter({
13
+ variable: "--font-body",
14
+ subsets: ["latin"],
15
+ });
16
+
17
+ const jetbrainsMono = JetBrains_Mono({
18
+ variable: "--font-mono",
19
+ subsets: ["latin"],
20
+ });
21
+
22
+ export const metadata: Metadata = {
23
+ title: "OpenVolo — AI-Native Social CRM",
24
+ description: "Manage X/Twitter and LinkedIn with Claude-powered agents",
25
+ icons: {
26
+ icon: [
27
+ { url: "/favicon.ico", sizes: "any" },
28
+ { url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
29
+ { url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
30
+ ],
31
+ apple: "/apple-touch-icon.png",
32
+ },
33
+ manifest: "/site.webmanifest",
34
+ };
35
+
36
+ export default function RootLayout({
37
+ children,
38
+ }: {
39
+ children: React.ReactNode;
40
+ }) {
41
+ return (
42
+ <html lang="en" suppressHydrationWarning>
43
+ <body
44
+ className={`${plusJakarta.variable} ${inter.variable} ${jetbrainsMono.variable} font-sans antialiased`}
45
+ >
46
+ <ThemeProvider
47
+ attribute="class"
48
+ defaultTheme="system"
49
+ enableSystem
50
+ disableTransitionOnChange
51
+ >
52
+ {children}
53
+ </ThemeProvider>
54
+ </body>
55
+ </html>
56
+ );
57
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ export default function Home() {
4
+ redirect("/dashboard");
5
+ }