botschat 0.1.2 → 0.1.4
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 +12 -3
- package/migrations/0009_google_auth.sql +10 -0
- package/migrations/0010_pairing_token_security.sql +10 -0
- package/package.json +1 -1
- package/packages/api/src/do/connection-do.ts +13 -7
- package/packages/api/src/env.ts +3 -0
- package/packages/api/src/index.ts +13 -5
- package/packages/api/src/routes/auth.ts +100 -0
- package/packages/api/src/routes/pairing.ts +12 -5
- package/packages/api/src/routes/setup.ts +199 -0
- package/packages/api/src/utils/firebase.ts +179 -0
- package/packages/api/src/utils/resolve-url.ts +79 -0
- package/packages/plugin/dist/src/channel.d.ts +1 -1
- package/packages/plugin/dist/src/channel.js +3 -3
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/package.json +5 -1
- package/packages/web/dist/assets/{index-C-wI8eHy.css → index-DuGeoFJT.css} +1 -1
- package/packages/web/dist/assets/index-DyzTR_Y4.js +847 -0
- package/packages/web/dist/botschat-logo.png +0 -0
- package/packages/web/dist/index.html +3 -3
- package/packages/web/index.html +1 -1
- package/packages/web/package.json +1 -0
- package/packages/web/src/App.tsx +39 -0
- package/packages/web/src/api.ts +12 -0
- package/packages/web/src/components/LoginPage.tsx +131 -13
- package/packages/web/src/components/OnboardingPage.tsx +383 -0
- package/packages/web/src/firebase.ts +93 -0
- package/packages/web/vite.config.ts +1 -1
- package/wrangler.toml +2 -0
- package/packages/web/dist/assets/index-CbPEKHLG.js +0 -93
|
Binary file
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
<html lang="en" data-theme="dark">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/
|
|
5
|
+
<link rel="icon" type="image/png" href="/botschat-logo.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet" />
|
|
10
10
|
<title>BotsChat</title>
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DyzTR_Y4.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DuGeoFJT.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/packages/web/index.html
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en" data-theme="dark">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" type="image/
|
|
5
|
+
<link rel="icon" type="image/png" href="/botschat-logo.png" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
package/packages/web/src/App.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import { ChatWindow } from "./components/ChatWindow";
|
|
|
17
17
|
import { ThreadPanel } from "./components/ThreadPanel";
|
|
18
18
|
import { JobList } from "./components/JobList";
|
|
19
19
|
import { LoginPage } from "./components/LoginPage";
|
|
20
|
+
import { OnboardingPage } from "./components/OnboardingPage";
|
|
20
21
|
import { DebugLogPanel } from "./components/DebugLogPanel";
|
|
21
22
|
import { CronSidebar } from "./components/CronSidebar";
|
|
22
23
|
import { CronDetail } from "./components/CronDetail";
|
|
@@ -38,6 +39,17 @@ export default function App() {
|
|
|
38
39
|
|
|
39
40
|
const [showSettings, setShowSettings] = useState(false);
|
|
40
41
|
|
|
42
|
+
// Onboarding: show setup page for new users who haven't connected OpenClaw yet.
|
|
43
|
+
// Once dismissed (skip or connected), we remember it for this session.
|
|
44
|
+
const [onboardingDismissed, setOnboardingDismissed] = useState(() => {
|
|
45
|
+
return localStorage.getItem("botschat_onboarding_dismissed") === "1";
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const handleDismissOnboarding = useCallback(() => {
|
|
49
|
+
setOnboardingDismissed(true);
|
|
50
|
+
localStorage.setItem("botschat_onboarding_dismissed", "1");
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
41
53
|
// Theme state – default to system preference then dark
|
|
42
54
|
const [theme, setTheme] = useState<"dark" | "light">(() => {
|
|
43
55
|
const saved = localStorage.getItem("botschat_theme");
|
|
@@ -655,6 +667,17 @@ export default function App() {
|
|
|
655
667
|
[state.jobs],
|
|
656
668
|
);
|
|
657
669
|
|
|
670
|
+
// Auto-dismiss onboarding when OpenClaw connects
|
|
671
|
+
useEffect(() => {
|
|
672
|
+
if (state.openclawConnected && !onboardingDismissed) {
|
|
673
|
+
// Delay slightly so user sees the "Connected!" success state
|
|
674
|
+
const timer = setTimeout(() => {
|
|
675
|
+
handleDismissOnboarding();
|
|
676
|
+
}, 1500);
|
|
677
|
+
return () => clearTimeout(timer);
|
|
678
|
+
}
|
|
679
|
+
}, [state.openclawConnected, onboardingDismissed, handleDismissOnboarding]);
|
|
680
|
+
|
|
658
681
|
// ---- Render ----
|
|
659
682
|
if (!state.user) {
|
|
660
683
|
return (
|
|
@@ -666,6 +689,22 @@ export default function App() {
|
|
|
666
689
|
);
|
|
667
690
|
}
|
|
668
691
|
|
|
692
|
+
// Show onboarding for new users: no channels loaded yet AND not dismissed
|
|
693
|
+
// Wait until channels have been fetched (they're loaded in the useEffect above)
|
|
694
|
+
// to avoid flashing onboarding for returning users.
|
|
695
|
+
const channelsLoaded = state.channels.length > 0;
|
|
696
|
+
const showOnboarding = !onboardingDismissed && !channelsLoaded && !state.openclawConnected;
|
|
697
|
+
|
|
698
|
+
if (showOnboarding) {
|
|
699
|
+
return (
|
|
700
|
+
<AppStateContext.Provider value={state}>
|
|
701
|
+
<AppDispatchContext.Provider value={dispatch}>
|
|
702
|
+
<OnboardingPage onSkip={handleDismissOnboarding} />
|
|
703
|
+
</AppDispatchContext.Provider>
|
|
704
|
+
</AppStateContext.Provider>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
669
708
|
const selectedAgent = state.agents.find((a) => a.id === state.selectedAgentId);
|
|
670
709
|
const selectedTask = state.tasks.find((t) => t.id === state.selectedTaskId);
|
|
671
710
|
const isBackgroundTask = selectedTask?.kind === "background";
|
package/packages/web/src/api.ts
CHANGED
|
@@ -68,6 +68,9 @@ export const authApi = {
|
|
|
68
68
|
request<AuthResponse>("POST", "/auth/register", { email, password, displayName }),
|
|
69
69
|
login: (email: string, password: string) =>
|
|
70
70
|
request<AuthResponse>("POST", "/auth/login", { email, password }),
|
|
71
|
+
/** Sign in with any Firebase provider (Google, GitHub, etc.) */
|
|
72
|
+
firebase: (idToken: string) =>
|
|
73
|
+
request<AuthResponse>("POST", "/auth/firebase", { idToken }),
|
|
71
74
|
me: () => request<{ id: string; email: string; displayName: string | null; settings: UserSettings }>("GET", "/me"),
|
|
72
75
|
};
|
|
73
76
|
|
|
@@ -240,3 +243,12 @@ export const pairingApi = {
|
|
|
240
243
|
request<{ id: string; token: string; label: string | null }>("POST", "/pairing-tokens", { label }),
|
|
241
244
|
delete: (id: string) => request<{ ok: boolean }>("DELETE", `/pairing-tokens/${id}`),
|
|
242
245
|
};
|
|
246
|
+
|
|
247
|
+
export const setupApi = {
|
|
248
|
+
/** Get the recommended cloudUrl from the backend (smart resolution). */
|
|
249
|
+
cloudUrl: () =>
|
|
250
|
+
request<{ cloudUrl: string; isLoopback: boolean; hint?: string }>(
|
|
251
|
+
"GET",
|
|
252
|
+
"/setup/cloud-url",
|
|
253
|
+
),
|
|
254
|
+
};
|
|
@@ -2,6 +2,28 @@ import React, { useState } from "react";
|
|
|
2
2
|
import { authApi, setToken } from "../api";
|
|
3
3
|
import { useAppDispatch } from "../store";
|
|
4
4
|
import { dlog } from "../debug-log";
|
|
5
|
+
import { isFirebaseConfigured, signInWithGoogle, signInWithGitHub } from "../firebase";
|
|
6
|
+
|
|
7
|
+
/** Google "G" logo SVG */
|
|
8
|
+
function GoogleIcon() {
|
|
9
|
+
return (
|
|
10
|
+
<svg width="18" height="18" viewBox="0 0 48 48" style={{ flexShrink: 0 }}>
|
|
11
|
+
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
|
|
12
|
+
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
|
|
13
|
+
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
|
|
14
|
+
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** GitHub logo SVG */
|
|
20
|
+
function GitHubIcon() {
|
|
21
|
+
return (
|
|
22
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" style={{ flexShrink: 0 }}>
|
|
23
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"/>
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
5
27
|
|
|
6
28
|
export function LoginPage() {
|
|
7
29
|
const dispatch = useAppDispatch();
|
|
@@ -11,6 +33,18 @@ export function LoginPage() {
|
|
|
11
33
|
const [displayName, setDisplayName] = useState("");
|
|
12
34
|
const [error, setError] = useState("");
|
|
13
35
|
const [loading, setLoading] = useState(false);
|
|
36
|
+
const [oauthLoading, setOauthLoading] = useState<"google" | "github" | null>(null);
|
|
37
|
+
|
|
38
|
+
const firebaseEnabled = isFirebaseConfigured();
|
|
39
|
+
const anyLoading = loading || !!oauthLoading;
|
|
40
|
+
|
|
41
|
+
const handleAuthSuccess = (res: { id: string; email: string; displayName?: string; token: string }) => {
|
|
42
|
+
setToken(res.token);
|
|
43
|
+
dispatch({
|
|
44
|
+
type: "SET_USER",
|
|
45
|
+
user: { id: res.id, email: res.email, displayName: res.displayName },
|
|
46
|
+
});
|
|
47
|
+
};
|
|
14
48
|
|
|
15
49
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
16
50
|
e.preventDefault();
|
|
@@ -27,11 +61,7 @@ export function LoginPage() {
|
|
|
27
61
|
res = await authApi.login(email, password);
|
|
28
62
|
}
|
|
29
63
|
dlog.info("Auth", `${isRegister ? "Register" : "Login"} success — user ${res.id} (${res.email})`);
|
|
30
|
-
|
|
31
|
-
dispatch({
|
|
32
|
-
type: "SET_USER",
|
|
33
|
-
user: { id: res.id, email: res.email, displayName: res.displayName },
|
|
34
|
-
});
|
|
64
|
+
handleAuthSuccess(res);
|
|
35
65
|
} catch (err) {
|
|
36
66
|
const message = err instanceof Error ? err.message : "Something went wrong";
|
|
37
67
|
dlog.error("Auth", `${isRegister ? "Register" : "Login"} failed: ${message}`);
|
|
@@ -41,6 +71,35 @@ export function LoginPage() {
|
|
|
41
71
|
}
|
|
42
72
|
};
|
|
43
73
|
|
|
74
|
+
const handleOAuthSignIn = async (provider: "google" | "github") => {
|
|
75
|
+
setError("");
|
|
76
|
+
setOauthLoading(provider);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
dlog.info("Auth", `Starting ${provider} sign-in`);
|
|
80
|
+
const signInFn = provider === "google" ? signInWithGoogle : signInWithGitHub;
|
|
81
|
+
const { idToken } = await signInFn();
|
|
82
|
+
dlog.info("Auth", `Got Firebase ID token from ${provider}, verifying with backend`);
|
|
83
|
+
const res = await authApi.firebase(idToken);
|
|
84
|
+
dlog.info("Auth", `${provider} sign-in success — user ${res.id} (${res.email})`);
|
|
85
|
+
handleAuthSuccess(res);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// Don't show error for user-cancelled popup
|
|
88
|
+
if (err instanceof Error && (
|
|
89
|
+
err.message.includes("popup-closed-by-user") ||
|
|
90
|
+
err.message.includes("cancelled")
|
|
91
|
+
)) {
|
|
92
|
+
dlog.info("Auth", `${provider} sign-in cancelled by user`);
|
|
93
|
+
} else {
|
|
94
|
+
const message = err instanceof Error ? err.message : `${provider} sign-in failed`;
|
|
95
|
+
dlog.error("Auth", `${provider} sign-in failed: ${message}`);
|
|
96
|
+
setError(message);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
setOauthLoading(null);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
44
103
|
return (
|
|
45
104
|
<div
|
|
46
105
|
className="min-h-screen flex items-center justify-center"
|
|
@@ -49,12 +108,11 @@ export function LoginPage() {
|
|
|
49
108
|
<div className="w-full max-w-md">
|
|
50
109
|
{/* Header */}
|
|
51
110
|
<div className="text-center mb-8">
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
</div>
|
|
111
|
+
<img
|
|
112
|
+
src="/botschat-logo.png"
|
|
113
|
+
alt="BotsChat"
|
|
114
|
+
className="inline-block w-16 h-16 mb-4"
|
|
115
|
+
/>
|
|
58
116
|
<h1 className="text-3xl font-bold" style={{ color: "var(--text-primary)" }}>
|
|
59
117
|
BotsChat
|
|
60
118
|
</h1>
|
|
@@ -75,6 +133,66 @@ export function LoginPage() {
|
|
|
75
133
|
{isRegister ? "Create account" : "Sign in"}
|
|
76
134
|
</h2>
|
|
77
135
|
|
|
136
|
+
{/* OAuth buttons */}
|
|
137
|
+
{firebaseEnabled && (
|
|
138
|
+
<>
|
|
139
|
+
<div className="space-y-3">
|
|
140
|
+
{/* Google */}
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={() => handleOAuthSignIn("google")}
|
|
144
|
+
disabled={anyLoading}
|
|
145
|
+
className="w-full flex items-center justify-center gap-3 py-2.5 px-4 font-medium text-body rounded-sm disabled:opacity-50 transition-colors hover:brightness-95"
|
|
146
|
+
style={{
|
|
147
|
+
background: "var(--bg-surface)",
|
|
148
|
+
color: "var(--text-primary)",
|
|
149
|
+
border: "1px solid var(--border)",
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{oauthLoading === "google" ? (
|
|
153
|
+
<span>Signing in...</span>
|
|
154
|
+
) : (
|
|
155
|
+
<>
|
|
156
|
+
<GoogleIcon />
|
|
157
|
+
<span>Continue with Google</span>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
</button>
|
|
161
|
+
|
|
162
|
+
{/* GitHub */}
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={() => handleOAuthSignIn("github")}
|
|
166
|
+
disabled={anyLoading}
|
|
167
|
+
className="w-full flex items-center justify-center gap-3 py-2.5 px-4 font-medium text-body rounded-sm disabled:opacity-50 transition-colors hover:brightness-95"
|
|
168
|
+
style={{
|
|
169
|
+
background: "var(--bg-surface)",
|
|
170
|
+
color: "var(--text-primary)",
|
|
171
|
+
border: "1px solid var(--border)",
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{oauthLoading === "github" ? (
|
|
175
|
+
<span>Signing in...</span>
|
|
176
|
+
) : (
|
|
177
|
+
<>
|
|
178
|
+
<GitHubIcon />
|
|
179
|
+
<span>Continue with GitHub</span>
|
|
180
|
+
</>
|
|
181
|
+
)}
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{/* Divider */}
|
|
186
|
+
<div className="flex items-center gap-3 my-5">
|
|
187
|
+
<div className="flex-1 h-px" style={{ background: "var(--border)" }} />
|
|
188
|
+
<span className="text-caption" style={{ color: "var(--text-muted)" }}>
|
|
189
|
+
or
|
|
190
|
+
</span>
|
|
191
|
+
<div className="flex-1 h-px" style={{ background: "var(--border)" }} />
|
|
192
|
+
</div>
|
|
193
|
+
</>
|
|
194
|
+
)}
|
|
195
|
+
|
|
78
196
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
79
197
|
{isRegister && (
|
|
80
198
|
<div>
|
|
@@ -145,7 +263,7 @@ export function LoginPage() {
|
|
|
145
263
|
|
|
146
264
|
<button
|
|
147
265
|
type="submit"
|
|
148
|
-
disabled={
|
|
266
|
+
disabled={anyLoading}
|
|
149
267
|
className="w-full py-2.5 font-bold text-body text-white rounded-sm disabled:opacity-50 transition-colors hover:brightness-110"
|
|
150
268
|
style={{ background: "var(--bg-active)" }}
|
|
151
269
|
>
|
|
@@ -153,7 +271,7 @@ export function LoginPage() {
|
|
|
153
271
|
? "..."
|
|
154
272
|
: isRegister
|
|
155
273
|
? "Create account"
|
|
156
|
-
: "Sign in"}
|
|
274
|
+
: "Sign in with email"}
|
|
157
275
|
</button>
|
|
158
276
|
</form>
|
|
159
277
|
|