qGuide: OpenAI API にリクエスト (localStorage版)

qGuide: OpenAI API にリクエスト (localStorage版)

qGuide: OpenAI API にリクエスト (localStorage版)

translate qGuide: Request to OpenAI API using localStorage

文字列 “投入データ” を Responses API に CORS 送信します。レスポンス(モデルが生成した文章)は、タスク処理画面上でストリーミング表示されます。プロンプト設定次第で、「誤植チェック機能」「文章リライト機能」「差戻理由の候補列挙機能」といった様々な支援機能(タスク処理者を支援する機能)を提供することが可能です。”APIキー” と “指示文” は、それぞれのユーザが localStorage に保存する必要があります。

Input / Output

  • ← STRING (STRING_TEXTFIELD) q_human_answer_body
  • ← localStorage apiKey
  • ← localStorage instruction
  • pre#user_result
  • div#user_status

Webストレージの機能およびリスクを理解したうえでご活用ください。

Code Example

HTML/JavaScript (click to close)
<style>
/* AI呼び出しボタン */
.user_aiBtn {
border: 1px solid #ccc;
padding: 6px 12px;
border-radius: 20px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
color: #333;
margin-right: 8px;
margin-bottom: 12px;
background-color: #fff;
font-family: system-ui, -apple-system, sans-serif;
}
.user_aiBtn:hover { background-color: #f0f0f0; border-color: #bbb; }
.user_aiBtn:disabled { opacity: 0.5; cursor: not-allowed; background-color: #eee; }

/* CONFIGボタン */
#user_btnConfig {
border: 1px solid #4a5568;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
transition: all 0.2s ease;
color: #fff;
background-color: #4a5568;
margin-right: 16px;
margin-bottom: 12px;
font-family: system-ui, -apple-system, sans-serif;
}
#user_btnConfig:hover { background-color: #2d3748; border-color: #1a202c; }
#user_btnConfig.user_active { background-color: #2b6cb0; border-color: #2c5282; box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); }

/* --- その他の設定エリア要素 --- */
.user_lbl {
display: block;
margin: 12px 0 4px 0;
font-weight: bold;
font-size: 13px;
font-family: system-ui, -apple-system, sans-serif;
}
.user_display_val {
background: #e9e9e9;
padding: 4px 8px;
border-radius: 4px;
font-family: Menlo, Monaco, Consolas, monospace;
color: #333;
font-size: 13px;
}
.user_display_inst {
background: #e9e9e9;
padding: 8px;
border-radius: 4px;
white-space: pre-wrap;
color: #333;
min-height: 1.5em;
margin-bottom: 4px;
font-size: 13px;
font-family: system-ui, -apple-system, sans-serif;
}
.user_btnEdit {
padding: 2px 8px;
font-size: 11px;
cursor: pointer;
border: 1px solid #bbb;
background-color: #fff;
border-radius: 4px;
margin-left: 8px;
vertical-align: middle;
}
.user_btnEdit:hover { background-color: #eee; }
#user_apiKey, #user_instruction {
width: 100%;
box-sizing: border-box;
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
font-size: 13px;
}
#user_instruction { resize: vertical; min-height: 4em; }
.user_actionBtn {
padding: 4px 12px;
border: 1px solid #bbb;
background-color: #fff;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.user_actionBtn:hover { background-color: #eee; }
#user_result {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
padding: 12px;
min-height: 8em;
white-space: pre-wrap;
word-break: break-word;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
font-size: 14px;
line-height: 1.6;
color: #222;
margin-top: 16px;
}
#user_status {
font: 12px/1.4 system-ui, sans-serif;
opacity: 0.75;
margin-top: 8px;
min-height: 1.4em;
}

@keyframes blink { 50% { opacity: 0; } }
.user_cursor {
display: inline-block;
width: 8px;
height: 1em;
background-color: #333;
margin-left: 2px;
animation: blink 1s step-end infinite;
vertical-align: text-bottom;
}
</style>

<button type="button" id="user_btnConfig">CONFIG</button>
<button type="button" class="user_aiBtn">gpt-5.4</button>
<button type="button" class="user_aiBtn">gpt-5.4-mini</button>
<button type="button" class="user_aiBtn">gpt-5.4-nano</button>
<button type="button" class="user_aiBtn">gpt-5-nano</button>
<button type="button" class="user_aiBtn">gpt-5-mini</button>
<button type="button" class="user_aiBtn">gpt-5</button>
<button type="button" class="user_aiBtn">gpt-5.3-codex</button>
<button type="button" class="user_aiBtn">gpt-5-codex</button>
<span id="user_lbl_api" class="user_lbl" style="display:none;">APIキー / API Key:</span>
<span id="user_apiKey_display" class="user_display_val" style="display:none;"></span>
<button type="button" class="user_btnEdit" id="user_btn_editApiKey" style="display:none;">Edit KEY</button>
<span id="user_apiKey_edit_area" style="display:none; width:100%;"></span>
<span id="user_lbl_inst" class="user_lbl" style="display:none;">指示 / Instruction (入力対象: <span id="user_lbl_target_field"></span>):</span>
<span id="user_instruction_display" class="user_display_inst" style="display:none;"></span>
<button type="button" class="user_btnEdit" id="user_btn_editInst" style="margin-left:0; margin-top:4px; display:none;">Edit PROMPT</button>
<span id="user_inst_edit_area" style="display:none; width:100%;"></span>
<hr id="user_set_hr" style="margin:16px 0 12px 0; border:none; border-top:1px dashed #ccc; display:none;">
<button type="button" id="user_btnSave" class="user_actionBtn" style="display:none;">設定を保存 / Save</button>
<button type="button" id="user_btnClear" class="user_actionBtn" style="display:none;">クリア / Clear</button>
<span id="user_settingStatus" style="margin-left:8px; color:#0066cc; font-weight:bold; font-size:13px; display:none;"></span>
<pre id="user_result"></pre>
<div id="user_status"></div>

<script>
qbpms.form.on('ready', () => {
  // --- 0. 対象フィールド★★★ 編集してください / EDIT here ★★★ ---
  const TARGET_FIELD_NAME = "q_human_answer_body";
  
  // ラベルに対象フィールド名を反映
  document.getElementById("user_lbl_target_field").innerText = TARGET_FIELD_NAME;

  // --- 1. 動的な入力要素の生成 ---
  const apiKeyInput = document.createElement("input");
  apiKeyInput.type = "password";
  apiKeyInput.id = "user_apiKey";
  apiKeyInput.placeholder = "sk-...";
  document.getElementById("user_apiKey_edit_area").appendChild(apiKeyInput);
  const instructionInput = document.createElement("textarea");
  instructionInput.id = "user_instruction";
  instructionInput.placeholder = "例: あなたは優秀なアシスタントです。簡潔に答えてください。 / Ex: You are a helpful assistant...";
  document.getElementById("user_inst_edit_area").appendChild(instructionInput);

  // --- 2. 各種定数と要素の取得 ---
  const STORAGE_KEY_API = "user_openai_api_key";
  const STORAGE_KEY_INST = "user_openai_instruction";
  const btnConfig = document.getElementById("user_btnConfig");
  const aiButtons = document.querySelectorAll(".user_aiBtn");
  const resultElement = document.getElementById("user_result");
  const statusElement = document.getElementById("user_status");
  const settingStatus = document.getElementById("user_settingStatus");

  // UI状態管理フラグ
  let isConfigOpen = false;
  let isEditingApi = false;
  let isEditingInst = false;

  // --- 3. UIの表示切り替え関数 (フラットな要素を個別に制御) ---
  function renderUI() {
    const storedKey = localStorage.getItem(STORAGE_KEY_API) || "";
    const storedInst = localStorage.getItem(STORAGE_KEY_INST) || "";
    apiKeyInput.value = storedKey;
    instructionInput.value = storedInst;
    const hasKey = !!storedKey;
    const hasInst = !!storedInst;
    const showApiDisp = isConfigOpen && hasKey && !isEditingApi;
    const showApiEdit = isConfigOpen && (!hasKey || isEditingApi);
    const showInstDisp = isConfigOpen && hasInst && !isEditingInst;
    const showInstEdit = isConfigOpen && (!hasInst || isEditingInst);

    if (isConfigOpen) {
      btnConfig.classList.add("user_active");
    } else {
      btnConfig.classList.remove("user_active");
    }

    document.getElementById("user_lbl_api").style.display = isConfigOpen ? "block" : "none";
    const eKeyDisp = document.getElementById("user_apiKey_display");
    eKeyDisp.style.display = showApiDisp ? "inline-block" : "none";
    if (showApiDisp) eKeyDisp.innerText = storedKey.length > 10 ? storedKey.substring(0, 10) + "..." : storedKey;
    document.getElementById("user_btn_editApiKey").style.display = showApiDisp ? "inline-block" : "none";
    document.getElementById("user_apiKey_edit_area").style.display = showApiEdit ? "block" : "none";
    document.getElementById("user_lbl_inst").style.display = isConfigOpen ? "block" : "none";
    const eInstDisp = document.getElementById("user_instruction_display");
    eInstDisp.style.display = showInstDisp ? "block" : "none";
    if (showInstDisp) eInstDisp.innerText = storedInst;
    document.getElementById("user_btn_editInst").style.display = showInstDisp ? "inline-block" : "none";
    document.getElementById("user_inst_edit_area").style.display = showInstEdit ? "block" : "none";
    document.getElementById("user_set_hr").style.display = isConfigOpen ? "block" : "none";
    document.getElementById("user_btnSave").style.display = isConfigOpen ? "inline-block" : "none";
    document.getElementById("user_btnClear").style.display = isConfigOpen ? "inline-block" : "none";
    settingStatus.style.display = isConfigOpen ? "inline-block" : "none";
  }

  // 初期化
  renderUI();

  // --- 4. 各種イベントリスナー ---
  btnConfig.addEventListener("click", () => {
    isConfigOpen = !isConfigOpen;
    if (!isConfigOpen) {
      isEditingApi = false;
      isEditingInst = false;
    }
    renderUI();
  });
  document.getElementById("user_btn_editApiKey").addEventListener("click", () => {
    isEditingApi = true;
    renderUI();
    apiKeyInput.focus();
  });
  document.getElementById("user_btn_editInst").addEventListener("click", () => {
    isEditingInst = true;
    renderUI();
    instructionInput.focus();
  });
  document.getElementById("user_btnSave").addEventListener("click", () => {
    localStorage.setItem(STORAGE_KEY_API, apiKeyInput.value.trim());
    localStorage.setItem(STORAGE_KEY_INST, instructionInput.value);
    isEditingApi = false;
    isEditingInst = false;
    renderUI();
    settingStatus.innerText = "設定を保存しました / Saved successfully.";
    setTimeout(() => { settingStatus.innerText = ""; }, 2500);
  });
  document.getElementById("user_btnClear").addEventListener("click", () => {
    localStorage.removeItem(STORAGE_KEY_API);
    localStorage.removeItem(STORAGE_KEY_INST);
    isEditingApi = false;
    isEditingInst = false;
    renderUI();
    settingStatus.innerText = "設定をクリアしました / Cleared successfully.";
    setTimeout(() => { settingStatus.innerText = ""; }, 2500);
  });

  // --- 5. AI呼び出し処理 ---
  aiButtons.forEach(button => {
    button.addEventListener("click", async (event) => {
      const strKey = localStorage.getItem(STORAGE_KEY_API);
      const strInstruction = localStorage.getItem(STORAGE_KEY_INST) || ""; 
      // 変数化したターゲットフィールド名を利用
      const strInput = qbpms.form.get(TARGET_FIELD_NAME);
      const modelName = event.currentTarget.innerText.trim();

      if (!strKey) {
        statusElement.innerText = "エラー: APIキーが設定されていません。CONFIGボタンから設定を保存してください。 / Error: API Key is not set.";
        resultElement.innerText = "";
        return;
      }
      if (!strInput) {
        statusElement.innerText = `エラー: 入力内容(${TARGET_FIELD_NAME})が空です。 / Error: Input field is empty.`;
        resultElement.innerText = "";
        return;
      }

      aiButtons.forEach(btn => btn.disabled = true);
      btnConfig.disabled = true;
      statusElement.innerText = "AIに接続中... / Connecting to AI...";
      resultElement.innerHTML = '<span class="user_cursor"></span>';
      const cursor = resultElement.querySelector(".user_cursor");

      try {
        const response = await fetch("https://api.openai.com/v1/responses", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${strKey}`
          },
          body: JSON.stringify({
            model: modelName,
            instructions: strInstruction,
            input: strInput,
            stream: true
          })
        });

        if (!response.ok) {
          let msg = `API Error: ${response.status}`;
          try {
            const err = await response.json();
            msg += `\n${JSON.stringify(err, null, 2)}`;
          } catch (_) {}
          throw new Error(msg);
        }
        statusElement.innerText = "AIが文章を生成中... / AI is generating text...";
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        let currentEvent = "";
        const flushLines = (chunkText) => {
          buffer += chunkText;
          const lines = buffer.split(/\r?\n/);
          buffer = lines.pop() || "";
          for (const line of lines) {
            if (!line.trim()) continue;
            if (line.startsWith("event:")) {
              currentEvent = line.replace(/^event:\s*/, "").trim();
              continue;
            }
            if (line.startsWith("data:")) {
              const dataRaw = line.replace(/^data:\s*/, "").trim();
              if (dataRaw === "[DONE]") return "DONE";
              
              try {
                const data = JSON.parse(dataRaw);
                if (currentEvent === "response.output_text.delta" && data.delta) {
                  cursor.insertAdjacentText("beforebegin", data.delta);
                }
                if (currentEvent === "response.completed") {
                  return "DONE";
                }
                if (currentEvent === "response.error") {
                  throw new Error(data?.error?.message || "Unknown streaming error");
                }
              } catch (e) {
                console.error("SSE JSON parse error:", dataRaw, e);
              }
            }
          }
        };
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          const text = decoder.decode(value, { stream: true });
          const status = flushLines(text);
          if (status === "DONE") break;
        }
        statusElement.innerText = "生成完了 / Generation Completed";
      } catch (error) {
        console.error("Request failed:", error);
        statusElement.innerText = "エラーが発生しました。 / An error occurred.";
        resultElement.innerText = String(error.message || error);
      } finally {
        const c = document.querySelector("#user_result .user_cursor");
        c?.remove();
        aiButtons.forEach(btn => btn.disabled = false);
        btnConfig.disabled = false;
      }
    });
  });
});
</script>
自由改変可能な HTML/JavaScript コードです (MIT License)。いかなる保証もありません。
(JavaScript を用いたデコレーションProfessional editionでのみ利用可能です: M213)

Capture

Capture

See Also

上部へスクロール

Questetra Supportをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む