@iota-uz/sdk 0.4.23 → 0.4.25

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.
@@ -2454,6 +2454,33 @@ var ChatMachine = class {
2454
2454
  });
2455
2455
  }
2456
2456
  }
2457
+ async _resumeAcceptedRunOrPoll(sessionId, runId) {
2458
+ setRunMarker(sessionId, runId);
2459
+ try {
2460
+ await this._runResumeStream(sessionId, runId);
2461
+ } catch (err) {
2462
+ if (this.disposed) {
2463
+ return;
2464
+ }
2465
+ console.warn("[ChatMachine] resumeStream failed, switching to status polling fallback:", err);
2466
+ const getStreamStatus2 = this.dataSource.getStreamStatus;
2467
+ const status = getStreamStatus2 ? await getStreamStatus2(sessionId).catch(() => null) : null;
2468
+ if (!status?.active) {
2469
+ clearRunMarker(sessionId);
2470
+ await this._syncSessionFromServer(sessionId, true).catch(() => {
2471
+ });
2472
+ this._updateMessaging({ generationInProgress: false });
2473
+ return;
2474
+ }
2475
+ setRunMarker(sessionId, status.runId ?? runId);
2476
+ this._updateMessaging({
2477
+ isStreaming: false,
2478
+ loading: false,
2479
+ generationInProgress: true
2480
+ });
2481
+ this._startPassivePolling(sessionId);
2482
+ }
2483
+ }
2457
2484
  // =====================================================================
2458
2485
  // Private — actions
2459
2486
  // =====================================================================
@@ -2572,20 +2599,28 @@ var ChatMachine = class {
2572
2599
  streamingContent: ""
2573
2600
  });
2574
2601
  try {
2575
- const compactResult = await this.dataSource.compactSessionHistory(curSessionId);
2576
- const summary = compactResult.summary || "";
2602
+ const accepted = await this.dataSource.compactSessionHistory(curSessionId);
2603
+ if (!accepted.runId) {
2604
+ throw new Error("Async compaction run metadata is missing");
2605
+ }
2577
2606
  this._updateMessaging({
2578
- turns: applyTurnLifecycleForPendingQuestion([createCompactedSystemTurn(curSessionId, summary)], null),
2607
+ turns: applyTurnLifecycleForPendingQuestion(
2608
+ [createCompactedSystemTurn(curSessionId, "Compacting conversation history...")],
2609
+ null
2610
+ ),
2579
2611
  pendingQuestion: null
2580
2612
  });
2581
- const result = await this.dataSource.fetchSession(curSessionId);
2582
- if (result) {
2583
- this._updateSession({ session: result.session });
2584
- this._setTurnsFromFetch(result.turns, result.pendingQuestion || null);
2585
- } else {
2586
- this._setTurnsFromFetch([], null);
2613
+ await this._resumeAcceptedRunOrPoll(curSessionId, accepted.runId);
2614
+ if (!this.state.messaging.generationInProgress) {
2615
+ const result = await this.dataSource.fetchSession(curSessionId);
2616
+ if (result) {
2617
+ this._updateSession({ session: result.session });
2618
+ this._setTurnsFromFetch(result.turns, result.pendingQuestion || null);
2619
+ } else {
2620
+ this._setTurnsFromFetch([], null);
2621
+ }
2622
+ this._updateMessaging({ codeOutputs: [] });
2587
2623
  }
2588
- this._updateMessaging({ codeOutputs: [] });
2589
2624
  } catch (err) {
2590
2625
  const normalized = normalizeRPCError(err, "Failed to compact session history");
2591
2626
  this._updateInput({ inputError: normalized.userMessage });
@@ -2750,8 +2785,6 @@ var ChatMachine = class {
2750
2785
  this._notifySessionsUpdated("session_created", targetSessionId);
2751
2786
  if (this.onSessionCreated) {
2752
2787
  this.onSessionCreated(targetSessionId);
2753
- } else {
2754
- this.dataSource.navigateToSession?.(targetSessionId);
2755
2788
  }
2756
2789
  }
2757
2790
  this._clearStreamError();
@@ -3037,9 +3070,24 @@ var ChatMachine = class {
3037
3070
  return;
3038
3071
  }
3039
3072
  if (result.success) {
3073
+ this._updateMessaging({
3074
+ pendingQuestion: null,
3075
+ turns: applyTurnLifecycleForPendingQuestion(this.state.messaging.turns, null)
3076
+ });
3040
3077
  if (result.data) {
3041
- this._updateSession({ session: result.data.session });
3042
- this._setTurnsFromFetch(result.data.turns, result.data.pendingQuestion || null);
3078
+ await this._resumeAcceptedRunOrPoll(curSessionId, result.data.runId);
3079
+ if (!this.state.messaging.generationInProgress) {
3080
+ const fetchResult = await this.dataSource.fetchSession(curSessionId);
3081
+ if (this.disposed) {
3082
+ return;
3083
+ }
3084
+ if (fetchResult) {
3085
+ this._updateSession({ session: fetchResult.session });
3086
+ this._setTurnsFromFetch(fetchResult.turns, fetchResult.pendingQuestion || null);
3087
+ } else {
3088
+ this._updateSession({ error: "Failed to load updated session", errorRetryable: true });
3089
+ }
3090
+ }
3043
3091
  } else if (curSessionId !== "new") {
3044
3092
  const fetchResult = await this.dataSource.fetchSession(curSessionId);
3045
3093
  if (this.disposed) {
@@ -3083,7 +3131,9 @@ var ChatMachine = class {
3083
3131
  pendingQuestion: null,
3084
3132
  turns: applyTurnLifecycleForPendingQuestion(this.state.messaging.turns, null)
3085
3133
  });
3086
- if (curSessionId !== "new") {
3134
+ if (result.data) {
3135
+ await this._resumeAcceptedRunOrPoll(curSessionId, result.data.runId);
3136
+ } else if (curSessionId !== "new") {
3087
3137
  const fetchResult = await this.dataSource.fetchSession(curSessionId);
3088
3138
  if (this.disposed) {
3089
3139
  return;
@@ -3303,12 +3353,151 @@ function useBranding() {
3303
3353
  }, [context.extensions?.branding, t]);
3304
3354
  return branding;
3305
3355
  }
3306
- function ChatHeader({ session, onBack, readOnly, logoSlot, actionsSlot }) {
3356
+ function hashString(str) {
3357
+ let hash = 0;
3358
+ for (let i = 0; i < str.length; i++) {
3359
+ const char = str.charCodeAt(i);
3360
+ hash = (hash << 5) - hash + char;
3361
+ hash = hash & hash;
3362
+ }
3363
+ return Math.abs(hash);
3364
+ }
3365
+ var colorPalette = [
3366
+ { bg: "bg-blue-500", text: "text-white" },
3367
+ { bg: "bg-green-500", text: "text-white" },
3368
+ { bg: "bg-purple-500", text: "text-white" },
3369
+ { bg: "bg-pink-500", text: "text-white" },
3370
+ { bg: "bg-indigo-500", text: "text-white" },
3371
+ { bg: "bg-teal-500", text: "text-white" },
3372
+ { bg: "bg-orange-500", text: "text-white" },
3373
+ { bg: "bg-cyan-500", text: "text-white" },
3374
+ { bg: "bg-amber-500", text: "text-white" },
3375
+ { bg: "bg-lime-500", text: "text-white" }
3376
+ ];
3377
+ var sizeClasses = {
3378
+ xs: "w-6 h-6 text-[10px]",
3379
+ sm: "w-8 h-8 text-xs",
3380
+ md: "w-10 h-10 text-sm",
3381
+ lg: "w-12 h-12 text-base"
3382
+ };
3383
+ function UserAvatar({
3384
+ firstName,
3385
+ lastName,
3386
+ initials: providedInitials,
3387
+ size = "md",
3388
+ className = ""
3389
+ }) {
3390
+ const derivedInitials = (() => {
3391
+ const firstChar = firstName?.trim()?.charAt(0) || "";
3392
+ const lastChar = lastName?.trim()?.charAt(0) || "";
3393
+ const combined = `${firstChar}${lastChar}`.trim();
3394
+ return combined || "U";
3395
+ })();
3396
+ const initials = (providedInitials?.trim() || derivedInitials).toUpperCase();
3397
+ const fullName = `${firstName}${lastName}`;
3398
+ const colorIndex = hashString(fullName) % colorPalette.length;
3399
+ const colors = colorPalette[colorIndex];
3400
+ return /* @__PURE__ */ jsxRuntime.jsx(
3401
+ "div",
3402
+ {
3403
+ className: `
3404
+ ${sizeClasses[size]}
3405
+ ${colors.bg}
3406
+ ${colors.text}
3407
+ ${className}
3408
+ rounded-full
3409
+ flex
3410
+ items-center
3411
+ justify-center
3412
+ font-semibold
3413
+ flex-shrink-0
3414
+ select-none
3415
+ `,
3416
+ "aria-label": `${firstName} ${lastName}`,
3417
+ title: `${firstName} ${lastName}`,
3418
+ children: initials
3419
+ }
3420
+ );
3421
+ }
3422
+ var MemoizedUserAvatar = React.memo(UserAvatar);
3423
+ MemoizedUserAvatar.displayName = "UserAvatar";
3424
+ var overlapClasses = {
3425
+ xs: "-ml-1.5",
3426
+ sm: "-ml-2"
3427
+ };
3428
+ var badgeSizeClasses = {
3429
+ xs: "w-6 h-6 text-[10px]",
3430
+ sm: "w-8 h-8 text-xs"
3431
+ };
3432
+ function AvatarStackInner({
3433
+ users,
3434
+ max = 3,
3435
+ size = "sm",
3436
+ onClick,
3437
+ className = ""
3438
+ }) {
3439
+ const visible = users.slice(0, max);
3440
+ const overflow = users.length - max;
3441
+ const interactive = typeof onClick === "function";
3442
+ const overlap = overlapClasses[size];
3443
+ const badgeSize = badgeSizeClasses[size];
3444
+ const handleKeyDown = (e) => {
3445
+ if (interactive && (e.key === "Enter" || e.key === " ")) {
3446
+ e.preventDefault();
3447
+ onClick();
3448
+ }
3449
+ };
3450
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3451
+ "div",
3452
+ {
3453
+ className: `inline-flex items-center ${interactive ? "cursor-pointer transition-opacity hover:opacity-80" : ""} ${className}`,
3454
+ onClick: interactive ? onClick : void 0,
3455
+ onKeyDown: interactive ? handleKeyDown : void 0,
3456
+ role: interactive ? "button" : void 0,
3457
+ tabIndex: interactive ? 0 : void 0,
3458
+ "aria-label": interactive ? `${users.length} members` : void 0,
3459
+ children: [
3460
+ visible.map((user, i) => /* @__PURE__ */ jsxRuntime.jsx(
3461
+ "div",
3462
+ {
3463
+ className: `${i > 0 ? overlap : ""} ring-2 ring-white dark:ring-gray-900 rounded-full`,
3464
+ style: { zIndex: visible.length - i },
3465
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3466
+ MemoizedUserAvatar,
3467
+ {
3468
+ firstName: user.firstName,
3469
+ lastName: user.lastName,
3470
+ initials: user.initials,
3471
+ size
3472
+ }
3473
+ )
3474
+ },
3475
+ `${user.firstName}-${user.lastName}-${i}`
3476
+ )),
3477
+ overflow > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
3478
+ "div",
3479
+ {
3480
+ className: `${overlap} ${badgeSize} rounded-full bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-300 font-medium flex items-center justify-center flex-shrink-0 ring-2 ring-white dark:ring-gray-900`,
3481
+ style: { zIndex: 0 },
3482
+ children: [
3483
+ "+",
3484
+ overflow
3485
+ ]
3486
+ }
3487
+ )
3488
+ ]
3489
+ }
3490
+ );
3491
+ }
3492
+ var AvatarStack = React.memo(AvatarStackInner);
3493
+ AvatarStack.displayName = "AvatarStack";
3494
+ function ChatHeader({ session, onBack, readOnly, logoSlot, actionsSlot, members, onMembersClick }) {
3307
3495
  const { t } = useTranslation();
3308
3496
  const branding = useBranding();
3309
3497
  const BackButton = onBack ? /* @__PURE__ */ jsxRuntime.jsx(
3310
3498
  "button",
3311
3499
  {
3500
+ type: "button",
3312
3501
  onClick: onBack,
3313
3502
  className: "cursor-pointer p-2 hover:bg-gray-100 dark:hover:bg-gray-700 active:bg-gray-200 dark:active:bg-gray-600 rounded-lg transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50",
3314
3503
  "aria-label": t("BiChat.Chat.GoBack"),
@@ -3327,23 +3516,42 @@ function ChatHeader({ session, onBack, readOnly, logoSlot, actionsSlot }) {
3327
3516
  ] }) });
3328
3517
  }
3329
3518
  const resolvedSessionTitle = session.title?.trim() || t("BiChat.Chat.NewChat");
3519
+ const isGroupSession = Boolean(session.isGroup || session.memberCount && session.memberCount > 1);
3520
+ const memberCount = session.memberCount ?? 0;
3521
+ const stackUsers = members && members.length > 0 ? members : [];
3330
3522
  return /* @__PURE__ */ jsxRuntime.jsx("header", { className: "bichat-header border-b border-gray-200 dark:border-gray-700 px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
3331
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
3523
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [
3332
3524
  BackButton,
3333
3525
  Logo,
3334
- /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-lg font-semibold text-[var(--bichat-text)]", children: resolvedSessionTitle }),
3335
- session.pinned && /* @__PURE__ */ jsxRuntime.jsx(
3336
- "svg",
3337
- {
3338
- className: "w-4 h-4 text-[var(--bichat-primary)]",
3339
- fill: "currentColor",
3340
- viewBox: "0 0 20 20",
3341
- "aria-label": t("BiChat.Chat.Pinned"),
3342
- children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 2a1 1 0 011 1v1.323l3.954 1.582 1.599-.8a1 1 0 01.894 1.79l-1.233.616 1.738 5.42a1 1 0 01-.285 1.05A3.989 3.989 0 0115 15a3.989 3.989 0 01-2.667-1.019 1 1 0 01-.285-1.05l1.715-5.349L11 6.477V16h2a1 1 0 110 2H7a1 1 0 110-2h2V6.477L6.237 7.582l1.715 5.349a1 1 0 01-.285 1.05A3.989 3.989 0 015 15a3.989 3.989 0 01-2.667-1.019 1 1 0 01-.285-1.05l1.738-5.42-1.233-.617a1 1 0 01.894-1.788l1.599.799L9 4.323V3a1 1 0 011-1z" })
3343
- }
3344
- )
3526
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
3527
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5", children: [
3528
+ /* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-lg font-semibold text-[var(--bichat-text)] truncate", children: resolvedSessionTitle }),
3529
+ session.pinned && /* @__PURE__ */ jsxRuntime.jsx(
3530
+ "svg",
3531
+ {
3532
+ className: "w-4 h-4 text-[var(--bichat-primary)] flex-shrink-0",
3533
+ fill: "currentColor",
3534
+ viewBox: "0 0 20 20",
3535
+ role: "img",
3536
+ "aria-label": t("BiChat.Chat.Pinned"),
3537
+ children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 2a1 1 0 011 1v1.323l3.954 1.582 1.599-.8a1 1 0 01.894 1.79l-1.233.616 1.738 5.42a1 1 0 01-.285 1.05A3.989 3.989 0 0115 15a3.989 3.989 0 01-2.667-1.019 1 1 0 01-.285-1.05l1.715-5.349L11 6.477V16h2a1 1 0 110 2H7a1 1 0 110-2h2V6.477L6.237 7.582l1.715 5.349a1 1 0 01-.285 1.05A3.989 3.989 0 015 15a3.989 3.989 0 01-2.667-1.019 1 1 0 01-.285-1.05l1.738-5.42-1.233-.617a1 1 0 01.894-1.788l1.599.799L9 4.323V3a1 1 0 011-1z" })
3538
+ }
3539
+ ),
3540
+ isGroupSession && stackUsers.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3541
+ AvatarStack,
3542
+ {
3543
+ users: stackUsers,
3544
+ max: 3,
3545
+ size: "xs",
3546
+ onClick: onMembersClick,
3547
+ className: "flex-shrink-0"
3548
+ }
3549
+ )
3550
+ ] }),
3551
+ isGroupSession && memberCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-600 dark:text-gray-400 mt-0.5", children: memberCount === 1 ? t("BiChat.Chat.OneMember") : t("BiChat.Chat.MemberCount").replace("{{count}}", String(memberCount)) })
3552
+ ] })
3345
3553
  ] }),
3346
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3554
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
3347
3555
  readOnly && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2 py-1 text-xs bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-200 rounded", children: t("BiChat.Chat.ReadOnly") }),
3348
3556
  session.status === "archived" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "px-2 py-1 text-xs bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded", children: t("BiChat.Chat.Archived") }),
3349
3557
  actionsSlot
@@ -4218,6 +4426,7 @@ function UserMessage({
4218
4426
  turn,
4219
4427
  turnId,
4220
4428
  initials = "U",
4429
+ authorName,
4221
4430
  slots,
4222
4431
  classNames: classNameOverrides,
4223
4432
  onCopy,
@@ -4425,6 +4634,16 @@ function UserMessage({
4425
4634
  };
4426
4635
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes.root, children: [
4427
4636
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes.wrapper, children: [
4637
+ authorName && /* @__PURE__ */ jsxRuntime.jsx(
4638
+ "span",
4639
+ {
4640
+ id: `${turn.id}-author`,
4641
+ role: "note",
4642
+ "aria-label": authorName,
4643
+ className: "mb-1 px-1 text-[11px] text-right text-gray-500 dark:text-gray-400",
4644
+ children: authorName
4645
+ }
4646
+ ),
4428
4647
  normalizedAttachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.attachments, children: renderSlot(
4429
4648
  slots?.attachments,
4430
4649
  attachmentsSlotProps,
@@ -4436,20 +4655,28 @@ function UserMessage({
4436
4655
  }
4437
4656
  )
4438
4657
  ) }),
4439
- turn.content && /* @__PURE__ */ jsxRuntime.jsx("div", { ref: bubbleRef, className: classes.bubble, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.content, children: isEditing ? /* @__PURE__ */ jsxRuntime.jsx(
4440
- EditForm,
4658
+ turn.content && /* @__PURE__ */ jsxRuntime.jsx(
4659
+ "div",
4441
4660
  {
4442
- draftContent,
4443
- onDraftChange: handleDraftChange,
4444
- onSave: handleEditSave,
4445
- onCancel: handleEditCancel,
4446
- onKeyDown: handleEditKeyDown,
4447
- textareaRef: editTextareaRef,
4448
- disabled: false,
4449
- originalContent: turn.content,
4450
- t
4661
+ ref: bubbleRef,
4662
+ className: classes.bubble,
4663
+ "aria-describedby": authorName ? `${turn.id}-author` : void 0,
4664
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: classes.content, children: isEditing ? /* @__PURE__ */ jsxRuntime.jsx(
4665
+ EditForm,
4666
+ {
4667
+ draftContent,
4668
+ onDraftChange: handleDraftChange,
4669
+ onSave: handleEditSave,
4670
+ onCancel: handleEditCancel,
4671
+ onKeyDown: handleEditKeyDown,
4672
+ textareaRef: editTextareaRef,
4673
+ disabled: false,
4674
+ originalContent: turn.content,
4675
+ t
4676
+ }
4677
+ ) : renderSlot(slots?.content, contentSlotProps, turn.content) })
4451
4678
  }
4452
- ) : renderSlot(slots?.content, contentSlotProps, turn.content) }) }),
4679
+ ),
4453
4680
  !hideActions && /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${classes.actions} ${isCopied ? "opacity-100" : ""}`, children: renderSlot(
4454
4681
  slots?.actions,
4455
4682
  actionsSlotProps,
@@ -4498,19 +4725,25 @@ function UserTurnView({
4498
4725
  turn,
4499
4726
  slots,
4500
4727
  classNames,
4501
- initials = "U",
4728
+ initials,
4502
4729
  hideAvatar,
4503
4730
  hideActions,
4504
4731
  hideTimestamp,
4505
- allowEdit
4732
+ allowEdit,
4733
+ showAuthorName = false
4506
4734
  }) {
4507
4735
  const { handleEdit, handleCopy } = useChatMessaging();
4736
+ const author = turn.userTurn.author;
4737
+ const fullName = [author?.firstName || "", author?.lastName || ""].join(" ").trim();
4738
+ const authorName = showAuthorName && fullName.length > 0 ? fullName : void 0;
4739
+ const resolvedInitials = initials ?? author?.initials ?? "U";
4508
4740
  return /* @__PURE__ */ jsxRuntime.jsx(
4509
4741
  UserMessage,
4510
4742
  {
4511
4743
  turn: turn.userTurn,
4512
4744
  turnId: turn.id,
4513
- initials,
4745
+ initials: resolvedInitials,
4746
+ authorName,
4514
4747
  slots,
4515
4748
  classNames,
4516
4749
  onCopy: handleCopy,
@@ -8505,7 +8738,7 @@ function StreamingBubble({ content, normalizedContent }) {
8505
8738
  }
8506
8739
  function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readOnly }) {
8507
8740
  const { t } = useTranslation();
8508
- const { currentSessionId, fetching } = useChatSession();
8741
+ const { session, currentSessionId, fetching } = useChatSession();
8509
8742
  const {
8510
8743
  turns,
8511
8744
  streamingContent,
@@ -8520,6 +8753,7 @@ function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readO
8520
8753
  () => streamingContent ? normalizeStreamingMarkdown(streamingContent) : "",
8521
8754
  [streamingContent]
8522
8755
  );
8756
+ const showAuthorNames = Boolean(session?.isGroup);
8523
8757
  const showEphemeral = showActivityTrace || showTypingIndicator;
8524
8758
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex-1 min-h-0", children: [
8525
8759
  /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "h-full overflow-y-auto overflow-x-hidden px-4 py-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto space-y-6", children: [
@@ -8529,6 +8763,10 @@ function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readO
8529
8763
  const prevDate = index > 0 ? new Date(turns[index - 1].createdAt) : null;
8530
8764
  const showDateSeparator = !!prevDate && !dateFns.isSameDay(turnDate, prevDate);
8531
8765
  const isLast = index === turns.length - 1;
8766
+ const userTurnProps = {
8767
+ allowEdit: readOnly ? false : isLast,
8768
+ showAuthorName: showAuthorNames
8769
+ };
8532
8770
  return /* @__PURE__ */ jsxRuntime.jsxs(React.Fragment, { children: [
8533
8771
  showDateSeparator && /* @__PURE__ */ jsxRuntime.jsx(DateSeparator, { date: turnDate }),
8534
8772
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -8538,7 +8776,7 @@ function MessageList({ renderUserTurn, renderAssistantTurn, thinkingVerbs, readO
8538
8776
  isLastTurn: isLast,
8539
8777
  renderUserTurn,
8540
8778
  renderAssistantTurn,
8541
- userTurnProps: readOnly ? { allowEdit: false } : { allowEdit: isLast },
8779
+ userTurnProps,
8542
8780
  assistantTurnProps: readOnly ? { allowRegenerate: false } : void 0
8543
8781
  }
8544
8782
  )
@@ -10744,37 +10982,655 @@ function SessionArtifactsPanel({
10744
10982
  }
10745
10983
  );
10746
10984
  }
10985
+ var DialogContext = React.createContext(null);
10986
+ var FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
10987
+ function InlineDialog({ open, onClose, className, children }) {
10988
+ const containerRef = React.useRef(null);
10989
+ const previousFocusRef = React.useRef(null);
10990
+ React.useEffect(() => {
10991
+ if (!open) {
10992
+ return;
10993
+ }
10994
+ previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
10995
+ const container = containerRef.current;
10996
+ if (!container) {
10997
+ return;
10998
+ }
10999
+ const getFocusable = () => Array.from(
11000
+ container.querySelectorAll(FOCUSABLE_SELECTOR)
11001
+ );
11002
+ const handler = (e) => {
11003
+ if (e.key === "Escape") {
11004
+ onClose();
11005
+ return;
11006
+ }
11007
+ if (e.key !== "Tab") {
11008
+ return;
11009
+ }
11010
+ const focusable = getFocusable();
11011
+ if (focusable.length === 0) {
11012
+ e.preventDefault();
11013
+ container.focus();
11014
+ return;
11015
+ }
11016
+ const first = focusable[0];
11017
+ const last = focusable[focusable.length - 1];
11018
+ if (e.shiftKey && document.activeElement === first) {
11019
+ e.preventDefault();
11020
+ last.focus();
11021
+ } else if (!e.shiftKey && document.activeElement === last) {
11022
+ e.preventDefault();
11023
+ first.focus();
11024
+ }
11025
+ };
11026
+ (getFocusable()[0] ?? container)?.focus();
11027
+ container.addEventListener("keydown", handler);
11028
+ return () => {
11029
+ container.removeEventListener("keydown", handler);
11030
+ previousFocusRef.current?.focus();
11031
+ };
11032
+ }, [open, onClose]);
11033
+ if (!open) {
11034
+ return null;
11035
+ }
11036
+ return /* @__PURE__ */ jsxRuntime.jsx(DialogContext.Provider, { value: onClose, children: /* @__PURE__ */ jsxRuntime.jsx(
11037
+ "div",
11038
+ {
11039
+ ref: containerRef,
11040
+ className,
11041
+ onClick: onClose,
11042
+ tabIndex: -1,
11043
+ children
11044
+ }
11045
+ ) });
11046
+ }
11047
+ function InlineDialogBackdrop(props) {
11048
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": "true", ...props });
11049
+ }
11050
+ function InlineDialogPanel({
11051
+ children,
11052
+ onClick,
11053
+ ...rest
11054
+ }) {
11055
+ const ref = React.useRef(null);
11056
+ React.useEffect(() => {
11057
+ if (!ref.current) {
11058
+ return;
11059
+ }
11060
+ const target = ref.current.querySelector("[data-autofocus]") ?? ref.current.querySelector(
11061
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
11062
+ );
11063
+ target?.focus();
11064
+ }, []);
11065
+ return /* @__PURE__ */ jsxRuntime.jsx(
11066
+ "div",
11067
+ {
11068
+ ref,
11069
+ role: "dialog",
11070
+ "aria-modal": "true",
11071
+ onClick: (e) => {
11072
+ e.stopPropagation();
11073
+ onClick?.(e);
11074
+ },
11075
+ ...rest,
11076
+ children
11077
+ }
11078
+ );
11079
+ }
11080
+ function InlineDialogTitle(props) {
11081
+ return /* @__PURE__ */ jsxRuntime.jsx("h2", { ...props });
11082
+ }
11083
+ function InlineDialogDescription(props) {
11084
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { ...props });
11085
+ }
10747
11086
 
10748
- // ui/src/bichat/components/StreamError.tsx
11087
+ // ui/src/bichat/components/SessionMembersModal.tsx
10749
11088
  init_useTranslation();
10750
- function StreamError({
10751
- error,
10752
- onRetry,
10753
- onRegenerate,
10754
- onDismiss,
10755
- compact = false
11089
+ init_useTranslation();
11090
+ function ConfirmModalBase({
11091
+ isOpen,
11092
+ title,
11093
+ message,
11094
+ onConfirm,
11095
+ onCancel,
11096
+ confirmText,
11097
+ cancelText,
11098
+ isDanger = false
10756
11099
  }) {
10757
11100
  const { t } = useTranslation();
10758
- return /* @__PURE__ */ jsxRuntime.jsxs(
10759
- framerMotion.motion.div,
10760
- {
10761
- initial: { opacity: 0, y: 10 },
10762
- animate: { opacity: 1, y: 0 },
10763
- exit: { opacity: 0, y: -10 },
10764
- className: `flex items-start gap-3 ${compact ? "px-3 py-2.5" : "px-4 py-3"} bg-red-50 dark:bg-red-950/40 border border-red-200/80 dark:border-red-900/60 rounded-xl shadow-sm`,
10765
- role: "alert",
10766
- children: [
10767
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 mt-0.5 flex items-center justify-center w-7 h-7 rounded-full bg-red-100 dark:bg-red-900/40", children: /* @__PURE__ */ jsxRuntime.jsx(
10768
- react.Warning,
10769
- {
10770
- className: "w-4 h-4 text-red-600 dark:text-red-400",
10771
- weight: "fill"
10772
- }
10773
- ) }),
11101
+ const resolvedConfirmText = confirmText?.trim() ? confirmText : t("BiChat.Common.Confirm");
11102
+ const resolvedCancelText = cancelText?.trim() ? cancelText : t("BiChat.Common.Cancel");
11103
+ return /* @__PURE__ */ jsxRuntime.jsxs(InlineDialog, { open: isOpen, onClose: onCancel, className: "relative z-40", children: [
11104
+ /* @__PURE__ */ jsxRuntime.jsx(InlineDialogBackdrop, { className: "fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm transition-opacity duration-200" }),
11105
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 flex items-center justify-center z-50 p-4", children: /* @__PURE__ */ jsxRuntime.jsxs(InlineDialogPanel, { className: "bg-white dark:bg-gray-800 rounded-2xl shadow-xl dark:shadow-2xl dark:shadow-black/30 max-w-sm w-full overflow-hidden", children: [
11106
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 pt-6 pb-5", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-4", children: [
11107
+ isDanger && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-xl bg-red-50 dark:bg-red-950/40 border border-red-200/60 dark:border-red-800/40", children: /* @__PURE__ */ jsxRuntime.jsx(react.WarningCircle, { size: 22, weight: "duotone", className: "text-red-600 dark:text-red-400" }) }),
10774
11108
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
10775
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-200 leading-snug", children: t("BiChat.Error.Generic") }),
10776
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-red-600/80 dark:text-red-400/70 break-words leading-relaxed", children: error }),
10777
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
11109
+ /* @__PURE__ */ jsxRuntime.jsx(InlineDialogTitle, { className: "text-base font-semibold text-gray-900 dark:text-gray-100 leading-snug", children: title }),
11110
+ /* @__PURE__ */ jsxRuntime.jsx(InlineDialogDescription, { className: "mt-2 text-sm text-gray-600 dark:text-gray-400 leading-relaxed", children: message })
11111
+ ] })
11112
+ ] }) }),
11113
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-2.5 px-6 pb-5", children: [
11114
+ /* @__PURE__ */ jsxRuntime.jsx(
11115
+ "button",
11116
+ {
11117
+ type: "button",
11118
+ onClick: onCancel,
11119
+ ...isDanger ? { "data-autofocus": true } : {},
11120
+ className: "cursor-pointer px-4 py-2 text-sm font-medium rounded-xl text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700/60 hover:bg-gray-200 dark:hover:bg-gray-700 active:bg-gray-250 dark:active:bg-gray-600 transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-800",
11121
+ "data-testid": "confirm-modal-cancel",
11122
+ children: resolvedCancelText
11123
+ }
11124
+ ),
11125
+ /* @__PURE__ */ jsxRuntime.jsx(
11126
+ "button",
11127
+ {
11128
+ type: "button",
11129
+ ...!isDanger ? { "data-autofocus": true } : {},
11130
+ onClick: onConfirm,
11131
+ className: [
11132
+ "cursor-pointer px-4 py-2 text-sm font-medium rounded-xl text-white",
11133
+ "transition-all duration-150 shadow-sm hover:shadow",
11134
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-800",
11135
+ isDanger ? "bg-red-600 hover:bg-red-700 active:bg-red-800 focus-visible:ring-red-500/50" : "bg-primary-600 hover:bg-primary-700 active:bg-primary-800 focus-visible:ring-primary-500/50"
11136
+ ].join(" "),
11137
+ "data-testid": "confirm-modal-confirm",
11138
+ children: resolvedConfirmText
11139
+ }
11140
+ )
11141
+ ] })
11142
+ ] }) })
11143
+ ] });
11144
+ }
11145
+ var ConfirmModal = React.memo(ConfirmModalBase);
11146
+ ConfirmModal.displayName = "ConfirmModal";
11147
+ var ConfirmModal_default = ConfirmModal;
11148
+ var ROLES = ["editor", "viewer"];
11149
+ function RoleSegmentedControl({
11150
+ value,
11151
+ onChange,
11152
+ disabled,
11153
+ size = "md",
11154
+ t
11155
+ }) {
11156
+ const btnBase = size === "sm" ? "px-2 py-0.5 text-[11px]" : "px-3 py-1 text-xs";
11157
+ const currentIndex = ROLES.indexOf(value);
11158
+ const handleKeyDown = (e) => {
11159
+ if (disabled) {
11160
+ return;
11161
+ }
11162
+ let nextIndex = null;
11163
+ if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
11164
+ e.preventDefault();
11165
+ nextIndex = currentIndex <= 0 ? ROLES.length - 1 : currentIndex - 1;
11166
+ } else if (e.key === "ArrowRight" || e.key === "ArrowDown") {
11167
+ e.preventDefault();
11168
+ nextIndex = currentIndex >= ROLES.length - 1 ? 0 : currentIndex + 1;
11169
+ }
11170
+ if (nextIndex !== null) {
11171
+ const nextRole = ROLES[nextIndex];
11172
+ onChange(nextRole);
11173
+ const target = e.currentTarget.querySelector(`[data-role="${nextRole}"]`);
11174
+ target?.focus();
11175
+ }
11176
+ };
11177
+ return /* @__PURE__ */ jsxRuntime.jsx(
11178
+ "div",
11179
+ {
11180
+ role: "radiogroup",
11181
+ "aria-label": t("BiChat.Share.RoleLabel"),
11182
+ className: "inline-flex rounded-lg border border-gray-200 dark:border-gray-700 p-0.5 bg-gray-50 dark:bg-gray-800/50",
11183
+ onKeyDown: handleKeyDown,
11184
+ children: ROLES.map((role) => /* @__PURE__ */ jsxRuntime.jsx(
11185
+ "button",
11186
+ {
11187
+ type: "button",
11188
+ role: "radio",
11189
+ "aria-checked": value === role,
11190
+ tabIndex: value === role ? 0 : -1,
11191
+ "data-role": role,
11192
+ disabled,
11193
+ onClick: () => onChange(role),
11194
+ className: `${btnBase} cursor-pointer rounded-md font-medium transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 disabled:opacity-50 disabled:cursor-not-allowed ${value === role ? "bg-primary-600 text-white shadow-sm" : "text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"}`,
11195
+ children: role === "editor" ? t("BiChat.Share.RoleEditor") : t("BiChat.Share.RoleViewer")
11196
+ },
11197
+ role
11198
+ ))
11199
+ }
11200
+ );
11201
+ }
11202
+ function MemberSkeleton() {
11203
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 rounded-xl px-3 py-2.5", "aria-hidden": "true", children: [
11204
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 animate-pulse flex-shrink-0" }),
11205
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 space-y-1.5", children: [
11206
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-28 rounded bg-gray-200 dark:bg-gray-700 animate-pulse" }),
11207
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-2.5 w-16 rounded bg-gray-100 dark:bg-gray-800 animate-pulse" })
11208
+ ] }),
11209
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-6 w-16 rounded-lg bg-gray-200 dark:bg-gray-700 animate-pulse" })
11210
+ ] });
11211
+ }
11212
+ function SessionMembersModal({ isOpen, sessionId, dataSource, onClose }) {
11213
+ const headingId = React.useId();
11214
+ const { t } = useTranslation();
11215
+ const statusTimerRef = React.useRef();
11216
+ const [loading, setLoading] = React.useState(false);
11217
+ const [saving, setSaving] = React.useState(false);
11218
+ const [error, setError] = React.useState(null);
11219
+ const [users, setUsers] = React.useState([]);
11220
+ const [members, setMembers] = React.useState([]);
11221
+ const [selectedUser, setSelectedUser] = React.useState(null);
11222
+ const [selectedRole, setSelectedRole] = React.useState("editor");
11223
+ const [query, setQuery] = React.useState("");
11224
+ const [confirmRemove, setConfirmRemove] = React.useState(null);
11225
+ const [statusMessage, setStatusMessage] = React.useState(null);
11226
+ const [dropdownOpen, setDropdownOpen] = React.useState(false);
11227
+ const [dropdownHighlightIndex, setDropdownHighlightIndex] = React.useState(0);
11228
+ const dropdownOptionRefs = React.useRef([]);
11229
+ const canManageMembers = Boolean(
11230
+ dataSource.listUsers && dataSource.listSessionMembers && dataSource.addSessionMember && dataSource.updateSessionMemberRole && dataSource.removeSessionMember
11231
+ );
11232
+ const refresh = React.useCallback(async () => {
11233
+ if (!sessionId || !canManageMembers) {
11234
+ return;
11235
+ }
11236
+ setLoading(true);
11237
+ setError(null);
11238
+ try {
11239
+ const [usersData, membersData] = await Promise.all([
11240
+ dataSource.listUsers(),
11241
+ dataSource.listSessionMembers(sessionId)
11242
+ ]);
11243
+ setUsers(usersData);
11244
+ setMembers(membersData);
11245
+ } catch {
11246
+ setError(t("BiChat.Share.LoadFailed"));
11247
+ } finally {
11248
+ setLoading(false);
11249
+ }
11250
+ }, [canManageMembers, dataSource, sessionId, t]);
11251
+ React.useEffect(() => {
11252
+ if (!isOpen) {
11253
+ return;
11254
+ }
11255
+ void refresh();
11256
+ }, [isOpen, refresh]);
11257
+ React.useEffect(() => {
11258
+ if (!isOpen) {
11259
+ setQuery("");
11260
+ setSelectedUser(null);
11261
+ setSelectedRole("editor");
11262
+ setError(null);
11263
+ setConfirmRemove(null);
11264
+ setStatusMessage(null);
11265
+ setDropdownOpen(false);
11266
+ setDropdownHighlightIndex(0);
11267
+ }
11268
+ }, [isOpen]);
11269
+ const memberIDs = React.useMemo(() => new Set(members.map((m) => m.user.id)), [members]);
11270
+ const availableUsers = React.useMemo(
11271
+ () => users.filter((user) => !memberIDs.has(user.id)),
11272
+ [users, memberIDs]
11273
+ );
11274
+ const filteredUsers = React.useMemo(() => {
11275
+ if (!query.trim()) {
11276
+ return availableUsers;
11277
+ }
11278
+ const q = query.toLowerCase();
11279
+ return availableUsers.filter(
11280
+ (u) => u.firstName.toLowerCase().includes(q) || u.lastName.toLowerCase().includes(q) || `${u.firstName} ${u.lastName}`.toLowerCase().includes(q)
11281
+ );
11282
+ }, [availableUsers, query]);
11283
+ React.useEffect(() => {
11284
+ setDropdownHighlightIndex(
11285
+ (i) => Math.min(Math.max(0, i), Math.max(0, filteredUsers.length - 1))
11286
+ );
11287
+ }, [filteredUsers.length]);
11288
+ React.useEffect(() => () => clearTimeout(statusTimerRef.current), []);
11289
+ const flashStatus = React.useCallback((msg) => {
11290
+ clearTimeout(statusTimerRef.current);
11291
+ setStatusMessage(msg);
11292
+ statusTimerRef.current = setTimeout(() => setStatusMessage(null), 3e3);
11293
+ }, []);
11294
+ const handleAdd = React.useCallback(async () => {
11295
+ if (!sessionId || !selectedUser || !dataSource.addSessionMember) {
11296
+ return;
11297
+ }
11298
+ setSaving(true);
11299
+ setError(null);
11300
+ try {
11301
+ await dataSource.addSessionMember(sessionId, selectedUser.id, selectedRole);
11302
+ setSelectedUser(null);
11303
+ setQuery("");
11304
+ flashStatus(t("BiChat.Share.MemberAdded"));
11305
+ await refresh();
11306
+ } catch {
11307
+ setError(t("BiChat.Share.AddFailed"));
11308
+ } finally {
11309
+ setSaving(false);
11310
+ }
11311
+ }, [sessionId, selectedUser, selectedRole, dataSource.addSessionMember, refresh, t, flashStatus]);
11312
+ const handleUpdateRole = React.useCallback(async (userId, role) => {
11313
+ if (!sessionId || !dataSource.updateSessionMemberRole) {
11314
+ return;
11315
+ }
11316
+ setSaving(true);
11317
+ setError(null);
11318
+ try {
11319
+ await dataSource.updateSessionMemberRole(sessionId, userId, role);
11320
+ await refresh();
11321
+ } catch {
11322
+ setError(t("BiChat.Share.UpdateFailed"));
11323
+ } finally {
11324
+ setSaving(false);
11325
+ }
11326
+ }, [sessionId, dataSource.updateSessionMemberRole, refresh]);
11327
+ const handleRemove = React.useCallback(async (userId) => {
11328
+ if (!sessionId || !dataSource.removeSessionMember) {
11329
+ return;
11330
+ }
11331
+ setSaving(true);
11332
+ setError(null);
11333
+ try {
11334
+ await dataSource.removeSessionMember(sessionId, userId);
11335
+ flashStatus(t("BiChat.Share.MemberRemoved"));
11336
+ await refresh();
11337
+ } catch {
11338
+ setError(t("BiChat.Share.RemoveFailed"));
11339
+ } finally {
11340
+ setSaving(false);
11341
+ }
11342
+ }, [sessionId, dataSource.removeSessionMember, refresh, flashStatus]);
11343
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11344
+ /* @__PURE__ */ jsxRuntime.jsxs(InlineDialog, { open: isOpen, onClose, className: "relative z-40", children: [
11345
+ /* @__PURE__ */ jsxRuntime.jsx(InlineDialogBackdrop, { className: "fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm transition-opacity duration-200" }),
11346
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 flex items-center justify-center z-50 p-4", children: /* @__PURE__ */ jsxRuntime.jsxs(
11347
+ InlineDialogPanel,
11348
+ {
11349
+ "aria-labelledby": headingId,
11350
+ className: "bg-white dark:bg-gray-800 rounded-2xl shadow-xl dark:shadow-2xl dark:shadow-black/30 max-w-md w-full",
11351
+ children: [
11352
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 pt-5 pb-4", children: [
11353
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
11354
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center w-9 h-9 rounded-xl bg-primary-50 dark:bg-primary-950/40 border border-primary-200/60 dark:border-primary-800/40", children: /* @__PURE__ */ jsxRuntime.jsx(react.UsersThree, { size: 18, weight: "duotone", className: "text-primary-600 dark:text-primary-400" }) }),
11355
+ /* @__PURE__ */ jsxRuntime.jsx(InlineDialogTitle, { id: headingId, className: "text-base font-semibold text-gray-900 dark:text-gray-100", children: t("BiChat.Share.Title") })
11356
+ ] }),
11357
+ /* @__PURE__ */ jsxRuntime.jsx(
11358
+ "button",
11359
+ {
11360
+ type: "button",
11361
+ onClick: onClose,
11362
+ className: "rounded-lg p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 dark:hover:text-gray-300 dark:hover:bg-gray-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50",
11363
+ "aria-label": t("BiChat.Common.Close"),
11364
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.X, { size: 18 })
11365
+ }
11366
+ )
11367
+ ] }),
11368
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 pb-5 space-y-4", children: [
11369
+ !canManageMembers && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border border-amber-200 bg-amber-50 px-3 py-2.5 text-xs text-amber-700 dark:border-amber-800/60 dark:bg-amber-900/20 dark:text-amber-300", children: t("BiChat.Share.Unsupported") }),
11370
+ error && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border border-red-200 bg-red-50 px-3 py-2.5 text-xs text-red-700 dark:border-red-800/60 dark:bg-red-900/20 dark:text-red-300", children: error }),
11371
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: statusMessage }),
11372
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
11373
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400", children: [
11374
+ t("BiChat.Share.Members"),
11375
+ !loading && members.length > 0 ? ` (${members.length})` : ""
11376
+ ] }),
11377
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-h-64 overflow-y-auto -mx-1 px-1 space-y-1", children: loading ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11378
+ /* @__PURE__ */ jsxRuntime.jsx(MemberSkeleton, {}),
11379
+ /* @__PURE__ */ jsxRuntime.jsx(MemberSkeleton, {}),
11380
+ /* @__PURE__ */ jsxRuntime.jsx(MemberSkeleton, {})
11381
+ ] }) : members.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-8 text-gray-400 dark:text-gray-500", children: [
11382
+ /* @__PURE__ */ jsxRuntime.jsx(react.UsersThree, { size: 32, weight: "thin", className: "mb-2" }),
11383
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm", children: t("BiChat.Share.Empty") })
11384
+ ] }) : members.map((member) => /* @__PURE__ */ jsxRuntime.jsxs(
11385
+ "div",
11386
+ {
11387
+ className: "flex items-center gap-3 rounded-xl px-3 py-2 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700/40",
11388
+ children: [
11389
+ /* @__PURE__ */ jsxRuntime.jsx(
11390
+ MemoizedUserAvatar,
11391
+ {
11392
+ firstName: member.user.firstName,
11393
+ lastName: member.user.lastName,
11394
+ initials: member.user.initials,
11395
+ size: "sm"
11396
+ }
11397
+ ),
11398
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "truncate text-sm font-medium text-gray-900 dark:text-gray-100", children: [
11399
+ member.user.firstName,
11400
+ " ",
11401
+ member.user.lastName
11402
+ ] }) }),
11403
+ member.role === "owner" ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-amber-50 dark:bg-amber-900/20 border border-amber-200/60 dark:border-amber-800/40 px-2.5 py-0.5 text-[11px] font-medium text-amber-700 dark:text-amber-300", children: [
11404
+ /* @__PURE__ */ jsxRuntime.jsx(react.Crown, { size: 12, weight: "duotone" }),
11405
+ t("BiChat.Share.RoleOwner")
11406
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
11407
+ /* @__PURE__ */ jsxRuntime.jsx(
11408
+ RoleSegmentedControl,
11409
+ {
11410
+ value: member.role,
11411
+ onChange: (role) => handleUpdateRole(member.user.id, role),
11412
+ disabled: saving,
11413
+ size: "sm",
11414
+ t
11415
+ }
11416
+ ),
11417
+ /* @__PURE__ */ jsxRuntime.jsx(
11418
+ "button",
11419
+ {
11420
+ type: "button",
11421
+ disabled: saving,
11422
+ onClick: () => setConfirmRemove(member),
11423
+ className: "cursor-pointer rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/50",
11424
+ "aria-label": `${t("BiChat.Share.Remove")} ${member.user.firstName} ${member.user.lastName}`,
11425
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.Trash, { size: 14 })
11426
+ }
11427
+ )
11428
+ ] })
11429
+ ]
11430
+ },
11431
+ member.user.id
11432
+ )) })
11433
+ ] }),
11434
+ canManageMembers && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border border-gray-200 dark:border-gray-700 p-3 space-y-3", children: [
11435
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400", children: t("BiChat.Share.AddMember") }),
11436
+ /* @__PURE__ */ jsxRuntime.jsxs(
11437
+ "div",
11438
+ {
11439
+ className: "relative",
11440
+ onBlur: (e) => {
11441
+ if (!e.currentTarget.contains(e.relatedTarget)) {
11442
+ setDropdownOpen(false);
11443
+ }
11444
+ },
11445
+ children: [
11446
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
11447
+ /* @__PURE__ */ jsxRuntime.jsx(
11448
+ react.MagnifyingGlass,
11449
+ {
11450
+ size: 14,
11451
+ className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500"
11452
+ }
11453
+ ),
11454
+ /* @__PURE__ */ jsxRuntime.jsx(
11455
+ "input",
11456
+ {
11457
+ type: "text",
11458
+ className: "w-full rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/60 pl-8 pr-3 py-2 text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 transition-colors focus:border-primary-400 dark:focus:border-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500/20",
11459
+ placeholder: t("BiChat.Share.SearchUsers"),
11460
+ value: selectedUser ? `${selectedUser.firstName} ${selectedUser.lastName}` : query,
11461
+ onFocus: () => {
11462
+ setDropdownOpen(true);
11463
+ setDropdownHighlightIndex(0);
11464
+ if (selectedUser) {
11465
+ setSelectedUser(null);
11466
+ setQuery("");
11467
+ }
11468
+ },
11469
+ onChange: (e) => {
11470
+ setQuery(e.target.value);
11471
+ setSelectedUser(null);
11472
+ setDropdownOpen(true);
11473
+ setDropdownHighlightIndex(0);
11474
+ },
11475
+ onKeyDown: (e) => {
11476
+ if (!dropdownOpen || filteredUsers.length === 0) {
11477
+ if (e.key === "Escape") {
11478
+ setDropdownOpen(false);
11479
+ }
11480
+ return;
11481
+ }
11482
+ if (e.key === "Escape") {
11483
+ e.preventDefault();
11484
+ setDropdownOpen(false);
11485
+ setDropdownHighlightIndex(0);
11486
+ return;
11487
+ }
11488
+ if (e.key === "ArrowDown") {
11489
+ e.preventDefault();
11490
+ const next = (dropdownHighlightIndex + 1) % filteredUsers.length;
11491
+ setDropdownHighlightIndex(next);
11492
+ setTimeout(() => dropdownOptionRefs.current[next]?.focus(), 0);
11493
+ return;
11494
+ }
11495
+ if (e.key === "ArrowUp") {
11496
+ e.preventDefault();
11497
+ const next = dropdownHighlightIndex <= 0 ? filteredUsers.length - 1 : dropdownHighlightIndex - 1;
11498
+ setDropdownHighlightIndex(next);
11499
+ setTimeout(() => dropdownOptionRefs.current[next]?.focus(), 0);
11500
+ return;
11501
+ }
11502
+ if (e.key === "Enter") {
11503
+ e.preventDefault();
11504
+ const user = filteredUsers[dropdownHighlightIndex];
11505
+ if (user) {
11506
+ setSelectedUser(user);
11507
+ setQuery("");
11508
+ setDropdownOpen(false);
11509
+ }
11510
+ }
11511
+ }
11512
+ }
11513
+ )
11514
+ ] }),
11515
+ dropdownOpen && !selectedUser && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute z-10 mt-1 max-h-48 w-full overflow-auto rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: filteredUsers.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-2 text-sm text-gray-500 dark:text-gray-400", children: availableUsers.length === 0 ? t("BiChat.Share.NoUsersAvailable") : t("BiChat.Share.NoSearchResults") }) : filteredUsers.map((user, index) => /* @__PURE__ */ jsxRuntime.jsxs(
11516
+ "button",
11517
+ {
11518
+ type: "button",
11519
+ ref: (el) => {
11520
+ dropdownOptionRefs.current[index] = el;
11521
+ },
11522
+ className: `flex w-full items-center gap-2.5 px-3 py-2 cursor-pointer transition-colors hover:bg-primary-50 dark:hover:bg-primary-900/20 ${index === dropdownHighlightIndex ? "bg-primary-50 dark:bg-primary-900/20" : ""}`,
11523
+ onMouseDown: (e) => e.preventDefault(),
11524
+ onClick: () => {
11525
+ setSelectedUser(user);
11526
+ setQuery("");
11527
+ setDropdownOpen(false);
11528
+ },
11529
+ children: [
11530
+ /* @__PURE__ */ jsxRuntime.jsx(
11531
+ MemoizedUserAvatar,
11532
+ {
11533
+ firstName: user.firstName,
11534
+ lastName: user.lastName,
11535
+ initials: user.initials,
11536
+ size: "xs"
11537
+ }
11538
+ ),
11539
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-sm text-gray-900 dark:text-gray-100", children: [
11540
+ user.firstName,
11541
+ " ",
11542
+ user.lastName
11543
+ ] })
11544
+ ]
11545
+ },
11546
+ user.id
11547
+ )) })
11548
+ ]
11549
+ }
11550
+ ),
11551
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
11552
+ /* @__PURE__ */ jsxRuntime.jsx(
11553
+ RoleSegmentedControl,
11554
+ {
11555
+ value: selectedRole,
11556
+ onChange: setSelectedRole,
11557
+ disabled: saving,
11558
+ t
11559
+ }
11560
+ ),
11561
+ /* @__PURE__ */ jsxRuntime.jsxs(
11562
+ "button",
11563
+ {
11564
+ type: "button",
11565
+ onClick: handleAdd,
11566
+ disabled: saving || !selectedUser,
11567
+ className: "inline-flex cursor-pointer items-center gap-1.5 rounded-xl bg-primary-600 px-3.5 py-2 text-sm font-medium text-white shadow-sm transition-all duration-150 hover:bg-primary-700 hover:shadow active:bg-primary-800 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-800",
11568
+ children: [
11569
+ /* @__PURE__ */ jsxRuntime.jsx(react.UserPlus, { size: 14 }),
11570
+ t("BiChat.Share.Add")
11571
+ ]
11572
+ }
11573
+ )
11574
+ ] })
11575
+ ] })
11576
+ ] })
11577
+ ]
11578
+ }
11579
+ ) })
11580
+ ] }),
11581
+ /* @__PURE__ */ jsxRuntime.jsx(
11582
+ ConfirmModal,
11583
+ {
11584
+ isOpen: !!confirmRemove,
11585
+ isDanger: true,
11586
+ title: t("BiChat.Share.RemoveConfirmTitle"),
11587
+ message: confirmRemove ? t("BiChat.Share.RemoveConfirmMessage").replace(
11588
+ "{{name}}",
11589
+ `${confirmRemove.user.firstName} ${confirmRemove.user.lastName}`
11590
+ ) : "",
11591
+ confirmText: t("BiChat.Share.Remove"),
11592
+ onConfirm: () => {
11593
+ if (confirmRemove) {
11594
+ void handleRemove(confirmRemove.user.id);
11595
+ }
11596
+ setConfirmRemove(null);
11597
+ },
11598
+ onCancel: () => setConfirmRemove(null)
11599
+ }
11600
+ )
11601
+ ] });
11602
+ }
11603
+
11604
+ // ui/src/bichat/components/StreamError.tsx
11605
+ init_useTranslation();
11606
+ function StreamError({
11607
+ error,
11608
+ onRetry,
11609
+ onRegenerate,
11610
+ onDismiss,
11611
+ compact = false
11612
+ }) {
11613
+ const { t } = useTranslation();
11614
+ return /* @__PURE__ */ jsxRuntime.jsxs(
11615
+ framerMotion.motion.div,
11616
+ {
11617
+ initial: { opacity: 0, y: 10 },
11618
+ animate: { opacity: 1, y: 0 },
11619
+ exit: { opacity: 0, y: -10 },
11620
+ className: `flex items-start gap-3 ${compact ? "px-3 py-2.5" : "px-4 py-3"} bg-red-50 dark:bg-red-950/40 border border-red-200/80 dark:border-red-900/60 rounded-xl shadow-sm`,
11621
+ role: "alert",
11622
+ children: [
11623
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 mt-0.5 flex items-center justify-center w-7 h-7 rounded-full bg-red-100 dark:bg-red-900/40", children: /* @__PURE__ */ jsxRuntime.jsx(
11624
+ react.Warning,
11625
+ {
11626
+ className: "w-4 h-4 text-red-600 dark:text-red-400",
11627
+ weight: "fill"
11628
+ }
11629
+ ) }),
11630
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
11631
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-red-800 dark:text-red-200 leading-snug", children: t("BiChat.Error.Generic") }),
11632
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-xs text-red-600/80 dark:text-red-400/70 break-words leading-relaxed", children: error }),
11633
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
10778
11634
  onRetry && /* @__PURE__ */ jsxRuntime.jsxs(
10779
11635
  "button",
10780
11636
  {
@@ -10872,7 +11728,8 @@ function ChatSessionCore({
10872
11728
  updateQueueItem
10873
11729
  } = useChatInput();
10874
11730
  const isArchived = session?.status === "archived";
10875
- const effectiveReadOnly = Boolean(readOnly ?? isReadOnly) || isArchived;
11731
+ const accessReadOnly = session?.access ? !session.access.canWrite : false;
11732
+ const effectiveReadOnly = Boolean(readOnly ?? isReadOnly) || isArchived || accessReadOnly;
10876
11733
  const [restoring, setRestoring] = React.useState(false);
10877
11734
  const handleRestore = React.useCallback(async () => {
10878
11735
  if (!session?.id) {
@@ -10893,6 +11750,8 @@ function ChatSessionCore({
10893
11750
  const [artifactsPanelExpanded, setArtifactsPanelExpanded] = React.useState(
10894
11751
  artifactsPanelDefaultExpanded
10895
11752
  );
11753
+ const [membersModalOpen, setMembersModalOpen] = React.useState(false);
11754
+ const [headerMembers, setHeaderMembers] = React.useState(null);
10896
11755
  const [artifactsPanelWidth, setArtifactsPanelWidth] = React.useState(ARTIFACTS_PANEL_WIDTH_DEFAULT);
10897
11756
  const [isResizingArtifactsPanel, setIsResizingArtifactsPanel] = React.useState(false);
10898
11757
  const layoutContainerRef = React.useRef(null);
@@ -10927,6 +11786,25 @@ function ChatSessionCore({
10927
11786
  } catch {
10928
11787
  }
10929
11788
  }, [artifactsPanelStorageKey, showArtifactsPanel]);
11789
+ React.useEffect(() => {
11790
+ if (!session?.id || !dataSource.listSessionMembers) {
11791
+ setHeaderMembers(null);
11792
+ return;
11793
+ }
11794
+ let cancelled = false;
11795
+ dataSource.listSessionMembers(session.id).then((members) => {
11796
+ if (!cancelled) {
11797
+ setHeaderMembers(members.map((m) => m.user));
11798
+ }
11799
+ }).catch(() => {
11800
+ if (!cancelled) {
11801
+ setHeaderMembers(null);
11802
+ }
11803
+ });
11804
+ return () => {
11805
+ cancelled = true;
11806
+ };
11807
+ }, [session?.id, dataSource.listSessionMembers]);
10930
11808
  const handleArtifactsResizeStart = React.useCallback(() => {
10931
11809
  setIsResizingArtifactsPanel(true);
10932
11810
  }, []);
@@ -11016,8 +11894,26 @@ function ChatSessionCore({
11016
11894
  }
11017
11895
  }
11018
11896
  };
11019
- const headerActions = showArtifactsControls ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11020
- /* @__PURE__ */ jsxRuntime.jsxs(
11897
+ const canShowShareButton = Boolean(
11898
+ session?.access?.canManageMembers && dataSource.listUsers && dataSource.listSessionMembers && dataSource.addSessionMember && dataSource.updateSessionMemberRole && dataSource.removeSessionMember
11899
+ );
11900
+ const shareButton = canShowShareButton ? /* @__PURE__ */ jsxRuntime.jsxs(
11901
+ "button",
11902
+ {
11903
+ type: "button",
11904
+ onClick: () => setMembersModalOpen(true),
11905
+ className: "inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-gray-500 transition-all duration-150 hover:bg-gray-100 hover:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200",
11906
+ "aria-label": t("BiChat.Share.Title"),
11907
+ title: t("BiChat.Share.Title"),
11908
+ children: [
11909
+ /* @__PURE__ */ jsxRuntime.jsx(react.ShareNetwork, { className: "h-4 w-4" }),
11910
+ t("BiChat.Share.Button")
11911
+ ]
11912
+ }
11913
+ ) : null;
11914
+ const headerActions = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
11915
+ shareButton,
11916
+ showArtifactsControls && /* @__PURE__ */ jsxRuntime.jsxs(
11021
11917
  "button",
11022
11918
  {
11023
11919
  type: "button",
@@ -11037,7 +11933,7 @@ function ChatSessionCore({
11037
11933
  }
11038
11934
  ),
11039
11935
  actionsSlot
11040
- ] }) : actionsSlot;
11936
+ ] });
11041
11937
  return /* @__PURE__ */ jsxRuntime.jsxs(
11042
11938
  "main",
11043
11939
  {
@@ -11050,7 +11946,9 @@ function ChatSessionCore({
11050
11946
  onBack,
11051
11947
  readOnly: effectiveReadOnly,
11052
11948
  logoSlot,
11053
- actionsSlot: headerActions
11949
+ actionsSlot: headerActions,
11950
+ members: headerMembers ?? (session?.owner ? [session.owner] : void 0),
11951
+ onMembersClick: canShowShareButton ? () => setMembersModalOpen(true) : void 0
11054
11952
  }
11055
11953
  ),
11056
11954
  error && /* @__PURE__ */ jsxRuntime.jsx(
@@ -11247,6 +12145,15 @@ function ChatSessionCore({
11247
12145
  ) })
11248
12146
  ]
11249
12147
  }
12148
+ ),
12149
+ canShowShareButton && /* @__PURE__ */ jsxRuntime.jsx(
12150
+ SessionMembersModal,
12151
+ {
12152
+ isOpen: membersModalOpen,
12153
+ sessionId: session?.id,
12154
+ dataSource,
12155
+ onClose: () => setMembersModalOpen(false)
12156
+ }
11250
12157
  )
11251
12158
  ]
11252
12159
  }
@@ -11269,7 +12176,7 @@ function ChatSession(props) {
11269
12176
  // ui/src/bichat/index.ts
11270
12177
  init_MarkdownRenderer();
11271
12178
  init_ChartCard();
11272
- var sizeClasses = {
12179
+ var sizeClasses2 = {
11273
12180
  sm: {
11274
12181
  container: "py-6 px-3",
11275
12182
  title: "text-sm",
@@ -11294,7 +12201,7 @@ function EmptyState({
11294
12201
  className = "",
11295
12202
  size = "md"
11296
12203
  }) {
11297
- const sizes = sizeClasses[size];
12204
+ const sizes = sizeClasses2[size];
11298
12205
  const prefersReducedMotion2 = framerMotion.useReducedMotion();
11299
12206
  const duration = prefersReducedMotion2 ? 0 : 0.4;
11300
12207
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -11354,7 +12261,7 @@ MemoizedEmptyState.displayName = "EmptyState";
11354
12261
 
11355
12262
  // ui/src/bichat/components/EditableText.tsx
11356
12263
  init_useTranslation();
11357
- var sizeClasses2 = {
12264
+ var sizeClasses3 = {
11358
12265
  sm: "text-sm",
11359
12266
  md: "text-base",
11360
12267
  lg: "text-lg"
@@ -11425,7 +12332,7 @@ var EditableText = React.forwardRef(
11425
12332
  const handleBlur = () => {
11426
12333
  handleSave();
11427
12334
  };
11428
- const sizeClass = sizeClasses2[size];
12335
+ const sizeClass = sizeClasses3[size];
11429
12336
  if (isEditing) {
11430
12337
  return /* @__PURE__ */ jsxRuntime.jsx(
11431
12338
  "div",
@@ -11485,7 +12392,7 @@ var MemoizedEditableText = React.memo(EditableText);
11485
12392
 
11486
12393
  // ui/src/bichat/components/SearchInput.tsx
11487
12394
  init_useTranslation();
11488
- var sizeClasses3 = {
12395
+ var sizeClasses4 = {
11489
12396
  sm: {
11490
12397
  container: "py-1.5 pl-8 pr-8 text-xs",
11491
12398
  icon: 14,
@@ -11518,7 +12425,7 @@ function SearchInput({
11518
12425
  const resolvedPlaceholder = placeholder ?? t("BiChat.Common.Search");
11519
12426
  const resolvedAriaLabel = ariaLabel ?? t("BiChat.Common.Search");
11520
12427
  const inputRef = React.useRef(null);
11521
- const sizes = sizeClasses3[size];
12428
+ const sizes = sizeClasses4[size];
11522
12429
  React.useEffect(() => {
11523
12430
  if (autoFocus && inputRef.current) {
11524
12431
  inputRef.current.focus();
@@ -11931,148 +12838,20 @@ function Toast({
11931
12838
 
11932
12839
  // ui/src/bichat/components/ToastContainer.tsx
11933
12840
  init_useTranslation();
11934
- function ToastContainer({ toasts, onDismiss, dismissLabel }) {
11935
- const { t } = useTranslation();
11936
- if (toasts.length === 0) {
11937
- return null;
11938
- }
11939
- return /* @__PURE__ */ jsxRuntime.jsx(
11940
- "div",
11941
- {
11942
- "aria-label": t("BiChat.Common.Notifications"),
11943
- className: "fixed top-6 right-6 z-[var(--bichat-z-toast,60)] flex flex-col gap-2 pointer-events-none",
11944
- children: toasts.map((toast) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsxRuntime.jsx(Toast, { ...toast, onDismiss, dismissLabel }) }, toast.id))
11945
- }
11946
- );
11947
- }
11948
-
11949
- // ui/src/bichat/components/ConfirmModal.tsx
11950
- init_useTranslation();
11951
- function ConfirmModalBase({
11952
- isOpen,
11953
- title,
11954
- message,
11955
- onConfirm,
11956
- onCancel,
11957
- confirmText,
11958
- cancelText,
11959
- isDanger = false
11960
- }) {
12841
+ function ToastContainer({ toasts, onDismiss, dismissLabel }) {
11961
12842
  const { t } = useTranslation();
11962
- const resolvedConfirmText = confirmText?.trim() ? confirmText : t("BiChat.Common.Confirm");
11963
- const resolvedCancelText = cancelText?.trim() ? cancelText : t("BiChat.Common.Cancel");
11964
- return /* @__PURE__ */ jsxRuntime.jsxs(react$1.Dialog, { open: isOpen, onClose: onCancel, className: "relative z-40", children: [
11965
- /* @__PURE__ */ jsxRuntime.jsx(react$1.DialogBackdrop, { className: "fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm transition-opacity duration-200" }),
11966
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 flex items-center justify-center z-50 p-4", children: /* @__PURE__ */ jsxRuntime.jsxs(react$1.DialogPanel, { className: "bg-white dark:bg-gray-800 rounded-2xl shadow-xl dark:shadow-2xl dark:shadow-black/30 max-w-sm w-full overflow-hidden", children: [
11967
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-6 pt-6 pb-5", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3.5", children: [
11968
- isDanger && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-shrink-0 flex items-center justify-center w-10 h-10 rounded-xl bg-red-50 dark:bg-red-950/40 border border-red-200/60 dark:border-red-800/40", children: /* @__PURE__ */ jsxRuntime.jsx(react.WarningCircle, { size: 22, weight: "duotone", className: "text-red-600 dark:text-red-400" }) }),
11969
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
11970
- /* @__PURE__ */ jsxRuntime.jsx(react$1.DialogTitle, { className: "text-base font-semibold text-gray-900 dark:text-gray-100 leading-snug", children: title }),
11971
- /* @__PURE__ */ jsxRuntime.jsx(react$1.Description, { className: "mt-2 text-sm text-gray-600 dark:text-gray-400 leading-relaxed", children: message })
11972
- ] })
11973
- ] }) }),
11974
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-2.5 px-6 pb-5", children: [
11975
- /* @__PURE__ */ jsxRuntime.jsx(
11976
- "button",
11977
- {
11978
- type: "button",
11979
- onClick: onCancel,
11980
- ...isDanger ? { "data-autofocus": true } : {},
11981
- className: "cursor-pointer px-4 py-2 text-sm font-medium rounded-xl text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700/60 hover:bg-gray-200 dark:hover:bg-gray-700 active:bg-gray-250 dark:active:bg-gray-600 transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-800",
11982
- "data-testid": "confirm-modal-cancel",
11983
- children: resolvedCancelText
11984
- }
11985
- ),
11986
- /* @__PURE__ */ jsxRuntime.jsx(
11987
- "button",
11988
- {
11989
- type: "button",
11990
- ...!isDanger ? { "data-autofocus": true } : {},
11991
- onClick: onConfirm,
11992
- className: [
11993
- "cursor-pointer px-4 py-2 text-sm font-medium rounded-xl text-white",
11994
- "transition-all duration-150 shadow-sm hover:shadow",
11995
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-gray-800",
11996
- isDanger ? "bg-red-600 hover:bg-red-700 active:bg-red-800 focus-visible:ring-red-500/50" : "bg-primary-600 hover:bg-primary-700 active:bg-primary-800 focus-visible:ring-primary-500/50"
11997
- ].join(" "),
11998
- "data-testid": "confirm-modal-confirm",
11999
- children: resolvedConfirmText
12000
- }
12001
- )
12002
- ] })
12003
- ] }) })
12004
- ] });
12005
- }
12006
- var ConfirmModal = React.memo(ConfirmModalBase);
12007
- ConfirmModal.displayName = "ConfirmModal";
12008
- var ConfirmModal_default = ConfirmModal;
12009
- function hashString(str) {
12010
- let hash = 0;
12011
- for (let i = 0; i < str.length; i++) {
12012
- const char = str.charCodeAt(i);
12013
- hash = (hash << 5) - hash + char;
12014
- hash = hash & hash;
12843
+ if (toasts.length === 0) {
12844
+ return null;
12015
12845
  }
12016
- return Math.abs(hash);
12017
- }
12018
- var colorPalette = [
12019
- { bg: "bg-blue-500", text: "text-white" },
12020
- { bg: "bg-green-500", text: "text-white" },
12021
- { bg: "bg-purple-500", text: "text-white" },
12022
- { bg: "bg-pink-500", text: "text-white" },
12023
- { bg: "bg-indigo-500", text: "text-white" },
12024
- { bg: "bg-teal-500", text: "text-white" },
12025
- { bg: "bg-orange-500", text: "text-white" },
12026
- { bg: "bg-cyan-500", text: "text-white" },
12027
- { bg: "bg-amber-500", text: "text-white" },
12028
- { bg: "bg-lime-500", text: "text-white" }
12029
- ];
12030
- var sizeClasses4 = {
12031
- sm: "w-8 h-8 text-xs",
12032
- md: "w-10 h-10 text-sm",
12033
- lg: "w-12 h-12 text-base"
12034
- };
12035
- function UserAvatar({
12036
- firstName,
12037
- lastName,
12038
- initials: providedInitials,
12039
- size = "md",
12040
- className = ""
12041
- }) {
12042
- const derivedInitials = (() => {
12043
- const firstChar = firstName?.trim()?.charAt(0) || "";
12044
- const lastChar = lastName?.trim()?.charAt(0) || "";
12045
- const combined = `${firstChar}${lastChar}`.trim();
12046
- return combined || "U";
12047
- })();
12048
- const initials = (providedInitials?.trim() || derivedInitials).toUpperCase();
12049
- const fullName = `${firstName}${lastName}`;
12050
- const colorIndex = hashString(fullName) % colorPalette.length;
12051
- const colors = colorPalette[colorIndex];
12052
12846
  return /* @__PURE__ */ jsxRuntime.jsx(
12053
12847
  "div",
12054
12848
  {
12055
- className: `
12056
- ${sizeClasses4[size]}
12057
- ${colors.bg}
12058
- ${colors.text}
12059
- ${className}
12060
- rounded-full
12061
- flex
12062
- items-center
12063
- justify-center
12064
- font-semibold
12065
- flex-shrink-0
12066
- select-none
12067
- `,
12068
- "aria-label": `${firstName} ${lastName}`,
12069
- title: `${firstName} ${lastName}`,
12070
- children: initials
12849
+ "aria-label": t("BiChat.Common.Notifications"),
12850
+ className: "fixed top-6 right-6 z-[var(--bichat-z-toast,60)] flex flex-col gap-2 pointer-events-none",
12851
+ children: toasts.map((toast) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsxRuntime.jsx(Toast, { ...toast, onDismiss, dismissLabel }) }, toast.id))
12071
12852
  }
12072
12853
  );
12073
12854
  }
12074
- var MemoizedUserAvatar = React.memo(UserAvatar);
12075
- MemoizedUserAvatar.displayName = "UserAvatar";
12076
12855
  function PermissionGuard({
12077
12856
  permissions,
12078
12857
  mode = "all",
@@ -12514,6 +13293,11 @@ var SessionItem = React.memo(
12514
13293
  const isTitleGenerating = !session.title?.trim();
12515
13294
  const displayTitle = isTitleGenerating ? t("BiChat.Common.Generating") : session.title ?? t("BiChat.Common.Untitled");
12516
13295
  const lastActivity = formatRelativeTime(session.updatedAt, t);
13296
+ const accessRole = session.access?.role ?? "owner";
13297
+ const roleLabel = accessRole === "editor" ? t("BiChat.Share.RoleEditor") : accessRole === "viewer" ? t("BiChat.Share.RoleViewer") : accessRole === "read_all" ? t("BiChat.Share.RoleReadOnly") : "";
13298
+ const visibilityLabel = session.isGroup ? t("BiChat.Sidebar.GroupChat") : accessRole === "editor" || accessRole === "viewer" ? t("BiChat.Sidebar.SharedWithYou") : "";
13299
+ const isGroupOrShared = Boolean(session.isGroup || session.memberCount && session.memberCount > 1);
13300
+ const metaParts = [lastActivity, visibilityLabel, roleLabel].filter(Boolean);
12517
13301
  const { handlers: longPressHandlers } = useLongPress({
12518
13302
  delay: 500,
12519
13303
  onLongPress: (e) => {
@@ -12655,17 +13439,23 @@ var SessionItem = React.memo(
12655
13439
  "data-testid": `${testIdPrefix}-session-${session.id}`,
12656
13440
  ...longPressHandlers,
12657
13441
  children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
12658
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-w-0 flex-1", children: [
12659
- /* @__PURE__ */ jsxRuntime.jsx(
12660
- MemoizedEditableText,
12661
- {
12662
- ref: editableTitleRef,
12663
- value: displayTitle,
12664
- onSave: (newTitle) => onRename?.(newTitle),
12665
- isLoading: isTitleGenerating
12666
- }
12667
- ),
12668
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] text-gray-400 dark:text-gray-500 truncate mt-0.5", children: lastActivity })
13442
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 min-w-0 flex-1", children: [
13443
+ isGroupOrShared && /* @__PURE__ */ jsxRuntime.jsx(react.UsersThree, { size: 14, weight: "duotone", className: "text-primary-500 dark:text-primary-400 mt-1 flex-shrink-0" }),
13444
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col min-w-0 flex-1", children: [
13445
+ /* @__PURE__ */ jsxRuntime.jsx(
13446
+ MemoizedEditableText,
13447
+ {
13448
+ ref: editableTitleRef,
13449
+ value: displayTitle,
13450
+ onSave: (newTitle) => onRename?.(newTitle),
13451
+ isLoading: isTitleGenerating
13452
+ }
13453
+ ),
13454
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-[11px] text-gray-400 dark:text-gray-500 truncate mt-0.5", children: [
13455
+ metaParts.join(" \u2022 "),
13456
+ isGroupOrShared && session.memberCount && session.memberCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex items-center ml-1 rounded-full bg-primary-50 dark:bg-primary-900/30 px-1.5 text-[10px] font-medium text-primary-600 dark:text-primary-400", children: session.memberCount })
13457
+ ] })
13458
+ ] })
12669
13459
  ] }),
12670
13460
  !isTouch && hasContextMenu && /* @__PURE__ */ jsxRuntime.jsxs(react$1.Menu, { children: [
12671
13461
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -13019,24 +13809,23 @@ function AllChatsList({ dataSource, onSessionSelect, activeSessionId }) {
13019
13809
  setOffset((prev) => prev + limit);
13020
13810
  }
13021
13811
  }, [fetching, hasMore]);
13022
- const loadMoreRef = React.useCallback(
13023
- (node) => {
13024
- if (!node || fetching || !hasMore) {
13025
- return;
13812
+ const loadMoreNodeRef = React.useRef(null);
13813
+ const loadMoreRef = React.useCallback((node) => {
13814
+ loadMoreNodeRef.current = node;
13815
+ }, []);
13816
+ React.useEffect(() => {
13817
+ const node = loadMoreNodeRef.current;
13818
+ if (!node || fetching || !hasMore) {
13819
+ return;
13820
+ }
13821
+ const observer = new IntersectionObserver((entries) => {
13822
+ if (entries[0].isIntersecting) {
13823
+ handleLoadMore();
13026
13824
  }
13027
- const observer = new IntersectionObserver(
13028
- (entries) => {
13029
- if (entries[0].isIntersecting) {
13030
- handleLoadMore();
13031
- }
13032
- },
13033
- { threshold: 0.1 }
13034
- );
13035
- observer.observe(node);
13036
- return () => observer.disconnect();
13037
- },
13038
- [fetching, hasMore, handleLoadMore]
13039
- );
13825
+ }, { threshold: 0.1 });
13826
+ observer.observe(node);
13827
+ return () => observer.disconnect();
13828
+ }, [fetching, hasMore, handleLoadMore]);
13040
13829
  const derivedUsers = React.useMemo(() => {
13041
13830
  if (dataSource.listUsers) {
13042
13831
  return users;
@@ -13094,57 +13883,61 @@ function AllChatsList({ dataSource, onSessionSelect, activeSessionId }) {
13094
13883
  role: "list",
13095
13884
  "aria-label": t("BiChat.AllChats.OrganizationChatSessions"),
13096
13885
  children: [
13097
- chats.map((chat) => /* @__PURE__ */ jsxRuntime.jsx(
13098
- framerMotion.motion.div,
13099
- {
13100
- initial: { opacity: 0, y: -10 },
13101
- animate: { opacity: 1, y: 0 },
13102
- exit: { opacity: 0, y: -10 },
13103
- children: /* @__PURE__ */ jsxRuntime.jsx(
13104
- "div",
13105
- {
13106
- role: "link",
13107
- tabIndex: 0,
13108
- onClick: () => onSessionSelect(chat.id),
13109
- onKeyDown: (e) => {
13110
- if (e.key === "Enter" || e.key === " ") {
13111
- e.preventDefault();
13112
- onSessionSelect(chat.id);
13113
- }
13114
- },
13115
- className: `
13886
+ chats.map((chat) => {
13887
+ const owner = chat.owner ?? {
13888
+ firstName: "",
13889
+ lastName: "",
13890
+ initials: "U"
13891
+ };
13892
+ const ownerName = [owner.firstName, owner.lastName].filter(Boolean).join(" ");
13893
+ return /* @__PURE__ */ jsxRuntime.jsx(
13894
+ framerMotion.motion.div,
13895
+ {
13896
+ initial: { opacity: 0, y: -10 },
13897
+ animate: { opacity: 1, y: 0 },
13898
+ exit: { opacity: 0, y: -10 },
13899
+ children: /* @__PURE__ */ jsxRuntime.jsx(
13900
+ "div",
13901
+ {
13902
+ role: "link",
13903
+ tabIndex: 0,
13904
+ onClick: () => onSessionSelect(chat.id),
13905
+ onKeyDown: (e) => {
13906
+ if (e.key === "Enter" || e.key === " ") {
13907
+ e.preventDefault();
13908
+ onSessionSelect(chat.id);
13909
+ }
13910
+ },
13911
+ className: `
13116
13912
  block px-3 py-2 rounded-lg transition-smooth group cursor-pointer
13117
13913
  ${chat.id === activeSessionId ? "bg-primary-50/50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400 border-l-4 border-primary-400 dark:border-primary-600" : "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 border-l-4 border-transparent"}
13118
13914
  `,
13119
- "aria-current": chat.id === activeSessionId ? "page" : void 0,
13120
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
13121
- /* @__PURE__ */ jsxRuntime.jsx(
13122
- MemoizedUserAvatar,
13123
- {
13124
- firstName: chat.owner.firstName,
13125
- lastName: chat.owner.lastName,
13126
- initials: chat.owner.initials,
13127
- size: "sm"
13128
- }
13129
- ),
13130
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
13131
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium truncate", children: chat.title || t("BiChat.Common.Untitled") }),
13132
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-gray-500 dark:text-gray-400 truncate", children: [
13133
- chat.owner.firstName,
13134
- " ",
13135
- chat.owner.lastName
13136
- ] }),
13137
- chat.status === "archived" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 mt-1 px-2 py-0.5 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 rounded-full text-xs", children: [
13138
- /* @__PURE__ */ jsxRuntime.jsx(react.Archive, { size: 12, className: "w-3 h-3" }),
13139
- t("BiChat.Chat.Archived")
13915
+ "aria-current": chat.id === activeSessionId ? "page" : void 0,
13916
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2", children: [
13917
+ /* @__PURE__ */ jsxRuntime.jsx(
13918
+ MemoizedUserAvatar,
13919
+ {
13920
+ firstName: owner.firstName,
13921
+ lastName: owner.lastName,
13922
+ initials: owner.initials,
13923
+ size: "sm"
13924
+ }
13925
+ ),
13926
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
13927
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium truncate", children: chat.title || t("BiChat.Common.Untitled") }),
13928
+ ownerName && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 truncate", children: ownerName }),
13929
+ chat.status === "archived" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 mt-1 px-2 py-0.5 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 rounded-full text-xs", children: [
13930
+ /* @__PURE__ */ jsxRuntime.jsx(react.Archive, { size: 12, className: "w-3 h-3" }),
13931
+ t("BiChat.Chat.Archived")
13932
+ ] })
13140
13933
  ] })
13141
13934
  ] })
13142
- ] })
13143
- }
13144
- )
13145
- },
13146
- chat.id
13147
- )),
13935
+ }
13936
+ )
13937
+ },
13938
+ chat.id
13939
+ );
13940
+ }),
13148
13941
  hasMore && /* @__PURE__ */ jsxRuntime.jsx("div", { ref: loadMoreRef, className: "py-4 text-center", children: fetching ? /* @__PURE__ */ jsxRuntime.jsx(SessionSkeleton, { count: 2 }) : /* @__PURE__ */ jsxRuntime.jsx(
13149
13942
  "button",
13150
13943
  {
@@ -13914,19 +14707,22 @@ function Sidebar2({
13914
14707
  animate: "visible",
13915
14708
  role: "list",
13916
14709
  "aria-label": t("BiChat.Sidebar.PinnedChats"),
13917
- children: pinnedSessions.map((session) => /* @__PURE__ */ jsxRuntime.jsx(
13918
- SessionItem_default,
13919
- {
13920
- session,
13921
- isActive: session.id === activeSessionId,
13922
- onSelect: () => handleSessionSelect(session.id),
13923
- onArchive: () => handleSessionArchive(session.id),
13924
- onPin: () => handleSessionPin(session.id, session.pinned),
13925
- onRename: (newTitle) => handleSessionRename(session.id, newTitle),
13926
- onRegenerateTitle: () => handleSessionRegenerateTitle(session.id)
13927
- },
13928
- session.id
13929
- ))
14710
+ children: pinnedSessions.map((session) => {
14711
+ const canWrite = session.access?.canWrite ?? true;
14712
+ return /* @__PURE__ */ jsxRuntime.jsx(
14713
+ SessionItem_default,
14714
+ {
14715
+ session,
14716
+ isActive: session.id === activeSessionId,
14717
+ onSelect: () => handleSessionSelect(session.id),
14718
+ onArchive: canWrite ? () => handleSessionArchive(session.id) : void 0,
14719
+ onPin: canWrite ? () => handleSessionPin(session.id, session.pinned) : void 0,
14720
+ onRename: canWrite ? (newTitle) => handleSessionRename(session.id, newTitle) : void 0,
14721
+ onRegenerateTitle: canWrite ? () => handleSessionRegenerateTitle(session.id) : void 0
14722
+ },
14723
+ session.id
14724
+ );
14725
+ })
13930
14726
  }
13931
14727
  ),
13932
14728
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-b border-gray-200 dark:border-gray-700 my-3" })
@@ -13948,19 +14744,22 @@ function Sidebar2({
13948
14744
  animate: "visible",
13949
14745
  role: "list",
13950
14746
  "aria-label": `${group.name} chats`,
13951
- children: group.sessions.map((session) => /* @__PURE__ */ jsxRuntime.jsx(
13952
- SessionItem_default,
13953
- {
13954
- session,
13955
- isActive: session.id === activeSessionId,
13956
- onSelect: () => handleSessionSelect(session.id),
13957
- onArchive: () => handleSessionArchive(session.id),
13958
- onPin: () => handleSessionPin(session.id, session.pinned),
13959
- onRename: (newTitle) => handleSessionRename(session.id, newTitle),
13960
- onRegenerateTitle: () => handleSessionRegenerateTitle(session.id)
13961
- },
13962
- session.id
13963
- ))
14747
+ children: group.sessions.map((session) => {
14748
+ const canWrite = session.access?.canWrite ?? true;
14749
+ return /* @__PURE__ */ jsxRuntime.jsx(
14750
+ SessionItem_default,
14751
+ {
14752
+ session,
14753
+ isActive: session.id === activeSessionId,
14754
+ onSelect: () => handleSessionSelect(session.id),
14755
+ onArchive: canWrite ? () => handleSessionArchive(session.id) : void 0,
14756
+ onPin: canWrite ? () => handleSessionPin(session.id, session.pinned) : void 0,
14757
+ onRename: canWrite ? (newTitle) => handleSessionRename(session.id, newTitle) : void 0,
14758
+ onRegenerateTitle: canWrite ? () => handleSessionRegenerateTitle(session.id) : void 0
14759
+ },
14760
+ session.id
14761
+ );
14762
+ })
13964
14763
  }
13965
14764
  )
13966
14765
  ] }, group.name)),
@@ -16121,6 +16920,77 @@ function useScrollToBottom(items) {
16121
16920
  scrollToBottom
16122
16921
  };
16123
16922
  }
16923
+ function useHttpDataSourceConfigFromApplet(options) {
16924
+ return React.useMemo(() => {
16925
+ const ctx = typeof window !== "undefined" ? window.__APPLET_CONTEXT__ : void 0;
16926
+ if (!ctx) {
16927
+ throw new Error(
16928
+ "Applet context not found. Ensure window.__APPLET_CONTEXT__ is injected by the backend."
16929
+ );
16930
+ }
16931
+ const rpcEndpoint = ctx.config?.rpcUIEndpoint ?? "/rpc";
16932
+ const streamEndpoint = ctx.config?.streamEndpoint ?? "/stream";
16933
+ const csrfToken = ctx.session?.csrfToken ?? (typeof window !== "undefined" ? window.__CSRF_TOKEN__ : void 0) ?? "";
16934
+ const isDev = typeof undefined?.DEV === "boolean" && undefined?.DEV;
16935
+ if (!csrfToken && isDev) {
16936
+ console.warn(
16937
+ "[useHttpDataSourceConfigFromApplet] CSRF token is empty \u2014 requests may be rejected by the server."
16938
+ );
16939
+ }
16940
+ return {
16941
+ baseUrl: "",
16942
+ rpcEndpoint,
16943
+ streamEndpoint,
16944
+ csrfToken,
16945
+ rpcTimeoutMs: options?.rpcTimeoutMs ?? 12e4,
16946
+ streamConnectTimeoutMs: options?.streamConnectTimeoutMs ?? 3e4
16947
+ };
16948
+ }, [options?.rpcTimeoutMs, options?.streamConnectTimeoutMs]);
16949
+ }
16950
+ var SESSION_PATH_REGEX = /\/session\/([^/]+)/;
16951
+ function useBichatRouter({
16952
+ navigate,
16953
+ pathname,
16954
+ onNavigate
16955
+ }) {
16956
+ const activeSessionId = React.useMemo(
16957
+ () => pathname.match(SESSION_PATH_REGEX)?.[1],
16958
+ [pathname]
16959
+ );
16960
+ const maybeClose = React.useCallback(() => {
16961
+ onNavigate?.();
16962
+ }, [onNavigate]);
16963
+ const onSessionSelect = React.useCallback(
16964
+ (sessionId) => {
16965
+ if (sessionId) {
16966
+ navigate(`/session/${sessionId}`);
16967
+ } else {
16968
+ navigate("/");
16969
+ }
16970
+ maybeClose();
16971
+ },
16972
+ [navigate, maybeClose]
16973
+ );
16974
+ const onNewChat = React.useCallback(() => {
16975
+ navigate("/");
16976
+ maybeClose();
16977
+ }, [navigate, maybeClose]);
16978
+ const onArchivedView = React.useCallback(() => {
16979
+ navigate("/archived");
16980
+ maybeClose();
16981
+ }, [navigate, maybeClose]);
16982
+ const onBack = React.useCallback(() => {
16983
+ navigate("/");
16984
+ maybeClose();
16985
+ }, [navigate, maybeClose]);
16986
+ return {
16987
+ activeSessionId,
16988
+ onSessionSelect,
16989
+ onNewChat,
16990
+ onArchivedView,
16991
+ onBack
16992
+ };
16993
+ }
16124
16994
 
16125
16995
  // ui/src/bichat/index.ts
16126
16996
  init_IotaContext();
@@ -16424,10 +17294,59 @@ function resolveArtifactName(artifact) {
16424
17294
  }
16425
17295
  return `${label} artifact`;
16426
17296
  }
17297
+ function mapSessionUser(rawUser) {
17298
+ if (!isRecord2(rawUser)) {
17299
+ return void 0;
17300
+ }
17301
+ const rawId = rawUser.id;
17302
+ const id = readNonEmptyString(rawId) ?? (typeof rawId === "number" && Number.isFinite(rawId) ? String(rawId) : null);
17303
+ if (!id) {
17304
+ return void 0;
17305
+ }
17306
+ const firstName = readString2(rawUser.firstName);
17307
+ const lastName = readString2(rawUser.lastName);
17308
+ const initials = readNonEmptyString(rawUser.initials) || `${firstName.charAt(0)}${lastName.charAt(0)}`.trim().toUpperCase() || "U";
17309
+ return {
17310
+ id,
17311
+ firstName,
17312
+ lastName,
17313
+ initials
17314
+ };
17315
+ }
17316
+ function mapSessionAccess(rawAccess) {
17317
+ if (!isRecord2(rawAccess)) {
17318
+ return void 0;
17319
+ }
17320
+ const role = readString2(rawAccess.role).toLowerCase();
17321
+ const source = readString2(rawAccess.source).toLowerCase();
17322
+ const normalizedRole = role === "owner" || role === "editor" || role === "viewer" || role === "read_all" ? role : "none";
17323
+ const normalizedSource = source === "owner" || source === "member" || source === "permission" ? source : "none";
17324
+ const canRead = rawAccess.canRead === true || rawAccess.canRead === "true";
17325
+ const canWrite = rawAccess.canWrite === true || rawAccess.canWrite === "true";
17326
+ const canManageMembers = rawAccess.canManageMembers === true || rawAccess.canManageMembers === "true";
17327
+ if (normalizedRole === "none" && normalizedSource === "none" && !canRead && !canWrite && !canManageMembers) {
17328
+ return void 0;
17329
+ }
17330
+ return {
17331
+ role: normalizedRole,
17332
+ source: normalizedSource,
17333
+ canRead,
17334
+ canWrite,
17335
+ canManageMembers
17336
+ };
17337
+ }
16427
17338
  function toSession(session) {
16428
17339
  return {
16429
- ...session,
16430
- status: session.status === "archived" ? "archived" : "active"
17340
+ id: readString2(session.id),
17341
+ title: readString2(session.title),
17342
+ status: session.status === "archived" ? "archived" : "active",
17343
+ pinned: Boolean(session.pinned),
17344
+ createdAt: readString2(session.createdAt),
17345
+ updatedAt: readString2(session.updatedAt),
17346
+ owner: mapSessionUser(session.owner),
17347
+ isGroup: Boolean(session.isGroup),
17348
+ memberCount: typeof session.memberCount === "number" ? session.memberCount : void 0,
17349
+ access: mapSessionAccess(session.access)
16431
17350
  };
16432
17351
  }
16433
17352
  function toSessionArtifact(artifact) {
@@ -16701,6 +17620,7 @@ function sanitizeConversationTurn(rawTurn, index, fallbackSessionID) {
16701
17620
  id: userTurnID,
16702
17621
  content: readString2(rawTurn.userTurn.content),
16703
17622
  attachments: sanitizeUserAttachments(rawTurn.userTurn.attachments, turnID),
17623
+ author: mapSessionUser(rawTurn.userTurn.author),
16704
17624
  createdAt: readString2(rawTurn.userTurn.createdAt, createdAt)
16705
17625
  },
16706
17626
  assistantTurn: sanitizeAssistantTurn(rawTurn.assistantTurn, createdAt, turnID),
@@ -17191,7 +18111,85 @@ async function clearSessionHistory(callRPC, sessionId) {
17191
18111
  return callRPC("bichat.session.clear", { id: sessionId });
17192
18112
  }
17193
18113
  async function compactSessionHistory(callRPC, sessionId) {
17194
- return callRPC("bichat.session.compact", { id: sessionId });
18114
+ const result = await callRPC("bichat.session.compact", { id: sessionId });
18115
+ if (!result.accepted) {
18116
+ throw new Error("Session compact request was not accepted");
18117
+ }
18118
+ if (result.operation !== "session_compact") {
18119
+ throw new Error(`Unexpected async operation: ${result.operation}`);
18120
+ }
18121
+ if (!result.sessionId || !result.runId) {
18122
+ throw new Error("Missing async run metadata");
18123
+ }
18124
+ return {
18125
+ accepted: true,
18126
+ operation: result.operation,
18127
+ sessionId: result.sessionId,
18128
+ runId: result.runId,
18129
+ startedAt: result.startedAt
18130
+ };
18131
+ }
18132
+ async function listUsers(callRPC) {
18133
+ const data = await callRPC("bichat.user.list", {});
18134
+ return data.users.map((user) => ({
18135
+ id: String(user.id),
18136
+ firstName: user.firstName || "",
18137
+ lastName: user.lastName || "",
18138
+ initials: user.initials || ""
18139
+ }));
18140
+ }
18141
+ async function listAllSessions(callRPC, options) {
18142
+ const data = await callRPC("bichat.session.listAll", {
18143
+ limit: options?.limit ?? 50,
18144
+ offset: options?.offset ?? 0,
18145
+ includeArchived: options?.includeArchived ?? false,
18146
+ userId: options?.userId ?? null
18147
+ });
18148
+ return {
18149
+ sessions: data.sessions.map(toSession),
18150
+ total: typeof data.total === "number" ? data.total : data.sessions.length,
18151
+ hasMore: Boolean(data.hasMore)
18152
+ };
18153
+ }
18154
+ async function listSessionMembers(callRPC, sessionId) {
18155
+ const data = await callRPC("bichat.session.members.list", { sessionId });
18156
+ return data.members.map((member) => ({
18157
+ user: {
18158
+ id: String(member.user.id),
18159
+ firstName: member.user.firstName,
18160
+ lastName: member.user.lastName,
18161
+ initials: member.user.initials
18162
+ },
18163
+ role: (() => {
18164
+ const normalizedRole = (member.role || "").toLowerCase();
18165
+ if (normalizedRole === "owner") {
18166
+ return "owner";
18167
+ }
18168
+ if (normalizedRole === "editor") {
18169
+ return "editor";
18170
+ }
18171
+ return "viewer";
18172
+ })(),
18173
+ createdAt: member.createdAt,
18174
+ updatedAt: member.updatedAt
18175
+ }));
18176
+ }
18177
+ async function addSessionMember(callRPC, sessionId, userId, role) {
18178
+ await callRPC("bichat.session.members.add", {
18179
+ sessionId,
18180
+ userId,
18181
+ role: role.toUpperCase()
18182
+ });
18183
+ }
18184
+ async function updateSessionMemberRole(callRPC, sessionId, userId, role) {
18185
+ await callRPC("bichat.session.members.updateRole", {
18186
+ sessionId,
18187
+ userId,
18188
+ role: role.toUpperCase()
18189
+ });
18190
+ }
18191
+ async function removeSessionMember(callRPC, sessionId, userId) {
18192
+ await callRPC("bichat.session.members.remove", { sessionId, userId });
17195
18193
  }
17196
18194
 
17197
18195
  // ui/src/bichat/utils/sseParser.ts
@@ -17248,6 +18246,9 @@ var TERMINAL_TYPES = /* @__PURE__ */ new Set(["done", "error"]);
17248
18246
  async function* parseBichatStream(reader) {
17249
18247
  let yieldedTerminal = false;
17250
18248
  for await (const event of parseSSEStream(reader)) {
18249
+ if (event.type === "ping") {
18250
+ continue;
18251
+ }
17251
18252
  const parsed = event;
17252
18253
  const inferredType = parsed.type || (parsed.content ? "content" : "error");
17253
18254
  const normalized = {
@@ -17585,7 +18586,7 @@ async function* sendMessage(deps, sessionId, content, attachments = [], signal,
17585
18586
  replaceFromMessageId: options?.replaceFromMessageID,
17586
18587
  attachments: streamAttachments
17587
18588
  };
17588
- const timeoutMs = deps.timeout ?? 0;
18589
+ const timeoutMs = deps.streamConnectTimeoutMs ?? 0;
17589
18590
  if (timeoutMs > 0) {
17590
18591
  connectionTimeoutID = setTimeout(() => {
17591
18592
  connectionTimedOut = true;
@@ -17620,7 +18621,7 @@ async function* sendMessage(deps, sessionId, content, attachments = [], signal,
17620
18621
  if (err.name === "AbortError") {
17621
18622
  yield {
17622
18623
  type: "error",
17623
- error: connectionTimedOut ? `Stream request timed out after ${deps.timeout}ms` : "Stream cancelled"
18624
+ error: connectionTimedOut ? `Stream request timed out after ${deps.streamConnectTimeoutMs}ms` : "Stream cancelled"
17624
18625
  };
17625
18626
  } else {
17626
18627
  yield {
@@ -17708,7 +18709,7 @@ async function resumeStream(deps, sessionId, runId, onChunk, signal) {
17708
18709
  const url = buildStreamUrl(deps, "/resume");
17709
18710
  const controller = new AbortController();
17710
18711
  let timeoutId;
17711
- const timeoutMs = deps.timeout;
18712
+ const timeoutMs = deps.connectTimeoutMs;
17712
18713
  if (timeoutMs != null && timeoutMs > 0) {
17713
18714
  timeoutId = setTimeout(() => controller.abort(), timeoutMs);
17714
18715
  }
@@ -17725,6 +18726,10 @@ async function resumeStream(deps, sessionId, runId, onChunk, signal) {
17725
18726
  if (!response.ok) {
17726
18727
  throw new Error(`Resume stream failed: HTTP ${response.status}`);
17727
18728
  }
18729
+ if (timeoutId !== void 0) {
18730
+ clearTimeout(timeoutId);
18731
+ timeoutId = void 0;
18732
+ }
17728
18733
  if (!response.body) {
17729
18734
  throw new Error("Resume response body is null");
17730
18735
  }
@@ -17758,11 +18763,7 @@ async function submitQuestionAnswers(callRPC, sessionId, questionId, answers) {
17758
18763
  });
17759
18764
  return {
17760
18765
  success: true,
17761
- data: {
17762
- session: toSession(result.session),
17763
- turns: normalizeTurns(sanitizeConversationTurns(result.turns, sessionId)),
17764
- pendingQuestion: sanitizePendingQuestion(result.pendingQuestion, sessionId)
17765
- }
18766
+ data: normalizeAsyncRunAccepted(result)
17766
18767
  };
17767
18768
  } catch (err) {
17768
18769
  return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
@@ -17770,12 +18771,33 @@ async function submitQuestionAnswers(callRPC, sessionId, questionId, answers) {
17770
18771
  }
17771
18772
  async function rejectPendingQuestion(callRPC, sessionId) {
17772
18773
  try {
17773
- await callRPC("bichat.question.reject", { sessionId });
17774
- return { success: true };
18774
+ const result = await callRPC("bichat.question.reject", { sessionId });
18775
+ return { success: true, data: normalizeAsyncRunAccepted(result) };
17775
18776
  } catch (err) {
17776
18777
  return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
17777
18778
  }
17778
18779
  }
18780
+ function isAsyncRunOperation(value) {
18781
+ return value === "question_submit" || value === "question_reject" || value === "session_compact";
18782
+ }
18783
+ function normalizeAsyncRunAccepted(input) {
18784
+ if (!input.accepted) {
18785
+ throw new Error("Async run request was not accepted");
18786
+ }
18787
+ if (!isAsyncRunOperation(input.operation)) {
18788
+ throw new Error(`Unexpected async operation: ${input.operation}`);
18789
+ }
18790
+ if (!input.sessionId || !input.runId) {
18791
+ throw new Error("Missing async run metadata");
18792
+ }
18793
+ return {
18794
+ accepted: true,
18795
+ operation: input.operation,
18796
+ sessionId: input.sessionId,
18797
+ runId: input.runId,
18798
+ startedAt: input.startedAt
18799
+ };
18800
+ }
17779
18801
 
17780
18802
  // ui/src/bichat/data/ArtifactManager.ts
17781
18803
  async function fetchSessionArtifacts(callRPC, sessionId, options) {
@@ -17805,7 +18827,12 @@ async function uploadSessionArtifacts(callRPC, sessionId, files, uploadFileFn) {
17805
18827
  const data = await callRPC("bichat.session.uploadArtifacts", {
17806
18828
  sessionId,
17807
18829
  attachments: uploads.map((upload) => ({
17808
- uploadId: upload.id
18830
+ id: String(upload.id),
18831
+ filename: upload.name,
18832
+ uploadId: upload.id,
18833
+ mimeType: upload.mimetype || "application/octet-stream",
18834
+ sizeBytes: upload.size,
18835
+ url: upload.url
17809
18836
  }))
17810
18837
  });
17811
18838
  return {
@@ -17842,15 +18869,13 @@ var HttpDataSource = class {
17842
18869
  this.config = {
17843
18870
  streamEndpoint: "/stream",
17844
18871
  uploadEndpoint: "/api/uploads",
17845
- timeout: 12e4,
17846
- ...config
18872
+ ...config,
18873
+ rpcTimeoutMs: typeof config.rpcTimeoutMs === "number" ? config.rpcTimeoutMs : 12e4,
18874
+ streamConnectTimeoutMs: typeof config.streamConnectTimeoutMs === "number" ? config.streamConnectTimeoutMs : 3e4
17847
18875
  };
17848
- if (config.navigateToSession) {
17849
- this.navigateToSession = config.navigateToSession;
17850
- }
17851
18876
  this.rpc = createAppletRPCClient({
17852
18877
  endpoint: `${this.config.baseUrl}${this.config.rpcEndpoint}`,
17853
- timeoutMs: this.config.timeout
18878
+ timeoutMs: this.config.rpcTimeoutMs
17854
18879
  });
17855
18880
  }
17856
18881
  // -------------------------------------------------------------------------
@@ -17932,6 +18957,24 @@ var HttpDataSource = class {
17932
18957
  async compactSessionHistory(sessionId) {
17933
18958
  return compactSessionHistory(this.boundCallRPC, sessionId);
17934
18959
  }
18960
+ async listUsers() {
18961
+ return listUsers(this.boundCallRPC);
18962
+ }
18963
+ async listAllSessions(options) {
18964
+ return listAllSessions(this.boundCallRPC, options);
18965
+ }
18966
+ async listSessionMembers(sessionId) {
18967
+ return listSessionMembers(this.boundCallRPC, sessionId);
18968
+ }
18969
+ async addSessionMember(sessionId, userId, role) {
18970
+ return addSessionMember(this.boundCallRPC, sessionId, userId, role);
18971
+ }
18972
+ async updateSessionMemberRole(sessionId, userId, role) {
18973
+ return updateSessionMemberRole(this.boundCallRPC, sessionId, userId, role);
18974
+ }
18975
+ async removeSessionMember(sessionId, userId) {
18976
+ return removeSessionMember(this.boundCallRPC, sessionId, userId);
18977
+ }
17935
18978
  // -------------------------------------------------------------------------
17936
18979
  // Message transport (delegates to MessageTransport)
17937
18980
  // -------------------------------------------------------------------------
@@ -17952,7 +18995,7 @@ var HttpDataSource = class {
17952
18995
  baseUrl: this.config.baseUrl,
17953
18996
  streamEndpoint: this.config.streamEndpoint,
17954
18997
  createHeaders: (h) => this.createHeaders(h),
17955
- timeoutMs: this.config.timeout
18998
+ timeoutMs: this.config.rpcTimeoutMs
17956
18999
  },
17957
19000
  sessionId
17958
19001
  );
@@ -17963,7 +19006,7 @@ var HttpDataSource = class {
17963
19006
  baseUrl: this.config.baseUrl,
17964
19007
  streamEndpoint: this.config.streamEndpoint,
17965
19008
  createHeaders: (h) => this.createHeaders(h),
17966
- timeout: this.config.timeout
19009
+ connectTimeoutMs: this.config.streamConnectTimeoutMs
17967
19010
  },
17968
19011
  sessionId,
17969
19012
  runId,
@@ -17987,7 +19030,8 @@ var HttpDataSource = class {
17987
19030
  callRPC: this.boundCallRPC,
17988
19031
  baseUrl: this.config.baseUrl,
17989
19032
  streamEndpoint: this.config.streamEndpoint,
17990
- timeout: this.config.timeout,
19033
+ rpcTimeoutMs: this.config.rpcTimeoutMs,
19034
+ streamConnectTimeoutMs: this.config.streamConnectTimeoutMs,
17991
19035
  createHeaders: (additional) => this.createHeaders(additional),
17992
19036
  uploadFileFn: this.boundUploadFile,
17993
19037
  logAttachmentLifecycle: () => {
@@ -18038,17 +19082,6 @@ var HttpDataSource = class {
18038
19082
  async deleteSessionArtifact(artifactId) {
18039
19083
  return deleteSessionArtifact(this.boundCallRPC, artifactId);
18040
19084
  }
18041
- // -------------------------------------------------------------------------
18042
- // Navigation (optional, deprecated)
18043
- // -------------------------------------------------------------------------
18044
- /**
18045
- * @deprecated Pass `onSessionCreated` to `ChatSessionProvider` instead.
18046
- */
18047
- navigateToSession(sessionId) {
18048
- if (typeof window !== "undefined") {
18049
- window.location.href = `/chat/${sessionId}`;
18050
- }
18051
- }
18052
19085
  };
18053
19086
  function createHttpDataSource(config) {
18054
19087
  return new HttpDataSource(config);
@@ -18067,6 +19100,7 @@ exports.AttachmentGrid = MemoizedAttachmentGrid;
18067
19100
  exports.AttachmentPreview = AttachmentPreview_default;
18068
19101
  exports.AttachmentUpload = AttachmentUpload_default;
18069
19102
  exports.Avatar = Avatar;
19103
+ exports.AvatarStack = AvatarStack;
18070
19104
  exports.BiChatLayout = BiChatLayout;
18071
19105
  exports.Bubble = Bubble;
18072
19106
  exports.CHART_VISUAL = CHART_VISUAL;
@@ -18110,6 +19144,7 @@ exports.SessionArtifactList = SessionArtifactList;
18110
19144
  exports.SessionArtifactPreview = SessionArtifactPreview;
18111
19145
  exports.SessionArtifactsPanel = SessionArtifactsPanel;
18112
19146
  exports.SessionItem = SessionItem_default;
19147
+ exports.SessionMembersModal = SessionMembersModal;
18113
19148
  exports.SessionSkeleton = SessionSkeleton;
18114
19149
  exports.Sidebar = Sidebar2;
18115
19150
  exports.Skeleton = MemoizedSkeleton;
@@ -18176,6 +19211,7 @@ exports.useActionButtonContext = useActionButtonContext;
18176
19211
  exports.useAttachments = useAttachments;
18177
19212
  exports.useAutoScroll = useAutoScroll;
18178
19213
  exports.useAvatarContext = useAvatarContext;
19214
+ exports.useBichatRouter = useBichatRouter;
18179
19215
  exports.useBubbleContext = useBubbleContext;
18180
19216
  exports.useChatInput = useChatInput;
18181
19217
  exports.useChatMessaging = useChatMessaging;
@@ -18183,6 +19219,7 @@ exports.useChatSession = useChatSession;
18183
19219
  exports.useConfig = useConfig;
18184
19220
  exports.useDataTable = useDataTable;
18185
19221
  exports.useFocusTrap = useFocusTrap;
19222
+ exports.useHttpDataSourceConfigFromApplet = useHttpDataSourceConfigFromApplet;
18186
19223
  exports.useImageGallery = useImageGallery;
18187
19224
  exports.useIotaContext = useIotaContext;
18188
19225
  exports.useKeyboardShortcuts = useKeyboardShortcuts;