// Configuration - Now using local AI simulation (no API required) const USE_LOCAL_AI = true; const LOCAL_AI_DELAY = 800; // ms delay to simulate AI thinking const LOCAL_IMAGE_MODEL = true; // Enable local image generation // State let isGenerating = false; let abortController = null; let localAiInterval = null; document.addEventListener('DOMContentLoaded', function() { const chatForm = document.getElementById('chatForm'); const messageInput = document.getElementById('messageInput'); const chatMessages = document.getElementById('chatMessages'); const charCount = document.getElementById('charCount'); const stopButton = document.getElementById('stopButton'); const sendButton = document.getElementById('sendButton'); const generateImageBtn = document.getElementById('generateImageBtn'); const generateImageButton = document.getElementById('generateImageButton'); const aiStatus = document.getElementById('aiStatus'); const apiStatus = document.getElementById('apiStatus'); // Character counter messageInput.addEventListener('input', function() { charCount.textContent = `${this.value.length}/1000`; }); // Image generation button if (generateImageBtn) { generateImageBtn.addEventListener('click', generateSceneImage); } // New image generation from last message if (generateImageButton) { generateImageButton.addEventListener('click', generateImageFromLastMessage); } // Send message chatForm.addEventListener('submit', async function(e) { e.preventDefault(); if (isGenerating) { stopGeneration(); return; } await sendMessage(); }); // Stop generation stopButton.addEventListener('click', stopGeneration); // Allow Shift+Enter for new line, Enter to send messageInput.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (isGenerating) { stopGeneration(); } else { chatForm.dispatchEvent(new Event('submit')); } } }); // Send message function async function sendMessage() { const text = messageInput.value.trim(); if (!text) return; // Add user message addMessage('user', text); messageInput.value = ''; charCount.textContent = '0/1000'; // Show typing indicator showTyping(); setGenerating(true); updateAPIStatus(true, 'Generating locally...'); try { const characterName = document.getElementById('characterName').textContent; const characterRole = document.querySelector('.bg-surface.rounded-xl.p-5 span').textContent; const responseLength = document.getElementById('responseLength').value; // Build system prompt const systemPrompt = buildSystemPrompt(characterName, characterRole, responseLength); // Get conversation history const messages = getConversationHistory(systemPrompt); // Use local AI simulation (no API calls) const aiResponse = await generateLocalAIResponse(characterName, messages, responseLength); removeTyping(); addMessage('ai', aiResponse); setGenerating(false); updateAPIStatus(true, 'Response ready'); // Check if AI response suggests image generation if (aiResponse.includes('generate an image') || aiResponse.includes('Would you like me to generate')) { // Auto-suggest image generation after a delay setTimeout(() => { if (confirm(`${characterName} suggested generating an image. Would you like to create one based on your last message?`)) { generateImageFromLastMessage(); } }, 1500); } } catch (error) { removeTyping(); console.error('AI Error:', error); addMessage('system', `Local AI error: ${error.message}. Using fallback response.`); // Fallback to local response const fallbackResponse = getFallbackResponse(); addMessage('ai', fallbackResponse); setGenerating(false); updateAPIStatus(true, 'Using fallback'); } } // Generate scene image using local model async function generateSceneImage() { if (!LOCAL_IMAGE_MODEL) { addMessage('system', 'Local image model is not enabled.'); return; } // Create and show image generator component const generator = document.createElement('image-generator'); // Create a container for the generator in the chat const container = document.createElement('div'); container.className = 'message user animate-message-slide mb-6'; container.innerHTML = `
You

Generating an image of the current scene using local AI model...

${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
`; chatMessages.appendChild(container); const genContainer = container.querySelector('#imageGenContainer'); genContainer.appendChild(generator); // Scroll to show chatMessages.scrollTop = chatMessages.scrollHeight; // Trigger generation after a short delay setTimeout(() => { generator.shadowRoot.querySelector('.generate-btn').click(); }, 500); } // Generate image from the most recent message in chat async function generateImageFromLastMessage() { if (!LOCAL_IMAGE_MODEL) { addMessage('system', 'Local image model is not enabled.'); return; } // Get the most recent USER message content const messages = chatMessages.querySelectorAll('.message.user'); let lastUserMessage = ''; let lastUserMessageElement = null; // Look for the last user message (skip system messages) for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; const p = msg.querySelector('p'); if (p && !p.textContent.includes('Generating an image') && !p.textContent.includes('*Generating image for')) { lastUserMessage = p.textContent.trim(); lastUserMessageElement = msg; break; } } if (!lastUserMessage) { // If no user message, fallback to last AI message const aiMessages = chatMessages.querySelectorAll('.message.ai'); for (let i = aiMessages.length - 1; i >= 0; i--) { const msg = aiMessages[i]; const p = msg.querySelector('p'); if (p && !p.textContent.includes('Here\'s an image inspired by')) { lastUserMessage = p.textContent.trim(); lastUserMessageElement = msg; break; } } } if (!lastUserMessage) { lastUserMessage = "A mysterious scene from the conversation"; } // Use the exact last message as prompt const prompt = lastUserMessage.substring(0, 200); // Limit length // Add a message indicating image generation is starting const thinkingMsg = addMessage('user', `*Generating image based on your message: "${prompt.substring(0, 60)}..."*`); // Show typing indicator showTyping(); setGenerating(true); updateAPIStatus(true, 'Generating image with AI model...'); try { // Use built-in AI image generator (simulated for now) const imageUrl = await generateImageWithLocalAIModel(prompt); removeTyping(); setGenerating(false); // Remove the thinking message if (thinkingMsg && thinkingMsg.parentNode) { thinkingMsg.parentNode.remove(); } // Display the generated image in chat displayGeneratedImage(imageUrl, prompt); updateAPIStatus(true, 'Image generated'); } catch (error) { removeTyping(); setGenerating(false); addMessage('system', `Image generation failed: ${error.message}. Using fallback.`); // Fallback to static image based on prompt const fallbackImage = getFallbackImage(prompt); if (thinkingMsg && thinkingMsg.parentNode) { thinkingMsg.parentNode.remove(); } displayGeneratedImage(fallbackImage, prompt); updateAPIStatus(true, 'Used fallback image'); } } // Generate image using built-in AI model simulation async function generateImageWithLocalAIModel(prompt) { // Simulate a local AI image model running in the browser // In a real implementation, this would use TensorFlow.js or ONNX Runtime // with a model like Stable Diffusion Lite // For demonstration, we'll use a more sophisticated simulation // that generates images based on the prompt content // Analyze prompt for themes const themes = analyzePromptForThemes(prompt); const category = themes.category; const style = themes.style; const seed = Math.floor(Math.random() * 1000) + 1; // Generate a deterministic image URL based on prompt const promptHash = hashString(prompt); const imageSeed = (promptHash + seed) % 1000; // Use static.photos with parameters that match the prompt const width = 400; const height = 300; return `https://static.photos/${category}/${width}x${height}/${imageSeed}`; } // Analyze prompt to determine image category and style function analyzePromptForThemes(prompt) { const lowerPrompt = prompt.toLowerCase(); let category = 'abstract'; let style = 'default'; // Determine category based on keywords if (lowerPrompt.includes('dragon') || lowerPrompt.includes('wizard') || lowerPrompt.includes('castle') || lowerPrompt.includes('fantasy')) { category = 'fantasy'; } else if (lowerPrompt.includes('cyber') || lowerPrompt.includes('robot') || lowerPrompt.includes('future') || lowerPrompt.includes('tech')) { category = 'technology'; } else if (lowerPrompt.includes('space') || lowerPrompt.includes('alien') || lowerPrompt.includes('star') || lowerPrompt.includes('planet')) { category = 'aerial'; } else if (lowerPrompt.includes('forest') || lowerPrompt.includes('nature') || lowerPrompt.includes('tree') || lowerPrompt.includes('mountain')) { category = 'nature'; } else if (lowerPrompt.includes('city') || lowerPrompt.includes('building') || lowerPrompt.includes('urban') || lowerPrompt.includes('street')) { category = 'cityscape'; } else if (lowerPrompt.includes('person') || lowerPrompt.includes('people') || lowerPrompt.includes('man') || lowerPrompt.includes('woman')) { category = 'people'; } else if (lowerPrompt.includes('viking') || lowerPrompt.includes('warrior') || lowerPrompt.includes('battle') || lowerPrompt.includes('ancient')) { category = 'vintage'; } // Determine style based on keywords if (lowerPrompt.includes('dark') || lowerPrompt.includes('night') || lowerPrompt.includes('shadow') || lowerPrompt.includes('black')) { style = 'dark'; } else if (lowerPrompt.includes('bright') || lowerPrompt.includes('light') || lowerPrompt.includes('sun') || lowerPrompt.includes('white')) { style = 'white'; } else if (lowerPrompt.includes('blue') || lowerPrompt.includes('ocean') || lowerPrompt.includes('sky') || lowerPrompt.includes('water')) { style = 'blue'; } else if (lowerPrompt.includes('green') || lowerPrompt.includes('forest') || lowerPrompt.includes('nature') || lowerPrompt.includes('plant')) { style = 'green'; } return { category, style }; } // Simple string hash function function hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash); } // Fallback image generation function getFallbackImage(prompt) { const hash = hashString(prompt); const categories = ['abstract', 'nature', 'technology', 'people', 'fantasy', 'cityscape']; const category = categories[hash % categories.length]; const seed = (hash % 999) + 1; return `https://static.photos/${category}/400x300/${seed}`; } // Display generated image in chat function displayGeneratedImage(imageUrl, prompt) { const messageDiv = document.createElement('div'); messageDiv.className = 'message ai animate-message-slide mb-6'; const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const avatar = document.querySelector('.bg-surface.rounded-xl.p-5 img')?.src || 'https://static.photos/people/48x48/5'; const characterName = document.getElementById('characterName').textContent; messageDiv.innerHTML = `
${characterName}

Here's an image generated from your message using our built-in AI image model:

Generated image

Prompt: "${prompt.substring(0, 100)}..."

${time}
`; chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; feather.replace(); } // Local AI response generation (simulated) async function generateLocalAIResponse(characterName, messages, length) { return new Promise((resolve) => { // Simulate AI thinking time setTimeout(() => { const lastUserMessage = messages[messages.length - 1]?.content || ''; const response = generateCharacterResponse(characterName, lastUserMessage, length); resolve(response); }, LOCAL_AI_DELAY); }); } // Character-specific response generation function generateCharacterResponse(characterName, userMessage, length) { const characterResponses = { 'Astrid': [ `*adjusts her robe thoughtfully* Your words, "${userMessage}", resonate with the ancient prophecies. The stars whisper of similar patterns in the Crystal Archives.`, `Ah, a curious mind indeed! ${userMessage} reminds me of the time when the Moonstone Amulet revealed its secrets to the chosen one.`, `*eyes twinkle with arcane energy* In Eldoria, such questions are pondered by the wisest sages. Let me share what the scrolls reveal about this matter.`, `The mystical winds carry echoes of your query. ${userMessage}... yes, I recall an enchantment that dealt with similar concepts in the Whispering Woods.`, `*gestures with a glowing staff* By the old gods, your inquiry touches upon forbidden lore. But for you, traveler, I shall reveal what I know.` ], 'Kael': [ `*takes a drag from his virtual cigarette* "${userMessage}"... that's a loaded question in Neo‑Tokyo. The data points to several possibilities, none of them pretty.`, `Hmm. ${userMessage}. Let me check my neural implant's database. Yeah, there's a case file from '48 that matches this pattern.`, `*checks his wrist‑holo* You're asking about ${userMessage}? That's corporate‑level intel. But for the right price... I might have some leads.`, `In this city, every byte has a price. Your query about "${userMessage}" is no exception. Let me dig through the encrypted channels.`, `*cyber‑eye flickers* ${userMessage}... that triggers a security alert. But I know a backdoor into the mainframe that might give us answers.` ], 'Lyra': [ `*checks star chart* Captain's log: our guest asks, "${userMessage}". This aligns with our recent discovery in the Andromeda sector.`, `Fascinating! ${userMessage} is precisely what we encountered near the quantum nebula. The alien flora there exhibited similar properties.`, `*adjusts comms headset* On the Aether, we've documented phenomena related to "${userMessage}". Let me pull up the holographic records.`, `Your curiosity about ${userMessage} reminds me of the Silicate Entities we met on Kepler‑186f. Their communication patterns were remarkably similar.`, `*gestures to the viewport* See that pulsar? It's emitting signals that correlate with your query about "${userMessage}". Coincidence? I think not.` ], 'Ragnar': [ `*grins, sharpening his axe* By Odin's beard! "${userMessage}" is a question worthy of a true warrior! Let me tell you a tale from the frozen north.`, `HA! ${userMessage} reminds me of the time I faced the Ice Giant Jörmund! His roars shook the very mountains with similar intent!`, `*drinks from a horn* Your words, "${userMessage}", echo in the great halls of Valhalla! The All‑Father himself would approve of such curiosity!`, `A warrior's mind is as sharp as his blade! ${userMessage}... let me consult the rune stones for their ancient wisdom on this matter.`, `*slams fist on table* ${userMessage}! A bold query! The skalds will sing of this day when wisdom was sought with such courage!` ], 'Elara': [ `*a leaf drifts into her hand* The forest whispers of your question: "${userMessage}". The ancient trees have dreamed of similar concepts.`, `Gentle one, ${userMessage}... let me consult the spirit of the river. Its flowing waters carry memories of such mysteries.`, `*birds gather nearby* Your curiosity about "${userMessage}" is known to the woodland creatures. The fox has seen similar patterns in the moonlit glades.`, `The moss on the standing stones tells stories related to ${userMessage}. Let me translate their silent language for you.`, `*breathes in the forest air* ${userMessage}... yes, the mycelium network beneath us pulses with knowledge of this. The mushrooms will guide us.` ], 'Victor': [ `*tinkers with a brass device* "${userMessage}" you say? That's precisely what my latest invention, the Aether‑Oscillograph, was designed to measure!`, `Fascinating! ${userMessage} aligns perfectly with the theoretical principles I outlined in my monograph on quantum‑steam dynamics!`, `*adjusts his goggles* Your query about "${userMessage}" reminds me of the incident with the Phase‑Shift Engine last Tuesday! Nearly vaporized my laboratory!`, `Ah, ${userMessage}! That's elementary, my dear friend! Let me demonstrate with this pocket‑sized Tesla coil and some copper wiring...`, `*consults a blueprint* "${userMessage}"... yes, yes! I have schematics for a device that could potentially address that very conundrum!` ] }; // Get character-specific responses or generic ones const responses = characterResponses[characterName] || [ `*considers thoughtfully* "${userMessage}"... that's an interesting perspective. Let me reflect on this.`, `Ah, your question about ${userMessage} touches upon deep matters. Allow me to share my thoughts.`, `*nods slowly* ${userMessage}. Yes, I have experience with similar situations. Here's what I've learned.`, `Fascinating inquiry! "${userMessage}" reminds me of something I encountered before. Let me elaborate.`, `*pauses for a moment* Your words, "${userMessage}", resonate with me. I believe I can offer some insight.` ]; // Adjust response length let response = responses[Math.floor(Math.random() * responses.length)]; if (length === 'short') { // Keep it brief response = response.split('.')[0] + '.'; } else if (length === 'detailed') { // Make it more detailed const details = [ ' The implications of this are far‑reaching, affecting multiple dimensions of our current situation.', ' I recall an ancient text that elaborates further on this very subject, suggesting deeper connections.', ' This aligns with the broader patterns we have observed throughout our journey together.', ' There are nuances here that warrant careful consideration, as they may reveal hidden truths.', ' Let me expand upon this with additional context from my own experiences and observations.' ]; response += details[Math.floor(Math.random() * details.length)]; } // Sometimes include image generation suggestion if (Math.random() > 0.7 && LOCAL_IMAGE_MODEL) { response += ' *Would you like me to generate an image of this scene?*'; } return response; } // Build system prompt function buildSystemPrompt(name, role, length) { const lengthMap = { short: 'Keep responses brief, 1-2 sentences.', medium: 'Respond with 2-4 sentences, descriptive but concise.', detailed: 'Respond with detailed, immersive paragraphs (4-6 sentences).' }; return `You are ${name}, a ${role}. You are in an immersive roleplay conversation. Stay in character at all times. ${lengthMap[length]} Use expressive language, show emotions, and advance the story. Never break character. If the user asks out-of-character questions, gently steer back to the roleplay.`; } // Get conversation history function getConversationHistory(systemPrompt) { const messages = [{ role: 'system', content: systemPrompt }]; const messageElements = chatMessages.querySelectorAll('.message'); messageElements.forEach(el => { const isUser = el.classList.contains('user'); const content = el.querySelector('p')?.textContent || ''; if (content && !content.includes('has joined the chat') && !content.includes('Chat cleared')) { messages.push({ role: isUser ? 'user' : 'assistant', content: content }); } }); return messages; } // Helper functions function getMaxTokens() { const length = document.getElementById('responseLength').value; switch(length) { case 'short': return 150; case 'detailed': return 400; default: return 250; } } function getTemperature() { const slider = document.querySelector('input[type="range"]'); return slider.value / 10; } function getFallbackResponse() { const character = document.getElementById('characterName').textContent; const fallbacks = { 'Astrid': ['*adjusts her robe thoughtfully* The stars align in curious patterns tonight. Your query resonates with an old prophecy I once deciphered in the Crystal Library.'], 'Kael': '*takes a drag from his virtual cigarette* The data doesn\'t lie, but it doesn\'t tell the whole truth either. In this city, every byte has a price.', 'Lyra': '*checks star chart* Captain\'s log: we\'re approaching an uncharted nebula. Your question reminds me of the time we first encountered the Silicate Entities.', 'Ragnar': '*grins, sharpening his axe* By Odin\'s beard! That\'s a tale worth telling over mead. Listen closely, for the winds carry whispers of glory.', 'Elara': '*a leaf drifts into her hand* The forest speaks of your curiosity. Let me share what the ancient trees have shown me in their dreams.', 'Victor': '*tinkers with a brass device* Fascinating! That aligns perfectly with my latest invention. Allow me to demonstrate the theoretical principles.' }; const generic = [ "I ponder your words carefully. There's more to this than meets the eye.", "Ah, an intriguing proposition! Let me weave that into our ongoing narrative.", "*considers thoughtfully* Your perspective adds a new layer to this situation.", "The universe holds many mysteries, and your question touches upon one of them.", "I must consult my knowledge on this matter. Meanwhile, tell me more of your thoughts." ]; return fallbacks[character] ? fallbacks[character][Math.floor(Math.random() * fallbacks[character].length)] : generic[Math.floor(Math.random() * generic.length)]; } // UI Functions function setGenerating(generating) { isGenerating = generating; if (generating) { stopButton.classList.remove('hidden'); sendButton.classList.add('hidden'); if (generateImageBtn) generateImageBtn.disabled = true; messageInput.disabled = true; aiStatus.textContent = `${document.getElementById('characterName').textContent} is thinking...`; updateAPIStatus(true, 'Generating locally'); } else { stopButton.classList.add('hidden'); sendButton.classList.remove('hidden'); if (generateImageBtn) generateImageBtn.disabled = false; messageInput.disabled = false; aiStatus.textContent = `AI is in character. Using local intelligence for immersive roleplay.`; abortController = null; if (localAiInterval) { clearInterval(localAiInterval); localAiInterval = null; } } } function stopGeneration() { if (abortController) { abortController.abort(); } removeTyping(); setGenerating(false); addMessage('system', 'Response generation stopped.'); updateAPIStatus(false, 'Stopped by user'); } function updateAPIStatus(connected, message) { if (connected) { apiStatus.textContent = message; apiStatus.className = 'px-2 py-1 rounded-full bg-green-900/30 text-green-400 text-xs'; } else { apiStatus.textContent = message; apiStatus.className = 'px-2 py-1 rounded-full bg-yellow-900/30 text-yellow-400 text-xs'; } } // Export functions to window window.addMessage = function(type, content) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${type} animate-message-slide mb-6`; const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const avatar = type === 'user' ? 'https://static.photos/people/48x48/10' : document.querySelector('.bg-surface.rounded-xl.p-5 img')?.src || 'https://static.photos/people/48x48/5'; const name = type === 'user' ? 'You' : document.getElementById('characterName').textContent; const nameColor = type === 'user' ? 'text-accent' : 'text-primary'; messageDiv.innerHTML = `
${type !== 'user' ? `` : ''}
${name}

${content}

${time}
${type === 'user' ? `` : ''}
`; chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; feather.replace(); }; function showTyping() { const typingDiv = document.createElement('div'); typingDiv.id = 'typingIndicator'; typingDiv.className = 'typing-indicator mb-6 flex gap-4'; typingDiv.innerHTML = `
${document.getElementById('characterName').textContent}
Thinking...
`; chatMessages.appendChild(typingDiv); chatMessages.scrollTop = chatMessages.scrollHeight; } function removeTyping() { const typing = document.getElementById('typingIndicator'); if (typing) typing.remove(); } // Pre‑fill example window.switchCharacter = function(name, role, avatar) { document.getElementById('characterName').textContent = name; const profile = document.querySelector('.bg-surface.rounded-xl.p-5'); const img = profile.querySelector('img'); const h2 = profile.querySelector('h2'); const span = profile.querySelector('span'); img.src = avatar; h2.textContent = name; span.textContent = role; addMessage('system', `${name} has joined the chat. Role: ${role}`); aiStatus.textContent = `AI is in character as ${name} (${role}). Using local intelligence for immersive roleplay.`; }; window.startNewScenario = function() { if (confirm('Start a new scenario? Current chat will be cleared.')) { chatMessages.innerHTML = ''; addMessage('system', 'New scenario started. Setting the stage...'); } }; window.clearChat = function() { chatMessages.innerHTML = ''; addMessage('system', 'Chat cleared. Ready for new adventures!'); }; // Initialize API status updateAPIStatus(true, 'Local AI Ready'); });