diff --git a/apps/Gemini Nano/chat.js b/apps/Gemini Nano/chat.js index 921e7f1..8db068b 100644 --- a/apps/Gemini Nano/chat.js +++ b/apps/Gemini Nano/chat.js @@ -1,4 +1,3 @@ - let personas = []; let isPaused = false; // This will control whether the interactions are paused or active @@ -38,10 +37,17 @@ async function generatePersonaName(personality, role) { const prompt = `Suggest a single, suitable name for a persona characterized as '${personality}' and whose profession is '${role}'. The name should be creative and reflect these attributes. Please provide only the name without any explanations or additional text.`; try { - const response = await aiPrompt('GeminiNano', prompt); - const suggestedName = response.split('\n')[0].trim(); // Takes only the first line to ensure it's just the name - console.log(`AI suggested name: ${suggestedName}`); - return suggestedName; + const response = await aiPrompt(prompt); + // Check if the response is defined and has a valid format before splitting + if (response && typeof response === 'string') { + const suggestedName = response.split('\n')[0].trim(); // Takes only the first line to ensure it's just the name + console.log(`AI suggested name: ${suggestedName}`); + return suggestedName; + } else { + // Handle cases where response is undefined or not a string + console.error('Invalid response format:', response); + return null; + } } catch (error) { console.error('Error generating persona name:', error); return null; // Return null if there's an error @@ -121,70 +127,40 @@ function appendBroadcastToUI(persona, event) { container.appendChild(personaDiv); } -function updateActionUI(persona, action) { +function updateActionUI(persona, message, isReaction = false) { + if (!message) { + console.error("Attempted to update UI with undefined or null message."); + return; // Prevent further processing + } + + const container = document.getElementById('personas-content'); const actionDiv = document.createElement('div'); - const parsedAction = marked.parse(action); // Convert Markdown to HTML - - // Determine the correct container based on whether the update is for a persona or the Game Master - let container = persona ? document.getElementById('personas-content') : document.getElementById('gmUpdatesList'); + const lines = message.split('\n'); + const firstLine = lines.shift(); // Remove and return the first line from the array + const rest = lines.join('\n'); // Join the remaining lines back into a single string - // Prepare the content format depending on the target - if (persona) { - // For persona updates, display with persona's name - actionDiv.innerHTML = `${persona.name} action: ${parsedAction}`; + if (isReaction) { + actionDiv.innerHTML = `Reaction to ${persona.name}'s action: ${firstLine}${rest ? `
${rest}` : ''}`; // Add the reaction header } else { - // For Game Master updates, use list item tags to match the list format - actionDiv.innerHTML = `
  • ${parsedAction}
  • `; + actionDiv.innerHTML = `${firstLine}${rest ? `
    ${rest}` : ''}`; // Add the action header } container.appendChild(actionDiv); } -async function gatherAndProcessActions() { - if (isPaused) { - console.log("Action processing is paused."); - return; // Exit the function if the system is paused - } - - let actions = []; - for (let persona of personas) { - const action = await promptForAction(persona); - actions.push({ persona: persona.name, action: action }); - persona.logEvent(`Action decided: ${action}`); - console.log(`${persona.name} took the action: ${action}`); - updateActionUI(persona, action); - } - - shuffleArray(actions); - - for (let action of actions) { - if (isPaused) { - console.log("Processing paused during action handling."); - return; // Check again if paused during processing loop - } - await processAction(action); - } -} - -function shuffleArray(array) { - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; // ES6 destructuring swap - } -} - async function promptForAction(persona) { const historySummary = persona.history.slice(-5).join(' '); const attributesDescription = `I am ${persona.name}, characterized as ${persona.attributes.personality} and professionally, I am a ${persona.attributes.role}.`; - - const prompt = `Pretend you are ${persona.name}, a ${persona.attributes.personality} ${persona.attributes.role}. Based on your attributes and recent history: ${historySummary}, what specific actions are you planning to take next? How will you approach your next challenges or opportunities? Please respond in just 2 or 3 sentences.`; + const prompt = `${attributesDescription} Given my attributes and recent history: ${historySummary}, what actions am I planning to take next? How will I approach my next challenges or opportunities? Respond as if you are the persona. Please provide a short response in 2 or 3 sentences.`; try { - const result = await aiPrompt('GeminiNano', prompt); - persona.history.push(`${result}`); - return result; + const result = await aiPrompt(prompt); + const trimmedResult = result.split('\n')[0].trim(); // Take only the first line to ensure it's just the name + persona.history.push(`${trimmedResult}`); + updateActionUI(persona, trimmedResult); // Update UI with the action + return trimmedResult; } catch (error) { - console.error('Error prompting AI for:', error); + console.error('Error prompting AI for action:', error); return "Error in AI interaction"; } } @@ -193,15 +169,25 @@ async function processAction(action) { for (let persona of personas) { if (persona.name === action.persona) continue; // Skip the actor - const reactionPrompt = `I am ${persona.name}, a ${persona.attributes.personality} ${persona.attributes.role}. Given the action '${action.action}' by ${action.persona}, how should I react?`; + const reactionPrompt = `Given my attributes and recent history: I am ${persona.name}, characterized as ${persona.attributes.personality} and professionally, I am a ${persona.attributes.role}. Considering the action '${action.action}' by ${action.persona}, how should I react? Please provide a short response in 2 or 3 sentences.`; try { - const reaction = await aiPrompt('GeminiNano', reactionPrompt); - persona.logEvent(`My reaction to ${action.persona}'s action: ${reaction}`); - updateActionUI(persona, `My reaction to ${action.persona}'s action: ${reaction}`); - - if (shouldInformGameMaster(reaction)) { - await informGameMaster(action, reaction); + const reaction = await aiPrompt(reactionPrompt); // Prompt for reaction + const trimmedReaction = reaction.split('\n')[0].trim(); // Ensure we use only the first line + persona.logEvent(`Reaction to ${action.persona}'s action: ${trimmedReaction}`); + updateActionUI(persona, `Reaction to ${action.persona}'s action: ${marked.parse(trimmedReaction)}`, true); + + const communicationPrompt = `Does the following statement involve communication with another persona? Response: "${trimmedReaction}". Please answer 'yes' or 'no'.`; + const involvesCommunicationHtml = await aiPrompt(communicationPrompt); + const involvesCommunication = extractText(involvesCommunicationHtml); + + if (involvesCommunication.toLowerCase().trim() === 'yes') { + const recipientPersona = personas.find(p => p.name === action.persona); // Find the recipient persona object + if (recipientPersona) { + await handleCommunication(persona, recipientPersona, 3); // Pass the full recipient persona object + } + } else { + await informGameMaster(action, trimmedReaction); // Inform Game Master if it's not communication } } catch (error) { console.error(`Error prompting AI for ${persona.name}:`, error); @@ -210,56 +196,49 @@ async function processAction(action) { } } -async function isCommunication(reaction) { - // Construct a prompt asking the AI if the reaction involves communication - const prompt = `Does the following statement involve communication with another persona? Response: "${reaction}". Please answer 'yes' or 'no'.`; - - try { - const response = await aiPrompt('GeminiNano', prompt); // Using the aiPrompt function to query the AI - console.log('AI determined:', response.trim().toLowerCase()); - return response.trim().toLowerCase() === 'yes'; - } catch (error) { - console.error('Error querying AI to determine if the reaction is communication:', error); - return false; // Assume no communication if there's an error +async function gatherAndProcessActions() { + if (isPaused) { + console.log("System is paused."); + return; } -} -async function shouldInformGameMaster(reaction) { - // Use the AI to determine if the reaction is communicative - const isComm = await isCommunication(reaction); - return !isComm; // Inform the Game Master if it is not a communication + let actions = []; + for (let persona of personas) { + let action = await promptForAction(persona); + actions.push({ persona: persona.name, action: action }); + persona.logEvent(`Action decided: ${action}`); + } + + shuffleArray(actions); + + for (let action of actions) { + await processAction(action); + } } -async function processAction(action) { +async function startActions() { for (let persona of personas) { - const reaction = await aiPrompt('GeminiNano', `Reaction prompt for ${persona.name}`); - persona.logEvent(`My reaction: ${reaction}`); - updateActionUI(persona, reaction); - - if (await shouldInformGameMaster(reaction)) { - await informGameMaster(action, reaction); - } + // Prompt each persona for an action + const action = await promptForAction(persona); + persona.logEvent(`Action taken: ${action}`); + updateActionUI(persona, action); } + await gatherAndProcessActions(); // Start processing actions after gathering them } async function informGameMaster(action, reaction) { - // Assume some mechanism to inform the Game Master - const gameMasterContext = `Game Master needs to update environment based on: ${action.action} which led to: ${reaction}`; - const environmentUpdate = await aiPrompt('GeminiNano', gameMasterContext); - - broadcastEvent(`Game Master has updated the environment: ${environmentUpdate}`); - updateActionUI(null, environmentUpdate); // Assuming updateActionUI can handle null persona for GM updates -} + const gmPrompt = `Game Master needs to update environment based on: ${action.action} which led to: ${reaction}. Provide context and consequences for this update.`; + const gmUpdate = await aiPrompt(gmPrompt); -function broadcastEvent(event) { - personas.forEach(persona => { - persona.logEvent(`Broadcast from Game Master: ${event}`); - }); - updateUI(); // Update the UI with the new game master's broadcast + const gmUpdatesList = document.getElementById('gmUpdatesList'); + const updateItem = document.createElement('li'); + updateItem.innerHTML = `Game Master Update: ${marked.parse(gmUpdate)}`; + gmUpdatesList.appendChild(updateItem); + + broadcastEvent(`Game Master Update: ${gmUpdate}`); } function extractText(html) { - // Use DOMParser to parse HTML string and extract text const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); return doc.body.textContent || ""; // Get plain text content from parsed HTML @@ -267,38 +246,33 @@ function extractText(html) { async function handleCommunication(initiator, recipient, exchanges) { let exchangeCount = 0; - let lastMessage = ""; // To hold the last message for reference in the next prompt + let lastMessage = ""; // To hold the last message for reference in the next prompt while (exchangeCount < exchanges) { - // Gathering the necessary context const initiatorHistory = initiator.history.slice(-5).join(' '); // Last 5 entries from initiator's history const recipientHistory = recipient.history.slice(-5).join(' '); // Last 5 entries from recipient's history - const initiatorAttributes = `${initiator.name} is a ${initiator.attributes.personality}, ${initiator.attributes.role}.`; - const recipientAttributes = `${recipient.name} is a ${recipient.attributes.personality}, ${recipient.attributes.role}.`; + const initiatorAttributes = `Attributes of ${initiator.name}: ${initiator.attributes.personality}, ${initiator.attributes.role}.`; + const recipientAttributes = `Attributes of ${recipient.name}: ${recipient.attributes.personality}, ${recipient.attributes.role}.`; - // Create the prompt for the initiator with a brevity directive - let initiatorPrompt; + let initiatorPrompt = `As ${initiator.name}, respond to ${recipient.name} who previously said: "${lastMessage}". Please role-play according to your attributes: ${initiatorAttributes} Recent interactions include: ${initiatorHistory}. Keep it short, 2 or 3 sentences.`; if (exchangeCount === 0) { - initiatorPrompt = `As ${initiator.name}, start a conversation with ${recipient.name}. You are a ${initiator.attributes.personality}, ${initiator.attributes.role}. Recent history includes: ${initiatorHistory}. Respond in just 2 or 3 sentences.`; - } else { - initiatorPrompt = `As ${initiator.name}, respond to ${recipient.name} who just said: "${lastMessage}". You are a ${initiator.attributes.personality}, ${initiator.attributes.role}. Recent history includes: ${initiatorHistory}. Keep your response to 2 or 3 sentences.`; + initiatorPrompt = `As ${initiator.name}, start a conversation with ${recipient.name}. Reflect your personality and role: ${initiatorAttributes} Recent interactions include: ${initiatorHistory}. Keep it short, 2 or 3 sentences.`; } try { - // Initiator speaks - const initiatorMessage = await aiPrompt('GeminiNano', initiatorPrompt); - initiator.logEvent(initiatorMessage); - updateActionUI(initiator, initiatorMessage); - lastMessage = initiatorMessage; // Update lastMessage to the most recent one - - // Prepare the prompt for the recipient with a similar brevity directive - const recipientPrompt = `As ${recipient.name}, respond to ${initiator.name} who just said: "${lastMessage}". You are a ${recipient.attributes.personality}, ${recipient.attributes.role}. Recent history includes: ${recipientHistory}. Please keep your response to just 2 or 3 sentences.`; + const initiatorMessage = await aiPrompt(initiatorPrompt); + const parsedInitiatorMessage = marked.parse(initiatorMessage); // Parse Markdown response + initiator.logEvent(parsedInitiatorMessage); + updateActionUI(initiator, parsedInitiatorMessage); + lastMessage = initiatorMessage; + + const recipientPrompt = `As ${recipient.name}, respond to ${initiator.name} who just said: "${lastMessage}". Please role-play according to your attributes: ${recipientAttributes} Recent interactions include: ${recipientHistory}. Keep it short, 2 or 3 sentences.`; - // Recipient responds - const recipientMessage = await aiPrompt('GeminiNano', recipientPrompt); - recipient.logEvent(recipientMessage); - updateActionUI(recipient, recipientMessage); - lastMessage = recipientMessage; // Update lastMessage to the most recent one + const recipientMessage = await aiPrompt(recipientPrompt); + const parsedRecipientMessage = marked.parse(recipientMessage); // Parse Markdown response + recipient.logEvent(parsedRecipientMessage); + updateActionUI(recipient, parsedRecipientMessage); + lastMessage = recipientMessage; exchangeCount += 1; } catch (error) { @@ -308,37 +282,125 @@ async function handleCommunication(initiator, recipient, exchanges) { } } -let aiSession = null; // Global variable to hold the session +function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; // ES6 destructuring swap + } +} + +function togglePauseResume() { + isPaused = !isPaused; + document.getElementById('pauseResumeButton').textContent = isPaused ? 'Resume' : 'Pause'; +} + +// Event Listeners +document.getElementById('createPersonaButton').addEventListener('click', createPersona); +document.getElementById('broadcastButton').addEventListener('click', handleBroadcast); +document.getElementById('start-process-actions-button').addEventListener('click', gatherAndProcessActions); +document.getElementById('pauseResumeButton').addEventListener('click', togglePauseResume); + +// AI Interaction function +let aiSession = null; // Will hold the session object for Gemini + +async function aiPrompt(prompt, retries = 3) { + const model = document.getElementById('apiChoice').value || 'gemini'; // Default to Gemini Nano + const apiKey = document.getElementById('apiKey').value; -async function aiPrompt(model, prompt, retries = 3) { while (retries > 0) { try { - if (!aiSession) { // Check if session exists - if (await window.ai.canCreateTextSession() === "no") { - throw new Error("Unable to create an AI session."); - } - aiSession = await window.ai.createTextSession(); // Create a session if none + switch (model) { + case 'gemini': + if (!aiSession) { // Check if session exists for Gemini + if (await window.ai.canCreateTextSession() === "no") { + throw new Error("Unable to create an AI session."); + } + aiSession = await window.ai.createTextSession(); // Create session if none exists + } + console.log('Sending prompt to GeminiNano:', prompt); + const resultGemini = await aiSession.prompt(prompt); + if (!resultGemini) throw new Error("Received undefined result from GeminiNano."); + console.log('Received response from GeminiNano:', resultGemini); + return resultGemini; + + case 'cohere': + if (!apiKey) { + throw new Error("API key for Cohere is not provided."); + } + console.log('Sending prompt to Cohere:', prompt); + return await callCohere(apiKey, prompt); + + default: + throw new Error('Unsupported AI model specified'); } - - console.log('Sending prompt to AI:', prompt); // Log the prompt for debugging - const result = await aiSession.prompt(prompt); - console.log('Received response from AI:', result); // Log the response for debugging - return result; // Return the AI's response } catch (error) { - console.error('Attempt failed in aiPrompt:', error); // Log attempt failure - retries -= 1; // Decrement the retry counter + console.error(`Attempt failed in aiPrompt using ${model}:`, error); + retries -= 1; if (retries <= 0) { - throw new Error("Error during AI interaction after several attempts"); + throw new Error(`Error during AI interaction after several attempts with ${model}`); } console.log(`Retrying... attempts left: ${retries}`); } } } -function togglePauseResume() { - isPaused = !isPaused; // Toggle the pause state - document.getElementById('pauseResumeButton').textContent = isPaused ? 'Resume' : 'Pause'; // Update button text based on state - console.log(isPaused ? "System is paused." : "System is resumed."); -} +async function callCohere(apiKey, fullPrompt, retries = 3) { + const url = 'https://api.cohere.com/v1/generate'; // Correct API endpoint for generation + let delay = 2000; // Start with a 2-second delay + const delayIncreaseFactor = 2.5; // Increase the delay more aggressively + + const data = { + "model": "command", // Ensure the correct model is specified + "prompt": fullPrompt, + "max_tokens": 50, // Adjust based on the expected length of the response + "temperature": 0.5, + "k": 0, + "p": 0.75, + "frequency_penalty": 0, + "presence_penalty": 0, + "stop_sequences": ["\n"] // Assuming the response should stop at the first newline + }; -document.getElementById('start-process-actions-button').addEventListener('click', gatherAndProcessActions); + while (retries > 0) { + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + 'Authorization': 'Bearer ' + apiKey + }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + const errorDetails = await response.text(); // Try to get more details from the response + console.error(`API returned non-ok status: ${response.status}, Details: ${errorDetails}`); + + if (response.status === 400) { + throw new Error(`Bad request to API: ${errorDetails}`); + } else if (response.status === 429) { + console.error(`Rate limit exceeded, retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + delay *= delayIncreaseFactor; // Increase the delay exponentially + retries--; // Decrement retries counter + continue; // Skip the rest of the loop and retry + } else { + throw new Error(`API returned non-ok status: ${response.status}`); + } + } + + const result = await response.json(); + if (result && result.generations && result.generations.length > 0 && result.generations[0].text) { + console.log('Received response from Cohere:', result.generations[0].text); + return result.generations[0].text.trim(); // Return the trimmed text + } else { + throw new Error("No valid text found in Cohere response."); + } + } catch (error) { + console.error('Cohere API Request Error:', error); + retries--; // Decrement retries counter + if (retries <= 0) throw new Error("Failed after multiple retries."); // Fail after all retries are exhausted + } + } +} diff --git a/apps/Gemini Nano/index.html b/apps/Gemini Nano/index.html index 1163ce7..efcd166 100644 --- a/apps/Gemini Nano/index.html +++ b/apps/Gemini Nano/index.html @@ -2,30 +2,43 @@ - Persona Chat + + AI Persona Interaction -
    - +
    + + +
    + + +
    - - - - - + + +
    -
    -

    Game Master Updates

    + + + +
    +

    Game Master Updates

      -
      -

      Personas Interactions

      -
      + +
      +

      Persona Interactions

      - + + diff --git a/apps/Gemini Nano/script.js b/apps/Gemini Nano/script.js new file mode 100644 index 0000000..8b2cd87 --- /dev/null +++ b/apps/Gemini Nano/script.js @@ -0,0 +1,8 @@ +document.getElementById('apiChoice').addEventListener('change', function () { + const apiKeyContainer = document.getElementById('apiKeyContainer'); + if (this.value === 'cohere') { + apiKeyContainer.style.display = 'block'; + } else { + apiKeyContainer.style.display = 'none'; + } +});