clay-server 2.26.0-beta.9 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/public/app.js CHANGED
@@ -2,7 +2,7 @@ import { avatarUrl, userAvatarUrl, mateAvatarUrl } from './modules/avatar.js';
2
2
  import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
3
3
  import { refreshIcons, iconHtml } from './modules/icons.js';
4
4
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
5
- import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge, updateSessionBadge, updateProjectBadge, closeDmUserPicker, spawnDustParticles, openMobileSheet, setMobileSheetMateData } from './modules/sidebar.js';
5
+ import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge, updateSessionBadge, updateProjectBadge, closeDmUserPicker, spawnDustParticles, openMobileSheet, setMobileSheetMateData, refreshMobileChatSheet } from './modules/sidebar.js';
6
6
  import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile, handleMateSearchResults } from './modules/mate-sidebar.js';
7
7
  import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
8
8
  import { initMateMemory, renderMemoryList, hideMemory } from './modules/mate-memory.js';
@@ -32,7 +32,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
32
32
  import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
33
33
  import { initLongPress } from './modules/longpress.js';
34
34
  import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
35
- import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState } from './modules/debate.js';
35
+ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState, exportDebateAsPdf, renderMcpDebateProposal } from './modules/debate.js';
36
36
 
37
37
  // --- Base path for multi-project routing ---
38
38
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -565,6 +565,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
565
565
  // --- DM Mode Functions ---
566
566
  function openDm(targetUserId) {
567
567
  if (!ws || ws.readyState !== 1) return;
568
+ // Persist DM state for refresh recovery
569
+ try { localStorage.setItem("clay-active-dm", targetUserId); } catch (e) {}
568
570
  // Check mate skill updates before opening mate DM
569
571
  if (typeof targetUserId === "string" && targetUserId.indexOf("mate_") === 0) {
570
572
  showMateOnboarding(function () {
@@ -816,6 +818,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
816
818
  dmMode = false;
817
819
  dmKey = null;
818
820
  dmTargetUser = null;
821
+ try { localStorage.removeItem("clay-active-dm"); } catch (e) {}
819
822
  setCurrentDmUser(null);
820
823
 
821
824
  var mainCol = document.getElementById("main-column");
@@ -1798,9 +1801,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
1798
1801
  };
1799
1802
  initSidebar(sidebarCtx);
1800
1803
  initIconStrip(sidebarCtx);
1801
- initMateSidebar(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; });
1802
- initMateKnowledge(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; });
1803
- initMateMemory(function () { return (dmMode && dmTargetUser && dmTargetUser.isMate) ? ws : null; }, { onShow: function () { hideKnowledge(); hideNotes(); } });
1804
+ var wsGetter = function () { return ws; };
1805
+ initMateSidebar(wsGetter);
1806
+ initMateKnowledge(wsGetter);
1807
+ initMateMemory(wsGetter, { onShow: function () { hideKnowledge(); hideNotes(); } });
1804
1808
  initMateWizard(
1805
1809
  function (msg) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(msg)); },
1806
1810
  function (mate) { handleMateCreatedInApp(mate); }
@@ -3040,6 +3044,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3040
3044
  var div = document.createElement("div");
3041
3045
  div.className = "msg-user" + (isOtherUser ? " msg-user-other" : "");
3042
3046
  div.dataset.turn = ++turnCounter;
3047
+ if (shouldGroupMessage("msg-user")) div.classList.add("grouped");
3043
3048
  var bubble = document.createElement("div");
3044
3049
  bubble.className = "bubble";
3045
3050
  bubble.dir = "auto";
@@ -3130,8 +3135,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3130
3135
  header.appendChild(nameSpan);
3131
3136
  var timeSpan = document.createElement("span");
3132
3137
  timeSpan.className = "dm-bubble-time";
3133
- var nowH = new Date();
3134
- timeSpan.textContent = String(nowH.getHours()).padStart(2, "0") + ":" + String(nowH.getMinutes()).padStart(2, "0");
3138
+ timeSpan.textContent = getMsgTime();
3135
3139
  header.appendChild(timeSpan);
3136
3140
  contentWrap.appendChild(header);
3137
3141
  contentWrap.appendChild(bubble);
@@ -3140,10 +3144,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3140
3144
  // Action bar below bubble (icons visible on hover)
3141
3145
  var actions = document.createElement("div");
3142
3146
  actions.className = "msg-actions";
3143
- var now = new Date();
3144
- var timeStr = String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0");
3145
3147
  actions.innerHTML =
3146
- '<span class="msg-action-time">' + timeStr + '</span>' +
3148
+ '<span class="msg-action-time">' + getMsgTime() + '</span>' +
3147
3149
  '<button class="msg-action-btn msg-action-copy" type="button" title="Copy">' + iconHtml("copy") + '</button>' +
3148
3150
  '<button class="msg-action-btn msg-action-fork" type="button" title="Fork">' + iconHtml("git-branch") + '</button>' +
3149
3151
  '<button class="msg-action-btn msg-action-rewind msg-user-rewind-btn" type="button" title="Rewind">' + iconHtml("rotate-ccw") + '</button>' +
@@ -3164,11 +3166,33 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3164
3166
  forceScrollToBottom();
3165
3167
  }
3166
3168
 
3169
+ // Track the timestamp of the current message being processed (from history _ts or now)
3170
+ var currentMsgTs = null;
3171
+
3172
+ function getMsgTime() {
3173
+ var d = currentMsgTs ? new Date(currentMsgTs) : new Date();
3174
+ return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
3175
+ }
3176
+
3177
+ function shouldGroupMessage(senderClass) {
3178
+ // Skip grouping during history replay if no timestamp data
3179
+ if (replayingHistory && !currentMsgTs) return false;
3180
+ var prev = messagesEl.lastElementChild;
3181
+ if (!prev || !prev.classList.contains(senderClass)) return false;
3182
+ var prevTime = prev.querySelector(".dm-bubble-time");
3183
+ if (!prevTime) return false;
3184
+ return prevTime.textContent === getMsgTime();
3185
+ }
3186
+
3167
3187
  function ensureAssistantBlock() {
3168
3188
  if (!currentMsgEl) {
3169
3189
  currentMsgEl = document.createElement("div");
3170
3190
  currentMsgEl.className = "msg-assistant";
3171
3191
  currentMsgEl.dataset.turn = turnCounter;
3192
+
3193
+ var grouped = shouldGroupMessage("msg-assistant");
3194
+ if (grouped) currentMsgEl.classList.add("grouped");
3195
+
3172
3196
  // Always render avatar + header structure (CSS controls visibility)
3173
3197
  var _isDm2 = document.body.classList.contains("mate-dm-active") && document.body.dataset.mateAvatarUrl;
3174
3198
  var avi = document.createElement("img");
@@ -3187,8 +3211,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3187
3211
  header.appendChild(nameSpan);
3188
3212
  var timeSpan = document.createElement("span");
3189
3213
  timeSpan.className = "dm-bubble-time";
3190
- var nowA = new Date();
3191
- timeSpan.textContent = String(nowA.getHours()).padStart(2, "0") + ":" + String(nowA.getMinutes()).padStart(2, "0");
3214
+ timeSpan.textContent = getMsgTime();
3192
3215
  header.appendChild(timeSpan);
3193
3216
  contentWrap.appendChild(header);
3194
3217
 
@@ -3692,7 +3715,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3692
3715
  // Auto-switch input to schedule mode: any message typed will be queued for after reset
3693
3716
  var delayUntilReset = msg.resetsAt - Date.now();
3694
3717
  if (delayUntilReset > 0) {
3695
- setScheduleDelayMs(delayUntilReset + 180000); // +3min buffer after reset
3718
+ setScheduleDelayMs(delayUntilReset + 60000); // +1min buffer after reset
3696
3719
  }
3697
3720
  rateLimitResetTimer = setTimeout(function () {
3698
3721
  rateLimitResetsAt = null;
@@ -4120,6 +4143,31 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4120
4143
 
4121
4144
  // Session restore is now server-driven (user-presence.json).
4122
4145
  // Mate DM restore is also server-driven via "restore_mate_dm" message.
4146
+ // Fallback: if server doesn't restore DM within 2s, try localStorage
4147
+ var savedDm = null;
4148
+ try { savedDm = localStorage.getItem("clay-active-dm"); } catch (e) {}
4149
+ if (savedDm && !dmMode && !mateProjectSlug) {
4150
+ var dmFallbackTimer = setTimeout(function () {
4151
+ if (!dmMode && savedDm) {
4152
+ console.log("[dm-restore] Server did not restore DM, using localStorage fallback:", savedDm);
4153
+ openDm(savedDm);
4154
+ }
4155
+ }, 2000);
4156
+ // Cancel fallback if server restores DM first
4157
+ var origHandler = ws.onmessage;
4158
+ var patchedOnce = false;
4159
+ var checkRestore = function (evt) {
4160
+ try {
4161
+ var d = JSON.parse(evt.data);
4162
+ if (d.type === "restore_mate_dm" && !patchedOnce) {
4163
+ patchedOnce = true;
4164
+ clearTimeout(dmFallbackTimer);
4165
+ }
4166
+ } catch (e) {}
4167
+ };
4168
+ ws.addEventListener("message", checkRestore);
4169
+ setTimeout(function () { ws.removeEventListener("message", checkRestore); }, 3000);
4170
+ }
4123
4171
  // Safety: clear returningFromMateDm after initial messages settle
4124
4172
  // (handles case where we connect to a non-main project that won't send restore_mate_dm)
4125
4173
  if (returningFromMateDm) {
@@ -4218,6 +4266,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4218
4266
  }
4219
4267
 
4220
4268
  function processMessage(msg) {
4269
+ // Preserve original timestamp from history replay
4270
+ currentMsgTs = msg._ts || null;
4221
4271
  var isMateDm = dmMode && dmTargetUser && dmTargetUser.isMate;
4222
4272
 
4223
4273
  // DEBUG: trace session/history loading
@@ -4232,6 +4282,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4232
4282
  if (isMateDm) {
4233
4283
  if (msg.type === "session_list") {
4234
4284
  renderMateSessionList(msg.sessions || []);
4285
+ refreshMobileChatSheet();
4235
4286
  // Override title bar with mate name and re-apply color
4236
4287
  var _mdn = (dmTargetUser.displayName || "New Mate");
4237
4288
  if (headerTitleEl) headerTitleEl.textContent = _mdn;
@@ -4337,6 +4388,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4337
4388
  if (dhBar) dhBar.remove();
4338
4389
  var dbBadges = document.querySelectorAll(".debate-header-badge");
4339
4390
  for (var dbi = 0; dbi < dbBadges.length; dbi++) dbBadges[dbi].remove();
4391
+ // Clean up ended mode banner if debate is not active on this session
4392
+ if (debateEndedMode) exitDebateEndedMode();
4340
4393
  }
4341
4394
  scrollToBottom();
4342
4395
  // Scroll to tool element if navigating from file edit history
@@ -4577,9 +4630,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4577
4630
  break;
4578
4631
 
4579
4632
  case "session_list":
4580
- if (isMateDm) {
4581
- renderMateSessionList(msg.sessions || []);
4582
- }
4633
+ renderMateSessionList(msg.sessions || []);
4583
4634
  renderSessionList(msg.sessions || []);
4584
4635
  handlePaletteSessionSwitch();
4585
4636
  break;
@@ -4670,6 +4721,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4670
4721
  break;
4671
4722
 
4672
4723
  case "user_message":
4724
+ if (msg._internal) break;
4673
4725
  resetThinkingGroup();
4674
4726
  if (msg.planContent) {
4675
4727
  setPlanContent(msg.planContent);
@@ -4691,7 +4743,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4691
4743
  header.className = "context-card-header";
4692
4744
  var icon = document.createElement("span");
4693
4745
  icon.className = "context-card-icon";
4694
- icon.textContent = "\uD83D\uDC41";
4746
+ icon.innerHTML = iconHtml("globe");
4695
4747
  header.appendChild(icon);
4696
4748
  var label = document.createElement("span");
4697
4749
  label.textContent = "Viewing tab";
@@ -4715,6 +4767,15 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4715
4767
  if (tabTitle || tabDomain) {
4716
4768
  var meta = document.createElement("div");
4717
4769
  meta.className = "context-card-meta";
4770
+ if (msg.tab.favIconUrl) {
4771
+ var fav = document.createElement("img");
4772
+ fav.className = "context-card-favicon";
4773
+ fav.src = msg.tab.favIconUrl;
4774
+ fav.width = 14;
4775
+ fav.height = 14;
4776
+ fav.onerror = function () { this.style.display = "none"; };
4777
+ meta.appendChild(fav);
4778
+ }
4718
4779
  var titleEl = document.createElement("span");
4719
4780
  titleEl.className = "context-card-title";
4720
4781
  titleEl.textContent = tabTitle;
@@ -4788,6 +4849,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4788
4849
  }
4789
4850
  renderPlanBanner("exit");
4790
4851
  getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
4852
+ } else if (msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) {
4853
+ getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
4791
4854
  } else if (getTodoTools()[msg.name]) {
4792
4855
  getTools()[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
4793
4856
  } else {
@@ -4796,7 +4859,18 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4796
4859
  break;
4797
4860
 
4798
4861
  case "tool_executing":
4799
- if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
4862
+ if ((msg.name === "propose_debate" || (msg.name && msg.name.indexOf("propose_debate") !== -1)) && msg.input) {
4863
+ var _dpTool = getTools()[msg.id];
4864
+ if (_dpTool) {
4865
+ if (_dpTool.el) _dpTool.el.style.display = "none";
4866
+ _dpTool.done = true;
4867
+ _dpTool.hidden = true;
4868
+ removeToolFromGroup(msg.id);
4869
+ }
4870
+ finalizeAssistantBlock();
4871
+ renderMcpDebateProposal(msg.id, msg.input);
4872
+ startUrgentBlink();
4873
+ } else if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
4800
4874
  var askTool = getTools()[msg.id];
4801
4875
  if (askTool) {
4802
4876
  if (askTool.el) askTool.el.style.display = "none";
@@ -5353,6 +5427,23 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5353
5427
  break;
5354
5428
 
5355
5429
  // --- @Mention ---
5430
+ case "mention_processing":
5431
+ // Broadcast: show/hide activity dot on mate avatar across all tabs
5432
+ if (msg.mateId) {
5433
+ var mateContainers = document.querySelectorAll('.icon-strip-mate[data-user-id="' + msg.mateId + '"]');
5434
+ for (var mi = 0; mi < mateContainers.length; mi++) {
5435
+ var dot = mateContainers[mi].querySelector(".icon-strip-status");
5436
+ if (msg.active) {
5437
+ if (dot) dot.classList.add("processing");
5438
+ mateContainers[mi].classList.add("mention-active");
5439
+ } else {
5440
+ if (dot) dot.classList.remove("processing");
5441
+ mateContainers[mi].classList.remove("mention-active");
5442
+ }
5443
+ }
5444
+ }
5445
+ break;
5446
+
5356
5447
  case "mention_start":
5357
5448
  handleMentionStart(msg);
5358
5449
  break;
@@ -5430,6 +5521,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5430
5521
  }
5431
5522
  break;
5432
5523
 
5524
+ case "debate_hand_raised":
5525
+ // Visual feedback: hand is raised, waiting for floor
5526
+ break;
5527
+
5433
5528
  case "debate_comment_queued":
5434
5529
  handleDebateCommentQueued(msg);
5435
5530
  break;
@@ -5446,6 +5541,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5446
5541
  showDebateConcludeConfirm(msg);
5447
5542
  break;
5448
5543
 
5544
+ case "debate_user_floor":
5545
+ showDebateUserFloor(msg);
5546
+ break;
5547
+
5548
+ case "debate_user_floor_done":
5549
+ renderDebateUserFloorDone(msg);
5550
+ break;
5551
+
5449
5552
  case "debate_user_resume":
5450
5553
  renderDebateUserResume(msg);
5451
5554
  break;
@@ -5791,6 +5894,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5791
5894
  isDmMode: function () { return dmMode && !(dmTargetUser && dmTargetUser.isMate); },
5792
5895
  getDmKey: function () { return dmKey; },
5793
5896
  handleDmSend: function () { handleDmSend(); },
5897
+ isDebateEndedMode: function () { return debateEndedMode; },
5898
+ handleDebateEndedSend: function () { handleDebateEndedSend(); },
5899
+ isDebateConcludeMode: function () { return false; },
5900
+ handleDebateConcludeSend: null,
5901
+ isDebateFloorMode: function () { return debateFloorMode; },
5902
+ handleDebateFloorSend: function () { handleDebateFloorSend(); },
5794
5903
  isMateDm: function () { return dmMode && dmTargetUser && dmTargetUser.isMate; },
5795
5904
  getMateName: function () { return dmTargetUser ? (dmTargetUser.displayName || "Mate") : "Mate"; },
5796
5905
  getMateAvatarUrl: function () { return document.body.dataset.mateAvatarUrl || ""; },
@@ -5817,13 +5926,16 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5817
5926
  // --- Debate module ---
5818
5927
  initDebate({
5819
5928
  get ws() { return ws; },
5929
+ sendWs: function (obj) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); },
5820
5930
  messagesEl: messagesEl,
5931
+ addToMessages: function (el) { addToMessages(el); },
5821
5932
  scrollToBottom: scrollToBottom,
5822
5933
  addCopyHandler: addCopyHandler,
5823
5934
  matesList: function () { return cachedMatesList || []; },
5824
5935
  availableBuiltins: function () { return cachedAvailableBuiltins || []; },
5825
5936
  currentMateId: function () { return (dmTargetUser && dmTargetUser.isMate) ? dmTargetUser.id : null; },
5826
5937
  requireSkills: requireSkills,
5938
+ showDebateEndedMode: function (msg) { showDebateEndedMode(msg); },
5827
5939
  });
5828
5940
 
5829
5941
  // --- STT module (voice input via Web Speech API) ---
@@ -7114,6 +7226,159 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7114
7226
  scrollToBottom();
7115
7227
  }
7116
7228
 
7229
+ var debateFloorMode = false;
7230
+
7231
+ var debateEndedMode = false;
7232
+
7233
+ function showDebateEndedMode(msg) {
7234
+ debateEndedMode = true;
7235
+ removeDebateBottomBar();
7236
+ var inputArea = document.getElementById("input-area");
7237
+ if (inputArea) {
7238
+ inputArea.classList.add("debate-floor-mode");
7239
+ inputArea.style.display = "";
7240
+ }
7241
+ var existingBanner = document.getElementById("debate-floor-banner");
7242
+ if (existingBanner) existingBanner.remove();
7243
+ var banner = document.createElement("div");
7244
+ banner.id = "debate-floor-banner";
7245
+ banner.className = "debate-floor-banner";
7246
+ banner.innerHTML = iconHtml("check-circle") + " <span>Debate ended</span>" +
7247
+ '<button class="debate-floor-done-btn" id="debate-ended-resume-btn">Resume</button>' +
7248
+ '<button class="debate-floor-done-btn" id="debate-ended-pdf-btn">' + iconHtml("download") + ' PDF</button>';
7249
+ if (inputArea && inputArea.parentNode) {
7250
+ inputArea.parentNode.insertBefore(banner, inputArea);
7251
+ }
7252
+ refreshIcons();
7253
+ // Resume button
7254
+ var resumeBtn = document.getElementById("debate-ended-resume-btn");
7255
+ if (resumeBtn) {
7256
+ resumeBtn.addEventListener("click", function () {
7257
+ handleDebateEndedSend();
7258
+ });
7259
+ }
7260
+ // PDF button
7261
+ var pdfBtn = document.getElementById("debate-ended-pdf-btn");
7262
+ if (pdfBtn) {
7263
+ pdfBtn.addEventListener("click", function () {
7264
+ pdfBtn.disabled = true;
7265
+ exportDebateAsPdf().then(function () { pdfBtn.disabled = false; }).catch(function () { pdfBtn.disabled = false; });
7266
+ });
7267
+ }
7268
+ var inputEl2 = document.getElementById("input");
7269
+ if (inputEl2) {
7270
+ inputEl2._origPlaceholder = inputEl2._origPlaceholder || inputEl2.placeholder;
7271
+ inputEl2.placeholder = "Continue with a new direction...";
7272
+ }
7273
+ scrollToBottom();
7274
+ }
7275
+
7276
+ function exitDebateEndedMode() {
7277
+ debateEndedMode = false;
7278
+ var inputArea = document.getElementById("input-area");
7279
+ if (inputArea) inputArea.classList.remove("debate-floor-mode");
7280
+ var banner = document.getElementById("debate-floor-banner");
7281
+ if (banner) banner.remove();
7282
+ var inputEl2 = document.getElementById("input");
7283
+ if (inputEl2 && inputEl2._origPlaceholder) {
7284
+ inputEl2.placeholder = inputEl2._origPlaceholder;
7285
+ delete inputEl2._origPlaceholder;
7286
+ }
7287
+ }
7288
+
7289
+ function handleDebateEndedSend() {
7290
+ var text = inputEl.value.trim();
7291
+ if (ws && ws.readyState === 1) {
7292
+ ws.send(JSON.stringify({ type: "debate_conclude_response", action: "continue", text: text }));
7293
+ }
7294
+ inputEl.value = "";
7295
+ exitDebateEndedMode();
7296
+ }
7297
+
7298
+ function showDebateUserFloor(msg) {
7299
+ debateFloorMode = true;
7300
+ // Remove debate bottom bar and show input area in floor mode
7301
+ removeDebateBottomBar();
7302
+ var inputArea = document.getElementById("input-area");
7303
+ if (inputArea) {
7304
+ inputArea.classList.add("debate-floor-mode");
7305
+ inputArea.style.display = "";
7306
+ }
7307
+ // Add floor banner above input
7308
+ var existingBanner = document.getElementById("debate-floor-banner");
7309
+ if (existingBanner) existingBanner.remove();
7310
+ var banner = document.createElement("div");
7311
+ banner.id = "debate-floor-banner";
7312
+ banner.className = "debate-floor-banner";
7313
+ banner.innerHTML = iconHtml("mic") + " <span>You have the floor</span>" +
7314
+ '<button class="debate-floor-done-btn" id="debate-floor-done-btn">Pass</button>';
7315
+ if (inputArea && inputArea.parentNode) {
7316
+ inputArea.parentNode.insertBefore(banner, inputArea);
7317
+ }
7318
+ refreshIcons();
7319
+ // Done button: exit floor mode without sending
7320
+ var doneBtn = document.getElementById("debate-floor-done-btn");
7321
+ if (doneBtn) {
7322
+ doneBtn.addEventListener("click", function () {
7323
+ // Pass without speaking: resume debate
7324
+ if (ws && ws.readyState === 1) {
7325
+ ws.send(JSON.stringify({ type: "debate_user_floor_response", text: "(The user passed without speaking)" }));
7326
+ }
7327
+ exitDebateFloorMode();
7328
+ showDebateBottomBar("live");
7329
+ });
7330
+ }
7331
+ // Update placeholder
7332
+ var inputEl = document.getElementById("input");
7333
+ if (inputEl) {
7334
+ inputEl._origPlaceholder = inputEl.placeholder;
7335
+ inputEl.placeholder = "Share your thoughts with the panel...";
7336
+ inputEl.focus();
7337
+ }
7338
+ scrollToBottom();
7339
+ }
7340
+
7341
+ function exitDebateFloorMode() {
7342
+ debateFloorMode = false;
7343
+ var inputArea = document.getElementById("input-area");
7344
+ if (inputArea) inputArea.classList.remove("debate-floor-mode");
7345
+ var banner = document.getElementById("debate-floor-banner");
7346
+ if (banner) banner.remove();
7347
+ var inputEl = document.getElementById("input");
7348
+ if (inputEl && inputEl._origPlaceholder) {
7349
+ inputEl.placeholder = inputEl._origPlaceholder;
7350
+ delete inputEl._origPlaceholder;
7351
+ }
7352
+ }
7353
+
7354
+ function handleDebateFloorSend() {
7355
+ var text = inputEl.value.trim();
7356
+ if (!text) return;
7357
+ if (ws && ws.readyState === 1) {
7358
+ ws.send(JSON.stringify({ type: "debate_user_floor_response", text: text }));
7359
+ }
7360
+ inputEl.value = "";
7361
+ exitDebateFloorMode();
7362
+ showDebateBottomBar("live");
7363
+ }
7364
+
7365
+ function renderDebateUserFloorDone(msg) {
7366
+ if (!messagesEl) return;
7367
+ var el = document.createElement("div");
7368
+ el.className = "debate-user-comment";
7369
+ var label = document.createElement("span");
7370
+ label.className = "debate-comment-label";
7371
+ label.innerHTML = iconHtml("mic") + " User:";
7372
+ var textEl = document.createElement("div");
7373
+ textEl.className = "debate-comment-text";
7374
+ textEl.textContent = msg.text || "";
7375
+ el.appendChild(label);
7376
+ el.appendChild(textEl);
7377
+ messagesEl.appendChild(el);
7378
+ refreshIcons();
7379
+ scrollToBottom();
7380
+ }
7381
+
7117
7382
  // Legacy handler kept for compatibility
7118
7383
  function showDebateSticky(phase, msg) {
7119
7384
  if (phase === "ended" || phase === "hide") {
@@ -7136,7 +7401,13 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7136
7401
  return;
7137
7402
  }
7138
7403
 
7139
- // Add badges next to header title
7404
+ // Show bottom bar regardless of header availability
7405
+ if (phase === "live") {
7406
+ debateHandRaiseOpen = false;
7407
+ showDebateBottomBar("live");
7408
+ }
7409
+
7410
+ // Add badges next to header title (optional, may not exist on mobile)
7140
7411
  var headerTitle = document.getElementById("header-title");
7141
7412
  if (!headerTitle) return;
7142
7413
 
@@ -7156,9 +7427,6 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7156
7427
  roundBadge.id = "debate-header-round";
7157
7428
  roundBadge.textContent = "R" + ((msg && msg.round) || 1);
7158
7429
  liveBadge.after(roundBadge);
7159
-
7160
- debateHandRaiseOpen = false;
7161
- showDebateBottomBar("live");
7162
7430
  }
7163
7431
  }
7164
7432
 
@@ -7177,6 +7445,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7177
7445
  bar.innerHTML =
7178
7446
  '<div class="debate-bottom-inner">' +
7179
7447
  '<button class="debate-bottom-hand" id="debate-bottom-hand">' + iconHtml("hand") + ' Raise hand</button>' +
7448
+ '<span class="debate-bottom-waiting hidden" id="debate-bottom-waiting">' + iconHtml("loader") + ' You will get the floor after the current speaker</span>' +
7180
7449
  '<button class="debate-bottom-stop" id="debate-bottom-stop">' + iconHtml("square") + ' Stop</button>' +
7181
7450
  '</div>';
7182
7451
 
@@ -7184,6 +7453,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7184
7453
  inputArea.style.display = "none";
7185
7454
  refreshIcons();
7186
7455
 
7456
+ // Restore raised state if hand was already raised
7457
+ if (debateHandRaiseOpen) {
7458
+ var handBtn = document.getElementById("debate-bottom-hand");
7459
+ var waitingEl = document.getElementById("debate-bottom-waiting");
7460
+ if (handBtn) { handBtn.classList.add("raised"); handBtn.classList.add("hidden"); }
7461
+ if (waitingEl) waitingEl.classList.remove("hidden");
7462
+ }
7463
+
7187
7464
  document.getElementById("debate-bottom-hand").addEventListener("click", function () {
7188
7465
  toggleDebateHandRaise();
7189
7466
  });
@@ -7223,16 +7500,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7223
7500
  showDebateBottomBar("live");
7224
7501
  });
7225
7502
  if (textArea) {
7226
- textArea.focus();
7227
7503
  textArea.addEventListener("keydown", function (e) {
7228
7504
  if (e.key === "Enter" && !e.shiftKey) {
7229
7505
  e.preventDefault();
7230
7506
  document.getElementById("debate-bottom-continue").click();
7231
7507
  }
7232
7508
  });
7233
- textArea.addEventListener("input", function () {
7234
- debateAutoResize(textArea, 12);
7235
- });
7236
7509
  }
7237
7510
  }
7238
7511
  }
@@ -7253,74 +7526,33 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
7253
7526
  var handBar = document.getElementById("debate-hand-raise-bar");
7254
7527
  if (handBar) handBar.remove();
7255
7528
  debateHandRaiseOpen = false;
7529
+ // Clean up floor/ended modes
7530
+ if (debateFloorMode) exitDebateFloorMode();
7531
+ if (debateEndedMode) exitDebateEndedMode();
7256
7532
  // Restore input area
7257
7533
  var inputArea = document.getElementById("input-area");
7258
7534
  if (inputArea) inputArea.style.display = "";
7259
7535
  }
7260
7536
 
7261
7537
  function toggleDebateHandRaise(forceState) {
7262
- var show = typeof forceState === "boolean" ? forceState : !debateHandRaiseOpen;
7263
- debateHandRaiseOpen = show;
7264
-
7265
- var existing = document.getElementById("debate-hand-raise-bar");
7266
- if (!show) {
7267
- if (existing) existing.remove();
7268
- return;
7269
- }
7270
- if (existing) {
7271
- var inp = existing.querySelector(".debate-hand-input");
7272
- if (inp) { inp.value = ""; inp.focus(); }
7273
- return;
7274
- }
7275
-
7276
- // Create hand raise bar above input area
7277
- var bar = document.createElement("div");
7278
- bar.id = "debate-hand-raise-bar";
7279
- bar.className = "debate-hand-raise-bar";
7280
- bar.innerHTML =
7281
- '<div class="debate-hand-raise-inner">' +
7282
- '<span class="debate-hand-raise-label">' + iconHtml("hand") + ' Your comment:</span>' +
7283
- '<textarea class="debate-hand-input" rows="1" placeholder="Type your comment..."></textarea>' +
7284
- '<button class="debate-hand-send">Send</button>' +
7285
- '<button class="debate-hand-cancel">Cancel</button>' +
7286
- '</div>';
7287
-
7288
- var inputArea = document.getElementById("input-area");
7289
- if (inputArea && inputArea.parentNode) {
7290
- inputArea.parentNode.insertBefore(bar, inputArea);
7291
- }
7292
- refreshIcons();
7293
-
7294
- var textarea = bar.querySelector(".debate-hand-input");
7295
- var sendBtn = bar.querySelector(".debate-hand-send");
7296
- var cancelBtn = bar.querySelector(".debate-hand-cancel");
7297
-
7298
- if (textarea) {
7299
- textarea.focus();
7300
- textarea.addEventListener("input", function () {
7301
- debateAutoResize(textarea, 12);
7302
- });
7538
+ var raise = typeof forceState === "boolean" ? forceState : !debateHandRaiseOpen;
7539
+ debateHandRaiseOpen = raise;
7540
+
7541
+ // Update UI: hide hand button, show waiting message
7542
+ var handBtn = document.getElementById("debate-bottom-hand");
7543
+ var waitingEl = document.getElementById("debate-bottom-waiting");
7544
+ if (raise) {
7545
+ if (handBtn) { handBtn.classList.add("raised"); handBtn.classList.add("hidden"); }
7546
+ if (waitingEl) waitingEl.classList.remove("hidden");
7547
+ } else {
7548
+ if (handBtn) { handBtn.classList.remove("raised"); handBtn.classList.remove("hidden"); }
7549
+ if (waitingEl) waitingEl.classList.add("hidden");
7303
7550
  }
7304
7551
 
7305
- sendBtn.addEventListener("click", function () {
7306
- var text = textarea ? textarea.value.trim() : "";
7307
- if (!text) return;
7308
- if (ws && ws.readyState === 1) {
7309
- ws.send(JSON.stringify({ type: "debate_comment", text: text }));
7310
- }
7311
- toggleDebateHandRaise(false);
7312
- });
7313
-
7314
- cancelBtn.addEventListener("click", function () {
7315
- toggleDebateHandRaise(false);
7316
- });
7317
-
7318
- if (textarea) {
7319
- textarea.addEventListener("keydown", function (e) {
7320
- if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendBtn.click(); }
7321
- if (e.key === "Escape") { toggleDebateHandRaise(false); }
7322
- });
7552
+ if (raise && ws && ws.readyState === 1) {
7553
+ ws.send(JSON.stringify({ type: "debate_hand_raise" }));
7323
7554
  }
7555
+ // Floor mode will be activated when server sends debate_user_floor
7324
7556
  }
7325
7557
 
7326
7558
  function sendDebateStickyComment() {