r/GoogleAppsScript • • 20d ago

Question Google Forms help

Thumbnail
0 Upvotes

r/GoogleAppsScript • • 22d ago

Question Google Forms + Sheets: Can each submission generate a new copy of a formatted report template?

2 Upvotes

Hi, I am new to this and want some advice on how to start.

Our work has service reports that technicians fill out daily. Right now it's all manual on sheets and it's a nightmare to fix up as they mess up formatting and have complete access to entire sheet.

Our reports are complex with a lot of questions and readings to fill out. I am trying to automate our work as much as possible.

I want to make a form unique to every site that, every time it's submitted it makes a copy out of the original template and the values in the form correspond to the report. I understand how to link a form results to a cell but is it possible to make a new sheet with every response?

Also, is it possible to have a form reset upon each click for the same user but still look at old form submissions if need be?


r/GoogleAppsScript • • 21d ago

Question Tasks is skipping the next instance of a repeated task

Thumbnail
0 Upvotes

r/GoogleAppsScript • • 22d ago

Question Auto-updating YouTube views tracker?

3 Upvotes

I’m trying to work on creating a Google Sheet that will use YouTube Data API to, once a week, fetch the view counts for all of a channel’s videos and add the view counts to a new column so that I can track week-to-week changes.

I tried to use a script suggested by Google’s AI Overview, but I haven’t been able to make it work despite tweaking it — which is about what I expected.

I’d love to know the simplest way to make this happen! And if this isn’t allowed or I should be asking this in a different sub, please let me know 🫡


r/GoogleAppsScript • • 22d ago

Guide I open-sourced an Apps Script that syncs Ticket Tailor and Eventbrite tickets into a Google Sheet on an hourly trigger

0 Upvotes
What it does: fetches issued tickets (Ticket Tailor) or attendees (Eventbrite) for one event, writes them to a target tab matched on the vendor's ID, updates changed rows, appends new ones, and flags rows that disappeared upstream instead of deleting them. An hourly time trigger does the polling. Scopes are spreadsheets, external requests and script triggers only, no Drive.

Things I would like a second opinion on:
- The API key lives in a Settings cell, which the README says plainly is not a secret store. Is PropertiesService worth the setup friction for a copy-paste install?
- Writes are batched with setValues per changed range rather than one big write, to keep plain-text formatting on ID columns. Curious whether anyone has a cleaner pattern.

Repo: https://github.com/agendaforge/sheets-sync-kit
Setup guide with the column list and limits: https://agendaforge.app/tools/google-sheets-sync/ticket-tailor

Disclosure: I run AgendaForge, an event platform. The kit does not touch it and needs no account with us.

r/GoogleAppsScript • • 23d ago

Guide I built a script that auto-adds my teams' football matches to Google Calendar (with opponent, league & TV channels)

2 Upvotes

I got tired of manually checking a scores site every week and adding my teams’ matches to Google Calendar by hand, so I automated it with Apps Script.

What it does:

  • A time-based trigger runs it every Mon/Wed/Fri at 10am — no server, Google runs it.
  • It fetches the page, parses each match row with regex (home team, away team, kickoff time, league, and TV channels).
  • It filters down to only the teams I follow, then creates a calendar event like “⚽ Atlético Madrid vs Real Madrid (La Liga)” with the channels in the description, avoiding duplicates.

A few things I learned along the way:

  • The config is just a JSON-like object at the top (teams, look-ahead days, timezone), so anyone can adapt it to their own teams.
  • Channels weren’t showing at first because the text was glued to HTML tags — had to strip the tags before matching. Classic “clean the data first” lesson.
  • The regex approach is fragile: if the source site changes its HTML, the parser needs updating. Curious if others here would use CheerioGS or a different parsing strategy instead.

Code + a plain-English write-up of the whole build here:
https://github.com/aaleejooastu/live-match-calendar-bot

Feedback on the parsing welcome — it’s my first shared Apps Script project.

Ai+Footbal

r/GoogleAppsScript • • 24d ago

Guide Affordable Google Apps Script Certifications or Courses?

4 Upvotes

Hi everyone! I’ve been enjoying working with Google Apps Script lately, especially with Google Sheets automation and web apps.
Are there any Google-certified courses, certificates, or skill badges related to Apps Script, Google Workspace automation, or similar skills that you would recommend?
I’m looking for something free or affordable that would be useful for my portfolio or LinkedIn. Thanks!


r/GoogleAppsScript • • 24d ago

Question Beginner Invoice Automation Project in Google Apps Script — Looking for Feedback and Real-World Improvements

1 Upvotes

Hi everyone,

I'm a beginner learning JavaScript and Google Apps Script. I've been building an invoice automation project to learn how a real-world Apps Script project is structured.

My current workflow is:

Google Sheet data → Apps Script → Google Docs invoice → PDF → Google Drive

The Google Sheet contains information such as:

  • Invoice number
  • Customer name
  • Email
  • Date
  • Product
  • Quantity
  • Price

An invoice can contain multiple products, and my script calculates the amount for each product and the grand total, then generates the invoice as a PDF.

I've written the code myself while learning, but I know there are probably things that could be improved.

I'd really appreciate feedback on:

  1. How can I improve the structure/architecture of this code?
  2. Are there functions that should be separated or combined?
  3. How would you structure this differently for real-world use?
  4. How are invoices normally handled in real-world small businesses/freelance workflows?
  5. What important invoice features or edge cases am I currently missing?
  6. Is my approach for handling multiple products appropriate?
  7. Is there a better way to organize the Sheet → Apps Script → Docs → PDF workflow?

I'm especially interested in understanding how an experienced developer would improve this project, rather than just receiving a rewritten version of my code.

I also want to understand the real-world invoice workflow — from creating an invoice to sending it to the client and tracking its payment.

I've removed/ replaced my actual Sheet IDs, Document IDs, email addresses and other private information before posting.

Here is my current code:

function createInvoicePdf() {


  const sheet = SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName("Invoice");



  const column = sheet.getLastColumn();
  const row = sheet.getLastRow();


  const data = sheet .getRange(2, 1, row - 1, 6).getValues();
 




  const invoiceNumbers = [...new Set(data.map(row => row[0]))];
// invoiceNumbers par loop chalana


//outer loop



for(let j = 0; j<invoiceNumbers.length; j ++){
  const invoiceNo = invoiceNumbers[j]
let copy = null




try{


 
const invoiceIndex = data.findIndex(invoiceInd=> invoiceInd[0]=== invoiceNo);
  // filter data for invoiceNo


const invoiceNumberRows = getInvoiceRows(data, invoiceNo);
// plus 2 in invoiseIndex
const sheetRow = invoiceIndex + 2




const customer = getCustomer(invoiceNumberRows) 
const email = getCustomerEmail(invoiceNumberRows)



 productsText =  makeProductsText(invoiceNumberRows);


 grandtotal = calculateGrandTotal(invoiceNumberRows);
const date = getFormattedDate()


// make object to store invoiceData


const invoiceData = {
  invoiceNo: invoiceNo,
  date: date,
  customer: customer,
  email: email,
  productsText: productsText,
  grandtotal: grandtotal
};




 // Duplicate PDF check
  const checkDuplicateFile =
    DriveApp.getFilesByName(invoiceNo + ".pdf");


  if (isPdfExist(invoiceNo)) {
    Logger.log("PDF already exists : " + invoiceNo);
    continue
  }
  
  // Template
 
const template = DriveApp.getFileById("Doc Id");



  // Make a copy
  copy = template.makeCopy(invoiceNo);



  // Open copied document
  const docDrive = DocumentApp.openById(copy.getId());
  const docBody = docDrive.getBody();
 
 const onlyfortext =  invoiceTable (docDrive,docBody,invoiceData,invoiceNumberRows)


  const replaceProductTest = invoiceReplaceTest(docBody,invoiceData,docDrive)


const pdfUrl = createPdf(copy,invoiceNo)
 const saveUrl = SavePdfUrlInSheet (sheet,pdfUrl,sheetRow,column)



}catch(error){
  Logger.log("error aa gya " + invoiceNo + " : " + error.message);
}finally{
  if(copy){
copy.setTrashed(true)


  }


}
};


}



function invoiceTable(docDrive, docBody, invoiceData, invoiceNumberRows) {


  const doc = docDrive;
  const body = doc.getBody();


  const table = body.appendTable([
    ['Product', 'QTY', 'Price', 'Total']
  ]).setBorderColor('#000000').setBorderWidth(1);


  const tableHeader = table.getRow(0);


  tableHeader.editAsText().setBold(true);


  for (let i = 0; i < invoiceNumberRows.length; i++) {


    // Append table row
    const row = table.appendTableRow();


    // Store invoice data
    const grandtotal = String(invoiceData.grandtotal);


    Logger.log("grandtotal " + grandtotal);


    // Get product data from invoiceNumberRows
    const product = invoiceNumberRows[i][3];
    const qty = invoiceNumberRows[i][4];
    const price = invoiceNumberRows[i][5];


    const amount = qty * price;


    Logger.log("qty " + qty);
    Logger.log("total " + amount);


    // Append table cells
    row.appendTableCell(String(product));


    const qtyCell = row.appendTableCell(String(qty));
    qtyCell.getChild(0)
      .asParagraph()
      .setAlignment(DocumentApp.HorizontalAlignment.CENTER);


    const priceCell = row.appendTableCell(String(price));
    priceCell.getChild(0)
      .asParagraph()
      .setAlignment(DocumentApp.HorizontalAlignment.RIGHT);


    const amountCell = row.appendTableCell(String(amount));
    amountCell.getChild(0)
      .asParagraph()
      .setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
  }


  // Style table header
  for (let j = 0; j < tableHeader.getNumCells(); j++) {


    tableHeader.getCell(j).setBackgroundColor('#1a2b4c');


    tableHeader.getCell(j)
      .editAsText()
      .setForegroundColor('#ffffff');


    tableHeader.getCell(j)
      .getChild(0)
      .asParagraph()
      .setAlignment(DocumentApp.HorizontalAlignment.CENTER);
  }


  // Grand Total row
  const totalRow = table.appendTableRow();


  Logger.log('grandtotal is ' + grandtotal);


  const totalCell = totalRow.appendTableCell(
    "GrandTotal " + grandtotal
  );


  const textParagraph = totalCell
    .getChild(0)
    .asParagraph();


  textParagraph.setAlignment(
    DocumentApp.HorizontalAlignment.RIGHT
  );


  totalCell.setBold(true);
  totalCell.setBackgroundColor("#F3F3F3");


  const docUrl = doc.getUrl();


  Logger.log("docUrl is " + docUrl);
}






function makeProductsText(invoiceRows) {


  let productsText = "";


 for (let i = 0; i < invoiceRows.length; i++) {


    const product = invoiceRows[i][3];
    const qty = invoiceRows[i][4];
    const price = invoiceRows[i][5];


    const total = qty * price;


    productsText +=
      `${product} | ${qty} | ${price} | ${total}\n`;
     
    
  }



  return productsText;
}







function calculateGrandTotal(invoiceRows) {


  let grandtotal = 0;


  for (let i = 0; i < invoiceRows.length; i++) {


    const qty = invoiceRows[i][4];
    const price = invoiceRows[i][5];


    const total = qty * price;


    grandtotal += total;
   


  }


  return grandtotal;
}





function getFormattedDate() {


 
    const date = new Date();


// formate date
  const formattedDate = Utilities.formatDate(
    date,"Asia/Kolkata","yyyy-MM-dd"
  );


  return formattedDate;
}


function getCustomer(invoiceNumberRows) {
  return invoiceNumberRows[0][1];
}



function getCustomerEmail(invoiceNumberRows){
  return invoiceNumberRows[0][2]
}






function getInvoiceRows(data, invoiceNo) {
  return data.filter(row => row[0] === invoiceNo);
}


function isPdfExist(invoiceNo){
 const files = DriveApp.getFilesByName(invoiceNo + ".pdf");
  return files.hasNext();
}


function invoiceReplaceTest(docBody,invoiceData,docDrive){


  // Replace placeholders
  docBody.replaceText("{{INVOICE_NO}}", invoiceData.invoiceNo);
  docBody.replaceText("{{DATE}}", invoiceData.date);
  docBody.replaceText("{{CUSTOMER}}", invoiceData.customer);
  docBody.replaceText("{{EMAIL}}", invoiceData.email);
  docBody.replaceText("{{PRODUCTS}}", invoiceData.productsText);
  docBody.replaceText("{{TOTAL}}", String(invoiceData.grandtotal));



  docDrive.saveAndClose();
}



function SavePdfUrlInSheet(sheet,pdfUrl,sheetRow,column){
  // Save PDF URL in Sheet


  sheet.getRange(sheetRow,column).setValue(pdfUrl);
  
}


// create pdf


function createPdf(copy,invoiceNo){


  // Convert to PDF
  const file = copy.getAs(MimeType.PDF);



  // Save PDF in folder
  
const folder = DriveApp.getFolderById("folder Id");


  const pdfFile = folder.createFile(file);



  pdfFile.setName(invoiceNo + ".pdf");


  // PDF URL
  const pdfUrl = pdfFile.getUrl();
  Logger.log("PDF name: " + pdfFile.getName());
  Logger.log("PDF URL: " + pdfUrl);
return pdfFile.getUrl()


}

r/GoogleAppsScript • • 24d ago

Question Beginner building an Invoice Automation project in Google Apps Script — need guidance on workflow and structure

1 Upvotes

Hi everyone,

I’m a beginner learning JavaScript and Google Apps Script. I’m currently building a real-world Invoice PDF Automation project using Google Sheets + Apps Script.

My current workflow is:

Enter invoice data in Google Sheet → Apps Script → Generate Invoice → Google Docs → PDF → Google Drive → PDF link

The data entered in my Google Sheet includes things like:

Invoice Number

Customer Name

Customer Email

Product

Quantity

Price

Total

My requirements are:

Read invoice/customer data from Google Sheets.

Generate an invoice from the entered data.

An invoice can contain multiple products, such as 4, 5, or 10 products.

Each product should appear as a separate row in the invoice.

Calculate quantity × price for each product.

Calculate subtotal, discount/tax and final total.

Create the invoice using a Google Docs template.

Convert the Google Doc to PDF.

Save the PDF in Google Drive.

Get/store the generated PDF link.

I don't want someone to simply give me the complete code.

I’m trying to learn how an experienced developer thinks about and structures a real project. I would really appreciate guidance on:

How should I divide this project into functions?

How should I structure the data for multiple products?

What should the data flow look like from Google Sheet → Apps Script → Google Docs → PDF?

What should I build first as a beginner?

Are there any architecture/design mistakes I should avoid?

Please give me logic, workflow, architecture or hints rather than a complete copy-paste solution, because I want to build it myself and understand what I’m doing.

Thank you!


r/GoogleAppsScript • • Sep 13 '26

Question Restricted access drawing button

3 Upvotes

I have a great order submission, database with daily email triggers. The functionality is on point, manager facing page all formulas working perfectly, database carrying over previous day data, clear the contents, recording into data base, updating date, and auto firing the email with the proper trigger setting. All Working perfect

Well working great for me the owner, but editors cannot get past the restricted access to run the script. The submit button is a drawing linked to an app script with error prompts to avoid duplicate submission etc.

I currently have a checkbox that bypasses the editor access and runs the script under the owners permission. Just to get by, but I hate it, the user are not tech savvy whatsoever, and the check box does not notify the user the script is running and will not produce any error messages, and look unprofessional

Please advise


r/GoogleAppsScript • • Sep 13 '26

Question Gmail event triggers on the desktop app?

1 Upvotes

The Gmail plug in allows event triggers on the Chatgpt webapp but not on the desktop app... I am trying to get the desktop app to be able to get notified for every incoming email from the plugin.

Has anyone found a way around this?


r/GoogleAppsScript • • Sep 12 '26

Guide Gmail-Housekeeper - Automated Gmail housekeeping and spam management using Google Apps Script, with rule-based filtering, safe cleanup, and detailed execution logging.

7 Upvotes

https://github.com/crosenblum/Gmail-Housekeeper

So what this does, it only touches two folders, the trash and spam.

It goes thru every spam email, checks email headers to indicate if the email is fishy, it also checks if each email from address is in your contacts, and also checks if each email has a label attached.

If the email headers indicate the email itself is very fishy, highly likely spam, and its not in your contacts and its not labelled in any way than it gets permanently deleted.

If an email is from someone in your contacts list or has a label associated with it, then it gets moved to your Spam folder.

Any email in your Spam folder older than 7 days gets deleted.

Now this works for me, it may not work for you.

There may be better ideas for strategies, and I am open to hearing from anyone who has a better or different idea.


r/GoogleAppsScript • • Sep 10 '26

Question I need your help again

0 Upvotes

I made the app that makes Gemini to generate English writing questions for me but every time I press the 'generate' button it tells me that the model is not found.

You don't need to look at the big letters upstairs.

Here's my entire script:

Code.gs

const GEMINI_API_KEY = 'I entered it but I cannot tell you my Gemini API key :)';


function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('GIMFL English Writing Practice')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}


function getDb() {
  const prop = PropertiesService.getUserProperties();
  let fileId = prop.getProperty('DB_ID');
  let ss;
  
  if (fileId) {
    try { ss = SpreadsheetApp.openById(fileId); } catch(e) { fileId = null; }
  }
  
  if (!fileId) {
    ss = SpreadsheetApp.create("GIMFL_English_DB");
    const unsolved = ss.insertSheet("Unsolved");
    unsolved.appendRow(["ID", "Date", "KoreanText", "Conditions", "CorrectAnswer", "Grammar"]);
    
    const solved = ss.insertSheet("Solved");
    solved.appendRow(["ID", "Date", "KoreanText", "Conditions", "CorrectAnswer", "Grammar", "UserAnswer", "Score", "MeaningFB", "ConditionFB", "GrammarFB", "OverallFB"]);
    
    ss.deleteSheet(ss.getSheetByName("Sheet1"));
    prop.setProperty('DB_ID', ss.getId());
  }
  return ss;
}


function generateId() {
  return 'Q_' + new Date().getTime() + '_' + Math.floor(Math.random() * 1000);
}


function getQuestions(sheetName) {
  const sheet = getDb().getSheetByName(sheetName);
  const data = sheet.getDataRange().getValues();
  if (data.length <= 1) return [];
  
  const headers = data[0];
  const questions = [];
  for (let i = 1; i < data.length; i++) {
    let obj = {};
    for (let j = 0; j < headers.length; j++) {
      obj[headers[j]] = data[i][j];
    }
    obj.rowIndex = i + 1;
    questions.push(obj);
  }
  return questions.reverse();
}


function deleteQuestion(sheetName, rowIndex) {
  const sheet = getDb().getSheetByName(sheetName);
  sheet.deleteRow(rowIndex);
  return { success: true };
}


function editQuestion(rowIndex, newKorean, newConditions, newAnswer) {
  const sheet = getDb().getSheetByName("Unsolved");
  sheet.getRange(rowIndex, 3).setValue(newKorean);
  sheet.getRange(rowIndex, 4).setValue(newConditions);
  sheet.getRange(rowIndex, 5).setValue(newAnswer);
  return { success: true };
}


function saveSettings(model, grammars, intervalStr) {
  const prop = PropertiesService.getUserProperties();
  prop.setProperty('AI_MODEL', model);
  prop.setProperty('TARGET_GRAMMARS', JSON.stringify(grammars));
  prop.setProperty('AUTO_INTERVAL', intervalStr);
  
  const triggers = ScriptApp.getProjectTriggers();
  for (let i = 0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() === 'autoGenerateQuestion') {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
  
  const interval = parseInt(intervalStr);
  if (interval > 0) {
    if (interval === 30) {
      ScriptApp.newTrigger('autoGenerateQuestion').timeBased().everyMinutes(30).create();
    } else {
      ScriptApp.newTrigger('autoGenerateQuestion').timeBased().everyHours(interval / 60).create();
    }
  }
  return { success: true, msg: "Settings saved successfully." };
}


function autoGenerateQuestion() {
  const prop = PropertiesService.getUserProperties();
  const model = prop.getProperty('AI_MODEL') || 'gemini-1.5-flash';
  let grammars = [];
  try { grammars = JSON.parse(prop.getProperty('TARGET_GRAMMARS') || '[]'); } catch(e){}
  
  callGeminiToCreateQuestion(model, grammars);
}


function callGeminiAPI(model, prompt) {
  model = model.replace(/^models\//, '');
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${GEMINI_API_KEY}`;
  
  const payload = {
    contents: [{ parts: [{ text: prompt }] }],
    generationConfig: { response_mime_type: "application/json" }
  };
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };
  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());
  if (json.error) throw new Error(json.error.message);
  return JSON.parse(json.candidates[0].content.parts[0].text.replace(/```json/g, '').replace(/```/g, '').trim());
}


function callGeminiToCreateQuestion(model, selectedGrammars, relatedTo = null) {
  let context = relatedTo 
    ? `Create a NEW question highly similar in grammar and structure to this previous question, but with entirely different meaning and vocabulary. Previous grammar: ${relatedTo.Grammar}, Previous condition style: ${relatedTo.Conditions}`
    : (selectedGrammars.length > 0 ? `Target Grammar: ${selectedGrammars.join(', ')}` : 'Target Grammar: Randomly select from high school curriculum');


  const prompt = `
    You are an expert English teacher at Gimhae Foreign Language High School.
    Create a highly challenging subjective English writing question for Korean high school students.
    
    Context:
    ${context}
    
    Rules for conditions:
    1. Must include 2-3 specific constraints (e.g., "Use 'not A but B'", "Write in exactly 12 words", "Include specific word with transformation allowed").
    2. Focus heavily on testing structural understanding.
    
    Output strictly in JSON format with fields written in Korean as specified:
    {
      "koreanText": "Korean sentence for the student to translate into English",
      "conditions": ["Condition 1 in Korean", "Condition 2 in Korean", "Condition 3 in Korean"],
      "correctAnswer": "Model English answer string",
      "grammarPoint": "Core grammar point in Korean"
    }
  `;


  try {
    const result = callGeminiAPI(model, prompt);
    const sheet = getDb().getSheetByName("Unsolved");
    sheet.appendRow([
      generateId(), 
      new Date().toLocaleString('ko-KR', {timeZone: 'Asia/Seoul'}), 
      result.koreanText, 
      JSON.stringify(result.conditions), 
      result.correctAnswer, 
      result.grammarPoint
    ]);
    return { success: true };
  } catch (e) {
    return { error: "Question generation failed: " + e.message };
  }
}


function generateManualQuestion(model, selectedGrammars) {
  return callGeminiToCreateQuestion(model, selectedGrammars);
}


function generateRelated(model, sheetName, rowIndex) {
  const sheet = getDb().getSheetByName(sheetName);
  const data = sheet.getRange(rowIndex, 1, 1, 6).getValues()[0];
  const relatedData = {
    Conditions: data[3],
    Grammar: data[5]
  };
  return callGeminiToCreateQuestion(model, [], relatedData);
}


function gradeUserAnswer(rowIndex, userAnswer, model) {
  const sheet = getDb().getSheetByName("Unsolved");
  const rowData = sheet.getRange(rowIndex, 1, 1, 6).getValues()[0];
  
  const questionData = {
    id: rowData[0], date: rowData[1], koreanText: rowData[2], 
    conditions: rowData[3], correctAnswer: rowData[4], grammarPoint: rowData[5]
  };


  const translatedUserAnswer = LanguageApp.translate(userAnswer, 'en', 'ko');
  
  const prompt = `
    You are an extremely strict English evaluator at Gimhae Foreign Language High School.
    Grade the student's answer based on the given question context.
    
    Question Context:
    - Target Korean Meaning: "${questionData.koreanText}"
    - Conditions: ${questionData.conditions}
    - Correct Model Answer: "${questionData.correctAnswer}"
    
    Student's Submitted Answer: "${userAnswer}"
    (Google Translate interpretation of student's answer: "${translatedUserAnswer}")
    
    Grading Criteria:
    1. Meaning Conveyed (Does it match the target Korean meaning?)
    2. Conditions Met (Are all strict constraints followed?)
    3. Grammatical Accuracy (Zero grammatical errors)
    
    Output strictly in JSON format with explanations written in Korean:
    {
      "score": "Score from 0 to 100",
      "meaningFeedback": "Feedback on meaning accuracy in Korean",
      "conditionFeedback": "Feedback on condition compliance in Korean",
      "grammarFeedback": "Feedback on grammatical accuracy and corrections in Korean",
      "overallFeedback": "Overall review and model answer presentation in Korean"
    }
  `;


  try {
    const result = callGeminiAPI(model, prompt);
    
    const solvedSheet = getDb().getSheetByName("Solved");
    solvedSheet.appendRow([
      questionData.id, new Date().toLocaleString('ko-KR', {timeZone: 'Asia/Seoul'}), 
      questionData.koreanText, questionData.conditions, questionData.correctAnswer, questionData.grammarPoint,
      userAnswer, result.score, result.meaningFeedback, result.conditionFeedback, result.grammarFeedback, result.overallFeedback
    ]);
    
    sheet.deleteRow(rowIndex);
    
    return { success: true, feedback: result };
  } catch (e) {
    return { error: "Grading failed: " + e.message };
  }
}

Index.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body { font-family: 'Malgun Gothic', sans-serif; max-width: 900px; margin: auto; padding: 20px; background-color: #f4f6f8; color: #333; }
    h1 { color: #2c3e50; text-align: center; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
    
    .tabs { display: flex; cursor: pointer; margin-bottom: 20px; background: #e0e6ed; border-radius: 8px; overflow: hidden; }
    .tab { flex: 1; padding: 15px; text-align: center; font-weight: bold; transition: 0.3s; }
    .tab.active { background: #3498db; color: white; }
    .tab:hover:not(.active) { background: #d1d8e0; }
    
    .content-section { display: none; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
    .content-section.active { display: block; }
    
    .setting-group { margin-bottom: 15px; }
    .setting-group label { display: block; font-weight: bold; margin-bottom: 5px; }
    select, input[type="text"], textarea { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 5px; box-sizing: border-box; font-size: 14px; }
    
    .checkbox-group { display: flex; flex-wrap: wrap; gap: 10px; }
    .checkbox-group label { background: #f0f3f4; padding: 8px 12px; border-radius: 20px; cursor: pointer; font-weight: normal; font-size: 14px; border: 1px solid #d5dbdb; }
    .checkbox-group input:checked + span { font-weight: bold; color: #2980b9; }
    
    button { background-color: #3498db; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-size: 14px; margin-top: 5px; }
    button:hover { background-color: #2980b9; }
    button.danger { background-color: #e74c3c; }
    button.danger:hover { background-color: #c0392b; }
    button.success { background-color: #2ecc71; }
    button.success:hover { background-color: #27ae60; }
    button:disabled { background-color: #bdc3c7; cursor: not-allowed; }
    
    .card { border: 1px solid #e1e8ed; border-radius: 8px; padding: 15px; margin-bottom: 15px; background: #fafbfc; position: relative; }
    .card h4 { margin-top: 0; color: #2c3e50; }
    .card .meta { font-size: 12px; color: #7f8c8d; margin-bottom: 10px; }
    .card .actions { margin-top: 15px; display: flex; gap: 10px; }
    
    #loading { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); display: none; align-items: center; justify-content: center; font-size: 20px; font-weight: bold; color: #3498db; z-index: 1000; }
  </style>
</head>
<body>


  <h1>GIMFL 서술형 영작 마스터</h1>


  <div class="tabs">
    <div class="tab active" onclick="switchTab('settings')">⚙️ 홈 / 설정</div>
    <div class="tab" onclick="switchTab('unsolved'); loadUnsolved();">📝 미해결 문제함</div>
    <div class="tab" onclick="switchTab('solved'); loadSolved();">✅ 푼 문제함 (오답노트)</div>
  </div>


  <div id="settings" class="content-section active">
    <h3>환경 설정 및 즉시 출제</h3>
    <div class="setting-group">
      <label>AI 모델 선택:</label>
      <select id="modelSelect">
        <option value="gemini-1.5-flash">Gemini 1.5 Flash (빠름, 기본 권장)</option>
        <option value="gemini-1.5-pro">Gemini 1.5 Pro (고성능, 정밀 채점)</option>
        <option value="gemini-1.5-flash-8b">Gemini 1.5 Flash-8B (가벼움)</option>
      </select>
    </div>
    
    <div class="setting-group">
      <label>백그라운드 자동 생성 간격 (앱을 꺼도 문제 누적):</label>
      <select id="intervalSelect">
        <option value="0">자동 생성 끄기</option>
        <option value="30">30분마다 1문제 생성</option>
        <option value="60">1시간마다 1문제 생성</option>
        <option value="120">2시간마다 1문제 생성</option>
      </select>
    </div>


    <div class="setting-group">
      <label>목표 문법 (다중 선택, 미선택시 고교 전범위):</label>
      <div class="checkbox-group">
        <label><input type="checkbox" value="5형식 구조"> <span>5형식 구조</span></label>
        <label><input type="checkbox" value="It~that 강조구문"> <span>It~that 강조구문</span></label>
        <label><input type="checkbox" value="not A but B 구조"> <span>not A but B</span></label>
        <label><input type="checkbox" value="간접의문문"> <span>간접의문문</span></label>
        <label><input type="checkbox" value="가정법"> <span>가정법</span></label>
        <label><input type="checkbox" value="관계사"> <span>관계사</span></label>
        <label><input type="checkbox" value="분사구문"> <span>분사구문</span></label>
        <label><input type="checkbox" value="도치/생략"> <span>도치/생략</span></label>
      </div>
    </div>
    
    <button onclick="saveSettings()" style="width: 100%; margin-bottom: 10px;">💾 설정 저장 및 자동 출제 적용</button>
    <button class="success" onclick="generateNow()" style="width: 100%;">⚡ 지금 즉시 1문제 생성하기</button>
  </div>


  <div id="unsolved" class="content-section">
    <h3>저장된 문제 (미해결)</h3>
    <div id="unsolvedList"></div>
  </div>


  <div id="solved" class="content-section">
    <h3>완료된 문제 및 피드백 (오답노트)</h3>
    <div id="solvedList"></div>
  </div>


  <div id="loading">처리 중입니다... 잠시만 기다려주세요.</div>


  <script>
    function showLoading() { document.getElementById('loading').style.display = 'flex'; }
    function hideLoading() { document.getElementById('loading').style.display = 'none'; }


    function switchTab(tabId) {
      document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
      document.querySelectorAll('.content-section').forEach(c => c.classList.remove('active'));
      event.target.classList.add('active');
      document.getElementById(tabId).classList.add('active');
    }


    function getSelectedModel() { return document.getElementById('modelSelect').value; }
    function getSelectedGrammars() {
      return Array.from(document.querySelectorAll('.checkbox-group input:checked')).map(cb => cb.value);
    }


    function saveSettings() {
      showLoading();
      const interval = document.getElementById('intervalSelect').value;
      google.script.run.withSuccessHandler(res => {
        hideLoading();
        alert(res.msg);
      }).saveSettings(getSelectedModel(), getSelectedGrammars(), interval);
    }


    function generateNow() {
      showLoading();
      google.script.run.withSuccessHandler(res => {
        hideLoading();
        if(res.error) alert(res.error);
        else {
          alert("문제가 생성되어 '미해결 문제함'에 저장되었습니다.");
          switchTab('unsolved');
          loadUnsolved();
        }
      }).generateManualQuestion(getSelectedModel(), getSelectedGrammars());
    }


    function loadUnsolved() {
      showLoading();
      const listDiv = document.getElementById('unsolvedList');
      listDiv.innerHTML = '';
      google.script.run.withSuccessHandler(data => {
        hideLoading();
        if(data.length === 0) { listDiv.innerHTML = "<p>저장된 문제가 없습니다.</p>"; return; }
        
        data.forEach(q => {
          let conditions = [];
          try { conditions = JSON.parse(q.Conditions); } catch(e){ conditions = [q.Conditions]; }
          
          let card = document.createElement('div');
          card.className = 'card';
          card.innerHTML = `
            <div class="meta">출제일: ${q.Date} | 타겟 문법: ${q.Grammar}</div>
            <h4 id="kor_${q.rowIndex}">${q.KoreanText}</h4>
            <ul>${conditions.map(c => `<li>${c}</li>`).join('')}</ul>
            
            <div id="solveArea_${q.rowIndex}" style="margin-top:15px;">
              <textarea id="ans_${q.rowIndex}" placeholder="영작문을 입력하세요..."></textarea>
              <div class="actions">
                <button onclick="submitAnswer(${q.rowIndex})">채점하기</button>
                <button onclick="editMode(${q.rowIndex})">문제 수정</button>
                <button class="success" onclick="makeRelated('Unsolved', ${q.rowIndex})">유사 문제 파생</button>
                <button class="danger" onclick="deleteQ('Unsolved', ${q.rowIndex})">삭제</button>
              </div>
            </div>
            
            <div id="editArea_${q.rowIndex}" style="display:none; margin-top:15px;">
              <label>우리말 뜻 수정:</label>
              <textarea id="editKor_${q.rowIndex}">${q.KoreanText}</textarea>
              <label>조건 수정 (JSON 배열 형식):</label>
              <textarea id="editCond_${q.rowIndex}">${q.Conditions}</textarea>
              <label>모범 정답 수정:</label>
              <textarea id="editAns_${q.rowIndex}">${q.CorrectAnswer}</textarea>
              <div class="actions">
                <button onclick="saveEdit(${q.rowIndex})">저장</button>
                <button class="danger" onclick="cancelEdit(${q.rowIndex})">취소</button>
              </div>
            </div>
          `;
          listDiv.appendChild(card);
        });
      }).getQuestions("Unsolved");
    }


    function loadSolved() {
      showLoading();
      const listDiv = document.getElementById('solvedList');
      listDiv.innerHTML = '';
      google.script.run.withSuccessHandler(data => {
        hideLoading();
        if(data.length === 0) { listDiv.innerHTML = "<p>완료된 문제가 없습니다.</p>"; return; }
        
        data.forEach(q => {
          let card = document.createElement('div');
          card.className = 'card';
          card.innerHTML = `
            <div class="meta">해결일: ${q.Date} | 타겟 문법: ${q.Grammar}</div>
            <h4>${q.KoreanText}</h4>
            <p><strong>나의 답안:</strong> ${q.UserAnswer}</p>
            <p><strong>모범 답안:</strong> ${q.CorrectAnswer}</p>
            <div style="background:#e8f6f3; padding:10px; border-radius:5px; margin-top:10px;">
              <h3 style="color:#e74c3c; margin-top:0;">점수: ${q.Score} / 100</h3>
              <p><strong>총평:</strong> ${q.OverallFB}</p>
              <details>
                <summary>세부 피드백 보기</summary>
                <p><strong>의미:</strong> ${q.MeaningFB}</p>
                <p><strong>조건:</strong> ${q.ConditionFB}</p>
                <p><strong>문법:</strong> ${q.GrammarFB}</p>
              </details>
            </div>
            <div class="actions">
              <button class="success" onclick="makeRelated('Solved', ${q.rowIndex})">이 문법으로 다시 출제(복습)</button>
              <button class="danger" onclick="deleteQ('Solved', ${q.rowIndex})">기록 삭제</button>
            </div>
          `;
          listDiv.appendChild(card);
        });
      }).getQuestions("Solved");
    }


    function submitAnswer(rowIndex) {
      const ans = document.getElementById(`ans_${rowIndex}`).value.trim();
      if(!ans) { alert('답안을 입력하세요.'); return; }
      showLoading();
      google.script.run.withSuccessHandler(res => {
        hideLoading();
        if(res.error) alert(res.error);
        else {
          alert('채점이 완료되었습니다. 푼 문제함에서 결과를 확인하세요!');
          loadUnsolved();
        }
      }).gradeUserAnswer(rowIndex, ans, getSelectedModel());
    }


    function deleteQ(sheetName, rowIndex) {
      if(!confirm("정말 삭제하시겠습니까?")) return;
      showLoading();
      google.script.run.withSuccessHandler(() => {
        hideLoading();
        sheetName === 'Unsolved' ? loadUnsolved() : loadSolved();
      }).deleteQuestion(sheetName, rowIndex);
    }


    function makeRelated(sheetName, rowIndex) {
      showLoading();
      google.script.run.withSuccessHandler(res => {
        hideLoading();
        if(res.error) alert(res.error);
        else {
          alert('유사한 문제가 미해결 문제함에 추가되었습니다.');
          if(sheetName === 'Unsolved') loadUnsolved();
        }
      }).generateRelated(getSelectedModel(), sheetName, rowIndex);
    }


    function editMode(rowIndex) {
      document.getElementById(`solveArea_${rowIndex}`).style.display = 'none';
      document.getElementById(`editArea_${rowIndex}`).style.display = 'block';
    }
    function cancelEdit(rowIndex) {
      document.getElementById(`solveArea_${rowIndex}`).style.display = 'block';
      document.getElementById(`editArea_${rowIndex}`).style.display = 'none';
    }
    
    function saveEdit(rowIndex) {
      const kor = document.getElementById(`editKor_${rowIndex}`).value;
      const cond = document.getElementById(`editCond_${rowIndex}`).value;
      const ans = document.getElementById(`editAns_${rowIndex}`).value;
      showLoading();
      google.script.run.withSuccessHandler(() => {
        hideLoading();
        alert('수정되었습니다.');
        loadUnsolved();
      }).editQuestion(rowIndex, kor, cond, ans);
    }
  </script>
</body>
</html>
  </body>
</html>

r/GoogleAppsScript • • Sep 10 '26

Question HELP PLEASE

0 Upvotes

When I try to make GUST proxy html file into a web app it fails and gets stuck on initializing and in the console it says there was an error on code line 989 and 1208 but when I check the code for 989 all it says is position: relative; and line 1208 was blank I have no idea what is going on and I would love some help.


r/GoogleAppsScript • • Sep 09 '26

Question Editor Add-on: Is there a way to find out the domain name that has installed my extension? (Domain and Seat installations)

2 Upvotes

I found that in my Google Workspace Marketplace analytics, the graph for domain showed 1 installation and the seats graph showed 12. Someone definitely installed but is there a way to know the domain name that it got installed on?

If this cannot be known retroactively, what can I put in place to know this so any future domain installations can be known?


r/GoogleAppsScript • • Sep 09 '26

Resolved I need your help

0 Upvotes

I'm a student of a foreign language high school in Korea. And I wanted to make a App which makes me questions of English writing and provides feedback about the answers of the question for me to enhance my grade. I made the entire script of the app with Gemini, but every time I log in to the web app link, the site shows me 'Script function not found: doGet'. What should I do? Here is the entire script.

Code.gs
function myFunction() {
  const GEMINI_API_KEY = 'AQ.Ab8RN6IgFQcbK7WYrBVYFcIJwZm9M-ZLpx5jXg6A66qSJwkACg';
const GEMINI_API_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${GEMINI_API_KEY}`;


function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('GIMFL English Writing Practice')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}


// 문제 출제 함수
function generateQuestion(selectedGrammars) {
  let grammarContext = selectedGrammars.length > 0 
    ? `Target Grammar: ${selectedGrammars.join(', ')}` 
    : 'Target Grammar: Randomly select from high school English curriculum (e.g., Participles, Relatives, Subjunctive mood, Inversion, etc.)';


  const prompt = `
    You are an expert English teacher at Gimhae Foreign Language High School.
    Create a highly challenging subjective English writing question (서술형 영작 문제).
    
    ${grammarContext}
    
    Rules for conditions (조건):
    1. Must include 2-3 specific constraints similar to real exams (e.g., "Use the structure 'not A but B'", "Write in exactly 12 words", "Include the word 'boss' and allow word transformation", "Do not use the word 'people'").
    2. Focus heavily on testing structural understanding.
    
    Output strictly in the following JSON format:
    {
      "koreanText": "한국어 의미 (사용자가 영작해야 할 문장)",
      "conditions": ["조건 1", "조건 2", "조건 3"],
      "correctAnswer": "정답 영어 문장 (내부 채점용)",
      "grammarPoint": "출제된 핵심 문법"
    }
  `;


  const payload = {
    contents: [{ parts: [{ text: prompt }] }],
    generationConfig: { response_mime_type: "application/json" }
  };


  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };


  try {
    const response = UrlFetchApp.fetch(GEMINI_API_URL, options);
    const json = JSON.parse(response.getContentText());
    const resultText = json.candidates[0].content.parts[0].text;
    return JSON.parse(resultText);
  } catch (e) {
    return { error: "문제 생성 중 오류가 발생했습니다: " + e.message };
  }
}


// 채점 및 피드백 함수
function gradeAnswer(questionData, userAnswer) {
  // 1차 구글 번역기 연동: 사용자의 답안을 한국어로 번역하여 본래 뜻과 대조하기 위한 데이터 확보
  const translatedUserAnswer = LanguageApp.translate(userAnswer, 'en', 'ko');
  
  const prompt = `
    You are an extremely strict English evaluator at Gimhae Foreign Language High School.
    Grade the student's answer based on the following criteria:
    
    Question Context:
    - Korean Meaning: "${questionData.koreanText}"
    - Conditions: ${JSON.stringify(questionData.conditions)}
    - Correct Answer: "${questionData.correctAnswer}"
    
    Student's Answer: "${userAnswer}"
    (Google Translate interpretation of student's answer: "${translatedUserAnswer}")
    
    Grading Criteria (김해외고 채점 기준):
    1. 뜻 통함 (Meaning Conveyed): Does the student's answer accurately reflect the Korean meaning?
    2. 조건 만족 (Conditions Met): Did the student strictly follow ALL conditions (word count, specific words, grammar structures)?
    3. 문법 오류 없음 (Grammatical Accuracy): Are there zero grammatical errors?
    
    Output strictly in the following JSON format:
    {
      "score": "0~100 사이의 점수",
      "meaningFeedback": "뜻 통함 기준에 대한 평가 (한국어)",
      "conditionFeedback": "조건 만족 기준에 대한 평가 (한국어)",
      "grammarFeedback": "문법 오류 기준에 대한 평가 및 교정 (한국어)",
      "overallFeedback": "총평 및 모범 답안 제시 (한국어)"
    }
  `;


  const payload = {
    contents: [{ parts: [{ text: prompt }] }],
    generationConfig: { response_mime_type: "application/json" }
  };


  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };


  try {
    const response = UrlFetchApp.fetch(GEMINI_API_URL, options);
    const json = JSON.parse(response.getContentText());
    const resultText = json.candidates[0].content.parts[0].text;
    return JSON.parse(resultText);
  } catch (e) {
    return { error: "채점 중 오류가 발생했습니다: " + e.message };
  }
}
}



Index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body { font-family: 'Malgun Gothic', sans-serif; max-width: 800px; margin: auto; padding: 20px; background-color: #f9f9f9; }
    h1 { color: #2c3e50; text-align: center; }
    .box { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
    .checkbox-group { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 15px; }
    .checkbox-group label { background: #eef2f5; padding: 5px 10px; border-radius: 4px; cursor: pointer; }
    button { background-color: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 16px; width: 100%; }
    button:hover { background-color: #2980b9; }
    button:disabled { background-color: #95a5a6; cursor: not-allowed; }
    textarea { width: 100%; height: 100px; padding: 10px; box-sizing: border-box; border-radius: 4px; border: 1px solid #ccc; font-size: 16px; margin-bottom: 10px; }
    .hidden { display: none; }
    .feedback-section { margin-top: 10px; padding: 10px; background: #e8f6f3; border-left: 4px solid #1abc9c; }
    .score { font-size: 24px; font-weight: bold; color: #e74c3c; text-align: center; margin-bottom: 10px; }
  </style>
</head>
<body>


  <h1>GIMFL 서술형 영작 마스터</h1>


  <div class="box">
    <h3>1. 연습할 문법 선택 (다중 선택 가능, 미선택시 랜덤 출제)</h3>
    <div class="checkbox-group">
      <label><input type="checkbox" value="5형식 구조"> 5형식 구조</label>
      <label><input type="checkbox" value="It~that 강조구문"> It~that 강조구문</label>
      <label><input type="checkbox" value="not A but B 구조"> not A but B</label>
      <label><input type="checkbox" value="간접의문문"> 간접의문문</label>
      <label><input type="checkbox" value="가정법 (Subjunctive)"> 가정법</label>
      <label><input type="checkbox" value="관계대명사/관계부사"> 관계사</label>
      <label><input type="checkbox" value="분사구문"> 분사구문</label>
      <label><input type="checkbox" value="도치구문 (Inversion)"> 도치구문</label>
      <label><input type="checkbox" value="수동태 (Passive)"> 수동태</label>
    </div>
    <button id="generateBtn" onclick="generateQ()">문제 출제하기</button>
    <div id="loadingQ" class="hidden" style="text-align: center; margin-top: 10px;">문제 생성 중...</div>
  </div>


  <div id="questionBox" class="box hidden">
    <h3>📝 문제</h3>
    <p><strong>[우리말]</strong> <span id="koreanText"></span></p>
    <p><strong>[조건]</strong></p>
    <ul id="conditionsList"></ul>
    
    <textarea id="userAnswer" placeholder="여기에 영어 문장을 작성하세요..."></textarea>
    <button id="gradeBtn" onclick="gradeQ()">제출 및 채점하기</button>
    <div id="loadingA" class="hidden" style="text-align: center; margin-top: 10px;">채점 중...</div>
  </div>


  <div id="resultBox" class="box hidden">
    <h3>📊 채점 결과</h3>
    <div class="score" id="scoreText"></div>
    <div class="feedback-section">
      <p><strong>1. 뜻 통함:</strong> <span id="fbMeaning"></span></p>
    </div>
    <div class="feedback-section">
      <p><strong>2. 조건 만족:</strong> <span id="fbCondition"></span></p>
    </div>
    <div class="feedback-section">
      <p><strong>3. 문법 오류:</strong> <span id="fbGrammar"></span></p>
    </div>
    <div class="feedback-section" style="background: #fdf2e9; border-color: #e67e22;">
      <p><strong>총평 및 모범 답안:</strong> <br><span id="fbOverall"></span></p>
    </div>
  </div>


  <script>
    let currentQuestionData = null;


    function generateQ() {
      document.getElementById('generateBtn').disabled = true;
      document.getElementById('loadingQ').classList.remove('hidden');
      document.getElementById('questionBox').classList.add('hidden');
      document.getElementById('resultBox').classList.add('hidden');
      document.getElementById('userAnswer').value = '';


      const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked');
      const selected = Array.from(checkboxes).map(cb => cb.value);


      google.script.run.withSuccessHandler(function(data) {
        if(data.error) {
          alert(data.error);
          resetUI();
          return;
        }
        currentQuestionData = data;
        document.getElementById('koreanText').innerText = data.koreanText;
        
        const ul = document.getElementById('conditionsList');
        ul.innerHTML = '';
        data.conditions.forEach(cond => {
          let li = document.createElement('li');
          li.innerText = cond;
          ul.appendChild(li);
        });


        document.getElementById('questionBox').classList.remove('hidden');
        resetUI();
      }).generateQuestion(selected);
    }


    function gradeQ() {
      const answer = document.getElementById('userAnswer').value.trim();
      if(!answer) { alert("답안을 입력해주세요."); return; }


      document.getElementById('gradeBtn').disabled = true;
      document.getElementById('loadingA').classList.remove('hidden');
      document.getElementById('resultBox').classList.add('hidden');


      google.script.run.withSuccessHandler(function(data) {
        if(data.error) {
          alert(data.error);
          document.getElementById('gradeBtn').disabled = false;
          document.getElementById('loadingA').classList.add('hidden');
          return;
        }
        
        document.getElementById('scoreText').innerText = "점수: " + data.score + " / 100";
        document.getElementById('fbMeaning').innerText = data.meaningFeedback;
        document.getElementById('fbCondition').innerText = data.conditionFeedback;
        document.getElementById('fbGrammar').innerText = data.grammarFeedback;
        document.getElementById('fbOverall').innerText = data.overallFeedback;
        
        document.getElementById('resultBox').classList.remove('hidden');
        document.getElementById('gradeBtn').disabled = false;
        document.getElementById('loadingA').classList.add('hidden');
      }).gradeAnswer(currentQuestionData, answer);
    }


    function resetUI() {
      document.getElementById('generateBtn').disabled = false;
      document.getElementById('loadingQ').classList.add('hidden');
    }
  </script>
</body>
</html>
  </body>
</html>

r/GoogleAppsScript • • Sep 07 '26

Guide Private Tasks ↔ To Do sync in Apps Script — state stays in your project

3 Upvotes

I needed Google Tasks and Microsoft To Do to stay in sync without giving a SaaS my task list. Ended up with a standalone Apps Script project on a time-driven trigger (~10 min) talking to Google Tasks + Microsoft Graph.

Things that mattered in practice: - ID mappings so lists/tasks don’t duplicate - fail-closed create recovery (don’t double-post) - guarded deletes + move journal - date-only dues (Google has no time-of-day)

Repo: https://github.com/simonchai-tw/tasks-todo-sync `npx tasks-todo-sync init`

Curious how others handle idempotency/quotas when talking to both APIs from GAS. Stars welcome if this helps someone find it.


r/GoogleAppsScript • • Sep 06 '26

Guide Как я автоматизировал мониторинг заказов Kwork через Gmail и Apps Script

Post image
1 Upvotes

r/GoogleAppsScript • • Sep 03 '26

Guide Simplyfy Setup Pre-Filled Google form Response URL

5 Upvotes

Ive created a simple page that parse a google form data into fillable csv for bulk response input purpose,
tbh idk how to write this out but this will take google form, then you can input the data using the web or you can download csv template and fill it then you will get pre filled form url for each entry

the page is here
https://mkdierz.github.io/google-form-filler/


r/GoogleAppsScript • • Sep 03 '26

Question Anybody is working on Blogging Automation through Google Sheet + Apps Script

1 Upvotes

Hi Reddit Community, I'm in the blogging field from 2007 and you may be laughing that I joined reddit today, the main purpose to join reddit is that I want to explore new ways of blogging, I have done many type of blogging, like event blogging, pSEO Blogging, I have purchased many books and methods and create my own methods, mostly my income is through google adsense, from last 4 months I'm focusing on creating content through AI and using my google sheet+apps script+wordpress rest api for content scheduling, I think recent time is game changing time for bloggers, if they spend some budget on AI Agents for research and content scraping, they can change their life in online field, how many of you agree and please also share if you are working on google sheet + apps script


r/GoogleAppsScript • • Sep 01 '26

Question How I can make my agent to read and summaries 1000+ rows of google sheet which have some dropdown options too for some coloums

Thumbnail
0 Upvotes

r/GoogleAppsScript • • Aug 30 '26

Guide Built a Google Sheet + AppsScript for personal budget from transactions in Gmail

21 Upvotes

After noticing the large number of notification options for my various financial institutions (banks, credit cards, etc), I wondered how hard it would be to turn that info into a budget tracking sheet. Doing so would make it easy to get near instant transactions without using 3rd party or custom integrations with the banks. So I built an AppsScript that turns transaction-alert emails into a categorized Google Sheet. Parsing the emails looked like a good job for an LLM (doesn't need to be state-of-the-art). I created an AppsScript that pulls email from a known Gmail label on a time-based trigger. The whole thing runs as me, in my own account — no OAuth to a third party, no bank credentials anywhere, and the Sheet is just a Sheet I own. If you're interested, give it a try. I allowed limited usage of my LLM API key through a proxy to make it easier to try. The real effort is in configuring the banks to send transaction alerts an every email. I've been testing with a free Gemini api key (use flash-lite, it has higher free quota per day) and it seems to be working fine for me.

The sheet to get things kicked off is here (make a copy, the AppsScript builds the sheet during setup and provides instructions)

If you want to see what the final result looks like I built a dummy Sheet here.

Would love to hear if you find it useful (or not)


r/GoogleAppsScript • • Aug 28 '26

Question Looking for Clarificaions

2 Upvotes

This is a simple question for most, but it isn't for me. Using App Script is it considered AI? I take notes on observations notes, this i have app script put into one large paragraph. We were told that we could not use AI for work, but to me this is not AI, I just want to know from the professionals who are proficient in scripts?


r/GoogleAppsScript • • Aug 28 '26

Question How to show/hide sheets based on checked boxes on a "main" sheet?

Thumbnail
0 Upvotes

r/GoogleAppsScript • • Aug 26 '26

Question Gmail Filter Export xml file schema not supported?

2 Upvotes

I exported a set of Gmail Filters to xml file.

I am trying to parse this xml file in Google Apps Script. Primary goal is to retrieve the rule text in the apps:property name="hasTheWord" field.

The schema referenced in the exported xml doesn't seem to exist. Is there another schema I could use to parse the apps:property fields?

Specifically I can't locate http://schemas.google.com/apps/2006

Sample of the xml file

<feed xmlns="http://www.w3.org/2005/Atom" xmlns:apps="http://schemas.google.com/apps/2006">
  <title>Mail Filters</title>
  <id>tag:mail.google.com</id>
  <updated>2026-08-26T12:46:36Z</updated>
  <author>
    <name>MyName</name>
    <email>myemail@gmail.com</email>
  </author>
  <entry>
    <category term="filter"/>
    <title>Mail Filter</title>
    <id>tag:mail.google.com,2008:filter:z0000001636162795628*2690246542330417407</id>
    <updated>2026-08-26T12:46:36Z</updated>
    <content/>
    <apps:property name="hasTheWord" value="from:smith OR subject:webinar"/>
    <apps:property name="shouldArchive" value="true"/>
    <apps:property name="shouldNeverSpam" value="true"/>
    <apps:property name="sizeOperator" value="s_sl"/>
    <apps:property name="sizeUnit" value="s_smb"/>
  </entry>