@townco/gui-template 0.1.0
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/README.md +176 -0
- package/dist/assets/acp-sdk-DjnJtAf8.js +1 -0
- package/dist/assets/icons-B1wHwGRq.js +1 -0
- package/dist/assets/index-B6Vo9iGc.css +1 -0
- package/dist/assets/index-C9pfdbke.js +7 -0
- package/dist/assets/markdown-BLVKlqjB.js +1 -0
- package/dist/assets/radix-zBfH0NEp.js +5 -0
- package/dist/assets/react-BUwpDnu9.js +49 -0
- package/dist/assets/vendor-CNJ4Zu8U.js +48 -0
- package/dist/index.html +19 -0
- package/index.html +12 -0
- package/package.json +42 -0
- package/postcss.config.js +5 -0
- package/src/App.tsx +54 -0
- package/src/ChatView.tsx +173 -0
- package/src/config.ts +14 -0
- package/src/env.d.ts +9 -0
- package/src/main.tsx +13 -0
- package/vite.config.ts +94 -0
package/dist/index.html
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Agent Chat</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-C9pfdbke.js"></script>
|
|
8
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-CNJ4Zu8U.js">
|
|
9
|
+
<link rel="modulepreload" crossorigin href="/assets/react-BUwpDnu9.js">
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/acp-sdk-DjnJtAf8.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/radix-zBfH0NEp.js">
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-B1wHwGRq.js">
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/assets/markdown-BLVKlqjB.js">
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B6Vo9iGc.css">
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div id="root"></div>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
package/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Agent Chat</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@townco/gui-template",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"src",
|
|
8
|
+
"index.html",
|
|
9
|
+
"vite.config.ts",
|
|
10
|
+
"postcss.config.js",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/federicoweber/agent_hub.git"
|
|
16
|
+
},
|
|
17
|
+
"author": "Federico Weber",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"preview": "vite preview",
|
|
22
|
+
"check": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@townco/ui": "^0.1.0",
|
|
26
|
+
"lucide-react": "^0.552.0",
|
|
27
|
+
"react": "^19.2.0",
|
|
28
|
+
"react-dom": "^19.2.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@tailwindcss/postcss": "^4.1.17",
|
|
32
|
+
"@townco/tsconfig": "^0.1.0",
|
|
33
|
+
"@types/react": "^19.2.2",
|
|
34
|
+
"@types/react-dom": "^19.2.2",
|
|
35
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
36
|
+
"autoprefixer": "^10.4.21",
|
|
37
|
+
"postcss": "^8.5.6",
|
|
38
|
+
"tailwindcss": "^4.1.17",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"vite": "^7.2.1"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AcpClient } from "@townco/ui";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { ChatView } from "./ChatView.js";
|
|
4
|
+
import { config } from "./config.js";
|
|
5
|
+
|
|
6
|
+
function App() {
|
|
7
|
+
const [client, setClient] = useState<AcpClient | null>(null);
|
|
8
|
+
const [error, setError] = useState<string | null>(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
// Create AcpClient with HTTP transport
|
|
12
|
+
try {
|
|
13
|
+
const acpClient = new AcpClient({
|
|
14
|
+
type: "http",
|
|
15
|
+
options: {
|
|
16
|
+
baseUrl: config.agentServerUrl,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
setClient(acpClient);
|
|
21
|
+
|
|
22
|
+
// Clean up on unmount
|
|
23
|
+
return () => {
|
|
24
|
+
acpClient.disconnect().catch(console.error);
|
|
25
|
+
};
|
|
26
|
+
} catch (err) {
|
|
27
|
+
const errorMessage =
|
|
28
|
+
err instanceof Error ? err.message : "Failed to initialize ACP client";
|
|
29
|
+
setError(errorMessage);
|
|
30
|
+
console.error("Failed to initialize ACP client:", err);
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
if (error) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex items-center justify-center h-screen bg-[var(--color-bg)]">
|
|
38
|
+
<div className="text-center p-8 max-w-md">
|
|
39
|
+
<h1 className="text-2xl font-bold text-red-500 mb-4">
|
|
40
|
+
Initialization Error
|
|
41
|
+
</h1>
|
|
42
|
+
<p className="text-[var(--color-text)] mb-4">{error}</p>
|
|
43
|
+
<p className="text-sm text-[var(--color-text-secondary)]">
|
|
44
|
+
Failed to initialize the ACP client. Check the console for details.
|
|
45
|
+
</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return <ChatView client={client} />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default App;
|
package/src/ChatView.tsx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AcpClient,
|
|
3
|
+
useChatMessages,
|
|
4
|
+
useChatSession,
|
|
5
|
+
useChatStore,
|
|
6
|
+
} from "@townco/ui";
|
|
7
|
+
import {
|
|
8
|
+
ChatInputField,
|
|
9
|
+
ChatInputRoot,
|
|
10
|
+
ChatInputSubmit,
|
|
11
|
+
ChatInputToolbar,
|
|
12
|
+
ChatSecondaryPanel,
|
|
13
|
+
cn,
|
|
14
|
+
HeightTransition,
|
|
15
|
+
Message,
|
|
16
|
+
MessageContent,
|
|
17
|
+
type TodoItem,
|
|
18
|
+
} from "@townco/ui/gui";
|
|
19
|
+
import { ChevronDown, SendIcon } from "lucide-react";
|
|
20
|
+
import { useEffect, useRef, useState } from "react";
|
|
21
|
+
|
|
22
|
+
export interface ChatViewProps {
|
|
23
|
+
client: AcpClient | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ChatView({ client }: ChatViewProps) {
|
|
27
|
+
// Use shared hooks from @townco/ui/core
|
|
28
|
+
const { connectionStatus, connect } = useChatSession(client);
|
|
29
|
+
const { messages } = useChatMessages(client);
|
|
30
|
+
const error = useChatStore((state) => state.error);
|
|
31
|
+
|
|
32
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
const [isHeaderExpanded, setIsHeaderExpanded] = useState(false);
|
|
34
|
+
const [todos] = useState<TodoItem[]>([]);
|
|
35
|
+
|
|
36
|
+
// Auto-scroll to bottom when new messages arrive
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const getStatusColor = () => {
|
|
42
|
+
switch (connectionStatus) {
|
|
43
|
+
case "connected":
|
|
44
|
+
return "bg-green-500";
|
|
45
|
+
case "connecting":
|
|
46
|
+
return "bg-yellow-500";
|
|
47
|
+
case "error":
|
|
48
|
+
return "bg-red-500";
|
|
49
|
+
default:
|
|
50
|
+
return "bg-gray-500";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const getStatusText = () => {
|
|
55
|
+
switch (connectionStatus) {
|
|
56
|
+
case "connected":
|
|
57
|
+
return "Connected";
|
|
58
|
+
case "connecting":
|
|
59
|
+
return "Connecting...";
|
|
60
|
+
case "error":
|
|
61
|
+
return "Connection Error";
|
|
62
|
+
default:
|
|
63
|
+
return "No Server";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex flex-col h-screen bg-background text-foreground">
|
|
69
|
+
{/* Header */}
|
|
70
|
+
<div className="relative border-b border-border bg-card z-10">
|
|
71
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
72
|
+
<h1 className="text-xl font-semibold m-0">Agent Chat</h1>
|
|
73
|
+
<div className="flex items-center gap-3">
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<div className={cn("w-2 h-2 rounded-full", getStatusColor())} />
|
|
76
|
+
<span className="text-sm text-muted-foreground">
|
|
77
|
+
{getStatusText()}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={() => setIsHeaderExpanded(!isHeaderExpanded)}
|
|
83
|
+
className="p-1 rounded hover:bg-background transition-colors"
|
|
84
|
+
aria-label={
|
|
85
|
+
isHeaderExpanded ? "Collapse header" : "Expand header"
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
<ChevronDown
|
|
89
|
+
className={cn(
|
|
90
|
+
"w-5 h-5 text-foreground transition-transform duration-200",
|
|
91
|
+
isHeaderExpanded && "rotate-180",
|
|
92
|
+
)}
|
|
93
|
+
/>
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="absolute top-full left-0 right-0 z-20">
|
|
99
|
+
<HeightTransition>
|
|
100
|
+
{isHeaderExpanded && (
|
|
101
|
+
<div className="bg-card border-b border-border px-6 py-4 shadow-lg">
|
|
102
|
+
<ChatSecondaryPanel todos={todos} />
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</HeightTransition>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Connection Error Banner */}
|
|
110
|
+
{connectionStatus === "error" && error && (
|
|
111
|
+
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-4">
|
|
112
|
+
<div className="flex items-start justify-between gap-4">
|
|
113
|
+
<div className="flex-1">
|
|
114
|
+
<h3 className="text-sm font-semibold text-red-500 mb-1">
|
|
115
|
+
Connection Error
|
|
116
|
+
</h3>
|
|
117
|
+
<p className="text-sm text-foreground whitespace-pre-line">
|
|
118
|
+
{error}
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={connect}
|
|
124
|
+
className="px-4 py-2 text-sm rounded-lg bg-red-500 text-white font-medium hover:bg-red-600 transition-colors"
|
|
125
|
+
>
|
|
126
|
+
Retry
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{/* Messages */}
|
|
133
|
+
<div className="flex-1 overflow-y-auto py-4">
|
|
134
|
+
{messages.length === 0 ? (
|
|
135
|
+
<div className="flex items-center justify-center h-full">
|
|
136
|
+
<div className="text-center text-muted-foreground">
|
|
137
|
+
<p className="text-lg mb-2">No messages yet</p>
|
|
138
|
+
<p className="text-sm">
|
|
139
|
+
{connectionStatus === "connected"
|
|
140
|
+
? "Start a conversation with the agent"
|
|
141
|
+
: "Type a message to test the UI (no server connected)"}
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
) : (
|
|
146
|
+
<div className="flex flex-col gap-4 px-4">
|
|
147
|
+
{messages.map((message) => (
|
|
148
|
+
<Message key={message.id} message={message}>
|
|
149
|
+
<MessageContent
|
|
150
|
+
message={message}
|
|
151
|
+
thinkingDisplayStyle="collapsible"
|
|
152
|
+
/>
|
|
153
|
+
</Message>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
<div ref={messagesEndRef} />
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Input area */}
|
|
161
|
+
<div className="p-4 bg-background">
|
|
162
|
+
<ChatInputRoot client={client}>
|
|
163
|
+
<ChatInputField placeholder="Type a message..." />
|
|
164
|
+
<ChatInputToolbar className="justify-end">
|
|
165
|
+
<ChatInputSubmit>
|
|
166
|
+
<SendIcon className="size-4" />
|
|
167
|
+
</ChatInputSubmit>
|
|
168
|
+
</ChatInputToolbar>
|
|
169
|
+
</ChatInputRoot>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the agent server URL from environment variables
|
|
3
|
+
* Falls back to localhost:3000 if not set
|
|
4
|
+
*/
|
|
5
|
+
export function getAgentServerUrl(): string {
|
|
6
|
+
return import.meta.env.VITE_AGENT_URL || "http://localhost:3000";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Application configuration
|
|
11
|
+
*/
|
|
12
|
+
export const config = {
|
|
13
|
+
agentServerUrl: getAgentServerUrl(),
|
|
14
|
+
} as const;
|
package/src/env.d.ts
ADDED
package/src/main.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import App from "./App.js";
|
|
4
|
+
import "@townco/ui/styles/global.css";
|
|
5
|
+
|
|
6
|
+
const rootElement = document.getElementById("root");
|
|
7
|
+
if (!rootElement) throw new Error("Root element not found");
|
|
8
|
+
|
|
9
|
+
ReactDOM.createRoot(rootElement).render(
|
|
10
|
+
<React.StrictMode>
|
|
11
|
+
<App />
|
|
12
|
+
</React.StrictMode>,
|
|
13
|
+
);
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import { defineConfig } from "vite";
|
|
4
|
+
|
|
5
|
+
// Create a stub module for stdio transport to prevent Node.js dependencies
|
|
6
|
+
const stdioStubPlugin = () => ({
|
|
7
|
+
name: "stdio-stub",
|
|
8
|
+
resolveId(id: string) {
|
|
9
|
+
// Intercept stdio transport imports and replace with stub
|
|
10
|
+
if (
|
|
11
|
+
id.includes("sdk/transports/stdio") ||
|
|
12
|
+
id.includes("transports/stdio.js")
|
|
13
|
+
) {
|
|
14
|
+
return id;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
},
|
|
18
|
+
load(id: string) {
|
|
19
|
+
if (
|
|
20
|
+
id.includes("sdk/transports/stdio") ||
|
|
21
|
+
id.includes("transports/stdio.js")
|
|
22
|
+
) {
|
|
23
|
+
// Return a stub implementation that throws an error if used
|
|
24
|
+
return `
|
|
25
|
+
export class StdioTransport {
|
|
26
|
+
constructor() {
|
|
27
|
+
throw new Error("StdioTransport is not available in the browser. Use HttpTransport or WebSocketTransport instead.");
|
|
28
|
+
}
|
|
29
|
+
async connect() { throw new Error("StdioTransport not available in browser"); }
|
|
30
|
+
async disconnect() {}
|
|
31
|
+
async send() { throw new Error("StdioTransport not available in browser"); }
|
|
32
|
+
async *receive() { throw new Error("StdioTransport not available in browser"); }
|
|
33
|
+
isConnected() { return false; }
|
|
34
|
+
onSessionUpdate() { return () => {}; }
|
|
35
|
+
onError() { return () => {}; }
|
|
36
|
+
}
|
|
37
|
+
export const StdioTransportOptions = {};
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// https://vitejs.dev/config/
|
|
45
|
+
export default defineConfig({
|
|
46
|
+
plugins: [react(), stdioStubPlugin()],
|
|
47
|
+
resolve: {
|
|
48
|
+
alias: {
|
|
49
|
+
"@": path.resolve(__dirname, "./src"),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
optimizeDeps: {
|
|
53
|
+
exclude: [],
|
|
54
|
+
},
|
|
55
|
+
build: {
|
|
56
|
+
rollupOptions: {
|
|
57
|
+
output: {
|
|
58
|
+
manualChunks: (id) => {
|
|
59
|
+
// Split React and ReactDOM into their own chunk
|
|
60
|
+
if (
|
|
61
|
+
id.includes("node_modules/react") ||
|
|
62
|
+
id.includes("node_modules/react-dom")
|
|
63
|
+
) {
|
|
64
|
+
return "react";
|
|
65
|
+
}
|
|
66
|
+
// Split Radix UI components into their own chunk
|
|
67
|
+
if (id.includes("node_modules/@radix-ui")) {
|
|
68
|
+
return "radix";
|
|
69
|
+
}
|
|
70
|
+
// Split markdown rendering into its own chunk
|
|
71
|
+
if (
|
|
72
|
+
id.includes("node_modules/react-markdown") ||
|
|
73
|
+
id.includes("node_modules/remark-gfm")
|
|
74
|
+
) {
|
|
75
|
+
return "markdown";
|
|
76
|
+
}
|
|
77
|
+
// Split lucide icons into their own chunk
|
|
78
|
+
if (id.includes("node_modules/lucide-react")) {
|
|
79
|
+
return "icons";
|
|
80
|
+
}
|
|
81
|
+
// Split ACP SDK into its own chunk
|
|
82
|
+
if (id.includes("node_modules/@agentclientprotocol")) {
|
|
83
|
+
return "acp-sdk";
|
|
84
|
+
}
|
|
85
|
+
// Split other node_modules into a vendor chunk
|
|
86
|
+
if (id.includes("node_modules")) {
|
|
87
|
+
return "vendor";
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
chunkSizeWarningLimit: 600, // Slightly increase limit to avoid false positives
|
|
93
|
+
},
|
|
94
|
+
});
|