convoai 1.5.3 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/dist/src/commands/agent/chat.js +2 -4
- package/dist/src/commands/agent/chat.js.map +1 -1
- package/dist/src/commands/agent/join.js +2 -4
- package/dist/src/commands/agent/join.js.map +1 -1
- package/dist/src/commands/dev.d.ts +2 -0
- package/dist/src/commands/dev.js +73 -0
- package/dist/src/commands/dev.js.map +1 -0
- package/dist/src/commands/go.js +3 -8
- package/dist/src/commands/go.js.map +1 -1
- package/dist/src/commands/init.d.ts +2 -0
- package/dist/src/commands/init.js +326 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/openclaw.js +2 -4
- package/dist/src/commands/openclaw.js.map +1 -1
- package/dist/src/commands/quickstart.js +137 -56
- package/dist/src/commands/quickstart.js.map +1 -1
- package/dist/src/index.js +11 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/starters/web/server/convoai-api.d.ts +42 -0
- package/dist/src/starters/web/server/convoai-api.js +116 -0
- package/dist/src/starters/web/server/convoai-api.js.map +1 -0
- package/dist/src/starters/web/server/index.d.ts +1 -0
- package/dist/src/starters/web/server/index.js +85 -0
- package/dist/src/starters/web/server/index.js.map +1 -0
- package/dist/src/starters/web/server/routes/callback.d.ts +2 -0
- package/dist/src/starters/web/server/routes/callback.js +10 -0
- package/dist/src/starters/web/server/routes/callback.js.map +1 -0
- package/dist/src/starters/web/server/routes/knowledge.d.ts +2 -0
- package/dist/src/starters/web/server/routes/knowledge.js +15 -0
- package/dist/src/starters/web/server/routes/knowledge.js.map +1 -0
- package/dist/src/starters/web/server/routes/session.d.ts +2 -0
- package/dist/src/starters/web/server/routes/session.js +82 -0
- package/dist/src/starters/web/server/routes/session.js.map +1 -0
- package/dist/src/starters/web/server/routes/token.d.ts +2 -0
- package/dist/src/starters/web/server/routes/token.js +19 -0
- package/dist/src/starters/web/server/routes/token.js.map +1 -0
- package/dist/src/web/serve.d.ts +7 -0
- package/dist/src/web/serve.js +42 -0
- package/dist/src/web/serve.js.map +1 -0
- package/package.json +3 -1
- package/src/starters/web/.env.example +28 -0
- package/src/starters/web/README.md +81 -0
- package/src/starters/web/connectors/README.md +35 -0
- package/src/starters/web/frontend/app.js +352 -0
- package/src/starters/web/frontend/index.html +54 -0
- package/src/starters/web/frontend/style.css +241 -0
- package/src/starters/web/package.json +23 -0
- package/src/starters/web/python-server/.env.example +23 -0
- package/src/starters/web/python-server/README.md +37 -0
- package/src/starters/web/python-server/app.py +202 -0
- package/src/starters/web/python-server/requirements.txt +4 -0
- package/src/starters/web/python-server/token_builder.py +99 -0
- package/src/starters/web/server/convoai-api.ts +243 -0
- package/src/starters/web/server/index.ts +109 -0
- package/src/starters/web/server/routes/callback.ts +13 -0
- package/src/starters/web/server/routes/knowledge.ts +19 -0
- package/src/starters/web/server/routes/session.ts +122 -0
- package/src/starters/web/server/routes/token.ts +22 -0
- package/src/starters/web/server/tsconfig.json +14 -0
- package/src/web/chat-client.html +1 -1
- package/src/web/client.html +1 -1
- package/src/web/serve.ts +45 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// ConvoAI Starter — Frontend Logic
|
|
2
|
+
// Customize: modify the UI, add features, change interaction flow.
|
|
3
|
+
|
|
4
|
+
const SERVER = window.location.origin;
|
|
5
|
+
|
|
6
|
+
let client = null;
|
|
7
|
+
let localTrack = null;
|
|
8
|
+
let currentSession = null;
|
|
9
|
+
let isMuted = false;
|
|
10
|
+
let analyserNode = null;
|
|
11
|
+
let freqAnimFrame = null;
|
|
12
|
+
let historyTimer = null;
|
|
13
|
+
let lastHistoryCount = 0;
|
|
14
|
+
|
|
15
|
+
// Subtitle state — track partial (ephemeral) and final messages
|
|
16
|
+
let subtitleEntries = []; // { role, text, final, id }
|
|
17
|
+
let ephemeral = null; // current partial message
|
|
18
|
+
|
|
19
|
+
const $ = (id) => document.getElementById(id);
|
|
20
|
+
|
|
21
|
+
// ─── Agent Status ──────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function setAgentStatus(text, state) {
|
|
24
|
+
const el = $('agent-status');
|
|
25
|
+
el.textContent = text;
|
|
26
|
+
el.className = 'agent-status' + (state ? ' ' + state : '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setAgentRing(state) {
|
|
30
|
+
$('agent-ring').className = 'agent-ring' + (state ? ' ' + state : '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Subtitles ─────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
// Incremental rendering — only update what changed, no full rebuild
|
|
36
|
+
let renderedCount = 0;
|
|
37
|
+
let ephemeralEl = null;
|
|
38
|
+
|
|
39
|
+
function renderSubtitles() {
|
|
40
|
+
const container = $('subtitles');
|
|
41
|
+
|
|
42
|
+
// Append new final entries only
|
|
43
|
+
while (renderedCount < subtitleEntries.length) {
|
|
44
|
+
const entry = subtitleEntries[renderedCount];
|
|
45
|
+
// Remove ephemeral before adding final
|
|
46
|
+
if (ephemeralEl) { ephemeralEl.remove(); ephemeralEl = null; }
|
|
47
|
+
container.appendChild(createSubLine(entry.role, entry.text, true));
|
|
48
|
+
renderedCount++;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Update or create ephemeral element
|
|
52
|
+
if (ephemeral) {
|
|
53
|
+
if (!ephemeralEl) {
|
|
54
|
+
ephemeralEl = createSubLine(ephemeral.role, ephemeral.text, false);
|
|
55
|
+
container.appendChild(ephemeralEl);
|
|
56
|
+
} else {
|
|
57
|
+
// Just update text, don't recreate
|
|
58
|
+
ephemeralEl.querySelector('.sub-body').textContent = ephemeral.text;
|
|
59
|
+
}
|
|
60
|
+
} else if (ephemeralEl) {
|
|
61
|
+
ephemeralEl.remove();
|
|
62
|
+
ephemeralEl = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
container.scrollTop = container.scrollHeight;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createSubLine(role, text, isFinal) {
|
|
69
|
+
const div = document.createElement('div');
|
|
70
|
+
div.className = 'sub-line ' + (role === 'user' ? 'user' : 'agent') + (isFinal ? '' : ' partial');
|
|
71
|
+
|
|
72
|
+
const label = document.createElement('div');
|
|
73
|
+
label.className = 'label';
|
|
74
|
+
label.textContent = role === 'user' ? 'You' : 'AI';
|
|
75
|
+
|
|
76
|
+
const body = document.createElement('div');
|
|
77
|
+
body.className = 'sub-body';
|
|
78
|
+
body.textContent = text;
|
|
79
|
+
|
|
80
|
+
div.appendChild(label);
|
|
81
|
+
div.appendChild(body);
|
|
82
|
+
return div;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleTranscript(msg) {
|
|
86
|
+
const obj = msg.object;
|
|
87
|
+
const text = msg.text || '';
|
|
88
|
+
|
|
89
|
+
console.log('[subtitle]', JSON.stringify(msg));
|
|
90
|
+
|
|
91
|
+
if (obj === 'user.transcription') {
|
|
92
|
+
if (msg.final) {
|
|
93
|
+
subtitleEntries.push({ role: 'user', text });
|
|
94
|
+
ephemeral = null;
|
|
95
|
+
} else {
|
|
96
|
+
ephemeral = { role: 'user', text };
|
|
97
|
+
}
|
|
98
|
+
} else if (obj === 'assistant.transcription') {
|
|
99
|
+
const status = msg.turn_status;
|
|
100
|
+
// turn_status: 0=IN_PROGRESS, 1=END, 2=INTERRUPTED
|
|
101
|
+
// If turn_status is missing, use final field as fallback
|
|
102
|
+
if (status === 1 || (status === undefined && msg.final === true)) {
|
|
103
|
+
subtitleEntries.push({ role: 'assistant', text });
|
|
104
|
+
ephemeral = null;
|
|
105
|
+
} else if (status === 2) {
|
|
106
|
+
subtitleEntries.push({ role: 'assistant', text: text + ' [interrupted]' });
|
|
107
|
+
ephemeral = null;
|
|
108
|
+
} else {
|
|
109
|
+
// In progress (status === 0, undefined, or any other value)
|
|
110
|
+
ephemeral = { role: 'assistant', text };
|
|
111
|
+
}
|
|
112
|
+
} else if (obj && text) {
|
|
113
|
+
// Unknown message type — show it anyway
|
|
114
|
+
const role = obj.includes('user') ? 'user' : 'assistant';
|
|
115
|
+
subtitleEntries.push({ role, text });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
renderSubtitles();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Buttons ───────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function setMainBtn(text, danger, disabled) {
|
|
124
|
+
const btn = $('btn-start');
|
|
125
|
+
btn.textContent = text;
|
|
126
|
+
btn.className = 'btn-main' + (danger ? ' danger' : '');
|
|
127
|
+
btn.disabled = disabled;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function updateMicIcon() {
|
|
131
|
+
$('mic-icon').style.display = isMuted ? 'none' : 'block';
|
|
132
|
+
$('mic-off-icon').style.display = isMuted ? 'block' : 'none';
|
|
133
|
+
$('btn-mic').className = 'btn-circle' + (isMuted ? ' muted' : '');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function showInterrupt(show) {
|
|
137
|
+
$('btn-interrupt').style.visibility = show ? 'visible' : 'hidden';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Frequency Bars ────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function startFreqBars() {
|
|
143
|
+
$('freq-bars').classList.add('active');
|
|
144
|
+
if (!localTrack || !localTrack._mediaStreamTrack) return;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const ctx = new AudioContext();
|
|
148
|
+
const src = ctx.createMediaStreamSource(new MediaStream([localTrack._mediaStreamTrack]));
|
|
149
|
+
analyserNode = ctx.createAnalyser();
|
|
150
|
+
analyserNode.fftSize = 64;
|
|
151
|
+
src.connect(analyserNode);
|
|
152
|
+
const data = new Uint8Array(analyserNode.frequencyBinCount);
|
|
153
|
+
const spans = $('freq-bars').querySelectorAll('span');
|
|
154
|
+
|
|
155
|
+
(function draw() {
|
|
156
|
+
analyserNode.getByteFrequencyData(data);
|
|
157
|
+
[data[1], data[3], data[6], data[9], data[13]].forEach((v, i) => {
|
|
158
|
+
spans[i].style.height = Math.max(4, (v / 255) * 28) + 'px';
|
|
159
|
+
});
|
|
160
|
+
freqAnimFrame = requestAnimationFrame(draw);
|
|
161
|
+
})();
|
|
162
|
+
} catch {
|
|
163
|
+
const spans = $('freq-bars').querySelectorAll('span');
|
|
164
|
+
(function rand() {
|
|
165
|
+
spans.forEach(s => { s.style.height = (4 + Math.random() * 18) + 'px'; });
|
|
166
|
+
freqAnimFrame = requestAnimationFrame(rand);
|
|
167
|
+
})();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function stopFreqBars() {
|
|
172
|
+
$('freq-bars').classList.remove('active');
|
|
173
|
+
if (freqAnimFrame) { cancelAnimationFrame(freqAnimFrame); freqAnimFrame = null; }
|
|
174
|
+
analyserNode = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Mic / Interrupt ───────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function toggleMic() {
|
|
180
|
+
if (!localTrack) return;
|
|
181
|
+
isMuted = !isMuted;
|
|
182
|
+
localTrack.setMuted(isMuted);
|
|
183
|
+
updateMicIcon();
|
|
184
|
+
if (isMuted) { stopFreqBars(); setAgentStatus('Muted', ''); }
|
|
185
|
+
else { startFreqBars(); setAgentStatus('Listening', 'active'); }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function interruptAgent() {
|
|
189
|
+
if (!currentSession) return;
|
|
190
|
+
try {
|
|
191
|
+
await fetch(SERVER + '/session/interrupt', {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
headers: { 'Content-Type': 'application/json' },
|
|
194
|
+
body: JSON.stringify({ agentId: currentSession.agentId }),
|
|
195
|
+
});
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Session ───────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
async function toggleConversation() {
|
|
202
|
+
if (currentSession) await stopConversation();
|
|
203
|
+
else await startConversation();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function startConversation() {
|
|
207
|
+
setMainBtn('Connecting...', false, true);
|
|
208
|
+
setAgentStatus('Connecting...', '');
|
|
209
|
+
subtitleEntries = [];
|
|
210
|
+
ephemeral = null;
|
|
211
|
+
renderedCount = 0;
|
|
212
|
+
if (ephemeralEl) { ephemeralEl.remove(); ephemeralEl = null; }
|
|
213
|
+
$('subtitles').innerHTML = '';
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const res = await fetch(SERVER + '/session/start', { method: 'POST' });
|
|
217
|
+
if (!res.ok) {
|
|
218
|
+
const err = await res.json().catch(() => ({}));
|
|
219
|
+
throw new Error(err.error || 'Failed to start');
|
|
220
|
+
}
|
|
221
|
+
currentSession = await res.json();
|
|
222
|
+
|
|
223
|
+
client = AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' });
|
|
224
|
+
|
|
225
|
+
// Agent audio
|
|
226
|
+
client.on('user-published', async (user, mediaType) => {
|
|
227
|
+
await client.subscribe(user, mediaType);
|
|
228
|
+
if (mediaType === 'audio') {
|
|
229
|
+
user.audioTrack.play();
|
|
230
|
+
setAgentRing('speaking');
|
|
231
|
+
setAgentStatus('Agent speaking', 'active');
|
|
232
|
+
showInterrupt(true);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
client.on('user-unpublished', (user, mediaType) => {
|
|
237
|
+
if (mediaType === 'audio') {
|
|
238
|
+
setAgentRing('listening');
|
|
239
|
+
setAgentStatus('Listening', 'active');
|
|
240
|
+
showInterrupt(false);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
client.on('user-left', () => {
|
|
245
|
+
setAgentRing('');
|
|
246
|
+
showInterrupt(false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await client.join(currentSession.appId, currentSession.channel, currentSession.token, currentSession.uid);
|
|
250
|
+
|
|
251
|
+
// ─── Real-time subtitles via DataStream ────────────────────────────
|
|
252
|
+
// User transcription comes through DataStream; agent transcription
|
|
253
|
+
// falls back to history API polling (DataStream doesn't carry it).
|
|
254
|
+
client.on('stream-message', (_uid, data) => {
|
|
255
|
+
try {
|
|
256
|
+
let text = data instanceof Uint8Array
|
|
257
|
+
? new TextDecoder().decode(data) : String(data);
|
|
258
|
+
|
|
259
|
+
// Direct JSON (v2)
|
|
260
|
+
try {
|
|
261
|
+
const msg = JSON.parse(text);
|
|
262
|
+
if (msg.object) { handleTranscript(msg); return; }
|
|
263
|
+
} catch {}
|
|
264
|
+
|
|
265
|
+
// Chunked: message_id|part_idx|part_sum|base64
|
|
266
|
+
if (text.includes('|')) {
|
|
267
|
+
const parts = text.split('|');
|
|
268
|
+
if (parts.length >= 4 && parseInt(parts[2], 10) === 1) {
|
|
269
|
+
try {
|
|
270
|
+
const msg = JSON.parse(atob(parts.slice(3).join('|')));
|
|
271
|
+
if (msg.object) handleTranscript(msg);
|
|
272
|
+
} catch {}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch {}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
localTrack = await AgoraRTC.createMicrophoneAudioTrack({ AEC: true, ANS: true, AGC: true });
|
|
279
|
+
await client.publish([localTrack]);
|
|
280
|
+
|
|
281
|
+
isMuted = false;
|
|
282
|
+
updateMicIcon();
|
|
283
|
+
$('btn-mic').disabled = false;
|
|
284
|
+
setMainBtn('End', true, false);
|
|
285
|
+
setAgentStatus('Listening', 'active');
|
|
286
|
+
setAgentRing('listening');
|
|
287
|
+
startFreqBars();
|
|
288
|
+
startHistoryFallback();
|
|
289
|
+
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error('Start failed:', err);
|
|
292
|
+
setAgentStatus(err.message, 'error');
|
|
293
|
+
setMainBtn('Start', false, false);
|
|
294
|
+
setAgentRing('');
|
|
295
|
+
currentSession = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function stopConversation() {
|
|
300
|
+
setMainBtn('Stopping...', true, true);
|
|
301
|
+
stopFreqBars();
|
|
302
|
+
stopHistoryFallback();
|
|
303
|
+
showInterrupt(false);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
if (localTrack) { localTrack.stop(); localTrack.close(); localTrack = null; }
|
|
307
|
+
if (client) { await client.leave(); client = null; }
|
|
308
|
+
await fetch(SERVER + '/session/stop', { method: 'POST' });
|
|
309
|
+
} catch (err) { console.error('Stop:', err); }
|
|
310
|
+
|
|
311
|
+
currentSession = null;
|
|
312
|
+
isMuted = false;
|
|
313
|
+
updateMicIcon();
|
|
314
|
+
$('btn-mic').disabled = true;
|
|
315
|
+
setMainBtn('Start', false, false);
|
|
316
|
+
setAgentStatus('Ready to talk', '');
|
|
317
|
+
setAgentRing('');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── History Polling Fallback ──────────────────────────────────────────────
|
|
321
|
+
// Agent transcription doesn't come through DataStream, so poll history API.
|
|
322
|
+
|
|
323
|
+
function startHistoryFallback() {
|
|
324
|
+
lastHistoryCount = 0;
|
|
325
|
+
historyTimer = setInterval(async () => {
|
|
326
|
+
if (!currentSession) return;
|
|
327
|
+
try {
|
|
328
|
+
const res = await fetch(SERVER + '/history?agentId=' + currentSession.agentId);
|
|
329
|
+
if (!res.ok) return;
|
|
330
|
+
const data = await res.json();
|
|
331
|
+
const entries = data.history || data.chat_history || data.messages || [];
|
|
332
|
+
|
|
333
|
+
for (let i = lastHistoryCount; i < entries.length; i++) {
|
|
334
|
+
const entry = entries[i];
|
|
335
|
+
const role = (entry.role === 'assistant' || entry.role === 'agent') ? 'assistant' : 'user';
|
|
336
|
+
const text = entry.content || entry.text || '';
|
|
337
|
+
if (!text) continue;
|
|
338
|
+
// Only add agent messages from history (user comes via DataStream)
|
|
339
|
+
if (role === 'assistant') {
|
|
340
|
+
const isDup = subtitleEntries.some(s => s.text === text && s.role === role);
|
|
341
|
+
if (!isDup) { subtitleEntries.push({ role, text }); renderSubtitles(); }
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
lastHistoryCount = entries.length;
|
|
345
|
+
} catch {}
|
|
346
|
+
}, 2000);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function stopHistoryFallback() {
|
|
350
|
+
if (historyTimer) { clearInterval(historyTimer); historyTimer = null; }
|
|
351
|
+
}
|
|
352
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ConvoAI Starter</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
|
|
11
|
+
<!-- Agent — always vertically centered -->
|
|
12
|
+
<div class="center-stage">
|
|
13
|
+
<div id="agent-ring" class="agent-ring">
|
|
14
|
+
<div class="agent-avatar">AI</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div id="freq-bars" class="freq-bars">
|
|
17
|
+
<span></span><span></span><span></span><span></span><span></span>
|
|
18
|
+
</div>
|
|
19
|
+
<div id="agent-status" class="agent-status">Ready to talk</div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Subtitles — bottom overlay, like real subtitles -->
|
|
23
|
+
<div id="subtitles" class="subtitles"></div>
|
|
24
|
+
|
|
25
|
+
<!-- Controls — fixed bottom -->
|
|
26
|
+
<div class="control-bar">
|
|
27
|
+
<button id="btn-mic" class="btn-circle" title="Mute" onclick="toggleMic()" disabled>
|
|
28
|
+
<svg id="mic-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
29
|
+
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
|
|
30
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
|
31
|
+
<line x1="12" y1="19" x2="12" y2="22"/>
|
|
32
|
+
</svg>
|
|
33
|
+
<svg id="mic-off-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none">
|
|
34
|
+
<line x1="2" y1="2" x2="22" y2="22"/>
|
|
35
|
+
<path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2"/>
|
|
36
|
+
<path d="M5 10v2a7 7 0 0 0 12 5"/>
|
|
37
|
+
<path d="M15 9.34V5a3 3 0 0 0-5.68-1.33"/>
|
|
38
|
+
<path d="M9 9v3a3 3 0 0 0 5.12 2.12"/>
|
|
39
|
+
<line x1="12" y1="19" x2="12" y2="22"/>
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
<button id="btn-start" class="btn-main" onclick="toggleConversation()">Start</button>
|
|
43
|
+
<button id="btn-interrupt" class="btn-circle" title="Interrupt" onclick="interruptAgent()" style="visibility:hidden">
|
|
44
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
|
45
|
+
<rect x="6" y="4" width="4" height="16" rx="1"/>
|
|
46
|
+
<rect x="14" y="4" width="4" height="16" rx="1"/>
|
|
47
|
+
</svg>
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<script src="/agora-sdk.js"></script>
|
|
52
|
+
<script src="/app.js"></script>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/* ConvoAI Starter — Dark Geek Theme
|
|
2
|
+
* Palette: Happy Hues #4
|
|
3
|
+
* Customize: change theme colors here */
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--bg: #16161a;
|
|
7
|
+
--surface: #242629;
|
|
8
|
+
--text: #fffffe;
|
|
9
|
+
--dim: #94a1b2;
|
|
10
|
+
--accent: #7f5af0;
|
|
11
|
+
--green: #2cb67d;
|
|
12
|
+
--gray: #72757e;
|
|
13
|
+
--red: #e74c3c;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
|
|
20
|
+
background: var(--bg);
|
|
21
|
+
color: var(--text);
|
|
22
|
+
height: 100vh;
|
|
23
|
+
width: 100vw;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
-webkit-font-smoothing: antialiased;
|
|
26
|
+
position: relative;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ── Center Stage — agent always centered ───────────────────────────────── */
|
|
30
|
+
|
|
31
|
+
.center-stage {
|
|
32
|
+
position: absolute;
|
|
33
|
+
top: 50%;
|
|
34
|
+
left: 50%;
|
|
35
|
+
transform: translate(-50%, -60%);
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 20px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Avatar ring */
|
|
43
|
+
|
|
44
|
+
.agent-ring {
|
|
45
|
+
width: 18vh;
|
|
46
|
+
height: 18vh;
|
|
47
|
+
min-width: 100px;
|
|
48
|
+
min-height: 100px;
|
|
49
|
+
max-width: 180px;
|
|
50
|
+
max-height: 180px;
|
|
51
|
+
border-radius: 50%;
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
border: 2px solid rgba(255, 255, 255, 0.06);
|
|
56
|
+
transition: border-color 0.5s ease, box-shadow 0.5s ease;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.agent-ring.listening {
|
|
60
|
+
border-color: rgba(127, 90, 240, 0.4);
|
|
61
|
+
box-shadow: 0 0 60px rgba(127, 90, 240, 0.1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.agent-ring.speaking {
|
|
65
|
+
border-color: rgba(44, 182, 125, 0.4);
|
|
66
|
+
box-shadow: 0 0 60px rgba(44, 182, 125, 0.1);
|
|
67
|
+
animation: breathe 3s ease-in-out infinite;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.agent-avatar {
|
|
71
|
+
width: calc(100% - 10px);
|
|
72
|
+
height: calc(100% - 10px);
|
|
73
|
+
border-radius: 50%;
|
|
74
|
+
background: var(--surface);
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
font-size: 3.5vh;
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
color: var(--dim);
|
|
81
|
+
letter-spacing: 2px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Frequency bars */
|
|
85
|
+
|
|
86
|
+
.freq-bars {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 4px;
|
|
90
|
+
height: 28px;
|
|
91
|
+
opacity: 0;
|
|
92
|
+
transition: opacity 0.3s ease;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.freq-bars.active { opacity: 1; }
|
|
96
|
+
|
|
97
|
+
.freq-bars span {
|
|
98
|
+
width: 4px;
|
|
99
|
+
min-height: 4px;
|
|
100
|
+
height: 4px;
|
|
101
|
+
background: var(--accent);
|
|
102
|
+
border-radius: 2px;
|
|
103
|
+
transition: height 0.08s ease-out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Status text */
|
|
107
|
+
|
|
108
|
+
.agent-status {
|
|
109
|
+
font-size: 14px;
|
|
110
|
+
color: var(--dim);
|
|
111
|
+
transition: color 0.3s ease;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.agent-status.active { color: var(--green); }
|
|
115
|
+
.agent-status.error { color: var(--red); }
|
|
116
|
+
|
|
117
|
+
/* ── Subtitles — bottom overlay ─────────────────────────────────────────── */
|
|
118
|
+
|
|
119
|
+
.subtitles {
|
|
120
|
+
position: fixed;
|
|
121
|
+
bottom: 100px;
|
|
122
|
+
left: 50%;
|
|
123
|
+
transform: translateX(-50%);
|
|
124
|
+
width: 90%;
|
|
125
|
+
max-width: 640px;
|
|
126
|
+
max-height: 35vh;
|
|
127
|
+
overflow-y: auto;
|
|
128
|
+
display: flex;
|
|
129
|
+
flex-direction: column;
|
|
130
|
+
gap: 6px;
|
|
131
|
+
padding: 8px 0;
|
|
132
|
+
scroll-behavior: smooth;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.sub-line {
|
|
136
|
+
padding: 8px 16px;
|
|
137
|
+
border-radius: 10px;
|
|
138
|
+
font-size: 14px;
|
|
139
|
+
line-height: 1.5;
|
|
140
|
+
animation: fadeUp 0.2s ease-out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.sub-line.user {
|
|
144
|
+
align-self: flex-end;
|
|
145
|
+
background: rgba(127, 90, 240, 0.1);
|
|
146
|
+
color: var(--text);
|
|
147
|
+
max-width: 80%;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.sub-line.agent {
|
|
151
|
+
align-self: flex-start;
|
|
152
|
+
background: rgba(255, 255, 255, 0.05);
|
|
153
|
+
color: var(--text);
|
|
154
|
+
max-width: 80%;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.sub-line.partial {
|
|
158
|
+
opacity: 0.5;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.sub-line .label {
|
|
162
|
+
font-size: 10px;
|
|
163
|
+
text-transform: uppercase;
|
|
164
|
+
letter-spacing: 1px;
|
|
165
|
+
color: var(--dim);
|
|
166
|
+
margin-bottom: 2px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ── Control Bar — fixed bottom ─────────────────────────────────────────── */
|
|
170
|
+
|
|
171
|
+
.control-bar {
|
|
172
|
+
position: fixed;
|
|
173
|
+
bottom: 0;
|
|
174
|
+
left: 0;
|
|
175
|
+
right: 0;
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: center;
|
|
179
|
+
gap: 16px;
|
|
180
|
+
padding: 20px 24px 32px;
|
|
181
|
+
background: linear-gradient(transparent, var(--bg) 40%);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.btn-main {
|
|
185
|
+
height: 52px;
|
|
186
|
+
min-width: 140px;
|
|
187
|
+
padding: 0 32px;
|
|
188
|
+
border: none;
|
|
189
|
+
border-radius: 26px;
|
|
190
|
+
font-size: 15px;
|
|
191
|
+
font-weight: 600;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
transition: all 0.2s ease;
|
|
194
|
+
background: var(--accent);
|
|
195
|
+
color: var(--text);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.btn-main:hover { filter: brightness(1.1); }
|
|
199
|
+
.btn-main:active { transform: scale(0.97); }
|
|
200
|
+
.btn-main:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
|
201
|
+
|
|
202
|
+
.btn-main.danger {
|
|
203
|
+
background: rgba(231, 76, 60, 0.12);
|
|
204
|
+
color: var(--red);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.btn-main.danger:hover { background: rgba(231, 76, 60, 0.2); }
|
|
208
|
+
|
|
209
|
+
.btn-circle {
|
|
210
|
+
width: 52px;
|
|
211
|
+
height: 52px;
|
|
212
|
+
border: none;
|
|
213
|
+
border-radius: 50%;
|
|
214
|
+
background: var(--surface);
|
|
215
|
+
color: var(--dim);
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
transition: all 0.2s ease;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.btn-circle:hover { background: #2e3035; color: var(--text); }
|
|
224
|
+
.btn-circle:disabled { opacity: 0.25; cursor: not-allowed; }
|
|
225
|
+
.btn-circle.muted { background: rgba(231, 76, 60, 0.12); color: var(--red); }
|
|
226
|
+
|
|
227
|
+
/* ── Scrollbar ──────────────────────────────────────────────────────────── */
|
|
228
|
+
|
|
229
|
+
.subtitles::-webkit-scrollbar { width: 0; }
|
|
230
|
+
|
|
231
|
+
/* ── Animations ─────────────────────────────────────────────────────────── */
|
|
232
|
+
|
|
233
|
+
@keyframes fadeUp {
|
|
234
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
235
|
+
to { opacity: 1; transform: translateY(0); }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@keyframes breathe {
|
|
239
|
+
0%, 100% { box-shadow: 0 0 60px rgba(44, 182, 125, 0.1); }
|
|
240
|
+
50% { box-shadow: 0 0 80px rgba(44, 182, 125, 0.2), 0 0 120px rgba(44, 182, 125, 0.06); }
|
|
241
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-convoai-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"convoai-starter": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsx server/index.ts",
|
|
9
|
+
"build": "tsc -p server/tsconfig.json",
|
|
10
|
+
"start": "node dist/server/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"agora-rtc-sdk-ng": "^4.24.0",
|
|
14
|
+
"agora-token": "^2.0.5",
|
|
15
|
+
"dotenv": "^16.4.0",
|
|
16
|
+
"express": "^4.21.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/express": "^5.0.0",
|
|
20
|
+
"tsx": "^4.19.0",
|
|
21
|
+
"typescript": "^5.6.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Agora Credentials (from https://console.agora.io)
|
|
2
|
+
AGORA_APP_ID=
|
|
3
|
+
AGORA_APP_CERTIFICATE=
|
|
4
|
+
AGORA_CUSTOMER_ID=
|
|
5
|
+
AGORA_CUSTOMER_SECRET=
|
|
6
|
+
|
|
7
|
+
# Region: "global" or "cn"
|
|
8
|
+
AGORA_REGION=global
|
|
9
|
+
|
|
10
|
+
# LLM Configuration
|
|
11
|
+
LLM_VENDOR=openai
|
|
12
|
+
LLM_MODEL=gpt-4o-mini
|
|
13
|
+
LLM_API_KEY=
|
|
14
|
+
LLM_URL=
|
|
15
|
+
LLM_STYLE=openai
|
|
16
|
+
|
|
17
|
+
# TTS Configuration
|
|
18
|
+
TTS_VENDOR=microsoft
|
|
19
|
+
TTS_API_KEY=
|
|
20
|
+
|
|
21
|
+
# ASR Configuration
|
|
22
|
+
ASR_VENDOR=ares
|
|
23
|
+
ASR_LANGUAGE=en-US
|