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.
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,196 @@
1
+ import React from "react";
2
+ import { CircleDot, FolderKanban } from "lucide-react";
3
+
4
+ interface LinearIssueData {
5
+ id?: string;
6
+ title?: string;
7
+ identifier?: string;
8
+ state?: { name?: string; color?: string; type?: string };
9
+ priority?: number;
10
+ priorityLabel?: string;
11
+ assignee?: { name?: string; displayName?: string; email?: string };
12
+ url?: string;
13
+ description?: string;
14
+ }
15
+
16
+ interface LinearProjectData {
17
+ id?: string;
18
+ name?: string;
19
+ state?: string;
20
+ progress?: number;
21
+ url?: string;
22
+ description?: string;
23
+ }
24
+
25
+ interface LinearRendererProps {
26
+ data: unknown;
27
+ }
28
+
29
+ function isLinearIssue(data: unknown): data is LinearIssueData {
30
+ if (typeof data !== "object" || data === null) return false;
31
+ return ("title" in data && ("identifier" in data || "state" in data || "priority" in data));
32
+ }
33
+
34
+ function isLinearProject(data: unknown): data is LinearProjectData {
35
+ if (typeof data !== "object" || data === null) return false;
36
+ return "name" in data && ("progress" in data || ("state" in data && typeof (data as any).state === "string"));
37
+ }
38
+
39
+ const PRIORITY_LABELS: Record<number, { label: string; style: string }> = {
40
+ 0: { label: "No priority", style: "text-muted-foreground" },
41
+ 1: { label: "Urgent", style: "text-destructive" },
42
+ 2: { label: "High", style: "text-orange-500" },
43
+ 3: { label: "Medium", style: "text-yellow-500" },
44
+ 4: { label: "Low", style: "text-muted-foreground" },
45
+ };
46
+
47
+ function IssueCard({ issue }: { issue: LinearIssueData }) {
48
+ const priorityInfo = issue.priority !== undefined ? PRIORITY_LABELS[issue.priority] : null;
49
+ const assigneeName = issue.assignee?.displayName || issue.assignee?.name;
50
+
51
+ return (
52
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
53
+ <div className="flex items-start gap-2">
54
+ <CircleDot
55
+ className="h-4 w-4 mt-0.5 shrink-0"
56
+ style={issue.state?.color ? { color: issue.state.color } : undefined}
57
+ />
58
+ <div className="min-w-0 flex-1">
59
+ <p className="text-sm font-medium text-foreground">
60
+ {issue.url ? (
61
+ <a href={issue.url} target="_blank" rel="noopener noreferrer" className="hover:underline">
62
+ {issue.title}
63
+ </a>
64
+ ) : (
65
+ issue.title
66
+ )}
67
+ {issue.identifier && (
68
+ <span className="text-muted-foreground font-normal"> {issue.identifier}</span>
69
+ )}
70
+ </p>
71
+ <div className="flex items-center gap-2 mt-0.5 flex-wrap">
72
+ {issue.state?.name && (
73
+ <span
74
+ className="rounded-full bg-muted px-2 py-0.5 text-xs"
75
+ style={issue.state.color ? { color: issue.state.color } : undefined}
76
+ >
77
+ {issue.state.name}
78
+ </span>
79
+ )}
80
+ {priorityInfo && (
81
+ <span className={`text-xs ${priorityInfo.style}`}>
82
+ {issue.priorityLabel || priorityInfo.label}
83
+ </span>
84
+ )}
85
+ {assigneeName && (
86
+ <span className="text-xs text-muted-foreground">{assigneeName}</span>
87
+ )}
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ function ProjectCard({ project }: { project: LinearProjectData }) {
96
+ const progressPercent = project.progress !== undefined ? Math.round(project.progress * 100) : null;
97
+
98
+ return (
99
+ <div className="rounded-lg border border-border bg-card p-3 space-y-2">
100
+ <div className="flex items-start gap-2">
101
+ <FolderKanban 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
+ {project.url ? (
105
+ <a href={project.url} target="_blank" rel="noopener noreferrer" className="hover:underline">
106
+ {project.name}
107
+ </a>
108
+ ) : (
109
+ project.name
110
+ )}
111
+ </p>
112
+ {project.state && (
113
+ <span className="text-xs text-muted-foreground">{project.state}</span>
114
+ )}
115
+ </div>
116
+ </div>
117
+ {progressPercent !== null && (
118
+ <div className="space-y-1">
119
+ <div className="h-1.5 rounded-full bg-muted overflow-hidden">
120
+ <div
121
+ className="h-full rounded-full bg-primary transition-all"
122
+ style={{ width: `${progressPercent}%` }}
123
+ />
124
+ </div>
125
+ <p className="text-xs text-muted-foreground text-right">{progressPercent}%</p>
126
+ </div>
127
+ )}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ export function LinearRenderer({ data }: LinearRendererProps) {
133
+ if (isLinearIssue(data)) {
134
+ return <IssueCard issue={data} />;
135
+ }
136
+
137
+ if (isLinearProject(data)) {
138
+ return <ProjectCard project={data} />;
139
+ }
140
+
141
+ if (Array.isArray(data)) {
142
+ const issues = data.filter(isLinearIssue);
143
+ if (issues.length > 0) {
144
+ return (
145
+ <div className="space-y-2">
146
+ {issues.map((issue, i) => (
147
+ <IssueCard key={issue.id || i} issue={issue} />
148
+ ))}
149
+ </div>
150
+ );
151
+ }
152
+ const projects = data.filter(isLinearProject);
153
+ if (projects.length > 0) {
154
+ return (
155
+ <div className="space-y-2">
156
+ {projects.map((project, i) => (
157
+ <ProjectCard key={project.id || i} project={project} />
158
+ ))}
159
+ </div>
160
+ );
161
+ }
162
+ }
163
+
164
+ // Object with nodes/issues array (Linear API pattern)
165
+ if (typeof data === "object" && data !== null) {
166
+ const arr = (data as any).nodes || (data as any).issues || (data as any).projects;
167
+ if (Array.isArray(arr)) {
168
+ const issues = arr.filter(isLinearIssue);
169
+ if (issues.length > 0) {
170
+ return (
171
+ <div className="space-y-2">
172
+ {issues.map((issue, i) => (
173
+ <IssueCard key={issue.id || i} issue={issue} />
174
+ ))}
175
+ </div>
176
+ );
177
+ }
178
+ const projects = arr.filter(isLinearProject);
179
+ if (projects.length > 0) {
180
+ return (
181
+ <div className="space-y-2">
182
+ {projects.map((project, i) => (
183
+ <ProjectCard key={project.id || i} project={project} />
184
+ ))}
185
+ </div>
186
+ );
187
+ }
188
+ }
189
+ }
190
+
191
+ return (
192
+ <pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
193
+ {JSON.stringify(data, null, 2)}
194
+ </pre>
195
+ );
196
+ }
@@ -0,0 +1,151 @@
1
+ import React from "react";
2
+ import { User, FileText, ExternalLink, Clock } from "lucide-react";
3
+
4
+ interface LinkedInProfileData {
5
+ id?: string;
6
+ localizedFirstName?: string;
7
+ localizedLastName?: string;
8
+ firstName?: string;
9
+ lastName?: string;
10
+ name?: string;
11
+ headline?: string;
12
+ vanityName?: string;
13
+ profilePicture?: unknown;
14
+ }
15
+
16
+ interface LinkedInPostData {
17
+ id?: string;
18
+ text?: string;
19
+ commentary?: string;
20
+ created?: { time?: number };
21
+ timestamp?: string;
22
+ author?: string;
23
+ }
24
+
25
+ interface LinkedInRendererProps {
26
+ data: unknown;
27
+ }
28
+
29
+ function isProfile(data: unknown): data is LinkedInProfileData {
30
+ if (typeof data !== "object" || data === null) return false;
31
+ return "localizedFirstName" in data || "firstName" in data || ("name" in data && "headline" in data);
32
+ }
33
+
34
+ function isPost(data: unknown): data is LinkedInPostData {
35
+ if (typeof data !== "object" || data === null) return false;
36
+ return ("text" in data || "commentary" in data) && !("localizedFirstName" in data);
37
+ }
38
+
39
+ function formatRelativeDate(dateStr: string | number): string {
40
+ try {
41
+ const date = typeof dateStr === "number" ? new Date(dateStr) : new Date(dateStr);
42
+ const now = new Date();
43
+ const diffMs = now.getTime() - date.getTime();
44
+ const diffDays = Math.floor(diffMs / 86400000);
45
+ if (diffDays === 0) return "today";
46
+ if (diffDays === 1) return "yesterday";
47
+ if (diffDays < 7) return `${diffDays}d ago`;
48
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
49
+ } catch {
50
+ return String(dateStr);
51
+ }
52
+ }
53
+
54
+ function ProfileCard({ profile }: { profile: LinkedInProfileData }) {
55
+ const name = profile.name ||
56
+ [profile.localizedFirstName || profile.firstName, profile.localizedLastName || profile.lastName]
57
+ .filter(Boolean)
58
+ .join(" ");
59
+
60
+ return (
61
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1">
62
+ <div className="flex items-start gap-2">
63
+ <User className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
64
+ <div className="min-w-0 flex-1">
65
+ <p className="text-sm font-medium text-foreground">{name || "LinkedIn Profile"}</p>
66
+ {profile.headline && (
67
+ <p className="text-xs text-muted-foreground line-clamp-2">{profile.headline}</p>
68
+ )}
69
+ {profile.vanityName && (
70
+ <p className="flex items-center gap-1 text-xs text-muted-foreground">
71
+ <ExternalLink className="h-3 w-3" />
72
+ linkedin.com/in/{profile.vanityName}
73
+ </p>
74
+ )}
75
+ </div>
76
+ </div>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ function PostCard({ post }: { post: LinkedInPostData }) {
82
+ const content = post.text || post.commentary;
83
+ const time = post.created?.time || post.timestamp;
84
+
85
+ return (
86
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
87
+ <div className="flex items-center gap-2">
88
+ <FileText className="h-4 w-4 text-muted-foreground shrink-0" />
89
+ {post.author && (
90
+ <span className="text-xs font-medium text-foreground">{post.author}</span>
91
+ )}
92
+ {time && (
93
+ <span className="flex items-center gap-1 text-xs text-muted-foreground ml-auto">
94
+ <Clock className="h-3 w-3" />
95
+ {formatRelativeDate(time)}
96
+ </span>
97
+ )}
98
+ </div>
99
+ {content && (
100
+ <p className="text-sm text-foreground line-clamp-4">{content}</p>
101
+ )}
102
+ </div>
103
+ );
104
+ }
105
+
106
+ export function LinkedInRenderer({ data }: LinkedInRendererProps) {
107
+ // Profile
108
+ if (isProfile(data)) return <ProfileCard profile={data} />;
109
+
110
+ // {posts: [...]}
111
+ if (typeof data === "object" && data !== null && "posts" in data) {
112
+ const posts = (data as any).posts;
113
+ if (Array.isArray(posts)) {
114
+ return (
115
+ <div className="space-y-1.5">
116
+ {posts.filter(isPost).map((p, i) => <PostCard key={p.id || i} post={p} />)}
117
+ </div>
118
+ );
119
+ }
120
+ }
121
+
122
+ // {elements: [...]}
123
+ if (typeof data === "object" && data !== null && "elements" in data) {
124
+ const elements = (data as any).elements;
125
+ if (Array.isArray(elements)) {
126
+ const posts = elements.filter(isPost);
127
+ if (posts.length > 0) {
128
+ return (
129
+ <div className="space-y-1.5">
130
+ {posts.map((p, i) => <PostCard key={p.id || i} post={p} />)}
131
+ </div>
132
+ );
133
+ }
134
+ }
135
+ }
136
+
137
+ if (isPost(data)) return <PostCard post={data} />;
138
+
139
+ if (Array.isArray(data)) {
140
+ const posts = data.filter(isPost);
141
+ if (posts.length > 0) {
142
+ return <div className="space-y-1.5">{posts.map((p, i) => <PostCard key={p.id || i} post={p} />)}</div>;
143
+ }
144
+ }
145
+
146
+ return (
147
+ <pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
148
+ {JSON.stringify(data, null, 2)}
149
+ </pre>
150
+ );
151
+ }
@@ -0,0 +1,223 @@
1
+ import React from "react";
2
+ import { FileText, Database, ExternalLink, Clock } from "lucide-react";
3
+
4
+ interface NotionPageData {
5
+ id?: string;
6
+ url?: string;
7
+ last_edited_time?: string;
8
+ lastEditedTime?: string;
9
+ properties?: Record<string, unknown>;
10
+ title?: string;
11
+ }
12
+
13
+ interface NotionDatabaseData {
14
+ id?: string;
15
+ title?: string | Array<{ plain_text?: string }>;
16
+ description?: string | Array<{ plain_text?: string }>;
17
+ url?: string;
18
+ lastEditedTime?: string;
19
+ }
20
+
21
+ interface NotionRendererProps {
22
+ data: unknown;
23
+ }
24
+
25
+ function isNotionPage(data: unknown): data is NotionPageData {
26
+ if (typeof data !== "object" || data === null) return false;
27
+ // Raw Notion shape with properties
28
+ if ("properties" in data) return true;
29
+ // Notion URL-based detection
30
+ if ("id" in data && "url" in data && String((data as any).url || "").includes("notion")) return true;
31
+ // Normalized Supyagent shape: has title + id + parentType (distinguishes from database)
32
+ if ("title" in data && "id" in data && "parentType" in data) return true;
33
+ return false;
34
+ }
35
+
36
+ function isNotionDatabase(data: unknown): data is NotionDatabaseData {
37
+ if (typeof data !== "object" || data === null) return false;
38
+ // Raw Notion shape: title is an array
39
+ if ("title" in data && Array.isArray((data as any).title) && "id" in data) return true;
40
+ // Normalized Supyagent shape: has propertyCount (distinguishes from pages)
41
+ if ("title" in data && "id" in data && "propertyCount" in data) return true;
42
+ return false;
43
+ }
44
+
45
+ function extractPageTitle(page: NotionPageData): string {
46
+ if (page.title) return page.title;
47
+ if (page.properties) {
48
+ for (const val of Object.values(page.properties)) {
49
+ if (typeof val === "object" && val !== null && "title" in val) {
50
+ const titles = (val as any).title;
51
+ if (Array.isArray(titles) && titles.length > 0) {
52
+ return titles.map((t: any) => t.plain_text || t.text?.content || "").join("");
53
+ }
54
+ }
55
+ }
56
+ // Check Name property (common)
57
+ const name = page.properties.Name || page.properties.name;
58
+ if (typeof name === "object" && name !== null && "title" in name) {
59
+ const titles = (name as any).title;
60
+ if (Array.isArray(titles) && titles.length > 0) {
61
+ return titles.map((t: any) => t.plain_text || "").join("");
62
+ }
63
+ }
64
+ }
65
+ return "Untitled";
66
+ }
67
+
68
+ function formatRelativeDate(dateStr: string): string {
69
+ try {
70
+ const date = new Date(dateStr);
71
+ const now = new Date();
72
+ const diffMs = now.getTime() - date.getTime();
73
+ const diffDays = Math.floor(diffMs / 86400000);
74
+ if (diffDays === 0) return "today";
75
+ if (diffDays === 1) return "yesterday";
76
+ if (diffDays < 7) return `${diffDays}d ago`;
77
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
78
+ } catch {
79
+ return dateStr;
80
+ }
81
+ }
82
+
83
+ function PageCard({ page }: { page: NotionPageData }) {
84
+ const title = extractPageTitle(page);
85
+
86
+ return (
87
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1">
88
+ <div className="flex items-start gap-2">
89
+ <FileText className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
90
+ <div className="min-w-0 flex-1">
91
+ <p className="text-sm font-medium text-foreground truncate">
92
+ {page.url ? (
93
+ <a href={page.url} target="_blank" rel="noopener noreferrer" className="hover:underline">
94
+ {title}
95
+ </a>
96
+ ) : (
97
+ title
98
+ )}
99
+ </p>
100
+ </div>
101
+ <div className="flex items-center gap-1.5 shrink-0">
102
+ {(page.last_edited_time || page.lastEditedTime) && (
103
+ <span className="flex items-center gap-1 text-xs text-muted-foreground">
104
+ <Clock className="h-3 w-3" />
105
+ {formatRelativeDate((page.last_edited_time || page.lastEditedTime)!)}
106
+ </span>
107
+ )}
108
+ {page.url && (
109
+ <a href={page.url} target="_blank" rel="noopener noreferrer" className="text-muted-foreground hover:text-foreground">
110
+ <ExternalLink className="h-3 w-3" />
111
+ </a>
112
+ )}
113
+ </div>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ function DatabaseCard({ db }: { db: NotionDatabaseData }) {
120
+ const title = typeof db.title === "string"
121
+ ? db.title
122
+ : Array.isArray(db.title) && db.title.length > 0
123
+ ? db.title.map((t) => t.plain_text || "").join("")
124
+ : "Untitled database";
125
+ const desc = typeof db.description === "string"
126
+ ? db.description
127
+ : Array.isArray(db.description) && db.description.length > 0
128
+ ? db.description.map((d) => d.plain_text || "").join("")
129
+ : null;
130
+
131
+ return (
132
+ <div className="rounded-lg border border-border bg-card p-3 space-y-1">
133
+ <div className="flex items-start gap-2">
134
+ <Database className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
135
+ <div className="min-w-0 flex-1">
136
+ <p className="text-sm font-medium text-foreground truncate">
137
+ {db.url ? (
138
+ <a href={db.url} target="_blank" rel="noopener noreferrer" className="hover:underline">
139
+ {title}
140
+ </a>
141
+ ) : (
142
+ title
143
+ )}
144
+ </p>
145
+ {desc && <p className="text-xs text-muted-foreground line-clamp-2">{desc}</p>}
146
+ </div>
147
+ </div>
148
+ </div>
149
+ );
150
+ }
151
+
152
+ export function NotionRenderer({ data }: NotionRendererProps) {
153
+ // Pages array
154
+ if (typeof data === "object" && data !== null && "pages" in data) {
155
+ const pages = (data as any).pages;
156
+ if (Array.isArray(pages)) {
157
+ return (
158
+ <div className="space-y-1.5">
159
+ {pages.filter(isNotionPage).map((p, i) => <PageCard key={p.id || i} page={p} />)}
160
+ </div>
161
+ );
162
+ }
163
+ }
164
+
165
+ // Databases array
166
+ if (typeof data === "object" && data !== null && "databases" in data) {
167
+ const dbs = (data as any).databases;
168
+ if (Array.isArray(dbs)) {
169
+ return (
170
+ <div className="space-y-1.5">
171
+ {dbs.filter(isNotionDatabase).map((d, i) => <DatabaseCard key={d.id || i} db={d} />)}
172
+ </div>
173
+ );
174
+ }
175
+ }
176
+
177
+ // Results array (query response)
178
+ if (typeof data === "object" && data !== null && "results" in data) {
179
+ const results = (data as any).results;
180
+ if (Array.isArray(results)) {
181
+ const pages = results.filter(isNotionPage);
182
+ if (pages.length > 0) {
183
+ return (
184
+ <div className="space-y-1.5">
185
+ {pages.map((p, i) => <PageCard key={p.id || i} page={p} />)}
186
+ </div>
187
+ );
188
+ }
189
+ const dbs = results.filter(isNotionDatabase);
190
+ if (dbs.length > 0) {
191
+ return (
192
+ <div className="space-y-1.5">
193
+ {dbs.map((d, i) => <DatabaseCard key={d.id || i} db={d} />)}
194
+ </div>
195
+ );
196
+ }
197
+ }
198
+ }
199
+
200
+ // Single page
201
+ if (isNotionPage(data)) return <PageCard page={data} />;
202
+
203
+ // Single database
204
+ if (isNotionDatabase(data)) return <DatabaseCard db={data} />;
205
+
206
+ // Array
207
+ if (Array.isArray(data)) {
208
+ const pages = data.filter(isNotionPage);
209
+ if (pages.length > 0) {
210
+ return (
211
+ <div className="space-y-1.5">
212
+ {pages.map((p, i) => <PageCard key={p.id || i} page={p} />)}
213
+ </div>
214
+ );
215
+ }
216
+ }
217
+
218
+ return (
219
+ <pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
220
+ {JSON.stringify(data, null, 2)}
221
+ </pre>
222
+ );
223
+ }