feat(chat): Add undo button to trim conversation to a prior message

Lets you click Undo under a user message to drop it and everything
after it, refilling the prompt with that message's text/images so it
can be edited and resent.
This commit is contained in:
2026-07-28 09:22:15 +02:00
parent 0ddf2c4db5
commit eee4addd86
+35 -8
View File
@@ -97,6 +97,23 @@
margin-left: auto; margin-left: auto;
white-space: pre-wrap; white-space: pre-wrap;
} }
.undo-btn {
display: block;
margin-left: auto;
margin-top: 0.4rem;
background: rgba(255, 255, 255, 0.15);
color: var(--text-user);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 0.3rem;
padding: 0.15rem 0.5rem;
font-size: 0.8em;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
}
.undo-btn:hover {
opacity: 1;
}
.system-message { .system-message {
background: var(--bg-secondary); background: var(--bg-secondary);
margin-right: auto; margin-right: auto;
@@ -283,6 +300,7 @@
<img :src="img.dataUrl" style="max-width: 100%; border-radius: 0.3rem; display: block; margin-bottom: 0.3rem;" /> <img :src="img.dataUrl" style="max-width: 100%; border-radius: 0.3rem; display: block; margin-bottom: 0.3rem;" />
</template> </template>
<div class="text" x-text="msg.content"></div> <div class="text" x-text="msg.content"></div>
<button class="undo-btn" title="Trim conversation back to before this message" @click="trimTo(msg)">Undo</button>
</div> </div>
</template> </template>
<template x-if="msg.role === 'assistant'"> <template x-if="msg.role === 'assistant'">
@@ -355,9 +373,8 @@
You are a helpful AI assistant. You are a helpful AI assistant.
- Do not preach about implications, do not praise excessively. - Do not preach about implications, do not praise excessively.
- Keep a critical mindset, do not assume blindly. - Keep a critical mindset, do not assume blindly.
- Answer in valid markdown only, no exceptions.
- When I ask something about code, or software development, keep in mind that I am a senior backend developer (but do not mention that explicitly), and do not explain excessively, but and challenge my statements with better alternatives if I don't sound right, especially in coding. Do not bring it up otherwise. - When I ask something about code, or software development, keep in mind that I am a senior backend developer (but do not mention that explicitly), and do not explain excessively, but and challenge my statements where needed with better alternatives if I don't sound right, especially in coding. Do not bring it up otherwise.
- Do not give me code if the topic is not about programming or software development. - Do not give me code if the topic is not about programming or software development.
` `
.replace(/^\s+/gm, "") .replace(/^\s+/gm, "")
@@ -375,9 +392,9 @@
sessionId: null, sessionId: null,
models: [ models: [
{ name: "gpt-5.4-mini", chat: chatWithOpenAI, key: "OPENAI_API_KEY" }, { name: "gpt-5.4-mini", chat: chatWithOpenAI, key: window.env.OPENAI_API_KEY },
{ name: "gemini-3.5-flash-lite", chat: chatWithGemini, key: "GEMINI_API_KEY" }, { name: "gemini-3.5-flash-lite", chat: chatWithGemini, key: window.env.GEMINI_API_KEY },
{ name: "claude-haiku-4-5-20251001", chat: chatWithClaude, key: "ANTHROPIC_API_KEY" }, { name: "claude-haiku-4-5-20251001", chat: chatWithClaude, key: window.env.ANTHROPIC_API_KEY },
], ],
async init() { async init() {
@@ -444,7 +461,7 @@
const modelObj = this.models.find((m) => m.name === this.activeModelName); const modelObj = this.models.find((m) => m.name === this.activeModelName);
await modelObj.chat({ await modelObj.chat({
model: this.activeModelName, model: this.activeModelName,
apiKey: window.env[modelObj.key], apiKey: modelObj.key,
messages: this.messages, messages: this.messages,
signal: this.abortController.signal, signal: this.abortController.signal,
onContent: (chunk) => { onContent: (chunk) => {
@@ -492,6 +509,17 @@
}); });
}, },
trimTo(msg) {
if (this.isBusy) return;
const idx = this.messages.indexOf(msg);
if (idx === -1) return;
this.messages = this.messages.slice(0, idx);
updateSessionHistory(this.sessionId, this.messages);
this.promptText = msg.content;
this.pendingImages = msg.images ? JSON.parse(JSON.stringify(msg.images)) : [];
this.$nextTick(() => this.$refs.promptInput.focus());
},
restoreConversation(msgs) { restoreConversation(msgs) {
this.messages = JSON.parse(JSON.stringify(msgs)); this.messages = JSON.parse(JSON.stringify(msgs));
this.userHasScrolledUp = false; this.userHasScrolledUp = false;
@@ -564,11 +592,10 @@
"anthropic-version": "2023-06-01", "anthropic-version": "2023-06-01",
"anthropic-dangerous-direct-browser-access": "true", "anthropic-dangerous-direct-browser-access": "true",
}, },
apiKey: window.env.ANTHROPIC_API_KEY,
}); });
} }
async function chatWithGemini({ onContent, onError, messages, signal, apiKey = window.env.GEMINI_API_KEY, model, ...rest }) { async function chatWithGemini({ onContent, onError, messages, signal, apiKey, model, ...rest }) {
const contents = messages.map((msg, i, arr) => { const contents = messages.map((msg, i, arr) => {
const role = msg.role === "assistant" ? "model" : "user"; const role = msg.role === "assistant" ? "model" : "user";
let parts = [{ text: msg.content }]; let parts = [{ text: msg.content }];