diff --git a/.env.example b/.env.example index f1e9ee3..38ea27c 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,10 @@ # rename me to .env and change below keys STREAM_KEY="live_XXXXXXXXXX" +DEEPSEEK_API_KEY= +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +GEMINI_API_KEY= +VITE_ELEVENLABS_API_KEY = +ELEVENLABS_API_KEY = +OPENROUTER_API_KEY= +VITE_WEBHOOK_URL= \ No newline at end of file diff --git a/ai_animation/experiment_log.md b/ai_animation/experiment_log.md index 95bc055..3d96212 100644 --- a/ai_animation/experiment_log.md +++ b/ai_animation/experiment_log.md @@ -3,3 +3,4 @@ | Problem | Attempted Solution | Real Outcome | Current $ Balance | | :----------------------------------------------------------- | :----------------- | :----------- | :---------------- | | 1. Relationships chart is blank.
2. Game stops after narrator summary in phase 2. | 1. Updated `PhaseSchema` in `types/gameState.ts` to include `agent_relationships` definition.
2. Uncommented `updateChatWindows(currentPhase, true);` in `phase.ts`. | | $0 | +| Add webhook notifications for phase changes | 1. Added `webhookUrl` config to `src/config.ts`
2. Created `src/webhooks/phaseNotifier.ts` with fire-and-forget webhook notification
3. Added `notifyPhaseChange()` call in `_setPhase()` function
4. Updated `.env.example` with `VITE_WEBHOOK_URL` | | $0 | diff --git a/ai_animation/src/config.ts b/ai_animation/src/config.ts index 0664143..ff56613 100644 --- a/ai_animation/src/config.ts +++ b/ai_animation/src/config.ts @@ -15,5 +15,8 @@ export const config = { soundEffectFrequency: 3, // Whether speech/TTS is enabled (can be toggled via debug menu) - speechEnabled: import.meta.env.VITE_DEBUG_MODE ? false : true + speechEnabled: import.meta.env.VITE_DEBUG_MODE ? false : true, + + // Webhook URL for phase change notifications (optional) + webhookUrl: import.meta.env.VITE_WEBHOOK_URL || '' } diff --git a/ai_animation/src/phase.ts b/ai_animation/src/phase.ts index 8721bf9..a39d257 100644 --- a/ai_animation/src/phase.ts +++ b/ai_animation/src/phase.ts @@ -10,6 +10,7 @@ import { config } from "./config"; import { debugMenuInstance } from "./debug/debugMenu"; import { showTwoPowerConversation, closeTwoPowerConversation } from "./components/twoPowerConversation"; import { PowerENUM } from "./types/map"; +import { notifyPhaseChange } from "./webhooks/phaseNotifier"; const MOMENT_THRESHOLD = 8.0 // If we're in debug mode, show it quick, otherwise show it for 30 seconds @@ -17,6 +18,11 @@ const MOMENT_DISPLAY_TIMEOUT_MS = config.isDebugMode ? 5000 : 30000 // FIXME: Going to previous phases is borked. Units do not animate properly, map doesn't update. export function _setPhase(phaseIndex: number) { + console.log(`[Phase] _setPhase called with index: ${phaseIndex}`); + + // Store the old phase index at the very beginning + const oldPhaseIndex = gameState.phaseIndex; + if (config.isDebugMode) { debugMenuInstance.updateTools() } @@ -58,6 +64,9 @@ export function _setPhase(phaseIndex: number) { // Finally, update the gameState with the current phaseIndex gameState.phaseIndex = phaseIndex + + // Send webhook notification for phase change + notifyPhaseChange(oldPhaseIndex, phaseIndex); } diff --git a/ai_animation/src/webhooks/phaseNotifier.ts b/ai_animation/src/webhooks/phaseNotifier.ts new file mode 100644 index 0000000..c5eb3fe --- /dev/null +++ b/ai_animation/src/webhooks/phaseNotifier.ts @@ -0,0 +1,140 @@ +import { config } from '../config'; +import { gameState } from '../gameState'; + +// Test webhook URL validity on startup +testWebhookUrl().catch(err => console.error("Webhook test failed:", err)); + +async function testWebhookUrl() { + const webhookUrl = config.webhookUrl || import.meta.env.VITE_WEBHOOK_URL || ''; + + if (!webhookUrl) { + console.log("⚠️ No webhook URL configured (optional feature)"); + return; + } + + try { + // For Discord webhooks, we can test with a GET request + if (webhookUrl.includes('discord.com/api/webhooks')) { + const response = await fetch(webhookUrl, { + method: 'GET', + }); + + if (response.ok) { + const webhookInfo = await response.json(); + console.log(`✅ Discord webhook is valid and ready (${webhookInfo.name || 'Unnamed webhook'})`); + } else if (response.status === 401) { + console.error(`❌ Discord webhook invalid: Unauthorized (check webhook URL)`); + } else { + console.error(`❌ Discord webhook error: ${response.status}`); + } + } else { + // For non-Discord webhooks, just validate the URL format + try { + new URL(webhookUrl); + console.log(`✅ Webhook URL is valid: ${webhookUrl.substring(0, 50)}...`); + } catch { + console.error(`❌ Invalid webhook URL format`); + } + } + } catch (error) { + console.error("❌ Webhook connection error:", error); + } +} + +/** + * Sends a webhook notification when a phase changes + * This is a fire-and-forget operation that won't block the UI + */ +export async function notifyPhaseChange(oldPhaseIndex: number, newPhaseIndex: number): Promise { + console.log(`[Webhook] Phase change detected: ${oldPhaseIndex} -> ${newPhaseIndex}`); + + // Skip if no webhook URL is configured + if (!config.webhookUrl) { + console.log('[Webhook] No webhook URL configured, skipping notification'); + return; + } + + // Skip if game data is not loaded + if (!gameState.gameData || !gameState.gameData.phases) { + console.warn('[Webhook] Game data not loaded, cannot send notification'); + return; + } + + const currentPhase = gameState.gameData.phases[newPhaseIndex]; + if (!currentPhase) { + console.warn(`[Webhook] Phase at index ${newPhaseIndex} not found`); + return; + } + + // Determine direction of phase change + let direction: 'forward' | 'backward' | 'jump'; + if (newPhaseIndex === oldPhaseIndex + 1) { + direction = 'forward'; + } else if (newPhaseIndex === oldPhaseIndex - 1) { + direction = 'backward'; + } else { + direction = 'jump'; + } + + const payload = { + event: 'phase_change', + timestamp: new Date().toISOString(), + game_id: gameState.gameId || 0, + phase_index: newPhaseIndex, + phase_name: currentPhase.name, + phase_year: currentPhase.year || parseInt(currentPhase.name.substring(1, 5)) || null, + is_playing: gameState.isPlaying, + direction: direction, + total_phases: gameState.gameData.phases.length + }; + + // Discord webhooks need a different format + const isDiscordWebhook = config.webhookUrl.includes('discord.com/api/webhooks'); + const webhookPayload = isDiscordWebhook ? { + content: `Phase Change: **${currentPhase.name}** (${direction})`, + embeds: [{ + title: "AI Diplomacy Phase Update", + color: direction === 'forward' ? 0x00ff00 : direction === 'backward' ? 0xff0000 : 0x0000ff, + fields: [ + { name: "Phase", value: currentPhase.name, inline: true }, + { name: "Year", value: String(payload.phase_year || "Unknown"), inline: true }, + { name: "Direction", value: direction, inline: true }, + { name: "Game ID", value: String(payload.game_id), inline: true }, + { name: "Phase Index", value: `${newPhaseIndex}/${payload.total_phases}`, inline: true }, + { name: "Auto-playing", value: payload.is_playing ? "Yes" : "No", inline: true } + ], + timestamp: payload.timestamp + }] + } : payload; + + console.log(`[Webhook] Sending notification for phase ${currentPhase.name} to ${config.webhookUrl}`); + + try { + // Fire and forget - we don't await this + fetch(config.webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(webhookPayload) + }) + .then(response => { + if (response.ok) { + console.log(`[Webhook] ✅ Successfully sent notification for phase ${currentPhase.name}`); + } else { + console.warn(`[Webhook] ❌ Failed with status ${response.status}: ${response.statusText}`); + } + }) + .catch(error => { + // Log errors but don't let them break the animation + console.error('[Webhook] ❌ Network error:', error); + }); + + if (config.isDebugMode) { + console.log('[Webhook] Debug - Full payload:', payload); + } + } catch (error) { + // Catch any synchronous errors (shouldn't happen with fetch) + console.error('[Webhook] ❌ Unexpected error:', error); + } +} \ No newline at end of file diff --git a/lm_game.py b/lm_game.py index 138cb5d..867da62 100644 --- a/lm_game.py +++ b/lm_game.py @@ -623,7 +623,6 @@ async def main(): try: current_year = int(current_year_str) consolidation_year = current_year - 2 # Two years ago - logger.info(f"[DIARY CONSOLIDATION] Current year: {current_year}, Consolidation year: {consolidation_year}") logger.info(f"[DIARY CONSOLIDATION] Phase check - ends with 'M': {current_short_phase.endswith('M')}, starts with 'S': {current_short_phase.startswith('S')}") logger.info(f"[DIARY CONSOLIDATION] Consolidation year check: {consolidation_year} >= 1901: {consolidation_year >= 1901}")