mitdow's picture
🐳 22/02 - 09:19 - Mixed Content: The page at '<URL>' was loaded over HTTPS, but requested an insecure element '<URL>'. This request was automatically upgraded to HTTPS, For more information see <URL>
d9b86e5 verified
Raw
History Blame Contribute Delete
37 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat | AI Companion</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.glass-panel {
background: rgba(30, 30, 40, 0.7);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.message-bubble {
max-width: 80%;
word-wrap: break-word;
}
.action-text {
color: #f472b6;
font-style: italic;
font-weight: 500;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.typing-indicator span {
animation: typing 1.4s infinite;
display: inline-block;
margin: 0 1px;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 100% { opacity: 0.2; transform: translateY(0); }
50% { opacity: 1; transform: translateY(-2px); }
}
.sidebar-item {
transition: all 0.2s;
}
.sidebar-item:hover {
background: rgba(168, 85, 247, 0.1);
border-left: 3px solid #a855f7;
}
.active-chat {
background: rgba(168, 85, 247, 0.15);
border-left: 3px solid #a855f7;
}
</style>
</head>
<body class="bg-gray-950 text-gray-100 h-screen overflow-hidden flex">
<!-- Mobile Sidebar Toggle -->
<button id="sidebarToggle" class="lg:hidden fixed top-4 left-4 z-50 p-2 bg-gray-800 rounded-lg shadow-lg">
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
<!-- Sidebar -->
<aside id="sidebar" class="fixed lg:static inset-y-0 left-0 z-40 w-80 bg-gray-900 border-r border-gray-800 transform -translate-x-full lg:translate-x-0 transition-transform duration-300 flex flex-col">
<!-- Sidebar Header -->
<div class="p-4 border-b border-gray-800">
<div class="flex items-center justify-between mb-4">
<h1 class="text-xl font-bold gradient-text flex items-center gap-2">
<i data-lucide="heart" class="w-5 h-5 text-pink-500"></i>
AI Companion
</h1>
<button id="closeSidebar" class="lg:hidden text-gray-400 hover:text-white">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<button onclick="window.location.href='index.html'" class="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-500 hover:to-pink-500 text-white py-2 rounded-lg font-medium transition-all flex items-center justify-center gap-2">
<i data-lucide="plus" class="w-4 h-4"></i>
New Character
</button>
</div>
<!-- Recent Chats -->
<div class="flex-1 overflow-y-auto py-4">
<h3 class="px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Recent Chats</h3>
<div id="recentChatsList" class="space-y-1">
<!-- Populated by JS -->
</div>
</div>
<!-- Settings & User -->
<div class="border-t border-gray-800 p-4 space-y-2">
<button id="settingsBtn" class="sidebar-item w-full flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-white">
<i data-lucide="settings" class="w-5 h-5"></i>
<span>Settings</span>
</button>
<button id="clearHistory" class="sidebar-item w-full flex items-center gap-3 px-4 py-3 rounded-lg text-gray-300 hover:text-red-400">
<i data-lucide="trash-2" class="w-5 h-5"></i>
<span>Clear History</span>
</button>
</div>
</aside>
<!-- Main Chat Area -->
<main class="flex-1 flex flex-col h-full relative">
<!-- Chat Header -->
<header class="glass-panel border-b border-gray-800 p-4 flex items-center justify-between sticky top-0 z-30">
<div class="flex items-center gap-3 ml-12 lg:ml-0">
<div class="relative">
<img id="headerAvatar" src="" alt="Character" class="w-10 h-10 rounded-full object-cover border-2 border-pink-500/30">
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-gray-900"></div>
</div>
<div>
<h2 id="headerName" class="font-semibold text-white">Loading...</h2>
<p class="text-xs text-gray-400">Online • Scenario Active</p>
</div>
</div>
<div class="flex items-center gap-2">
<button id="scenarioBtn" class="p-2 hover:bg-gray-800 rounded-lg transition-colors text-gray-400 hover:text-pink-400" title="View Scenario">
<i data-lucide="book-open" class="w-5 h-5"></i>
</button>
<button id="imageGenBtn" class="p-2 hover:bg-gray-800 rounded-lg transition-colors text-gray-400 hover:text-purple-400" title="Generate Image">
<i data-lucide="image" class="w-5 h-5"></i>
</button>
</div>
</header>
<!-- Messages Container -->
<div id="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-hide bg-gradient-to-b from-gray-950 to-gray-900">
<!-- Scene Setup Message -->
<div id="sceneMessage" class="hidden">
<div class="flex justify-center mb-6">
<div class="glass-panel rounded-2xl p-4 max-w-2xl border-l-4 border-pink-500">
<div class="flex items-center gap-2 mb-2 text-pink-400 text-sm font-semibold">
<i data-lucide="sparkles" class="w-4 h-4"></i>
Scene Setting
</div>
<p id="sceneText" class="text-gray-300 text-sm leading-relaxed"></p>
</div>
</div>
</div>
<!-- Messages will be inserted here -->
</div>
<!-- Typing Indicator -->
<div id="typingIndicator" class="hidden px-4 py-2">
<div class="flex items-center gap-2 text-gray-500 text-sm">
<span id="typingName">Character</span>
<div class="typing-indicator">
<span class="w-1.5 h-1.5 bg-pink-500 rounded-full"></span>
<span class="w-1.5 h-1.5 bg-pink-500 rounded-full"></span>
<span class="w-1.5 h-1.5 bg-pink-500 rounded-full"></span>
</div>
</div>
</div>
<!-- Input Area -->
<div class="glass-panel border-t border-gray-800 p-4">
<form id="chatForm" class="flex gap-3 max-w-4xl mx-auto">
<div class="flex-1 relative">
<input type="text" id="messageInput" placeholder="Type your message... (use * for actions)"
class="w-full bg-gray-800/50 border border-gray-700 rounded-xl px-4 py-3 pr-12 text-white placeholder-gray-500 focus:outline-none focus:border-pink-500/50 transition-colors">
<button type="button" id="quickAction" class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-pink-400 transition-colors" title="Quick Action">
<i data-lucide="zap" class="w-5 h-5"></i>
</button>
</div>
<button type="submit" class="bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-500 hover:to-purple-500 text-white px-6 py-3 rounded-xl font-medium transition-all transform hover:scale-105 flex items-center gap-2">
<i data-lucide="send" class="w-5 h-5"></i>
</button>
</form>
<p class="text-center text-xs text-gray-600 mt-2">Press Enter to send • Use *text* for actions</p>
</div>
</main>
<!-- Settings Modal -->
<div id="settingsModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 hidden items-center justify-center p-4">
<div class="glass-panel rounded-2xl p-6 max-w-md w-full border border-gray-700">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-semibold">Settings</h3>
<button id="closeSettings" class="text-gray-400 hover:text-white">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Image Generation</label>
<div class="bg-green-900/30 border border-green-700/50 rounded-lg px-3 py-2 text-sm text-green-300">
<i data-lucide="check-circle" class="w-4 h-4 inline mr-1"></i>
Using Pollinations.ai (Free - No API Key Required)
</div>
<p class="text-xs text-gray-500 mt-1">NSFW-friendly • Seed-based consistency • No authentication needed</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">OpenRouter API Key (Chat)</label>
<input type="text" id="openaiApiKey" placeholder="Enter your OpenRouter API key..."
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:border-purple-500 focus:outline-none font-mono">
<p class="text-xs text-gray-500 mt-1">Get free key at openrouter.ai/keys</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Response Style</label>
<select id="responseStyle" class="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:border-purple-500 focus:outline-none">
<option value="balanced">Balanced</option>
<option value="creative">Creative/Descriptive</option>
<option value="direct">Direct</option>
</select>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-300">NSFW Content</span>
<button id="toggleNSFW" class="w-12 h-6 bg-pink-600 rounded-full relative transition-colors">
<span class="absolute right-1 top-1 w-4 h-4 bg-white rounded-full transition-transform"></span>
</button>
</div>
</div>
<button id="saveSettings" class="w-full mt-6 bg-purple-600 hover:bg-purple-500 text-white py-3 rounded-lg font-medium transition-colors">
Save Settings
</button>
</div>
</div>
<!-- Scenario Modal -->
<div id="scenarioModal" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 hidden items-center justify-center p-4">
<div class="glass-panel rounded-2xl p-6 max-w-lg w-full border border-gray-700">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold gradient-text">Current Scenario</h3>
<button id="closeScenario" class="text-gray-400 hover:text-white">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="bg-gray-800/30 rounded-xl p-4 mb-4">
<h4 id="scenarioTitle" class="font-semibold text-white mb-2"></h4>
<p id="scenarioDesc" class="text-gray-300 text-sm leading-relaxed"></p>
</div>
<div class="flex gap-2 text-sm text-gray-400">
<i data-lucide="info" class="w-4 h-4"></i>
<span>This scene sets the context for your conversation</span>
</div>
</div>
</div>
<script>
// Initialize icons
lucide.createIcons();
// State
let currentCharacter = null;
let messages = [];
let isTyping = false;
// Load character
function loadCharacter() {
const saved = localStorage.getItem('currentCharacter');
if (saved) {
currentCharacter = JSON.parse(saved);
document.getElementById('headerName').textContent = currentCharacter.name;
document.getElementById('typingName').textContent = currentCharacter.name;
document.getElementById('headerAvatar').src = currentCharacter.image || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&h=100&fit=crop';
document.getElementById('scenarioTitle').textContent = `Scene: ${currentCharacter.name}`;
document.getElementById('scenarioDesc').textContent = currentCharacter.scene;
// Show scene setting if first message exists
if (currentCharacter.scene) {
document.getElementById('sceneMessage').classList.remove('hidden');
document.getElementById('sceneText').textContent = currentCharacter.scene;
}
// Add first message if exists
if (currentCharacter.firstMessage) {
addMessage('character', currentCharacter.firstMessage);
}
} else {
// Redirect to creator if no character
window.location.href = 'index.html';
}
}
// Parse message for actions (*text*)
function parseMessage(text) {
if (typeof text !== 'string' || !text) return '';
// Ultra-strict filtering of error strings
const cleaned = String(text).trim();
const forbidden = ['undefined', 'null', 'undefinednull', 'nullundefined', '[object Object]', 'NaN'];
if (forbidden.includes(cleaned) || cleaned.includes('undefined') && cleaned.includes('null')) {
console.error('Blocked error text:', cleaned);
return '*smiles*';
}
// Replace *text* with styled span
return cleaned.replace(/\*(.*?)\*/g, '<span class="action-text">$1</span>');
}
// Safe text validator - prevents undefinednull completely
function validateText(text, fallback = '*looks at you*') {
// Handle all falsy values
if (text === null || text === undefined) return fallback;
// Convert to string safely
let str;
try {
str = String(text);
} catch (e) {
return fallback;
}
// Check for error concatenations
const lower = str.toLowerCase().trim();
if (lower === 'undefined' || lower === 'null' || lower === 'undefinednull' ||
lower === 'nullundefined' || lower === '[object object]' || lower === '') {
console.error('Invalid text detected, using fallback:', str);
return fallback;
}
// Check for partial matches
if ((str.includes('undefined') && str.includes('null')) ||
str.includes('[object Object]')) {
return fallback;
}
return str;
}
// Add message to chat
function addMessage(sender, text, imageUrl = null) {
const container = document.getElementById('messagesContainer');
const isUser = sender === 'user';
const wrapper = document.createElement('div');
wrapper.className = `flex ${isUser ? 'justify-end' : 'justify-start'} mb-4`;
let content = '';
if (imageUrl) {
content = `<img src="${imageUrl}" class="rounded-lg max-w-sm shadow-lg border border-gray-700" alt="Generated">`;
} else {
// Use validator for maximum safety
const safeText = validateText(text, '*smiles warmly*');
const parsedText = parseMessage(safeText);
content = `<div class="message-bubble glass-panel ${isUser ? 'bg-pink-600/20 border-pink-500/30' : 'bg-gray-800/50'} rounded-2xl px-4 py-3 text-gray-100">${parsedText}</div>`;
}
wrapper.innerHTML = content;
container.appendChild(wrapper);
container.scrollTop = container.scrollHeight;
// Save to history - only save valid text
const validatedText = validateText(text, '');
const messageToSave = {
sender,
text: validatedText,
imageUrl,
timestamp: new Date()
};
// Don't save if text is empty after validation (unless it has an image)
if (validatedText || imageUrl) {
messages.push(messageToSave);
localStorage.setItem(`chat_${currentCharacter.id}`, JSON.stringify(messages));
}
}
// API KEYS - Replace with your own OpenRouter key at openrouter.ai/keys
const OPENROUTER_API_KEY = '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';
// Images use Pollinations.ai - FREE, no API key needed!
// REAL AI API Integration (OpenRouter)
async function getAIResponse(userMessage) {
// Check if API key is configured
if (!OPENROUTER_API_KEY || OPENROUTER_API_KEY === 'undefined') {
return "*looks confused* API key not configured properly...";
}
const systemPrompt = `You are ${currentCharacter.name}. ${currentCharacter.personality || ''}
Scene context: ${currentCharacter.scene || 'A casual conversation'}
Appearance: ${currentCharacter.appearance || 'Attractive'}
Respond in character. Use *asterisks* for physical actions. Keep responses concise (1-3 sentences) and engaging. Be flirty and playful.`;
try {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
'HTTP-Referer': window.location.href,
'X-Title': 'AI Companion Chat'
},
body: JSON.stringify({
model: 'nousresearch/hermes-4-70b',
messages: [
{ role: 'system', content: systemPrompt },
...messages.slice(-6).map(m => ({
role: m.sender === 'user' ? 'user' : 'assistant',
content: m.text || ''
})),
{ role: 'user', content: userMessage }
],
temperature: 0.9,
max_tokens: 500,
stream: false
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('OpenRouter Response:', data);
if (data.error) throw new Error(data.error.message || 'API returned error');
// Handle OpenRouter response format with safety checks
let responseText = '';
if (data.choices && data.choices[0]) {
if (data.choices[0].message && data.choices[0].message.content) {
responseText = data.choices[0].message.content;
} else if (data.choices[0].text) {
responseText = data.choices[0].text;
}
}
// Final validation before returning
if (!responseText || typeof responseText !== 'string') {
console.error('Invalid response text:', responseText, 'Full data:', data);
return "*connection seems fuzzy* Could you repeat that?";
}
// Check for error concatenation
if (responseText === 'undefined' || responseText === 'null' || responseText.includes('undefinednull')) {
return "*tilts head* I didn't catch that properly...";
}
return responseText;
} catch (error) {
console.error('API Error:', error);
return `*connection interrupted* [API Error: ${error.message}]`;
}
}
// Simulate AI Response (now with real API)
async function simulateResponse(userMessage) {
isTyping = true;
document.getElementById('typingIndicator').classList.remove('hidden');
let response = await getAIResponse(userMessage);
// Final validation layer
response = validateText(response, '*looks at you with a gentle smile* Sorry, I got distracted for a moment.');
isTyping = false;
document.getElementById('typingIndicator').classList.add('hidden');
addMessage('character', response);
}
// Handle send message
document.getElementById('chatForm').addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('messageInput');
const text = input.value.trim();
if (text && !isTyping) {
addMessage('user', text);
input.value = '';
simulateResponse(text);
}
});
// Quick action button
document.getElementById('quickAction').addEventListener('click', () => {
const input = document.getElementById('messageInput');
input.value = '*' + input.value + '*';
input.focus();
});
// Sidebar toggle
const sidebar = document.getElementById('sidebar');
document.getElementById('sidebarToggle').addEventListener('click', () => {
sidebar.classList.remove('-translate-x-full');
});
document.getElementById('closeSidebar').addEventListener('click', () => {
sidebar.classList.add('-translate-x-full');
});
// Load recent chats
function loadRecentChats() {
const recent = JSON.parse(localStorage.getItem('recentCharacters') || '[]');
const list = document.getElementById('recentChatsList');
list.innerHTML = '';
recent.forEach(char => {
const item = document.createElement('button');
item.className = 'sidebar-item w-full flex items-center gap-3 px-4 py-3 text-left';
if (currentCharacter && char.id === currentCharacter.id) {
item.classList.add('active-chat');
}
const imgUrl = char.image && char.image.startsWith('http') ? char.image : 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&h=100&fit=crop';
item.innerHTML = `
<img src="${imgUrl}" class="w-10 h-10 rounded-full object-cover border border-gray-700">
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-white truncate">${char.name}</h4>
<p class="text-xs text-gray-500 truncate">${char.scene || 'No scenario'}</p>
</div>
`;
item.addEventListener('click', () => {
localStorage.setItem('currentCharacter', JSON.stringify(char));
location.reload();
});
list.appendChild(item);
});
}
// Settings Modal
const settingsModal = document.getElementById('settingsModal');
document.getElementById('settingsBtn').addEventListener('click', () => {
settingsModal.classList.remove('hidden');
settingsModal.classList.add('flex');
});
document.getElementById('closeSettings').addEventListener('click', () => {
settingsModal.classList.add('hidden');
settingsModal.classList.remove('flex');
});
document.getElementById('saveSettings').addEventListener('click', () => {
settingsModal.classList.add('hidden');
settingsModal.classList.remove('flex');
alert('Settings saved! Using hardcoded API keys.');
});
// Pre-fill settings with hardcoded OpenRouter key for visibility
const openaiKeyInput = document.getElementById('openaiApiKey');
if (openaiKeyInput) openaiKeyInput.value = OPENROUTER_API_KEY;
// Remove SinkIn.ai key input if it exists - no longer needed!
const apiKeyInput = document.getElementById('apiKey');
if (apiKeyInput && apiKeyInput.parentElement) {
apiKeyInput.parentElement.remove();
}
// Scenario Modal
const scenarioModal = document.getElementById('scenarioModal');
document.getElementById('scenarioBtn').addEventListener('click', () => {
scenarioModal.classList.remove('hidden');
scenarioModal.classList.add('flex');
});
document.getElementById('closeScenario').addEventListener('click', () => {
scenarioModal.classList.add('hidden');
scenarioModal.classList.remove('flex');
});
// Pollinations.AI Image Generation - NO API KEY NEEDED!
// Supports NSFW, uses seeds for consistency, negative prompts
document.getElementById('imageGenBtn').addEventListener('click', async () => {
if (!currentCharacter) return;
// Get or create persistent seed for this character (ensures consistency)
if (!currentCharacter.imageSeed) {
currentCharacter.imageSeed = Math.floor(Math.random() * 1000000);
// Save updated character
localStorage.setItem('currentCharacter', JSON.stringify(currentCharacter));
}
const seed = currentCharacter.imageSeed;
// Get recent chat context
const recentMessages = messages.slice(-2);
let contextPrompt = '';
let situationDescription = '';
if (recentMessages.length > 0) {
let foundAction = null;
let lastCharMessage = null;
for (let i = recentMessages.length - 1; i >= 0; i--) {
const msg = recentMessages[i];
if (!msg || typeof msg !== 'object') continue;
const text = String(msg.text || '');
if (msg.sender === 'character' && !lastCharMessage) {
lastCharMessage = text;
}
const actionMatch = text.match(/\*(.*?)\*/);
if (actionMatch && actionMatch[1] && !foundAction) {
foundAction = actionMatch[1];
}
}
if (foundAction) {
situationDescription = foundAction;
contextPrompt = foundAction;
} else if (lastCharMessage) {
const shortMsg = lastCharMessage.substring(0, 100);
situationDescription = shortMsg;
contextPrompt = `reacting to conversation, natural expression, candid moment`;
} else {
contextPrompt = `in scene, natural pose, realistic lighting`;
}
} else {
situationDescription = `meeting`;
contextPrompt = `first encounter, initial meeting`;
}
// Add loading message
const loadingId = Date.now();
const container = document.getElementById('messagesContainer');
const wrapper = document.createElement('div');
wrapper.id = `loading-${loadingId}`;
wrapper.className = 'flex justify-start mb-4';
wrapper.innerHTML = `
<div class="message-bubble glass-panel bg-gray-800/50 rounded-2xl px-4 py-3 text-gray-400 flex items-center gap-2">
<div class="animate-spin w-4 h-4 border-2 border-pink-500 border-t-transparent rounded-full"></div>
<span>Generating image... (Seed: ${seed})</span>
</div>
`;
container.appendChild(wrapper);
container.scrollTop = container.scrollHeight;
try {
// Build shorter, cleaner prompt - Pollinations works better with concise prompts
const basePrompt = currentCharacter.appearance || 'beautiful portrait';
const shortContext = situationDescription.substring(0, 60);
// Keep prompt under 200 chars to avoid URL length issues
const combinedPrompt = `${basePrompt}, ${shortContext}, cyberrealistic, detailed, 8k, professional photo`.substring(0, 200);
const encodedPrompt = encodeURIComponent(combinedPrompt);
// Use simpler Pollinations URL format
const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?seed=${seed}&width=512&height=768&nologo=true&negative=blurry,lowquality,deformed`;
console.log('Pollinations URL length:', imageUrl.length);
console.log('URL:', imageUrl);
// Pollinations generates on-the-fly, use fetch to check if it's ready
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 15000); // 15s timeout
const response = await fetch(imageUrl, {
method: 'HEAD',
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok && response.status !== 200) {
throw new Error(`HTTP ${response.status}`);
}
const loadingEl = document.getElementById(`loading-${loadingId}`);
if (loadingEl) loadingEl.remove();
const displayContext = situationDescription.substring(0, 50) + (situationDescription.length > 50 ? '...' : '');
addMessage('character', `*${displayContext}*`, imageUrl);
} catch (error) {
console.error('Generation error:', error);
const loadingEl = document.getElementById(`loading-${loadingId}`);
if (loadingEl) {
loadingEl.innerHTML = `
<div class="message-bubble glass-panel bg-red-900/30 rounded-2xl px-4 py-3 text-red-300 text-sm">
<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i>
Image generation failed. Retrying with simpler prompt...
</div>
`;
}
// Simpler fallback - use generic portrait with seed
setTimeout(() => {
// Use HTTPS placeholder instead of Pollinations for fallback to avoid rate limits
const fallbackUrl = `https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=512&h=768&fit=crop&seed=${seed}`;
const loadingEl2 = document.getElementById(`loading-${loadingId}`);
if (loadingEl2) loadingEl2.remove();
addMessage('character', `*${situationDescription.substring(0, 40)}...*`, fallbackUrl);
}, 1000);
}
});
// Clear history
document.getElementById('clearHistory').addEventListener('click', () => {
if (confirm('Clear current chat history?')) {
messages = [];
localStorage.removeItem(`chat_${currentCharacter.id}`);
document.getElementById('messagesContainer').innerHTML = '';
document.getElementById('sceneMessage').classList.remove('hidden');
}
});
// Initialize
loadCharacter();
loadRecentChats();
// Load previous messages if exist
const messagesContainer = document.getElementById('messagesContainer');
if (currentCharacter) {
const savedMessages = localStorage.getItem(`chat_${currentCharacter.id}`);
if (savedMessages) {
try {
const parsed = JSON.parse(savedMessages);
if (Array.isArray(parsed)) {
// Clear current messages array before loading to prevent duplicates
messages = [];
parsed.forEach(msg => {
// Robust null/undefined check
if (!msg || typeof msg !== 'object') return;
// Use validator for strict checking
const safeText = validateText(msg.text, '');
// Skip empty messages without images
if (!safeText && !msg.imageUrl) return;
// Render without re-adding to array (we already set messages = parsed)
const isUser = msg.sender === 'user';
const wrapper = document.createElement('div');
wrapper.className = `flex ${isUser ? 'justify-end' : 'justify-start'} mb-4`;
let content = '';
if (msg.imageUrl) {
content = `<img src="${msg.imageUrl}" class="rounded-lg max-w-sm shadow-lg border border-gray-700" alt="Generated">`;
} else {
const parsedText = parseMessage(safeText);
content = `<div class="message-bubble glass-panel ${isUser ? 'bg-pink-600/20 border-pink-500/30' : 'bg-gray-800/50'} rounded-2xl px-4 py-3 text-gray-100">${parsedText}</div>`;
}
wrapper.innerHTML = content;
messagesContainer.appendChild(wrapper);
// Add to messages array manually to maintain state without triggering save
messages.push({
sender: msg.sender,
text: safeText,
imageUrl: msg.imageUrl,
timestamp: msg.timestamp || new Date()
});
});
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
} catch (e) {
console.error('Failed to load saved messages:', e);
localStorage.removeItem(`chat_${currentCharacter.id}`);
messages = [];
}
}
}
</script>
</body>
</html>