Add new conversation button
This commit is contained in:
@@ -999,6 +999,9 @@ body {
|
||||
}
|
||||
|
||||
.conversations-panel__search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-1);
|
||||
padding: var(--s-1) var(--s1);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
flex-shrink: 0;
|
||||
@@ -1006,7 +1009,7 @@ body {
|
||||
|
||||
.conversations-search {
|
||||
display: block;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
height: var(--s2);
|
||||
padding: 0 var(--s-1);
|
||||
font-family: inherit;
|
||||
@@ -1023,6 +1026,40 @@ body {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* The new-conversation button pairs with the search input: the
|
||||
same height, the same border, one icon. */
|
||||
.conversations-new {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--s2);
|
||||
height: var(--s2);
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
color: var(--color-text-muted);
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.conversations-new svg {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
}
|
||||
|
||||
.conversations-new:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.conversations-new:hover {
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.conversations-panel__list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -146,4 +146,8 @@
|
||||
<path d="M19 17V5a2 2 0 0 0-2-2H4"/>
|
||||
<path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>
|
||||
</symbol>
|
||||
<symbol id="plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M12 5v14"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.8 KiB |
@@ -80,6 +80,7 @@ class Odidere {
|
||||
onSelectConversation: (id) => this.switchConversation(id),
|
||||
onRenameConversation: (id) => this.renameConversation(id),
|
||||
onDeleteConversation: (id) => this.deleteConversation(id),
|
||||
onNewConversation: () => this.newConversation(),
|
||||
onPushToTalkStart: () => this.handlePushToTalkStart(),
|
||||
onPushToTalkEnd: () => this.handlePushToTalkEnd(),
|
||||
onTranscribeStart: () => this.handleTranscribeStart(),
|
||||
@@ -700,16 +701,7 @@ class Odidere {
|
||||
async reset() {
|
||||
this.tts.stop();
|
||||
const id = this.conversationId;
|
||||
// Clear the selection first, so the aborted run's cleanup does
|
||||
// not open a new subscription to a conversation being deleted.
|
||||
this.conversationId = null;
|
||||
this.syncController?.abort();
|
||||
this.runController?.abort();
|
||||
if (this.currentRun) {
|
||||
try {
|
||||
await this.currentRun;
|
||||
} catch {}
|
||||
}
|
||||
await this.detachConversation();
|
||||
if (id) {
|
||||
try {
|
||||
await this.api.deleteConversation(id);
|
||||
@@ -742,6 +734,31 @@ class Odidere {
|
||||
this.updateSendEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* detachConversation unselects the conversation, aborts any open
|
||||
* sync or inference stream, and waits for an in-flight run to
|
||||
* finish its cleanup.
|
||||
*
|
||||
* It does not stop the run on the server; it only ends local
|
||||
* stream delivery.
|
||||
*
|
||||
* conversationId is cleared before the aborts so that the ending
|
||||
* streams do not persist the cache, open a new sync stream, or
|
||||
* fetch approvals for the old conversation. Awaiting currentRun
|
||||
* lets run's finally block clear busy and drafts before the caller
|
||||
* clears local state or loads another conversation.
|
||||
*/
|
||||
async detachConversation() {
|
||||
this.conversationId = null;
|
||||
this.syncController?.abort();
|
||||
this.runController?.abort();
|
||||
if (this.currentRun) {
|
||||
try {
|
||||
await this.currentRun;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
// CONVERSATIONS
|
||||
// ====================
|
||||
@@ -873,23 +890,29 @@ class Odidere {
|
||||
}
|
||||
this.conversations = this.conversations.filter((c) => c.id !== id);
|
||||
if (id === this.conversationId) {
|
||||
// The server-side delete cancels an ongoing inference. Detach
|
||||
// the local streams before the state goes, so the aborted
|
||||
// run's cleanup does not open a new subscription.
|
||||
// The server-side delete cancels an ongoing inference.
|
||||
this.tts.stop();
|
||||
this.conversationId = null;
|
||||
this.syncController?.abort();
|
||||
this.runController?.abort();
|
||||
if (this.currentRun) {
|
||||
try {
|
||||
await this.currentRun;
|
||||
} catch {}
|
||||
}
|
||||
await this.detachConversation();
|
||||
this.clearLocalConversation();
|
||||
}
|
||||
this.filterConversations(this.conversationQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* newConversation closes the picker and clears the view for a
|
||||
* new conversation. It sends no request: send() creates the
|
||||
* conversation when the next message goes out, so an abandoned
|
||||
* click leaves no empty conversation on the server. A run in
|
||||
* flight on the old conversation keeps running there.
|
||||
*/
|
||||
async newConversation() {
|
||||
this.ui.closeConversations();
|
||||
if (!this.conversationId) return;
|
||||
this.tts.stop();
|
||||
await this.detachConversation();
|
||||
this.clearLocalConversation();
|
||||
}
|
||||
|
||||
// ====================
|
||||
// SYNC
|
||||
// ====================
|
||||
|
||||
@@ -116,6 +116,7 @@ export class UI {
|
||||
);
|
||||
this.$conversationsClose = document.getElementById('conversations-close');
|
||||
this.$conversationsSearch = document.getElementById('conversation-search');
|
||||
this.$conversationNew = document.getElementById('conversation-new');
|
||||
this.$conversationsList = document.getElementById('conversation-list');
|
||||
this.$tplConversationItem = document.getElementById(
|
||||
'tpl-conversation-item',
|
||||
@@ -1166,6 +1167,9 @@ 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;
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
aria-label="Search conversations"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="conversations-new"
|
||||
id="conversation-new"
|
||||
aria-label="New conversation"
|
||||
>
|
||||
<svg class="icon"><use href="/static/icons.svg#plus"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="conversations-panel__list" id="conversation-list"></div>
|
||||
|
||||
Reference in New Issue
Block a user