create-supyagent-app 0.1.12 → 0.1.14

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 (38) hide show
  1. package/dist/index.js +37 -0
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/base/src/app/globals.css +0 -1
  5. package/templates/base/src/components/ai-elements/tool.tsx +166 -0
  6. package/templates/base/src/components/chat-message.tsx +2 -2
  7. package/templates/base/src/components/supyagent/tool-message.tsx +98 -0
  8. package/templates/base/src/components/supyagent/tool-renderers.tsx +69 -0
  9. package/templates/base/src/components/supyagent/tools/brevo.tsx +140 -0
  10. package/templates/base/src/components/supyagent/tools/calendar.tsx +158 -0
  11. package/templates/base/src/components/supyagent/tools/calendly.tsx +161 -0
  12. package/templates/base/src/components/supyagent/tools/compute.tsx +113 -0
  13. package/templates/base/src/components/supyagent/tools/discord.tsx +174 -0
  14. package/templates/base/src/components/supyagent/tools/docs.tsx +94 -0
  15. package/templates/base/src/components/supyagent/tools/drive.tsx +124 -0
  16. package/templates/base/src/components/supyagent/tools/generic.tsx +19 -0
  17. package/templates/base/src/components/supyagent/tools/github.tsx +142 -0
  18. package/templates/base/src/components/supyagent/tools/gmail.tsx +161 -0
  19. package/templates/base/src/components/supyagent/tools/hubspot.tsx +206 -0
  20. package/templates/base/src/components/supyagent/tools/inbox.tsx +158 -0
  21. package/templates/base/src/components/supyagent/tools/jira.tsx +164 -0
  22. package/templates/base/src/components/supyagent/tools/linear.tsx +196 -0
  23. package/templates/base/src/components/supyagent/tools/linkedin.tsx +151 -0
  24. package/templates/base/src/components/supyagent/tools/notion.tsx +223 -0
  25. package/templates/base/src/components/supyagent/tools/pipedrive.tsx +154 -0
  26. package/templates/base/src/components/supyagent/tools/resend.tsx +92 -0
  27. package/templates/base/src/components/supyagent/tools/salesforce.tsx +163 -0
  28. package/templates/base/src/components/supyagent/tools/search.tsx +141 -0
  29. package/templates/base/src/components/supyagent/tools/sheets.tsx +109 -0
  30. package/templates/base/src/components/supyagent/tools/slack.tsx +135 -0
  31. package/templates/base/src/components/supyagent/tools/slides.tsx +64 -0
  32. package/templates/base/src/components/supyagent/tools/stripe.tsx +235 -0
  33. package/templates/base/src/components/supyagent/tools/telegram.tsx +116 -0
  34. package/templates/base/src/components/supyagent/tools/twilio.tsx +118 -0
  35. package/templates/base/src/components/supyagent/tools/twitter.tsx +139 -0
  36. package/templates/base/src/components/ui/badge.tsx +35 -0
  37. package/templates/base/src/components/ui/collapsible.tsx +11 -0
  38. package/templates/package-json/package.json.tmpl +3 -1
@@ -0,0 +1,135 @@
1
+ import React from "react";
2
+ import { MessageSquare, Hash, Reply } from "lucide-react";
3
+
4
+ interface SlackMessageData {
5
+ text?: string;
6
+ channel?: string;
7
+ user?: string;
8
+ ts?: string;
9
+ ok?: boolean;
10
+ reactions?: Array<{ name: string; count?: number }>;
11
+ thread_ts?: string;
12
+ reply_count?: number;
13
+ channels?: Array<{ id?: string; name?: string; num_members?: number; memberCount?: number }>;
14
+ }
15
+
16
+ interface SlackRendererProps {
17
+ data: unknown;
18
+ }
19
+
20
+ function isSlackMessage(data: unknown): data is SlackMessageData {
21
+ return typeof data === "object" && data !== null && ("text" in data || "channel" in data);
22
+ }
23
+
24
+ function isChannelList(data: unknown): data is { channels: Array<{ id?: string; name?: string; num_members?: number; memberCount?: number }> } {
25
+ return typeof data === "object" && data !== null && "channels" in data && Array.isArray((data as any).channels);
26
+ }
27
+
28
+ function MessageBubble({ message }: { message: SlackMessageData }) {
29
+ return (
30
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
31
+ <div className="flex items-center gap-2">
32
+ <MessageSquare className="h-4 w-4 text-muted-foreground shrink-0" />
33
+ {message.channel && (
34
+ <span className="flex items-center gap-0.5 text-xs text-muted-foreground">
35
+ <Hash className="h-3 w-3" />
36
+ {message.channel}
37
+ </span>
38
+ )}
39
+ {message.user && (
40
+ <span className="text-xs font-medium text-foreground">{message.user}</span>
41
+ )}
42
+ {message.thread_ts && (
43
+ <span className="flex items-center gap-1 text-xs text-muted-foreground">
44
+ <Reply className="h-3 w-3" />
45
+ {message.reply_count ? `${message.reply_count} replies` : "Thread"}
46
+ </span>
47
+ )}
48
+ </div>
49
+ {message.text && (
50
+ <p className="text-sm text-foreground">{message.text}</p>
51
+ )}
52
+ {message.reactions && message.reactions.length > 0 && (
53
+ <div className="flex flex-wrap gap-1.5 pt-1">
54
+ {message.reactions.map((reaction) => (
55
+ <span
56
+ key={reaction.name}
57
+ className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
58
+ >
59
+ :{reaction.name}:
60
+ {reaction.count && reaction.count > 1 && (
61
+ <span className="font-medium">{reaction.count}</span>
62
+ )}
63
+ </span>
64
+ ))}
65
+ </div>
66
+ )}
67
+ </div>
68
+ );
69
+ }
70
+
71
+ function ChannelCard({ channel }: { channel: { id?: string; name?: string; num_members?: number; memberCount?: number } }) {
72
+ const members = channel.num_members ?? channel.memberCount;
73
+ return (
74
+ <div className="flex items-center gap-2 rounded-lg border border-border bg-card p-3">
75
+ <Hash className="h-4 w-4 text-muted-foreground shrink-0" />
76
+ <span className="text-sm text-foreground flex-1">{channel.name || channel.id}</span>
77
+ {members !== undefined && (
78
+ <span className="text-xs text-muted-foreground">{members} members</span>
79
+ )}
80
+ </div>
81
+ );
82
+ }
83
+
84
+ export function SlackRenderer({ data }: SlackRendererProps) {
85
+ // Channel list response
86
+ if (isChannelList(data)) {
87
+ const channels = (data as any).channels;
88
+ return (
89
+ <div className="space-y-1.5">
90
+ {channels.map((ch: any, i: number) => (
91
+ <ChannelCard key={ch.id || i} channel={ch} />
92
+ ))}
93
+ </div>
94
+ );
95
+ }
96
+
97
+ if (isSlackMessage(data)) {
98
+ return <MessageBubble message={data} />;
99
+ }
100
+
101
+ if (Array.isArray(data)) {
102
+ const messages = data.filter(isSlackMessage);
103
+ if (messages.length > 0) {
104
+ return (
105
+ <div className="space-y-2">
106
+ {messages.map((msg, i) => (
107
+ <MessageBubble key={msg.ts || i} message={msg} />
108
+ ))}
109
+ </div>
110
+ );
111
+ }
112
+ }
113
+
114
+ if (typeof data === "object" && data !== null && "messages" in data) {
115
+ const messages = (data as { messages: unknown[] }).messages;
116
+ if (Array.isArray(messages)) {
117
+ const slackMsgs = messages.filter(isSlackMessage);
118
+ if (slackMsgs.length > 0) {
119
+ return (
120
+ <div className="space-y-2">
121
+ {slackMsgs.map((msg, i) => (
122
+ <MessageBubble key={msg.ts || i} message={msg} />
123
+ ))}
124
+ </div>
125
+ );
126
+ }
127
+ }
128
+ }
129
+
130
+ return (
131
+ <pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
132
+ {JSON.stringify(data, null, 2)}
133
+ </pre>
134
+ );
135
+ }
@@ -0,0 +1,64 @@
1
+ import React from "react";
2
+ import { Presentation, ExternalLink } from "lucide-react";
3
+
4
+ interface SlidesData {
5
+ presentationId?: string;
6
+ title?: string;
7
+ url?: string;
8
+ slides?: unknown[];
9
+ pageSize?: unknown;
10
+ }
11
+
12
+ interface SlidesRendererProps {
13
+ data: unknown;
14
+ }
15
+
16
+ function isSlidesData(data: unknown): data is SlidesData {
17
+ return typeof data === "object" && data !== null && ("presentationId" in data || ("title" in data && "slides" in data));
18
+ }
19
+
20
+ function SlidesCard({ presentation }: { presentation: SlidesData }) {
21
+ const slideUrl = presentation.url || (presentation.presentationId ? `https://docs.google.com/presentation/d/${presentation.presentationId}` : null);
22
+ const slideCount = presentation.slides?.length;
23
+
24
+ return (
25
+ <div className="rounded-lg border border-border bg-card p-3">
26
+ <div className="flex items-start gap-2">
27
+ <Presentation className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
28
+ <div className="min-w-0 flex-1">
29
+ <p className="text-sm font-medium text-foreground">
30
+ {slideUrl ? (
31
+ <a href={slideUrl} target="_blank" rel="noopener noreferrer" className="hover:underline">
32
+ {presentation.title || "Presentation"}
33
+ </a>
34
+ ) : (
35
+ presentation.title || "Presentation"
36
+ )}
37
+ </p>
38
+ {slideCount !== undefined && (
39
+ <p className="text-xs text-muted-foreground">
40
+ {slideCount} {slideCount === 1 ? "slide" : "slides"}
41
+ </p>
42
+ )}
43
+ </div>
44
+ {slideUrl && (
45
+ <a href={slideUrl} target="_blank" rel="noopener noreferrer" className="shrink-0 text-muted-foreground hover:text-foreground">
46
+ <ExternalLink className="h-3.5 w-3.5" />
47
+ </a>
48
+ )}
49
+ </div>
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export function SlidesRenderer({ data }: SlidesRendererProps) {
55
+ if (isSlidesData(data)) {
56
+ return <SlidesCard presentation={data} />;
57
+ }
58
+
59
+ return (
60
+ <pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
61
+ {JSON.stringify(data, null, 2)}
62
+ </pre>
63
+ );
64
+ }
@@ -0,0 +1,235 @@
1
+ import React from "react";
2
+ import { CreditCard, User, FileText, Clock, RefreshCw } from "lucide-react";
3
+
4
+ interface StripeCustomerData {
5
+ id?: string;
6
+ name?: string;
7
+ email?: string;
8
+ phone?: string;
9
+ created?: number;
10
+ }
11
+
12
+ interface StripeInvoiceData {
13
+ id?: string;
14
+ number?: string;
15
+ amount_due?: number;
16
+ amount_paid?: number;
17
+ currency?: string;
18
+ status?: string;
19
+ customer_name?: string;
20
+ due_date?: number;
21
+ }
22
+
23
+ interface StripeSubscriptionData {
24
+ id?: string;
25
+ status?: string;
26
+ plan?: { nickname?: string; amount?: number; currency?: string; interval?: string };
27
+ current_period_start?: number;
28
+ current_period_end?: number;
29
+ }
30
+
31
+ interface StripeBalanceData {
32
+ available?: Array<{ amount: number; currency: string }>;
33
+ pending?: Array<{ amount: number; currency: string }>;
34
+ }
35
+
36
+ interface StripeRendererProps {
37
+ data: unknown;
38
+ }
39
+
40
+ function isCustomer(data: unknown): data is StripeCustomerData {
41
+ return typeof data === "object" && data !== null && ("email" in data || "name" in data) && !("amount_due" in data) && !("plan" in data);
42
+ }
43
+
44
+ function isInvoice(data: unknown): data is StripeInvoiceData {
45
+ return typeof data === "object" && data !== null && ("number" in data || "amount_due" in data);
46
+ }
47
+
48
+ function isSubscription(data: unknown): data is StripeSubscriptionData {
49
+ return typeof data === "object" && data !== null && "plan" in data && "status" in data;
50
+ }
51
+
52
+ function isBalance(data: unknown): data is StripeBalanceData {
53
+ return typeof data === "object" && data !== null && ("available" in data || "pending" in data);
54
+ }
55
+
56
+ function formatAmount(amount: number, currency: string): string {
57
+ try {
58
+ return new Intl.NumberFormat(undefined, { style: "currency", currency: currency.toUpperCase() }).format(amount / 100);
59
+ } catch {
60
+ return `${(amount / 100).toFixed(2)} ${currency.toUpperCase()}`;
61
+ }
62
+ }
63
+
64
+ function formatUnixDate(ts: number): string {
65
+ try {
66
+ return new Date(ts * 1000).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
67
+ } catch {
68
+ return String(ts);
69
+ }
70
+ }
71
+
72
+ const STATUS_STYLES: Record<string, string> = {
73
+ paid: "bg-green-500/10 text-green-500",
74
+ active: "bg-green-500/10 text-green-500",
75
+ open: "bg-blue-500/10 text-blue-500",
76
+ draft: "bg-muted text-muted-foreground",
77
+ void: "bg-muted text-muted-foreground",
78
+ uncollectible: "bg-destructive/10 text-destructive",
79
+ past_due: "bg-destructive/10 text-destructive",
80
+ canceled: "bg-destructive/10 text-destructive",
81
+ trialing: "bg-yellow-500/10 text-yellow-600",
82
+ };
83
+
84
+ function StatusBadge({ status }: { status: string }) {
85
+ return (
86
+ <span className={`rounded-full px-2 py-0.5 text-xs ${STATUS_STYLES[status] || "bg-muted text-muted-foreground"}`}>
87
+ {status}
88
+ </span>
89
+ );
90
+ }
91
+
92
+ function CustomerCard({ customer }: { customer: StripeCustomerData }) {
93
+ return (
94
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1">
95
+ <div className="flex items-start gap-2">
96
+ <User className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
97
+ <div className="min-w-0 flex-1">
98
+ <p className="text-sm font-medium text-foreground">{customer.name || "Unnamed customer"}</p>
99
+ {customer.email && <p className="text-xs text-muted-foreground">{customer.email}</p>}
100
+ </div>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
105
+
106
+ function InvoiceCard({ invoice }: { invoice: StripeInvoiceData }) {
107
+ return (
108
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1">
109
+ <div className="flex items-start gap-2">
110
+ <FileText className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
111
+ <div className="min-w-0 flex-1">
112
+ <p className="text-sm font-medium text-foreground">{invoice.number || invoice.id}</p>
113
+ {invoice.customer_name && <p className="text-xs text-muted-foreground">{invoice.customer_name}</p>}
114
+ </div>
115
+ <div className="flex items-center gap-1.5 shrink-0">
116
+ {invoice.amount_due !== undefined && invoice.currency && (
117
+ <span className="text-sm font-medium text-foreground">{formatAmount(invoice.amount_due, invoice.currency)}</span>
118
+ )}
119
+ {invoice.status && <StatusBadge status={invoice.status} />}
120
+ </div>
121
+ </div>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ function SubscriptionCard({ sub }: { sub: StripeSubscriptionData }) {
127
+ return (
128
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1">
129
+ <div className="flex items-start gap-2">
130
+ <RefreshCw className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
131
+ <div className="min-w-0 flex-1">
132
+ <p className="text-sm font-medium text-foreground">
133
+ {sub.plan?.nickname || "Subscription"}
134
+ {sub.plan?.amount !== undefined && sub.plan?.currency && (
135
+ <span className="text-muted-foreground font-normal"> {formatAmount(sub.plan.amount, sub.plan.currency)}/{sub.plan.interval}</span>
136
+ )}
137
+ </p>
138
+ {sub.current_period_end && (
139
+ <p className="flex items-center gap-1 text-xs text-muted-foreground">
140
+ <Clock className="h-3 w-3" />
141
+ Renews {formatUnixDate(sub.current_period_end)}
142
+ </p>
143
+ )}
144
+ </div>
145
+ {sub.status && <StatusBadge status={sub.status} />}
146
+ </div>
147
+ </div>
148
+ );
149
+ }
150
+
151
+ function BalanceDisplay({ balance }: { balance: StripeBalanceData }) {
152
+ return (
153
+ <div className="rounded-lg border border-border bg-card p-3 space-y-2">
154
+ <div className="flex items-center gap-2">
155
+ <CreditCard className="h-4 w-4 text-muted-foreground" />
156
+ <span className="text-sm font-medium text-foreground">Balance</span>
157
+ </div>
158
+ <div className="grid grid-cols-2 gap-4 pl-6">
159
+ {balance.available && balance.available.length > 0 && (
160
+ <div>
161
+ <p className="text-xs text-muted-foreground">Available</p>
162
+ {balance.available.map((b, i) => (
163
+ <p key={i} className="text-sm font-medium text-foreground">{formatAmount(b.amount, b.currency)}</p>
164
+ ))}
165
+ </div>
166
+ )}
167
+ {balance.pending && balance.pending.length > 0 && (
168
+ <div>
169
+ <p className="text-xs text-muted-foreground">Pending</p>
170
+ {balance.pending.map((b, i) => (
171
+ <p key={i} className="text-sm font-medium text-foreground">{formatAmount(b.amount, b.currency)}</p>
172
+ ))}
173
+ </div>
174
+ )}
175
+ </div>
176
+ </div>
177
+ );
178
+ }
179
+
180
+ export function StripeRenderer({ data }: StripeRendererProps) {
181
+ // Balance
182
+ if (isBalance(data)) return <BalanceDisplay balance={data} />;
183
+
184
+ // List response {data: [...]}
185
+ if (typeof data === "object" && data !== null && "data" in data && Array.isArray((data as any).data)) {
186
+ const items = (data as any).data as unknown[];
187
+ const invoices = items.filter(isInvoice);
188
+ if (invoices.length > 0) {
189
+ return (
190
+ <div className="space-y-1.5">
191
+ {invoices.map((inv, i) => <InvoiceCard key={inv.id || i} invoice={inv} />)}
192
+ </div>
193
+ );
194
+ }
195
+ const subs = items.filter(isSubscription);
196
+ if (subs.length > 0) {
197
+ return (
198
+ <div className="space-y-1.5">
199
+ {subs.map((s, i) => <SubscriptionCard key={s.id || i} sub={s} />)}
200
+ </div>
201
+ );
202
+ }
203
+ const customers = items.filter(isCustomer);
204
+ if (customers.length > 0) {
205
+ return (
206
+ <div className="space-y-1.5">
207
+ {customers.map((c, i) => <CustomerCard key={c.id || i} customer={c} />)}
208
+ </div>
209
+ );
210
+ }
211
+ }
212
+
213
+ // Single items
214
+ if (isInvoice(data)) return <InvoiceCard invoice={data} />;
215
+ if (isSubscription(data)) return <SubscriptionCard sub={data} />;
216
+ if (isCustomer(data)) return <CustomerCard customer={data} />;
217
+
218
+ // Array
219
+ if (Array.isArray(data)) {
220
+ const invoices = data.filter(isInvoice);
221
+ if (invoices.length > 0) {
222
+ return <div className="space-y-1.5">{invoices.map((inv, i) => <InvoiceCard key={inv.id || i} invoice={inv} />)}</div>;
223
+ }
224
+ const customers = data.filter(isCustomer);
225
+ if (customers.length > 0) {
226
+ return <div className="space-y-1.5">{customers.map((c, i) => <CustomerCard key={c.id || i} customer={c} />)}</div>;
227
+ }
228
+ }
229
+
230
+ return (
231
+ <pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
232
+ {JSON.stringify(data, null, 2)}
233
+ </pre>
234
+ );
235
+ }
@@ -0,0 +1,116 @@
1
+ import React from "react";
2
+ import { MessageSquare, User, Clock } from "lucide-react";
3
+
4
+ interface TelegramMessageData {
5
+ message_id?: number;
6
+ text?: string;
7
+ from?: { username?: string; first_name?: string; last_name?: string };
8
+ chat?: { title?: string; username?: string; type?: string };
9
+ date?: number;
10
+ }
11
+
12
+ interface TelegramRendererProps {
13
+ data: unknown;
14
+ }
15
+
16
+ function isTelegramMessage(data: unknown): data is TelegramMessageData {
17
+ return typeof data === "object" && data !== null && ("text" in data || "message_id" in data);
18
+ }
19
+
20
+ function formatUnixDate(ts: number): string {
21
+ try {
22
+ const date = new Date(ts * 1000);
23
+ const now = new Date();
24
+ const diffMs = now.getTime() - date.getTime();
25
+ const diffMins = Math.floor(diffMs / 60000);
26
+ const diffHours = Math.floor(diffMs / 3600000);
27
+ const diffDays = Math.floor(diffMs / 86400000);
28
+ if (diffMins < 1) return "Just now";
29
+ if (diffMins < 60) return `${diffMins}m ago`;
30
+ if (diffHours < 24) return `${diffHours}h ago`;
31
+ if (diffDays < 7) return `${diffDays}d ago`;
32
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
33
+ } catch {
34
+ return String(ts);
35
+ }
36
+ }
37
+
38
+ function MessageCard({ message }: { message: TelegramMessageData }) {
39
+ const fromName = message.from
40
+ ? [message.from.first_name, message.from.last_name].filter(Boolean).join(" ") || message.from.username
41
+ : undefined;
42
+
43
+ return (
44
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
45
+ <div className="flex items-center gap-2">
46
+ <MessageSquare className="h-4 w-4 text-muted-foreground shrink-0" />
47
+ {fromName && (
48
+ <span className="flex items-center gap-1 text-xs font-medium text-foreground">
49
+ <User className="h-3 w-3" />
50
+ {fromName}
51
+ </span>
52
+ )}
53
+ {message.chat?.title && (
54
+ <span className="text-xs text-muted-foreground">{message.chat.title}</span>
55
+ )}
56
+ {message.date && (
57
+ <span className="flex items-center gap-1 text-xs text-muted-foreground ml-auto">
58
+ <Clock className="h-3 w-3" />
59
+ {formatUnixDate(message.date)}
60
+ </span>
61
+ )}
62
+ </div>
63
+ {message.text && (
64
+ <p className="text-sm text-foreground line-clamp-3">{message.text}</p>
65
+ )}
66
+ </div>
67
+ );
68
+ }
69
+
70
+ export function TelegramRenderer({ data }: TelegramRendererProps) {
71
+ // {messages: [...]}
72
+ if (typeof data === "object" && data !== null && "messages" in data) {
73
+ const msgs = (data as any).messages;
74
+ if (Array.isArray(msgs)) {
75
+ return (
76
+ <div className="space-y-2">
77
+ {msgs.filter(isTelegramMessage).map((m, i) => <MessageCard key={m.message_id || i} message={m} />)}
78
+ </div>
79
+ );
80
+ }
81
+ }
82
+
83
+ // {result: [...]} (Bot API shape)
84
+ if (typeof data === "object" && data !== null && "result" in data) {
85
+ const result = (data as any).result;
86
+ if (Array.isArray(result)) {
87
+ const msgs = result.filter(isTelegramMessage);
88
+ if (msgs.length > 0) {
89
+ return (
90
+ <div className="space-y-2">
91
+ {msgs.map((m, i) => <MessageCard key={m.message_id || i} message={m} />)}
92
+ </div>
93
+ );
94
+ }
95
+ }
96
+ }
97
+
98
+ if (isTelegramMessage(data)) return <MessageCard message={data} />;
99
+
100
+ if (Array.isArray(data)) {
101
+ const msgs = data.filter(isTelegramMessage);
102
+ if (msgs.length > 0) {
103
+ return (
104
+ <div className="space-y-2">
105
+ {msgs.map((m, i) => <MessageCard key={m.message_id || i} message={m} />)}
106
+ </div>
107
+ );
108
+ }
109
+ }
110
+
111
+ return (
112
+ <pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
113
+ {JSON.stringify(data, null, 2)}
114
+ </pre>
115
+ );
116
+ }
@@ -0,0 +1,118 @@
1
+ import React from "react";
2
+ import { Phone, ArrowUpRight, ArrowDownLeft, Clock } from "lucide-react";
3
+
4
+ interface TwilioMessageData {
5
+ sid?: string;
6
+ from?: string;
7
+ to?: string;
8
+ body?: string;
9
+ status?: string;
10
+ direction?: string;
11
+ date_sent?: string;
12
+ date_created?: string;
13
+ }
14
+
15
+ interface TwilioRendererProps {
16
+ data: unknown;
17
+ }
18
+
19
+ function isTwilioMessage(data: unknown): data is TwilioMessageData {
20
+ return typeof data === "object" && data !== null && ("body" in data || "sid" in data) && ("from" in data || "to" in data);
21
+ }
22
+
23
+ function formatRelativeDate(dateStr: string): string {
24
+ try {
25
+ const date = new Date(dateStr);
26
+ const now = new Date();
27
+ const diffMs = now.getTime() - date.getTime();
28
+ const diffMins = Math.floor(diffMs / 60000);
29
+ const diffHours = Math.floor(diffMs / 3600000);
30
+ const diffDays = Math.floor(diffMs / 86400000);
31
+ if (diffMins < 1) return "Just now";
32
+ if (diffMins < 60) return `${diffMins}m ago`;
33
+ if (diffHours < 24) return `${diffHours}h ago`;
34
+ if (diffDays < 7) return `${diffDays}d ago`;
35
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
36
+ } catch {
37
+ return dateStr;
38
+ }
39
+ }
40
+
41
+ const STATUS_STYLES: Record<string, string> = {
42
+ delivered: "bg-green-500/10 text-green-500",
43
+ sent: "bg-green-500/10 text-green-500",
44
+ received: "bg-blue-500/10 text-blue-500",
45
+ failed: "bg-destructive/10 text-destructive",
46
+ undelivered: "bg-destructive/10 text-destructive",
47
+ queued: "bg-yellow-500/10 text-yellow-600",
48
+ sending: "bg-blue-500/10 text-blue-500",
49
+ };
50
+
51
+ function MessageCard({ message }: { message: TwilioMessageData }) {
52
+ const isInbound = message.direction?.includes("inbound");
53
+ const DirectionIcon = isInbound ? ArrowDownLeft : ArrowUpRight;
54
+ const dateStr = message.date_sent || message.date_created;
55
+
56
+ return (
57
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
58
+ <div className="flex items-center gap-2">
59
+ <Phone className="h-4 w-4 text-muted-foreground shrink-0" />
60
+ <DirectionIcon className="h-3 w-3 text-muted-foreground shrink-0" />
61
+ <div className="flex items-center gap-1.5 flex-1 min-w-0">
62
+ {message.from && <span className="text-xs text-muted-foreground">{message.from}</span>}
63
+ <span className="text-xs text-muted-foreground">→</span>
64
+ {message.to && <span className="text-xs text-muted-foreground">{message.to}</span>}
65
+ </div>
66
+ <div className="flex items-center gap-1.5 shrink-0">
67
+ {message.status && (
68
+ <span className={`rounded-full px-2 py-0.5 text-xs ${STATUS_STYLES[message.status] || "bg-muted text-muted-foreground"}`}>
69
+ {message.status}
70
+ </span>
71
+ )}
72
+ {dateStr && (
73
+ <span className="flex items-center gap-1 text-xs text-muted-foreground">
74
+ <Clock className="h-3 w-3" />
75
+ {formatRelativeDate(dateStr)}
76
+ </span>
77
+ )}
78
+ </div>
79
+ </div>
80
+ {message.body && (
81
+ <p className="text-sm text-foreground line-clamp-3">{message.body}</p>
82
+ )}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ export function TwilioRenderer({ data }: TwilioRendererProps) {
88
+ // {messages: [...]}
89
+ if (typeof data === "object" && data !== null && "messages" in data) {
90
+ const msgs = (data as any).messages;
91
+ if (Array.isArray(msgs)) {
92
+ return (
93
+ <div className="space-y-1.5">
94
+ {msgs.filter(isTwilioMessage).map((m, i) => <MessageCard key={m.sid || i} message={m} />)}
95
+ </div>
96
+ );
97
+ }
98
+ }
99
+
100
+ if (isTwilioMessage(data)) return <MessageCard message={data} />;
101
+
102
+ if (Array.isArray(data)) {
103
+ const msgs = data.filter(isTwilioMessage);
104
+ if (msgs.length > 0) {
105
+ return (
106
+ <div className="space-y-1.5">
107
+ {msgs.map((m, i) => <MessageCard key={m.sid || i} message={m} />)}
108
+ </div>
109
+ );
110
+ }
111
+ }
112
+
113
+ return (
114
+ <pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
115
+ {JSON.stringify(data, null, 2)}
116
+ </pre>
117
+ );
118
+ }