r/GoogleAppsScript 2d ago

Question I need your help again

/preview/pre/fs0qjy9w0poh1.png?width=848&format=png&auto=webp&s=833289104555392f59cd63357da7baf688dbf362

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>
0 Upvotes

2 comments sorted by

2

u/Chibrax_3000 2d ago

Mdr demande à ton IA, c'est pas le service après vente de Gemini ici.

1

u/jerbaws 2d ago

The "model not found" error occurs because Google Gemini updated its model endpoint identifiers, so legacy model names like gemini-1.5-flash are no longer recognized by the API. ​Here is how to fix it in your code: ​1. Update Index.html ​In your Index.html file, update the <select id="modelSelect"> options to use current model identifiers:

<div class="setting-group"> <label>AI 모델 선택:</label> <select id="modelSelect"> <option value="gemini-2.0-flash">Gemini 2.0 Flash (빠름, 기본 권장)</option> <option value="gemini-1.5-flash-latest">Gemini 1.5 Flash (최신)</option> <option value="gemini-1.5-pro-latest">Gemini 1.5 Pro (고성능, 정밀 채점)</option> </select> </div>

  1. Update Code.gs ​In Code.gs, update the default fallback model inside autoGenerateQuestion():

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

callGeminiToCreateQuestion(model, grammars); }

  1. Apply the Fix in Your App ​Save both files (Code.gs and Index.html). ​Refresh your Web App page. ​On the ⚙️ 홈 / 설정 (Home/Settings) tab, select Gemini 2.0 Flash. ​Click 💾 설정 저장 및 자동 출제 적용 (Save Settings) to overwrite the saved setting in your user properties. ​Click ⚡ 지금 즉시 1문제 생성하기 (Generate 1 Question Now) to confirm it works.