@@ -1180,68 +1180,40 @@ messagesDiv.addEventListener('click', (e) => {
11801180 return ;
11811181 }
11821182
1183- // Delete message
1183+ // Delete message (user bubble only; bot bubbles intentionally lack a
1184+ // delete button — removing only the bot reply would leave an orphan
1185+ // user message that breaks LLM context alternation).
11841186 const deleteBtn = e . target . closest ( '.delete-msg-btn' ) ;
11851187 if ( deleteBtn ) {
11861188 e . preventDefault ( ) ;
11871189 const userMsgEl = deleteBtn . closest ( '.user-message-group' ) ;
1188- const botMsgEl = deleteBtn . closest ( '.flex.gap-3:not(.user-message-group)' ) ;
1189-
1190- if ( userMsgEl ) {
1191- showConfirmModal ( t ( 'delete_message_title' ) , t ( 'delete_message_confirm' ) , ( ) => {
1192- let nextSibling = userMsgEl . nextElementSibling ;
1193- let botReplyEl = null ;
1194- while ( nextSibling ) {
1195- if ( nextSibling . classList && nextSibling . classList . contains ( 'flex' ) && nextSibling . classList . contains ( 'gap-3' ) && ! nextSibling . classList . contains ( 'user-message-group' ) ) {
1196- botReplyEl = nextSibling ;
1197- break ;
1198- }
1199- nextSibling = nextSibling . nextElementSibling ;
1200- }
1201- userMsgEl . remove ( ) ;
1202- if ( botReplyEl ) botReplyEl . remove ( ) ;
1203-
1204- const userSeq = userMsgEl . dataset . seq ;
1205- if ( userSeq ) {
1206- fetch ( '/api/messages/delete' , {
1207- method : 'POST' ,
1208- headers : { 'Content-Type' : 'application/json' } ,
1209- body : JSON . stringify ( { session_id : sessionId , user_seq : parseInt ( userSeq ) } )
1210- } ) . then ( r => r . json ( ) ) . then ( data => {
1211- if ( data . status === 'success' ) console . log ( `Deleted ${ data . deleted } messages` ) ;
1212- } ) . catch ( err => console . error ( 'Failed to delete:' , err ) ) ;
1213- }
1214- } ) ;
1215- } else if ( botMsgEl ) {
1216- showConfirmModal ( t ( 'delete_message_title' ) , t ( 'delete_message_confirm' ) , ( ) => {
1217- // Find the preceding user message to get its seq
1218- let prevUserEl = botMsgEl . previousElementSibling ;
1219- while ( prevUserEl && ! prevUserEl . classList . contains ( 'user-message-group' ) ) {
1220- prevUserEl = prevUserEl . previousElementSibling ;
1221- }
1222-
1223- // Delete from database (keep user message, only delete bot reply)
1224- if ( prevUserEl ) {
1225- const userSeq = prevUserEl . dataset . seq ;
1226- if ( userSeq ) {
1227- fetch ( '/api/messages/delete' , {
1228- method : 'POST' ,
1229- headers : { 'Content-Type' : 'application/json' } ,
1230- body : JSON . stringify ( {
1231- session_id : sessionId ,
1232- user_seq : parseInt ( userSeq ) ,
1233- delete_user : false
1234- } )
1235- } ) . then ( r => r . json ( ) ) . then ( data => {
1236- if ( data . status === 'success' ) console . log ( `Deleted ${ data . deleted } bot reply messages` ) ;
1237- } ) . catch ( err => console . error ( 'Failed to delete bot reply:' , err ) ) ;
1238- }
1190+ if ( ! userMsgEl ) return ;
1191+
1192+ showConfirmModal ( t ( 'delete_message_title' ) , t ( 'delete_message_confirm' ) , ( ) => {
1193+ // Find the next bot reply for this turn (skip non-message nodes).
1194+ let botReplyEl = null ;
1195+ let sibling = userMsgEl . nextElementSibling ;
1196+ while ( sibling ) {
1197+ if ( sibling . classList && sibling . classList . contains ( 'bot-message-group' ) ) {
1198+ botReplyEl = sibling ;
1199+ break ;
12391200 }
1240-
1241- // Remove from DOM
1242- botMsgEl . remove ( ) ;
1243- } ) ;
1244- }
1201+ sibling = sibling . nextElementSibling ;
1202+ }
1203+ userMsgEl . remove ( ) ;
1204+ if ( botReplyEl ) botReplyEl . remove ( ) ;
1205+
1206+ const userSeq = userMsgEl . dataset . seq ;
1207+ if ( userSeq ) {
1208+ fetch ( '/api/messages/delete' , {
1209+ method : 'POST' ,
1210+ headers : { 'Content-Type' : 'application/json' } ,
1211+ body : JSON . stringify ( { session_id : sessionId , user_seq : parseInt ( userSeq ) } )
1212+ } ) . then ( r => r . json ( ) ) . then ( data => {
1213+ if ( data . status === 'success' ) console . log ( `Deleted ${ data . deleted } messages` ) ;
1214+ } ) . catch ( err => console . error ( 'Failed to delete:' , err ) ) ;
1215+ }
1216+ } ) ;
12451217 return ;
12461218 }
12471219
@@ -2028,17 +2000,25 @@ async function editUserMessage(msgEl) {
20282000 }
20292001 }
20302002
2031- // Find all subsequent messages (this message and everything after it)
2003+ // Remove this message bubble and every later bubble that belongs to
2004+ // this or a subsequent turn. We mirror the backend cascade contract:
2005+ // anything with a data-seq >= current seq, plus any live SSE bubble
2006+ // that is still being streamed (no seq yet) after this point.
2007+ const currentSeqNum = userSeq ? parseInt ( userSeq ) : null ;
20322008 const messagesToRemove = [ ] ;
20332009 let current = msgEl ;
20342010 while ( current ) {
2035- if ( current . classList && ( current . classList . contains ( 'user-message-group' ) || current . classList . contains ( 'flex' ) ) ) {
2036- messagesToRemove . push ( current ) ;
2011+ if ( current . classList && ( current . classList . contains ( 'user-message-group' ) || current . classList . contains ( 'bot-message-group' ) ) ) {
2012+ const seqAttr = current . dataset . seq ;
2013+ if ( seqAttr === undefined || seqAttr === '' ) {
2014+ // Live message without a persisted seq yet — treat as later.
2015+ messagesToRemove . push ( current ) ;
2016+ } else if ( currentSeqNum === null || parseInt ( seqAttr ) >= currentSeqNum ) {
2017+ messagesToRemove . push ( current ) ;
2018+ }
20372019 }
20382020 current = current . nextElementSibling ;
20392021 }
2040-
2041- // Remove all messages from this one onwards
20422022 messagesToRemove . forEach ( el => {
20432023 if ( el && el . parentNode ) el . parentNode . removeChild ( el ) ;
20442024 } ) ;
@@ -2266,8 +2246,10 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
22662246 if ( botEl ) return ;
22672247 if ( loadingEl ) { loadingEl . remove ( ) ; loadingEl = null ; }
22682248 botEl = document . createElement ( 'div' ) ;
2269- botEl . className = 'flex gap-3 px-4 sm:px-6 py-3' ;
2249+ botEl . className = 'flex gap-3 px-4 sm:px-6 py-3 bot-message-group ' ;
22702250 botEl . dataset . requestId = requestId ;
2251+ // Regenerate button starts hidden; it's revealed in the "done"
2252+ // event handler once seq metadata arrives from the backend.
22712253 botEl . innerHTML = `
22722254 <img src="assets/logo.jpg" alt="CowAgent" class="w-8 h-8 rounded-lg flex-shrink-0">
22732255 <div class="min-w-0 flex-1 max-w-[85%]">
@@ -2285,6 +2267,9 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
22852267 <button class="speak-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${ t ( 'speak_msg' ) } " style="display:none;">
22862268 <i class="fas fa-volume-up"></i>
22872269 </button>
2270+ <button class="regenerate-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-primary-400 dark:hover:text-primary-400 transition-colors cursor-pointer" title="${ t ( 'regenerate_response' ) } " style="display:none;">
2271+ <i class="fas fa-rotate-right"></i>
2272+ </button>
22882273 </div>
22892274 </div>
22902275 ` ;
@@ -2534,6 +2519,29 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo) {
25342519 if ( copyBtn && finalText ) copyBtn . style . display = '' ;
25352520 applyHighlighting ( botEl ) ;
25362521 }
2522+
2523+ // Backfill seq metadata so edit/regenerate buttons can call
2524+ // the delete API without a page refresh. Backend includes
2525+ // user_seq / bot_seq on the done event after persistence.
2526+ const targetBotEl = botEl || ( requestId ? messagesDiv . querySelector ( `[data-request-id="${ requestId } "]` ) : null ) ;
2527+ if ( targetBotEl ) {
2528+ if ( item . bot_seq !== undefined && item . bot_seq !== null ) {
2529+ targetBotEl . dataset . seq = item . bot_seq ;
2530+ }
2531+ // Reveal regenerate button now that the seq is wired up.
2532+ const regenBtn = targetBotEl . querySelector ( '.regenerate-msg-btn' ) ;
2533+ if ( regenBtn ) regenBtn . style . display = '' ;
2534+ if ( item . user_seq !== undefined && item . user_seq !== null ) {
2535+ // Locate the preceding user bubble for this turn.
2536+ let prev = targetBotEl . previousElementSibling ;
2537+ while ( prev && ! prev . classList . contains ( 'user-message-group' ) ) {
2538+ prev = prev . previousElementSibling ;
2539+ }
2540+ if ( prev && ! prev . dataset . seq ) {
2541+ prev . dataset . seq = item . user_seq ;
2542+ }
2543+ }
2544+ }
25372545 renderBotSpeakerButton ( botEl , finalText ) ;
25382546 scrollChatToBottom ( ) ;
25392547
@@ -2857,7 +2865,7 @@ function localizeCancelMarker(text) {
28572865
28582866function createBotMessageEl ( content , timestamp , requestId , msg ) {
28592867 const el = document . createElement ( 'div' ) ;
2860- el . className = 'flex gap-3 px-4 sm:px-6 py-3' ;
2868+ el . className = 'flex gap-3 px-4 sm:px-6 py-3 bot-message-group ' ;
28612869 if ( requestId ) el . dataset . requestId = requestId ;
28622870
28632871 let stepsHtml = '' ;
@@ -2889,15 +2897,12 @@ function createBotMessageEl(content, timestamp, requestId, msg) {
28892897 <button class="copy-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${ currentLang === 'zh' ? '复制' : 'Copy' } ">
28902898 <i class="fas fa-copy"></i>
28912899 </button>
2892- <button class="regenerate-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-primary-400 dark:hover:text-primary-400 transition-colors cursor-pointer" title="${ t ( 'regenerate_response' ) } ">
2893- <i class="fas fa-rotate-right"></i>
2894- </button>
2895- <button class="delete-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-red-500 dark:hover:text-red-400 transition-colors cursor-pointer" title="${ t ( 'delete_message_title' ) } ">
2896- <i class="fas fa-trash"></i>
2897- </button>
28982900 <button class="speak-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${ t ( 'speak_msg' ) } " style="display:none;">
28992901 <i class="fas fa-volume-up"></i>
29002902 </button>
2903+ <button class="regenerate-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-primary-400 dark:hover:text-primary-400 transition-colors cursor-pointer" title="${ t ( 'regenerate_response' ) } ">
2904+ <i class="fas fa-rotate-right"></i>
2905+ </button>
29012906 </div>
29022907 </div>
29032908 ` ;
0 commit comments