botschat 0.1.8 → 0.1.9

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.
@@ -28,7 +28,7 @@
28
28
  <link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet" />
29
29
 
30
30
  <title>BotsChat</title>
31
- <script type="module" crossorigin src="/assets/index-C-FpELeN.js"></script>
31
+ <script type="module" crossorigin src="/assets/index-BcHAQzqW.js"></script>
32
32
  <link rel="stylesheet" crossorigin href="/assets/index-B1sFqYiM.css">
33
33
  </head>
34
34
  <body>
@@ -30,6 +30,7 @@ import { MobileLayout } from "./components/MobileLayout";
30
30
  import { dlog } from "./debug-log";
31
31
  import { E2eService } from "./e2e";
32
32
  import { gtagPageView } from "./analytics";
33
+ import { randomUUID } from "./utils/uuid";
33
34
 
34
35
  export default function App() {
35
36
  const [state, dispatch] = useReducer(appReducer, initialState, (init): AppState => {
@@ -506,7 +507,7 @@ export default function App() {
506
507
  // from the server when the user navigates to that session.
507
508
  if (!isCurrentSession(sessionKey)) break;
508
509
  const chatMsg: ChatMessage = {
509
- id: crypto.randomUUID(),
510
+ id: randomUUID(),
510
511
  sender: "agent",
511
512
  text: msg.text as string,
512
513
  timestamp: Date.now(),
@@ -523,7 +524,7 @@ export default function App() {
523
524
  case "agent.media": {
524
525
  if (!isCurrentSession(sessionKey)) break;
525
526
  const mediaMsg: ChatMessage = {
526
- id: crypto.randomUUID(),
527
+ id: randomUUID(),
527
528
  sender: "agent",
528
529
  text: (msg.caption as string) ?? "",
529
530
  mediaUrl: msg.mediaUrl as string,
@@ -541,7 +542,7 @@ export default function App() {
541
542
  case "agent.a2ui": {
542
543
  if (!isCurrentSession(sessionKey)) break;
543
544
  const a2uiMsg: ChatMessage = {
544
- id: crypto.randomUUID(),
545
+ id: randomUUID(),
545
546
  sender: "agent",
546
547
  text: "",
547
548
  a2ui: msg.jsonl as string,
@@ -700,7 +701,7 @@ export default function App() {
700
701
  const token = getToken();
701
702
  if (!token) return;
702
703
 
703
- const sessionId = crypto.randomUUID();
704
+ const sessionId = randomUUID();
704
705
  dlog.info("WS", `Connecting WebSocket (session=${sessionId.slice(0, 8)}...)`);
705
706
  const client = new BotsChatWSClient({
706
707
  userId: state.user.id,
@@ -5,6 +5,7 @@ import { MessageContent } from "./MessageContent";
5
5
  import { ModelSelect } from "./ModelSelect";
6
6
  import { SessionTabs } from "./SessionTabs";
7
7
  import { dlog } from "../debug-log";
8
+ import { randomUUID } from "../utils/uuid";
8
9
 
9
10
  type ChatWindowProps = {
10
11
  sendMessage: (msg: WSMessage) => void;
@@ -229,7 +230,7 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
229
230
  setSkillVersion((v) => v + 1);
230
231
 
231
232
  const msg: ChatMessage = {
232
- id: crypto.randomUUID(),
233
+ id: randomUUID(),
233
234
  sender: "user",
234
235
  text: `/model ${modelId}`,
235
236
  timestamp: Date.now(),
@@ -381,7 +382,7 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
381
382
  }
382
383
 
383
384
  const msg: ChatMessage = {
384
- id: crypto.randomUUID(),
385
+ id: randomUUID(),
385
386
  sender: "user",
386
387
  text: trimmed,
387
388
  timestamp: Date.now(),
@@ -416,7 +417,7 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
416
417
  if (!sessionKey) return;
417
418
  dlog.info("A2UI", `Action triggered: ${action}`);
418
419
  const msg: ChatMessage = {
419
- id: crypto.randomUUID(),
420
+ id: randomUUID(),
420
421
  sender: "user",
421
422
  text: action,
422
423
  timestamp: Date.now(),
@@ -448,7 +449,7 @@ export function ChatWindow({ sendMessage }: ChatWindowProps) {
448
449
  // Send the chosen label as a user message (show the readable label, not the
449
450
  // technical value, so the chat history reads naturally)
450
451
  const msg: ChatMessage = {
451
- id: crypto.randomUUID(),
452
+ id: randomUUID(),
452
453
  sender: "user",
453
454
  text: label,
454
455
  timestamp: Date.now(),
@@ -861,4 +862,3 @@ function ActionButton({
861
862
  </button>
862
863
  );
863
864
  }
864
-
@@ -77,6 +77,9 @@ export function LoginPage() {
77
77
  }
78
78
  dlog.info("Auth", `${isRegister ? "Register" : "Login"} success — user ${res.id} (${res.email})`);
79
79
  handleAuthSuccess(res);
80
+ if (isRegister) {
81
+ localStorage.setItem("botschat_onboarding_dismissed", "1");
82
+ }
80
83
  } catch (err) {
81
84
  const message = err instanceof Error ? err.message : "Something went wrong";
82
85
  dlog.error("Auth", `${isRegister ? "Register" : "Login"} failed: ${message}`);
@@ -10,6 +10,7 @@ import { CronSidebar } from "./CronSidebar";
10
10
  import { CronDetail } from "./CronDetail";
11
11
  import { ModelSelect } from "./ModelSelect";
12
12
  import { ConnectionSettings } from "./ConnectionSettings";
13
+ import { E2ESettings } from "./E2ESettings";
13
14
  import { dlog } from "../debug-log";
14
15
 
15
16
  type MobileScreen =
@@ -258,6 +259,17 @@ export function MobileLayout({
258
259
  )}
259
260
  {theme === "dark" ? "Light Mode" : "Dark Mode"}
260
261
  </button>
262
+ <button
263
+ className="w-full text-left px-4 py-2.5 text-body flex items-center gap-2.5"
264
+ style={{ color: "var(--text-primary)" }}
265
+ onClick={() => { onOpenSettings(); setShowUserMenu(false); }}
266
+ >
267
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
268
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
269
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
270
+ </svg>
271
+ Settings
272
+ </button>
261
273
  <div style={{ borderTop: "1px solid var(--border)" }} />
262
274
  <button
263
275
  className="w-full text-left px-4 py-2.5 text-body flex items-center gap-2.5"
@@ -277,7 +289,7 @@ export function MobileLayout({
277
289
  <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
278
290
  {screen === "channel-list" && (
279
291
  <div className="flex-1 min-h-0 overflow-y-auto" style={{ background: "var(--bg-secondary)" }}>
280
- <Sidebar />
292
+ <Sidebar onOpenSettings={onOpenSettings} />
281
293
  </div>
282
294
  )}
283
295
 
@@ -360,7 +372,7 @@ function MobileSettingsModal({
360
372
  onClose: () => void;
361
373
  handleDefaultModelChange: (modelId: string) => Promise<void>;
362
374
  }) {
363
- const [tab, setTab] = useState<"general" | "connection">("general");
375
+ const [tab, setTab] = useState<"general" | "connection" | "security">("general");
364
376
 
365
377
  return (
366
378
  <div
@@ -405,6 +417,17 @@ function MobileSettingsModal({
405
417
  >
406
418
  Connection
407
419
  </button>
420
+ <button
421
+ className="pb-2 text-caption font-bold transition-colors"
422
+ style={{
423
+ color: tab === "security" ? "var(--text-primary)" : "var(--text-muted)",
424
+ borderBottom: tab === "security" ? "2px solid var(--bg-active)" : "2px solid transparent",
425
+ marginBottom: "-1px",
426
+ }}
427
+ onClick={() => setTab("security")}
428
+ >
429
+ Security
430
+ </button>
408
431
  </div>
409
432
 
410
433
  {/* Tab content — scrollable */}
@@ -440,6 +463,10 @@ function MobileSettingsModal({
440
463
  {tab === "connection" && (
441
464
  <ConnectionSettings />
442
465
  )}
466
+
467
+ {tab === "security" && (
468
+ <E2ESettings />
469
+ )}
443
470
  </div>
444
471
 
445
472
  <button
@@ -3,7 +3,7 @@ import { useAppState, useAppDispatch } from "../store";
3
3
  import { agentsApi, channelsApi } from "../api";
4
4
  import { dlog } from "../debug-log";
5
5
 
6
- export function Sidebar() {
6
+ export function Sidebar({ onOpenSettings }: { onOpenSettings?: () => void } = {}) {
7
7
  const state = useAppState();
8
8
  const dispatch = useAppDispatch();
9
9
  const [showCreate, setShowCreate] = useState(false);
@@ -88,6 +88,19 @@ export function Sidebar() {
88
88
  <span className="text-[--text-sidebar-active] font-bold text-h2 truncate flex-1">
89
89
  BotsChat
90
90
  </span>
91
+ {onOpenSettings && (
92
+ <button
93
+ onClick={onOpenSettings}
94
+ className="p-1 rounded transition-colors hover:bg-[--sidebar-hover]"
95
+ style={{ color: "var(--text-sidebar)" }}
96
+ title="Settings"
97
+ >
98
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
99
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
100
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
101
+ </svg>
102
+ </button>
103
+ )}
91
104
  <svg className="w-3 h-3 text-[--text-sidebar]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
92
105
  <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
93
106
  </svg>
@@ -4,6 +4,7 @@ import { messagesApi } from "../api";
4
4
  import type { WSMessage } from "../ws";
5
5
  import { MessageContent } from "./MessageContent";
6
6
  import { dlog } from "../debug-log";
7
+ import { randomUUID } from "../utils/uuid";
7
8
 
8
9
  type ThreadPanelProps = {
9
10
  sendMessage: (msg: WSMessage) => void;
@@ -47,7 +48,7 @@ export function ThreadPanel({ sendMessage }: ThreadPanelProps) {
47
48
  dlog.info("Thread", `Send reply: ${trimmed.length > 120 ? trimmed.slice(0, 120) + "…" : trimmed}`, { threadId: state.activeThreadId });
48
49
 
49
50
  const msg: ChatMessage = {
50
- id: crypto.randomUUID(),
51
+ id: randomUUID(),
51
52
  sender: "user",
52
53
  text: trimmed,
53
54
  timestamp: Date.now(),
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Generate a UUID v4. Uses crypto.randomUUID() when available (HTTPS),
3
+ * otherwise falls back to crypto.getRandomValues() for HTTP/insecure contexts
4
+ * where randomUUID is not defined.
5
+ */
6
+ export function randomUUID(): string {
7
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
8
+ return crypto.randomUUID();
9
+ }
10
+ // Fallback for insecure context (e.g. http://0.0.0.0:8787): UUID v4 via getRandomValues
11
+ const bytes = new Uint8Array(16);
12
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
13
+ crypto.getRandomValues(bytes);
14
+ } else {
15
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
16
+ }
17
+ bytes[6] = (bytes[6]! & 0x0f) | 0x40;
18
+ bytes[8] = (bytes[8]! & 0x3f) | 0x80;
19
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
20
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
21
+ }