From 950e33adc2067a3255106b2d77700e5cfd89d5c9 Mon Sep 17 00:00:00 2001 From: Ken Kahn Date: Mon, 3 Jun 2024 18:18:08 +0200 Subject: [PATCH] Integrated Gemini Nano into Connections game --- apps/Connections-v2/index.html | 19 ++- apps/Connections-v2/script.js | 229 +++++++++++++++++++++------------ 2 files changed, 160 insertions(+), 88 deletions(-) diff --git a/apps/Connections-v2/index.html b/apps/Connections-v2/index.html index 7e3aa2c..1c01b6c 100644 --- a/apps/Connections-v2/index.html +++ b/apps/Connections-v2/index.html @@ -30,28 +30,33 @@

How to Play Connections

  • Instructions: Lost? Click the "Instructions" button anytime to review the rules.
  • -

    This app was created by conversing with ChatGPT 4 (August 3, 2023 version).

    +

    This app was created by conversing with ChatGPT 4 (August 3, 2023 version). + It was subsequently enhanced using ChatGPT 4 in May 2024 to support cohere.ai API and Gemini Nano. +

    -
    - - -
    - - + + +
    +
    diff --git a/apps/Connections-v2/script.js b/apps/Connections-v2/script.js index 8cd1bad..83f11d8 100644 --- a/apps/Connections-v2/script.js +++ b/apps/Connections-v2/script.js @@ -159,25 +159,90 @@ function constructFullPrompt(userPrompt) { return [userPrompt, ...jsonStructure].join("\n"); } -document.getElementById('initializeGame').addEventListener('click', function() { +document.getElementById('initializeGame').addEventListener('click', async function() { const apiKey = document.getElementById('apiKey').value; - if (!apiKey) { - // Notify the user to enter an API key - updateStatusMessage('Please enter an API key.', 'invalid-message'); - return; - } - const apiChoice = document.getElementById('apiChoice').value; const userPrompt = document.getElementById('gpt4Prompt').value; const fullPrompt = constructFullPrompt(userPrompt); - if (apiChoice === 'openai') { - callAPI(apiKey, fullPrompt, callOpenAI); - } else if (apiChoice === 'cohere') { - callAPI(apiKey, fullPrompt, callCohere); + showLoadingIcon(); + + try { + let result; + switch (apiChoice) { + case 'openai': + result = await callOpenAI(apiKey, fullPrompt); + break; + case 'cohere': + result = await callCohere(apiKey, fullPrompt); + break; + case 'gemini': + result = await geminiNano(fullPrompt); + if (result === undefined) { // Assume geminiNano() handles its own errors + throw new Error("Session creation or prompt failed in Gemini Nano."); + } + break; + default: + throw new Error("Unsupported AI provider selected."); + } + handleApiResponse(result); + } catch (error) { + // Specific error handling for Gemini Nano or other API errors + console.error('Error during API interaction:', error); + // Only update the status message if it's not already set by the API-specific function + if (document.getElementById('statusMessage').textContent === "") { + updateStatusMessage(error.message, "invalid-message"); + } + } finally { + hideLoadingIcon(); + } +}); + +document.getElementById('apiChoice').addEventListener('change', function() { + var selectedAI = this.value; + var apiKeyContainer = document.getElementById('apiKeyContainer'); + if (selectedAI === 'openai' || selectedAI === 'cohere') { + apiKeyContainer.style.display = 'block'; // Show API key input for OpenAI or Cohere + } else { + apiKeyContainer.style.display = 'none'; // Hide for Gemini or no selection } }); +function handleApiResponse(result) { + console.log("API response:", result); // Log the API result for debugging purposes + + if (result) { + let jsonSubstring = ""; + try { + // Normalize the JSON string by removing potentially problematic control characters + result = result.replace(/\r?\n|\r|\t/g, '').trim(); + + // Handle case-insensitive 'json' block detection and removal of backticks + const jsonStart = result.toLowerCase().indexOf('```json') + 7; // Use toLowerCase() for case-insensitive matching + if (jsonStart > 6) { // Check if '```json' was actually found + const jsonEnd = result.indexOf('```', jsonStart); // Find the end index of the JSON data + jsonSubstring = result.substring(jsonStart, jsonEnd).trim(); // Extract the JSON string + jsonSubstring = jsonSubstring.replace(/```/g, '').trim(); // Ensure removal of any backticks around JSON + } else { + // Assume the response is plain JSON if no markdown backticks found + jsonSubstring = result.trim(); + } + + console.log("Processed JSON string before parsing:", jsonSubstring); // Log the JSON string for debugging + + // Parse the JSON string + categories = JSON.parse(jsonSubstring); + initGame(); // Initialize the game with new categories + } catch (error) { + console.error('Error parsing JSON:', error, 'JSON String:', jsonSubstring); + updateStatusMessage("Failed to parse JSON from the API response.", "invalid-message"); + } + } else { + console.error("No valid response from AI."); + updateStatusMessage("No valid response from AI.", "invalid-message"); + } +} + function updateStatusMessage(message, className) { const statusMessage = document.getElementById('statusMessage'); statusMessage.textContent = message; @@ -186,13 +251,13 @@ function updateStatusMessage(message, className) { function constructFullPrompt(userPrompt) { const jsonStructure = [ - 'The output should be in valid JSON format with double quotes, like so:', + 'The output should be 4 categories in valid JSON format with double quotes, like so:', '{', ' "FISH": ["Bass", "Flounder", "Salmon", "Trout"], ', ' "PLANETS": ["Earth", "Mars", "Jupiter", "Venus"], ', ' "COLORS": ["Red", "Blue", "Green", "Yellow"], ', ' "FRUITS": ["Apple", "Banana", "Cherry", "Date"]', - '}. ' + '}. Be sure to quote each entry properly.' ]; return [userPrompt, ...jsonStructure].join("\n"); } @@ -212,54 +277,39 @@ function hideLoadingIcon() { } function callOpenAI(apiKey, fullPrompt) { - // Implementation of the OpenAI API call using fullPrompt console.log("Calling OpenAI with prompt:", fullPrompt); - // Show loading icon when starting the API call showLoadingIcon(); - // Prepare the fetch API call for GPT-4 - fetch('https://api.openai.com/v1/chat/completions', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - model: "gpt-4", - messages: [ - {"role": "system", "content": "You are a helpful assistant."}, - { - "role": "user", - "content": fullPrompt - } - ] - }) + return fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: "gpt-4", + messages: [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": fullPrompt} + ] }) - .then(response => response.json()) - .then(data => { - // Hide loading icon when API call is successful - hideLoadingIcon(); - console.log(data.choices[0].message.content); - // The output is already in JSON format, so you can directly access its properties - // const jsonString = data.choices[0].message.content.replace(/'/g, '"'); - categories = JSON.parse(data.choices[0].message.content); // Update the 'categories' object - initGame(); // Call the existing initGame logic to refresh the game - }).catch(error => { - console.error('Error:', error); - // Hide loading icon when API call fails - loadingIcon.className = 'loading-icon-hidden'; - // Inform the user - const statusMessage = document.getElementById('statusMessage'); - statusMessage.textContent = 'Error parsing JSON from GPT-4.'; - statusMessage.className = 'invalid-message'; - - // Ask whether to try again - const tryAgain = window.confirm('An error occurred while parsing the JSON data. Would you like to try again?'); - if (tryAgain) { - // Trigger the game initialization again (you may want to wrap this in a function) - document.getElementById('initializeGame').click(); - } - }); + }) + .then(response => response.json()) + .then(data => { + if (data.choices && data.choices.length > 0) { + return data.choices[0].message.content; // Return the response content directly + } else { + throw new Error("Invalid response format from OpenAI."); + } + }) + .catch(error => { + console.error('Error:', error); + updateStatusMessage('Error communicating with OpenAI.', 'invalid-message'); + throw error; // Re-throw to be caught by caller + }) + .finally(() => { + hideLoadingIcon(); + }); } function callCohere(apiKey, fullPrompt) { @@ -272,7 +322,7 @@ function callCohere(apiKey, fullPrompt) { "message": "Generate 4 categories of words with 4 unique words each, formatted as JSON." }; - fetch(url, { + return fetch(url, { method: 'POST', headers: { 'accept': 'application/json', @@ -283,34 +333,19 @@ function callCohere(apiKey, fullPrompt) { }) .then(response => response.json()) .then(data => { - try { - let jsonSubstring; - // Check if the response is embedded in additional text - if (data.text && data.text.includes('```json')) { - const jsonStart = data.text.indexOf('```json') + 7; // Finds start of JSON code block - const jsonEnd = data.text.indexOf('```', jsonStart); // Finds end of JSON code block - jsonSubstring = data.text.substring(jsonStart, jsonEnd).trim(); // Extracts the JSON substring - } else if (data.text) { - jsonSubstring = data.text.trim(); // Direct JSON - } - - if (jsonSubstring) { - categories = JSON.parse(jsonSubstring); // Parses the JSON to the categories object - initGame(); // Re-initializes the game with new categories - } else { - throw new Error("No JSON data found in response."); - } - } catch (error) { - console.error('Error parsing JSON:', error); - updateStatusMessage('Failed to parse JSON from Cohere response.', 'invalid-message'); - } finally { - hideLoadingIcon(); + if (data.text) { + return data.text; // Return the response text directly + } else { + throw new Error("No valid JSON data found in Cohere response."); } }) .catch(error => { console.error('API Request Error:', error); - hideLoadingIcon(); updateStatusMessage('Error making API request to Cohere.', 'invalid-message'); + throw error; // Re-throw to be caught by caller + }) + .finally(() => { + hideLoadingIcon(); }); } @@ -321,6 +356,38 @@ function updateGameWithCategories(categories) { initGameWithNewCategories(categories); } +let aiSession = null; // Global variable to hold the session + +async function geminiNano(prompt) { + if (!aiSession) { // Check if session exists + try { + let canCreateSession = await window.ai.canCreateTextSession(); + if (canCreateSession === "no") { + // Inform the user and provide a link to the documentation + const statusMessage = document.getElementById('statusMessage'); + statusMessage.innerHTML = 'Unable to create an AI session. Please refer to the documentation for more information.'; + statusMessage.className = 'invalid-message'; + throw new Error("Session creation denied."); // Abort the function by throwing an error + } + aiSession = await window.ai.createTextSession(); // Create a session if possible + } catch (error) { + // console.error('Error checking session capability:', error); + const statusMessage = document.getElementById('statusMessage'); + statusMessage.innerHTML = 'Error checking AI session capability. Please refer to the documentation for troubleshooting.'; + statusMessage.className = 'invalid-message'; + throw error; // Re-throw to ensure the caller knows the function has failed + } + } + + try { + const result = await aiSession.prompt(prompt); + return result; // Return the AI's response + } catch (error) { + console.error('Error in aiPrompt:', error); // Log errors in aiPrompt + throw new Error("Error during AI interaction"); + } +} + // Get the modal var modal = document.getElementById("instructionsModal");