
qGuide: OpenAI API にリクエスト (localStorage版) v202606
qGuide: Request to OpenAI API using localStorage v202606
指定データ項目に格納されているテキスト情報とファイル情報を Responses API に CORS 送信します。レスポンス(モデルが生成した文章)は、タスク処理画面上でストリーミング表示されます。プロンプト設定次第で、「誤植チェック機能」「文章リライト機能」「差戻理由の候補列挙機能」といった様々な支援機能(タスク処理者を支援する機能)を提供することが可能です。”APIキー” と “指示文” (最大8個)は、それぞれのユーザが localStorage に保存します。
Input / Output
- ←
user_config_area - ←
user_buttons_area - ←
user_files_area - ← localStorage
- →
pre#user_result - →
div#user_status
Webストレージの機能およびリスクを理解したうえでご活用ください。
Code Example
HTML/JavaScript (click to open)
<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_input {
width: 100%;
box-sizing: border-box;
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
font-size: 13px;
margin-bottom: 4px;
}
textarea.user_input { 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;
margin-right: 8px;
}
.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_text {
font: 12px/1.4 system-ui, sans-serif;
opacity: 0.75;
margin-top: 8px;
min-height: 1.4em;
display: block;
}
/* ファイル一覧用スタイル */
.user_file_cb_label {
display: inline-block;
margin-right: 16px;
font-size: 13px;
cursor: pointer;
font-family: system-ui, -apple-system, sans-serif;
}
.user_file_cb {
margin-right: 4px;
vertical-align: middle;
}
@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>
<span id="user_files_area" style="display:none; margin-bottom:12px; background:#e2e8f0; padding:8px 12px; border-radius:4px;"></span>
<span id="user_buttons_area" style="display:block;"></span>
<span id="user_config_area" style="display:none; padding:16px; background:#f5f7fa; border: 1px solid #cbd5e0; border-radius:8px; margin-bottom:12px;"></span>
<pre id="user_result"></pre>
<span id="user_status" class="user_status_text"></span>
<script>
qbpms.form.on('ready', () => {
// --- 0. 基本設定★★★ 編集してください / EDIT here ★★★ ---
const TARGET_FIELD_NAMES = "title,q_body"; // カンマ区切りで複数指定可能
const TARGET_FILES_FIELD_NAME = "q_attached"; // ファイル型フィールド名 (空文字で無効)
const STORAGE_PREFIX = "user_inquiry_response_"; // localStorage Memory Prefix (アプリ・工程ごとなど)
// --- デフォルト設定 ---
const DEFAULT_MODEL = "gpt-5.4-mini";
const DEFAULT_INST = [
{ title: "回答草稿500文字", body: "お客様からの「問い合わせタイトル」と「問い合わせ本文」を基に『回答文の草稿』を書いて。500文字程度で。" },
{ title: "景表法チェック", body: "景品表示法に照らして、「広告文案」を評価して。『合格・要修正・NG』の3段階で判定し、その理由を述べて。" },
{ title: "vCard生成", body: "名刺画像やメール本文から個人情報を抽出し、Google Contact (連絡先) 用の vCard 文字列にして。(.vcf ファイル用のデータ)" },
{ title: "論理飛躍の指摘", body: "論理飛躍を指摘してください。特に、前提不足、因果関係の弱さ、主語のすり替わり、抽象語によるごまかし、課題・解決策・効果の不一致を確認してください。読者が「なぜそう言えるのか?」と感じる箇所を列挙し、それぞれ不足している説明と改善案を簡潔に示してください。" },
{ title: "M5", body: "レビューして" },
{ title: "M6", body: "レビューして" },
{ title: "M7", body: "レビューして" },
{ title: "M8", body: "レビューして" }
];
// ストレージキー
const KEY_API = STORAGE_PREFIX + "openai_api_key";
const KEY_MODEL = STORAGE_PREFIX + "openai_model";
const KEY_INST_TITLE = STORAGE_PREFIX + "openai_inst_title_";
const KEY_INST_BODY = STORAGE_PREFIX + "openai_inst_body_";
const filesArea = document.getElementById("user_files_area");
const buttonsArea = document.getElementById("user_buttons_area");
const configArea = document.getElementById("user_config_area");
const resultElement = document.getElementById("user_result");
const statusElement = document.getElementById("user_status");
// Questetraのコンテキストパス
const qctx = window.qbpms?.contextPath || "";
let isConfigOpen = false;
// --- ユーティリティ: ArrayBuffer を Base64 に変換 ---
function user_arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
// --- ファイル一覧の描画関数 ---
function renderFileList() {
filesArea.innerHTML = "";
if (!TARGET_FILES_FIELD_NAME) {
filesArea.style.display = "none";
return;
}
const files = qbpms.form.get(TARGET_FILES_FIELD_NAME);
if (!files || files.length === 0) {
filesArea.style.display = "none";
return;
}
filesArea.style.display = "block";
const lbl = document.createElement("span");
lbl.className = "user_lbl";
lbl.style.marginTop = "0";
lbl.innerText = "Attachments (Check target files):";
filesArea.appendChild(lbl);
files.forEach((file) => {
const label = document.createElement("label");
label.className = "user_file_cb_label";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.value = file.id;
cb.dataset.filename = file.name;
cb.dataset.pdiid = file.processDataInstanceId;
cb.checked = true;
cb.className = "user_file_cb";
label.appendChild(cb);
label.appendChild(document.createTextNode(file.name));
filesArea.appendChild(label);
});
}
// --- 1. 初期描画とファイル項目の変更監視 ---
renderFileList();
if (TARGET_FILES_FIELD_NAME) {
qbpms.form.on('change', TARGET_FILES_FIELD_NAME, renderFileList);
}
// --- 2. メインボタン群の動的生成 ---
const btnConfig = document.createElement("button");
btnConfig.type = "button";
btnConfig.id = "user_btnConfig";
btnConfig.innerText = "CONFIG";
buttonsArea.appendChild(btnConfig);
const aiButtons = [];
for (let i = 0; i < 8; i++) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "user_aiBtn";
buttonsArea.appendChild(btn);
aiButtons.push(btn);
}
// --- 3. CONFIGエリアの動的生成 ---
const lblApi = document.createElement("span"); lblApi.className = "user_lbl"; lblApi.innerText = "APIキー / API Key (マスクなし):"; configArea.appendChild(lblApi);
const inpApi = document.createElement("input"); inpApi.type = "text"; inpApi.className = "user_input"; inpApi.placeholder = "sk-..."; configArea.appendChild(inpApi);
const lblModel = document.createElement("span");
lblModel.className = "user_lbl";
lblModel.innerHTML = 'モデル / Model: <a href="https://platform.openai.com/docs/models" target="_blank" rel="noopener noreferrer" style="font-weight: normal; font-size: 12px; margin-left: 12px; color: #2b6cb0; text-decoration: underline;">⇒ Available Models (OpenAI Docs)</a>';
configArea.appendChild(lblModel);
const inpModel = document.createElement("input"); inpModel.type = "text"; inpModel.className = "user_input"; inpModel.placeholder = "gpt-5.4"; configArea.appendChild(inpModel);
const instInputs = [];
for (let i = 0; i < 8; i++) {
const lbl = document.createElement("span"); lbl.className = "user_lbl"; lbl.innerText = `指示文${i + 1} / Instruction ${i + 1}:`; configArea.appendChild(lbl);
const inpT = document.createElement("input"); inpT.type = "text"; inpT.className = "user_input"; inpT.placeholder = "Instruction Title"; configArea.appendChild(inpT);
const inpB = document.createElement("textarea"); inpB.className = "user_input"; inpB.placeholder = "Instruction Details"; configArea.appendChild(inpB);
instInputs.push({ title: inpT, body: inpB });
}
const hr = document.createElement("hr"); hr.style.margin = "16px 0"; hr.style.borderTop = "1px dashed #ccc"; configArea.appendChild(hr);
const btnSave = document.createElement("button"); btnSave.type = "button"; btnSave.className = "user_actionBtn"; btnSave.innerText = "設定を保存 / Save"; configArea.appendChild(btnSave);
const btnClear = document.createElement("button"); btnClear.type = "button"; btnClear.className = "user_actionBtn"; btnClear.innerText = "初期化 / Reset"; configArea.appendChild(btnClear);
const saveStatus = document.createElement("span"); saveStatus.style.color = "#0066cc"; saveStatus.style.fontWeight = "bold"; saveStatus.style.fontSize = "13px"; configArea.appendChild(saveStatus);
// --- 4. 設定値の読み込み ---
function loadSettings() {
inpApi.value = localStorage.getItem(KEY_API) || "";
inpModel.value = localStorage.getItem(KEY_MODEL) || DEFAULT_MODEL;
for (let i = 0; i < 8; i++) {
const storedT = localStorage.getItem(KEY_INST_TITLE + i);
const storedB = localStorage.getItem(KEY_INST_BODY + i);
instInputs[i].title.value = storedT !== null ? storedT : DEFAULT_INST[i].title;
instInputs[i].body.value = storedB !== null ? storedB : DEFAULT_INST[i].body;
aiButtons[i].innerText = instInputs[i].title.value;
}
}
loadSettings();
// --- 5. 各種イベントリスナー ---
btnConfig.addEventListener("click", () => {
isConfigOpen = !isConfigOpen;
configArea.style.display = isConfigOpen ? "block" : "none";
if (isConfigOpen) {
btnConfig.classList.add("user_active");
loadSettings();
} else {
btnConfig.classList.remove("user_active");
}
});
btnSave.addEventListener("click", () => {
localStorage.setItem(KEY_API, inpApi.value.trim());
localStorage.setItem(KEY_MODEL, inpModel.value.trim());
for (let i = 0; i < 8; i++) {
localStorage.setItem(KEY_INST_TITLE + i, instInputs[i].title.value);
localStorage.setItem(KEY_INST_BODY + i, instInputs[i].body.value);
aiButtons[i].innerText = instInputs[i].title.value;
}
saveStatus.innerText = " 設定を保存しました / Saved";
setTimeout(() => { saveStatus.innerText = ""; }, 2500);
});
btnClear.addEventListener("click", () => {
if (!confirm("すべての設定をデフォルトに戻しますか? / Do you want to revert to the default?")) return;
localStorage.removeItem(KEY_API);
localStorage.removeItem(KEY_MODEL);
for (let i = 0; i < 8; i++) {
localStorage.removeItem(KEY_INST_TITLE + i);
localStorage.removeItem(KEY_INST_BODY + i);
}
loadSettings();
saveStatus.innerText = " 初期化しました / Initialized ";
setTimeout(() => { saveStatus.innerText = ""; }, 2500);
});
// --- テキスト入力値の抽出 ---
function getTargetTextInputs() {
let combinedInput = "";
if (TARGET_FIELD_NAMES) {
const fields = TARGET_FIELD_NAMES.split(",");
fields.forEach(f => {
const val = qbpms.form.get(f.trim());
if (val) combinedInput += `【${f.trim()}】\n${val}\n\n`;
});
}
return combinedInput.trim();
}
// --- 6. AI呼び出し処理 ---
aiButtons.forEach((btn, index) => {
btn.addEventListener("click", async () => {
const strKey = localStorage.getItem(KEY_API);
const modelName = localStorage.getItem(KEY_MODEL) || DEFAULT_MODEL;
const strInstruction = instInputs[index].body.value;
const strInput = getTargetTextInputs();
if (!strKey) {
statusElement.innerText = "ERROR: The API key has not been set. Save the setting in CONFIG.";
resultElement.innerText = "";
return;
}
// 処理開始: UIの無効化
aiButtons.forEach(b => b.disabled = true);
btnConfig.disabled = true;
resultElement.innerHTML = '';
const contentParts = [];
const uploadedFileIds = []; // クリーンアップ用のID保持配列
let processHasError = false; // エラー表示の上書き防止フラグ
if (strInput) {
contentParts.push({ type: "input_text", text: strInput });
}
// --- [ファイル取得・アップロード処理] ---
if (TARGET_FILES_FIELD_NAME) {
const checkboxes = document.querySelectorAll(".user_file_cb");
for (const cb of checkboxes) {
if (cb.checked) {
const qfileId = cb.value;
const filename = cb.dataset.filename;
const pdiId = cb.dataset.pdiid;
statusElement.innerText = ` ${filename} downloading ...`;
try {
const fetchUrl = `${qctx}/API/OR/ProcessInstance/File/download?id=${encodeURIComponent(qfileId)}&processDataInstanceId=${encodeURIComponent(pdiId)}`;
const dlRes = await fetch(fetchUrl);
if (!dlRes.ok) {
throw new Error(`HTTP Error: ${dlRes.status}`);
}
const contentType = dlRes.headers.get('content-type') || 'application/octet-stream';
const buf = await dlRes.arrayBuffer();
if (contentType.startsWith('image/')) {
const base64 = user_arrayBufferToBase64(buf);
const dataUrl = `data:${contentType};base64,${base64}`;
contentParts.push({ type: "input_image", image_url: dataUrl });
statusElement.innerText = `画像「${filename}」を添付しました。/ Attached`;
} else {
statusElement.innerText = ` ${filename} uploading ...`;
const formData = new FormData();
const blob = new Blob([new Uint8Array(buf)], { type: contentType });
const fileObj = (typeof File !== 'undefined') ? new File([blob], filename, { type: contentType }) : blob;
formData.append("purpose", "user_data");
formData.append("file", fileObj, filename);
const upRes = await fetch("https://api.openai.com/v1/files", {
method: "POST",
headers: { "Authorization": `Bearer ${strKey}` },
body: formData
});
if (!upRes.ok) {
let msg = `OpenAI File Upload Error: ${upRes.status}`;
try { msg += `\n${JSON.stringify(await upRes.json(), null, 2)}`; } catch (_) {}
throw new Error(msg);
}
const upJson = await upRes.json();
const uploadedFileId = upJson.id;
uploadedFileIds.push(uploadedFileId); // クリーンアップ対象として保持
contentParts.push({ type: 'input_file', file_id: uploadedFileId });
statusElement.innerText = `ファイル「${filename}」を添付しました。/ Attached`;
}
} catch (error) {
console.error("File fetch/upload error:", error);
statusElement.innerText = `Error: Failed to process the file "${filename}".`;
aiButtons.forEach(b => b.disabled = false);
btnConfig.disabled = false;
return;
}
}
}
}
if (contentParts.length === 0) {
statusElement.innerText = `Error: 対象フィールドまたはファイル が空です。/ The target field or file is empty.`;
aiButtons.forEach(b => b.disabled = false);
btnConfig.disabled = false;
return;
}
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",
"Accept": "text/event-stream",
"Authorization": `Bearer ${strKey}`
},
body: JSON.stringify({
model: modelName,
instructions: strInstruction,
input: [ { role: "user", content: contentParts } ],
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";
let data = null;
try {
data = JSON.parse(dataRaw);
} catch (e) {
// パース失敗時は握りつぶさずコンソールに出力してスキップ
console.error("SSE JSON parse error:", dataRaw, e);
continue;
}
// ★イベント処理とパースを分離
if (currentEvent === "response.output_text.delta" && data.delta) {
cursor.insertAdjacentText("beforebegin", data.delta);
} else if (currentEvent === "response.completed") {
return "DONE";
} else if (currentEvent === "error" || currentEvent === "response.error") {
// ここで throw すれば、外側の catch (error) で確実に補足・画面表示されます
throw new Error(data?.error?.message || "Streaming API Error");
}
}
}
};
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;
}
} catch (error) {
processHasError = true;
console.error("Request failed:", error);
statusElement.innerText = "エラーが発生しました。 / An error occurred.";
cursor?.insertAdjacentText("beforebegin", `\n[Exception] ${String(error.message || error)}`);
} finally {
const c = document.querySelector("#user_result .user_cursor");
c?.remove();
// ★アップロードしたファイルの削除クリーンアップ処理
if (uploadedFileIds.length > 0 && strKey) {
if (!processHasError) {
statusElement.innerText = "生成完了 (一時ファイルを削除中...) / Cleaning up files...";
}
for (const fid of uploadedFileIds) {
try {
await fetch(`https://api.openai.com/v1/files/${fid}`, {
method: "DELETE",
headers: { "Authorization": `Bearer ${strKey}` }
});
} catch (cleanupErr) {
console.error(`Failed to delete file ${fid}:`, cleanupErr);
}
}
}
if (!processHasError) {
statusElement.innerText = "処理完了 / Completed";
}
aiButtons.forEach(b => b.disabled = false);
btnConfig.disabled = false;
}
});
});
});
</script>
自由改変可能な HTML/JavaScript コードです (MIT License)。いかなる保証もありません。
(JavaScript を用いたデコレーションは Professional editionでのみ利用可能です: M213)
(JavaScript を用いたデコレーションは Professional editionでのみ利用可能です: M213)
Capture

Capture
See Also




