Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function initialiseCompactChatComposer(composer, resizeDraft = resizeComp
const menu = composer.querySelector('.chat-composer-more-actions');
const panel = menu?.querySelector('.button-row');
if (!panel) return;
const speechActions = panel.querySelector('.composer-speech-actions') || panel;
const summary = menu.querySelector('summary');
const label = summary?.querySelector('.composer-more-label');
let closeWatcher;
Expand Down Expand Up @@ -107,8 +108,8 @@ export function initialiseCompactChatComposer(composer, resizeDraft = resizeComp
const compact = isCompactComposer();
if (compact) {
if (mic?.classList.contains('active-dictation') || voice?.getAttribute('aria-pressed') === 'true') menu.open = false;
place(mic, mic?.classList.contains('active-dictation') ? active : panel);
place(voice, voice?.getAttribute('aria-pressed') === 'true' ? active : panel);
place(mic, mic?.classList.contains('active-dictation') ? active : speechActions);
place(voice, voice?.getAttribute('aria-pressed') === 'true' ? active : speechActions);
place(cancel, active);
place(retry, active);
place(status, status?.dataset.passive === 'true' ? panel : composer);
Expand Down
91 changes: 91 additions & 0 deletions src/frontend/web/von_interface/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -18952,3 +18952,94 @@ html[data-footer-uptime="false"][data-footer-runtimecost="false"][data-footer-bu
margin: 0;
padding: 0;
}

/* Group the composer's secondary actions without changing their disclosure. */
.chat-composer-more-actions[open] > .composer-options-panel {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-items: stretch;
justify-content: flex-start;
gap: 0;
padding: 0;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.composer-options-section {
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
min-width: 0;
padding: 16px;
}
.composer-options-section + .composer-options-section {
border-top: 1px solid #e2e8f0;
}
.composer-options-section h3 {
flex-basis: 100%;
margin: 0;
color: #334155;
font-size: 0.875rem;
font-weight: 650;
line-height: 1.4;
}
.composer-speech-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.composer-options-section .btn,
.composer-options-section select {
min-height: 44px;
margin: 0;
border-radius: 8px;
font-size: 0.875rem;
}
.composer-options-section .btn {
white-space: normal;
text-align: left;
}
.composer-options-section label[for="dictationEngineSelect"] {
flex-basis: 100%;
color: #475569;
font-size: 0.8125rem;
}
.composer-options-section #dictationEngineSelect {
width: 100%;
min-width: 0;
padding: 8px;
border: 1px solid #cbd5e1;
background: #fff;
color: #334155;
}
.composer-options-section .annotation-toggle {
min-height: 44px;
margin: 0;
padding: 6px 10px;
box-sizing: border-box;
}
.composer-options-section .compact-speech-help,
.composer-options-section #imageAttachmentHelp {
display: block;
flex-basis: 100%;
margin: 0;
color: #64748b;
font-size: 0.75rem;
line-height: 1.6;
overflow-wrap: anywhere;
}
.composer-options-section :is(button, select, input):focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
@media (max-width: 800px), (max-width: 1024px) and (pointer: coarse) {
.chat-composer-more-actions[open] > .composer-options-panel {
max-height: 55dvh;
overflow-y: auto;
border-radius: 8px;
background: #f8fafc;
}
.composer-options-section { padding: 12px; }
}
57 changes: 34 additions & 23 deletions src/frontend/web/von_interface/templates/chat_tab.html
Original file line number Diff line number Diff line change
Expand Up @@ -247,27 +247,38 @@ <h3 id="conversationReferenceInspectorTitle" class="chat-reference-inspector-tit

<details class="chat-composer-more-actions">
<summary title="More actions"><span class="composer-more-label">More actions</span><span class="composer-more-icon" aria-hidden="true">⋯</span><span class="composer-add-icon" aria-hidden="true">+</span></summary>
<div class="button-row">
<p class="compact-speech-help">Von dictation and voice send audio and conversation context to OpenAI. Von does not save the audio. Browser recognition uses your browser’s speech service.</p>
<button id="voiceConversationButton" class="btn btn-secondary" type="button" aria-pressed="false" title="Have a voice conversation: speech is sent automatically and Von replies aloud. You can also Shift-click or long-press the microphone.">Start voice</button>
<label for="dictationEngineSelect">Dictation</label>
<select id="dictationEngineSelect" aria-describedby="dictationStatus">
<div class="button-row composer-options-panel">
<section class="composer-options-section" aria-labelledby="composerSpeechHeading">
<h3 id="composerSpeechHeading">Voice &amp; dictation</h3>
<div class="composer-speech-actions">
<button id="voiceConversationButton" class="btn btn-secondary" type="button" aria-pressed="false" title="Have a voice conversation: speech is sent automatically and Von replies aloud. You can also Shift-click or long-press the microphone.">Start voice</button>
</div>
<label for="dictationEngineSelect">Dictation</label>
<select id="dictationEngineSelect" aria-describedby="dictationStatus">
<option value="streaming">Live transcription · context-aware</option>
<option value="recorded" selected>Recorded audio · context-aware</option>
<option value="browser">Browser recognition · limited support</option>
</select>
<button id="resetButton" class="btn btn-secondary" type="button"
title="End this conversation (resets Von's context while keeping the conversation view)">End
conversation</button>

<p id="imageAttachmentHelp">If your keyboard cannot paste a screenshot, use Paste image and allow clipboard access, or choose Attach image. Your caption stays in the draft. Attach up to eight still PNG, JPEG or WebP images, each up to 8 MiB and 25 million pixels. Review previews before sending. Remove an image to replace it.</p>
<button id="pasteImageButton" class="btn btn-secondary" type="button" aria-describedby="imageAttachmentHelp">Paste image</button>
<button id="uploadFileButton" class="btn btn-secondary" type="button"
</select>
<label class="annotation-toggle" title="Speak Von responses aloud">
<input type="checkbox" id="ttsToggle">
<span class="eye-icon" aria-hidden="true">TTS</span>
<span class="toggle-label">Speech</span>
</label>
<p class="compact-speech-help">Von dictation and voice send audio and conversation context to OpenAI. Von does not save the audio. Browser recognition uses your browser’s speech service.</p>
</section>
<section class="composer-options-section" aria-labelledby="composerFilesHeading">
<h3 id="composerFilesHeading">Images &amp; files</h3>
<button id="pasteImageButton" class="btn btn-secondary" type="button" aria-describedby="imageAttachmentHelp">Paste image</button>
<button id="uploadFileButton" class="btn btn-secondary" type="button"
title="Upload a file, drag and drop onto the conversation, or paste an image">Upload File</button>
<input id="uploadFileInput" class="visually-hidden" type="file" multiple aria-label="Upload file" />
<input id="uploadFileInput" class="visually-hidden" type="file" multiple aria-label="Upload file" />

{% if expert_tabs_enabled %}
<label class="annotation-toggle" title="Enable concept annotations in chat responses">
<p id="imageAttachmentHelp">If your keyboard cannot paste a screenshot, use Paste image and allow clipboard access, or choose Attach image. Your caption stays in the draft. Attach up to eight still PNG, JPEG or WebP images, each up to 8 MiB and 25 million pixels. Review previews before sending. Remove an image to replace it.</p>
</section>
<section class="composer-options-section" aria-labelledby="composerConversationHeading">
<h3 id="composerConversationHeading">Conversation</h3>
{% if expert_tabs_enabled %}
<label class="annotation-toggle" title="Enable concept annotations in chat responses">
<input type="checkbox" id="annotationToggle">
<span class="eye-icon">
<svg aria-hidden="true" class="icon icon-annotate" viewBox="0 0 24 24" width="16" height="16"
Expand All @@ -277,14 +288,14 @@ <h3 id="conversationReferenceInspectorTitle" class="chat-reference-inspector-tit
</svg>
</span>
<span class="toggle-label">Annotations</span>
</label>
{% endif %}
</label>
{% endif %}

<label class="annotation-toggle" title="Speak Von responses aloud">
<input type="checkbox" id="ttsToggle">
<span class="eye-icon" aria-hidden="true">TTS</span>
<span class="toggle-label">Speech</span>
</label>
<button id="resetButton" class="btn btn-secondary" type="button"
title="End this conversation (resets Von's context while keeping the conversation view)">End
conversation</button>

</section>
</div>
</details>
</div>
Expand Down
17 changes: 17 additions & 0 deletions tests/browser/composerControls.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ async function bounds(page, selector) {
assert.equal((await bounds(page, '.chat-composer')).height, composer.height, 'menu must not expand desktop composer');
}
if (evidence) await page.screenshot({ path: path.join(evidence, `${profile.name}-menu.png`), fullPage: true });
// Each group remains reachable within the scrolling disclosure.
const options = page.locator('.composer-options-panel');
for (const id of ['dictationEngineSelect', 'ttsToggle', 'pasteImageButton', 'uploadFileButton', 'annotationToggle', 'resetButton']) {
const control = page.locator(`#${id}`);
await control.scrollIntoViewIfNeeded();
await control.click({ trial: true });
const box = await control.boundingBox();
const panelBox = await options.boundingBox();
assert(box.x >= panelBox.x && box.x + box.width <= panelBox.x + panelBox.width + 1, `${id} fits panel`);
}
await page.locator('#ttsToggle').check();
await expect(page.locator('#ttsToggle')).toBeChecked();
await page.locator('#dictationEngineSelect').selectOption('browser');
await expect(page.locator('#dictationEngineSelect')).toHaveValue('browser');
await page.locator('#dictationEngineSelect').selectOption('recorded');
await page.locator('#resetButton').scrollIntoViewIfNeeded();
if (evidence) await page.screenshot({ path: path.join(evidence, `${profile.name}-menu-bottom.png`), fullPage: true });
await summary.focus();
await page.keyboard.press('Space');
await expect(page.locator('.chat-composer-more-actions')).not.toHaveAttribute('open', '');
Expand Down
Loading