mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(voice): add typed AI chat beside voice
Chat sits next to the mic in the command dock. Replies are silent and never request the microphone; the first message connects with the selected provider and runs the same map actions as voice through each provider's text API (OpenAI OPENAI_CHAT_MODEL, Gemini GEMINI_CHAT_MODEL via generateContent, which never starts Live). Only one mode runs at a time. - Chat panel rendered by the voice control presenter: conversation, recent and quick-command suggestions, Up/Down history, attachments (map-layer references or small UTF-8 text files, sent only with the message) - Chat and voice panels sit above the dock or dock into the right rail's column; minimized bars sit centred above the dock, and height animates on swaps - A centred panel steps down to its bar for the Location and Visual Presets trays without moving focus, rests above an open tray, and docks right while expanded over one; it returns to the centre once the trays close - Minimized voice previews the last sentence from each side; the empty Voice Preview suggests things to say - Settings opened from chat keep chat's frame and show the shared AI provider and quick commands; voice Settings show Back to voice - Closing chat ends a typed session; the voice panel's close turns voice off - The dock widens its voice control for the chat button and restyles its wings, which show icons only at 900px and below; assistant panels share the left panels' glass, and Cyber keeps its own surfaces - Server text-chat handlers with body, time and per-IP limits (GEV_RATELIMIT_*_CHAT_PER_MIN), documented in SECURITY.md; tests and all eleven locales Co-authored-by: Darren Dignam <[email protected]>
This commit is contained in:
1 parent
720260af58
commit
af461febae
57 files changed
+6500
-63
No files matched your search
@@ -64,7 +64,7 @@ export function createManualVoicePanel({
|
||||
hidden: ui.state.card.visible,
|
||||
text: () => (previewOn ? copy.previewEmpty : copy.previewDisabled),
|
||||
});
|
||||
if (restoreFocus) void ui.focus(['settingsButton', 'button']);
|
||||
if (restoreFocus) void ui.focus(['settingsButton', 'chatButton', 'button']);
|
||||
};
|
||||
const refuse = (reason) => ({
|
||||
ok: false,
|
||||
@@ -79,11 +79,18 @@ export function createManualVoicePanel({
|
||||
const transferFocus = changed && holdsFocus(panes[view]);
|
||||
open = true;
|
||||
view = next;
|
||||
// An explicit request restores a minimized panel; automatic reveals do not.
|
||||
if (!automatic && ui.state.voicePanel.minimized)
|
||||
ui.update('voicePanel', { minimized: false });
|
||||
onPresentationIntent?.({ automatic });
|
||||
sync();
|
||||
// The tab takes focus once it shows, inside a shown panel.
|
||||
// The view's tab takes focus once it shows, inside a shown panel, or the
|
||||
// next visible control when the tab is hidden: voice Settings show Back
|
||||
// instead, and Preview falls to Settings.
|
||||
if (focus || transferFocus)
|
||||
void ui.focus([tabs[next === 'preview' ? 0 : 1]]);
|
||||
void ui.focus(
|
||||
next === 'preview' ? [tabs[0], tabs[1]] : [tabs[1], 'voiceBack'],
|
||||
);
|
||||
return { ok: true, changed, ...snapshot() };
|
||||
};
|
||||
const close = (onlyView) => {
|
||||
@@ -94,6 +101,9 @@ export function createManualVoicePanel({
|
||||
return { ok: true, changed: false, ...snapshot() };
|
||||
const changed = open;
|
||||
open = false;
|
||||
// A caller's context (Settings opened from chat) ends with the panel.
|
||||
if (ui.state.voicePanel.minimized || ui.state.voicePanel.context)
|
||||
ui.update('voicePanel', { minimized: false, context: null });
|
||||
onPresentationIntent?.();
|
||||
sync();
|
||||
return { ok: true, changed, ...snapshot() };
|
||||
@@ -110,9 +120,13 @@ export function createManualVoicePanel({
|
||||
};
|
||||
offs.push(
|
||||
ui.on('settings-button', toggleSettings),
|
||||
// Preview has no visible tab; the Settings tab toggles back to it.
|
||||
ui.on('tab', (index) =>
|
||||
select(index === 0 ? 'preview' : 'settings', { focus: true }),
|
||||
index === 1 && open && view === 'settings'
|
||||
? select('preview', { focus: true })
|
||||
: select(index === 0 ? 'preview' : 'settings', { focus: true }),
|
||||
),
|
||||
ui.on('voice-back', () => select('preview', { focus: true })),
|
||||
ui.on('tab-keydown', (index, event) => {
|
||||
const next =
|
||||
event.key === 'Home'
|
||||
@@ -130,7 +144,10 @@ export function createManualVoicePanel({
|
||||
ui.on('panel-keydown', (event) => {
|
||||
if (event.key !== 'Escape' || !open) return;
|
||||
event.preventDefault();
|
||||
close();
|
||||
// Voice Settings step back to the preview, like chat Settings to chat.
|
||||
if (view === 'settings' && !ui.state.voicePanel.context)
|
||||
select('preview', { focus: true });
|
||||
else close();
|
||||
}),
|
||||
ui.on('pointerleave', (event) => {
|
||||
if (event.pointerType === 'touch') return;
|
||||
|
||||
Reference in new issue
Block a user