
qGuide: Request to OpenAI API using localStorage v202606
qGuide: OpenAI API にリクエスト (localStorage版) v202606
Sends the text and file information stored in specified data fields 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 “instructions” (up to eight items) must be saved in localStorage by each user.
Input / Output
- ←
user_config_area - ←
user_buttons_area - ←
user_files_area - ← localStorage
- →
pre#user_result - →
div#user_status
Please use web storage after understanding its functions and risks.
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>
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
See Also




