create-supyagent-app 0.1.13 → 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.
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/base/src/app/globals.css +0 -1
- package/templates/base/src/components/ai-elements/tool.tsx +166 -0
- package/templates/base/src/components/chat-message.tsx +2 -2
- package/templates/base/src/components/supyagent/tool-message.tsx +98 -0
- package/templates/base/src/components/supyagent/tool-renderers.tsx +69 -0
- package/templates/base/src/components/supyagent/tools/brevo.tsx +140 -0
- package/templates/base/src/components/supyagent/tools/calendar.tsx +158 -0
- package/templates/base/src/components/supyagent/tools/calendly.tsx +161 -0
- package/templates/base/src/components/supyagent/tools/compute.tsx +113 -0
- package/templates/base/src/components/supyagent/tools/discord.tsx +174 -0
- package/templates/base/src/components/supyagent/tools/docs.tsx +94 -0
- package/templates/base/src/components/supyagent/tools/drive.tsx +124 -0
- package/templates/base/src/components/supyagent/tools/generic.tsx +19 -0
- package/templates/base/src/components/supyagent/tools/github.tsx +142 -0
- package/templates/base/src/components/supyagent/tools/gmail.tsx +161 -0
- package/templates/base/src/components/supyagent/tools/hubspot.tsx +206 -0
- package/templates/base/src/components/supyagent/tools/inbox.tsx +158 -0
- package/templates/base/src/components/supyagent/tools/jira.tsx +164 -0
- package/templates/base/src/components/supyagent/tools/linear.tsx +196 -0
- package/templates/base/src/components/supyagent/tools/linkedin.tsx +151 -0
- package/templates/base/src/components/supyagent/tools/notion.tsx +223 -0
- package/templates/base/src/components/supyagent/tools/pipedrive.tsx +154 -0
- package/templates/base/src/components/supyagent/tools/resend.tsx +92 -0
- package/templates/base/src/components/supyagent/tools/salesforce.tsx +163 -0
- package/templates/base/src/components/supyagent/tools/search.tsx +141 -0
- package/templates/base/src/components/supyagent/tools/sheets.tsx +109 -0
- package/templates/base/src/components/supyagent/tools/slack.tsx +135 -0
- package/templates/base/src/components/supyagent/tools/slides.tsx +64 -0
- package/templates/base/src/components/supyagent/tools/stripe.tsx +235 -0
- package/templates/base/src/components/supyagent/tools/telegram.tsx +116 -0
- package/templates/base/src/components/supyagent/tools/twilio.tsx +118 -0
- package/templates/base/src/components/supyagent/tools/twitter.tsx +139 -0
- package/templates/base/src/components/ui/badge.tsx +35 -0
- package/templates/base/src/components/ui/collapsible.tsx +11 -0
- package/templates/package-json/package.json.tmpl +3 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { User, Mail, BarChart3 } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface BrevoContactData {
|
|
5
|
+
id?: number;
|
|
6
|
+
email?: string;
|
|
7
|
+
attributes?: { FIRSTNAME?: string; LASTNAME?: string; [key: string]: unknown };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface BrevoCampaignData {
|
|
11
|
+
id?: number;
|
|
12
|
+
name?: string;
|
|
13
|
+
subject?: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
statistics?: {
|
|
16
|
+
globalStats?: { opened?: number; clicked?: number; sent?: number };
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface BrevoRendererProps {
|
|
21
|
+
data: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isBrevoContact(data: unknown): data is BrevoContactData {
|
|
25
|
+
return typeof data === "object" && data !== null && "email" in data && ("attributes" in data || "id" in data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isBrevoCampaign(data: unknown): data is BrevoCampaignData {
|
|
29
|
+
return typeof data === "object" && data !== null && "name" in data && ("subject" in data || "status" in data);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STATUS_STYLES: Record<string, string> = {
|
|
33
|
+
sent: "bg-green-500/10 text-green-500",
|
|
34
|
+
draft: "bg-muted text-muted-foreground",
|
|
35
|
+
queued: "bg-blue-500/10 text-blue-500",
|
|
36
|
+
suspended: "bg-yellow-500/10 text-yellow-600",
|
|
37
|
+
archive: "bg-muted text-muted-foreground",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function ContactCard({ contact }: { contact: BrevoContactData }) {
|
|
41
|
+
const name = contact.attributes
|
|
42
|
+
? [contact.attributes.FIRSTNAME, contact.attributes.LASTNAME].filter(Boolean).join(" ")
|
|
43
|
+
: undefined;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="rounded-lg border border-border bg-card p-3">
|
|
47
|
+
<div className="flex items-start gap-2">
|
|
48
|
+
<User className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
49
|
+
<div className="min-w-0 flex-1">
|
|
50
|
+
{name && <p className="text-sm font-medium text-foreground">{name}</p>}
|
|
51
|
+
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
|
52
|
+
<Mail className="h-3 w-3" />
|
|
53
|
+
{contact.email}
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function CampaignCard({ campaign }: { campaign: BrevoCampaignData }) {
|
|
62
|
+
const stats = campaign.statistics?.globalStats;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
|
|
66
|
+
<div className="flex items-start gap-2">
|
|
67
|
+
<Mail className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
68
|
+
<div className="min-w-0 flex-1">
|
|
69
|
+
<p className="text-sm font-medium text-foreground">{campaign.name}</p>
|
|
70
|
+
{campaign.subject && <p className="text-xs text-muted-foreground">{campaign.subject}</p>}
|
|
71
|
+
</div>
|
|
72
|
+
{campaign.status && (
|
|
73
|
+
<span className={`rounded-full px-2 py-0.5 text-xs ${STATUS_STYLES[campaign.status] || "bg-muted text-muted-foreground"}`}>
|
|
74
|
+
{campaign.status}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
{stats && (
|
|
79
|
+
<div className="flex items-center gap-4 pl-6">
|
|
80
|
+
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
81
|
+
<BarChart3 className="h-3 w-3" />
|
|
82
|
+
{stats.sent ?? 0} sent
|
|
83
|
+
</span>
|
|
84
|
+
{stats.opened !== undefined && (
|
|
85
|
+
<span className="text-xs text-muted-foreground">{stats.opened} opened</span>
|
|
86
|
+
)}
|
|
87
|
+
{stats.clicked !== undefined && (
|
|
88
|
+
<span className="text-xs text-muted-foreground">{stats.clicked} clicked</span>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function BrevoRenderer({ data }: BrevoRendererProps) {
|
|
97
|
+
// {contacts: [...]}
|
|
98
|
+
if (typeof data === "object" && data !== null && "contacts" in data) {
|
|
99
|
+
const contacts = (data as any).contacts;
|
|
100
|
+
if (Array.isArray(contacts)) {
|
|
101
|
+
return (
|
|
102
|
+
<div className="space-y-1.5">
|
|
103
|
+
{contacts.filter(isBrevoContact).map((c, i) => <ContactCard key={c.id || i} contact={c} />)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// {campaigns: [...]}
|
|
110
|
+
if (typeof data === "object" && data !== null && "campaigns" in data) {
|
|
111
|
+
const campaigns = (data as any).campaigns;
|
|
112
|
+
if (Array.isArray(campaigns)) {
|
|
113
|
+
return (
|
|
114
|
+
<div className="space-y-1.5">
|
|
115
|
+
{campaigns.filter(isBrevoCampaign).map((c, i) => <CampaignCard key={c.id || i} campaign={c} />)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isBrevoContact(data)) return <ContactCard contact={data} />;
|
|
122
|
+
if (isBrevoCampaign(data)) return <CampaignCard campaign={data} />;
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(data)) {
|
|
125
|
+
const contacts = data.filter(isBrevoContact);
|
|
126
|
+
if (contacts.length > 0) {
|
|
127
|
+
return <div className="space-y-1.5">{contacts.map((c, i) => <ContactCard key={c.id || i} contact={c} />)}</div>;
|
|
128
|
+
}
|
|
129
|
+
const campaigns = data.filter(isBrevoCampaign);
|
|
130
|
+
if (campaigns.length > 0) {
|
|
131
|
+
return <div className="space-y-1.5">{campaigns.map((c, i) => <CampaignCard key={c.id || i} campaign={c} />)}</div>;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
|
|
137
|
+
{JSON.stringify(data, null, 2)}
|
|
138
|
+
</pre>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Calendar, MapPin, Users, Check, X, HelpCircle } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface CalendarEventData {
|
|
5
|
+
id?: string;
|
|
6
|
+
summary?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
start?: { dateTime?: string; date?: string };
|
|
9
|
+
end?: { dateTime?: string; date?: string };
|
|
10
|
+
location?: string;
|
|
11
|
+
attendees?: Array<{
|
|
12
|
+
email?: string;
|
|
13
|
+
displayName?: string;
|
|
14
|
+
responseStatus?: string;
|
|
15
|
+
}>;
|
|
16
|
+
status?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CalendarRendererProps {
|
|
20
|
+
data: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isCalendarEvent(data: unknown): data is CalendarEventData {
|
|
24
|
+
return typeof data === "object" && data !== null && ("summary" in data || "start" in data);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatRelativeDateTime(dt?: { dateTime?: string; date?: string }): string {
|
|
28
|
+
if (!dt) return "";
|
|
29
|
+
const raw = dt.dateTime || dt.date;
|
|
30
|
+
if (!raw) return "";
|
|
31
|
+
try {
|
|
32
|
+
const date = new Date(raw);
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const diffMs = date.getTime() - now.getTime();
|
|
35
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
36
|
+
|
|
37
|
+
const timeStr = dt.dateTime
|
|
38
|
+
? date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })
|
|
39
|
+
: "";
|
|
40
|
+
|
|
41
|
+
if (diffDays === 0) {
|
|
42
|
+
return timeStr ? `Today at ${timeStr}` : "Today";
|
|
43
|
+
}
|
|
44
|
+
if (diffDays === 1) {
|
|
45
|
+
return timeStr ? `Tomorrow at ${timeStr}` : "Tomorrow";
|
|
46
|
+
}
|
|
47
|
+
if (diffDays === -1) {
|
|
48
|
+
return timeStr ? `Yesterday at ${timeStr}` : "Yesterday";
|
|
49
|
+
}
|
|
50
|
+
if (diffDays > 1 && diffDays < 7) {
|
|
51
|
+
const dayName = date.toLocaleDateString(undefined, { weekday: "long" });
|
|
52
|
+
return timeStr ? `${dayName} at ${timeStr}` : dayName;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return dt.dateTime
|
|
56
|
+
? date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })
|
|
57
|
+
: date.toLocaleDateString(undefined, { dateStyle: "medium" });
|
|
58
|
+
} catch {
|
|
59
|
+
return raw;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ResponseStatusIcon({ status }: { status?: string }) {
|
|
64
|
+
switch (status) {
|
|
65
|
+
case "accepted":
|
|
66
|
+
return <Check className="h-3 w-3 text-green-500" />;
|
|
67
|
+
case "declined":
|
|
68
|
+
return <X className="h-3 w-3 text-destructive" />;
|
|
69
|
+
case "tentative":
|
|
70
|
+
return <HelpCircle className="h-3 w-3 text-yellow-500" />;
|
|
71
|
+
default:
|
|
72
|
+
return <HelpCircle className="h-3 w-3 text-muted-foreground" />;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function EventCard({ event }: { event: CalendarEventData }) {
|
|
77
|
+
const startStr = formatRelativeDateTime(event.start);
|
|
78
|
+
const endStr = formatRelativeDateTime(event.end);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="rounded-lg border border-border bg-card p-3 space-y-2">
|
|
82
|
+
<div className="flex items-start gap-2">
|
|
83
|
+
<Calendar className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
84
|
+
<div className="min-w-0 flex-1">
|
|
85
|
+
<p className="text-sm font-medium text-foreground">
|
|
86
|
+
{event.summary || "Untitled event"}
|
|
87
|
+
</p>
|
|
88
|
+
{startStr && (
|
|
89
|
+
<p className="text-xs text-muted-foreground">
|
|
90
|
+
{startStr}
|
|
91
|
+
{endStr ? ` \u2192 ${endStr}` : ""}
|
|
92
|
+
</p>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
{event.location && (
|
|
97
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
98
|
+
<MapPin className="h-3 w-3 shrink-0" />
|
|
99
|
+
<span className="truncate">{event.location}</span>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
{event.attendees && event.attendees.length > 0 && (
|
|
103
|
+
<div className="flex items-start gap-1.5 text-xs text-muted-foreground">
|
|
104
|
+
<Users className="h-3 w-3 shrink-0 mt-0.5" />
|
|
105
|
+
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
|
106
|
+
{event.attendees.map((a, i) => (
|
|
107
|
+
<span key={i} className="inline-flex items-center gap-1">
|
|
108
|
+
<ResponseStatusIcon status={a.responseStatus} />
|
|
109
|
+
{a.displayName || a.email}
|
|
110
|
+
</span>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function CalendarRenderer({ data }: CalendarRendererProps) {
|
|
120
|
+
if (isCalendarEvent(data)) {
|
|
121
|
+
return <EventCard event={data} />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(data)) {
|
|
125
|
+
const events = data.filter(isCalendarEvent);
|
|
126
|
+
if (events.length > 0) {
|
|
127
|
+
return (
|
|
128
|
+
<div className="space-y-2">
|
|
129
|
+
{events.map((event, i) => (
|
|
130
|
+
<EventCard key={event.id || i} event={event} />
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (typeof data === "object" && data !== null && "events" in data) {
|
|
138
|
+
const events = (data as { events: unknown[] }).events;
|
|
139
|
+
if (Array.isArray(events)) {
|
|
140
|
+
const calEvents = events.filter(isCalendarEvent);
|
|
141
|
+
if (calEvents.length > 0) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="space-y-2">
|
|
144
|
+
{calEvents.map((event, i) => (
|
|
145
|
+
<EventCard key={event.id || i} event={event} />
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
|
|
155
|
+
{JSON.stringify(data, null, 2)}
|
|
156
|
+
</pre>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Calendar, Clock, MapPin } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface CalendlyEventData {
|
|
5
|
+
uri?: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
status?: string;
|
|
8
|
+
start_time?: string;
|
|
9
|
+
end_time?: string;
|
|
10
|
+
location?: { type?: string; location?: string };
|
|
11
|
+
event_type?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface CalendlyEventTypeData {
|
|
15
|
+
uri?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
duration?: number;
|
|
18
|
+
active?: boolean;
|
|
19
|
+
slug?: string;
|
|
20
|
+
description_plain?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CalendlyRendererProps {
|
|
24
|
+
data: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isCalendlyEvent(data: unknown): data is CalendlyEventData {
|
|
28
|
+
return typeof data === "object" && data !== null && "name" in data && ("start_time" in data || "status" in data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isCalendlyEventType(data: unknown): data is CalendlyEventTypeData {
|
|
32
|
+
return typeof data === "object" && data !== null && "name" in data && ("duration" in data || "slug" in data || "active" in data);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatDateTime(dateStr: string): string {
|
|
36
|
+
try {
|
|
37
|
+
return new Date(dateStr).toLocaleString(undefined, {
|
|
38
|
+
month: "short",
|
|
39
|
+
day: "numeric",
|
|
40
|
+
hour: "numeric",
|
|
41
|
+
minute: "2-digit",
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
return dateStr;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const STATUS_STYLES: Record<string, string> = {
|
|
49
|
+
active: "bg-green-500/10 text-green-500",
|
|
50
|
+
canceled: "bg-destructive/10 text-destructive",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function EventCard({ event }: { event: CalendlyEventData }) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="rounded-lg border border-border bg-card p-3 space-y-1">
|
|
56
|
+
<div className="flex items-start gap-2">
|
|
57
|
+
<Calendar className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
58
|
+
<div className="min-w-0 flex-1">
|
|
59
|
+
<p className="text-sm font-medium text-foreground">{event.name}</p>
|
|
60
|
+
{event.start_time && (
|
|
61
|
+
<p className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
62
|
+
<Clock className="h-3 w-3" />
|
|
63
|
+
{formatDateTime(event.start_time)}
|
|
64
|
+
{event.end_time && ` - ${formatDateTime(event.end_time)}`}
|
|
65
|
+
</p>
|
|
66
|
+
)}
|
|
67
|
+
{event.location?.location && (
|
|
68
|
+
<p className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
69
|
+
<MapPin className="h-3 w-3" />
|
|
70
|
+
{event.location.location}
|
|
71
|
+
</p>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
{event.status && (
|
|
75
|
+
<span className={`rounded-full px-2 py-0.5 text-xs ${STATUS_STYLES[event.status] || "bg-muted text-muted-foreground"}`}>
|
|
76
|
+
{event.status}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function EventTypeCard({ eventType }: { eventType: CalendlyEventTypeData }) {
|
|
85
|
+
return (
|
|
86
|
+
<div className="rounded-lg border border-border bg-card p-3">
|
|
87
|
+
<div className="flex items-start gap-2">
|
|
88
|
+
<Calendar className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
89
|
+
<div className="min-w-0 flex-1">
|
|
90
|
+
<p className="text-sm font-medium text-foreground">{eventType.name}</p>
|
|
91
|
+
{eventType.description_plain && (
|
|
92
|
+
<p className="text-xs text-muted-foreground line-clamp-2">{eventType.description_plain}</p>
|
|
93
|
+
)}
|
|
94
|
+
{eventType.duration !== undefined && (
|
|
95
|
+
<p className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
96
|
+
<Clock className="h-3 w-3" />
|
|
97
|
+
{eventType.duration} min
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
{eventType.active !== undefined && (
|
|
102
|
+
<span className={`rounded-full px-2 py-0.5 text-xs ${eventType.active ? "bg-green-500/10 text-green-500" : "bg-muted text-muted-foreground"}`}>
|
|
103
|
+
{eventType.active ? "Active" : "Inactive"}
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function CalendlyRenderer({ data }: CalendlyRendererProps) {
|
|
112
|
+
if (typeof data === "object" && data !== null) {
|
|
113
|
+
const d = data as Record<string, unknown>;
|
|
114
|
+
|
|
115
|
+
// {collection: [...]}
|
|
116
|
+
if (Array.isArray(d.collection)) {
|
|
117
|
+
const events = d.collection.filter(isCalendlyEvent);
|
|
118
|
+
const types = d.collection.filter(isCalendlyEventType);
|
|
119
|
+
if (types.length > 0 && events.length === 0) {
|
|
120
|
+
return <div className="space-y-1.5">{types.map((t, i) => <EventTypeCard key={t.uri || i} eventType={t} />)}</div>;
|
|
121
|
+
}
|
|
122
|
+
if (events.length > 0) {
|
|
123
|
+
return <div className="space-y-1.5">{events.map((e, i) => <EventCard key={e.uri || i} event={e} />)}</div>;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// {scheduled_events: [...]}
|
|
128
|
+
if (Array.isArray(d.scheduled_events)) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="space-y-1.5">
|
|
131
|
+
{d.scheduled_events.filter(isCalendlyEvent).map((e: CalendlyEventData, i: number) => <EventCard key={e.uri || i} event={e} />)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// {event_types: [...]}
|
|
137
|
+
if (Array.isArray(d.event_types)) {
|
|
138
|
+
return (
|
|
139
|
+
<div className="space-y-1.5">
|
|
140
|
+
{d.event_types.filter(isCalendlyEventType).map((t: CalendlyEventTypeData, i: number) => <EventTypeCard key={t.uri || i} eventType={t} />)}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isCalendlyEvent(data)) return <EventCard event={data} />;
|
|
147
|
+
if (isCalendlyEventType(data)) return <EventTypeCard eventType={data} />;
|
|
148
|
+
|
|
149
|
+
if (Array.isArray(data)) {
|
|
150
|
+
const events = data.filter(isCalendlyEvent);
|
|
151
|
+
if (events.length > 0) {
|
|
152
|
+
return <div className="space-y-1.5">{events.map((e, i) => <EventCard key={e.uri || i} event={e} />)}</div>;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
|
|
158
|
+
{JSON.stringify(data, null, 2)}
|
|
159
|
+
</pre>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Terminal, AlertTriangle, Check, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface ComputeData {
|
|
5
|
+
stdout?: string;
|
|
6
|
+
stderr?: string;
|
|
7
|
+
exit_code?: number;
|
|
8
|
+
exitCode?: number;
|
|
9
|
+
duration?: number;
|
|
10
|
+
duration_ms?: number;
|
|
11
|
+
output?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
files?: Array<{ name?: string; url?: string; size?: number }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ComputeRendererProps {
|
|
18
|
+
data: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isComputeData(data: unknown): data is ComputeData {
|
|
22
|
+
if (typeof data !== "object" || data === null) return false;
|
|
23
|
+
return "stdout" in data || "stderr" in data || "exit_code" in data || "exitCode" in data || "output" in data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatDuration(ms: number): string {
|
|
27
|
+
if (ms < 1000) return `${ms}ms`;
|
|
28
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
29
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ComputeRenderer({ data }: ComputeRendererProps) {
|
|
33
|
+
if (!isComputeData(data)) {
|
|
34
|
+
return (
|
|
35
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
|
|
36
|
+
{JSON.stringify(data, null, 2)}
|
|
37
|
+
</pre>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const exitCode = data.exit_code ?? data.exitCode;
|
|
42
|
+
const duration = data.duration_ms ?? data.duration;
|
|
43
|
+
const stdout = data.stdout || data.output || "";
|
|
44
|
+
const stderr = data.stderr || data.error || "";
|
|
45
|
+
const success = exitCode === undefined || exitCode === 0;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="space-y-2">
|
|
49
|
+
<div className="rounded-lg border border-border bg-background overflow-hidden">
|
|
50
|
+
{/* Header */}
|
|
51
|
+
<div className="flex items-center gap-2 px-3 py-1.5 bg-muted border-b border-border">
|
|
52
|
+
<Terminal className="h-3.5 w-3.5 text-muted-foreground" />
|
|
53
|
+
<span className="text-xs text-muted-foreground flex-1">Output</span>
|
|
54
|
+
<div className="flex items-center gap-2">
|
|
55
|
+
{duration !== undefined && (
|
|
56
|
+
<span className="text-xs text-muted-foreground">{formatDuration(duration)}</span>
|
|
57
|
+
)}
|
|
58
|
+
{exitCode !== undefined && (
|
|
59
|
+
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs ${
|
|
60
|
+
success ? "text-green-500 bg-green-500/10" : "text-destructive bg-destructive/10"
|
|
61
|
+
}`}>
|
|
62
|
+
{success ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />}
|
|
63
|
+
exit {exitCode}
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Stdout */}
|
|
70
|
+
{stdout && (
|
|
71
|
+
<pre className="p-3 text-xs text-foreground overflow-x-auto max-h-80 overflow-y-auto font-mono whitespace-pre-wrap">
|
|
72
|
+
{stdout}
|
|
73
|
+
</pre>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{/* Stderr */}
|
|
77
|
+
{stderr && (
|
|
78
|
+
<div className="border-t border-border">
|
|
79
|
+
<div className="flex items-center gap-1.5 px-3 py-1 bg-destructive/5">
|
|
80
|
+
<AlertTriangle className="h-3 w-3 text-destructive" />
|
|
81
|
+
<span className="text-xs text-destructive">stderr</span>
|
|
82
|
+
</div>
|
|
83
|
+
<pre className="p-3 text-xs text-destructive overflow-x-auto max-h-40 overflow-y-auto font-mono whitespace-pre-wrap">
|
|
84
|
+
{stderr}
|
|
85
|
+
</pre>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{!stdout && !stderr && (
|
|
90
|
+
<p className="p-3 text-xs text-muted-foreground italic">No output</p>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Output files */}
|
|
95
|
+
{data.files && data.files.length > 0 && (
|
|
96
|
+
<div className="space-y-1">
|
|
97
|
+
{data.files.map((file, i) => (
|
|
98
|
+
<div key={i} className="flex items-center gap-2 rounded-lg border border-border bg-card p-2">
|
|
99
|
+
<Terminal className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
100
|
+
{file.url ? (
|
|
101
|
+
<a href={file.url} target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline truncate">
|
|
102
|
+
{file.name || file.url}
|
|
103
|
+
</a>
|
|
104
|
+
) : (
|
|
105
|
+
<span className="text-xs text-foreground truncate">{file.name}</span>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|