qGuide: Request to OpenAI API using localStorage
qGuide: OpenAI API にリクエスト (localStorage版)
Sends the Input-string to the Responses API via CORS. The responses (the sentence generated by the model) will be displayed in streaming format. Depending on the prompt settings, various support functions to assist task operators can be provided, such as a typo check function, a text rewrite function, and a list of possible reasons for rejection. The “API key” and “instruction text” must be saved in localStorage by each user.
Input / Output
- ← STRING (STRING_TEXTFIELD)
q_human_answer_body - ← localStorage
apiKey - ← localStorage
instruction - →
pre#user_result - →
div#user_status
Please use web storage after understanding its functions and risks.
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>
Freely modifiable HTML/JavaScript code, MIT License. No warranty of any kind.
(Decoration using JavaScript is only available in the Professional edition: M213)
(Decoration using JavaScript is only available in the Professional edition: M213)
Capture

Capture




