File size: 32,874 Bytes
f112559 c13e4ae 231ae6d 3d5188c c13e4ae 91d0941 d9f7e10 3d5188c 231ae6d ee4abc0 3d5188c ee4abc0 d9f7e10 231ae6d ee4abc0 d9f7e10 91d0941 3d5188c d9f7e10 3d5188c d9f7e10 3d5188c c13e4ae 3d5188c c13e4ae 3d5188c d9f7e10 3d5188c c13e4ae 662ab87 3d5188c c13e4ae 3d5188c c13e4ae f112559 3d5188c 662ab87 231ae6d ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 662ab87 ee4abc0 c13e4ae 3d5188c c13e4ae 3d5188c 231ae6d c13e4ae 3d5188c c13e4ae 3d5188c 231ae6d 3d5188c c13e4ae 3d5188c 231ae6d 3d5188c c13e4ae 3d5188c c13e4ae 3d5188c c13e4ae 3d5188c d9f7e10 3d5188c d9f7e10 f112559 d9f7e10 c13e4ae d9f7e10 3d5188c d9f7e10 3d5188c d9f7e10 3d5188c d9f7e10 3d5188c d9f7e10 3d5188c d9f7e10 c13e4ae d9f7e10 c13e4ae d9f7e10 3d5188c d9f7e10 3d5188c c13e4ae d9f7e10 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 |
// 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 = `
<div class="flex gap-4 justify-end">
<div class="text-right">
<div class="font-medium text-accent mb-2">You</div>
<div class="bg-primary/20 rounded-2xl rounded-tr-none p-4">
<p><i>Generating an image of the current scene using local AI model...</i></p>
<div id="imageGenContainer" style="margin-top: 1rem;"></div>
</div>
<div class="text-xs text-gray-500 mt-2">${new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</div>
</div>
<img src="https://static.photos/people/48x48/10" class="w-12 h-12 rounded-full flex-shrink-0">
</div>
`;
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 = `
<div class="flex gap-4">
<img src="${avatar}" class="w-12 h-12 rounded-full flex-shrink-0">
<div>
<div class="font-medium text-primary mb-1">${characterName}</div>
<div class="bg-surface-light rounded-2xl rounded-tl-none p-4 hovered-element">
<p class="hovered-element">Here's an image generated from your message using our built-in AI image model:</p>
<div class="image-gen-preview mt-3 hovered-element">
<img src="${imageUrl}" alt="Generated image" class="rounded-lg hovered-element" style="max-width: 100%; height: auto;">
</div>
<p class="text-sm text-gray-400 mt-2 hovered-element">Prompt: "${prompt.substring(0, 100)}..."</p>
</div>
<div class="text-xs text-gray-500 mt-2">${time}</div>
</div>
</div>
`;
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 = `
<div class="flex gap-4 ${type === 'user' ? 'justify-end' : ''}">
${type !== 'user' ? `<img src="${avatar}" class="w-12 h-12 rounded-full flex-shrink-0">` : ''}
<div class="${type === 'user' ? 'text-right' : ''}">
<div class="font-medium ${nameColor} mb-1">${name}</div>
<div class="${type === 'user' ? 'bg-primary/20 rounded-2xl rounded-tr-none' : 'bg-surface-light rounded-2xl rounded-tl-none'} p-4">
<p>${content}</p>
</div>
<div class="text-xs text-gray-500 mt-2">${time}</div>
</div>
${type === 'user' ? `<img src="${avatar}" class="w-12 h-12 rounded-full flex-shrink-0">` : ''}
</div>
`;
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 = `
<img src="${document.querySelector('.bg-surface.rounded-xl.p-5 img')?.src || 'https://static.photos/people/48x48/5'}" class="w-12 h-12 rounded-full flex-shrink-0">
<div>
<div class="font-medium text-primary mb-1">${document.getElementById('characterName').textContent}</div>
<div class="bg-surface-light rounded-2xl rounded-tl-none p-4 flex gap-1 items-center">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<span class="ml-2 text-sm text-gray-400">Thinking...</span>
</div>
</div>
`;
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');
});
|