qGuide: OpenAI API にリクエスト (localStorage版)
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)
(JavaScript を用いたデコレーションは Professional editionでのみ利用可能です: M213)
Capture

Capture




