/* ───────────────────────────────────────────────────────────────────────────── */ /* AI GENERATION (FIXED) */ /* ───────────────────────────────────────────────────────────────────────────── */ async function generateWithAI() { const aiBtn = document.getElementById('aiGenBtn'); const aiOnlyMode = document.getElementById('aiOnlyMode')?.checked || false; aiBtn.disabled = true; aiBtn.textContent = aiOnlyMode ? 'DEEP GENERATING...' : 'THINKING...'; try { const genre = document.getElementById('genreSelect').value; const year = document.getElementById('yearInput').value; const genderReq = document.getElementById('genderSelect').value; const jobType = document.getElementById('jobType').value; const complexity = parseInt(document.getElementById('complexSelect').value); // Build prompt (same as before)... const prompt = `...`; // (Keep your existing prompt here) let persona; let rawResponse; // Try selected AI provider if (aiConfig.provider === 'ollama') { // Ollama implementation const response = await fetch(`${aiConfig.ollamaEndpoint}/api/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'llama3.2', prompt: prompt, stream: false, options: { temperature: aiOnlyMode ? 0.95 : 0.8, max_tokens: 2048 } }) }); if (!response.ok) throw new Error(`Ollama error: ${response.status}`); const data = await response.json(); rawResponse = data.response; } else if (aiConfig.provider === 'groq') { // Groq implementation if (!aiConfig.apiKey) throw new Error('Groq API key required'); const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { method: 'POST', headers: { 'Authorization': `Bearer ${aiConfig.apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'llama-3.3-70b-versatile', messages: [ { role: 'system', content: 'You are an expert character creator. Return only valid JSON.' }, { role: 'user', content: prompt } ], temperature: aiOnlyMode ? 0.95 : 0.8, max_tokens: 2048, response_format: { type: 'json_object' } }) }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(`Groq API error: ${response.status} - ${error.error?.message || 'Unknown error'}`); } const data = await response.json(); rawResponse = data.choices[0].message.content; } else if (aiConfig.provider === 'gemini') { // Fixed Gemini implementation if (!aiConfig.apiKey) throw new Error('Gemini API key required'); const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${aiConfig.apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { temperature: aiOnlyMode ? 0.95 : 0.8, topP: 0.95, maxOutputTokens: 2048, responseMimeType: "application/json" } }) } ); if (!response.ok) { if (response.status === 429) { throw new Error('Rate limited. Try again in a minute.'); } const error = await response.json().catch(() => ({})); throw new Error(`Gemini API error: ${response.status} - ${error.error?.message || 'Unknown error'}`); } const data = await response.json(); rawResponse = data.candidates[0].content.parts[0].text; } // Parse the AI response try { persona = JSON.parse(rawResponse); } catch (e) { // Try to extract JSON if wrapped in markdown const jsonMatch = rawResponse.match(/\{[\s\S]*\}/); if (jsonMatch) { persona = JSON.parse(jsonMatch[0]); } else { throw new Error('Invalid JSON response from AI'); } } // Validate required fields const requiredFields = ['name', 'species', 'job', 'appearance', 'items', 'background', 'secret', 'speech', 'personality']; const missingFields = requiredFields.filter(f => !persona[f] || (Array.isArray(persona[f]) && persona[f].length === 0)); if (missingFields.length > 0 && aiOnlyMode) { console.warn('AI missed fields:', missingFields); throw new Error(`Missing required fields: ${missingFields.join(', ')}`); } // Fill in missing fields with fallbacks (only in non-strict mode) if (!aiOnlyMode) { if (!persona.name) persona.name = `${pick(library.names[genderReq === 'any' ? 'Male' : genderReq])} · ${genderReq} · ${year}`; if (!persona.species) persona.species = generateSpecies(genre, genderReq); // Add other fallbacks as needed } // Format items if (Array.isArray(persona.items)) { persona.items = persona.items.map(i => { if (typeof i === 'string' && i.startsWith('·')) return i; return `· ${i}`; }).join('\n'); } // Add metadata const genderUsed = genderReq === 'any' ? pick(['Male', 'Female', 'Non-binary']) : genderReq; if (!persona.name.includes('·')) { persona.name = `${persona.name} · ${genderUsed} · ${year}`; } // Set as current persona currentPersona = persona; currentParams = { genre, year, gender: genderUsed, jobType, complexity }; // Render all fields for (const key in persona) { if (key.startsWith('_')) continue; const el = document.getElementById(`out-${key}`); if (el) { el.textContent = persona[key]; el.classList.remove('placeholder'); } } updateCharCount(); document.getElementById('dossierGrid').classList.add('visible'); document.getElementById('expBtn').disabled = false; document.getElementById('portraitBtn').disabled = false; document.querySelectorAll('.regenerate-btn').forEach(btn => btn.disabled = false); document.querySelectorAll('.speak-btn').forEach(btn => btn.disabled = false); saveToHistory(persona); showToast(`${aiOnlyMode ? 'Deep AI' : 'AI-assisted'} persona generated!`, 'success'); } catch (err) { console.error('AI generation failed:', err); showToast(`AI Error: ${err.message}`, 'error'); if (aiOnlyMode && confirm('AI generation failed. Use standard generator instead?')) { generate(); } } finally { aiBtn.disabled = false; aiBtn.textContent = 'AI GEN'; } } /* ───────────────────────────────────────────────────────────────────────────── */ /* PORTRAIT GENERATION (SECURITY IMPROVEMENT) */ /* ───────────────────────────────────────────────────────────────────────────── */ function sanitizePrompt(prompt) { // Remove any potentially dangerous characters return prompt.replace(/[<>{}[\]\\]/g, '').substring(0, 500); } async function generatePortrait() { if (!currentPersona) { showToast('Generate a persona first', 'error'); return; } const modal = document.getElementById('portraitModal'); const container = document.getElementById('portraitImageContainer'); modal.classList.remove('hidden'); container.innerHTML = '