incognitolm commited on
Commit
31f4a53
Β·
1 Parent(s): fa6106f

Message versions

Browse files
public/css/chat.css CHANGED
@@ -99,13 +99,20 @@
99
 
100
  /* Version navigator */
101
  .msg-version-nav {
 
 
 
102
  display: flex;
103
  align-items: center;
104
  gap: 4px;
105
  font-size: 12px;
106
  color: var(--text-muted);
 
 
107
  }
108
 
 
 
109
  .msg-version-nav button {
110
  display: flex; align-items: center; justify-content: center;
111
  width: 20px; height: 20px; border-radius: 50%;
@@ -118,34 +125,22 @@
118
  .msg-actions {
119
  display: flex;
120
  gap: 3px;
121
- }
122
-
123
- .msg-actions.msg-actions-right {
124
- justify-content: flex-end;
125
- }
126
-
127
- .msg-actions.msg-actions-left {
128
- justify-content: flex-start;
129
- }
130
-
131
- .msg-controls {
132
- display: flex;
133
- flex-direction: column;
134
- gap: 4px;
135
  opacity: 0;
136
  transition: opacity var(--transition);
137
  pointer-events: none;
 
 
 
 
138
  }
139
 
140
- .msg-controls.msg-controls-right {
141
- align-items: flex-end;
142
- }
143
-
144
- .msg-controls.msg-controls-left {
145
- align-items: flex-start;
146
  }
147
 
148
- .msg-group:hover .msg-controls {
149
  opacity: 1;
150
  pointer-events: auto;
151
  }
 
99
 
100
  /* Version navigator */
101
  .msg-version-nav {
102
+ position: absolute;
103
+ bottom: -4px;
104
+ right: 0;
105
  display: flex;
106
  align-items: center;
107
  gap: 4px;
108
  font-size: 12px;
109
  color: var(--text-muted);
110
+ opacity: 0;
111
+ transition: opacity var(--transition);
112
  }
113
 
114
+ .msg-group:hover .msg-version-nav { opacity: 1; }
115
+
116
  .msg-version-nav button {
117
  display: flex; align-items: center; justify-content: center;
118
  width: 20px; height: 20px; border-radius: 50%;
 
125
  .msg-actions {
126
  display: flex;
127
  gap: 3px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  opacity: 0;
129
  transition: opacity var(--transition);
130
  pointer-events: none;
131
+ /* Default: left-aligned (assistant messages) */
132
+ position: absolute;
133
+ bottom: 4px;
134
+ left: 0;
135
  }
136
 
137
+ /* Right-aligned (user messages) */
138
+ .msg-actions.msg-actions-right {
139
+ left: auto;
140
+ right: 0;
 
 
141
  }
142
 
143
+ .msg-group:hover .msg-actions {
144
  opacity: 1;
145
  pointer-events: auto;
146
  }
public/css/input.css CHANGED
@@ -71,6 +71,22 @@
71
  }
72
  .attach-btn:hover { color: var(--text); background: var(--bg-hover); }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  /* ── Bottom input bar ────────────────────────────────────────────────────── */
75
  .bottom-input-bar {
76
  flex-shrink: 0;
@@ -112,16 +128,6 @@
112
  }
113
  .attach-btn-bottom:hover { color: var(--text); background: var(--bg-hover); }
114
 
115
- .back-btn-bottom {
116
- flex-shrink: 0;
117
- display: flex; align-items: center; justify-content: center;
118
- width: 30px; height: 30px; border-radius: 50%;
119
- color: var(--text-muted);
120
- margin-bottom: 4px;
121
- transition: color var(--transition), background var(--transition);
122
- }
123
- .back-btn-bottom:hover { color: var(--text); background: var(--bg-hover); }
124
-
125
  .bottom-textarea-wrap {
126
  flex: 1; min-width: 0;
127
  display: flex; flex-direction: column; gap: 6px;
 
71
  }
72
  .attach-btn:hover { color: var(--text); background: var(--bg-hover); }
73
 
74
+ /* Back button */
75
+ .back-row {
76
+ text-align: center;
77
+ margin-bottom: 8px;
78
+ }
79
+ .back-btn {
80
+ padding: 4px 8px;
81
+ font-size: 12px;
82
+ color: var(--text-muted);
83
+ background: none;
84
+ border: none;
85
+ cursor: pointer;
86
+ transition: color var(--transition);
87
+ }
88
+ .back-btn:hover { color: var(--text); }
89
+
90
  /* ── Bottom input bar ────────────────────────────────────────────────────── */
91
  .bottom-input-bar {
92
  flex-shrink: 0;
 
128
  }
129
  .attach-btn-bottom:hover { color: var(--text); background: var(--bg-hover); }
130
 
 
 
 
 
 
 
 
 
 
 
131
  .bottom-textarea-wrap {
132
  flex: 1; min-width: 0;
133
  display: flex; flex-direction: column; gap: 6px;
public/index.html CHANGED
@@ -122,10 +122,10 @@
122
 
123
  <!-- Bottom input (active during chat) -->
124
  <div id="bottom-input-bar" class="bottom-input-bar hidden">
 
 
 
125
  <div class="bottom-input-wrap">
126
- <button id="bottom-back-btn" class="back-btn-bottom hidden" title="Back to conversation">
127
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
128
- </button>
129
  <button id="bottom-attach-btn" class="attach-btn-bottom" title="Attach">
130
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
131
  </button>
 
122
 
123
  <!-- Bottom input (active during chat) -->
124
  <div id="bottom-input-bar" class="bottom-input-bar hidden">
125
+ <div id="back-row" class="back-row hidden">
126
+ <button id="back-btn" class="back-btn" title="Back to conversation">← Back</button>
127
+ </div>
128
  <div class="bottom-input-wrap">
 
 
 
129
  <button id="bottom-attach-btn" class="attach-btn-bottom" title="Attach">
130
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
131
  </button>
public/js/app.js CHANGED
@@ -5,7 +5,7 @@ import {
5
  createNewSession, showWelcomeScreen,
6
  switchSession, currentSessionId, onSessionChange,
7
  } from './sessions.js';
8
- import { submitMessage, renderSession, setActiveSession, getIsStreaming } from './chat.js';
9
  import { openAuthModal, closeModal, openPasteEditor } from './modals.js';
10
  import { openSettings, applyTheme } from './settings.js';
11
  import { showNotification, autoResize, escHtml } from './ui.js';
@@ -128,7 +128,8 @@ function triggerBottomSend() {
128
  if (!text && attachments.length === 0) return;
129
  if (bottomInput) { bottomInput.value = ''; autoResize(bottomInput, 6); }
130
  clearFilePreviewRow();
131
- doSend(text || '', attachments);
 
132
  }
133
 
134
  document.querySelectorAll('.tool-btn-sm').forEach(btn =>
@@ -136,8 +137,8 @@ document.querySelectorAll('.tool-btn-sm').forEach(btn =>
136
 
137
  // ── Core send ─────────────────────────────────────────────────────────────
138
 
139
- function doSend(text, attachments = []) {
140
- submitMessage(text, attachments);
141
  }
142
 
143
  // ── Attachments ───────────────────────────────────────────────────────────
 
5
  createNewSession, showWelcomeScreen,
6
  switchSession, currentSessionId, onSessionChange,
7
  } from './sessions.js';
8
+ import { submitMessage, renderSession, setActiveSession, getIsStreaming, getReEditingIndex } from './chat.js';
9
  import { openAuthModal, closeModal, openPasteEditor } from './modals.js';
10
  import { openSettings, applyTheme } from './settings.js';
11
  import { showNotification, autoResize, escHtml } from './ui.js';
 
128
  if (!text && attachments.length === 0) return;
129
  if (bottomInput) { bottomInput.value = ''; autoResize(bottomInput, 6); }
130
  clearFilePreviewRow();
131
+ const editIndex = getReEditingIndex();
132
+ doSend(text || '', attachments, editIndex);
133
  }
134
 
135
  document.querySelectorAll('.tool-btn-sm').forEach(btn =>
 
137
 
138
  // ── Core send ─────────────────────────────────────────────────────────────
139
 
140
+ function doSend(text, attachments = [], editIndex = undefined) {
141
+ submitMessage(text, attachments, false, editIndex);
142
  }
143
 
144
  // ── Attachments ───────────────────────────────────────────────────────────
public/js/chat.js CHANGED
@@ -1,6 +1,6 @@
1
  // chat.js β€” Chat rendering, streaming, versioning, editing
2
  import { send, on, off } from './ws.js';
3
- import { sessions, currentSessionId } from './sessions.js';
4
  import {
5
  renderMarkdown, attachCodeCopyListeners, attachSvgPanelListeners,
6
  escHtml, showNotification, autoResize,
@@ -12,9 +12,9 @@ let streamingBubble = null;
12
  let streamingText = '';
13
  let autoScroll = true;
14
  let pendingAssets = [];
 
15
 
16
- let editingMessageIndex = null;
17
- let originalHistory = null;
18
 
19
  export function setActiveSession(id) { activeSessionId = id; }
20
  export function getIsStreaming() { return isStreaming; }
@@ -35,6 +35,11 @@ on('chat:messageEdited', (msg) => {
35
  // Update the session history
36
  const s = sessions.find(s => s.id === msg.sessionId);
37
  if (s) s.history = msg.history;
 
 
 
 
 
38
  }
39
  });
40
  on('chat:versionSelected', (msg) => { if (msg.sessionId === activeSessionId) renderHistory(msg.history); });
@@ -46,42 +51,7 @@ on('ws:connected', () => {
46
  }
47
  });
48
 
49
- function enterEditMode(messageIndex) {
50
- const session = sessions.find(s => s.id === activeSessionId);
51
- if (!session) return;
52
- editingMessageIndex = messageIndex;
53
- originalHistory = [...session.history];
54
- const truncatedHistory = session.history.slice(0, messageIndex + 1);
55
- session.history = truncatedHistory;
56
- renderHistory(truncatedHistory);
57
- // Move content to bottom input
58
- const msg = truncatedHistory[messageIndex];
59
- const bottomInput = document.getElementById('bottom-input');
60
- if (bottomInput) {
61
- bottomInput.value = msg.content;
62
- autoResize(bottomInput);
63
- bottomInput.focus();
64
- }
65
- // Show back button
66
- document.getElementById('bottom-back-btn')?.classList.remove('hidden');
67
- // Clear attachments
68
- clearFilePreviewRow();
69
- }
70
-
71
- function exitEditMode() {
72
- if (originalHistory) {
73
- const session = sessions.find(s => s.id === activeSessionId);
74
- if (session) session.history = originalHistory;
75
- renderHistory(originalHistory);
76
- }
77
- editingMessageIndex = null;
78
- originalHistory = null;
79
- // Clear input
80
- const bottomInput = document.getElementById('bottom-input');
81
- if (bottomInput) bottomInput.value = '';
82
- // Hide back button
83
- document.getElementById('bottom-back-btn')?.classList.add('hidden');
84
- }
85
 
86
  export function renderSession(session) {
87
  if (!session || !session.history?.length) { showWelcome(); return; }
@@ -170,9 +140,9 @@ function appendUserMsg(box, msg, index) {
170
  controls.className = 'msg-controls msg-controls-right';
171
  controls.appendChild(buildActions([
172
  { icon: 'πŸ“‹', title: 'Copy', fn: () => copyText(text) },
173
- { icon: '✏️', title: 'Edit', fn: () => enterEditMode(index) },
174
  ], 'right'));
175
- // No versions for user messages
176
  wrap.appendChild(controls);
177
 
178
  box.appendChild(wrap);
@@ -384,6 +354,27 @@ function startAssistantEdit(wrap, index, msg) {
384
  ta.setSelectionRange(ta.value.length, ta.value.length);
385
  }
386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  function makeUserEditTextarea(value) {
388
  const ta = document.createElement('textarea');
389
  ta.value = value;
@@ -495,7 +486,6 @@ function onChatDone(msg) {
495
  pendingAssets = [];
496
  }
497
  }
498
- if (editingMessageIndex !== null) exitEditMode();
499
  }
500
 
501
  function onChatAborted(msg) {
@@ -560,11 +550,19 @@ function appendAsset(asset) {
560
 
561
  // ── Submit ────────────────────────────────────────────────────────────────
562
 
563
- export function submitMessage(text, attachments = [], regenerate = false) {
564
  if (!text.trim() && attachments.length === 0 && !regenerate) return;
565
  if (isStreaming) { send({ type: 'chat:stop' }); return; }
566
  if (!activeSessionId) return;
567
 
 
 
 
 
 
 
 
 
568
  const images = attachments.filter(a => a.type === 'image');
569
  const textFiles= attachments.filter(a => a.type === 'text');
570
 
@@ -664,10 +662,21 @@ function openImageModal(src) {
664
  import('./modals.js').then(m => m.openImageModal(src));
665
  }
666
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
  // Scroll tracking
668
  document.getElementById('chat-view')?.addEventListener('scroll', e => {
669
  const el = e.target;
670
  autoScroll = el.scrollHeight - el.scrollTop - el.clientHeight < 60;
671
- }, true);
672
-
673
- document.getElementById('bottom-back-btn')?.addEventListener('click', exitEditMode);
 
1
  // chat.js β€” Chat rendering, streaming, versioning, editing
2
  import { send, on, off } from './ws.js';
3
+ import { currentSessionId, getCurrentSession } from './sessions.js';
4
  import {
5
  renderMarkdown, attachCodeCopyListeners, attachSvgPanelListeners,
6
  escHtml, showNotification, autoResize,
 
12
  let streamingText = '';
13
  let autoScroll = true;
14
  let pendingAssets = [];
15
+ let reEditingIndex = null;
16
 
17
+ export function getReEditingIndex() { return reEditingIndex; }
 
18
 
19
  export function setActiveSession(id) { activeSessionId = id; }
20
  export function getIsStreaming() { return isStreaming; }
 
35
  // Update the session history
36
  const s = sessions.find(s => s.id === msg.sessionId);
37
  if (s) s.history = msg.history;
38
+ // If edited message is user, regenerate the response
39
+ const editedMsg = msg.history[msg.messageIndex];
40
+ if (editedMsg && editedMsg.role === 'user') {
41
+ submitMessage('', [], true);
42
+ }
43
  }
44
  });
45
  on('chat:versionSelected', (msg) => { if (msg.sessionId === activeSessionId) renderHistory(msg.history); });
 
51
  }
52
  });
53
 
54
+ // ── Views ─────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  export function renderSession(session) {
57
  if (!session || !session.history?.length) { showWelcome(); return; }
 
140
  controls.className = 'msg-controls msg-controls-right';
141
  controls.appendChild(buildActions([
142
  { icon: 'πŸ“‹', title: 'Copy', fn: () => copyText(text) },
143
+ { icon: '✏️', title: 'Edit', fn: () => startUserReEdit(wrap, index, msg, text) },
144
  ], 'right'));
145
+ if (msg.versions?.length > 1) controls.appendChild(buildVersionNav(msg, index));
146
  wrap.appendChild(controls);
147
 
148
  box.appendChild(wrap);
 
354
  ta.setSelectionRange(ta.value.length, ta.value.length);
355
  }
356
 
357
+ function startUserReEdit(wrap, index, msg, text) {
358
+ reEditingIndex = index;
359
+ // Truncate the displayed history to this message
360
+ const session = getCurrentSession();
361
+ if (session) {
362
+ const truncated = session.history.slice(0, index + 1);
363
+ renderHistory(truncated);
364
+ }
365
+ // Put the text in bottom input
366
+ const bottomInput = document.getElementById('bottom-input');
367
+ if (bottomInput) {
368
+ bottomInput.value = text;
369
+ autoResize(bottomInput);
370
+ bottomInput.focus();
371
+ }
372
+ // Show back button
373
+ document.getElementById('back-row')?.classList.remove('hidden');
374
+ // Clear attachments
375
+ clearFilePreviewRow();
376
+ }
377
+
378
  function makeUserEditTextarea(value) {
379
  const ta = document.createElement('textarea');
380
  ta.value = value;
 
486
  pendingAssets = [];
487
  }
488
  }
 
489
  }
490
 
491
  function onChatAborted(msg) {
 
550
 
551
  // ── Submit ────────────────────────────────────────────────────────────────
552
 
553
+ export function submitMessage(text, attachments = [], regenerate = false, editIndex = undefined) {
554
  if (!text.trim() && attachments.length === 0 && !regenerate) return;
555
  if (isStreaming) { send({ type: 'chat:stop' }); return; }
556
  if (!activeSessionId) return;
557
 
558
+ if (editIndex !== undefined) {
559
+ // Send edit
560
+ send({ type: 'chat:editMessage', sessionId: activeSessionId, messageIndex: editIndex, newContent: text });
561
+ reEditingIndex = null;
562
+ document.getElementById('back-row')?.classList.add('hidden');
563
+ return;
564
+ }
565
+
566
  const images = attachments.filter(a => a.type === 'image');
567
  const textFiles= attachments.filter(a => a.type === 'text');
568
 
 
662
  import('./modals.js').then(m => m.openImageModal(src));
663
  }
664
 
665
+ // Back button for re-editing
666
+ document.getElementById('back-btn')?.addEventListener('click', () => {
667
+ reEditingIndex = null;
668
+ // Restore full history
669
+ const session = getCurrentSession();
670
+ if (session) renderHistory(session.history);
671
+ // Clear input
672
+ const bottomInput = document.getElementById('bottom-input');
673
+ if (bottomInput) bottomInput.value = '';
674
+ // Hide back
675
+ document.getElementById('back-row')?.classList.add('hidden');
676
+ });
677
+
678
  // Scroll tracking
679
  document.getElementById('chat-view')?.addEventListener('scroll', e => {
680
  const el = e.target;
681
  autoScroll = el.scrollHeight - el.scrollTop - el.clientHeight < 60;
682
+ }, true);
 
 
server/wsHandler.js CHANGED
@@ -218,10 +218,7 @@ const handlers = {
218
  m.versions.unshift({ content: newContent, tail: history.slice(messageIndex + 1), timestamp: Date.now() });
219
  m.currentVersionIdx = 0;
220
  m.content = newContent;
221
- let newHistory = history;
222
- if (m.role === 'user') {
223
- newHistory = history.slice(0, messageIndex + 1);
224
- }
225
  if (client.userId) await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory });
226
  else sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory });
227
  safeSend(ws, { type: 'chat:messageEdited', sessionId, messageIndex, message: m, history: newHistory });
 
218
  m.versions.unshift({ content: newContent, tail: history.slice(messageIndex + 1), timestamp: Date.now() });
219
  m.currentVersionIdx = 0;
220
  m.content = newContent;
221
+ const newHistory = history.slice(0, messageIndex + 1);
 
 
 
222
  if (client.userId) await sessionStore.updateUserSession(client.userId, client.accessToken, sessionId, { history: newHistory });
223
  else sessionStore.updateTempSession(client.tempId, sessionId, { history: newHistory });
224
  safeSend(ws, { type: 'chat:messageEdited', sessionId, messageIndex, message: m, history: newHistory });