botschat 0.1.4 → 0.1.7
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 +64 -24
- package/migrations/0011_e2e_encryption.sql +35 -0
- package/package.json +7 -2
- package/packages/api/package.json +2 -1
- package/packages/api/src/do/connection-do.ts +162 -42
- package/packages/api/src/index.ts +132 -13
- package/packages/api/src/routes/auth.ts +127 -30
- package/packages/api/src/routes/pairing.ts +14 -1
- package/packages/api/src/routes/setup.ts +72 -24
- package/packages/api/src/routes/upload.ts +12 -8
- package/packages/api/src/utils/auth.ts +212 -43
- package/packages/api/src/utils/id.ts +30 -14
- package/packages/api/src/utils/rate-limit.ts +73 -0
- package/packages/plugin/dist/src/accounts.d.ts.map +1 -1
- package/packages/plugin/dist/src/accounts.js +1 -0
- package/packages/plugin/dist/src/accounts.js.map +1 -1
- package/packages/plugin/dist/src/channel.d.ts +1 -0
- package/packages/plugin/dist/src/channel.d.ts.map +1 -1
- package/packages/plugin/dist/src/channel.js +151 -9
- package/packages/plugin/dist/src/channel.js.map +1 -1
- package/packages/plugin/dist/src/types.d.ts +16 -0
- package/packages/plugin/dist/src/types.d.ts.map +1 -1
- package/packages/plugin/dist/src/ws-client.d.ts +2 -0
- package/packages/plugin/dist/src/ws-client.d.ts.map +1 -1
- package/packages/plugin/dist/src/ws-client.js +14 -3
- package/packages/plugin/dist/src/ws-client.js.map +1 -1
- package/packages/plugin/package.json +4 -3
- package/packages/web/dist/architecture.png +0 -0
- package/packages/web/dist/assets/index-BoNQoJjQ.js +1497 -0
- package/packages/web/dist/assets/{index-DuGeoFJT.css → index-ewBIratI.css} +1 -1
- package/packages/web/dist/botschat-icon.svg +4 -0
- package/packages/web/dist/index.html +23 -3
- package/packages/web/dist/manifest.json +24 -0
- package/packages/web/dist/sw.js +40 -0
- package/packages/web/index.html +21 -1
- package/packages/web/package.json +1 -0
- package/packages/web/src/App.tsx +286 -103
- package/packages/web/src/analytics.ts +57 -0
- package/packages/web/src/api.ts +67 -3
- package/packages/web/src/components/ChatWindow.tsx +11 -11
- package/packages/web/src/components/ConnectionSettings.tsx +477 -0
- package/packages/web/src/components/CronDetail.tsx +475 -235
- package/packages/web/src/components/CronSidebar.tsx +1 -1
- package/packages/web/src/components/DebugLogPanel.tsx +116 -3
- package/packages/web/src/components/E2ESettings.tsx +122 -0
- package/packages/web/src/components/IconRail.tsx +56 -27
- package/packages/web/src/components/JobList.tsx +2 -6
- package/packages/web/src/components/LoginPage.tsx +143 -104
- package/packages/web/src/components/MobileLayout.tsx +480 -0
- package/packages/web/src/components/OnboardingPage.tsx +159 -21
- package/packages/web/src/components/ResizeHandle.tsx +34 -0
- package/packages/web/src/components/Sidebar.tsx +1 -1
- package/packages/web/src/components/TaskBar.tsx +2 -2
- package/packages/web/src/components/ThreadPanel.tsx +2 -5
- package/packages/web/src/e2e.ts +133 -0
- package/packages/web/src/hooks/useIsMobile.ts +27 -0
- package/packages/web/src/index.css +59 -0
- package/packages/web/src/main.tsx +12 -0
- package/packages/web/src/store.ts +16 -8
- package/packages/web/src/ws.ts +78 -4
- package/scripts/dev.sh +16 -16
- package/scripts/test-e2e-live.ts +194 -0
- package/scripts/verify-e2e-db.ts +48 -0
- package/scripts/verify-e2e.ts +56 -0
- package/wrangler.toml +3 -1
- package/packages/web/dist/assets/index-DyzTR_Y4.js +0 -847
package/packages/web/src/App.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useReducer, useEffect, useCallback, useRef, useState } from "react";
|
|
2
|
+
import { Group, Panel, useDefaultLayout } from "react-resizable-panels";
|
|
2
3
|
import {
|
|
3
4
|
appReducer,
|
|
4
5
|
initialState,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
type AppState,
|
|
9
10
|
type ActiveView,
|
|
10
11
|
} from "./store";
|
|
11
|
-
import { getToken, setToken, agentsApi, channelsApi, tasksApi, jobsApi, authApi, messagesApi, modelsApi, meApi, sessionsApi, type ModelInfo } from "./api";
|
|
12
|
+
import { getToken, setToken, setRefreshToken, agentsApi, channelsApi, tasksApi, jobsApi, authApi, messagesApi, modelsApi, meApi, sessionsApi, type ModelInfo } from "./api";
|
|
12
13
|
import { ModelSelect } from "./components/ModelSelect";
|
|
13
14
|
import { BotsChatWSClient, type WSMessage } from "./ws";
|
|
14
15
|
import { IconRail } from "./components/IconRail";
|
|
@@ -18,10 +19,17 @@ import { ThreadPanel } from "./components/ThreadPanel";
|
|
|
18
19
|
import { JobList } from "./components/JobList";
|
|
19
20
|
import { LoginPage } from "./components/LoginPage";
|
|
20
21
|
import { OnboardingPage } from "./components/OnboardingPage";
|
|
22
|
+
import { ConnectionSettings } from "./components/ConnectionSettings";
|
|
23
|
+
import { E2ESettings } from "./components/E2ESettings";
|
|
21
24
|
import { DebugLogPanel } from "./components/DebugLogPanel";
|
|
22
25
|
import { CronSidebar } from "./components/CronSidebar";
|
|
23
26
|
import { CronDetail } from "./components/CronDetail";
|
|
27
|
+
import { ResizeHandle } from "./components/ResizeHandle";
|
|
28
|
+
import { useIsMobile } from "./hooks/useIsMobile";
|
|
29
|
+
import { MobileLayout } from "./components/MobileLayout";
|
|
24
30
|
import { dlog } from "./debug-log";
|
|
31
|
+
import { E2eService } from "./e2e";
|
|
32
|
+
import { gtagPageView } from "./analytics";
|
|
25
33
|
|
|
26
34
|
export default function App() {
|
|
27
35
|
const [state, dispatch] = useReducer(appReducer, initialState, (init): AppState => {
|
|
@@ -36,8 +44,18 @@ export default function App() {
|
|
|
36
44
|
});
|
|
37
45
|
const wsClientRef = useRef<BotsChatWSClient | null>(null);
|
|
38
46
|
const handleWSMessageRef = useRef<(msg: WSMessage) => void>(() => {});
|
|
47
|
+
const creatingGeneralRef = useRef(false);
|
|
39
48
|
|
|
40
49
|
const [showSettings, setShowSettings] = useState(false);
|
|
50
|
+
const [settingsTab, setSettingsTab] = useState<"general" | "connection" | "security">("general");
|
|
51
|
+
|
|
52
|
+
// Track whether the initial channels fetch has completed (prevents onboarding flash)
|
|
53
|
+
const [channelsLoadedOnce, setChannelsLoadedOnce] = useState(false);
|
|
54
|
+
|
|
55
|
+
// Responsive layout hooks (must be called unconditionally)
|
|
56
|
+
const isMobile = useIsMobile();
|
|
57
|
+
const mainLayout = useDefaultLayout({ id: "botschat-main" });
|
|
58
|
+
const contentLayout = useDefaultLayout({ id: "botschat-content" });
|
|
41
59
|
|
|
42
60
|
// Onboarding: show setup page for new users who haven't connected OpenClaw yet.
|
|
43
61
|
// Once dismissed (skip or connected), we remember it for this session.
|
|
@@ -60,6 +78,9 @@ export default function App() {
|
|
|
60
78
|
useEffect(() => {
|
|
61
79
|
document.documentElement.setAttribute("data-theme", theme);
|
|
62
80
|
localStorage.setItem("botschat_theme", theme);
|
|
81
|
+
// Sync PWA theme-color meta tag with current theme
|
|
82
|
+
const meta = document.querySelector('meta[name="theme-color"]');
|
|
83
|
+
if (meta) meta.setAttribute("content", theme === "dark" ? "#1A1D21" : "#FFFFFF");
|
|
63
84
|
}, [theme]);
|
|
64
85
|
|
|
65
86
|
// Persist active view (messages / automations)
|
|
@@ -67,6 +88,11 @@ export default function App() {
|
|
|
67
88
|
localStorage.setItem("botschat_active_view", state.activeView);
|
|
68
89
|
}, [state.activeView]);
|
|
69
90
|
|
|
91
|
+
// Google Analytics: track virtual page views for SPA tabs
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
gtagPageView(state.activeView);
|
|
94
|
+
}, [state.activeView]);
|
|
95
|
+
|
|
70
96
|
// Persist selected cron task for automations view
|
|
71
97
|
useEffect(() => {
|
|
72
98
|
if (state.selectedCronTaskId) {
|
|
@@ -98,13 +124,12 @@ export default function App() {
|
|
|
98
124
|
.then((user) => {
|
|
99
125
|
dlog.info("Auth", `Logged in as ${user.email} (${user.id})`);
|
|
100
126
|
dispatch({ type: "SET_USER", user });
|
|
101
|
-
|
|
102
|
-
dispatch({ type: "SET_DEFAULT_MODEL", model: user.settings.defaultModel });
|
|
103
|
-
}
|
|
127
|
+
// defaultModel comes from plugin via connection.status, not from user.settings
|
|
104
128
|
})
|
|
105
129
|
.catch((err) => {
|
|
106
130
|
dlog.warn("Auth", `Auto-login failed: ${err}`);
|
|
107
131
|
setToken(null);
|
|
132
|
+
setRefreshToken(null);
|
|
108
133
|
});
|
|
109
134
|
}
|
|
110
135
|
}, []);
|
|
@@ -154,6 +179,7 @@ export default function App() {
|
|
|
154
179
|
channelsApi.list().then(({ channels }) => {
|
|
155
180
|
dlog.info("Channels", `Loaded ${channels.length} channels`, channels.map((c) => ({ id: c.id, name: c.name })));
|
|
156
181
|
dispatch({ type: "SET_CHANNELS", channels });
|
|
182
|
+
setChannelsLoadedOnce(true);
|
|
157
183
|
});
|
|
158
184
|
}
|
|
159
185
|
}, [state.user]);
|
|
@@ -263,12 +289,48 @@ export default function App() {
|
|
|
263
289
|
}).catch((err) => {
|
|
264
290
|
dlog.error("Sessions", `Failed to load sessions: ${err}`);
|
|
265
291
|
});
|
|
292
|
+
} else if (agent?.isDefault && !creatingGeneralRef.current && onboardingDismissed) {
|
|
293
|
+
// Default agent has no channelId yet — auto-create the "General" channel
|
|
294
|
+
// (which also creates "Session 1") so the user sees a chat immediately.
|
|
295
|
+
// Gated by onboardingDismissed to avoid flashing the onboarding page away.
|
|
296
|
+
dlog.info("Sessions", "Default agent has no channel — auto-creating General channel");
|
|
297
|
+
creatingGeneralRef.current = true;
|
|
298
|
+
dispatch({ type: "SET_TASKS", tasks: [] });
|
|
299
|
+
dispatch({ type: "SELECT_TASK", taskId: null });
|
|
300
|
+
dispatch({ type: "SET_SESSIONS", sessions: [] });
|
|
301
|
+
|
|
302
|
+
channelsApi
|
|
303
|
+
.create({ name: "General", openclawAgentId: "main" })
|
|
304
|
+
.then(async (channel) => {
|
|
305
|
+
dlog.info("Sessions", `General channel created: ${channel.id}`);
|
|
306
|
+
// Reload agents and channels so the default agent picks up the new channelId
|
|
307
|
+
const [{ agents: freshAgents }, { channels: freshChannels }] = await Promise.all([
|
|
308
|
+
agentsApi.list(),
|
|
309
|
+
channelsApi.list(),
|
|
310
|
+
]);
|
|
311
|
+
dispatch({ type: "SET_AGENTS", agents: freshAgents });
|
|
312
|
+
dispatch({ type: "SET_CHANNELS", channels: freshChannels });
|
|
313
|
+
// Channel creation auto-creates "Session 1" — load and select it
|
|
314
|
+
const { sessions: newSessions } = await sessionsApi.list(channel.id);
|
|
315
|
+
dispatch({ type: "SET_SESSIONS", sessions: newSessions });
|
|
316
|
+
if (newSessions.length > 0) {
|
|
317
|
+
dispatch({
|
|
318
|
+
type: "SELECT_SESSION",
|
|
319
|
+
sessionId: newSessions[0].id,
|
|
320
|
+
sessionKey: newSessions[0].sessionKey,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
.catch((err) => {
|
|
325
|
+
dlog.error("Sessions", `Failed to auto-create General channel: ${err}`);
|
|
326
|
+
creatingGeneralRef.current = false;
|
|
327
|
+
});
|
|
266
328
|
} else {
|
|
267
329
|
dispatch({ type: "SET_TASKS", tasks: [] });
|
|
268
330
|
dispatch({ type: "SELECT_TASK", taskId: null });
|
|
269
331
|
dispatch({ type: "SET_SESSIONS", sessions: [] });
|
|
270
332
|
}
|
|
271
|
-
}, [state.selectedAgentId, selectedAgentChannelId, isMessagesView]);
|
|
333
|
+
}, [state.selectedAgentId, selectedAgentChannelId, isMessagesView, onboardingDismissed]);
|
|
272
334
|
|
|
273
335
|
// ---- Load jobs when a background task is selected ----
|
|
274
336
|
useEffect(() => {
|
|
@@ -305,12 +367,29 @@ export default function App() {
|
|
|
305
367
|
let stale = false;
|
|
306
368
|
messagesApi
|
|
307
369
|
.list(state.user.id, state.selectedSessionKey)
|
|
308
|
-
.then(({ messages, replyCounts }) => {
|
|
370
|
+
.then(async ({ messages, replyCounts }) => {
|
|
309
371
|
// Guard against stale responses when the user rapidly switches channels:
|
|
310
372
|
// the cleanup function sets `stale = true` before the new effect runs.
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
373
|
+
if (stale) return;
|
|
374
|
+
|
|
375
|
+
// Decrypt history if possible
|
|
376
|
+
const decryptedMessages = await Promise.all(messages.map(async (m) => {
|
|
377
|
+
if (m.encrypted && E2eService.hasKey()) {
|
|
378
|
+
try {
|
|
379
|
+
// Use message ID as context ID (nonce source)
|
|
380
|
+
const plaintext = await E2eService.decrypt(m.text, m.id);
|
|
381
|
+
return { ...m, text: plaintext, isEncryptedLocked: false };
|
|
382
|
+
} catch (err) {
|
|
383
|
+
console.warn(`Failed to decrypt message ${m.id}`, err);
|
|
384
|
+
return { ...m, isEncryptedLocked: true };
|
|
385
|
+
}
|
|
386
|
+
} else if (m.encrypted) {
|
|
387
|
+
return { ...m, isEncryptedLocked: true };
|
|
388
|
+
}
|
|
389
|
+
return m;
|
|
390
|
+
}));
|
|
391
|
+
|
|
392
|
+
dispatch({ type: "SET_MESSAGES", messages: decryptedMessages as ChatMessage[], replyCounts });
|
|
314
393
|
})
|
|
315
394
|
.catch((err) => {
|
|
316
395
|
console.error("Failed to load message history:", err);
|
|
@@ -689,11 +768,10 @@ export default function App() {
|
|
|
689
768
|
);
|
|
690
769
|
}
|
|
691
770
|
|
|
692
|
-
// Show onboarding for new users:
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
const
|
|
696
|
-
const showOnboarding = !onboardingDismissed && !channelsLoaded && !state.openclawConnected;
|
|
771
|
+
// Show onboarding for new users: channels have been fetched (first API call completed)
|
|
772
|
+
// and none exist. This prevents flashing onboarding for returning users whose
|
|
773
|
+
// channel list simply hasn't loaded yet.
|
|
774
|
+
const showOnboarding = !onboardingDismissed && channelsLoadedOnce && state.channels.length === 0 && !state.openclawConnected;
|
|
697
775
|
|
|
698
776
|
if (showOnboarding) {
|
|
699
777
|
return (
|
|
@@ -712,67 +790,122 @@ export default function App() {
|
|
|
712
790
|
|
|
713
791
|
const isAutomationsView = state.activeView === "automations";
|
|
714
792
|
|
|
793
|
+
// ---- Mobile layout ----
|
|
794
|
+
if (isMobile) {
|
|
795
|
+
return (
|
|
796
|
+
<AppStateContext.Provider value={state}>
|
|
797
|
+
<AppDispatchContext.Provider value={dispatch}>
|
|
798
|
+
<MobileLayout
|
|
799
|
+
sendMessage={sendMessage}
|
|
800
|
+
theme={theme}
|
|
801
|
+
onToggleTheme={toggleTheme}
|
|
802
|
+
showSettings={showSettings}
|
|
803
|
+
onOpenSettings={() => setShowSettings(true)}
|
|
804
|
+
onCloseSettings={() => setShowSettings(false)}
|
|
805
|
+
handleDefaultModelChange={handleDefaultModelChange}
|
|
806
|
+
handleSelectJob={handleSelectJob}
|
|
807
|
+
/>
|
|
808
|
+
</AppDispatchContext.Provider>
|
|
809
|
+
</AppStateContext.Provider>
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ---- Desktop layout with resizable panels ----
|
|
715
814
|
return (
|
|
716
815
|
<AppStateContext.Provider value={state}>
|
|
717
816
|
<AppDispatchContext.Provider value={dispatch}>
|
|
718
817
|
<div className="flex flex-col h-screen">
|
|
719
818
|
<div className="flex flex-1 min-h-0">
|
|
720
|
-
{/* Icon Rail
|
|
819
|
+
{/* Icon Rail — fixed 48px, outside Group */}
|
|
721
820
|
<IconRail onToggleTheme={toggleTheme} onOpenSettings={() => setShowSettings(true)} theme={theme} />
|
|
722
821
|
|
|
723
|
-
{/*
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
<ChatWindow sendMessage={sendMessage} />
|
|
743
|
-
|
|
744
|
-
{/* Detail Panel (right side, conditional) */}
|
|
745
|
-
<ThreadPanel sendMessage={sendMessage} />
|
|
746
|
-
</div>
|
|
747
|
-
</>
|
|
822
|
+
{/* Resizable panels: Sidebar + Content */}
|
|
823
|
+
<Group
|
|
824
|
+
orientation="horizontal"
|
|
825
|
+
defaultLayout={mainLayout.defaultLayout}
|
|
826
|
+
onLayoutChanged={mainLayout.onLayoutChanged}
|
|
827
|
+
id="botschat-main"
|
|
828
|
+
className="flex-1"
|
|
829
|
+
>
|
|
830
|
+
{/* Sidebar panel */}
|
|
831
|
+
<Panel id="sidebar" defaultSize="15%" minSize="5%" maxSize="30%">
|
|
832
|
+
{isAutomationsView ? <CronSidebar /> : <Sidebar />}
|
|
833
|
+
</Panel>
|
|
834
|
+
|
|
835
|
+
<ResizeHandle />
|
|
836
|
+
|
|
837
|
+
{/* Main content panel */}
|
|
838
|
+
<Panel id="content">
|
|
839
|
+
{isAutomationsView ? (
|
|
840
|
+
<CronDetail />
|
|
748
841
|
) : (
|
|
749
|
-
<div className="flex-1 flex
|
|
750
|
-
|
|
751
|
-
<
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
strokeWidth={1}
|
|
757
|
-
style={{ color: "var(--text-muted)" }}
|
|
842
|
+
<div className="flex-1 flex flex-col min-w-0 h-full">
|
|
843
|
+
{hasSession ? (
|
|
844
|
+
<Group
|
|
845
|
+
orientation="horizontal"
|
|
846
|
+
defaultLayout={contentLayout.defaultLayout}
|
|
847
|
+
onLayoutChanged={contentLayout.onLayoutChanged}
|
|
848
|
+
id="botschat-content"
|
|
758
849
|
>
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
850
|
+
{/* JobList — conditional */}
|
|
851
|
+
{isBackgroundTask && (
|
|
852
|
+
<>
|
|
853
|
+
<Panel id="joblist" defaultSize="15%" minSize="5%" maxSize="30%">
|
|
854
|
+
<JobList
|
|
855
|
+
jobs={state.jobs}
|
|
856
|
+
selectedJobId={state.selectedJobId}
|
|
857
|
+
onSelectJob={handleSelectJob}
|
|
858
|
+
/>
|
|
859
|
+
</Panel>
|
|
860
|
+
<ResizeHandle />
|
|
861
|
+
</>
|
|
862
|
+
)}
|
|
863
|
+
|
|
864
|
+
{/* ChatWindow — main area */}
|
|
865
|
+
<Panel id="chat">
|
|
866
|
+
<ChatWindow sendMessage={sendMessage} />
|
|
867
|
+
</Panel>
|
|
868
|
+
|
|
869
|
+
{/* ThreadPanel — conditional */}
|
|
870
|
+
{state.activeThreadId && (
|
|
871
|
+
<>
|
|
872
|
+
<ResizeHandle />
|
|
873
|
+
<Panel id="thread" defaultSize="28%" minSize="10%" maxSize="50%">
|
|
874
|
+
<ThreadPanel sendMessage={sendMessage} />
|
|
875
|
+
</Panel>
|
|
876
|
+
</>
|
|
877
|
+
)}
|
|
878
|
+
</Group>
|
|
879
|
+
) : (
|
|
880
|
+
<div className="flex-1 flex items-center justify-center" style={{ background: "var(--bg-surface)" }}>
|
|
881
|
+
<div className="text-center">
|
|
882
|
+
<svg
|
|
883
|
+
className="w-20 h-20 mx-auto mb-4"
|
|
884
|
+
fill="none"
|
|
885
|
+
viewBox="0 0 24 24"
|
|
886
|
+
stroke="currentColor"
|
|
887
|
+
strokeWidth={1}
|
|
888
|
+
style={{ color: "var(--text-muted)" }}
|
|
889
|
+
>
|
|
890
|
+
<path
|
|
891
|
+
strokeLinecap="round"
|
|
892
|
+
strokeLinejoin="round"
|
|
893
|
+
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
|
894
|
+
/>
|
|
895
|
+
</svg>
|
|
896
|
+
<p className="text-body font-bold" style={{ color: "var(--text-muted)" }}>
|
|
897
|
+
Select a channel to get started
|
|
898
|
+
</p>
|
|
899
|
+
<p className="text-caption mt-1" style={{ color: "var(--text-muted)" }}>
|
|
900
|
+
Choose a channel from the sidebar
|
|
901
|
+
</p>
|
|
902
|
+
</div>
|
|
903
|
+
</div>
|
|
904
|
+
)}
|
|
772
905
|
</div>
|
|
773
906
|
)}
|
|
774
|
-
</
|
|
775
|
-
|
|
907
|
+
</Panel>
|
|
908
|
+
</Group>
|
|
776
909
|
</div>
|
|
777
910
|
|
|
778
911
|
{/* Global debug log panel — collapsible at bottom */}
|
|
@@ -787,11 +920,11 @@ export default function App() {
|
|
|
787
920
|
onClick={() => setShowSettings(false)}
|
|
788
921
|
>
|
|
789
922
|
<div
|
|
790
|
-
className="rounded-lg p-6 w-[
|
|
923
|
+
className="rounded-lg p-6 w-[540px] max-w-[90vw] max-h-[85vh] flex flex-col"
|
|
791
924
|
style={{ background: "var(--bg-surface)", boxShadow: "var(--shadow-lg)" }}
|
|
792
925
|
onClick={(e) => e.stopPropagation()}
|
|
793
926
|
>
|
|
794
|
-
<div className="flex items-center justify-between mb-
|
|
927
|
+
<div className="flex items-center justify-between mb-4">
|
|
795
928
|
<h2 className="text-h1 font-bold" style={{ color: "var(--text-primary)" }}>
|
|
796
929
|
Settings
|
|
797
930
|
</h2>
|
|
@@ -806,46 +939,96 @@ export default function App() {
|
|
|
806
939
|
</button>
|
|
807
940
|
</div>
|
|
808
941
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
<
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
942
|
+
{/* Tab bar */}
|
|
943
|
+
<div className="flex gap-4 mb-4" style={{ borderBottom: "1px solid var(--border)" }}>
|
|
944
|
+
<button
|
|
945
|
+
className="pb-2 text-caption font-bold transition-colors"
|
|
946
|
+
style={{
|
|
947
|
+
color: settingsTab === "general" ? "var(--text-primary)" : "var(--text-muted)",
|
|
948
|
+
borderBottom: settingsTab === "general" ? "2px solid var(--bg-active)" : "2px solid transparent",
|
|
949
|
+
marginBottom: "-1px",
|
|
950
|
+
}}
|
|
951
|
+
onClick={() => setSettingsTab("general")}
|
|
952
|
+
>
|
|
953
|
+
General
|
|
954
|
+
</button>
|
|
955
|
+
<button
|
|
956
|
+
className="pb-2 text-caption font-bold transition-colors"
|
|
957
|
+
style={{
|
|
958
|
+
color: settingsTab === "connection" ? "var(--text-primary)" : "var(--text-muted)",
|
|
959
|
+
borderBottom: settingsTab === "connection" ? "2px solid var(--bg-active)" : "2px solid transparent",
|
|
960
|
+
marginBottom: "-1px",
|
|
961
|
+
}}
|
|
962
|
+
onClick={() => setSettingsTab("connection")}
|
|
963
|
+
>
|
|
964
|
+
Connection
|
|
965
|
+
</button>
|
|
966
|
+
<button
|
|
967
|
+
className="pb-2 text-caption font-bold transition-colors"
|
|
968
|
+
style={{
|
|
969
|
+
color: settingsTab === "security" ? "var(--text-primary)" : "var(--text-muted)",
|
|
970
|
+
borderBottom: settingsTab === "security" ? "2px solid var(--bg-active)" : "2px solid transparent",
|
|
971
|
+
marginBottom: "-1px",
|
|
972
|
+
}}
|
|
973
|
+
onClick={() => setSettingsTab("security")}
|
|
974
|
+
>
|
|
975
|
+
Security
|
|
976
|
+
</button>
|
|
977
|
+
</div>
|
|
978
|
+
|
|
979
|
+
{/* Tab content — scrollable */}
|
|
980
|
+
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
981
|
+
{settingsTab === "general" && (
|
|
982
|
+
<div className="space-y-5">
|
|
983
|
+
{/* Default Model */}
|
|
984
|
+
<div>
|
|
985
|
+
<label
|
|
986
|
+
className="block text-caption font-bold mb-1.5"
|
|
987
|
+
style={{ color: "var(--text-secondary)" }}
|
|
988
|
+
>
|
|
989
|
+
Default Model
|
|
990
|
+
</label>
|
|
991
|
+
<ModelSelect
|
|
992
|
+
value={state.defaultModel ?? ""}
|
|
993
|
+
onChange={handleDefaultModelChange}
|
|
994
|
+
models={state.models}
|
|
995
|
+
placeholder="Not set (use agent default)"
|
|
996
|
+
/>
|
|
997
|
+
<p className="text-tiny mt-1.5" style={{ color: "var(--text-muted)" }}>
|
|
998
|
+
Default model for new conversations. You can override per session using{" "}
|
|
999
|
+
<code>/model</code> or per automation in its settings.
|
|
1000
|
+
</p>
|
|
1001
|
+
</div>
|
|
1002
|
+
|
|
1003
|
+
{/* Current Session Model */}
|
|
1004
|
+
<div>
|
|
1005
|
+
<label
|
|
1006
|
+
className="block text-caption font-bold mb-1.5"
|
|
1007
|
+
style={{ color: "var(--text-secondary)" }}
|
|
1008
|
+
>
|
|
1009
|
+
Current Session Model
|
|
1010
|
+
</label>
|
|
1011
|
+
<span
|
|
1012
|
+
className="text-body font-mono"
|
|
1013
|
+
style={{ color: (state.sessionModel || state.defaultModel) ? "var(--text-primary)" : "var(--text-muted)" }}
|
|
1014
|
+
>
|
|
1015
|
+
{state.sessionModel ?? state.defaultModel ?? "Not connected"}
|
|
1016
|
+
</span>
|
|
1017
|
+
</div>
|
|
1018
|
+
</div>
|
|
1019
|
+
)}
|
|
1020
|
+
|
|
1021
|
+
{settingsTab === "connection" && (
|
|
1022
|
+
<ConnectionSettings />
|
|
1023
|
+
)}
|
|
1024
|
+
|
|
1025
|
+
{settingsTab === "security" && (
|
|
1026
|
+
<E2ESettings />
|
|
1027
|
+
)}
|
|
845
1028
|
</div>
|
|
846
1029
|
|
|
847
1030
|
<div
|
|
848
|
-
className="mt-
|
|
1031
|
+
className="mt-4 pt-4 flex justify-end"
|
|
849
1032
|
style={{ borderTop: "1px solid var(--border)" }}
|
|
850
1033
|
>
|
|
851
1034
|
<button
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Analytics (GA4). Only loads when VITE_GA_MEASUREMENT_ID is set (e.g. in .env.production).
|
|
3
|
+
* Set VITE_GA_MEASUREMENT_ID to your GA4 Measurement ID (e.g. G-XXXXXXXXXX) to enable.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
dataLayer: unknown[];
|
|
9
|
+
gtag?: (...args: unknown[]) => void;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID as string | undefined;
|
|
14
|
+
|
|
15
|
+
function loadGtag(): boolean {
|
|
16
|
+
if (!MEASUREMENT_ID || typeof window === "undefined") return false;
|
|
17
|
+
if (window.gtag) return true;
|
|
18
|
+
|
|
19
|
+
window.dataLayer = window.dataLayer || [];
|
|
20
|
+
window.gtag = function gtag() {
|
|
21
|
+
window.dataLayer.push(arguments);
|
|
22
|
+
};
|
|
23
|
+
window.gtag("js", new Date());
|
|
24
|
+
window.gtag("config", MEASUREMENT_ID);
|
|
25
|
+
|
|
26
|
+
const script = document.createElement("script");
|
|
27
|
+
script.async = true;
|
|
28
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${MEASUREMENT_ID}`;
|
|
29
|
+
document.head.appendChild(script);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let initialized = false;
|
|
34
|
+
|
|
35
|
+
export function initAnalytics(): void {
|
|
36
|
+
if (initialized) return;
|
|
37
|
+
initialized = loadGtag();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isAnalyticsEnabled(): boolean {
|
|
41
|
+
return initialized && !!window.gtag;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Send a page_view or custom event to GA4. Use after route/view changes in the SPA.
|
|
46
|
+
*/
|
|
47
|
+
export function gtagEvent(name: string, params?: Record<string, string>): void {
|
|
48
|
+
if (!MEASUREMENT_ID || !window.gtag) return;
|
|
49
|
+
window.gtag("event", name, params);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Track a virtual page view (e.g. "Messages" / "Automations" tab).
|
|
54
|
+
*/
|
|
55
|
+
export function gtagPageView(page: string): void {
|
|
56
|
+
gtagEvent("page_view", { page_path: `/${page}`, page_title: page });
|
|
57
|
+
}
|