Even if you are not coder, it is important to know the difference between the very significant work that goes into making a Large Language model such as ChatGPT, Gemini, Claude or Deepseek and a wrapper which calls upon an API and minimally processes what you get. These wrappers have been used to skirt age limits and create a legal grey area where their own age limits are not aligned with the original LLMs intentions. Some will sneak terms and conditions in minimising their liability to the bare minimum.
In such a situation, would you not rather have full control and make your own wrapper? In this video, I show you the work needed to do this and the safeguards that can easily be put in place.
If you need help with this, then please do reach out and we can discuss the support you need.
Here is the source code featured in the video.
// --- Global Variables ---
// Optional: Set an email for error notifications, if needed
const ADMIN_EMAIL_FOR_ERRORS = ""; // Set to null or "" if not needed
// --- Column Names (MUST match your Sheet headers EXACTLY) ---
const STATUS_COLUMN_NAME = "Status";
const PROMPT_COLUMN_NAME = "Your Story Prompt / Idea"; // Make sure this matches the Form question/Sheet header
const EMAIL_COLUMN_NAME = "Email address"; // Make sure this matches the Form question/Sheet header
const STORY_COLUMN_NAME = "Generated Story";
const ERROR_COLUMN_NAME = "Error Details"; // Optional: Add this column to your sheet
// Function to get API key securely
function getApiKey() {
const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
if (!apiKey) {
Logger.log("FATAL ERROR: GEMINI_API_KEY script property not set.");
// Optionally notify admin here if the key is missing entirely
if (ADMIN_EMAIL_FOR_ERRORS) {
MailApp.sendEmail(ADMIN_EMAIL_FOR_ERRORS, "CRITICAL Story Script Error", "Gemini API Key is missing in Script Properties.");
}
}
return apiKey;
}
// --- Triggered on Form Submission ---
function onFormSubmit(e) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const newRow = e.range.getRow();
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
// --- Get Column Indices ---
const statusColIndex = headers.indexOf(STATUS_COLUMN_NAME) + 1;
const promptColIndex = headers.indexOf(PROMPT_COLUMN_NAME) + 1;
const emailColIndex = headers.indexOf(EMAIL_COLUMN_NAME) + 1;
const storyColIndex = headers.indexOf(STORY_COLUMN_NAME) + 1;
const errorColIndex = headers.indexOf(ERROR_COLUMN_NAME) + 1; // Optional
// --- Basic Validation ---
if (statusColIndex <= 0 || promptColIndex <= 0 || emailColIndex <= 0 || storyColIndex <= 0) {
Logger.log("Error: One or more required columns ('Status', 'Your Story Prompt / Idea', 'Your Email Address', 'Generated Story') not found.");
// Attempt to write error to sheet if possible, otherwise just log
try { if(statusColIndex > 0) sheet.getRange(newRow, statusColIndex).setValue("Config Error"); } catch(err){}
if (ADMIN_EMAIL_FOR_ERRORS) MailApp.sendEmail(ADMIN_EMAIL_FOR_ERRORS, "Story Script Config Error", "Check column names in the Sheet and Script.");
return;
}
// --- 1. Set Initial Status ---
sheet.getRange(newRow, statusColIndex).setValue("Processing");
// --- 2. Get Data from Submission ---
const storyPrompt = sheet.getRange(newRow, promptColIndex).getValue();
const studentEmail = sheet.getRange(newRow, emailColIndex).getValue();
// Validate email slightly
if (!studentEmail || !studentEmail.includes('@')) {
Logger.log(`Invalid student email found in row ${newRow}: ${studentEmail}`);
sheet.getRange(newRow, statusColIndex).setValue("Error - Invalid Email");
if (errorColIndex > 0) sheet.getRange(newRow, errorColIndex).setValue("Invalid student email format.");
// Optionally notify admin
if (ADMIN_EMAIL_FOR_ERRORS) MailApp.sendEmail(ADMIN_EMAIL_FOR_ERRORS, "Story Script - Invalid Email", `Invalid email address "${studentEmail}" submitted for prompt: "${storyPrompt}" in row ${newRow}.`);
return;
}
// --- 3. Call Gemini API ---
const apiKey = getApiKey();
if (!apiKey) {
sheet.getRange(newRow, statusColIndex).setValue("Error - API Key Missing");
if (errorColIndex > 0) sheet.getRange(newRow, errorColIndex).setValue("API Key configuration missing.");
// Admin should have been notified by getApiKey()
return;
}
try {
const aiResponse = callGeminiApiForStory(storyPrompt, apiKey);
// --- 4. Process AI Response ---
if (aiResponse && aiResponse.text) {
const generatedStory = aiResponse.text;
sheet.getRange(newRow, storyColIndex).setValue(generatedStory); // Log the story
// --- 5. Send Story Directly to Student ---
const subject = "Here's your AI-generated story!";
const body = `Hi!\n\nYou gave the prompt:\n"${storyPrompt}"\n\nHere's the story generated by AI:\n\n---\n${generatedStory}\n---\n\nHave fun!`; // Customize as needed
MailApp.sendEmail(studentEmail, subject, body);
sheet.getRange(newRow, statusColIndex).setValue("Sent"); // Update status to Sent
Logger.log(`Story sent successfully to ${studentEmail} for row ${newRow}`);
} else {
// Handle API errors or filtering
const errorMessage = (aiResponse && aiResponse.error) ? aiResponse.error : "Unknown error generating story.";
Logger.log(`AI Error for row ${newRow}: ${errorMessage}`);
sheet.getRange(newRow, statusColIndex).setValue("Error - AI Failed");
if (storyColIndex > 0) sheet.getRange(newRow, storyColIndex).setValue("Error - See details"); // Put placeholder in story column
if (errorColIndex > 0) sheet.getRange(newRow, errorColIndex).setValue(errorMessage); // Log specific error
// Optionally notify admin about the failure
if (ADMIN_EMAIL_FOR_ERRORS) MailApp.sendEmail(ADMIN_EMAIL_FOR_ERRORS, "Story Script - AI Error", `Failed to generate story for prompt: "${storyPrompt}" (Row ${newRow}). Error: ${errorMessage}`);
}
} catch (error) {
// Handle script errors during API call or processing
Logger.log(`Script Error processing row ${newRow}: ${error}`);
sheet.getRange(newRow, statusColIndex).setValue("Error - Script Failed");
if (errorColIndex > 0) sheet.getRange(newRow, errorColIndex).setValue(`Script Error: ${error.message}`);
// Notify admin about the script failure
if (ADMIN_EMAIL_FOR_ERRORS) MailApp.sendEmail(ADMIN_EMAIL_FOR_ERRORS, "Story Script - Script Error", `Script failed while processing prompt: "${storyPrompt}" (Row ${newRow}). Error: ${error}`);
}
}
// --- Function to call the Gemini API (Adapted for Story Generation) ---
function callGeminiApiForStory(promptText, apiKey) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`;
// --- Customize the Prompt for Gemini ---
// Be specific! Tell it the desired length, tone, audience etc.
const fullPrompt = `Write a fun, short story (around 150-300 words) suitable for classroom use, based on the following student prompt. Be creative and engaging! Prompt: "${promptText}"`;
const requestBody = {
"contents": [{
"parts": [{
"text": fullPrompt
}]
}],
// --- Optional but Recommended: Add Safety Settings ---
// Adjust categories and thresholds as needed for your classroom context
"safetySettings": [
{ "category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_LOW_AND_ABOVE" },
{ "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_LOW_AND_ABOVE" },
{ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_LOW_AND_ABOVE" },
{ "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_LOW_AND_ABOVE" }
],
// --- Optional: Generation Configuration ---
"generationConfig": {
"temperature": 0.8, // Controls randomness (higher = more creative/random)
"maxOutputTokens": 512 // Limit story length (adjust as needed)
// "topK": ...
// "topP": ...
}
};
const options = {
'method': 'post',
'contentType': 'application/json',
'payload': JSON.stringify(requestBody),
'muteHttpExceptions': true // Crucial for error handling
};
try {
const response = UrlFetchApp.fetch(url, options);
const responseCode = response.getResponseCode();
const responseBody = response.getContentText();
if (responseCode === 200) {
const jsonResponse = JSON.parse(responseBody);
if (jsonResponse.candidates && jsonResponse.candidates.length > 0 &&
jsonResponse.candidates[0].content && jsonResponse.candidates[0].content.parts &&
jsonResponse.candidates[0].content.parts.length > 0 && jsonResponse.candidates[0].content.parts[0].text) {
return { text: jsonResponse.candidates[0].content.parts[0].text.trim() }; // Return the story text
} else if (jsonResponse.promptFeedback && jsonResponse.promptFeedback.blockReason) {
// Handle blocked prompts due to safety settings
const blockReason = jsonResponse.promptFeedback.blockReason;
Logger.log(`Prompt blocked for row. Reason: ${blockReason}`);
let detail = "";
if(jsonResponse.promptFeedback.safetyRatings) {
detail = JSON.stringify(jsonResponse.promptFeedback.safetyRatings);
}
return { error: `Story generation blocked by safety filters (${blockReason}). ${detail}` };
} else if (jsonResponse.candidates && jsonResponse.candidates[0] && jsonResponse.candidates[0].finishReason && jsonResponse.candidates[0].finishReason !== "STOP") {
// Handle other finish reasons like SAFETY or MAX_TOKENS
Logger.log(`Story generation finished unexpectedly. Reason: ${jsonResponse.candidates[0].finishReason}`);
return { error: `Story generation stopped due to: ${jsonResponse.candidates[0].finishReason}` };
}
else {
Logger.log("Unexpected AI response structure: " + responseBody);
return { error: "Unexpected response structure from AI." };
}
} else {
Logger.log(`Gemini API Error - Code: ${responseCode}, Body: ${responseBody}`);
return { error: `AI API request failed (${responseCode}). Details: ${responseBody}` };
}
} catch (error) {
Logger.log("Error during UrlFetchApp call: " + error);
return { error: `Network or script error during API call: ${error.message}` };
}
}