openclaw-liveavatar 1.0.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/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +42 -0
- package/.next/app-path-routes-manifest.json +6 -0
- package/.next/build-manifest.json +33 -0
- package/.next/cache/.previewinfo +1 -0
- package/.next/cache/.rscinfo +1 -0
- package/.next/cache/.tsbuildinfo +1 -0
- package/.next/cache/chrome-devtools-workspace-uuid +1 -0
- package/.next/cache/next-devtools-config.json +1 -0
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/1.pack +0 -0
- package/.next/cache/webpack/client-production/2.pack +0 -0
- package/.next/cache/webpack/client-production/3.pack +0 -0
- package/.next/cache/webpack/client-production/4.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +58 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +61 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +320 -0
- package/.next/routes-manifest.json +53 -0
- package/.next/server/app/_not-found/page.js +5 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +4 -0
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +15 -0
- package/.next/server/app/api/get-avatars/route.js +1 -0
- package/.next/server/app/api/get-avatars/route.js.nft.json +1 -0
- package/.next/server/app/api/get-avatars/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/start-session/route.js +1 -0
- package/.next/server/app/api/start-session/route.js.nft.json +1 -0
- package/.next/server/app/api/start-session/route_client-reference-manifest.js +1 -0
- package/.next/server/app/index.html +4 -0
- package/.next/server/app/index.meta +7 -0
- package/.next/server/app/index.rsc +16 -0
- package/.next/server/app/page.js +9 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +6 -0
- package/.next/server/chunks/361.js +9 -0
- package/.next/server/chunks/611.js +6 -0
- package/.next/server/chunks/873.js +22 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +4 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +19 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +6 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/144d3bae-37bcc55d23f188ee.js +1 -0
- package/.next/static/chunks/255-35bf8c00c5dde345.js +1 -0
- package/.next/static/chunks/336-a66237a0a1db954a.js +1 -0
- package/.next/static/chunks/4bd1b696-c023c6e3521b1417.js +1 -0
- package/.next/static/chunks/app/_not-found/page-dfc6e5d8e6c6203c.js +1 -0
- package/.next/static/chunks/app/api/get-avatars/route-8017e1cff542d5d0.js +1 -0
- package/.next/static/chunks/app/api/start-session/route-8017e1cff542d5d0.js +1 -0
- package/.next/static/chunks/app/layout-ff675313cc8f8fcf.js +1 -0
- package/.next/static/chunks/app/page-9e4b703722bef650.js +1 -0
- package/.next/static/chunks/framework-de98b93a850cfc71.js +1 -0
- package/.next/static/chunks/main-1a0dcce460eb61ce.js +1 -0
- package/.next/static/chunks/main-app-e7f1007edc7ad7e1.js +1 -0
- package/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
- package/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-4a462cecab786e93.js +1 -0
- package/.next/static/css/bfd73afa11897439.css +3 -0
- package/.next/static/v_GdCj8lVweDVhmIhhEcM/_buildManifest.js +1 -0
- package/.next/static/v_GdCj8lVweDVhmIhhEcM/_ssgManifest.js +1 -0
- package/.next/trace +2 -0
- package/.next/types/app/api/get-avatars/route.ts +347 -0
- package/.next/types/app/api/start-session/route.ts +347 -0
- package/.next/types/app/layout.ts +84 -0
- package/.next/types/app/page.ts +84 -0
- package/.next/types/cache-life.d.ts +141 -0
- package/.next/types/package.json +1 -0
- package/.next/types/routes.d.ts +74 -0
- package/.next/types/validator.ts +88 -0
- package/README.md +241 -0
- package/app/api/config.ts +18 -0
- package/app/api/get-avatars/route.ts +117 -0
- package/app/api/start-session/route.ts +95 -0
- package/app/globals.css +3 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +9 -0
- package/bin/cli.js +100 -0
- package/package.json +66 -0
- package/src/components/LiveAvatarSession.tsx +825 -0
- package/src/components/OpenClawDemo.tsx +399 -0
- package/src/gateway/client.ts +522 -0
- package/src/gateway/types.ts +83 -0
- package/src/liveavatar/context.tsx +750 -0
- package/src/liveavatar/index.ts +6 -0
- package/src/liveavatar/types.ts +10 -0
- package/src/liveavatar/useAvatarActions.ts +41 -0
- package/src/liveavatar/useChatHistory.ts +7 -0
- package/src/liveavatar/useSession.ts +37 -0
- package/src/liveavatar/useTextChat.ts +32 -0
- package/src/liveavatar/useVoiceChat.ts +70 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
+
import { LiveAvatarSession } from "./LiveAvatarSession";
|
|
5
|
+
import { getGatewayClient } from "../gateway/client";
|
|
6
|
+
import { GatewayConnectionState } from "../gateway/types";
|
|
7
|
+
|
|
8
|
+
type SessionState = "idle" | "connecting" | "session" | "ended" | "error";
|
|
9
|
+
|
|
10
|
+
interface Avatar {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
preview_url?: string;
|
|
14
|
+
is_expired?: boolean;
|
|
15
|
+
is_custom?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Collapsible section component for avatar groups
|
|
19
|
+
const AvatarSection = ({
|
|
20
|
+
title,
|
|
21
|
+
avatars,
|
|
22
|
+
isExpanded,
|
|
23
|
+
onToggle,
|
|
24
|
+
onSelectAvatar,
|
|
25
|
+
badge,
|
|
26
|
+
showExpiredBadge = true,
|
|
27
|
+
}: {
|
|
28
|
+
title: string;
|
|
29
|
+
avatars: Avatar[];
|
|
30
|
+
isExpanded: boolean;
|
|
31
|
+
onToggle: () => void;
|
|
32
|
+
onSelectAvatar: (id: string) => void;
|
|
33
|
+
badge?: { text: string; color: string };
|
|
34
|
+
showExpiredBadge?: boolean;
|
|
35
|
+
}) => {
|
|
36
|
+
// For custom avatars, count non-expired; for public avatars, all are active
|
|
37
|
+
const expiredCount = avatars.filter(a => a.is_expired === true).length;
|
|
38
|
+
const activeCount = avatars.length - expiredCount;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="w-full">
|
|
42
|
+
<button
|
|
43
|
+
onClick={onToggle}
|
|
44
|
+
className="w-full flex items-center justify-between px-3 py-2 bg-gray-800/50 hover:bg-gray-800/70 rounded-lg transition-colors"
|
|
45
|
+
>
|
|
46
|
+
<div className="flex items-center gap-2">
|
|
47
|
+
<svg
|
|
48
|
+
className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? "rotate-90" : ""}`}
|
|
49
|
+
fill="none"
|
|
50
|
+
stroke="currentColor"
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
>
|
|
53
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
54
|
+
</svg>
|
|
55
|
+
<span className="text-sm font-medium text-white">{title}</span>
|
|
56
|
+
{badge && (
|
|
57
|
+
<span className={`text-[10px] px-1.5 py-0.5 rounded ${badge.color}`}>
|
|
58
|
+
{badge.text}
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
<span className="text-xs text-gray-500">
|
|
63
|
+
{expiredCount > 0 ? `${activeCount} active / ${avatars.length} total` : `${avatars.length} available`}
|
|
64
|
+
</span>
|
|
65
|
+
</button>
|
|
66
|
+
|
|
67
|
+
{isExpanded && avatars.length > 0 && (
|
|
68
|
+
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-3 mt-3 px-1">
|
|
69
|
+
{avatars.map((avatar) => {
|
|
70
|
+
const isExpired = avatar.is_expired === true;
|
|
71
|
+
return (
|
|
72
|
+
<button
|
|
73
|
+
key={avatar.id}
|
|
74
|
+
onClick={() => onSelectAvatar(avatar.id)}
|
|
75
|
+
disabled={isExpired}
|
|
76
|
+
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all group ${
|
|
77
|
+
isExpired
|
|
78
|
+
? "border-red-500/30 opacity-50 cursor-not-allowed"
|
|
79
|
+
: "border-white/10 hover:border-orange-500/50 hover:scale-105"
|
|
80
|
+
}`}
|
|
81
|
+
>
|
|
82
|
+
{avatar.preview_url ? (
|
|
83
|
+
<img
|
|
84
|
+
src={avatar.preview_url}
|
|
85
|
+
alt={avatar.name}
|
|
86
|
+
className="w-full h-full object-cover"
|
|
87
|
+
/>
|
|
88
|
+
) : (
|
|
89
|
+
<div className="w-full h-full bg-gray-700 flex items-center justify-center">
|
|
90
|
+
<span className="text-gray-400 text-xs text-center px-1">
|
|
91
|
+
{avatar.name.slice(0, 10)}
|
|
92
|
+
</span>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-end">
|
|
96
|
+
<div className="w-full p-2 bg-gradient-to-t from-black/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
|
|
97
|
+
<span className="text-white text-xs truncate block">{avatar.name}</span>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
{showExpiredBadge && isExpired && (
|
|
101
|
+
<div className="absolute top-1 left-1 bg-red-500 text-white text-[10px] px-1.5 py-0.5 rounded">
|
|
102
|
+
Expired
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</button>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{isExpanded && avatars.length === 0 && (
|
|
112
|
+
<div className="text-center py-6 text-gray-500 text-sm">
|
|
113
|
+
No {title.toLowerCase()} available
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const OpenClawDemo = () => {
|
|
121
|
+
const [sessionToken, setSessionToken] = useState("");
|
|
122
|
+
const [error, setError] = useState<string | null>(null);
|
|
123
|
+
const [sessionState, setSessionState] = useState<SessionState>("connecting"); // Start connecting immediately
|
|
124
|
+
const [customAvatars, setCustomAvatars] = useState<Avatar[]>([]);
|
|
125
|
+
const [publicAvatars, setPublicAvatars] = useState<Avatar[]>([]);
|
|
126
|
+
const [loadingAvatars, setLoadingAvatars] = useState(true);
|
|
127
|
+
const [gatewayState, setGatewayState] = useState<GatewayConnectionState>("disconnected");
|
|
128
|
+
const [customExpanded, setCustomExpanded] = useState(false); // Start collapsed since likely expired
|
|
129
|
+
const [publicExpanded, setPublicExpanded] = useState(true); // Start expanded
|
|
130
|
+
const hasAutoStartedRef = useRef(false);
|
|
131
|
+
|
|
132
|
+
// Start a session with an optional specific avatar
|
|
133
|
+
const startSessionWithAvatar = useCallback(async (avatarId?: string) => {
|
|
134
|
+
setError(null);
|
|
135
|
+
setSessionState("connecting");
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const sessionRes = await fetch("/api/start-session", {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: { "Content-Type": "application/json" },
|
|
141
|
+
body: JSON.stringify(avatarId ? { avatarId } : {}),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!sessionRes.ok) {
|
|
145
|
+
const errorData = await sessionRes.json();
|
|
146
|
+
throw new Error(errorData.error || "Failed to start session");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { session_token } = await sessionRes.json();
|
|
150
|
+
setSessionToken(session_token);
|
|
151
|
+
setSessionState("session");
|
|
152
|
+
} catch (err: unknown) {
|
|
153
|
+
setError((err as Error).message);
|
|
154
|
+
setSessionState("error");
|
|
155
|
+
}
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
// Fetch avatars and connect to gateway on mount
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
// Fetch available avatars and auto-start with first available
|
|
161
|
+
const fetchAvatarsAndAutoStart = async () => {
|
|
162
|
+
setLoadingAvatars(true);
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch("/api/get-avatars");
|
|
165
|
+
if (res.ok) {
|
|
166
|
+
const data = await res.json();
|
|
167
|
+
const custom: Avatar[] = data.customAvatars || [];
|
|
168
|
+
const publicList: Avatar[] = data.publicAvatars || [];
|
|
169
|
+
setCustomAvatars(custom);
|
|
170
|
+
setPublicAvatars(publicList);
|
|
171
|
+
|
|
172
|
+
// Auto-start with first available avatar (prefer active custom, then public)
|
|
173
|
+
if (!hasAutoStartedRef.current) {
|
|
174
|
+
hasAutoStartedRef.current = true;
|
|
175
|
+
const firstActiveCustom = custom.find(a => !a.is_expired);
|
|
176
|
+
const firstPublic = publicList[0];
|
|
177
|
+
const firstAvatar = firstActiveCustom || firstPublic;
|
|
178
|
+
|
|
179
|
+
if (firstAvatar) {
|
|
180
|
+
console.log("[AutoStart] Starting session with avatar:", firstAvatar.name);
|
|
181
|
+
startSessionWithAvatar(firstAvatar.id);
|
|
182
|
+
} else {
|
|
183
|
+
// No avatars available, go to selection screen
|
|
184
|
+
setSessionState("ended");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
setSessionState("error");
|
|
189
|
+
setError("Failed to fetch avatars");
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.error("Failed to fetch avatars:", err);
|
|
193
|
+
setSessionState("error");
|
|
194
|
+
setError("Failed to connect to avatar service");
|
|
195
|
+
} finally {
|
|
196
|
+
setLoadingAvatars(false);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
fetchAvatarsAndAutoStart();
|
|
200
|
+
|
|
201
|
+
// Connect to OpenClaw gateway
|
|
202
|
+
const gateway = getGatewayClient();
|
|
203
|
+
gateway.onConnectionState(setGatewayState);
|
|
204
|
+
gateway.connect().catch((err) => {
|
|
205
|
+
console.log("[OpenClaw] Gateway not available:", err.message);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return () => {
|
|
209
|
+
gateway.offConnectionState(setGatewayState);
|
|
210
|
+
};
|
|
211
|
+
}, [startSessionWithAvatar]);
|
|
212
|
+
|
|
213
|
+
const onSessionStopped = useCallback(() => {
|
|
214
|
+
setSessionToken("");
|
|
215
|
+
setSessionState("ended");
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
218
|
+
// Connecting screen (shown immediately on load)
|
|
219
|
+
if (sessionState === "connecting") {
|
|
220
|
+
return (
|
|
221
|
+
<div className="w-full h-full flex flex-col items-center justify-center gap-6 p-4">
|
|
222
|
+
<div className="flex flex-col items-center gap-4">
|
|
223
|
+
<div className="relative">
|
|
224
|
+
<div className="w-16 h-16 border-4 border-orange-500/30 rounded-full"></div>
|
|
225
|
+
<div className="absolute top-0 left-0 w-16 h-16 border-4 border-orange-500 rounded-full border-t-transparent animate-spin"></div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div className="text-center">
|
|
229
|
+
<div className="flex items-center justify-center gap-3 mb-4">
|
|
230
|
+
<div className="w-10 h-10 bg-gradient-to-br from-orange-500 to-red-600 rounded-xl flex items-center justify-center">
|
|
231
|
+
<span className="text-xl">🦞</span>
|
|
232
|
+
</div>
|
|
233
|
+
<h1 className="text-2xl font-bold text-white">
|
|
234
|
+
OpenClaw LiveAvatar
|
|
235
|
+
</h1>
|
|
236
|
+
</div>
|
|
237
|
+
<p className="text-gray-400 text-sm">
|
|
238
|
+
Connecting to your AI avatar...
|
|
239
|
+
</p>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Error screen
|
|
247
|
+
if (sessionState === "error") {
|
|
248
|
+
return (
|
|
249
|
+
<div className="w-full h-full flex flex-col items-center justify-center gap-6 p-4">
|
|
250
|
+
<div className="w-full max-w-md flex flex-col items-center gap-6">
|
|
251
|
+
<div className="text-center">
|
|
252
|
+
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
253
|
+
<svg
|
|
254
|
+
className="w-8 h-8 text-red-500"
|
|
255
|
+
fill="none"
|
|
256
|
+
stroke="currentColor"
|
|
257
|
+
viewBox="0 0 24 24"
|
|
258
|
+
>
|
|
259
|
+
<path
|
|
260
|
+
strokeLinecap="round"
|
|
261
|
+
strokeLinejoin="round"
|
|
262
|
+
strokeWidth={2}
|
|
263
|
+
d="M6 18L18 6M6 6l12 12"
|
|
264
|
+
/>
|
|
265
|
+
</svg>
|
|
266
|
+
</div>
|
|
267
|
+
<h1 className="text-2xl font-bold text-white mb-2">
|
|
268
|
+
Connection Failed
|
|
269
|
+
</h1>
|
|
270
|
+
<p className="text-gray-400 mb-4">
|
|
271
|
+
Could not connect to the LiveAvatar service.
|
|
272
|
+
</p>
|
|
273
|
+
{error && (
|
|
274
|
+
<div className="text-red-400 bg-red-900/30 px-4 py-3 rounded-lg text-sm mb-4">
|
|
275
|
+
{error}
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<button
|
|
281
|
+
onClick={() => setSessionState("idle")}
|
|
282
|
+
className="px-6 py-2 bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white rounded-lg transition-all"
|
|
283
|
+
>
|
|
284
|
+
Back to Avatar Selection
|
|
285
|
+
</button>
|
|
286
|
+
|
|
287
|
+
<p className="text-xs text-gray-500 text-center">
|
|
288
|
+
Make sure your LIVEAVATAR_API_KEY is configured in .env.local
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Avatar selection screen (only shown after session ends)
|
|
296
|
+
if (sessionState === "ended") {
|
|
297
|
+
return (
|
|
298
|
+
<div className="w-full h-full flex flex-col items-center justify-center gap-6 p-4">
|
|
299
|
+
<div className="w-full max-w-2xl flex flex-col items-center gap-6">
|
|
300
|
+
{/* Header */}
|
|
301
|
+
<div className="text-center">
|
|
302
|
+
<div className="flex items-center justify-center gap-3 mb-4">
|
|
303
|
+
<div className="w-10 h-10 bg-gradient-to-br from-orange-500 to-red-600 rounded-xl flex items-center justify-center">
|
|
304
|
+
<span className="text-xl">🦞</span>
|
|
305
|
+
</div>
|
|
306
|
+
<h1 className="text-2xl font-bold text-white">
|
|
307
|
+
OpenClaw LiveAvatar
|
|
308
|
+
</h1>
|
|
309
|
+
</div>
|
|
310
|
+
<p className="text-gray-400 mb-2">Session ended. Select an avatar to start a new conversation.</p>
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{/* OpenClaw Connection Status */}
|
|
314
|
+
<div className="flex items-center gap-2 px-4 py-2 bg-gray-800/50 rounded-lg">
|
|
315
|
+
<div className={`w-2 h-2 rounded-full ${
|
|
316
|
+
gatewayState === "connected" ? "bg-green-500" :
|
|
317
|
+
gatewayState === "connecting" ? "bg-yellow-500 animate-pulse" :
|
|
318
|
+
"bg-gray-500"
|
|
319
|
+
}`} />
|
|
320
|
+
<span className="text-sm text-gray-300">
|
|
321
|
+
{gatewayState === "connected" ? "OpenClaw Connected" :
|
|
322
|
+
gatewayState === "connecting" ? "Connecting to OpenClaw..." :
|
|
323
|
+
"OpenClaw Disconnected"}
|
|
324
|
+
</span>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{/* Avatar Lists */}
|
|
328
|
+
<div className="w-full space-y-4">
|
|
329
|
+
<h2 className="text-lg font-medium text-white">Select an Avatar</h2>
|
|
330
|
+
{loadingAvatars ? (
|
|
331
|
+
<div className="flex items-center justify-center py-12">
|
|
332
|
+
<div className="w-8 h-8 border-2 border-orange-500/30 border-t-orange-500 rounded-full animate-spin" />
|
|
333
|
+
</div>
|
|
334
|
+
) : customAvatars.length === 0 && publicAvatars.length === 0 ? (
|
|
335
|
+
<div className="text-center py-12 text-gray-400">
|
|
336
|
+
No avatars available. Check your LiveAvatar API key.
|
|
337
|
+
</div>
|
|
338
|
+
) : (
|
|
339
|
+
<div className="space-y-4">
|
|
340
|
+
{/* Custom Avatars Section */}
|
|
341
|
+
{customAvatars.length > 0 && (
|
|
342
|
+
<AvatarSection
|
|
343
|
+
title="Custom Avatars"
|
|
344
|
+
avatars={customAvatars}
|
|
345
|
+
isExpanded={customExpanded}
|
|
346
|
+
onToggle={() => setCustomExpanded(!customExpanded)}
|
|
347
|
+
onSelectAvatar={startSessionWithAvatar}
|
|
348
|
+
/>
|
|
349
|
+
)}
|
|
350
|
+
|
|
351
|
+
{/* Public Avatars Section */}
|
|
352
|
+
{publicAvatars.length > 0 && (
|
|
353
|
+
<AvatarSection
|
|
354
|
+
title="Public Avatars"
|
|
355
|
+
avatars={publicAvatars}
|
|
356
|
+
isExpanded={publicExpanded}
|
|
357
|
+
onToggle={() => setPublicExpanded(!publicExpanded)}
|
|
358
|
+
onSelectAvatar={startSessionWithAvatar}
|
|
359
|
+
showExpiredBadge={false}
|
|
360
|
+
/>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{/* Footer */}
|
|
367
|
+
<p className="text-xs text-gray-500 text-center">
|
|
368
|
+
Powered by{" "}
|
|
369
|
+
<a
|
|
370
|
+
href="https://liveavatar.com"
|
|
371
|
+
target="_blank"
|
|
372
|
+
rel="noopener noreferrer"
|
|
373
|
+
className="text-blue-400 hover:underline"
|
|
374
|
+
>
|
|
375
|
+
LiveAvatar
|
|
376
|
+
</a>{" "}
|
|
377
|
+
+{" "}
|
|
378
|
+
<a
|
|
379
|
+
href="https://openclaw.ai"
|
|
380
|
+
target="_blank"
|
|
381
|
+
rel="noopener noreferrer"
|
|
382
|
+
className="text-orange-400 hover:underline"
|
|
383
|
+
>
|
|
384
|
+
OpenClaw
|
|
385
|
+
</a>
|
|
386
|
+
</p>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Session screen
|
|
393
|
+
return (
|
|
394
|
+
<LiveAvatarSession
|
|
395
|
+
sessionAccessToken={sessionToken}
|
|
396
|
+
onSessionStopped={onSessionStopped}
|
|
397
|
+
/>
|
|
398
|
+
);
|
|
399
|
+
};
|