create-supyagent-app 0.1.26 → 0.1.28
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 +7 -7
- package/templates/base/src/components/supyagent/tool-message.tsx +3 -1
- package/templates/base/src/components/supyagent/tool-renderers.tsx +4 -0
- package/templates/base/src/components/supyagent/tools/browser.tsx +134 -0
- package/templates/base/src/components/supyagent/tools/whatsapp.tsx +120 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-supyagent-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "Create a supyagent-powered chatbot app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"templates"
|
|
12
12
|
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"clean": "rm -rf dist"
|
|
16
|
+
},
|
|
13
17
|
"dependencies": {
|
|
14
18
|
"@clack/prompts": "^0.8.0",
|
|
15
19
|
"nypm": "^0.4.0",
|
|
@@ -19,9 +23,5 @@
|
|
|
19
23
|
"tsup": "^8.3.0",
|
|
20
24
|
"typescript": "^5.7.0"
|
|
21
25
|
},
|
|
22
|
-
"license": "MIT"
|
|
23
|
-
|
|
24
|
-
"build": "tsup",
|
|
25
|
-
"clean": "rm -rf dist"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
26
|
+
"license": "MIT"
|
|
27
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
humanizeToolName,
|
|
21
21
|
getProviderLabel,
|
|
22
22
|
getProviderFromToolName,
|
|
23
|
+
resolveToolName,
|
|
23
24
|
ToolInput,
|
|
24
25
|
} from "@supyagent/sdk/react";
|
|
25
26
|
import { getToolRenderer } from "./tool-renderers";
|
|
@@ -30,9 +31,10 @@ interface ToolMessageProps {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function ToolMessage({ part, addToolApprovalResponse }: ToolMessageProps) {
|
|
33
|
-
const
|
|
34
|
+
const rawToolName = extractToolName(part);
|
|
34
35
|
const state = extractState(part);
|
|
35
36
|
const args = extractArgs(part);
|
|
37
|
+
const toolName = resolveToolName(rawToolName, args);
|
|
36
38
|
|
|
37
39
|
const toolState = resolveState(state);
|
|
38
40
|
const isDone = toolState === "output-available";
|
|
@@ -29,6 +29,8 @@ import { BashRenderer } from "./tools/bash";
|
|
|
29
29
|
import { ImageRenderer } from "./tools/image";
|
|
30
30
|
import { AudioRenderer } from "./tools/audio";
|
|
31
31
|
import { VideoRenderer } from "./tools/video";
|
|
32
|
+
import { WhatsAppRenderer } from "./tools/whatsapp";
|
|
33
|
+
import { BrowserRenderer } from "./tools/browser";
|
|
32
34
|
import { GenericRenderer } from "./tools/generic";
|
|
33
35
|
|
|
34
36
|
export interface ToolRendererProps {
|
|
@@ -70,6 +72,8 @@ const renderers: Record<string, ComponentType<ToolRendererProps>> = {
|
|
|
70
72
|
image: ImageRenderer,
|
|
71
73
|
audio: AudioRenderer,
|
|
72
74
|
video: VideoRenderer,
|
|
75
|
+
whatsapp: WhatsAppRenderer,
|
|
76
|
+
browser: BrowserRenderer,
|
|
73
77
|
};
|
|
74
78
|
|
|
75
79
|
export function getToolRenderer(formatterType: string): ComponentType<ToolRendererProps> {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Globe, ExternalLink, FileText, Image } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface BrowserData {
|
|
5
|
+
url?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
content?: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
markdown?: string;
|
|
10
|
+
screenshot_url?: string;
|
|
11
|
+
screenshot?: string;
|
|
12
|
+
status?: number | string;
|
|
13
|
+
links?: Array<{ text?: string; href?: string }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface BrowserRendererProps {
|
|
17
|
+
data: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isBrowserData(data: unknown): data is BrowserData {
|
|
21
|
+
if (typeof data !== "object" || data === null) return false;
|
|
22
|
+
return "url" in data || "content" in data || "text" in data || "markdown" in data || "screenshot_url" in data || "screenshot" in data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getDomain(url: string): string {
|
|
26
|
+
try {
|
|
27
|
+
return new URL(url).hostname.replace("www.", "");
|
|
28
|
+
} catch {
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function BrowserRenderer({ data }: BrowserRendererProps) {
|
|
34
|
+
if (!isBrowserData(data)) {
|
|
35
|
+
return (
|
|
36
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
|
|
37
|
+
{JSON.stringify(data, null, 2)}
|
|
38
|
+
</pre>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const content = data.content || data.text || data.markdown || "";
|
|
43
|
+
const screenshotUrl = data.screenshot_url || data.screenshot;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-2">
|
|
47
|
+
{/* URL Header */}
|
|
48
|
+
{data.url && (
|
|
49
|
+
<div className="rounded-lg border border-border bg-card overflow-hidden">
|
|
50
|
+
<div className="flex items-center gap-2 px-3 py-2">
|
|
51
|
+
<Globe className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
52
|
+
<div className="min-w-0 flex-1">
|
|
53
|
+
{data.title && (
|
|
54
|
+
<p className="text-sm font-medium text-foreground truncate">{data.title}</p>
|
|
55
|
+
)}
|
|
56
|
+
<p className="text-xs text-muted-foreground truncate">{getDomain(data.url)}</p>
|
|
57
|
+
</div>
|
|
58
|
+
{data.status && (
|
|
59
|
+
<span className={`inline-flex rounded-full px-2 py-0.5 text-xs ${
|
|
60
|
+
Number(data.status) >= 200 && Number(data.status) < 300
|
|
61
|
+
? "text-green-500 bg-green-500/10"
|
|
62
|
+
: Number(data.status) >= 400
|
|
63
|
+
? "text-destructive bg-destructive/10"
|
|
64
|
+
: "text-muted-foreground bg-muted"
|
|
65
|
+
}`}>
|
|
66
|
+
{data.status}
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
<a href={data.url} target="_blank" rel="noopener noreferrer" className="shrink-0 text-muted-foreground hover:text-foreground">
|
|
70
|
+
<ExternalLink className="h-3.5 w-3.5" />
|
|
71
|
+
</a>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{/* Screenshot */}
|
|
77
|
+
{screenshotUrl && (
|
|
78
|
+
<div className="rounded-lg border border-border bg-background overflow-hidden">
|
|
79
|
+
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-muted border-b border-border">
|
|
80
|
+
<Image className="h-3.5 w-3.5 text-muted-foreground" />
|
|
81
|
+
<span className="text-xs text-muted-foreground">Screenshot</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="p-2">
|
|
84
|
+
<img
|
|
85
|
+
src={screenshotUrl}
|
|
86
|
+
alt={data.title || "Page screenshot"}
|
|
87
|
+
className="w-full rounded border border-border"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Extracted content */}
|
|
94
|
+
{content && (
|
|
95
|
+
<div className="rounded-lg border border-border bg-background overflow-hidden">
|
|
96
|
+
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-muted border-b border-border">
|
|
97
|
+
<FileText className="h-3.5 w-3.5 text-muted-foreground" />
|
|
98
|
+
<span className="text-xs text-muted-foreground">Extracted content</span>
|
|
99
|
+
</div>
|
|
100
|
+
<pre className="p-3 text-xs text-foreground overflow-x-auto max-h-80 overflow-y-auto font-mono whitespace-pre-wrap">
|
|
101
|
+
{content}
|
|
102
|
+
</pre>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{/* Links */}
|
|
107
|
+
{data.links && data.links.length > 0 && (
|
|
108
|
+
<div className="rounded-lg border border-border bg-card p-3 space-y-1">
|
|
109
|
+
<p className="text-xs font-medium text-muted-foreground mb-1.5">Links ({data.links.length})</p>
|
|
110
|
+
<div className="space-y-1 max-h-40 overflow-y-auto">
|
|
111
|
+
{data.links.slice(0, 20).map((link, i) => (
|
|
112
|
+
<div key={i} className="flex items-center gap-1.5">
|
|
113
|
+
<ExternalLink className="h-3 w-3 text-muted-foreground shrink-0" />
|
|
114
|
+
{link.href ? (
|
|
115
|
+
<a href={link.href} target="_blank" rel="noopener noreferrer" className="text-xs text-primary hover:underline truncate">
|
|
116
|
+
{link.text || link.href}
|
|
117
|
+
</a>
|
|
118
|
+
) : (
|
|
119
|
+
<span className="text-xs text-foreground truncate">{link.text}</span>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{!data.url && !content && !screenshotUrl && (
|
|
128
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
|
|
129
|
+
{JSON.stringify(data, null, 2)}
|
|
130
|
+
</pre>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { MessageCircle, User, Clock } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface WhatsAppMessageData {
|
|
5
|
+
id?: string;
|
|
6
|
+
from?: string;
|
|
7
|
+
to?: string;
|
|
8
|
+
body?: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
timestamp?: number | string;
|
|
11
|
+
type?: string;
|
|
12
|
+
status?: string;
|
|
13
|
+
contact_name?: string;
|
|
14
|
+
profile_name?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface WhatsAppRendererProps {
|
|
18
|
+
data: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isWhatsAppMessage(data: unknown): data is WhatsAppMessageData {
|
|
22
|
+
return typeof data === "object" && data !== null && ("body" in data || "text" in data || "from" in data);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatTimestamp(ts: number | string): string {
|
|
26
|
+
try {
|
|
27
|
+
const date = typeof ts === "number"
|
|
28
|
+
? new Date(ts > 1e12 ? ts : ts * 1000)
|
|
29
|
+
: new Date(ts);
|
|
30
|
+
const now = new Date();
|
|
31
|
+
const diffMs = now.getTime() - date.getTime();
|
|
32
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
33
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
34
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
35
|
+
if (diffMins < 1) return "Just now";
|
|
36
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
37
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
38
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
39
|
+
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
40
|
+
} catch {
|
|
41
|
+
return String(ts);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function MessageCard({ message }: { message: WhatsAppMessageData }) {
|
|
46
|
+
const sender = message.profile_name || message.contact_name || message.from;
|
|
47
|
+
const body = message.body || message.text;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="rounded-lg border border-border bg-card p-3 space-y-1.5">
|
|
51
|
+
<div className="flex items-center gap-2">
|
|
52
|
+
<MessageCircle className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
53
|
+
{sender && (
|
|
54
|
+
<span className="flex items-center gap-1 text-xs font-medium text-foreground">
|
|
55
|
+
<User className="h-3 w-3" />
|
|
56
|
+
{sender}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
{message.type && message.type !== "text" && (
|
|
60
|
+
<span className="inline-flex rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
|
|
61
|
+
{message.type}
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
{message.status && (
|
|
65
|
+
<span className={`inline-flex rounded-full px-2 py-0.5 text-xs ${
|
|
66
|
+
message.status === "delivered" || message.status === "read"
|
|
67
|
+
? "text-green-500 bg-green-500/10"
|
|
68
|
+
: "text-muted-foreground bg-muted"
|
|
69
|
+
}`}>
|
|
70
|
+
{message.status}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
{message.timestamp && (
|
|
74
|
+
<span className="flex items-center gap-1 text-xs text-muted-foreground ml-auto">
|
|
75
|
+
<Clock className="h-3 w-3" />
|
|
76
|
+
{formatTimestamp(message.timestamp)}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
{body && (
|
|
81
|
+
<p className="text-sm text-foreground line-clamp-3">{body}</p>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function WhatsAppRenderer({ data }: WhatsAppRendererProps) {
|
|
88
|
+
// {messages: [...]}
|
|
89
|
+
if (typeof data === "object" && data !== null && "messages" in data) {
|
|
90
|
+
const msgs = (data as any).messages;
|
|
91
|
+
if (Array.isArray(msgs)) {
|
|
92
|
+
return (
|
|
93
|
+
<div className="space-y-2">
|
|
94
|
+
{msgs.filter(isWhatsAppMessage).map((m, i) => <MessageCard key={m.id || i} message={m} />)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Single message
|
|
101
|
+
if (isWhatsAppMessage(data)) return <MessageCard message={data} />;
|
|
102
|
+
|
|
103
|
+
// Array of messages
|
|
104
|
+
if (Array.isArray(data)) {
|
|
105
|
+
const msgs = data.filter(isWhatsAppMessage);
|
|
106
|
+
if (msgs.length > 0) {
|
|
107
|
+
return (
|
|
108
|
+
<div className="space-y-2">
|
|
109
|
+
{msgs.map((m, i) => <MessageCard key={m.id || i} message={m} />)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<pre className="rounded-lg border border-border bg-background p-3 text-xs text-foreground overflow-x-auto max-h-96 overflow-y-auto">
|
|
117
|
+
{JSON.stringify(data, null, 2)}
|
|
118
|
+
</pre>
|
|
119
|
+
);
|
|
120
|
+
}
|