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.
@@ -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/svg+xml" href="/favicon.svg" />
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-CbPEKHLG.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-C-wI8eHy.css">
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>
@@ -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/svg+xml" href="/favicon.svg" />
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 />
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@tailwindcss/typography": "^0.5.19",
13
+ "firebase": "^12.9.0",
13
14
  "react": "^19.0.0",
14
15
  "react-dom": "^19.0.0",
15
16
  "react-markdown": "^10.1.0",
@@ -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";
@@ -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
- setToken(res.token);
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
- <div
53
- className="inline-flex items-center justify-center w-16 h-16 rounded-xl text-white text-2xl font-bold mb-4"
54
- style={{ background: "#1264A3" }}
55
- >
56
- BC
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={loading}
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