Replace click routing with direct button handlers
This commit is contained in:
+196
-197
@@ -95,6 +95,7 @@ export class UI {
|
||||
// Toolbar.
|
||||
this.$context = document.getElementById('context');
|
||||
this.$reset = document.getElementById('reset');
|
||||
this.$scrollUp = document.getElementById('scroll-up');
|
||||
this.$scrollDown = document.getElementById('scroll-down');
|
||||
this.$mute = document.getElementById('mute');
|
||||
this.$settings = document.getElementById('settings');
|
||||
@@ -214,6 +215,11 @@ export class UI {
|
||||
// stay clickable whatever the last enabled state was.
|
||||
this.$send.disabled = false;
|
||||
} else {
|
||||
// A pending stop confirmation must not stay highlighted on the
|
||||
// button after the run ends and the button returns to send.
|
||||
if (this.$pendingConfirm?.$target === this.$send) {
|
||||
this._clearConfirm();
|
||||
}
|
||||
// Restore the enabled state recorded before the run. A caller
|
||||
// cannot leave the button disabled by forgetting to call
|
||||
// updateSendEnabled after this.
|
||||
@@ -277,9 +283,8 @@ export class UI {
|
||||
const $name = $chip.querySelector('.compose__attachment-name');
|
||||
$name.textContent = file.name;
|
||||
$name.title = file.name;
|
||||
$chip
|
||||
.querySelector('.compose__attachment-remove')
|
||||
.addEventListener('click', () => onRemove(index));
|
||||
$chip.querySelector('.compose__attachment-remove').onclick = () =>
|
||||
onRemove(index);
|
||||
this.$attachments.appendChild($chip);
|
||||
}
|
||||
}
|
||||
@@ -402,6 +407,12 @@ export class UI {
|
||||
` [data-parent-id="${CSS.escape(id)}"]`,
|
||||
)
|
||||
.forEach(($bubble) => {
|
||||
if (
|
||||
this.$pendingConfirm &&
|
||||
$bubble.contains(this.$pendingConfirm.$target)
|
||||
) {
|
||||
this._clearConfirm();
|
||||
}
|
||||
$bubble.remove();
|
||||
});
|
||||
}
|
||||
@@ -543,6 +554,7 @@ export class UI {
|
||||
$content.innerHTML =
|
||||
'<em class="message__placeholder">System Default</em>';
|
||||
}
|
||||
this._bindMessageActions($bubble, { id: 'system' });
|
||||
this.$chat.insertBefore($bubble, this.$chatLoading);
|
||||
}
|
||||
|
||||
@@ -557,6 +569,7 @@ export class UI {
|
||||
this.$tplErrorMessage.content.cloneNode(true).firstElementChild;
|
||||
$bubble.querySelector('.message__content').textContent = message;
|
||||
$bubble.dataset.copyText = message;
|
||||
this._bindMessageActions($bubble, { isError: true });
|
||||
this.$chat.insertBefore($bubble, this.$chatLoading);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
@@ -567,7 +580,15 @@ export class UI {
|
||||
*/
|
||||
clearChat() {
|
||||
for (const $bubble of [...this.$chat.children]) {
|
||||
if ($bubble !== this.$chatLoading) $bubble.remove();
|
||||
if ($bubble !== this.$chatLoading) {
|
||||
if (
|
||||
this.$pendingConfirm &&
|
||||
$bubble.contains(this.$pendingConfirm.$target)
|
||||
) {
|
||||
this._clearConfirm();
|
||||
}
|
||||
$bubble.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,7 +655,7 @@ export class UI {
|
||||
const $bubble = this.$chat.querySelector(
|
||||
`[data-id="${CSS.escape(messageId)}"]`,
|
||||
);
|
||||
const $btn = $bubble?.querySelector('[data-action="play"]');
|
||||
const $btn = $bubble?.querySelector('.message__action-btn--play');
|
||||
if (!$btn) return;
|
||||
$btn.replaceChildren(this._icon(playing ? 'stop' : 'volume'));
|
||||
$btn.classList.toggle('playing', playing);
|
||||
@@ -877,6 +898,12 @@ export class UI {
|
||||
|
||||
/** closeConversations hides the picker. */
|
||||
closeConversations() {
|
||||
if (
|
||||
this.$pendingConfirm &&
|
||||
this.$conversationsOverlay.contains(this.$pendingConfirm.$target)
|
||||
) {
|
||||
this._clearConfirm();
|
||||
}
|
||||
this.$conversationsOverlay.classList.remove('open');
|
||||
this.$conversations.focus();
|
||||
}
|
||||
@@ -890,6 +917,12 @@ export class UI {
|
||||
* @param {{currentId?: string | null}} [options]
|
||||
*/
|
||||
renderConversationList(rows, { currentId = null } = {}) {
|
||||
if (
|
||||
this.$pendingConfirm &&
|
||||
this.$conversationsList.contains(this.$pendingConfirm.$target)
|
||||
) {
|
||||
this._clearConfirm();
|
||||
}
|
||||
this.$conversationsList.replaceChildren();
|
||||
if (rows.length === 0) {
|
||||
const $empty = this.document.createElement('div');
|
||||
@@ -903,7 +936,8 @@ export class UI {
|
||||
for (const conversation of rows) {
|
||||
const $item =
|
||||
this.$tplConversationItem.content.cloneNode(true).firstElementChild;
|
||||
$item.dataset.conversationId = conversation.id;
|
||||
const id = conversation.id;
|
||||
$item.dataset.conversationId = id;
|
||||
$item.querySelector('.conversations__item-title').textContent =
|
||||
conversationLabel(conversation);
|
||||
$item.querySelector('.conversations__item-created').textContent =
|
||||
@@ -914,10 +948,19 @@ export class UI {
|
||||
`Updated ${CONVERSATION_DATE.format(
|
||||
new Date(conversation.updated_at),
|
||||
)}`;
|
||||
if (conversation.id === currentId) {
|
||||
if (id === currentId) {
|
||||
$item.classList.add('conversations__item--current');
|
||||
$item.setAttribute('aria-current', 'true');
|
||||
}
|
||||
$item.querySelector('.conversations__item-main').onclick = () =>
|
||||
this.handlers.onSelectConversation(id);
|
||||
$item.querySelector('.conversations__item-rename').onclick = () =>
|
||||
this.handlers.onRenameConversation(id);
|
||||
const $delete = $item.querySelector('.conversations__item-delete');
|
||||
$delete.onclick = () =>
|
||||
this._confirm(`conversation:${id}`, $delete, 'pending', () =>
|
||||
this.handlers.onDeleteConversation(id),
|
||||
);
|
||||
this.$conversationsList.appendChild($item);
|
||||
}
|
||||
}
|
||||
@@ -1057,9 +1100,21 @@ export class UI {
|
||||
// ====================
|
||||
|
||||
_bindEvents() {
|
||||
// One delegated click listener routes every data-action.
|
||||
this.document.addEventListener('click', (event) =>
|
||||
this._handleClick(event),
|
||||
// Clear a pending two-press confirmation when a click lands
|
||||
// outside its button. Running in the capture phase clears an
|
||||
// earlier confirmation before the clicked button's own handler
|
||||
// arms or confirms a new one.
|
||||
this.document.addEventListener(
|
||||
'click',
|
||||
(event) => {
|
||||
if (
|
||||
this.$pendingConfirm &&
|
||||
!this.$pendingConfirm.$target.contains(event.target)
|
||||
) {
|
||||
this._clearConfirm();
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
// Press-and-hold for push-to-talk and transcription.
|
||||
@@ -1103,8 +1158,18 @@ export class UI {
|
||||
this.handlers.onComposeInput();
|
||||
});
|
||||
|
||||
// Attachments.
|
||||
this.$attach.addEventListener('click', () => this.$fileInput.click());
|
||||
// Compose buttons and attachments.
|
||||
this.$attach.onclick = () => this.$fileInput.click();
|
||||
this.$location.onclick = () => this.handlers.onLocationToggle();
|
||||
this.$send.onclick = () => {
|
||||
if (this.$send.classList.contains('loading')) {
|
||||
this._confirm('stop', this.$send, 'pending', () =>
|
||||
this.handlers.onStop(),
|
||||
);
|
||||
} else {
|
||||
this.handlers.onComposeSubmit();
|
||||
}
|
||||
};
|
||||
this.$fileInput.addEventListener('change', (event) => {
|
||||
this.handlers.onAttachFiles(event.target.files);
|
||||
this.$fileInput.value = '';
|
||||
@@ -1115,9 +1180,27 @@ export class UI {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
// Toolbar buttons.
|
||||
this.$settings.onclick = () => this._openSettings();
|
||||
this.$conversations.onclick = () => this.openConversations();
|
||||
this.$reset.onclick = () =>
|
||||
this._confirm('reset', this.$reset, 'footer__toolbar-btn--pending', () =>
|
||||
this.handlers.onReset(),
|
||||
);
|
||||
this.$scrollUp.onclick = () => {
|
||||
this.setAutoScroll(false);
|
||||
this.$chat.scrollTop = 0;
|
||||
};
|
||||
this.$scrollDown.onclick = () => {
|
||||
// Set scrollTop first, so the scroll listener sees the
|
||||
// bottom position and does not turn following off again.
|
||||
this.$chat.scrollTop = this.$chat.scrollHeight;
|
||||
this.setAutoScroll(true);
|
||||
};
|
||||
this.$mute.onclick = () => this.handlers.onToggleMute();
|
||||
|
||||
// Settings modal.
|
||||
this.$settings.addEventListener('click', () => this._openSettings());
|
||||
this.$settingsClose.addEventListener('click', () => this._closeSettings());
|
||||
this.$settingsClose.onclick = () => this._closeSettings();
|
||||
this.$settingsOverlay.addEventListener('click', (event) => {
|
||||
if (event.target === this.$settingsOverlay) this._closeSettings();
|
||||
});
|
||||
@@ -1131,11 +1214,9 @@ export class UI {
|
||||
}
|
||||
});
|
||||
this.$settingsTabs.forEach(($tab) => {
|
||||
$tab.addEventListener('click', () => this._switchTab($tab.dataset.tab));
|
||||
$tab.onclick = () => this._switchTab($tab.dataset.tab);
|
||||
});
|
||||
this.$saveSystemMessage.addEventListener('click', () =>
|
||||
this._saveSystemMessage(),
|
||||
);
|
||||
this.$saveSystemMessage.onclick = () => this._saveSystemMessage();
|
||||
this.$model.addEventListener('change', () => {
|
||||
const { provider, model } = this.getSelectedModel();
|
||||
this.handlers.onModelChange({ provider, model });
|
||||
@@ -1144,12 +1225,8 @@ export class UI {
|
||||
this.handlers.onVoiceChange(this.$voice.value),
|
||||
);
|
||||
|
||||
// Conversations modal. The toolbar button opens it through the
|
||||
// data-action route; these listeners close it and report the
|
||||
// search text and the selected row.
|
||||
this.$conversationsClose.addEventListener('click', () =>
|
||||
this.closeConversations(),
|
||||
);
|
||||
// Conversations modal.
|
||||
this.$conversationsClose.onclick = () => this.closeConversations();
|
||||
this.$conversationsOverlay.addEventListener('click', (event) => {
|
||||
if (event.target === this.$conversationsOverlay) {
|
||||
this.closeConversations();
|
||||
@@ -1167,39 +1244,11 @@ export class UI {
|
||||
this.$conversationsSearch.addEventListener('input', () =>
|
||||
this.handlers.onConversationSearch(this.$conversationsSearch.value),
|
||||
);
|
||||
this.$conversationNew.addEventListener('click', () =>
|
||||
this.handlers.onNewConversation(),
|
||||
);
|
||||
this.$conversationsList.addEventListener('click', (event) => {
|
||||
const $item = event.target.closest('[data-conversation-id]');
|
||||
if (!$item) return;
|
||||
const id = $item.dataset.conversationId;
|
||||
const $rename = event.target.closest('.conversations__item-rename');
|
||||
if ($rename) {
|
||||
this._clearConfirm();
|
||||
this.handlers.onRenameConversation(id);
|
||||
return;
|
||||
}
|
||||
const $delete = event.target.closest('.conversations__item-delete');
|
||||
if ($delete) {
|
||||
// Deleting is irreversible: the second press on the same
|
||||
// button deletes, any other click cancels the request.
|
||||
this._confirm(`conversation:${id}`, $delete, 'pending', () =>
|
||||
this.handlers.onDeleteConversation(id),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._clearConfirm();
|
||||
this.handlers.onSelectConversation(id);
|
||||
});
|
||||
this.$conversationNew.onclick = () => this.handlers.onNewConversation();
|
||||
|
||||
// Compose settings modal.
|
||||
this.$composeSettings.addEventListener('click', () =>
|
||||
this.handlers.onComposeSettings(),
|
||||
);
|
||||
this.$composeSettingsClose.addEventListener('click', () =>
|
||||
this._closeComposeSettings(),
|
||||
);
|
||||
this.$composeSettings.onclick = () => this.handlers.onComposeSettings();
|
||||
this.$composeSettingsClose.onclick = () => this._closeComposeSettings();
|
||||
this.$composeSettingsOverlay.addEventListener('click', (event) => {
|
||||
if (event.target === this.$composeSettingsOverlay) {
|
||||
this._closeComposeSettings();
|
||||
@@ -1207,143 +1256,6 @@ export class UI {
|
||||
});
|
||||
}
|
||||
|
||||
_handleClick(event) {
|
||||
const $target = event.target.closest('[data-action]');
|
||||
const action = $target?.dataset.action;
|
||||
|
||||
// A click outside any action button cancels every pending
|
||||
// confirmation.
|
||||
if (!action) {
|
||||
this._clearConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
const $message = $target.closest('.message');
|
||||
const messageId = $message?.dataset.messageId || $message?.dataset.id;
|
||||
|
||||
switch (action) {
|
||||
case 'send':
|
||||
if (this.$send.classList.contains('loading')) {
|
||||
this._confirm('stop', $target, 'pending', () =>
|
||||
this.handlers.onStop(),
|
||||
);
|
||||
} else {
|
||||
this._clearConfirm();
|
||||
this.handlers.onComposeSubmit();
|
||||
}
|
||||
break;
|
||||
case 'reset':
|
||||
this._confirm('reset', $target, 'footer__toolbar-btn--pending', () =>
|
||||
this.handlers.onReset(),
|
||||
);
|
||||
break;
|
||||
case 'delete':
|
||||
// Error bubbles are display-only: remove without a request.
|
||||
if (
|
||||
!$message ||
|
||||
(!$message.dataset.id && !$message.dataset.messageId)
|
||||
) {
|
||||
this._clearConfirm();
|
||||
$message?.remove();
|
||||
break;
|
||||
}
|
||||
this._confirm(
|
||||
`delete:${messageId}:${$message.dataset.field ?? ''}` +
|
||||
`:${$message.dataset.toolCallId ?? ''}`,
|
||||
$target,
|
||||
'pending',
|
||||
() =>
|
||||
this.handlers.onDelete({
|
||||
id: messageId,
|
||||
field: $message.dataset.field || null,
|
||||
toolCallId: $message.dataset.toolCallId || null,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'approve':
|
||||
case 'deny': {
|
||||
this._clearConfirm();
|
||||
const execution = this.pendingApprovals.find(
|
||||
({ messageId: mid, toolCallId }) =>
|
||||
mid === $message?.dataset.parentId &&
|
||||
toolCallId === $message?.dataset.toolCallId,
|
||||
);
|
||||
if (!execution) break;
|
||||
if (action === 'approve') {
|
||||
this.handlers.onApprove(execution);
|
||||
} else {
|
||||
this.handlers.onDeny(execution);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'edit':
|
||||
this._clearConfirm();
|
||||
this.handlers.onEdit({
|
||||
id: messageId,
|
||||
field: $message?.dataset.field || null,
|
||||
toolCallId: $message?.dataset.toolCallId || null,
|
||||
});
|
||||
break;
|
||||
case 'play':
|
||||
this._clearConfirm();
|
||||
if (messageId) this.handlers.onPlay(messageId);
|
||||
break;
|
||||
case 'copy': {
|
||||
this._clearConfirm();
|
||||
const text = $message?.dataset.copyText || '';
|
||||
this._copyToClipboard($target, text);
|
||||
break;
|
||||
}
|
||||
case 'inspect': {
|
||||
this._clearConfirm();
|
||||
const isOpen = $message.classList.toggle('message--debug-open');
|
||||
$target.setAttribute('aria-expanded', String(isOpen));
|
||||
break;
|
||||
}
|
||||
case 'scroll-up':
|
||||
this._clearConfirm();
|
||||
this.setAutoScroll(false);
|
||||
this.$chat.scrollTop = 0;
|
||||
break;
|
||||
case 'scroll-down':
|
||||
// Set scrollTop first, so the scroll listener sees the
|
||||
// bottom position and does not turn following off again.
|
||||
this._clearConfirm();
|
||||
this.$chat.scrollTop = this.$chat.scrollHeight;
|
||||
this.setAutoScroll(true);
|
||||
break;
|
||||
case 'mute':
|
||||
this._clearConfirm();
|
||||
this.handlers.onToggleMute();
|
||||
break;
|
||||
case 'location':
|
||||
this._clearConfirm();
|
||||
this.handlers.onLocationToggle();
|
||||
break;
|
||||
case 'conversations':
|
||||
this._clearConfirm();
|
||||
this.openConversations();
|
||||
break;
|
||||
case 'settings':
|
||||
this._clearConfirm();
|
||||
this._openSettings();
|
||||
break;
|
||||
case 'compose-settings':
|
||||
this._clearConfirm();
|
||||
this.handlers.onComposeSettings();
|
||||
break;
|
||||
case 'attach':
|
||||
// Handled by the listener on the button itself.
|
||||
break;
|
||||
case 'ptt':
|
||||
case 'transcribe':
|
||||
// Handled by press-and-hold.
|
||||
break;
|
||||
default:
|
||||
this._clearConfirm();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* _confirm runs action on the second press of the same control.
|
||||
* The first press marks the control; any other click clears the
|
||||
@@ -1507,15 +1419,13 @@ export class UI {
|
||||
if (event.key === 'Escape') close(null);
|
||||
};
|
||||
|
||||
$modal
|
||||
.querySelector('.modal__close')
|
||||
.addEventListener('click', () => close(null));
|
||||
$modal.querySelector('.modal__close').onclick = () => close(null);
|
||||
$overlay.addEventListener('click', (event) => {
|
||||
if (event.target === $overlay) close(null);
|
||||
});
|
||||
this.document.addEventListener('keydown', onKey);
|
||||
|
||||
$save.addEventListener('click', () => {
|
||||
$save.onclick = () => {
|
||||
let value;
|
||||
try {
|
||||
value = readValue($modal);
|
||||
@@ -1538,7 +1448,7 @@ export class UI {
|
||||
return;
|
||||
}
|
||||
close(value);
|
||||
});
|
||||
};
|
||||
|
||||
$modal.querySelector('textarea, input')?.focus();
|
||||
});
|
||||
@@ -1548,6 +1458,86 @@ export class UI {
|
||||
// PRIVATE: RENDERING
|
||||
// ====================
|
||||
|
||||
/**
|
||||
* _bindMessageActions attaches click handlers to the action
|
||||
* buttons present in a cloned message bubble.
|
||||
*
|
||||
* Copy reads $bubble.dataset.copyText when clicked so tool bubbles
|
||||
* copy their updated output after _fillToolOutput runs. Approve
|
||||
* and deny look up the current execution record in
|
||||
* this.pendingApprovals when clicked.
|
||||
*
|
||||
* @param {HTMLElement} $bubble
|
||||
* @param {{id?: string | null, field?: string | null, toolCallId?: string | null, isError?: boolean}} [options]
|
||||
*/
|
||||
_bindMessageActions(
|
||||
$bubble,
|
||||
{ id = null, field = null, toolCallId = null, isError = false } = {},
|
||||
) {
|
||||
const $play = $bubble.querySelector('.message__action-btn--play');
|
||||
if ($play) {
|
||||
$play.onclick = () => {
|
||||
if (id) this.handlers.onPlay(id);
|
||||
};
|
||||
}
|
||||
|
||||
const $delete = $bubble.querySelector('.message__action-btn--delete');
|
||||
if ($delete) {
|
||||
$delete.onclick = () => {
|
||||
// Error bubbles are display-only: remove without a request.
|
||||
if (isError || !id) {
|
||||
$bubble.remove();
|
||||
return;
|
||||
}
|
||||
this._confirm(
|
||||
`delete:${id}:${field ?? ''}:${toolCallId ?? ''}`,
|
||||
$delete,
|
||||
'pending',
|
||||
() => this.handlers.onDelete({ id, field, toolCallId }),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const $inspect = $bubble.querySelector('.message__action-btn--inspect');
|
||||
if ($inspect) {
|
||||
$inspect.onclick = () => {
|
||||
const isOpen = $bubble.classList.toggle('message--debug-open');
|
||||
$inspect.setAttribute('aria-expanded', String(isOpen));
|
||||
};
|
||||
}
|
||||
|
||||
const $edit = $bubble.querySelector('.message__action-btn--edit');
|
||||
if ($edit) {
|
||||
$edit.onclick = () => this.handlers.onEdit({ id, field, toolCallId });
|
||||
}
|
||||
|
||||
const $copy = $bubble.querySelector('.message__action-btn--copy');
|
||||
if ($copy) {
|
||||
$copy.onclick = () =>
|
||||
this._copyToClipboard($copy, $bubble.dataset.copyText || '');
|
||||
}
|
||||
|
||||
const findExecution = () =>
|
||||
this.pendingApprovals.find(
|
||||
(execution) =>
|
||||
execution.messageId === id && execution.toolCallId === toolCallId,
|
||||
);
|
||||
const $approve = $bubble.querySelector('.message__approval-btn--approve');
|
||||
if ($approve) {
|
||||
$approve.onclick = () => {
|
||||
const execution = findExecution();
|
||||
if (execution) this.handlers.onApprove(execution);
|
||||
};
|
||||
}
|
||||
const $deny = $bubble.querySelector('.message__approval-btn--deny');
|
||||
if ($deny) {
|
||||
$deny.onclick = () => {
|
||||
const execution = findExecution();
|
||||
if (execution) this.handlers.onDeny(execution);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_renderUserMessage(row) {
|
||||
const $bubble =
|
||||
this.$tplUserMessage.content.cloneNode(true).firstElementChild;
|
||||
@@ -1557,6 +1547,7 @@ export class UI {
|
||||
$bubble.dataset.copyText = text;
|
||||
this._renderImagePills($bubble, row);
|
||||
this._populateDebug($bubble.querySelector('.message__debug-list'), row);
|
||||
this._bindMessageActions($bubble, { id: row.id });
|
||||
this.$chat.insertBefore($bubble, this.$chatLoading);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
@@ -1588,6 +1579,7 @@ export class UI {
|
||||
$bubble.dataset.voice = row.voice || '';
|
||||
$bubble.dataset.copyText = text;
|
||||
this._populateDebug($bubble.querySelector('.message__debug-list'), row);
|
||||
this._bindMessageActions($bubble, { id: row.id });
|
||||
this.$chat.insertBefore($bubble, this.$chatLoading);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
@@ -1603,6 +1595,7 @@ export class UI {
|
||||
$bubble.querySelector('.message__content').textContent = reasoning;
|
||||
$bubble.dataset.copyText = reasoning;
|
||||
this._populateDebug($bubble.querySelector('.message__debug-list'), row);
|
||||
this._bindMessageActions($bubble, { id: row.id, field: 'reasoning' });
|
||||
this.$chat.insertBefore($bubble, this.$chatLoading);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
@@ -1651,6 +1644,11 @@ export class UI {
|
||||
...result,
|
||||
voice: parentRow?.voice,
|
||||
});
|
||||
this._bindMessageActions($bubble, {
|
||||
id: parentRow?.id ?? null,
|
||||
field: parentRow ? 'tool' : null,
|
||||
toolCallId: callId || null,
|
||||
});
|
||||
this.$chat.insertBefore($bubble, this.$chatLoading);
|
||||
this._applyApproval($bubble);
|
||||
this.scrollToBottom();
|
||||
@@ -1689,6 +1687,7 @@ export class UI {
|
||||
const text = extractText(row.content?.content);
|
||||
$bubble.querySelector('.message__content').textContent = text;
|
||||
$bubble.dataset.copyText = text;
|
||||
this._bindMessageActions($bubble, { id: row.id });
|
||||
this.$chat.insertBefore($bubble, this.$chatLoading);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
type="button"
|
||||
class="compose__action-btn"
|
||||
id="compose-settings"
|
||||
data-action="compose-settings"
|
||||
aria-label="Compose settings"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#settings-2"></use></svg>
|
||||
@@ -23,7 +22,6 @@
|
||||
type="button"
|
||||
class="compose__action-btn"
|
||||
id="attach"
|
||||
data-action="attach"
|
||||
aria-label="Attach files"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#attach"></use></svg>
|
||||
@@ -32,7 +30,6 @@
|
||||
type="button"
|
||||
class="compose__action-btn"
|
||||
id="location"
|
||||
data-action="location"
|
||||
aria-label="Include location"
|
||||
title="Include GPS location"
|
||||
>
|
||||
@@ -64,7 +61,6 @@
|
||||
type="button"
|
||||
class="compose__action-btn compose__action-btn--send"
|
||||
id="send"
|
||||
data-action="send"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<svg class="icon compose__icon--send"><use href="/static/icons.svg#send"></use></svg>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
type="button"
|
||||
class="footer__toolbar-btn"
|
||||
id="settings"
|
||||
data-action="settings"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#settings"></use></svg>
|
||||
@@ -13,7 +12,6 @@
|
||||
type="button"
|
||||
class="footer__toolbar-btn"
|
||||
id="conversations"
|
||||
data-action="conversations"
|
||||
aria-label="Conversations"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#menu"></use></svg>
|
||||
@@ -24,7 +22,6 @@
|
||||
type="button"
|
||||
class="footer__toolbar-btn"
|
||||
id="reset"
|
||||
data-action="reset"
|
||||
aria-label="Reset conversation"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
|
||||
@@ -33,7 +30,6 @@
|
||||
type="button"
|
||||
class="footer__toolbar-btn"
|
||||
id="scroll-up"
|
||||
data-action="scroll-up"
|
||||
aria-label="Scroll to top"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#scroll-up"></use></svg>
|
||||
@@ -42,7 +38,6 @@
|
||||
type="button"
|
||||
class="footer__toolbar-btn"
|
||||
id="scroll-down"
|
||||
data-action="scroll-down"
|
||||
aria-label="Scroll to bottom and follow"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#scroll-down"></use></svg>
|
||||
@@ -51,7 +46,6 @@
|
||||
type="button"
|
||||
class="footer__toolbar-btn"
|
||||
id="mute"
|
||||
data-action="mute"
|
||||
aria-label="Mute"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#volume"></use></svg>
|
||||
|
||||
@@ -9,15 +9,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn message__action-btn--delete"
|
||||
data-action="delete"
|
||||
aria-label="Delete message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="inspect"
|
||||
class="message__action-btn message__action-btn--inspect"
|
||||
aria-label="Show details"
|
||||
aria-expanded="false"
|
||||
>
|
||||
@@ -25,16 +23,14 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="edit"
|
||||
class="message__action-btn message__action-btn--edit"
|
||||
aria-label="Edit message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="copy"
|
||||
class="message__action-btn message__action-btn--copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
|
||||
@@ -64,7 +60,6 @@
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn message__action-btn--play"
|
||||
data-action="play"
|
||||
aria-label="Play"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#volume"></use></svg>
|
||||
@@ -72,15 +67,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn message__action-btn--delete"
|
||||
data-action="delete"
|
||||
aria-label="Delete message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="inspect"
|
||||
class="message__action-btn message__action-btn--inspect"
|
||||
aria-label="Show details"
|
||||
aria-expanded="false"
|
||||
>
|
||||
@@ -88,16 +81,14 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="edit"
|
||||
class="message__action-btn message__action-btn--edit"
|
||||
aria-label="Edit message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="copy"
|
||||
class="message__action-btn message__action-btn--copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
|
||||
@@ -120,8 +111,7 @@
|
||||
<div class="message__actions-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="copy"
|
||||
class="message__action-btn message__action-btn--copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
|
||||
@@ -129,7 +119,6 @@
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn message__action-btn--delete"
|
||||
data-action="delete"
|
||||
aria-label="Delete message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
|
||||
@@ -179,15 +168,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn message__action-btn--delete"
|
||||
data-action="delete"
|
||||
aria-label="Delete message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="inspect"
|
||||
class="message__action-btn message__action-btn--inspect"
|
||||
aria-label="Show details"
|
||||
aria-expanded="false"
|
||||
>
|
||||
@@ -195,16 +182,14 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="edit"
|
||||
class="message__action-btn message__action-btn--edit"
|
||||
aria-label="Edit message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="copy"
|
||||
class="message__action-btn message__action-btn--copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
|
||||
@@ -241,13 +226,11 @@
|
||||
<button
|
||||
type="button"
|
||||
class="message__approval-btn message__approval-btn--approve"
|
||||
data-action="approve"
|
||||
aria-label="Approve tool call"
|
||||
>Approve</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__approval-btn message__approval-btn--deny"
|
||||
data-action="deny"
|
||||
aria-label="Deny tool call"
|
||||
>Deny</button>
|
||||
</div>
|
||||
@@ -257,15 +240,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn message__action-btn--delete"
|
||||
data-action="delete"
|
||||
aria-label="Delete message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#trash"></use></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="inspect"
|
||||
class="message__action-btn message__action-btn--inspect"
|
||||
aria-label="Show details"
|
||||
aria-expanded="false"
|
||||
>
|
||||
@@ -273,16 +254,14 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="edit"
|
||||
class="message__action-btn message__action-btn--edit"
|
||||
aria-label="Edit message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="copy"
|
||||
class="message__action-btn message__action-btn--copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
|
||||
@@ -310,16 +289,14 @@
|
||||
<div class="message__actions-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="edit"
|
||||
class="message__action-btn message__action-btn--edit"
|
||||
aria-label="Edit system message"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#edit"></use></svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="message__action-btn"
|
||||
data-action="copy"
|
||||
class="message__action-btn message__action-btn--copy"
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#copy"></use></svg>
|
||||
|
||||
Reference in New Issue
Block a user