botschat 0.1.1 → 0.1.3

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.
@@ -0,0 +1,383 @@
1
+ import React, { useEffect, useState, useCallback } from "react";
2
+ import { pairingApi, setupApi, type PairingToken } from "../api";
3
+ import { useAppState } from "../store";
4
+ import { dlog } from "../debug-log";
5
+
6
+ /** Clipboard copy button with feedback */
7
+ function CopyButton({ text }: { text: string }) {
8
+ const [copied, setCopied] = useState(false);
9
+
10
+ const handleCopy = async () => {
11
+ try {
12
+ await navigator.clipboard.writeText(text);
13
+ setCopied(true);
14
+ setTimeout(() => setCopied(false), 2000);
15
+ } catch {
16
+ // Fallback for insecure context
17
+ const ta = document.createElement("textarea");
18
+ ta.value = text;
19
+ document.body.appendChild(ta);
20
+ ta.select();
21
+ document.execCommand("copy");
22
+ document.body.removeChild(ta);
23
+ setCopied(true);
24
+ setTimeout(() => setCopied(false), 2000);
25
+ }
26
+ };
27
+
28
+ return (
29
+ <button
30
+ onClick={handleCopy}
31
+ className="shrink-0 px-2.5 py-1 text-tiny font-medium rounded-sm transition-colors"
32
+ style={{
33
+ background: copied ? "var(--accent-green)" : "var(--bg-hover)",
34
+ color: copied ? "#fff" : "var(--text-secondary)",
35
+ }}
36
+ >
37
+ {copied ? "Copied!" : "Copy"}
38
+ </button>
39
+ );
40
+ }
41
+
42
+ /** Code block with copy button */
43
+ function CodeBlock({ code, multiline }: { code: string; multiline?: boolean }) {
44
+ return (
45
+ <div
46
+ className="flex items-start gap-2 rounded-md px-3 py-2.5"
47
+ style={{ background: "var(--code-bg)", border: "1px solid var(--border)" }}
48
+ >
49
+ <pre
50
+ className="flex-1 text-caption font-mono overflow-x-auto whitespace-pre-wrap break-all"
51
+ style={{ color: "var(--text-primary)" }}
52
+ >
53
+ {code}
54
+ </pre>
55
+ <CopyButton text={code} />
56
+ </div>
57
+ );
58
+ }
59
+
60
+ /** Pulsing dot for "waiting" state */
61
+ function PulsingDot({ color }: { color: string }) {
62
+ return (
63
+ <span className="relative flex h-3 w-3">
64
+ <span
65
+ className="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
66
+ style={{ backgroundColor: color }}
67
+ />
68
+ <span
69
+ className="relative inline-flex rounded-full h-3 w-3"
70
+ style={{ backgroundColor: color }}
71
+ />
72
+ </span>
73
+ );
74
+ }
75
+
76
+ export function OnboardingPage({ onSkip }: { onSkip: () => void }) {
77
+ const state = useAppState();
78
+ const [pairingToken, setPairingToken] = useState<string | null>(null);
79
+ const [loadingToken, setLoadingToken] = useState(true);
80
+
81
+ // Cloud URL — resolved by backend (smart priority), editable by user
82
+ const [cloudUrl, setCloudUrl] = useState<string>(
83
+ typeof window !== "undefined" ? window.location.origin : "https://console.botschat.app",
84
+ );
85
+ const [cloudUrlLoopback, setCloudUrlLoopback] = useState(false);
86
+ const [cloudUrlHint, setCloudUrlHint] = useState<string | undefined>();
87
+ const [editingUrl, setEditingUrl] = useState(false);
88
+
89
+ // Fetch recommended cloudUrl from backend
90
+ useEffect(() => {
91
+ let cancelled = false;
92
+
93
+ async function fetchCloudUrl() {
94
+ try {
95
+ const data = await setupApi.cloudUrl();
96
+ if (cancelled) return;
97
+ setCloudUrl(data.cloudUrl);
98
+ setCloudUrlLoopback(data.isLoopback);
99
+ setCloudUrlHint(data.hint);
100
+ } catch (err) {
101
+ dlog.warn("Onboarding", `Failed to fetch cloudUrl, using origin: ${err}`);
102
+ // Fallback: detect loopback from window.location
103
+ const host = window.location.hostname;
104
+ const loopback = host === "localhost" || host.startsWith("127.");
105
+ setCloudUrlLoopback(loopback);
106
+ if (loopback) {
107
+ setCloudUrlHint(
108
+ "This URL (localhost) only works on this machine. " +
109
+ "If your OpenClaw is on a different host, replace with its LAN IP.",
110
+ );
111
+ }
112
+ }
113
+ }
114
+
115
+ fetchCloudUrl();
116
+ return () => { cancelled = true; };
117
+ }, []);
118
+
119
+ // Load or create pairing token
120
+ useEffect(() => {
121
+ let cancelled = false;
122
+
123
+ async function ensurePairingToken() {
124
+ setLoadingToken(true);
125
+ try {
126
+ // Check existing tokens
127
+ const { tokens } = await pairingApi.list();
128
+ if (cancelled) return;
129
+
130
+ if (tokens.length > 0) {
131
+ dlog.info("Onboarding", `Found ${tokens.length} existing pairing tokens`);
132
+ const { token } = await pairingApi.create("Onboarding setup");
133
+ if (!cancelled) setPairingToken(token);
134
+ } else {
135
+ dlog.info("Onboarding", "No pairing tokens found, creating one");
136
+ const { token } = await pairingApi.create("Default");
137
+ if (!cancelled) setPairingToken(token);
138
+ }
139
+ } catch (err) {
140
+ dlog.error("Onboarding", `Failed to get pairing token: ${err}`);
141
+ } finally {
142
+ if (!cancelled) setLoadingToken(false);
143
+ }
144
+ }
145
+
146
+ ensurePairingToken();
147
+ return () => { cancelled = true; };
148
+ }, []);
149
+
150
+ const setupCommand = pairingToken
151
+ ? `openclaw plugins install @botschat/openclaw-plugin && \\
152
+ openclaw config set channels.botschat.cloudUrl ${cloudUrl} && \\
153
+ openclaw config set channels.botschat.pairingToken ${pairingToken} && \\
154
+ openclaw config set channels.botschat.enabled true && \\
155
+ openclaw gateway restart`
156
+ : "Loading...";
157
+
158
+ const isConnected = state.openclawConnected;
159
+
160
+ return (
161
+ <div
162
+ className="min-h-screen flex items-center justify-center p-4"
163
+ style={{ background: "var(--bg-secondary)" }}
164
+ >
165
+ <div className="w-full max-w-xl">
166
+ {/* Header */}
167
+ <div className="text-center mb-8">
168
+ <div
169
+ className="inline-flex items-center justify-center w-16 h-16 rounded-xl text-white text-2xl font-bold mb-4"
170
+ style={{ background: "#1264A3" }}
171
+ >
172
+ BC
173
+ </div>
174
+ <h1 className="text-3xl font-bold" style={{ color: "var(--text-primary)" }}>
175
+ Welcome to BotsChat!
176
+ </h1>
177
+ <p className="mt-2" style={{ color: "var(--text-secondary)" }}>
178
+ Connect your OpenClaw agent to start chatting.
179
+ </p>
180
+ </div>
181
+
182
+ {/* Main card */}
183
+ <div
184
+ className="rounded-md p-8"
185
+ style={{
186
+ background: "var(--bg-surface)",
187
+ boxShadow: "var(--shadow-lg)",
188
+ }}
189
+ >
190
+ {isConnected ? (
191
+ /* Success state */
192
+ <div className="text-center py-6">
193
+ <div
194
+ className="inline-flex items-center justify-center w-16 h-16 rounded-full mb-4"
195
+ style={{ background: "rgba(43, 172, 118, 0.15)" }}
196
+ >
197
+ <svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="var(--accent-green)" strokeWidth={2.5}>
198
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
199
+ </svg>
200
+ </div>
201
+ <h2 className="text-h1 font-bold mb-2" style={{ color: "var(--text-primary)" }}>
202
+ OpenClaw Connected!
203
+ </h2>
204
+ <p className="text-body mb-6" style={{ color: "var(--text-secondary)" }}>
205
+ Your agent is ready. Start chatting now.
206
+ </p>
207
+ <button
208
+ onClick={onSkip}
209
+ className="px-6 py-2.5 font-bold text-body text-white rounded-sm transition-colors hover:brightness-110"
210
+ style={{ background: "var(--bg-active)" }}
211
+ >
212
+ Start Chatting
213
+ </button>
214
+ </div>
215
+ ) : (
216
+ /* Setup steps */
217
+ <>
218
+ {/* Connection status */}
219
+ <div
220
+ className="flex items-center gap-3 rounded-md px-4 py-3 mb-6"
221
+ style={{
222
+ background: "rgba(232, 162, 48, 0.1)",
223
+ border: "1px solid rgba(232, 162, 48, 0.3)",
224
+ }}
225
+ >
226
+ <PulsingDot color="var(--accent-yellow)" />
227
+ <span className="text-caption font-medium" style={{ color: "var(--accent-yellow)" }}>
228
+ Waiting for OpenClaw connection...
229
+ </span>
230
+ </div>
231
+
232
+ {/* Step 1 */}
233
+ <div className="mb-6">
234
+ <div className="flex items-center gap-2 mb-2">
235
+ <span
236
+ className="inline-flex items-center justify-center w-6 h-6 rounded-full text-tiny font-bold text-white"
237
+ style={{ background: "var(--bg-active)" }}
238
+ >
239
+ 1
240
+ </span>
241
+ <h3 className="text-body font-bold" style={{ color: "var(--text-primary)" }}>
242
+ Run this command on your OpenClaw machine
243
+ </h3>
244
+ </div>
245
+ <p className="text-caption mb-3 ml-8" style={{ color: "var(--text-secondary)" }}>
246
+ This installs the BotsChat plugin, configures the connection, and restarts the gateway.
247
+ </p>
248
+ {/* Loopback URL warning */}
249
+ {cloudUrlLoopback && (
250
+ <div
251
+ className="flex items-start gap-2 rounded-md px-3 py-2.5 mb-3 ml-8 text-caption"
252
+ style={{
253
+ background: "rgba(232, 162, 48, 0.1)",
254
+ border: "1px solid rgba(232, 162, 48, 0.25)",
255
+ color: "var(--accent-yellow)",
256
+ }}
257
+ >
258
+ <svg className="w-4 h-4 mt-0.5 shrink-0" viewBox="0 0 20 20" fill="currentColor">
259
+ <path fillRule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.168 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 6a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 6zm0 9a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
260
+ </svg>
261
+ <span>
262
+ {cloudUrlHint || "localhost URL may not be reachable from other machines."}
263
+ {" "}
264
+ <button
265
+ onClick={() => setEditingUrl(true)}
266
+ className="underline font-medium hover:brightness-110"
267
+ style={{ color: "var(--text-link)" }}
268
+ >
269
+ Change URL
270
+ </button>
271
+ </span>
272
+ </div>
273
+ )}
274
+ {/* Editable URL inline */}
275
+ {editingUrl && (
276
+ <div className="flex items-center gap-2 mb-3 ml-8">
277
+ <label className="text-caption font-bold shrink-0" style={{ color: "var(--text-secondary)" }}>
278
+ Cloud URL:
279
+ </label>
280
+ <input
281
+ type="text"
282
+ value={cloudUrl}
283
+ onChange={(e) => {
284
+ setCloudUrl(e.target.value.replace(/\/+$/, ""));
285
+ setCloudUrlLoopback(false);
286
+ }}
287
+ className="flex-1 px-2.5 py-1.5 rounded-sm text-caption font-mono"
288
+ style={{
289
+ background: "var(--code-bg)",
290
+ border: "1px solid var(--border)",
291
+ color: "var(--text-primary)",
292
+ outline: "none",
293
+ }}
294
+ placeholder="http://192.168.x.x:8787"
295
+ autoFocus
296
+ />
297
+ <button
298
+ onClick={() => setEditingUrl(false)}
299
+ className="px-2.5 py-1 text-tiny font-medium rounded-sm"
300
+ style={{ background: "var(--bg-active)", color: "#fff" }}
301
+ >
302
+ Done
303
+ </button>
304
+ </div>
305
+ )}
306
+ <div className="ml-8">
307
+ {loadingToken ? (
308
+ <div
309
+ className="rounded-md px-3 py-2.5 animate-pulse"
310
+ style={{ background: "var(--code-bg)", height: "80px" }}
311
+ />
312
+ ) : (
313
+ <CodeBlock code={setupCommand} multiline />
314
+ )}
315
+ </div>
316
+ </div>
317
+
318
+ {/* Step 2 */}
319
+ <div className="mb-6">
320
+ <div className="flex items-center gap-2 mb-2">
321
+ <span
322
+ className="inline-flex items-center justify-center w-6 h-6 rounded-full text-tiny font-bold text-white"
323
+ style={{ background: "var(--bg-active)" }}
324
+ >
325
+ 2
326
+ </span>
327
+ <h3 className="text-body font-bold" style={{ color: "var(--text-primary)" }}>
328
+ Verify connection
329
+ </h3>
330
+ </div>
331
+ <p className="text-caption ml-8" style={{ color: "var(--text-secondary)" }}>
332
+ Check the gateway logs — you should see "Authenticated with BotsChat cloud":
333
+ </p>
334
+ <div className="ml-8 mt-2">
335
+ <CodeBlock code="openclaw gateway logs" />
336
+ </div>
337
+ </div>
338
+
339
+ {/* PAT info (collapsible) */}
340
+ <details className="mb-4">
341
+ <summary
342
+ className="text-caption font-medium cursor-pointer select-none"
343
+ style={{ color: "var(--text-link)" }}
344
+ >
345
+ Or configure manually
346
+ </summary>
347
+ <div className="mt-3 space-y-3 ml-1">
348
+ <div>
349
+ <label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
350
+ Your Pairing Token
351
+ </label>
352
+ {pairingToken ? (
353
+ <CodeBlock code={pairingToken} />
354
+ ) : (
355
+ <span className="text-caption" style={{ color: "var(--text-muted)" }}>Loading...</span>
356
+ )}
357
+ </div>
358
+ <div>
359
+ <label className="block text-caption font-bold mb-1" style={{ color: "var(--text-secondary)" }}>
360
+ Cloud URL
361
+ </label>
362
+ <CodeBlock code={cloudUrl} />
363
+ </div>
364
+ </div>
365
+ </details>
366
+
367
+ {/* Skip */}
368
+ <div className="text-center pt-2">
369
+ <button
370
+ onClick={onSkip}
371
+ className="text-caption hover:underline"
372
+ style={{ color: "var(--text-muted)" }}
373
+ >
374
+ Skip for now
375
+ </button>
376
+ </div>
377
+ </>
378
+ )}
379
+ </div>
380
+ </div>
381
+ </div>
382
+ );
383
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Firebase client initialization for OAuth Sign-In (Google, GitHub).
3
+ *
4
+ * The Firebase config is loaded from Vite environment variables.
5
+ * Create a `.env` file in packages/web/ with your Firebase config:
6
+ *
7
+ * VITE_FIREBASE_API_KEY=AIzaSy...
8
+ * VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
9
+ * VITE_FIREBASE_PROJECT_ID=your-project-id
10
+ */
11
+
12
+ import { initializeApp, type FirebaseApp } from "firebase/app";
13
+ import {
14
+ getAuth,
15
+ GoogleAuthProvider,
16
+ GithubAuthProvider,
17
+ signInWithPopup,
18
+ type Auth,
19
+ } from "firebase/auth";
20
+
21
+ const firebaseConfig = {
22
+ apiKey: import.meta.env.VITE_FIREBASE_API_KEY as string,
23
+ authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN as string,
24
+ projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID as string,
25
+ };
26
+
27
+ let app: FirebaseApp | null = null;
28
+ let auth: Auth | null = null;
29
+
30
+ /** Check if Firebase is configured (all required env vars present). */
31
+ export function isFirebaseConfigured(): boolean {
32
+ return !!(firebaseConfig.apiKey && firebaseConfig.authDomain && firebaseConfig.projectId);
33
+ }
34
+
35
+ function getFirebaseAuth(): Auth {
36
+ if (!auth) {
37
+ if (!isFirebaseConfigured()) {
38
+ throw new Error("Firebase is not configured. Set VITE_FIREBASE_* env vars.");
39
+ }
40
+ app = initializeApp(firebaseConfig);
41
+ auth = getAuth(app);
42
+ }
43
+ return auth;
44
+ }
45
+
46
+ export type FirebaseSignInResult = {
47
+ idToken: string;
48
+ email: string;
49
+ displayName: string | null;
50
+ photoURL: string | null;
51
+ provider: "google" | "github";
52
+ };
53
+
54
+ /**
55
+ * Sign in with Google via popup and return the Firebase ID token.
56
+ */
57
+ export async function signInWithGoogle(): Promise<FirebaseSignInResult> {
58
+ const firebaseAuth = getFirebaseAuth();
59
+ const provider = new GoogleAuthProvider();
60
+ provider.addScope("email");
61
+ provider.addScope("profile");
62
+
63
+ const result = await signInWithPopup(firebaseAuth, provider);
64
+ const idToken = await result.user.getIdToken();
65
+
66
+ return {
67
+ idToken,
68
+ email: result.user.email ?? "",
69
+ displayName: result.user.displayName,
70
+ photoURL: result.user.photoURL,
71
+ provider: "google",
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Sign in with GitHub via popup and return the Firebase ID token.
77
+ */
78
+ export async function signInWithGitHub(): Promise<FirebaseSignInResult> {
79
+ const firebaseAuth = getFirebaseAuth();
80
+ const provider = new GithubAuthProvider();
81
+ provider.addScope("user:email");
82
+
83
+ const result = await signInWithPopup(firebaseAuth, provider);
84
+ const idToken = await result.user.getIdToken();
85
+
86
+ return {
87
+ idToken,
88
+ email: result.user.email ?? "",
89
+ displayName: result.user.displayName,
90
+ photoURL: result.user.photoURL,
91
+ provider: "github",
92
+ };
93
+ }
@@ -11,7 +11,7 @@ export default defineConfig({
11
11
  port: 3000,
12
12
  proxy: {
13
13
  "/api": {
14
- target: "https://botschat-api.auxtenwpc.workers.dev",
14
+ target: "https://console.botschat.app",
15
15
  changeOrigin: true,
16
16
  ws: true,
17
17
  },
package/wrangler.toml CHANGED
@@ -34,6 +34,8 @@ new_sqlite_classes = ["ConnectionDO"]
34
34
  # --- Environment Variables ---
35
35
  [vars]
36
36
  ENVIRONMENT = "development"
37
+ # Firebase project ID for Google/GitHub Sign-In token verification.
38
+ FIREBASE_PROJECT_ID = "botschat-130ff"
37
39
 
38
40
  # --- Dev settings ---
39
41
  [dev]