create-supyagent-app 0.1.14 → 0.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-supyagent-app",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Create a supyagent-powered chatbot app",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,15 +1,26 @@
1
1
  import React from "react";
2
- import { Users, Building2, Mail, Phone } from "lucide-react";
2
+ import {
3
+ Users,
4
+ Building2,
5
+ Mail,
6
+ Phone,
7
+ Globe,
8
+ Briefcase,
9
+ DollarSign,
10
+ Calendar,
11
+ } from "lucide-react";
12
+
13
+ /* ------------------------------------------------------------------ */
14
+ /* Interfaces */
15
+ /* ------------------------------------------------------------------ */
3
16
 
4
17
  interface HubspotContactData {
5
18
  id?: string;
6
- /** Normalized fields returned by Supyagent API */
7
19
  firstName?: string;
8
20
  lastName?: string;
9
21
  email?: string;
10
22
  phone?: string;
11
23
  company?: string;
12
- /** Raw HubSpot properties (direct HubSpot API shape) */
13
24
  properties?: {
14
25
  firstname?: string;
15
26
  lastname?: string;
@@ -22,38 +33,114 @@ interface HubspotContactData {
22
33
 
23
34
  interface HubspotCompanyData {
24
35
  id?: string;
36
+ name?: string;
37
+ domain?: string;
38
+ industry?: string;
39
+ employeeCount?: number | null;
40
+ createdAt?: string;
41
+ updatedAt?: string;
42
+ /** Legacy shape: raw HubSpot properties */
25
43
  properties?: {
26
44
  name?: string;
27
45
  domain?: string;
28
46
  industry?: string;
29
47
  phone?: string;
48
+ numberofemployees?: string;
30
49
  [key: string]: unknown;
31
50
  };
32
51
  }
33
52
 
53
+ interface HubspotDealData {
54
+ id?: string;
55
+ name?: string;
56
+ amount?: number | null;
57
+ stage?: string;
58
+ closeDate?: string;
59
+ pipeline?: string;
60
+ createdAt?: string;
61
+ updatedAt?: string;
62
+ }
63
+
34
64
  interface HubSpotRendererProps {
35
65
  data: unknown;
36
66
  }
37
67
 
68
+ /* ------------------------------------------------------------------ */
69
+ /* Type guards */
70
+ /* ------------------------------------------------------------------ */
71
+
38
72
  function isHubspotContact(data: unknown): data is HubspotContactData {
39
73
  if (typeof data !== "object" || data === null) return false;
40
74
  const d = data as any;
41
- // Normalized shape from Supyagent API
42
75
  if ("firstName" in d || "lastName" in d || (d.email && d.id)) return true;
43
- // Raw HubSpot properties shape
44
76
  const props = d.properties;
45
77
  return props && ("firstname" in props || "lastname" in props || "email" in props);
46
78
  }
47
79
 
48
80
  function isHubspotCompany(data: unknown): data is HubspotCompanyData {
49
81
  if (typeof data !== "object" || data === null) return false;
50
- const props = (data as any).properties;
82
+ const d = data as any;
83
+ // Flat shape from Supyagent API (has domain at top level)
84
+ if ("domain" in d && !("email" in d) && !("amount" in d)) return true;
85
+ // Legacy raw HubSpot properties shape
86
+ const props = d.properties;
51
87
  return props && ("name" in props || "domain" in props);
52
88
  }
53
89
 
90
+ function isHubspotDeal(data: unknown): data is HubspotDealData {
91
+ if (typeof data !== "object" || data === null) return false;
92
+ const d = data as any;
93
+ return ("amount" in d || "stage" in d || "closeDate" in d) && !("email" in d);
94
+ }
95
+
96
+ /* ------------------------------------------------------------------ */
97
+ /* Helpers */
98
+ /* ------------------------------------------------------------------ */
99
+
100
+ function formatCurrency(value: number): string {
101
+ try {
102
+ return new Intl.NumberFormat(undefined, {
103
+ style: "currency",
104
+ currency: "USD",
105
+ minimumFractionDigits: 0,
106
+ maximumFractionDigits: 0,
107
+ }).format(value);
108
+ } catch {
109
+ return `$${value.toLocaleString()}`;
110
+ }
111
+ }
112
+
113
+ function formatDate(dateStr: string): string {
114
+ try {
115
+ return new Date(dateStr).toLocaleDateString(undefined, {
116
+ month: "short",
117
+ day: "numeric",
118
+ year: "numeric",
119
+ });
120
+ } catch {
121
+ return dateStr;
122
+ }
123
+ }
124
+
125
+ function getStageBadge(stage?: string) {
126
+ if (!stage) return null;
127
+ const s = stage.toLowerCase();
128
+ if (s === "closedwon" || s === "closed won")
129
+ return { label: "Won", className: "text-green-500 bg-green-500/10" };
130
+ if (s === "closedlost" || s === "closed lost")
131
+ return { label: "Lost", className: "text-red-400 bg-red-400/10" };
132
+ // Custom stage IDs — show as "In Progress"
133
+ if (/^\d+$/.test(stage))
134
+ return { label: "In Progress", className: "text-blue-400 bg-blue-400/10" };
135
+ return { label: stage, className: "text-muted-foreground bg-muted" };
136
+ }
137
+
138
+ /* ------------------------------------------------------------------ */
139
+ /* Cards */
140
+ /* ------------------------------------------------------------------ */
141
+
54
142
  function ContactCard({ contact }: { contact: HubspotContactData }) {
55
143
  const p = contact.properties || {};
56
- // Support both normalized (firstName) and raw (properties.firstname) shapes
57
144
  const first = contact.firstName || p.firstname;
58
145
  const last = contact.lastName || p.lastname;
59
146
  const email = contact.email || p.email;
@@ -92,107 +179,252 @@ function ContactCard({ contact }: { contact: HubspotContactData }) {
92
179
 
93
180
  function CompanyCard({ company }: { company: HubspotCompanyData }) {
94
181
  const p = company.properties || {};
182
+ // Support both flat shape (name at top level) and legacy (properties.name)
183
+ const name = company.name || p.name;
184
+ const domain = company.domain || p.domain;
185
+ const industry = company.industry || p.industry;
186
+ const phone = p.phone;
187
+ const employeeCount =
188
+ company.employeeCount ?? (p.numberofemployees ? Number(p.numberofemployees) : null);
95
189
 
96
190
  return (
97
191
  <div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
98
192
  <div className="flex items-start gap-2">
99
193
  <Building2 className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
100
194
  <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>
195
+ <p className="text-sm font-medium text-foreground">
196
+ {name || "Unknown company"}
197
+ </p>
198
+ {industry && (
199
+ <p className="text-xs text-muted-foreground">{industry}</p>
104
200
  )}
105
201
  </div>
106
202
  </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>
203
+ <div className="flex flex-wrap gap-x-4 gap-y-1 pl-6 text-xs text-muted-foreground">
204
+ {domain && (
205
+ <span className="flex items-center gap-1">
206
+ <Globe className="h-3 w-3" />
207
+ {domain}
208
+ </span>
110
209
  )}
111
- {p.phone && (
112
- <span className="flex items-center gap-1 text-xs text-muted-foreground">
210
+ {phone && (
211
+ <span className="flex items-center gap-1">
113
212
  <Phone className="h-3 w-3" />
114
- {p.phone}
213
+ {phone}
214
+ </span>
215
+ )}
216
+ {employeeCount != null && employeeCount > 0 && (
217
+ <span className="flex items-center gap-1">
218
+ <Users className="h-3 w-3" />
219
+ {employeeCount.toLocaleString()} employees
220
+ </span>
221
+ )}
222
+ </div>
223
+ </div>
224
+ );
225
+ }
226
+
227
+ function DealCard({ deal }: { deal: HubspotDealData }) {
228
+ const stageBadge = getStageBadge(deal.stage);
229
+
230
+ return (
231
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
232
+ <div className="flex items-start gap-2">
233
+ <Briefcase className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
234
+ <div className="min-w-0 flex-1">
235
+ <p className="text-sm font-medium text-foreground">
236
+ {deal.name || "Untitled deal"}
237
+ </p>
238
+ <div className="flex items-center gap-2 mt-0.5 flex-wrap">
239
+ {deal.amount != null && (
240
+ <span className="flex items-center gap-1 text-xs font-medium text-foreground">
241
+ <DollarSign className="h-3 w-3" />
242
+ {formatCurrency(deal.amount)}
243
+ </span>
244
+ )}
245
+ {stageBadge && (
246
+ <span
247
+ className={`rounded-full px-2 py-0.5 text-xs ${stageBadge.className}`}
248
+ >
249
+ {stageBadge.label}
250
+ </span>
251
+ )}
252
+ </div>
253
+ </div>
254
+ </div>
255
+ <div className="flex flex-wrap gap-x-4 gap-y-1 pl-6 text-xs text-muted-foreground">
256
+ {deal.closeDate && (
257
+ <span className="flex items-center gap-1">
258
+ <Calendar className="h-3 w-3" />
259
+ Close: {formatDate(deal.closeDate)}
115
260
  </span>
116
261
  )}
262
+ {deal.pipeline && deal.pipeline !== "default" && (
263
+ <span>{deal.pipeline}</span>
264
+ )}
117
265
  </div>
118
266
  </div>
119
267
  );
120
268
  }
121
269
 
270
+ /* ------------------------------------------------------------------ */
271
+ /* List wrapper with optional paging indicator */
272
+ /* ------------------------------------------------------------------ */
273
+
274
+ function ListWrapper({
275
+ children,
276
+ hasMore,
277
+ }: {
278
+ children: React.ReactNode;
279
+ hasMore?: boolean;
280
+ }) {
281
+ return (
282
+ <div className="space-y-2">
283
+ {children}
284
+ {hasMore && (
285
+ <p className="text-xs text-muted-foreground text-center pt-1">
286
+ More results available
287
+ </p>
288
+ )}
289
+ </div>
290
+ );
291
+ }
292
+
293
+ function hasPaging(data: unknown): boolean {
294
+ if (typeof data !== "object" || data === null) return false;
295
+ const paging = (data as any).paging;
296
+ return paging && typeof paging === "object" && paging.next;
297
+ }
298
+
299
+ /* ------------------------------------------------------------------ */
300
+ /* Renderer */
301
+ /* ------------------------------------------------------------------ */
302
+
122
303
  export function HubSpotRenderer({ data }: HubSpotRendererProps) {
123
304
  // Single contact
124
305
  if (isHubspotContact(data)) {
125
306
  return <ContactCard contact={data} />;
126
307
  }
127
308
 
309
+ // Single deal
310
+ if (isHubspotDeal(data)) {
311
+ return <DealCard deal={data} />;
312
+ }
313
+
128
314
  // Single company
129
315
  if (isHubspotCompany(data)) {
130
316
  return <CompanyCard company={data} />;
131
317
  }
132
318
 
133
- // Array
319
+ // Flat array
134
320
  if (Array.isArray(data)) {
321
+ const deals = data.filter(isHubspotDeal);
322
+ if (deals.length > 0) {
323
+ return (
324
+ <ListWrapper>
325
+ {deals.map((d, i) => (
326
+ <DealCard key={d.id || i} deal={d} />
327
+ ))}
328
+ </ListWrapper>
329
+ );
330
+ }
135
331
  const contacts = data.filter(isHubspotContact);
136
332
  if (contacts.length > 0) {
137
333
  return (
138
- <div className="space-y-2">
334
+ <ListWrapper>
139
335
  {contacts.map((c, i) => (
140
336
  <ContactCard key={c.id || i} contact={c} />
141
337
  ))}
142
- </div>
338
+ </ListWrapper>
143
339
  );
144
340
  }
145
341
  const companies = data.filter(isHubspotCompany);
146
342
  if (companies.length > 0) {
147
343
  return (
148
- <div className="space-y-2">
344
+ <ListWrapper>
149
345
  {companies.map((c, i) => (
150
346
  <CompanyCard key={c.id || i} company={c} />
151
347
  ))}
152
- </div>
348
+ </ListWrapper>
153
349
  );
154
350
  }
155
351
  }
156
352
 
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);
353
+ // Wrapper objects: { contacts: [...] }, { companies: [...] }, { deals: [...] }
354
+ if (typeof data === "object" && data !== null) {
355
+ const d = data as any;
356
+ const more = hasPaging(data);
357
+
358
+ if ("contacts" in d && Array.isArray(d.contacts)) {
359
+ const valid = d.contacts.filter(isHubspotContact);
162
360
  if (valid.length > 0) {
163
361
  return (
164
- <div className="space-y-2">
165
- {valid.map((c, i) => (
362
+ <ListWrapper hasMore={more}>
363
+ {valid.map((c: HubspotContactData, i: number) => (
166
364
  <ContactCard key={c.id || i} contact={c} />
167
365
  ))}
168
- </div>
366
+ </ListWrapper>
169
367
  );
170
368
  }
171
369
  }
172
- }
173
370
 
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)) {
371
+ if ("companies" in d && Array.isArray(d.companies)) {
372
+ const valid = d.companies.filter(isHubspotCompany);
373
+ if (valid.length > 0) {
374
+ return (
375
+ <ListWrapper hasMore={more}>
376
+ {valid.map((c: HubspotCompanyData, i: number) => (
377
+ <CompanyCard key={c.id || i} company={c} />
378
+ ))}
379
+ </ListWrapper>
380
+ );
381
+ }
382
+ }
383
+
384
+ if ("deals" in d && Array.isArray(d.deals)) {
385
+ const valid = d.deals.filter(isHubspotDeal);
386
+ if (valid.length > 0) {
387
+ return (
388
+ <ListWrapper hasMore={more}>
389
+ {valid.map((deal: HubspotDealData, i: number) => (
390
+ <DealCard key={deal.id || i} deal={deal} />
391
+ ))}
392
+ </ListWrapper>
393
+ );
394
+ }
395
+ }
396
+
397
+ // Raw HubSpot shape: { results: [...] }
398
+ if ("results" in d && Array.isArray(d.results)) {
399
+ const results = d.results;
400
+ const deals = results.filter(isHubspotDeal);
401
+ if (deals.length > 0) {
402
+ return (
403
+ <ListWrapper hasMore={more}>
404
+ {deals.map((deal: HubspotDealData, i: number) => (
405
+ <DealCard key={deal.id || i} deal={deal} />
406
+ ))}
407
+ </ListWrapper>
408
+ );
409
+ }
178
410
  const contacts = results.filter(isHubspotContact);
179
411
  if (contacts.length > 0) {
180
412
  return (
181
- <div className="space-y-2">
182
- {contacts.map((c, i) => (
413
+ <ListWrapper hasMore={more}>
414
+ {contacts.map((c: HubspotContactData, i: number) => (
183
415
  <ContactCard key={c.id || i} contact={c} />
184
416
  ))}
185
- </div>
417
+ </ListWrapper>
186
418
  );
187
419
  }
188
420
  const companies = results.filter(isHubspotCompany);
189
421
  if (companies.length > 0) {
190
422
  return (
191
- <div className="space-y-2">
192
- {companies.map((c, i) => (
423
+ <ListWrapper hasMore={more}>
424
+ {companies.map((c: HubspotCompanyData, i: number) => (
193
425
  <CompanyCard key={c.id || i} company={c} />
194
426
  ))}
195
- </div>
427
+ </ListWrapper>
196
428
  );
197
429
  }
198
430
  }
@@ -17,7 +17,7 @@
17
17
  "{{aiProviderPackage}}": "{{aiProviderVersion}}",
18
18
  "@prisma/client": "^6.2.0",
19
19
  "@radix-ui/react-collapsible": "^1.1.0",
20
- "@supyagent/sdk": "^0.1.12",
20
+ "@supyagent/sdk": "^0.1.13",
21
21
  "class-variance-authority": "^0.7.0",
22
22
  "ai": "^6.0.0",
23
23
  "clsx": "^2.1.0",