Skip to content

Commit 255bab9

Browse files
committed
add editing chat messages + if you chose a model it is now used as default for new chats
1 parent 29653b5 commit 255bab9

6 files changed

Lines changed: 164 additions & 8 deletions

File tree

‎src/addons/addons/ai-integration/chat/stream.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,12 @@ export function submitFromComposer () {
467467
const contextString = buildContextString(session, attachmentType);
468468

469469
chatView.removeWelcomeHeader(pane);
470-
const userBubble = chatView.appendUserMessage(pane, userText, attachmentType !== '0' ? attachmentName : null);
470+
const userBubble = chatView.appendUserMessage(
471+
pane,
472+
userText,
473+
attachmentType !== '0' ? attachmentName : null,
474+
session.chatHistory.length
475+
);
471476

472477
composer.renderAttachmentUI();
473478

‎src/addons/addons/ai-integration/main.js‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
store,
66
getActiveSession,
77
createNewSession,
8-
loadSessionsForCurrentProject
8+
loadSessionsForCurrentProject,
9+
loadDefaultModelId
910
} from './state/sessions.js';
1011
import * as host from './ui/host.js';
1112
import * as chatView from './ui/chatView.js';
@@ -30,6 +31,11 @@ export default class main {
3031
main.applySettings(settings);
3132

3233
host.setComposerHandlers({onSend: submitFromComposer, onStop: stopActiveRequest});
34+
try {
35+
await loadDefaultModelId();
36+
} catch (e) {
37+
console.error('[Torchy] could not load the default model', e);
38+
}
3339
await main.onProjectChanged();
3440
await host.initHost(addon);
3541
main.refreshModels();

‎src/addons/addons/ai-integration/state/sessions.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '../helpers/db.js';
88

99
const MAX_TAB_NAME_LENGTH = 28;
10+
const KEY_DEFAULT_MODEL = 'ui_defaultModelId';
1011

1112
export class ChatSession {
1213
constructor (id, projectId, data = {}) {
@@ -51,6 +52,8 @@ export const store = {
5152
modelErrors: {},
5253
modelsLoading: false,
5354
canUse: false,
55+
// Last model the user picked, new chats start on it.
56+
defaultModelId: null,
5457

5558
sessions: [],
5659
activeSessionId: null,
@@ -99,6 +102,17 @@ export function closeSession (sessionId) {
99102
}
100103
}
101104

105+
export function setDefaultModelId (modelId) {
106+
if (!modelId || store.defaultModelId === modelId) return;
107+
store.defaultModelId = modelId;
108+
saveMetadata(KEY_DEFAULT_MODEL, modelId);
109+
}
110+
111+
export async function loadDefaultModelId () {
112+
const saved = await loadMetadata(KEY_DEFAULT_MODEL);
113+
if (typeof saved === 'string' && saved) store.defaultModelId = saved;
114+
}
115+
102116
export function updateAndSaveSession (session) {
103117
if (session) saveSession(session.toJSON());
104118
}

‎src/addons/addons/ai-integration/style.css‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,42 @@
5151
margin-top: 10px;
5252
}
5353

54+
.torchy-edit-message {
55+
margin-left: auto;
56+
margin-right: 4px;
57+
align-self: center;
58+
width: 22px;
59+
height: 22px;
60+
padding: 3px;
61+
flex: 0 0 auto;
62+
border: none;
63+
border-radius: 4px;
64+
background: none;
65+
color: #7C7766;
66+
cursor: pointer;
67+
opacity: 0;
68+
transition: opacity 0.1s;
69+
}
70+
71+
.torchy-edit-message svg {
72+
width: 16px;
73+
height: 16px;
74+
display: block;
75+
}
76+
77+
.user-message:hover .torchy-edit-message,
78+
.torchy-edit-message:focus-visible {
79+
opacity: 1;
80+
}
81+
82+
.torchy-edit-message:hover {
83+
background: var(--ui-tertiary);
84+
}
85+
86+
.user-message .torchy-edit-message + .message {
87+
margin-left: 0;
88+
}
89+
5490
.user-message .message {
5591
color: #7C7766;
5692
font-family: "Helvetica Neue", sans-serif;

‎src/addons/addons/ai-integration/ui/chatView.js‎

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {closeHost} from './host.js';
1414
import {
1515
getInputText,
1616
setInputText,
17+
focusInput,
1718
getSelectedModelId,
1819
getAttachmentType,
1920
renderAttachmentUI,
@@ -199,9 +200,25 @@ function buildAttachmentChip (name) {
199200
return chip;
200201
}
201202

202-
export function appendUserMessage (pane, text, attachmentName) {
203+
/**
204+
* @param {number} turnIndex index this message has (or will have) in `session.chatHistory`;
205+
* editing the message rolls the chat back to it.
206+
*/
207+
export function appendUserMessage (pane, text, attachmentName, turnIndex) {
203208
const userMessage = document.createElement('div');
204209
userMessage.className = 'user-message';
210+
userMessage.dataset.turnIndex = String(turnIndex);
211+
212+
const edit = document.createElement('button');
213+
edit.type = 'button';
214+
edit.className = 'torchy-edit-message';
215+
edit.title = 'Edit this message';
216+
edit.innerHTML =
217+
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" ' +
218+
'stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" ' +
219+
'd="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Z" /></svg>';
220+
userMessage.appendChild(edit);
221+
205222
const message = document.createElement('div');
206223
message.className = 'message';
207224

@@ -213,9 +230,73 @@ export function appendUserMessage (pane, text, attachmentName) {
213230

214231
userMessage.appendChild(message);
215232
pane.appendChild(userMessage);
233+
attachEditButtonListeners(userMessage);
216234
return userMessage;
217235
}
218236

237+
const ROLLBACK_WARNING_HTML =
238+
'<p>Editing goes back to this point in the chat: this message and everything after it, ' +
239+
"including Torchy's answers, will be deleted. The message is put back in the text box so you can change it " +
240+
'and send it again.</p><br><p>Code you already added to your workspace is not touched.</p>';
241+
242+
function countCodeChunks (history) {
243+
return history.reduce((count, turn) => {
244+
if (turn.role !== 'assistant') return count;
245+
return count + (String(turn.message || '').match(FENCE_REGEX) || []).length;
246+
}, 0);
247+
}
248+
249+
function rollbackToTurn (session, turnIndex, text) {
250+
const pane = getPaneForSession(session.id);
251+
const kept = session.chatHistory.slice(0, Math.max(0, Math.min(turnIndex, session.chatHistory.length)));
252+
session.chatHistory = kept;
253+
session.allCodeChunksEverAdded = session.allCodeChunksEverAdded.slice(0, countCodeChunks(kept));
254+
session.domCache = null;
255+
session.inputText = text;
256+
if (pane) rehydrateChatPane(session, pane);
257+
if (session.id === store.activeSessionId) {
258+
setInputText(text);
259+
focusInput();
260+
}
261+
renderChatUI();
262+
updateAndSaveSession(session);
263+
}
264+
265+
function attachEditButtonListeners (parentElement) {
266+
if (!parentElement) return;
267+
parentElement.querySelectorAll('.torchy-edit-message').forEach(button => {
268+
if (button.dataset.listenerAttached) return;
269+
button.dataset.listenerAttached = 'true';
270+
button.addEventListener('click', () => {
271+
const session = getActiveSession();
272+
const userMessage = button.closest('.user-message');
273+
if (!session || !userMessage) return;
274+
if (session.isBlabbering) {
275+
helpers.showAlert('TorchyWaitForAIToFinishWarning');
276+
return;
277+
}
278+
const turnIndex = Number(userMessage.dataset.turnIndex);
279+
if (!Number.isInteger(turnIndex)) return;
280+
const textEl = userMessage.querySelector('.message > span');
281+
const text = textEl ? textEl.textContent : '';
282+
283+
const ScratchBlocks = window.ScratchBlocks;
284+
if (!ScratchBlocks || typeof ScratchBlocks.prompt !== 'function') return;
285+
ScratchBlocks.prompt(
286+
ROLLBACK_WARNING_HTML,
287+
null,
288+
() => {
289+
if (session.isBlabbering) return;
290+
rollbackToTurn(session, turnIndex, text);
291+
},
292+
'Edit this message?',
293+
ScratchBlocks.BROADCAST_MESSAGE_VARIABLE_TYPE,
294+
true
295+
);
296+
});
297+
});
298+
}
299+
219300
export function removeWelcomeHeader (pane) {
220301
const header = pane.querySelector('.Popup_Header');
221302
if (header) header.remove();
@@ -446,12 +527,12 @@ export function rehydrateChatPane (session, pane) {
446527

447528
let codeChunkCounter = 0;
448529

449-
session.chatHistory.forEach(turn => {
530+
session.chatHistory.forEach((turn, turnIndex) => {
450531
if (turn.role === 'user') {
451532
const text = String(turn.message || '').split('\n\n\nContext:\n')[0];
452533
const attachmentName =
453534
turn.attachment && turn.attachment.type !== '0' ? turn.attachment.name : null;
454-
appendUserMessage(pane, text, attachmentName);
535+
appendUserMessage(pane, text, attachmentName, turnIndex);
455536
return;
456537
}
457538
if (turn.role !== 'assistant') return;
@@ -582,10 +663,11 @@ function insertChunkIntoWorkspace (chunkData, workspace, replacingBlockIds) {
582663

583664
export function restoreInteractivity (pane) {
584665
if (!pane) return;
585-
pane.querySelectorAll('.insert_button').forEach(button => {
666+
pane.querySelectorAll('.insert_button, .torchy-edit-message').forEach(button => {
586667
delete button.dataset.listenerAttached;
587668
});
588669
attachInsertButtonListeners(pane);
670+
attachEditButtonListeners(pane);
589671
pane.querySelectorAll('.torchy-warnings').forEach(wrap => {
590672
const toggle = wrap.querySelector('.torchy-warnings-toggle');
591673
const list = wrap.querySelector('.torchy-warnings-list');

‎src/addons/addons/ai-integration/ui/composer.js‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {store, getActiveSession, updateAndSaveSession} from '../state/sessions.js';
1+
import {store, getActiveSession, updateAndSaveSession, setDefaultModelId} from '../state/sessions.js';
22
import {pickDefaultModel} from '../chat/models.js';
33
import {addCustomModelId, CUSTOM_MODELS_GROUP} from '../chat/customModels.js';
44

@@ -143,7 +143,8 @@ export function rebuildModelOptions () {
143143
const previous = select.value;
144144
select.textContent = '';
145145

146-
const defaultModel = pickDefaultModel(store.AIModels);
146+
const defaultModel =
147+
store.AIModels.find(model => model.key === store.defaultModelId) || pickDefaultModel(store.AIModels);
147148
const defaultId = defaultModel ? defaultModel.key : null;
148149
const groups = new Map();
149150
const groupFor = (provider, label) => {
@@ -235,6 +236,16 @@ export function renderAttachmentUI () {
235236
select.value = session ? session.attachmentType : '0';
236237
}
237238

239+
/** Make `modelId` the model new chats start on. */
240+
function rememberDefaultModel (modelId) {
241+
setDefaultModelId(modelId);
242+
const select = modelSelect();
243+
if (!select) return;
244+
Array.from(select.options).forEach(option => {
245+
option.defaultSelected = option.value === modelId;
246+
});
247+
}
248+
238249
/** Ask for a model id with the editor's prompt modal, then select (and remember) it. */
239250
function promptForModelId (providerId) {
240251
const provider = store.providers.find(p => p.id === providerId);
@@ -259,6 +270,7 @@ function selectTypedModel (provider, input) {
259270
session.selectedModelId = model.key;
260271
updateAndSaveSession(session);
261272
}
273+
setDefaultModelId(model.key);
262274
rebuildModelOptions();
263275
}
264276

@@ -306,6 +318,7 @@ export function createComposer ({onSend, onStop}) {
306318
promptForModelId(value.slice(ADD_MODEL_PREFIX.length));
307319
return;
308320
}
321+
rememberDefaultModel(value);
309322
const session = getActiveSession();
310323
if (!session) return;
311324
session.selectedModelId = value;

0 commit comments

Comments
 (0)