mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +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
@@ -0,0 +1,92 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { bindVoiceMiniPreview, lastSentence } from './voiceMiniPreview.js';
|
||||
import { createTestVoiceUi } from '../testSupport/voiceUi.mjs';
|
||||
|
||||
test('lastSentence keeps only the final sentence across scripts', () => {
|
||||
assert.equal(
|
||||
lastSentence('Flying to Tokyo. Showing flights now.', 'en'),
|
||||
'Showing flights now.',
|
||||
);
|
||||
assert.equal(
|
||||
lastSentence(' One sentence only ', 'en'),
|
||||
'One sentence only',
|
||||
);
|
||||
assert.equal(
|
||||
lastSentence('東京へ移動します。便を表示します。', 'ja'),
|
||||
'便を表示します。',
|
||||
);
|
||||
assert.equal(lastSentence('', 'en'), '');
|
||||
assert.equal(lastSentence(null, 'en'), '');
|
||||
});
|
||||
|
||||
function fixture({ textOnly = false } = {}) {
|
||||
const ui = createTestVoiceUi({ panel: true });
|
||||
const listeners = new Set();
|
||||
const session = {
|
||||
adapter: { getStartOptions: () => ({ textOnly }) },
|
||||
subscribe(fn) {
|
||||
listeners.add(fn);
|
||||
return () => listeners.delete(fn);
|
||||
},
|
||||
};
|
||||
const emit = (event) => listeners.forEach((fn) => fn(event));
|
||||
const unbind = bindVoiceMiniPreview({ ui, session });
|
||||
return { ui, emit, unbind, listeners };
|
||||
}
|
||||
|
||||
test('voice preview shows the last user and assistant sentence', () => {
|
||||
const f = fixture();
|
||||
const mini = () => f.ui.state.mini;
|
||||
assert.deepEqual(mini(), { user: '', assistant: '' });
|
||||
assert.equal(f.ui.state.root.voiceExchange, false);
|
||||
f.emit({ type: 'transcript', role: 'user', text: 'Hi. Show live flights.' });
|
||||
assert.equal(mini().user, 'Show live flights.');
|
||||
assert.equal(f.ui.state.root.voiceExchange, true);
|
||||
f.emit({
|
||||
type: 'transcript',
|
||||
role: 'assistant',
|
||||
text: 'Showing flights. ',
|
||||
responseId: 'r1',
|
||||
});
|
||||
f.emit({
|
||||
type: 'transcript',
|
||||
role: 'assistant',
|
||||
text: 'There are 214',
|
||||
responseId: 'r1',
|
||||
});
|
||||
assert.equal(mini().assistant, 'There are 214');
|
||||
f.emit({
|
||||
type: 'transcript',
|
||||
role: 'assistant',
|
||||
text: 'Showing flights. There are 214 aircraft.',
|
||||
responseId: 'r1',
|
||||
final: true,
|
||||
});
|
||||
assert.equal(mini().assistant, 'There are 214 aircraft.');
|
||||
f.emit({
|
||||
type: 'transcript',
|
||||
role: 'assistant',
|
||||
text: 'Done.',
|
||||
responseId: 'r2',
|
||||
});
|
||||
assert.equal(mini().assistant, 'Done.');
|
||||
f.emit({ type: 'state', state: 'connecting' });
|
||||
assert.deepEqual(mini(), { user: '', assistant: '' });
|
||||
assert.equal(f.ui.state.root.voiceExchange, false);
|
||||
f.unbind();
|
||||
assert.equal(f.listeners.size, 0);
|
||||
});
|
||||
|
||||
test('typed chat turns do not reach the voice preview', () => {
|
||||
const f = fixture({ textOnly: true });
|
||||
f.emit({ type: 'transcript', role: 'user', text: 'Typed question.' });
|
||||
f.emit({
|
||||
type: 'transcript',
|
||||
role: 'assistant',
|
||||
text: 'Typed reply.',
|
||||
final: true,
|
||||
});
|
||||
assert.deepEqual(f.ui.state.mini, { user: '', assistant: '' });
|
||||
assert.equal(f.ui.state.root.voiceExchange, false);
|
||||
});
|
||||
Reference in new issue
Block a user