7070 display : flex;
7171 gap : 10px ;
7272 }
73- # settings-btn , # clear-btn {
73+ # settings-btn , # clear-btn , # export-btn {
7474 background : var (--bg-tertiary );
7575 border : 1px solid var (--border-color );
7676 color : var (--text-secondary );
8484 gap : 6px ;
8585 font-family : inherit;
8686 }
87- # settings-btn : hover , # clear-btn : hover {
87+ # settings-btn : hover , # clear-btn : hover , # export-btn : hover {
8888 background : var (--accent-color );
8989 color : white;
9090 border-color : var (--accent-color );
431431 font-size : 0.8rem ;
432432 cursor : pointer;
433433 margin-top : 8px ;
434+ transition : all var (--transition-fast );
435+ }
436+ .retry-button : hover {
437+ background : var (--accent-hover );
434438 }
435439 .error-message {
436440 color : var (--error-color );
458462 height : 8px ;
459463 background : var (--accent-color );
460464 border-radius : 50% ;
465+ animation : bounce 1.5s infinite ease-in-out;
461466 }
462467 .typing-text {
463468 color : var (--text-secondary );
464469 font-size : 0.9rem ;
465470 }
471+ .edit-button {
472+ position : absolute;
473+ top : 8px ;
474+ right : 8px ;
475+ background : var (--bg-tertiary );
476+ color : var (--text-secondary );
477+ border : 1px solid var (--border-color );
478+ border-radius : 4px ;
479+ padding : 4px 8px ;
480+ font-size : 0.8rem ;
481+ cursor : pointer;
482+ opacity : 0 ;
483+ transition : all var (--transition-fast );
484+ }
485+ .user : hover .edit-button {
486+ opacity : 1 ;
487+ }
488+ .edit-button : hover {
489+ background : var (--accent-color );
490+ color : white;
491+ border-color : var (--accent-color );
492+ }
493+ /* Edit mode styling */
494+ .edit-mode {
495+ padding : 12px ;
496+ background : var (--bg-tertiary );
497+ border-radius : var (--radius-sm );
498+ border : 1px solid var (--accent-color );
499+ }
500+ .edit-textarea {
501+ width : 100% ;
502+ min-height : 100px ;
503+ background : var (--bg-secondary );
504+ border : 1px solid var (--border-color );
505+ border-radius : var (--radius-sm );
506+ color : var (--text-primary );
507+ padding : 12px ;
508+ font-family : inherit;
509+ font-size : 1rem ;
510+ margin-bottom : 10px ;
511+ resize : vertical;
512+ }
513+ .edit-buttons {
514+ display : flex;
515+ gap : 8px ;
516+ justify-content : flex-end;
517+ }
518+ .edit-save , .edit-cancel {
519+ padding : 8px 16px ;
520+ border-radius : var (--radius-sm );
521+ font-weight : 500 ;
522+ cursor : pointer;
523+ transition : all var (--transition-fast );
524+ border : none;
525+ font-family : inherit;
526+ font-size : 0.9rem ;
527+ }
528+ .edit-save {
529+ background : var (--accent-color );
530+ color : white;
531+ }
532+ .edit-save : hover {
533+ background : var (--accent-hover );
534+ }
535+ .edit-cancel {
536+ background : var (--bg-secondary );
537+ color : var (--text-primary );
538+ border : 1px solid var (--border-color );
539+ }
540+ .edit-cancel : hover {
541+ background : var (--bg-tertiary );
542+ }
466543 @media (max-width : 768px ) {
467544 .message {
468545 max-width : 90% ;
492569</ head >
493570< body >
494571 < header >
495- < div class ="logo "> V1.2 </ div >
572+ < div class ="logo "> V1.3 </ div >
496573 < div class ="header-buttons ">
497574 < button id ="clear-btn " aria-label ="Clear chat ">
498575 < svg xmlns ="http://www.w3.org/2000/svg " width ="18 " height ="18 " viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 " stroke-linecap ="round " stroke-linejoin ="round "> < polyline points ="3 6 5 6 21 6 "> </ polyline > < path d ="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2 "> </ path > < line x1 ="10 " y1 ="11 " x2 ="10 " y2 ="17 "> </ line > < line x1 ="14 " y1 ="11 " x2 ="14 " y2 ="17 "> </ line > </ svg >
499576 Clear Chat
500577 </ button >
578+ < button id ="export-btn " aria-label ="Export chat ">
579+ < svg xmlns ="http://www.w3.org/2000/svg " width ="18 " height ="18 " viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 " stroke-linecap ="round " stroke-linejoin ="round "> < path d ="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4 "> </ path > < polyline points ="7 10 12 15 17 10 "> </ polyline > < line x1 ="12 " y1 ="15 " x2 ="12 " y2 ="3 "> </ line > </ svg >
580+ Export Chat
581+ </ button >
501582 < button id ="settings-btn " aria-label ="Open settings ">
502583 < svg xmlns ="http://www.w3.org/2000/svg " width ="18 " height ="18 " viewBox ="0 0 24 24 " fill ="none " stroke ="currentColor " stroke-width ="2 " stroke-linecap ="round " stroke-linejoin ="round "> < circle cx ="12 " cy ="12 " r ="3 "> </ circle > < path d ="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z "> </ path > </ svg >
503584 </ button >
@@ -552,6 +633,7 @@ <h2>API Configuration:</h2>
552633 const send = document . getElementById ( 'send' ) ;
553634 const stop = document . getElementById ( 'stop' ) ;
554635 const clearBtn = document . getElementById ( 'clear-btn' ) ;
636+ const exportBtn = document . getElementById ( 'export-btn' ) ;
555637 const settingsBtn = document . getElementById ( 'settings-btn' ) ;
556638 const settingsModal = document . getElementById ( 'settings-modal' ) ;
557639 const closeBtn = document . querySelector ( '.close' ) ;
@@ -602,6 +684,34 @@ <h2 style="margin-bottom: 12px; color: white;">ChatAPI</h2>
602684
603685 clearBtn . addEventListener ( 'click' , clearChat ) ;
604686
687+ function exportChat ( ) {
688+ if ( messages . length === 0 ) {
689+ alert ( 'No messages to export' ) ;
690+ return ;
691+ }
692+
693+ const exportData = {
694+ metadata : {
695+ exportDate : new Date ( ) . toISOString ( ) ,
696+ exportDateFormatted : new Date ( ) . toLocaleString ( ) ,
697+ totalMessages : messages . length ,
698+ model : apiSettings . model
699+ } ,
700+ messages : messages . filter ( msg => msg . role !== 'system' )
701+ } ;
702+
703+ const dataStr = JSON . stringify ( exportData , null , 2 ) ;
704+ const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent ( dataStr ) ;
705+ const timestamp = new Date ( ) . toISOString ( ) . slice ( 0 , 19 ) . replace ( / : / g, '-' ) ;
706+ const filename = `chat-export-${ timestamp } .json` ;
707+ const linkElement = document . createElement ( 'a' ) ;
708+ linkElement . setAttribute ( 'href' , dataUri ) ;
709+ linkElement . setAttribute ( 'download' , filename ) ;
710+ linkElement . click ( ) ;
711+ }
712+
713+ exportBtn . addEventListener ( 'click' , exportChat ) ;
714+
605715 let controller = null ;
606716
607717 settingsBtn . addEventListener ( 'click' , ( ) => {
@@ -676,10 +786,10 @@ <h2 style="margin-bottom: 12px; color: white;">ChatAPI</h2>
676786 messages = [ ...systemMessages ] ;
677787 }
678788
679- messages . forEach ( msg => {
789+ messages . forEach ( ( msg , index ) => {
680790 if ( msg . role === 'system' ) return ;
681791 const roleClass = msg . role === 'assistant' ? 'ai' : msg . role ;
682- const msgEl = appendMessage ( roleClass , msg . content ) ;
792+ const msgEl = appendMessage ( roleClass , msg . content , index ) ;
683793 if ( msg . role === 'assistant' ) {
684794 msgEl . innerHTML = marked . parse ( msg . content ) ;
685795 }
@@ -703,19 +813,105 @@ <h2 style="margin-bottom: 12px; color: white;">ChatAPI</h2>
703813 }
704814 }
705815
706- function appendMessage ( role , text ) {
816+ function appendMessage ( role , text , index ) {
707817 const msg = document . createElement ( 'div' ) ;
708818 msg . className = `message ${ role } ` ;
819+ if ( index !== undefined ) {
820+ msg . dataset . index = index ;
821+ }
822+
709823 if ( role === 'ai' ) {
710824 msg . innerHTML = marked . parse ( text ) ;
711- } else {
712- msg . textContent = text ;
825+ } else if ( role === 'user' ) {
826+ const contentDiv = document . createElement ( 'div' ) ;
827+ contentDiv . textContent = text ;
828+ msg . appendChild ( contentDiv ) ;
829+
830+ // Add edit button
831+ const editBtn = document . createElement ( 'button' ) ;
832+ editBtn . className = 'edit-button' ;
833+ editBtn . innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>' ;
834+ editBtn . setAttribute ( 'aria-label' , 'Edit message' ) ;
835+ editBtn . addEventListener ( 'click' , ( ) => enableMessageEdit ( msg ) ) ;
836+ msg . appendChild ( editBtn ) ;
713837 }
838+
714839 chat . appendChild ( msg ) ;
715840 chat . scrollTop = chat . scrollHeight ;
716841 return msg ;
717842 }
718843
844+ function enableMessageEdit ( messageElement ) {
845+ const index = parseInt ( messageElement . dataset . index ) ;
846+ const originalContent = messages [ index ] . content ;
847+
848+ messageElement . classList . add ( 'edit-mode' ) ;
849+ const editHTML = `
850+ <textarea class="edit-textarea">${ originalContent } </textarea>
851+ <div class="edit-buttons">
852+ <button class="edit-save" aria-label="Save changes">Save</button>
853+ <button class="edit-cancel" aria-label="Cancel editing">Cancel</button>
854+ </div>
855+ ` ;
856+ messageElement . innerHTML = editHTML ;
857+
858+ const textarea = messageElement . querySelector ( '.edit-textarea' ) ;
859+ textarea . focus ( ) ;
860+
861+ messageElement . querySelector ( '.edit-save' ) . addEventListener ( 'click' , ( ) => {
862+ const newContent = textarea . value . trim ( ) ;
863+ if ( newContent && newContent !== originalContent ) {
864+ saveEditedMessage ( index , newContent , messageElement ) ;
865+ }
866+ } ) ;
867+
868+ messageElement . querySelector ( '.edit-cancel' ) . addEventListener ( 'click' , ( ) => {
869+ cancelEdit ( messageElement , originalContent , index ) ;
870+ } ) ;
871+ }
872+
873+ function saveEditedMessage ( index , newContent , messageElement ) {
874+ messages [ index ] . content = newContent ;
875+
876+ const messagesToRemove = messages . slice ( index + 1 ) ;
877+ messagesToRemove . forEach ( ( msg , i ) => {
878+ const msgElement = chat . querySelector ( `[data-index="${ index + 1 + i } "]` ) ;
879+ if ( msgElement ) msgElement . remove ( ) ;
880+ } ) ;
881+
882+ messages = messages . slice ( 0 , index + 1 ) ;
883+
884+ messages = messages . filter ( msg => msg . role !== 'assistant' ) ;
885+
886+ saveMessagesToStorage ( ) ;
887+
888+ messageElement . classList . remove ( 'edit-mode' ) ;
889+ messageElement . textContent = newContent ;
890+ messageElement . dataset . index = index ;
891+
892+ const editBtn = document . createElement ( 'button' ) ;
893+ editBtn . className = 'edit-button' ;
894+ editBtn . innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>' ;
895+ editBtn . addEventListener ( 'click' , ( ) => enableMessageEdit ( messageElement ) ) ;
896+ messageElement . appendChild ( editBtn ) ;
897+
898+ const indicator = document . getElementById ( 'typing-indicator' ) ;
899+ if ( indicator ) indicator . remove ( ) ;
900+ sendMessage ( newContent , true , index ) ;
901+ }
902+
903+ function cancelEdit ( messageElement , originalContent , originalIndex ) {
904+ messageElement . classList . remove ( 'edit-mode' ) ;
905+ messageElement . textContent = originalContent ;
906+
907+ const editBtn = document . createElement ( 'button' ) ;
908+ editBtn . className = 'edit-button' ;
909+ editBtn . innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>' ;
910+ editBtn . addEventListener ( 'click' , ( ) => enableMessageEdit ( messageElement ) ) ;
911+ messageElement . appendChild ( editBtn ) ;
912+ messageElement . dataset . index = originalIndex ;
913+ }
914+
719915 function addCopyButtons ( ) {
720916 document . querySelectorAll ( '.ai pre' ) . forEach ( block => {
721917 if ( block . querySelector ( '.copy-button' ) ) return ;
@@ -756,25 +952,33 @@ <h2 style="margin-bottom: 12px; color: white;">ChatAPI</h2>
756952 function resendLastMessage ( ) {
757953 if ( messages . length > 0 && messages [ messages . length - 1 ] . role === 'user' ) {
758954 const lastUserMessage = messages [ messages . length - 1 ] . content ;
955+ const messageIndex = messages . findIndex ( msg => msg . content === lastUserMessage ) ;
956+
759957 if ( messages . length > 1 && messages [ messages . length - 2 ] . role === 'assistant' ) {
760958 messages . pop ( ) ;
959+ const aiMessage = chat . querySelector ( '.ai:last-child' ) ;
960+ if ( aiMessage ) aiMessage . remove ( ) ;
761961 }
962+
762963 const indicator = document . getElementById ( 'typing-indicator' ) ;
763964 if ( indicator ) indicator . remove ( ) ;
764- sendMessage ( lastUserMessage , true ) ;
965+ sendMessage ( lastUserMessage , true , messageIndex ) ;
765966 }
766967 }
767968
768- async function sendMessage ( userInputOverride = null , isRetry = false ) {
969+ async function sendMessage ( userInputOverride = null , isRetry = false , messageIndex = null ) {
769970 const userInput = userInputOverride || input . value . trim ( ) ;
770971 if ( ! userInput ) return ;
771972
772973 if ( ! isRetry ) {
773974 hideWelcomeMessage ( ) ;
774- appendMessage ( 'user' , userInput ) ;
975+ const msgEl = appendMessage ( 'user' , userInput , messages . length ) ;
775976 input . value = '' ;
776977 messages . push ( { role : 'user' , content : userInput } ) ;
777978 saveMessagesToStorage ( ) ;
979+ } else if ( messageIndex !== null ) {
980+ messages [ messageIndex ] . content = userInput ;
981+ saveMessagesToStorage ( ) ;
778982 }
779983
780984 currentAiMessageElement = null ;
0 commit comments