import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Modal } from '../Modal'; import { AdminConfig, NFT, GameCharacter, WeaponType, DecorativeFishType, BubbleSize, SavedReply } from '../../types'; import { generateImageWithGemini, GeminiImageResponse } from '../../geminiApi'; import { SoundEvent } from '../../sounds'; import { COOLDOWN_RED_ARROW_DEFENSE, RED_ARROW_PROJECTILE_SPEED, RED_ARROW_PROJECTILE_DAMAGE, PLAYER_SPEED, RED_ARROW_DEFAULT_SHOT_COUNT, RED_ARROW_PROJECTILE_IMAGE_URL, DOGE_WHALE_ESCAPE_PLAYER_IMAGE_URL, DOGE_WHALE_ESCAPE_WALL_IMAGE_URL, DOGE_WHALE_ESCAPE_GOAL_IMAGE_URL, DOGE_WHALE_ESCAPE_MINE_IMAGE_URL, DOGE_WHALE_ESCAPE_BOMB_IMAGE_URL, DOGE_WHALE_ESCAPE_JELLYFISH_IMAGE_URL, DOGE_WHALE_ESCAPE_CROCODILE_IMAGE_URL, DOGE_WHALE_ESCAPE_ALGAE_IMAGE_URL, DOGE_WHALE_ESCAPE_CURRENT_N_IMAGE_URL, DOGE_WHALE_ESCAPE_CURRENT_S_IMAGE_URL, DOGE_WHALE_ESCAPE_CURRENT_E_IMAGE_URL, DOGE_WHALE_ESCAPE_CURRENT_W_IMAGE_URL, DOGE_WHALE_ESCAPE_SNAKE_IMAGE_URL, DOGE_WHALE_ESCAPE_LIFEUP_IMAGE_URL } from '../../constants'; import { GoogleGenAI, GenerateContentResponse } from "@google/genai"; import { generateId } from '../../utils'; interface AdminPanelModalProps { isOpen: boolean; onClose: () => void; adminConfig: AdminConfig; runtimeNftDefinitions: Record; runtimeGameCharacters: Record; onUpdateAdminConfig: (newConfig: Partial) => void; onUpdateNftImage: (nftId: string, newImageUrl: string) => void; onUpdateCharacterAttributes: (characterId: string, updates: Partial) => void; showNotification: (message: string, type?: 'success' | 'error' | 'info' | 'warning' | 'special') => void; isMaximizable?: boolean; isMaximized?: boolean; onToggleMaximize?: () => void; onLogout: () => void; // New prop for logout } type AdminTab = 'aiGenerator' | 'nfts' | 'characters' | 'staking' | 'assets' | 'gameplay' | 'sounds' | 'effectsVisuals' | 'aiChat' | 'knowledgeBase' | 'dogeEscapeAssets'; type SoundUrlKey = Extract; type AdminConfigImageKey = Extract; const convertFileToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result as string); reader.onerror = error => reject(error); }); }; interface CharacterAttributeInputs { name: string; imageURL: string; imageBase64?: string | null; defaultWeaponType?: WeaponType; autoFireHealthThreshold?: number; baseHealth?: number; baseSpeed?: number; } const formatWeaponTypeName = (enumKey: string) => { return enumKey.replace(/([A-Z])/g, ' $1').trim().replace(/_/g, ' '); }; const formatSoundEventName = (event: SoundEvent): string => { return event .split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); }; interface AIChatMessage { id: string; sender: 'user' | 'ai' | 'error'; text: string; lang?: string; // Language of the user's query } const languageOptions = [ { name: 'English', code: 'en' }, { name: 'Spanish', code: 'es' }, { name: 'French', code: 'fr' }, { name: 'German', code: 'de' }, { name: 'Japanese', code: 'ja' }, { name: 'Chinese', code: 'zh' }, { name: 'Korean', code: 'ko' }, ]; const dogeEscapeAssetConfigKeys: Array<{key: AdminConfigImageKey, displayName: string, defaultUrl: string}> = [ { key: 'dogeEscapePlayerImageUrl', displayName: 'Doge Escape Player', defaultUrl: DOGE_WHALE_ESCAPE_PLAYER_IMAGE_URL }, { key: 'dogeEscapeWallImageUrl', displayName: 'Doge Escape Wall', defaultUrl: DOGE_WHALE_ESCAPE_WALL_IMAGE_URL }, { key: 'dogeEscapeGoalImageUrl', displayName: 'Doge Escape Goal', defaultUrl: DOGE_WHALE_ESCAPE_GOAL_IMAGE_URL }, { key: 'dogeEscapeMineImageUrl', displayName: 'Doge Escape Mine', defaultUrl: DOGE_WHALE_ESCAPE_MINE_IMAGE_URL }, { key: 'dogeEscapeBombImageUrl', displayName: 'Doge Escape Bomb', defaultUrl: DOGE_WHALE_ESCAPE_BOMB_IMAGE_URL }, { key: 'dogeEscapeJellyfishImageUrl', displayName: 'Doge Escape Jellyfish', defaultUrl: DOGE_WHALE_ESCAPE_JELLYFISH_IMAGE_URL }, { key: 'dogeEscapeCrocodileImageUrl', displayName: 'Doge Escape Crocodile', defaultUrl: DOGE_WHALE_ESCAPE_CROCODILE_IMAGE_URL }, { key: 'dogeEscapeAlgaeImageUrl', displayName: 'Doge Escape Toxic Algae', defaultUrl: DOGE_WHALE_ESCAPE_ALGAE_IMAGE_URL }, { key: 'dogeEscapeCurrentNImageUrl', displayName: 'Doge Escape Current (North)', defaultUrl: DOGE_WHALE_ESCAPE_CURRENT_N_IMAGE_URL }, { key: 'dogeEscapeCurrentSImageUrl', displayName: 'Doge Escape Current (South)', defaultUrl: DOGE_WHALE_ESCAPE_CURRENT_S_IMAGE_URL }, { key: 'dogeEscapeCurrentEImageUrl', displayName: 'Doge Escape Current (East)', defaultUrl: DOGE_WHALE_ESCAPE_CURRENT_E_IMAGE_URL }, { key: 'dogeEscapeCurrentWImageUrl', displayName: 'Doge Escape Current (West)', defaultUrl: DOGE_WHALE_ESCAPE_CURRENT_W_IMAGE_URL }, { key: 'dogeEscapeSnakeImageUrl', displayName: 'Doge Escape Snake', defaultUrl: DOGE_WHALE_ESCAPE_SNAKE_IMAGE_URL }, { key: 'dogeEscapeLifeUpImageUrl', displayName: 'Doge Escape Life-Up', defaultUrl: DOGE_WHALE_ESCAPE_LIFEUP_IMAGE_URL }, ]; export const AdminPanelModal: React.FC = ({ isOpen, onClose, adminConfig, runtimeNftDefinitions, runtimeGameCharacters, onUpdateAdminConfig, onUpdateNftImage, onUpdateCharacterAttributes, showNotification, isMaximizable, isMaximized, onToggleMaximize, onLogout, // Destructure new prop }) => { const [activeTab, setActiveTab] = useState('aiChat'); const [localAdminConfig, setLocalAdminConfig] = useState(adminConfig); const [nftImageInputs, setNftImageInputs] = useState>({}); const [pendingNftImageChanges, setPendingNftImageChanges] = useState>({}); const [characterAttributeInputs, setCharacterAttributeInputs] = useState>({}); const [aiPrompts, setAiPrompts] = useState>({}); const [aiGeneratedImagePreviews, setAiGeneratedImagePreviews] = useState>({}); const [isGenerating, setIsGenerating] = useState>({}); const [superAiAgentEnabled, setSuperAiAgentEnabled] = useState(true); const [aiSuggestionDimensions, setAiSuggestionDimensions] = useState(''); const [aiSuggestionDirection, setAiSuggestionDirection] = useState('any'); const [aiSuggestionStyle, setAiSuggestionStyle] = useState('any'); const [aiSuggestionBackground, setAiSuggestionBackground] = useState('any'); const [adminAssetUrlInputs, setAdminAssetUrlInputs] = useState>({} as Record); const [pendingAdminAssetBase64, setPendingAdminAssetBase64] = useState>>({}); const [pendingSoundUploads, setPendingSoundUploads] = useState>>({}); // AI Chat Bot State const [aiChatMessages, setAiChatMessages] = useState([]); const [currentAiQuery, setCurrentAiQuery] = useState(''); const [selectedAiLanguage, setSelectedAiLanguage] = useState(languageOptions[0].code); // Default to English code const [isAiResponding, setIsAiResponding] = useState(false); const aiChatLogRef = useRef(null); const geminiAi = useMemo(() => new GoogleGenAI({ apiKey: process.env.API_KEY! }), []); // Knowledge Base State const [newReplyTopic, setNewReplyTopic] = useState(''); const [newReplyAnswer, setNewReplyAnswer] = useState(''); useEffect(() => { if (isOpen) { const newLocalAdminConfig = JSON.parse(JSON.stringify(adminConfig)); if (!newLocalAdminConfig.savedReplies) { // Initialize if undefined newLocalAdminConfig.savedReplies = []; } setLocalAdminConfig(newLocalAdminConfig); const initialNftImageInputs: Record = {}; const initialPendingNftChanges: Record = {}; Object.keys(runtimeNftDefinitions).forEach(nftId => { const img = runtimeNftDefinitions[nftId].image; initialNftImageInputs[nftId] = img.startsWith('data:image') ? '' : img; initialPendingNftChanges[nftId] = img.startsWith('data:image') ? img : null; }); setNftImageInputs(initialNftImageInputs); setPendingNftImageChanges(initialPendingNftChanges); const initialCharAttrInputs: Record = {}; Object.keys(runtimeGameCharacters).forEach(charId => { const char = runtimeGameCharacters[charId]; initialCharAttrInputs[charId] = { name: char.name, imageURL: char.image.startsWith('data:image') ? '' : char.image, imageBase64: char.image.startsWith('data:image') ? char.image : null, defaultWeaponType: char.defaultWeaponType || WeaponType.StandardBullet, autoFireHealthThreshold: char.autoFireHealthThreshold || 0, baseHealth: char.baseHealth || (char.type === 'player' ? 100 : (char.type === 'enemy' ? 25 : 100)), baseSpeed: char.baseSpeed || (char.type === 'enemy' ? 1 : PLAYER_SPEED), }; }); setCharacterAttributeInputs(initialCharAttrInputs); const currentAdminAssetUrlInputs: Record = {} as Record; const currentPendingAdminAssetBase64: Partial> = {}; (Object.keys(newLocalAdminConfig) as Array).forEach(key => { if ((key.toLowerCase().includes('image') || key.toLowerCase().includes('imageurl')) && typeof newLocalAdminConfig[key] === 'string') { const imgVal = newLocalAdminConfig[key] as string; if (imgVal.startsWith('data:image')) { currentPendingAdminAssetBase64[key as AdminConfigImageKey] = imgVal; currentAdminAssetUrlInputs[key as AdminConfigImageKey] = ''; } else { currentAdminAssetUrlInputs[key as AdminConfigImageKey] = imgVal; currentPendingAdminAssetBase64[key as AdminConfigImageKey] = null; } } }); setAdminAssetUrlInputs(currentAdminAssetUrlInputs); setPendingAdminAssetBase64(currentPendingAdminAssetBase64); setAiPrompts({}); setAiGeneratedImagePreviews({}); setIsGenerating({}); setPendingSoundUploads({}); // Reset AI Chat state setAiChatMessages([]); setCurrentAiQuery(''); setIsAiResponding(false); // Reset Knowledge Base input fields setNewReplyTopic(''); setNewReplyAnswer(''); } }, [isOpen, adminConfig, runtimeNftDefinitions, runtimeGameCharacters]); const handleInputChange = (e: React.ChangeEvent) => { const { name, value, type } = e.target; const checked = (e.target as HTMLInputElement).checked; setLocalAdminConfig(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : type === 'number' ? parseFloat(value) : value, })); }; const handleNftImageUrlChange = (nftId: string, value: string) => { setNftImageInputs(prev => ({ ...prev, [nftId]: value })); setPendingNftImageChanges(prev => ({...prev, [nftId]: null})); }; const handleNftImageFileUpload = async (nftId: string, file: File | null) => { if (file) { try { const base64String = await convertFileToBase64(file); setPendingNftImageChanges(prev => ({...prev, [nftId]: base64String})); setNftImageInputs(prev => ({ ...prev, [nftId]: '' })); showNotification(`Image ready for NFT: ${runtimeNftDefinitions[nftId]?.name}. Click 'Apply Custom Image'.`, 'info'); } catch (error) { console.error("Error converting NFT image:", error); showNotification('Failed to load image file.', 'error');} } }; const handleApplyCustomNftImage = (nftId: string) => { const pendingBase64 = pendingNftImageChanges[nftId]; const urlInput = nftImageInputs[nftId]; if (pendingBase64) { onUpdateNftImage(nftId, pendingBase64); } else if (urlInput && urlInput.trim() !== '') { onUpdateNftImage(nftId, urlInput); } else { showNotification('No new image (URL or file) to apply.', 'warning'); } }; const handleCharacterAttributeChange = (characterId: string, field: keyof CharacterAttributeInputs, value: string | number | WeaponType | undefined) => { setCharacterAttributeInputs(prev => ({ ...prev, [characterId]: { ...(prev[characterId] || {} as CharacterAttributeInputs), [field]: value, ...(field === 'imageURL' && { imageBase64: null }) } })); }; const handleCharacterSelectChange = (characterId: string, field: keyof CharacterAttributeInputs, value: string) => { const weaponTypeEnumValue = WeaponType[value as keyof typeof WeaponType]; handleCharacterAttributeChange(characterId, field, weaponTypeEnumValue || WeaponType.StandardBullet); }; const handleCharacterImageFileUpload = async (characterId: string, file: File | null) => { if (file) { try { const base64String = await convertFileToBase64(file); setCharacterAttributeInputs(prev => ({ ...prev, [characterId]: { ...(prev[characterId] || {} as CharacterAttributeInputs), imageBase64: base64String, imageURL: '' } })); showNotification(`Image ready for Character: ${runtimeGameCharacters[characterId]?.name}. Click 'Apply Custom Image'.`, 'info'); } catch (error) { console.error("Error converting character image:", error); showNotification('Failed to load image file.', 'error');} } }; const handleApplyCustomCharacterImage = (characterId: string) => { const currentInputs = characterAttributeInputs[characterId]; if (!currentInputs) return; if (currentInputs.imageBase64) { onUpdateCharacterAttributes(characterId, { image: currentInputs.imageBase64 }); } else if (currentInputs.imageURL && currentInputs.imageURL.trim() !== '') { onUpdateCharacterAttributes(characterId, { image: currentInputs.imageURL }); } else { showNotification('No new image (URL or file) to apply for character.', 'warning'); } }; const handleApplyCharacterNonImageChanges = (characterId: string) => { const currentInputs = characterAttributeInputs[characterId]; if (!currentInputs) return; const updates: Partial = {}; const originalChar = runtimeGameCharacters[characterId]; if (currentInputs.name !== originalChar.name) updates.name = currentInputs.name; if (currentInputs.baseHealth !== originalChar.baseHealth && typeof currentInputs.baseHealth === 'number') updates.baseHealth = currentInputs.baseHealth; if (currentInputs.baseSpeed !== originalChar.baseSpeed && typeof currentInputs.baseSpeed === 'number') updates.baseSpeed = currentInputs.baseSpeed; if (originalChar.type === 'player') { if (currentInputs.defaultWeaponType !== (originalChar.defaultWeaponType || WeaponType.StandardBullet)) updates.defaultWeaponType = currentInputs.defaultWeaponType; const newThreshold = Number(currentInputs.autoFireHealthThreshold); if (newThreshold !== (originalChar.autoFireHealthThreshold || 0) && !isNaN(newThreshold)) updates.autoFireHealthThreshold = Math.max(0, Math.min(100, newThreshold)); } if (Object.keys(updates).length > 0) onUpdateCharacterAttributes(characterId, updates); else showNotification('No non-image attributes changed.', 'info'); }; const handleAdminConfigAssetUrlChange = (configKey: AdminConfigImageKey, value: string) => { setAdminAssetUrlInputs(prev => ({ ...prev, [configKey]: value })); setPendingAdminAssetBase64(prev => ({ ...prev, [configKey]: null })); setLocalAdminConfig(prev => ({ ...prev, [configKey]: value })); }; const handleAdminConfigAssetFileUpload = async (configKey: AdminConfigImageKey, file: File | null) => { if (file) { try { const base64String = await convertFileToBase64(file); setPendingAdminAssetBase64(prev => ({ ...prev, [configKey]: base64String })); setAdminAssetUrlInputs(prev => ({ ...prev, [configKey]: '' })); setLocalAdminConfig(prev => ({ ...prev, [configKey]: base64String })); showNotification(`Image ready for ${configKey}. Save all admin changes to finalize.`, 'info'); } catch (error) { console.error(`Error converting ${configKey} image:`, error); showNotification('Failed to load image file.', 'error'); } } }; const handleApplyCustomAdminConfigAssetImage = (configKey: AdminConfigImageKey) => { const pendingBase64 = pendingAdminAssetBase64[configKey]; const urlInput = adminAssetUrlInputs[configKey]; let applied = false; if (pendingBase64) { setLocalAdminConfig(prev => ({ ...prev, [configKey]: pendingBase64 })); applied = true; } else if (urlInput && urlInput.trim() !== '') { setLocalAdminConfig(prev => ({ ...prev, [configKey]: urlInput })); applied = true; } if (applied) showNotification(`Preview updated for ${configKey}. Save all admin changes to finalize.`, 'info'); else showNotification(`No new image (URL or file) to apply for ${configKey}.`, 'warning'); }; const handleSoundFileUpload = async (eventKey: SoundEvent, file: File | null) => { if (file) { try { const base64String = await convertFileToBase64(file); const adminSoundKeyPattern = `sound${eventKey.charAt(0).toUpperCase() + eventKey.slice(1).replace(/_([a-z])/g, (g) => g[1].toUpperCase())}Url`; const adminSoundKey = adminSoundKeyPattern as SoundUrlKey; setPendingSoundUploads(prev => ({...prev, [adminSoundKey]: base64String})); setLocalAdminConfig(prev => ({ ...prev, [adminSoundKey]: base64String })); showNotification(`${formatSoundEventName(eventKey)} sound updated. Save all admin changes to finalize.`, 'info'); } catch (error) { console.error(`Error converting sound file for ${eventKey}:`, error); showNotification(`Failed to load sound for ${eventKey}.`, 'error');} } }; const handleSaveAllAdminChanges = () => { const finalConfig = {...localAdminConfig}; (Object.keys(pendingAdminAssetBase64) as Array).forEach(key => { if (pendingAdminAssetBase64[key]) { finalConfig[key] = pendingAdminAssetBase64[key]!; } else if (adminAssetUrlInputs[key] && adminAssetUrlInputs[key].trim() !== '') { finalConfig[key] = adminAssetUrlInputs[key]; } }); Object.keys(pendingSoundUploads).forEach(stringKey => { const key = stringKey as SoundUrlKey; if (pendingSoundUploads[key]) { finalConfig[key] = pendingSoundUploads[key]!; } }); onUpdateAdminConfig(finalConfig); }; const handleAiPromptChange = (assetKey: string, prompt: string) => { setAiPrompts(prev => ({ ...prev, [assetKey]: prompt })); }; const generateCharacterPromptFromName = ( characterName: string, characterType: 'player' | 'enemy' ): string => { const cleanName = characterName.replace(/\p{Emoji}/gu, '').trim(); let baseDescription = ""; if (characterType === 'player') { baseDescription = `A game character asset, a player Doge Whale, visually representing: ${cleanName}.`; } else { baseDescription = `A game character asset, an enemy sea creature, visually representing: ${cleanName}.`; } let prompt = `${baseDescription}`; if (aiSuggestionStyle && aiSuggestionStyle !== 'any') { prompt += ` Style: ${aiSuggestionStyle}.`; } else { prompt += ` Style: 3D render.`; } if (aiSuggestionDimensions) { prompt += ` Dimensions: ${aiSuggestionDimensions}.`; } if (aiSuggestionDirection && aiSuggestionDirection !== 'any') { prompt += ` Direction: ${aiSuggestionDirection}.`; } if (aiSuggestionBackground && aiSuggestionBackground !== 'any') { prompt += ` Background: ${aiSuggestionBackground}.`; } else { prompt += ` Background: transparent background for versatility.`; } return prompt.trim(); }; const handleAutoGenerateForCharacter = async (characterId: string) => { const character = runtimeGameCharacters[characterId]; if (!character) return; const assetKey = `char_${characterId}`; const generatedPrompt = generateCharacterPromptFromName( character.name, character.type ); setAiPrompts(prev => ({ ...prev, [assetKey]: generatedPrompt })); showNotification(`Prompt suggested for ${character.name}: "${generatedPrompt.substring(0,50)}..."`, 'info'); await handleGenerateAiImage(assetKey, 'character', characterId); }; const handleGenerateAiImage = async (assetKey: string, itemType: string, idOrConfigKey: string) => { const userPrompt = aiPrompts[assetKey]; if (!userPrompt) { showNotification("Please enter or auto-generate a prompt for the AI.", "warning"); return; } setIsGenerating(prev => ({ ...prev, [assetKey]: true })); setAiGeneratedImagePreviews(prev => ({ ...prev, [assetKey]: null })); let fullPrompt = ""; let itemNameForNotification = idOrConfigKey; switch (itemType) { case 'nft': fullPrompt = `A digital art NFT for a game, depicting a Doge Whale character. The NFT should represent: ${userPrompt}.`; itemNameForNotification = runtimeNftDefinitions[idOrConfigKey]?.name || idOrConfigKey; break; case 'character': fullPrompt = userPrompt; itemNameForNotification = runtimeGameCharacters[idOrConfigKey]?.name || idOrConfigKey; break; case 'adminConfigAsset': // Generic handler for AdminConfig assets based on idOrConfigKey const configKeyName = idOrConfigKey as keyof AdminConfig; const displayNameForKey = configKeyName .replace('adminConfig_', '') .replace('ImageUrl', '') .replace('Image', '') .replace(/([A-Z])/g, ' $1').trim(); if (configKeyName.includes('dogeEscape')) { fullPrompt = `A game asset for 'Doge Whale Escape', a 2D maze game. The asset is a ${displayNameForKey}. Appearance: ${userPrompt}. Style: Clear, slightly pixelated or cartoonish, suitable for a grid-based game.`; } else if (configKeyName === 'gameBackgroundImageUrl') { fullPrompt = `A vibrant and immersive game background for an underwater space MMORPG featuring Doge Whales. The scene should be: ${userPrompt}. Scenic and detailed.`; } else if (configKeyName === 'defaultEnemyImageUrl') { fullPrompt = `A generic enemy creature for an underwater space game. Appearance: ${userPrompt}. Needs to be distinct and somewhat menacing.`; } else if (configKeyName === 'seaTokenImageUrl') { fullPrompt = `A single, distinct game currency item called a "Sea Token". It should look like: ${userPrompt}. Small, clear, iconic design.`; } else if (configKeyName === 'redArrowImageUrl') { fullPrompt = `A sleek, powerful red arrow projectile for a defensive weapon system in a space game. Appearance: ${userPrompt}. Dynamic, sci-fi style, glowing red.`; } else if (configKeyName === 'flameParticleImageUrl') { fullPrompt = `Small, bright, fiery particles or hot embers, suitable for a flame cone weapon effect in a game. Details: ${userPrompt}. Style: VFX sprite, clear background if possible.`; } else if (configKeyName === 'enemyBurningEffectImageUrl') { fullPrompt = `A visual effect for a game, representing an enemy character on fire. Could be a sprite sheet for an animation or a static overlay. Details: ${userPrompt}. Style: Game visual effect, transparent background suitable for overlay.`; } else if (configKeyName === 'fireballProjectileImageUrl') { fullPrompt = `A dynamic and impactful fireball projectile for a fantasy or sci-fi game weapon. Details: ${userPrompt}. Style: Magical, fiery, possibly with a trailing effect, on a transparent background.`; } else if (configKeyName.startsWith('decorativeFish')) { const fishType = displayNameForKey.replace('Decorative Fish ', ''); fullPrompt = `A decorative ${fishType.toLowerCase()} fish for an underwater space game. Appearance: ${userPrompt}.`; } else if (configKeyName.startsWith('bubble')) { const bubbleSize = displayNameForKey.replace('Bubble ', ''); fullPrompt = `A ${bubbleSize.toLowerCase()} bubble effect for an underwater game. Appearance: ${userPrompt}.`; } else { fullPrompt = `A game asset: ${displayNameForKey}. Description: ${userPrompt}.`; } itemNameForNotification = displayNameForKey; break; default: showNotification("Unknown item type for AI generation.", "error"); setIsGenerating(prev => ({ ...prev, [assetKey]: false })); return; } const result: GeminiImageResponse = await generateImageWithGemini(fullPrompt); setIsGenerating(prev => ({ ...prev, [assetKey]: false })); if (result.success && result.imageUrl) { setAiGeneratedImagePreviews(prev => ({ ...prev, [assetKey]: result.imageUrl! })); if (superAiAgentEnabled) { handleApplyAiImage(assetKey, itemType, idOrConfigKey, result.imageUrl); showNotification(`AI image generated and auto-applied to ${itemNameForNotification}!`, "success"); } else { showNotification(`Image generated for ${itemNameForNotification}! Preview below. Click 'Apply AI Image'.`, "success"); } } else { if (result.errorType === 'quota_exhausted') { showNotification(result.message || 'API Quota Exceeded. Check your plan or wait for reset.', 'error'); } else { showNotification(result.message || `Failed to generate image for ${itemNameForNotification}. Check console.`, 'error'); } } }; const handleApplyAiImage = (assetKey: string, itemType: string, idOrConfigKey: string, imageUrlToApply?: string) => { const imageUrl = imageUrlToApply || aiGeneratedImagePreviews[assetKey]; if (!imageUrl) { showNotification("No AI-generated image to apply.", "warning"); return; } let itemNameForNotification = idOrConfigKey; if (itemType === 'nft') { onUpdateNftImage(idOrConfigKey, imageUrl); itemNameForNotification = runtimeNftDefinitions[idOrConfigKey]?.name || idOrConfigKey; showNotification(`AI image applied to NFT: ${itemNameForNotification}.`, "success"); } else if (itemType === 'character') { onUpdateCharacterAttributes(idOrConfigKey, { image: imageUrl }); itemNameForNotification = runtimeGameCharacters[idOrConfigKey]?.name || idOrConfigKey; showNotification(`AI image applied to Character: ${itemNameForNotification}.`, "success"); } else if (itemType === 'adminConfigAsset') { // Updated for generic admin config assets const configKey = idOrConfigKey as AdminConfigImageKey; setLocalAdminConfig(prev => ({ ...prev, [configKey]: imageUrl })); setPendingAdminAssetBase64(prev => ({ ...prev, [configKey]: imageUrl })); setAdminAssetUrlInputs(prev => ({ ...prev, [configKey]: '' })); itemNameForNotification = configKey.replace('adminConfig_', '').replace('ImageUrl', '').replace('Image', '').replace(/([A-Z])/g, ' $1').trim(); showNotification(`AI image applied to ${itemNameForNotification}. Save all admin changes to finalize.`, "success"); } else { showNotification("Unknown item type for applying AI image.", "error"); return; } if (!superAiAgentEnabled || imageUrlToApply) { setAiGeneratedImagePreviews(prev => ({ ...prev, [assetKey]: null })); } }; // AI Chat Bot Logic const handleSendAiQuery = async () => { if (currentAiQuery.trim() === '' || isAiResponding) return; const userMessage: AIChatMessage = { id: generateId(), sender: 'user', text: currentAiQuery, lang: languageOptions.find(l => l.code === selectedAiLanguage)?.name || selectedAiLanguage, }; setAiChatMessages(prev => [...prev, userMessage]); setIsAiResponding(true); setCurrentAiQuery(''); try { const languageName = languageOptions.find(l => l.code === selectedAiLanguage)?.name || 'English'; let knowledgeBaseText = ""; if (localAdminConfig.savedReplies && localAdminConfig.savedReplies.length > 0) { knowledgeBaseText = "You have access to the following predefined knowledge base. Use this information when relevant to the user's query:\n"; knowledgeBaseText += JSON.stringify(localAdminConfig.savedReplies.map(r => ({topic: r.topic, answer: r.answer}))) + "\n---\n"; } const systemInstruction = `${knowledgeBaseText}You are a helpful AI assistant for a game administrator. Respond to the user's query in ${languageName}. Be concise and helpful.`; const response: GenerateContentResponse = await geminiAi.models.generateContent({ model: 'gemini-2.5-flash-preview-04-17', contents: userMessage.text, config: { systemInstruction: systemInstruction }, }); const aiTextResponse = response.text; const aiMessage: AIChatMessage = { id: generateId(), sender: 'ai', text: aiTextResponse, }; setAiChatMessages(prev => [...prev, aiMessage]); } catch (error: any) { console.error("Error with AI Chat:", error); const errorMessage: AIChatMessage = { id: generateId(), sender: 'error', text: `AI Error: ${error.message || 'Could not get a response.'}`, }; setAiChatMessages(prev => [...prev, errorMessage]); } finally { setIsAiResponding(false); } }; useEffect(() => { if (aiChatLogRef.current) { aiChatLogRef.current.scrollTop = aiChatLogRef.current.scrollHeight; } }, [aiChatMessages]); // Knowledge Base Management const handleAddReply = () => { if (!newReplyTopic.trim() || !newReplyAnswer.trim()) { showNotification("Topic and Answer cannot be empty.", "warning"); return; } setLocalAdminConfig(prev => ({ ...prev, savedReplies: [ ...(prev.savedReplies || []), { id: generateId(), topic: newReplyTopic.trim(), answer: newReplyAnswer.trim() } ] })); setNewReplyTopic(''); setNewReplyAnswer(''); showNotification("Reply added to knowledge base. Save all admin changes to finalize.", "info"); }; const handleDeleteReply = (replyId: string) => { setLocalAdminConfig(prev => ({ ...prev, savedReplies: (prev.savedReplies || []).filter(reply => reply.id !== replyId) })); showNotification("Reply removed from knowledge base. Save all admin changes to finalize.", "info"); }; const renderAssetImageEditor = ( editorAssetKey: string, displayName: string, itemType: 'nft' | 'character' | 'adminConfigAsset', idOrConfigKey: string, currentImageUrl: string | undefined ) => { const assetSpecificPrompt = aiPrompts[editorAssetKey] || ''; const isAssetGenerating = isGenerating[editorAssetKey] || false; const assetAiPreview = aiGeneratedImagePreviews[editorAssetKey] || null; const character = itemType === 'character' ? runtimeGameCharacters[idOrConfigKey] : null; let assetUrlInputValue = ''; if (itemType === 'adminConfigAsset') { assetUrlInputValue = adminAssetUrlInputs[idOrConfigKey as AdminConfigImageKey] || ''; } else if (itemType === 'nft') { assetUrlInputValue = nftImageInputs[idOrConfigKey] || ''; } else if (itemType === 'character') { assetUrlInputValue = characterAttributeInputs[idOrConfigKey]?.imageURL || ''; } return (
{displayName}
{`${displayName} (e.currentTarget.src = 'https://via.placeholder.com/150?text=Error')} />