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.
- 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,161 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Mail, Paperclip, Tag } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface EmailData {
|
|
5
|
+
id?: string;
|
|
6
|
+
subject?: string;
|
|
7
|
+
from?: string;
|
|
8
|
+
to?: string | string[];
|
|
9
|
+
date?: string;
|
|
10
|
+
snippet?: string;
|
|
11
|
+
body?: string;
|
|
12
|
+
hasAttachments?: boolean;
|
|
13
|
+
labels?: string[];
|
|
14
|
+
/** Supyagent API returns labelIds instead of labels */
|
|
15
|
+
labelIds?: string[];
|
|
16
|
+
isUnread?: boolean;
|
|
17
|
+
isStarred?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface GmailRendererProps {
|
|
21
|
+
data: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isEmailData(data: unknown): data is EmailData {
|
|
25
|
+
return typeof data === "object" && data !== null && ("subject" in data || "from" in data || "snippet" in data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatRelativeDate(dateStr: string): string {
|
|
29
|
+
try {
|
|
30
|
+
const date = new Date(dateStr);
|
|
31
|
+
const now = new Date();
|
|
32
|
+
const diffMs = now.getTime() - date.getTime();
|
|
33
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
34
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
35
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
36
|
+
|
|
37
|
+
if (diffMins < 1) return "Just now";
|
|
38
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
39
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
40
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
41
|
+
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
42
|
+
} catch {
|
|
43
|
+
return dateStr;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatRecipients(to: string | string[] | undefined): string | null {
|
|
48
|
+
if (!to) return null;
|
|
49
|
+
if (Array.isArray(to)) return to.join(", ");
|
|
50
|
+
return to;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Internal Gmail label IDs that shouldn't be shown as tags */
|
|
54
|
+
const HIDDEN_LABELS = new Set(["INBOX", "UNREAD", "SENT", "DRAFT", "SPAM", "TRASH", "STARRED", "IMPORTANT"]);
|
|
55
|
+
|
|
56
|
+
function humanizeLabel(label: string): string {
|
|
57
|
+
// "CATEGORY_UPDATES" → "Updates", "CATEGORY_PERSONAL" → "Personal"
|
|
58
|
+
if (label.startsWith("CATEGORY_")) return label.slice(9).charAt(0) + label.slice(10).toLowerCase();
|
|
59
|
+
return label;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveLabels(email: EmailData): string[] {
|
|
63
|
+
if (email.labels && email.labels.length > 0) return email.labels;
|
|
64
|
+
if (email.labelIds && email.labelIds.length > 0) {
|
|
65
|
+
return email.labelIds
|
|
66
|
+
.filter((id) => !HIDDEN_LABELS.has(id))
|
|
67
|
+
.map(humanizeLabel);
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function EmailCard({ email }: { email: EmailData }) {
|
|
73
|
+
const recipients = formatRecipients(email.to);
|
|
74
|
+
const labels = resolveLabels(email);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="rounded-lg border border-border bg-card p-3 space-y-2">
|
|
78
|
+
<div className="flex items-start gap-2">
|
|
79
|
+
<Mail className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
80
|
+
<div className="min-w-0 flex-1">
|
|
81
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
82
|
+
{email.subject || "No subject"}
|
|
83
|
+
</p>
|
|
84
|
+
{email.from && (
|
|
85
|
+
<p className="text-xs text-muted-foreground truncate">{email.from}</p>
|
|
86
|
+
)}
|
|
87
|
+
{recipients && (
|
|
88
|
+
<p className="text-xs text-muted-foreground truncate">To: {recipients}</p>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
92
|
+
{email.hasAttachments && (
|
|
93
|
+
<Paperclip className="h-3.5 w-3.5 text-muted-foreground" />
|
|
94
|
+
)}
|
|
95
|
+
{email.date && (
|
|
96
|
+
<span className="text-xs text-muted-foreground">{formatRelativeDate(email.date)}</span>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
{labels.length > 0 && (
|
|
101
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
102
|
+
{labels.map((label) => (
|
|
103
|
+
<span
|
|
104
|
+
key={label}
|
|
105
|
+
className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
|
|
106
|
+
>
|
|
107
|
+
<Tag className="h-2.5 w-2.5" />
|
|
108
|
+
{label}
|
|
109
|
+
</span>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
{(email.snippet || email.body) && (
|
|
114
|
+
<p className="text-xs text-muted-foreground line-clamp-3">
|
|
115
|
+
{email.snippet || email.body}
|
|
116
|
+
</p>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function GmailRenderer({ data }: GmailRendererProps) {
|
|
123
|
+
if (isEmailData(data)) {
|
|
124
|
+
return <EmailCard email={data} />;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(data)) {
|
|
128
|
+
const emails = data.filter(isEmailData);
|
|
129
|
+
if (emails.length > 0) {
|
|
130
|
+
return (
|
|
131
|
+
<div className="space-y-2">
|
|
132
|
+
{emails.map((email, i) => (
|
|
133
|
+
<EmailCard key={email.id || i} email={email} />
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof data === "object" && data !== null && "messages" in data) {
|
|
141
|
+
const messages = (data as { messages: unknown[] }).messages;
|
|
142
|
+
if (Array.isArray(messages)) {
|
|
143
|
+
const emails = messages.filter(isEmailData);
|
|
144
|
+
if (emails.length > 0) {
|
|
145
|
+
return (
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
{emails.map((email, i) => (
|
|
148
|
+
<EmailCard key={email.id || i} email={email} />
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
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,206 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Users, Building2, Mail, Phone } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface HubspotContactData {
|
|
5
|
+
id?: string;
|
|
6
|
+
/** Normalized fields returned by Supyagent API */
|
|
7
|
+
firstName?: string;
|
|
8
|
+
lastName?: string;
|
|
9
|
+
email?: string;
|
|
10
|
+
phone?: string;
|
|
11
|
+
company?: string;
|
|
12
|
+
/** Raw HubSpot properties (direct HubSpot API shape) */
|
|
13
|
+
properties?: {
|
|
14
|
+
firstname?: string;
|
|
15
|
+
lastname?: string;
|
|
16
|
+
email?: string;
|
|
17
|
+
phone?: string;
|
|
18
|
+
company?: string;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface HubspotCompanyData {
|
|
24
|
+
id?: string;
|
|
25
|
+
properties?: {
|
|
26
|
+
name?: string;
|
|
27
|
+
domain?: string;
|
|
28
|
+
industry?: string;
|
|
29
|
+
phone?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface HubSpotRendererProps {
|
|
35
|
+
data: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isHubspotContact(data: unknown): data is HubspotContactData {
|
|
39
|
+
if (typeof data !== "object" || data === null) return false;
|
|
40
|
+
const d = data as any;
|
|
41
|
+
// Normalized shape from Supyagent API
|
|
42
|
+
if ("firstName" in d || "lastName" in d || (d.email && d.id)) return true;
|
|
43
|
+
// Raw HubSpot properties shape
|
|
44
|
+
const props = d.properties;
|
|
45
|
+
return props && ("firstname" in props || "lastname" in props || "email" in props);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isHubspotCompany(data: unknown): data is HubspotCompanyData {
|
|
49
|
+
if (typeof data !== "object" || data === null) return false;
|
|
50
|
+
const props = (data as any).properties;
|
|
51
|
+
return props && ("name" in props || "domain" in props);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ContactCard({ contact }: { contact: HubspotContactData }) {
|
|
55
|
+
const p = contact.properties || {};
|
|
56
|
+
// Support both normalized (firstName) and raw (properties.firstname) shapes
|
|
57
|
+
const first = contact.firstName || p.firstname;
|
|
58
|
+
const last = contact.lastName || p.lastname;
|
|
59
|
+
const email = contact.email || p.email;
|
|
60
|
+
const phone = contact.phone || p.phone;
|
|
61
|
+
const company = contact.company || p.company;
|
|
62
|
+
const name = [first, last].filter(Boolean).join(" ") || "Unknown contact";
|
|
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
|
+
<Users 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">{name}</p>
|
|
70
|
+
{company && (
|
|
71
|
+
<p className="text-xs text-muted-foreground">{company}</p>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 pl-6">
|
|
76
|
+
{email && (
|
|
77
|
+
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
78
|
+
<Mail className="h-3 w-3" />
|
|
79
|
+
{email}
|
|
80
|
+
</span>
|
|
81
|
+
)}
|
|
82
|
+
{phone && (
|
|
83
|
+
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
84
|
+
<Phone className="h-3 w-3" />
|
|
85
|
+
{phone}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function CompanyCard({ company }: { company: HubspotCompanyData }) {
|
|
94
|
+
const p = company.properties || {};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
|
|
98
|
+
<div className="flex items-start gap-2">
|
|
99
|
+
<Building2 className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
100
|
+
<div className="min-w-0 flex-1">
|
|
101
|
+
<p className="text-sm font-medium text-foreground">{p.name || "Unknown company"}</p>
|
|
102
|
+
{p.industry && (
|
|
103
|
+
<p className="text-xs text-muted-foreground">{p.industry}</p>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 pl-6">
|
|
108
|
+
{p.domain && (
|
|
109
|
+
<span className="text-xs text-muted-foreground">{p.domain}</span>
|
|
110
|
+
)}
|
|
111
|
+
{p.phone && (
|
|
112
|
+
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
113
|
+
<Phone className="h-3 w-3" />
|
|
114
|
+
{p.phone}
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function HubSpotRenderer({ data }: HubSpotRendererProps) {
|
|
123
|
+
// Single contact
|
|
124
|
+
if (isHubspotContact(data)) {
|
|
125
|
+
return <ContactCard contact={data} />;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Single company
|
|
129
|
+
if (isHubspotCompany(data)) {
|
|
130
|
+
return <CompanyCard company={data} />;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Array
|
|
134
|
+
if (Array.isArray(data)) {
|
|
135
|
+
const contacts = data.filter(isHubspotContact);
|
|
136
|
+
if (contacts.length > 0) {
|
|
137
|
+
return (
|
|
138
|
+
<div className="space-y-2">
|
|
139
|
+
{contacts.map((c, i) => (
|
|
140
|
+
<ContactCard key={c.id || i} contact={c} />
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const companies = data.filter(isHubspotCompany);
|
|
146
|
+
if (companies.length > 0) {
|
|
147
|
+
return (
|
|
148
|
+
<div className="space-y-2">
|
|
149
|
+
{companies.map((c, i) => (
|
|
150
|
+
<CompanyCard key={c.id || i} company={c} />
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Object with contacts array (Supyagent API shape)
|
|
158
|
+
if (typeof data === "object" && data !== null && "contacts" in data) {
|
|
159
|
+
const contacts = (data as any).contacts;
|
|
160
|
+
if (Array.isArray(contacts)) {
|
|
161
|
+
const valid = contacts.filter(isHubspotContact);
|
|
162
|
+
if (valid.length > 0) {
|
|
163
|
+
return (
|
|
164
|
+
<div className="space-y-2">
|
|
165
|
+
{valid.map((c, i) => (
|
|
166
|
+
<ContactCard key={c.id || i} contact={c} />
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Object with results array
|
|
175
|
+
if (typeof data === "object" && data !== null && "results" in data) {
|
|
176
|
+
const results = (data as any).results;
|
|
177
|
+
if (Array.isArray(results)) {
|
|
178
|
+
const contacts = results.filter(isHubspotContact);
|
|
179
|
+
if (contacts.length > 0) {
|
|
180
|
+
return (
|
|
181
|
+
<div className="space-y-2">
|
|
182
|
+
{contacts.map((c, i) => (
|
|
183
|
+
<ContactCard key={c.id || i} contact={c} />
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const companies = results.filter(isHubspotCompany);
|
|
189
|
+
if (companies.length > 0) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="space-y-2">
|
|
192
|
+
{companies.map((c, i) => (
|
|
193
|
+
<CompanyCard key={c.id || i} company={c} />
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
|
|
203
|
+
{JSON.stringify(data, null, 2)}
|
|
204
|
+
</pre>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Bell, Calendar, Mail, MessageSquare } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface InboxEventData {
|
|
5
|
+
id?: string;
|
|
6
|
+
/** Legacy fields */
|
|
7
|
+
type?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
source?: string;
|
|
11
|
+
timestamp?: string;
|
|
12
|
+
created_at?: string;
|
|
13
|
+
read?: boolean;
|
|
14
|
+
/** Actual API fields */
|
|
15
|
+
event_type?: string;
|
|
16
|
+
summary?: string;
|
|
17
|
+
provider?: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
received_at?: string;
|
|
20
|
+
updated_at?: string;
|
|
21
|
+
payload?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface InboxRendererProps {
|
|
25
|
+
data: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isInboxEvent(data: unknown): data is InboxEventData {
|
|
29
|
+
if (typeof data !== "object" || data === null) return false;
|
|
30
|
+
// Actual API shape
|
|
31
|
+
if ("event_type" in data || "summary" in data) return true;
|
|
32
|
+
// Legacy/mock shape
|
|
33
|
+
if ("title" in data || ("type" in data && "source" in data)) return true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveEventType(event: InboxEventData): string | undefined {
|
|
38
|
+
if (event.event_type) {
|
|
39
|
+
// "email.received" → "email", "message" → "message"
|
|
40
|
+
return event.event_type.split(".")[0];
|
|
41
|
+
}
|
|
42
|
+
return event.type;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getEventIcon(type?: string) {
|
|
46
|
+
switch (type) {
|
|
47
|
+
case "email":
|
|
48
|
+
return Mail;
|
|
49
|
+
case "message":
|
|
50
|
+
return MessageSquare;
|
|
51
|
+
case "calendar":
|
|
52
|
+
return Calendar;
|
|
53
|
+
default:
|
|
54
|
+
return Bell;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatTimestamp(dateStr: string): string {
|
|
59
|
+
try {
|
|
60
|
+
const date = new Date(dateStr);
|
|
61
|
+
const now = new Date();
|
|
62
|
+
const diffMs = now.getTime() - date.getTime();
|
|
63
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
64
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
65
|
+
|
|
66
|
+
if (diffHours < 1) return "Just now";
|
|
67
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
68
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
69
|
+
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
70
|
+
} catch {
|
|
71
|
+
return dateStr;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function EventCard({ event }: { event: InboxEventData }) {
|
|
76
|
+
const eventType = resolveEventType(event);
|
|
77
|
+
const Icon = getEventIcon(eventType);
|
|
78
|
+
const timestamp = event.received_at || event.timestamp || event.created_at;
|
|
79
|
+
const title = event.summary || event.title;
|
|
80
|
+
const source = event.provider || event.source;
|
|
81
|
+
const snippet = event.description || (event.payload?.snippet as string) || (event.payload?.text as string);
|
|
82
|
+
const isUnread = event.status === "unread" || event.read === false;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
|
|
86
|
+
<div className="flex items-start gap-2">
|
|
87
|
+
<Icon className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
88
|
+
<div className="min-w-0 flex-1">
|
|
89
|
+
<div className="flex items-center gap-2">
|
|
90
|
+
{eventType && (
|
|
91
|
+
<span className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground capitalize">
|
|
92
|
+
{eventType}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
{source && (
|
|
96
|
+
<span className="text-xs text-muted-foreground capitalize">{source}</span>
|
|
97
|
+
)}
|
|
98
|
+
{isUnread && (
|
|
99
|
+
<span className="h-1.5 w-1.5 rounded-full bg-primary shrink-0" />
|
|
100
|
+
)}
|
|
101
|
+
{timestamp && (
|
|
102
|
+
<span className="text-xs text-muted-foreground ml-auto shrink-0">
|
|
103
|
+
{formatTimestamp(timestamp)}
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
{title && (
|
|
108
|
+
<p className="text-sm font-medium text-foreground mt-1 line-clamp-1">{title}</p>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
{snippet && (
|
|
113
|
+
<p className="text-xs text-muted-foreground line-clamp-2 pl-6">{snippet}</p>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function InboxRenderer({ data }: InboxRendererProps) {
|
|
120
|
+
if (isInboxEvent(data)) {
|
|
121
|
+
return <EventCard event={data} />;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (Array.isArray(data)) {
|
|
125
|
+
const events = data.filter(isInboxEvent);
|
|
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 any).events;
|
|
139
|
+
if (Array.isArray(events)) {
|
|
140
|
+
const inboxEvents = events.filter(isInboxEvent);
|
|
141
|
+
if (inboxEvents.length > 0) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="space-y-2">
|
|
144
|
+
{inboxEvents.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,164 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { CircleDot, SquareKanban, User, ArrowUp, ArrowDown, Minus } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface JiraIssueData {
|
|
5
|
+
key?: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
self?: string;
|
|
8
|
+
fields?: {
|
|
9
|
+
summary?: string;
|
|
10
|
+
status?: { name?: string; statusCategory?: { colorName?: string; key?: string } };
|
|
11
|
+
priority?: { name?: string; id?: string };
|
|
12
|
+
assignee?: { displayName?: string; name?: string };
|
|
13
|
+
issuetype?: { name?: string };
|
|
14
|
+
created?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface JiraProjectData {
|
|
19
|
+
key?: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
projectTypeKey?: string;
|
|
23
|
+
self?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface JiraRendererProps {
|
|
27
|
+
data: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isJiraIssue(data: unknown): data is JiraIssueData {
|
|
31
|
+
return typeof data === "object" && data !== null && "key" in data && "fields" in data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isJiraProject(data: unknown): data is JiraProjectData {
|
|
35
|
+
return typeof data === "object" && data !== null && "key" in data && "name" in data && "projectTypeKey" in data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
39
|
+
blue: "bg-blue-500",
|
|
40
|
+
green: "bg-green-500",
|
|
41
|
+
yellow: "bg-yellow-500",
|
|
42
|
+
"blue-gray": "bg-slate-400",
|
|
43
|
+
undefined: "bg-muted-foreground",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function PriorityIcon({ name }: { name?: string }) {
|
|
47
|
+
const lower = (name || "").toLowerCase();
|
|
48
|
+
if (lower === "highest" || lower === "critical") return <ArrowUp className="h-3 w-3 text-destructive" />;
|
|
49
|
+
if (lower === "high") return <ArrowUp className="h-3 w-3 text-orange-500" />;
|
|
50
|
+
if (lower === "low") return <ArrowDown className="h-3 w-3 text-blue-500" />;
|
|
51
|
+
if (lower === "lowest") return <ArrowDown className="h-3 w-3 text-green-500" />;
|
|
52
|
+
return <Minus className="h-3 w-3 text-muted-foreground" />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function IssueCard({ issue }: { issue: JiraIssueData }) {
|
|
56
|
+
const f = issue.fields || {};
|
|
57
|
+
const statusColor = f.status?.statusCategory?.colorName || "undefined";
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
|
|
61
|
+
<div className="flex items-start gap-2">
|
|
62
|
+
<CircleDot className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
63
|
+
<div className="min-w-0 flex-1">
|
|
64
|
+
<p className="text-sm font-medium text-foreground">
|
|
65
|
+
<span className="text-muted-foreground font-normal">{issue.key}</span>{" "}
|
|
66
|
+
{f.summary}
|
|
67
|
+
</p>
|
|
68
|
+
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
|
69
|
+
{f.status?.name && (
|
|
70
|
+
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
|
|
71
|
+
<span className={`h-1.5 w-1.5 rounded-full ${STATUS_COLORS[statusColor] || STATUS_COLORS.undefined}`} />
|
|
72
|
+
{f.status.name}
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
75
|
+
{f.priority?.name && (
|
|
76
|
+
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
|
|
77
|
+
<PriorityIcon name={f.priority.name} />
|
|
78
|
+
{f.priority.name}
|
|
79
|
+
</span>
|
|
80
|
+
)}
|
|
81
|
+
{f.issuetype?.name && (
|
|
82
|
+
<span className="text-xs text-muted-foreground">{f.issuetype.name}</span>
|
|
83
|
+
)}
|
|
84
|
+
{f.assignee && (
|
|
85
|
+
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
86
|
+
<User className="h-3 w-3" />
|
|
87
|
+
{f.assignee.displayName || f.assignee.name}
|
|
88
|
+
</span>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ProjectCard({ project }: { project: JiraProjectData }) {
|
|
98
|
+
return (
|
|
99
|
+
<div className="rounded-lg border border-border bg-card p-3">
|
|
100
|
+
<div className="flex items-start gap-2">
|
|
101
|
+
<SquareKanban className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
|
|
102
|
+
<div className="min-w-0 flex-1">
|
|
103
|
+
<p className="text-sm font-medium text-foreground">
|
|
104
|
+
<span className="text-muted-foreground font-normal">{project.key}</span>{" "}
|
|
105
|
+
{project.name}
|
|
106
|
+
</p>
|
|
107
|
+
{project.projectTypeKey && (
|
|
108
|
+
<p className="text-xs text-muted-foreground">{project.projectTypeKey}</p>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function JiraRenderer({ data }: JiraRendererProps) {
|
|
117
|
+
// {issues: [...]}
|
|
118
|
+
if (typeof data === "object" && data !== null && "issues" in data) {
|
|
119
|
+
const issues = (data as any).issues;
|
|
120
|
+
if (Array.isArray(issues)) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="space-y-1.5">
|
|
123
|
+
{issues.filter(isJiraIssue).map((iss, i) => <IssueCard key={iss.key || i} issue={iss} />)}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// {projects: [...]}
|
|
130
|
+
if (typeof data === "object" && data !== null && "projects" in data) {
|
|
131
|
+
const projects = (data as any).projects;
|
|
132
|
+
if (Array.isArray(projects)) {
|
|
133
|
+
return (
|
|
134
|
+
<div className="space-y-1.5">
|
|
135
|
+
{projects.filter(isJiraProject).map((p, i) => <ProjectCard key={p.key || i} project={p} />)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Single issue
|
|
142
|
+
if (isJiraIssue(data)) return <IssueCard issue={data} />;
|
|
143
|
+
|
|
144
|
+
// Single project
|
|
145
|
+
if (isJiraProject(data)) return <ProjectCard project={data} />;
|
|
146
|
+
|
|
147
|
+
// Array
|
|
148
|
+
if (Array.isArray(data)) {
|
|
149
|
+
const issues = data.filter(isJiraIssue);
|
|
150
|
+
if (issues.length > 0) {
|
|
151
|
+
return (
|
|
152
|
+
<div className="space-y-1.5">
|
|
153
|
+
{issues.map((iss, i) => <IssueCard key={iss.key || i} issue={iss} />)}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
|
|
161
|
+
{JSON.stringify(data, null, 2)}
|
|
162
|
+
</pre>
|
|
163
|
+
);
|
|
164
|
+
}
|