```
chore: upgrade Vite to v7 and update OpenAI services - Upgrade vite from ^5.0.0 to ^7.1.10 with new Node.js requirements - Add dotenv package for environment variable management - Update STT to use gpt-4o-transcribe model - Remove verbose_json format from STT response - Format code with consistent style (quotes, spacing) - Add HMR WebSocket configuration for development - Improve UI layout with overflow handling - Reorganize chat interface component order - Add user message broadcasting to activity log ```
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"@deepgram/sdk": "^3.4.0",
|
"@deepgram/sdk": "^3.4.0",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.0",
|
"@openrouter/ai-sdk-provider": "^1.2.0",
|
||||||
"ai": "^5.0.76",
|
"ai": "^5.0.76",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"openai": "^4.20.0",
|
"openai": "^4.20.0",
|
||||||
"tiny-invariant": "^1.3.3",
|
"tiny-invariant": "^1.3.3",
|
||||||
@@ -36,6 +37,6 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"tsx": "^4.6.0",
|
"tsx": "^4.6.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.0.0"
|
"vite": "^7.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ export async function streamLLM(params: StreamLLMParams): Promise<string> {
|
|||||||
|
|
||||||
let fullText = "";
|
let fullText = "";
|
||||||
|
|
||||||
|
result.fullStream;
|
||||||
|
|
||||||
// Stream text chunks to the caller
|
// Stream text chunks to the caller
|
||||||
for await (const chunk of result.textStream) {
|
for await (const chunk of result.textStream) {
|
||||||
fullText += chunk;
|
fullText += chunk;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { getSystemPrompt } from './system-prompt.js';
|
import { getSystemPrompt } from "./system-prompt.js";
|
||||||
import { streamLLM, type Message } from './llm-openai.js';
|
import { streamLLM, type Message } from "./llm-openai.js";
|
||||||
import type { VoiceAssistantWebSocketServer } from '../websocket-server.js';
|
import type { VoiceAssistantWebSocketServer } from "../websocket-server.js";
|
||||||
|
|
||||||
interface ConversationContext {
|
interface ConversationContext {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -23,7 +23,7 @@ export function createConversation(): string {
|
|||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
conversations.set(id, {
|
conversations.set(id, {
|
||||||
id,
|
id,
|
||||||
messages: [{ role: 'system', content: getSystemPrompt() }],
|
messages: [{ role: "system", content: getSystemPrompt() }],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
lastActivity: new Date(),
|
lastActivity: new Date(),
|
||||||
});
|
});
|
||||||
@@ -49,12 +49,12 @@ export async function processUserMessage(params: {
|
|||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const conversation = conversations.get(params.conversationId);
|
const conversation = conversations.get(params.conversationId);
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
throw new Error('Conversation not found');
|
throw new Error("Conversation not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user message to context
|
// Add user message to context
|
||||||
conversation.messages.push({
|
conversation.messages.push({
|
||||||
role: 'user',
|
role: "user",
|
||||||
content: params.message,
|
content: params.message,
|
||||||
});
|
});
|
||||||
conversation.lastActivity = new Date();
|
conversation.lastActivity = new Date();
|
||||||
@@ -62,7 +62,7 @@ export async function processUserMessage(params: {
|
|||||||
// Note: User message is already broadcast by the caller (e.g., after STT in index.ts)
|
// Note: User message is already broadcast by the caller (e.g., after STT in index.ts)
|
||||||
// No need to broadcast again here to avoid duplication
|
// No need to broadcast again here to avoid duplication
|
||||||
|
|
||||||
let assistantResponse = '';
|
let assistantResponse = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Stream LLM response with tool execution
|
// Stream LLM response with tool execution
|
||||||
@@ -72,7 +72,7 @@ export async function processUserMessage(params: {
|
|||||||
// Broadcast streaming chunks to WebSocket
|
// Broadcast streaming chunks to WebSocket
|
||||||
if (params.wsServer) {
|
if (params.wsServer) {
|
||||||
params.wsServer.broadcast({
|
params.wsServer.broadcast({
|
||||||
type: 'assistant_chunk',
|
type: "assistant_chunk",
|
||||||
payload: { chunk },
|
payload: { chunk },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ export async function processUserMessage(params: {
|
|||||||
params.wsServer.broadcastActivityLog({
|
params.wsServer.broadcastActivityLog({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
type: 'tool_call',
|
type: "tool_call",
|
||||||
content: `Calling ${toolName}`,
|
content: `Calling ${toolName}`,
|
||||||
metadata: { toolName, arguments: args },
|
metadata: { toolName, arguments: args },
|
||||||
});
|
});
|
||||||
@@ -95,7 +95,7 @@ export async function processUserMessage(params: {
|
|||||||
params.wsServer.broadcastActivityLog({
|
params.wsServer.broadcastActivityLog({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
type: 'tool_result',
|
type: "tool_result",
|
||||||
content: `Tool ${toolName} completed`,
|
content: `Tool ${toolName} completed`,
|
||||||
metadata: { toolName, result },
|
metadata: { toolName, result },
|
||||||
});
|
});
|
||||||
@@ -107,7 +107,7 @@ export async function processUserMessage(params: {
|
|||||||
params.wsServer.broadcastActivityLog({
|
params.wsServer.broadcastActivityLog({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
type: 'assistant',
|
type: "assistant",
|
||||||
content: fullText,
|
content: fullText,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -116,16 +116,16 @@ export async function processUserMessage(params: {
|
|||||||
|
|
||||||
// Add assistant response to context
|
// Add assistant response to context
|
||||||
conversation.messages.push({
|
conversation.messages.push({
|
||||||
role: 'assistant',
|
role: "assistant",
|
||||||
content: assistantResponse,
|
content: assistantResponse,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
// Broadcast error to WebSocket
|
// Broadcast error to WebSocket
|
||||||
if (params.wsServer) {
|
if (params.wsServer) {
|
||||||
params.wsServer.broadcastActivityLog({
|
params.wsServer.broadcastActivityLog({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
type: 'error',
|
type: "error",
|
||||||
content: `Error: ${error.message}`,
|
content: `Error: ${error.message}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,8 @@ export async function processUserMessage(params: {
|
|||||||
export function cleanupConversations(maxAgeMinutes: number = 60): void {
|
export function cleanupConversations(maxAgeMinutes: number = 60): void {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
for (const [id, conv] of conversations.entries()) {
|
for (const [id, conv] of conversations.entries()) {
|
||||||
const ageMinutes = (now.getTime() - conv.lastActivity.getTime()) / (1000 * 60);
|
const ageMinutes =
|
||||||
|
(now.getTime() - conv.lastActivity.getTime()) / (1000 * 60);
|
||||||
if (ageMinutes > maxAgeMinutes) {
|
if (ageMinutes > maxAgeMinutes) {
|
||||||
conversations.delete(id);
|
conversations.delete(id);
|
||||||
}
|
}
|
||||||
@@ -153,7 +154,11 @@ export function cleanupConversations(maxAgeMinutes: number = 60): void {
|
|||||||
*/
|
*/
|
||||||
export function getConversationStats(): {
|
export function getConversationStats(): {
|
||||||
total: number;
|
total: number;
|
||||||
conversations: Array<{ id: string; messageCount: number; lastActivity: Date }>;
|
conversations: Array<{
|
||||||
|
id: string;
|
||||||
|
messageCount: number;
|
||||||
|
lastActivity: Date;
|
||||||
|
}>;
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
total: conversations.size,
|
total: conversations.size,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from "openai";
|
||||||
import { writeFile, unlink } from 'fs/promises';
|
import { writeFile, unlink } from "fs/promises";
|
||||||
import { join } from 'path';
|
import { join } from "path";
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from "os";
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
export interface STTConfig {
|
export interface STTConfig {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model?: 'whisper-1';
|
model?: "whisper-1";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TranscriptionResult {
|
export interface TranscriptionResult {
|
||||||
@@ -23,7 +23,7 @@ export function initializeSTT(sttConfig: STTConfig): void {
|
|||||||
openaiClient = new OpenAI({
|
openaiClient = new OpenAI({
|
||||||
apiKey: sttConfig.apiKey,
|
apiKey: sttConfig.apiKey,
|
||||||
});
|
});
|
||||||
console.log('✓ STT (OpenAI Whisper) initialized');
|
console.log("✓ STT (OpenAI Whisper) initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function transcribeAudio(
|
export async function transcribeAudio(
|
||||||
@@ -31,7 +31,7 @@ export async function transcribeAudio(
|
|||||||
format: string
|
format: string
|
||||||
): Promise<TranscriptionResult> {
|
): Promise<TranscriptionResult> {
|
||||||
if (!openaiClient || !config) {
|
if (!openaiClient || !config) {
|
||||||
throw new Error('STT not initialized. Call initializeSTT() first.');
|
throw new Error("STT not initialized. Call initializeSTT() first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@@ -46,26 +46,29 @@ export async function transcribeAudio(
|
|||||||
tempFilePath = join(tmpdir(), `audio-${uuidv4()}.${ext}`);
|
tempFilePath = join(tmpdir(), `audio-${uuidv4()}.${ext}`);
|
||||||
await writeFile(tempFilePath, audioBuffer);
|
await writeFile(tempFilePath, audioBuffer);
|
||||||
|
|
||||||
console.log(`[STT] Transcribing audio file: ${tempFilePath} (${audioBuffer.length} bytes)`);
|
console.log(
|
||||||
|
`[STT] Transcribing audio file: ${tempFilePath} (${audioBuffer.length} bytes)`
|
||||||
|
);
|
||||||
|
|
||||||
// Call OpenAI Whisper API
|
// Call OpenAI Whisper API
|
||||||
const response = await openaiClient.audio.transcriptions.create({
|
const response = await openaiClient.audio.transcriptions.create({
|
||||||
file: await import('fs').then((fs) => fs.createReadStream(tempFilePath!)),
|
file: await import("fs").then((fs) => fs.createReadStream(tempFilePath!)),
|
||||||
model: config.model || 'whisper-1',
|
model: config.model || "gpt-4o-transcribe",
|
||||||
response_format: 'verbose_json', // Get language and duration info
|
response_format: "json", // Get language and duration info
|
||||||
});
|
});
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
console.log(`[STT] Transcription complete in ${duration}ms: "${response.text}"`);
|
console.log(
|
||||||
|
`[STT] Transcription complete in ${duration}ms: "${response.text}"`
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: response.text,
|
text: response.text,
|
||||||
language: response.language,
|
|
||||||
duration: duration,
|
duration: duration,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[STT] Transcription error:', error);
|
console.error("[STT] Transcription error:", error);
|
||||||
throw new Error(`STT transcription failed: ${error.message}`);
|
throw new Error(`STT transcription failed: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up temporary file
|
// Clean up temporary file
|
||||||
@@ -83,16 +86,16 @@ function getFileExtension(format: string): string {
|
|||||||
// Map mime types or format strings to file extensions
|
// Map mime types or format strings to file extensions
|
||||||
const formatLower = format.toLowerCase();
|
const formatLower = format.toLowerCase();
|
||||||
|
|
||||||
if (formatLower.includes('webm')) return 'webm';
|
if (formatLower.includes("webm")) return "webm";
|
||||||
if (formatLower.includes('ogg')) return 'ogg';
|
if (formatLower.includes("ogg")) return "ogg";
|
||||||
if (formatLower.includes('mp3')) return 'mp3';
|
if (formatLower.includes("mp3")) return "mp3";
|
||||||
if (formatLower.includes('wav')) return 'wav';
|
if (formatLower.includes("wav")) return "wav";
|
||||||
if (formatLower.includes('m4a')) return 'm4a';
|
if (formatLower.includes("m4a")) return "m4a";
|
||||||
if (formatLower.includes('mp4')) return 'mp4';
|
if (formatLower.includes("mp4")) return "mp4";
|
||||||
if (formatLower.includes('flac')) return 'flac';
|
if (formatLower.includes("flac")) return "flac";
|
||||||
|
|
||||||
// Default to webm (common browser format)
|
// Default to webm (common browser format)
|
||||||
return 'webm';
|
return "webm";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSTTInitialized(): boolean {
|
export function isSTTInitialized(): boolean {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from "openai";
|
||||||
import { Readable } from 'stream';
|
import { Readable } from "stream";
|
||||||
|
|
||||||
export interface TTSConfig {
|
export interface TTSConfig {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
model?: 'tts-1' | 'tts-1-hd';
|
model?: "tts-1" | "tts-1-hd";
|
||||||
voice?: 'alloy' | 'echo' | 'fable' | 'onyx' | 'nova' | 'shimmer';
|
voice?: "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer";
|
||||||
responseFormat?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm';
|
responseFormat?: "mp3" | "opus" | "aac" | "flac" | "wav" | "pcm";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpeechResult {
|
export interface SpeechResult {
|
||||||
@@ -18,51 +18,61 @@ let config: TTSConfig | null = null;
|
|||||||
|
|
||||||
export function initializeTTS(ttsConfig: TTSConfig): void {
|
export function initializeTTS(ttsConfig: TTSConfig): void {
|
||||||
config = {
|
config = {
|
||||||
model: 'tts-1',
|
model: "tts-1",
|
||||||
voice: 'alloy',
|
voice: "alloy",
|
||||||
responseFormat: 'mp3',
|
responseFormat: "mp3",
|
||||||
...ttsConfig,
|
...ttsConfig,
|
||||||
};
|
};
|
||||||
openaiClient = new OpenAI({
|
openaiClient = new OpenAI({
|
||||||
apiKey: ttsConfig.apiKey,
|
apiKey: ttsConfig.apiKey,
|
||||||
});
|
});
|
||||||
console.log(`✓ TTS (OpenAI) initialized with voice: ${config.voice}, model: ${config.model}`);
|
console.log(
|
||||||
|
`✓ TTS (OpenAI) initialized with voice: ${config.voice}, model: ${config.model}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function synthesizeSpeech(text: string): Promise<SpeechResult> {
|
export async function synthesizeSpeech(text: string): Promise<SpeechResult> {
|
||||||
if (!openaiClient || !config) {
|
if (!openaiClient || !config) {
|
||||||
throw new Error('TTS not initialized. Call initializeTTS() first.');
|
throw new Error("TTS not initialized. Call initializeTTS() first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!text || text.trim().length === 0) {
|
if (!text || text.trim().length === 0) {
|
||||||
throw new Error('Cannot synthesize empty text');
|
throw new Error("Cannot synthesize empty text");
|
||||||
}
|
}
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[TTS] Synthesizing speech: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
|
console.log(
|
||||||
|
`[TTS] Synthesizing speech: "${text.substring(0, 50)}${
|
||||||
|
text.length > 50 ? "..." : ""
|
||||||
|
}"`
|
||||||
|
);
|
||||||
|
|
||||||
// Call OpenAI TTS API with streaming
|
// Call OpenAI TTS API with streaming
|
||||||
const response = await openaiClient.audio.speech.create({
|
const response = await openaiClient.audio.speech.create({
|
||||||
model: config.model!,
|
model: config.model!,
|
||||||
voice: config.voice!,
|
voice: config.voice!,
|
||||||
input: text,
|
input: text,
|
||||||
response_format: config.responseFormat as 'mp3' | 'opus' | 'aac' | 'flac',
|
response_format: config.responseFormat as "mp3" | "opus" | "aac" | "flac",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Response is a stream, convert to buffer
|
// Response is a stream, convert to buffer
|
||||||
const audioBuffer = await streamToBuffer(response.body as unknown as Readable);
|
const audioBuffer = await streamToBuffer(
|
||||||
|
response.body as unknown as Readable
|
||||||
|
);
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
console.log(`[TTS] Speech synthesis complete in ${duration}ms (${audioBuffer.length} bytes)`);
|
console.log(
|
||||||
|
`[TTS] Speech synthesis complete in ${duration}ms (${audioBuffer.length} bytes)`
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
audio: audioBuffer,
|
audio: audioBuffer,
|
||||||
format: config.responseFormat || 'mp3',
|
format: config.responseFormat || "mp3",
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[TTS] Speech synthesis error:', error);
|
console.error("[TTS] Speech synthesis error:", error);
|
||||||
throw new Error(`TTS synthesis failed: ${error.message}`);
|
throw new Error(`TTS synthesis failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,15 +81,15 @@ async function streamToBuffer(stream: Readable): Promise<Buffer> {
|
|||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
stream.on('data', (chunk) => {
|
stream.on("data", (chunk) => {
|
||||||
chunks.push(Buffer.from(chunk));
|
chunks.push(Buffer.from(chunk));
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('end', () => {
|
stream.on("end", () => {
|
||||||
resolve(Buffer.concat(chunks));
|
resolve(Buffer.concat(chunks));
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('error', (error) => {
|
stream.on("error", (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import "dotenv/config";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
@@ -20,7 +21,6 @@ import {
|
|||||||
captureTerminal,
|
captureTerminal,
|
||||||
killTerminal,
|
killTerminal,
|
||||||
} from "./daemon/terminal-manager.js";
|
} from "./daemon/terminal-manager.js";
|
||||||
import { initializeOpenAI } from "./agent/llm-openai.js";
|
|
||||||
import { initializeSTT, transcribeAudio } from "./agent/stt-openai.js";
|
import { initializeSTT, transcribeAudio } from "./agent/stt-openai.js";
|
||||||
import { initializeTTS, synthesizeSpeech } from "./agent/tts-openai.js";
|
import { initializeTTS, synthesizeSpeech } from "./agent/tts-openai.js";
|
||||||
import {
|
import {
|
||||||
@@ -137,8 +137,12 @@ async function createServer(httpServer: HttpServer, config: ServerConfig) {
|
|||||||
server: {
|
server: {
|
||||||
middlewareMode: true,
|
middlewareMode: true,
|
||||||
allowedHosts: ["localhost", "mohameds-macbook-pro.tail8fe838.ts.net"],
|
allowedHosts: ["localhost", "mohameds-macbook-pro.tail8fe838.ts.net"],
|
||||||
port: 3000,
|
hmr: {
|
||||||
hmr: false,
|
server: httpServer,
|
||||||
|
port: config.port,
|
||||||
|
protocol: "wss",
|
||||||
|
path: "/hmr",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
appType: "custom",
|
appType: "custom",
|
||||||
});
|
});
|
||||||
@@ -201,7 +205,6 @@ async function main() {
|
|||||||
// Initialize OpenAI client
|
// Initialize OpenAI client
|
||||||
const apiKey = process.env.OPENAI_API_KEY;
|
const apiKey = process.env.OPENAI_API_KEY;
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
initializeOpenAI(apiKey);
|
|
||||||
console.log("✓ OpenAI client initialized");
|
console.log("✓ OpenAI client initialized");
|
||||||
|
|
||||||
// Initialize STT (Whisper)
|
// Initialize STT (Whisper)
|
||||||
@@ -282,6 +285,14 @@ async function main() {
|
|||||||
// Wire orchestrator to WebSocket for text messages
|
// Wire orchestrator to WebSocket for text messages
|
||||||
wsServer.setMessageHandler(async (message: string) => {
|
wsServer.setMessageHandler(async (message: string) => {
|
||||||
try {
|
try {
|
||||||
|
// Broadcast user's text message as activity log
|
||||||
|
wsServer.broadcastActivityLog({
|
||||||
|
id: uuidv4(),
|
||||||
|
timestamp: new Date(),
|
||||||
|
type: "transcript",
|
||||||
|
content: message,
|
||||||
|
});
|
||||||
|
|
||||||
await processUserMessage({
|
await processUserMessage({
|
||||||
conversationId: defaultConversationId,
|
conversationId: defaultConversationId,
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -55,8 +55,9 @@
|
|||||||
.main {
|
.main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 2rem;
|
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-interface {
|
.chat-interface {
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
max-height: calc(100vh - 120px);
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-interface h2 {
|
.chat-interface h2 {
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ function App() {
|
|||||||
message: string,
|
message: string,
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
) => {
|
) => {
|
||||||
console.trace("addLog", type, message, metadata);
|
|
||||||
setLogs((prev) => [
|
setLogs((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -231,14 +230,6 @@ function App() {
|
|||||||
|
|
||||||
<main className="main">
|
<main className="main">
|
||||||
<div className="chat-interface">
|
<div className="chat-interface">
|
||||||
<h2>Chat with Assistant</h2>
|
|
||||||
|
|
||||||
<VoiceControls
|
|
||||||
onAudioRecorded={handleAudioRecorded}
|
|
||||||
isProcessing={isProcessingAudio}
|
|
||||||
isPlaying={isPlayingAudio}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="activity-log">
|
<div className="activity-log">
|
||||||
<div className="log-entries">
|
<div className="log-entries">
|
||||||
{logs.map((log) => (
|
{logs.map((log) => (
|
||||||
@@ -286,15 +277,11 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="test-controls">
|
<VoiceControls
|
||||||
<button
|
onAudioRecorded={handleAudioRecorded}
|
||||||
onClick={handlePing}
|
isProcessing={isProcessingAudio}
|
||||||
disabled={!ws.isConnected}
|
isPlaying={isPlayingAudio}
|
||||||
className="ping-button"
|
/>
|
||||||
>
|
|
||||||
Send Ping (Test)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user