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,167 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Card, CardContent } from "@/components/ui/card";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Textarea } from "@/components/ui/textarea";
8
+ import { Heart, Repeat2, MessageCircle, Loader2, Send } from "lucide-react";
9
+ import type { Engagement } from "@/lib/db/types";
10
+
11
+ interface EngagementActionsProps {
12
+ tweetId: string;
13
+ contentPostId: string;
14
+ engagementHistory: Engagement[];
15
+ }
16
+
17
+ type EngageAction = "like" | "unlike" | "retweet" | "unretweet" | "reply";
18
+
19
+ export function EngagementActions({
20
+ tweetId,
21
+ contentPostId,
22
+ engagementHistory,
23
+ }: EngagementActionsProps) {
24
+ const router = useRouter();
25
+ const [loading, setLoading] = useState<EngageAction | null>(null);
26
+ const [error, setError] = useState<string | null>(null);
27
+ const [showReply, setShowReply] = useState(false);
28
+ const [replyText, setReplyText] = useState("");
29
+
30
+ // Determine initial toggle state from engagement history
31
+ const hasLiked = engagementHistory.some(
32
+ (e) => e.engagementType === "like" && e.source === "manual"
33
+ );
34
+ const hasRetweeted = engagementHistory.some(
35
+ (e) => e.engagementType === "retweet" && e.source === "manual"
36
+ );
37
+
38
+ const [liked, setLiked] = useState(hasLiked);
39
+ const [retweeted, setRetweeted] = useState(hasRetweeted);
40
+
41
+ async function handleEngage(action: EngageAction, text?: string) {
42
+ setLoading(action);
43
+ setError(null);
44
+
45
+ try {
46
+ const res = await fetch("/api/platforms/x/engage", {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify({ action, tweetId, contentPostId, text }),
50
+ });
51
+
52
+ const data = await res.json();
53
+
54
+ if (!res.ok) {
55
+ setError(data.error ?? "Action failed");
56
+ return;
57
+ }
58
+
59
+ // Update optimistic state
60
+ if (action === "like") setLiked(true);
61
+ if (action === "unlike") setLiked(false);
62
+ if (action === "retweet") setRetweeted(true);
63
+ if (action === "unretweet") setRetweeted(false);
64
+ if (action === "reply") {
65
+ setReplyText("");
66
+ setShowReply(false);
67
+ }
68
+
69
+ // Refresh to update engagement history
70
+ router.refresh();
71
+ } catch {
72
+ setError("Action failed. Please try again.");
73
+ } finally {
74
+ setLoading(null);
75
+ }
76
+ }
77
+
78
+ return (
79
+ <Card>
80
+ <CardContent className="pt-6 space-y-3">
81
+ <h3 className="text-sm font-medium">Actions</h3>
82
+
83
+ <div className="flex items-center gap-2">
84
+ {/* Like / Unlike */}
85
+ <Button
86
+ variant={liked ? "default" : "outline"}
87
+ size="sm"
88
+ disabled={loading !== null}
89
+ onClick={() => handleEngage(liked ? "unlike" : "like")}
90
+ >
91
+ {loading === "like" || loading === "unlike" ? (
92
+ <Loader2 className="mr-1 h-4 w-4 animate-spin" />
93
+ ) : (
94
+ <Heart
95
+ className={`mr-1 h-4 w-4 ${liked ? "fill-current" : ""}`}
96
+ />
97
+ )}
98
+ {liked ? "Liked" : "Like"}
99
+ </Button>
100
+
101
+ {/* Retweet / Unretweet */}
102
+ <Button
103
+ variant={retweeted ? "default" : "outline"}
104
+ size="sm"
105
+ disabled={loading !== null}
106
+ onClick={() => handleEngage(retweeted ? "unretweet" : "retweet")}
107
+ >
108
+ {loading === "retweet" || loading === "unretweet" ? (
109
+ <Loader2 className="mr-1 h-4 w-4 animate-spin" />
110
+ ) : (
111
+ <Repeat2 className="mr-1 h-4 w-4" />
112
+ )}
113
+ {retweeted ? "Retweeted" : "Retweet"}
114
+ </Button>
115
+
116
+ {/* Reply toggle */}
117
+ <Button
118
+ variant={showReply ? "default" : "outline"}
119
+ size="sm"
120
+ disabled={loading !== null}
121
+ onClick={() => setShowReply(!showReply)}
122
+ >
123
+ <MessageCircle className="mr-1 h-4 w-4" />
124
+ Reply
125
+ </Button>
126
+ </div>
127
+
128
+ {/* Reply input */}
129
+ {showReply && (
130
+ <div className="space-y-2">
131
+ <Textarea
132
+ placeholder="Write your reply..."
133
+ value={replyText}
134
+ onChange={(e) => setReplyText(e.target.value)}
135
+ rows={3}
136
+ className="resize-none"
137
+ />
138
+ <div className="flex items-center justify-between">
139
+ <span className="text-xs text-muted-foreground">
140
+ {replyText.length}/280
141
+ </span>
142
+ <Button
143
+ size="sm"
144
+ disabled={!replyText.trim() || replyText.length > 280 || loading !== null}
145
+ onClick={() => handleEngage("reply", replyText)}
146
+ >
147
+ {loading === "reply" ? (
148
+ <Loader2 className="mr-1 h-4 w-4 animate-spin" />
149
+ ) : (
150
+ <Send className="mr-1 h-4 w-4" />
151
+ )}
152
+ Send Reply
153
+ </Button>
154
+ </div>
155
+ </div>
156
+ )}
157
+
158
+ {/* Error */}
159
+ {error && (
160
+ <div className="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200">
161
+ {error}
162
+ </div>
163
+ )}
164
+ </CardContent>
165
+ </Card>
166
+ );
167
+ }
@@ -0,0 +1,253 @@
1
+ import { notFound } from "next/navigation";
2
+ import Link from "next/link";
3
+ import { getContentItem, getThreadItems } from "@/lib/db/queries/content";
4
+ import { listEngagementsByContentPost } from "@/lib/db/queries/engagements";
5
+ import { getPlatformAccountByPlatform } from "@/lib/db/queries/platform-accounts";
6
+ import { Card, CardContent } from "@/components/ui/card";
7
+ import { Badge } from "@/components/ui/badge";
8
+ import { Button } from "@/components/ui/button";
9
+ import { ArrowLeft, ExternalLink, Heart, MessageCircle, Repeat2, Quote } from "lucide-react";
10
+ import { EngagementActions } from "./engagement-actions";
11
+
12
+ const contentTypeLabels: Record<string, string> = {
13
+ post: "Post",
14
+ article: "Article",
15
+ thread: "Thread",
16
+ reply: "Reply",
17
+ image: "Image",
18
+ video: "Video",
19
+ email: "Email",
20
+ dm: "DM",
21
+ newsletter: "Newsletter",
22
+ };
23
+
24
+ function formatDate(unix: number | null | undefined): string {
25
+ if (!unix) return "Unknown date";
26
+ return new Date(unix * 1000).toLocaleDateString("en-US", {
27
+ weekday: "short",
28
+ month: "short",
29
+ day: "numeric",
30
+ year: "numeric",
31
+ hour: "numeric",
32
+ minute: "2-digit",
33
+ });
34
+ }
35
+
36
+ export default async function ContentDetailPage({
37
+ params,
38
+ }: {
39
+ params: Promise<{ id: string }>;
40
+ }) {
41
+ const { id } = await params;
42
+ const item = getContentItem(id);
43
+
44
+ if (!item) {
45
+ notFound();
46
+ }
47
+
48
+ const snapshot = item.post?.engagementSnapshot
49
+ ? JSON.parse(item.post.engagementSnapshot)
50
+ : null;
51
+
52
+ // Check if X account is connected (for engagement actions)
53
+ const xAccount = getPlatformAccountByPlatform("x");
54
+ const canEngage = !!xAccount && xAccount.status === "active" && !!item.post?.platformPostId;
55
+
56
+ // Get engagement history for this post
57
+ const engagementHistory = item.post
58
+ ? listEngagementsByContentPost(item.post.id)
59
+ : [];
60
+
61
+ // Get thread context if this item belongs to a thread
62
+ const threadItems = item.threadId ? getThreadItems(item.threadId) : [];
63
+ const isThread = threadItems.length > 1;
64
+
65
+ return (
66
+ <div className="space-y-6 max-w-3xl">
67
+ {/* Back navigation */}
68
+ <div className="flex items-center gap-2">
69
+ <Button variant="ghost" size="sm" asChild>
70
+ <Link href="/dashboard/content">
71
+ <ArrowLeft className="mr-1 h-4 w-4" />
72
+ Content
73
+ </Link>
74
+ </Button>
75
+ </div>
76
+
77
+ {/* Main content card */}
78
+ <Card>
79
+ <CardContent className="pt-6 space-y-4">
80
+ {/* Header: type badges + date */}
81
+ <div className="flex items-center justify-between">
82
+ <div className="flex items-center gap-2">
83
+ <Badge variant="secondary">
84
+ {contentTypeLabels[item.contentType] ?? item.contentType}
85
+ </Badge>
86
+ {item.origin && (
87
+ <Badge variant="outline">{item.origin}</Badge>
88
+ )}
89
+ {item.direction && (
90
+ <Badge variant="outline">{item.direction}</Badge>
91
+ )}
92
+ {item.status === "draft" && (
93
+ <Badge variant="outline" className="border-yellow-500 text-yellow-600">
94
+ draft
95
+ </Badge>
96
+ )}
97
+ {isThread && (
98
+ <Badge variant="outline">
99
+ Thread ({threadItems.length} tweets)
100
+ </Badge>
101
+ )}
102
+ </div>
103
+ {item.post?.platformUrl && (
104
+ <Button variant="ghost" size="sm" asChild>
105
+ <a
106
+ href={item.post.platformUrl}
107
+ target="_blank"
108
+ rel="noopener noreferrer"
109
+ >
110
+ <ExternalLink className="mr-1 h-4 w-4" />
111
+ View on X
112
+ </a>
113
+ </Button>
114
+ )}
115
+ </div>
116
+
117
+ {/* Title (if present) */}
118
+ {item.title && (
119
+ <h2 className="text-lg font-semibold">{item.title}</h2>
120
+ )}
121
+
122
+ {/* Tweet body */}
123
+ <div className="whitespace-pre-wrap text-sm leading-relaxed">
124
+ {item.body ?? "No content"}
125
+ </div>
126
+
127
+ {/* Timestamp */}
128
+ <p className="text-xs text-muted-foreground">
129
+ {formatDate(item.post?.publishedAt ?? item.createdAt)}
130
+ </p>
131
+
132
+ {/* Engagement metrics */}
133
+ {snapshot && (
134
+ <div className="flex items-center gap-6 pt-2 border-t text-sm text-muted-foreground">
135
+ <span className="flex items-center gap-1.5" title="Likes">
136
+ <Heart className="h-4 w-4" />
137
+ {snapshot.likes ?? 0}
138
+ </span>
139
+ <span className="flex items-center gap-1.5" title="Replies">
140
+ <MessageCircle className="h-4 w-4" />
141
+ {snapshot.replies ?? 0}
142
+ </span>
143
+ <span className="flex items-center gap-1.5" title="Retweets">
144
+ <Repeat2 className="h-4 w-4" />
145
+ {snapshot.retweets ?? 0}
146
+ </span>
147
+ <span className="flex items-center gap-1.5" title="Quotes">
148
+ <Quote className="h-4 w-4" />
149
+ {snapshot.quotes ?? 0}
150
+ </span>
151
+ </div>
152
+ )}
153
+ </CardContent>
154
+ </Card>
155
+
156
+ {/* Thread context */}
157
+ {isThread && (
158
+ <Card>
159
+ <CardContent className="pt-6">
160
+ <h3 className="text-sm font-medium mb-3">Thread ({threadItems.length} tweets)</h3>
161
+ <div className="space-y-0">
162
+ {threadItems.map((ti, idx) => {
163
+ const isCurrent = ti.id === item.id;
164
+ return (
165
+ <div key={ti.id} className="flex gap-3">
166
+ {/* Vertical connector */}
167
+ <div className="flex flex-col items-center">
168
+ <div
169
+ className={`w-2.5 h-2.5 rounded-full shrink-0 ${
170
+ isCurrent ? "bg-primary" : "bg-muted-foreground/30"
171
+ }`}
172
+ />
173
+ {idx < threadItems.length - 1 && (
174
+ <div className="w-0.5 flex-1 bg-muted-foreground/20 min-h-4" />
175
+ )}
176
+ </div>
177
+
178
+ {/* Tweet content */}
179
+ {isCurrent ? (
180
+ <div className="pb-3 flex-1">
181
+ <p className="text-sm font-medium text-foreground">
182
+ {ti.body
183
+ ? ti.body.length > 100
184
+ ? ti.body.slice(0, 100) + "..."
185
+ : ti.body
186
+ : "No content"}
187
+ </p>
188
+ <Badge variant="secondary" className="text-xs mt-1">
189
+ Current
190
+ </Badge>
191
+ </div>
192
+ ) : (
193
+ <Link
194
+ href={`/dashboard/content/${ti.id}`}
195
+ className="pb-3 flex-1 group"
196
+ >
197
+ <p className="text-sm text-muted-foreground group-hover:text-foreground transition-colors">
198
+ {ti.body
199
+ ? ti.body.length > 100
200
+ ? ti.body.slice(0, 100) + "..."
201
+ : ti.body
202
+ : "No content"}
203
+ </p>
204
+ </Link>
205
+ )}
206
+ </div>
207
+ );
208
+ })}
209
+ </div>
210
+ </CardContent>
211
+ </Card>
212
+ )}
213
+
214
+ {/* Engagement action bar */}
215
+ {canEngage && item.post && (
216
+ <EngagementActions
217
+ tweetId={item.post.platformPostId!}
218
+ contentPostId={item.post.id}
219
+ engagementHistory={engagementHistory}
220
+ />
221
+ )}
222
+
223
+ {/* Engagement history */}
224
+ {engagementHistory.length > 0 && (
225
+ <Card>
226
+ <CardContent className="pt-6">
227
+ <h3 className="text-sm font-medium mb-3">Your Activity</h3>
228
+ <div className="space-y-2">
229
+ {engagementHistory.map((e) => (
230
+ <div
231
+ key={e.id}
232
+ className="flex items-center justify-between text-sm text-muted-foreground py-1 border-b last:border-b-0"
233
+ >
234
+ <div className="flex items-center gap-2">
235
+ <Badge variant="outline" className="text-xs">
236
+ {e.engagementType}
237
+ </Badge>
238
+ {e.content && (
239
+ <span className="truncate max-w-xs">{e.content}</span>
240
+ )}
241
+ </div>
242
+ <span className="text-xs">
243
+ {formatDate(e.createdAt)}
244
+ </span>
245
+ </div>
246
+ ))}
247
+ </div>
248
+ </CardContent>
249
+ </Card>
250
+ )}
251
+ </div>
252
+ );
253
+ }