diff --git a/ai_animation/src/config.ts b/ai_animation/src/config.ts index 3ce42cd..b8125b7 100644 --- a/ai_animation/src/config.ts +++ b/ai_animation/src/config.ts @@ -6,7 +6,7 @@ export const config = { playbackSpeed: 500, // Whether to enable debug mode (faster animations, more console logging) - isDebugMode: true, + isDebugMode: false, // Duration of unit movement animation in ms animationDuration: 1500, diff --git a/ai_animation/src/logger.ts b/ai_animation/src/logger.ts index 0c06810..faf2873 100644 --- a/ai_animation/src/logger.ts +++ b/ai_animation/src/logger.ts @@ -9,14 +9,16 @@ class Logger { } return _panel } + + // Modified to only log to console without updating the info panel log = (msg: string) => { if (typeof msg !== "string") { throw new Error(`Logger messages must be strings, you passed a ${typeof msg}`) } - this.infoPanel.textContent = msg; - + // Remove the update to infoPanel.textContent console.log(msg) } + // Updated function to update info panel with useful information and smooth transitions updateInfoPanel = () => { const totalPhases = gameState.gameData?.phases?.length || 0; diff --git a/ai_animation/src/main.ts b/ai_animation/src/main.ts index d43c28d..f472151 100644 --- a/ai_animation/src/main.ts +++ b/ai_animation/src/main.ts @@ -38,6 +38,9 @@ function initScene() { // Load coordinate data, then build the map gameState.loadBoardState().then(() => { initMap(gameState.scene).then(() => { + // Update info panel with initial power information + logger.updateInfoPanel(); + // Load default game file if in debug mode if (isDebugMode) { loadDefaultGameFile(); @@ -45,7 +48,8 @@ function initScene() { }) }).catch(err => { console.error("Error loading coordinates:", err); - logger.log(`Error loading coords: ${err.message}`) + // Use console.error instead of logger.log to avoid updating the info panel + console.error(`Error loading coords: ${err.message}`); }); // Handle resizing @@ -55,7 +59,6 @@ function initScene() { // Initialize info panel logger.updateInfoPanel(); - } // --- ANIMATION LOOP --- @@ -78,8 +81,12 @@ function animate() { ); // If messages are done playing but we haven't started unit animations yet + // AND we're not currently speaking, create animations if (!gameState.messagesPlaying && !gameState.isSpeaking && gameState.unitAnimations.length === 0 && gameState.isPlaying) { + + console.log("Animation check: messages done, not speaking, no animations running"); + if (gameState.gameData && gameState.gameData.phases) { // Get previous phase index const prevIndex = gameState.phaseIndex > 0 ? @@ -197,10 +204,11 @@ function loadDefaultGameFile() { }) .catch(error => { console.error("Error loading default game file:", error); - logger.log(`Error loading default game: ${error.message}`); + // Use console.error instead of logger.log to avoid updating the info panel + console.error(`Error loading default game: ${error.message}`); - // Fallback - tell user to drag & drop a file - logger.log('Please load a game file using the "Load Game" button.'); + // Fallback - tell user to drag & drop a file but don't update the info panel + console.log('Please load a game file using the "Load Game" button.'); }); } diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index 0d6b688..ffd434d 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -85,9 +85,11 @@ export function displayPhase(index, skipMessages = false) { const phaseBannerText = `Phase: ${currentPhase.name}`; addToNewsBanner(phaseBannerText); - // Update info panel with current phase details + // Log phase details to console only, don't update info panel with this const phaseInfo = `Phase: ${currentPhase.name}\nSCs: ${currentPhase.state?.centers ? JSON.stringify(currentPhase.state.centers) : 'None'}\nUnits: ${currentPhase.state?.units ? JSON.stringify(currentPhase.state.units) : 'None'}`; - logger.log(phaseInfo); + console.log(phaseInfo); // Use console.log instead of logger.log + + // Update info panel with power information logger.updateInfoPanel(); // Show messages with animation or immediately based on skipMessages flag @@ -129,6 +131,8 @@ export function displayPhaseWithAnimation(index) { * Handles speaking summaries and transitioning to the next phase */ export function advanceToNextPhase() { + console.log("advanceToNextPhase called"); + if (!gameState.gameData || !gameState.gameData.phases || gameState.phaseIndex < 0) { logger.log("Cannot advance phase: invalid game state"); return; @@ -140,6 +144,11 @@ export function advanceToNextPhase() { // Get current phase const currentPhase = gameState.gameData.phases[gameState.phaseIndex]; + console.log(`Current phase: ${currentPhase.name}, Has summary: ${Boolean(currentPhase.summary)}`); + if (currentPhase.summary) { + console.log(`Summary preview: "${currentPhase.summary.substring(0, 50)}..."`); + } + if (config.isDebugMode) { console.log(`Processing phase transition for ${currentPhase.name}`); } @@ -148,20 +157,24 @@ export function advanceToNextPhase() { if (currentPhase.summary && currentPhase.summary.trim() !== '') { // Update the news banner with full summary addToNewsBanner(`(${currentPhase.name}) ${currentPhase.summary}`); + console.log("Added summary to news banner, preparing to call speakSummary"); // Speak the summary and advance after speakSummary(currentPhase.summary) .then(() => { + console.log("Speech completed successfully"); if (gameState.isPlaying) { moveToNextPhase(); } }) - .catch(() => { + .catch((error) => { + console.error("Speech failed with error:", error); if (gameState.isPlaying) { moveToNextPhase(); } }); } else { + console.log("No summary available, skipping speech"); // No summary to speak, advance immediately moveToNextPhase(); } diff --git a/ai_animation/src/speech.ts b/ai_animation/src/speech.ts index 981b35f..4db2cbe 100644 --- a/ai_animation/src/speech.ts +++ b/ai_animation/src/speech.ts @@ -1,10 +1,62 @@ import { gameState } from "./gameState"; // --- ElevenLabs Text-to-Speech configuration --- -const ELEVENLABS_API_KEY = import.meta.env.VITE_ELEVENLABS_API_KEY || ""; +let ELEVENLABS_API_KEY = ''; + +// Try to load from import.meta.env +try { + // First check if we have the Vite-specific variable + if (import.meta.env.VITE_ELEVENLABS_API_KEY) { + ELEVENLABS_API_KEY = String(import.meta.env.VITE_ELEVENLABS_API_KEY).trim(); + // Simplified logging + } + // Fallback to the direct env variable (for dev environments) + else if (import.meta.env.ELEVENLABS_API_KEY) { + ELEVENLABS_API_KEY = String(import.meta.env.ELEVENLABS_API_KEY).trim(); + } + + + // Clean and validate the key + if (ELEVENLABS_API_KEY) { + // Remove any unexpected characters that might have been added + ELEVENLABS_API_KEY = ELEVENLABS_API_KEY.replace(/[^a-zA-Z0-9_\-]/g, ''); + console.log(`ElevenLabs API key: ${ELEVENLABS_API_KEY ? 'Valid' : 'Invalid'} (${ELEVENLABS_API_KEY.length} chars)`); + } +} catch (err) { + console.error('Error loading API key:', err); +} + const VOICE_ID = "onwK4e9ZLuTAKqWW03F9"; const MODEL_ID = "eleven_multilingual_v2"; +// Test the API key validity directly but don't log unless there's an issue +testElevenLabsKey().catch(err => console.error("Key test failed:", err)); + +async function testElevenLabsKey() { + if (!ELEVENLABS_API_KEY) { + console.warn("Cannot test API key - none provided"); + return; + } + + try { + const response = await fetch('https://api.elevenlabs.io/v1/voices', { + method: 'GET', + headers: { + 'xi-api-key': ELEVENLABS_API_KEY + } + }); + + if (response.ok) { + console.log("✅ ElevenLabs API key is valid and ready for TTS"); + } else { + const errorText = await response.text().catch(() => 'No error details available'); + console.error(`❌ ElevenLabs API key invalid: ${response.status}`); + } + } catch (error) { + console.error("❌ ElevenLabs API connection error"); + } +} + /** * Call ElevenLabs TTS to speak the summary out loud. * Returns a promise that resolves only after the audio finishes playing (or fails). @@ -13,6 +65,27 @@ const MODEL_ID = "eleven_multilingual_v2"; * @returns Promise that resolves when audio completes or rejects on error */ export async function speakSummary(summaryText: string): Promise { + if (!summaryText || summaryText.trim() === '') { + console.warn("No summary text provided to speakSummary function"); + return; + } + + // Check if the summary is in JSON format and extract the actual summary text + let textToSpeak = summaryText; + try { + // Check if it starts with a JSON format indicator + if (summaryText.trim().startsWith('{') && summaryText.includes('"summary"')) { + const parsedSummary = JSON.parse(summaryText); + if (parsedSummary.summary) { + textToSpeak = parsedSummary.summary; + // clean text, drop /n + textToSpeak = textToSpeak.replace(/\n/g, ' '); + } + } + } catch (error) { + console.warn("Failed to parse summary as JSON"); + } + if (!ELEVENLABS_API_KEY) { console.warn("No ElevenLabs API key found. Skipping TTS."); return; @@ -23,29 +96,26 @@ export async function speakSummary(summaryText: string): Promise { try { // Truncate text to first 100 characters for ElevenLabs - const truncatedText = summaryText.substring(0, 100); - if (truncatedText.length < summaryText.length) { - console.log(`TTS text truncated from ${summaryText.length} to 100 characters`); - } - + const truncatedText = textToSpeak.substring(0, 100); + // Hit ElevenLabs TTS endpoint with the truncated text + const headers = { + "xi-api-key": ELEVENLABS_API_KEY, + "Content-Type": "application/json", + "Accept": "audio/mpeg" + }; + const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`, { method: "POST", - headers: { - "xi-api-key": ELEVENLABS_API_KEY, - "Content-Type": "application/json", - "Accept": "audio/mpeg" - }, + headers: headers, body: JSON.stringify({ text: truncatedText, model_id: MODEL_ID, - // Optional fine-tuning parameters - // voice_settings: { stability: 0.3, similarity_boost: 0.8 }, }) }); if (!response.ok) { - throw new Error(`ElevenLabs TTS error: ${response.statusText}`); + throw new Error(`ElevenLabs TTS error: ${response.status}`); } // Convert response into a playable blob @@ -62,7 +132,7 @@ export async function speakSummary(summaryText: string): Promise { resolve(); }; }).catch(err => { - console.error("Audio playback error", err); + console.error("Audio playback error"); // Make sure to clear the flag even if there's an error gameState.isSpeaking = false; reject(err); @@ -70,7 +140,7 @@ export async function speakSummary(summaryText: string): Promise { }); } catch (err) { - console.error("Failed to generate TTS from ElevenLabs:", err); + console.error("Failed to generate TTS from ElevenLabs"); // Make sure to clear the flag if there's any exception gameState.isSpeaking = false; } diff --git a/ai_animation/src/types/gameState.ts b/ai_animation/src/types/gameState.ts index 0398faf..98a126b 100644 --- a/ai_animation/src/types/gameState.ts +++ b/ai_animation/src/types/gameState.ts @@ -14,6 +14,7 @@ const PhaseSchema = z.object({ centers: z.record(PowerENUMSchema, z.array(ProvinceENUMSchema)) }), year: z.number().optional(), + summary: z.string().optional(), }); export const GameSchema = z.object({ diff --git a/ai_animation/vite.config.js b/ai_animation/vite.config.js index 3cdc087..32355d9 100644 --- a/ai_animation/vite.config.js +++ b/ai_animation/vite.config.js @@ -1,9 +1,28 @@ /** @type {import('vite').UserConfig} */ -export default { - "preview": { - "allowedHosts": ["diplomacy"] - }, - "dev": { - "allowedHosts": ["diplomacy"] - } -} +import { defineConfig, loadEnv } from 'vite'; + +export default defineConfig(({ mode }) => { + // Load environment variables + const env = loadEnv(mode, process.cwd(), ''); + + console.log('Environment mode:', mode); + console.log('Environment variables loaded:', Object.keys(env).filter(key => key.startsWith('VITE_'))); + + return { + // Define environment variables that should be available in the client + define: { + // Expose all VITE_ prefixed environment variables to the client + ...Object.keys(env).filter(key => key.startsWith('VITE_')).reduce((acc, key) => { + acc[`import.meta.env.${key}`] = JSON.stringify(env[key]); + return acc; + }, {}) + }, + // Server configuration + "preview": { + "allowedHosts": ["diplomacy"] + }, + "dev": { + "allowedHosts": ["diplomacy"] + } + }; +});