qGuide: Request to OpenAI API with Image
The “input file” (image or pdf), the “input data” string, and the “API key” are sent to the Responses API via CORS. The response (text generated by the model) is streamed on the Task Form screen. Depending on the prompt settings, various support functions (functions to assist the task processor) can be provided, such as “analyzing ancient documents,” “detecting fault locations,” “counting populations,” and “extracting RGB color codes.”
Input / Output
- ← FILES
q_inputFiles - →
div#user_RadioPanel - ← STRING (STRING_TEXTFIELD)
q_input - ← STRING (STRING_TEXTFIELD)
q_instruction - ← STRING (STRING_TEXTFIELD)
q_openAiApiKey - →
pre#user_result - →
div#user_status
Code Example
HTML/JavaScript (click to close)
<style>
/* AI呼び出しボタン */
.user_aiBtn {
border: 1px solid #ccc;
background-color: #fff;
padding: 6px 12px;
border-radius: 20px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
color: #333;
margin-right: 8px; /* ボタン間の余白 */
margin-bottom: 12px;
}
.user_aiBtn:hover {
background-color: #f0f0f0;
border-color: #bbb;
}
.user_aiBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
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: 4px;
}
/* ステータス表示エリア */
#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;
}
.user_aiBtn2 {
border: 1px solid #ccc;
background-color: #fff;
padding: 1px 2px;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
color: #999;
margin-right: 8px;
margin-bottom: 1px;
}
</style>
▽ Input IMAGE/FILE: <button type='button' onclick='user_noFileSelected()' class='user_aiBtn2'>clear</button>
<div id="user_RadioPanel">(no file attached ...)</div>
<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-codex</button>
<button type="button" class="user_aiBtn">gpt-4o</button>
<button type="button" class="user_aiBtn">gpt-4o-mini</button>
<pre id="user_result"></pre>
<div id="user_status"></div>
<script>
function user_noFileSelected() {
const radios = document.querySelectorAll('input[type="radio"][name="user_fileSelector"]');
radios.forEach(radio => { radio.checked = false; });
}
const user_fieldFILES = 'q_inputFiles'; // ★★★ EDIT FieldName ★★★
qbpms.form.on('ready', () => {
user_refreshRadioPanel();
qbpms.form.on('change', user_fieldFILES, user_refreshRadioPanel);
});
function user_refreshRadioPanel () {
const elRadioPanel = document.querySelector("#user_RadioPanel");
while (elRadioPanel.firstChild) elRadioPanel.removeChild(elRadioPanel.firstChild);
const files = qbpms.form.get(user_fieldFILES) || [];
console.log('=== #of Files: ' + files.length + ' ===');
if (files.length === 0) {
elRadioPanel.innerText = '(No File Attached ...)';
}
files.forEach((file, i) => {
const elRadio = document.createElement('input');
elRadio.type = 'radio';
elRadio.id = `user_file_${i}`;
elRadio.name = 'user_fileSelector';
elRadio.value = String(i);
elRadio.dataset.qfileId = file.id;
elRadio.dataset.pdiId = file.processDataInstanceId;
elRadio.dataset.contentType = file.contentType || 'application/octet-stream';
elRadio.dataset.filename = file.name || 'input.bin';
if (i === 0) elRadio.checked = true;
const elLabel = document.createElement('label');
elLabel.htmlFor = elRadio.id;
elLabel.style.cursor = 'pointer';
elLabel.style.marginRight = '15px';
elLabel.appendChild(elRadio);
elLabel.appendChild(document.createTextNode(` ${file.name} (${file.contentType})`));
elRadioPanel.appendChild(elLabel);
elRadioPanel.appendChild(document.createElement("br"));
});
}
/** Questetra context path:
* https://<tenant>.questetra.net or https://s.questetra.net/<8digits>
*/
function user_getQcontextPath () {
const re = /https:\/\/[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]\.questetra\.net|https:\/\/s\.questetra\.net\/\d{8}/;
const m = location.href.match(re);
if (!m) { console.error("DecorationError: Not Questetra BPM Suite URL"); return null; }
return m[0];
}
/** ArrayBuffer -> Base64 */
function user_arrayBufferToBase64(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const chunk = 0x8000; // 32KB
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
return btoa(binary);
}
// ========== /v1/responses ストリーミング実行 ==========
const aiButtons = document.querySelectorAll(".user_aiBtn");
aiButtons.forEach(button => {
button.addEventListener("click", async (event) => {
const strKey = qbpms.form.get("q_openAiApiKey"); // ★★★ EDIT FieldName ★★★
const strInstruction = qbpms.form.get("q_instruction") || ""; // ★★★ EDIT FieldName ★★★
const strInput = qbpms.form.get("q_input"); // ★★★ EDIT FieldName ★★★
const resultEl = document.getElementById("user_result");
const statusEl = document.getElementById("user_status");
const modelName = event.currentTarget.innerText.trim();
if (!strKey || !strInput) {
statusEl.innerText = "エラー: APIキーと入力内容は必須です。";
resultEl.innerText = "";
return;
}
aiButtons.forEach(btn => btn.disabled = true);
statusEl.innerText = "準備中...";
resultEl.innerHTML = '<span class="user_cursor"></span>';
const cursor = resultEl.querySelector(".user_cursor");
let streamDone = false;
try {
// --- 添付ファイルの準備 ---
const selectedRadio = document.querySelector('input[name="user_fileSelector"]:checked');
let contentParts = [{ type: "input_text", text: String(strInput) }];
if (selectedRadio) {
const qctx = user_getQcontextPath();
if (!qctx) throw new Error("Questetraのコンテキストパスが取得できません。");
const qfileId = selectedRadio.dataset.qfileId;
const pdiId = selectedRadio.dataset.pdiId;
const contentType = selectedRadio.dataset.contentType || 'application/octet-stream';
const filename = selectedRadio.dataset.filename || 'input.bin';
const fetchUrl = `${qctx}/API/OR/ProcessInstance/File/download?id=${encodeURIComponent(qfileId)}&processDataInstanceId=${encodeURIComponent(pdiId)}`;
statusEl.innerText = `ファイル「${filename}」を取得中...`;
const dlRes = await fetch(fetchUrl, { credentials: 'same-origin' });
if (!dlRes.ok) throw new Error(`ファイルのダウンロード失敗: ${dlRes.status}`);
const buf = await dlRes.arrayBuffer();
if (contentType.startsWith('image/')) { // 画像は Base64 data URL で直接リクエストに含める
const base64 = user_arrayBufferToBase64(buf);
const dataUrl = `data:${contentType};base64,${base64}`;
contentParts.push({ type: "input_image", image_url: dataUrl });
statusEl.innerText = "画像を添付しました。";
} else { // PDFやOffice文書など、画像以外は Files API にアップロードしてIDで参照する
statusEl.innerText = "ファイルをアップロード中...";
const formData = new FormData();
const blob = new Blob([new Uint8Array(buf)], { type: contentType });
// Fileオブジェクトが使える環境では、ファイル名を指定して作成
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; // 例: file_abc123
// アップロードしたファイルのIDを参照する形でリクエストに含める
contentParts.push({ type: 'input_file', file_id: uploadedFileId });
statusEl.innerText = "ファイルを添付しました。";
}
}
// --- /v1/responses でストリーミング ---
statusEl.innerText = "AIに接続中...";
const res = 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 (!res.ok) {
let msg = `API Error: ${res.status}`;
try { msg += `\n${JSON.stringify(await res.json(), null, 2)}`; } catch(_){}
throw new Error(msg);
}
statusEl.innerText = "AIが文章を生成中...";
// --- SSE (Responses API) ---
const reader = res.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]") { streamDone = true; 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") { streamDone = true; 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;
}
statusEl.innerText = "生成が完了しました。";
} catch (error) {
console.error("Request failed:", error);
statusEl.innerText = "エラーが発生しました。";
resultEl.innerText = String(error.message || error);
} finally {
const c = document.querySelector("#user_result .user_cursor");
if (c) c.remove();
aiButtons.forEach(btn => btn.disabled = false);
}
});
});
</script>HTML/JavaScript Checkbox version (click to open)
<style>
/* AI呼び出しボタン */
.user_aiBtn {
border: 1px solid #ccc;
background-color: #fff;
padding: 6px 12px;
border-radius: 20px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
color: #333;
margin-right: 8px; /* ボタン間の余白 */
margin-bottom: 12px;
}
.user_aiBtn:hover {
background-color: #f0f0f0;
border-color: #bbb;
}
.user_aiBtn:disabled {
opacity: 0.5;
cursor: not-allowed;
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: 4px;
}
/* ステータス表示エリア */
#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;
}
.user_aiBtn2 {
border: 1px solid #ccc;
background-color: #fff;
padding: 1px 2px;
border-radius: 5px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
color: #999;
margin-right: 8px;
margin-bottom: 1px;
}
</style>
▽ Input IMAGE/FILE: <button type='button' onclick='user_noFileSelected()' class='user_aiBtn2'>clear</button>
<div id="user_CheckboxPanel">(no file attached ...)</div>
<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-codex</button>
<button type="button" class="user_aiBtn">gpt-4o</button>
<button type="button" class="user_aiBtn">gpt-4o-mini</button>
<pre id="user_result"></pre>
<div id="user_status"></div>
<script>
function user_noFileSelected() {
const checkboxes = document.querySelectorAll('input[type="checkbox"][name="user_fileSelector"]');
checkboxes.forEach(cb => { cb.checked = false; });
}
const user_fieldFILES = 'q_inputFiles'; // ★★★ EDIT FieldName ★★★
qbpms.form.on('ready', () => {
user_refreshCheckboxPanel();
qbpms.form.on('change', user_fieldFILES, user_refreshCheckboxPanel);
});
function user_refreshCheckboxPanel() {
const elPanel = document.querySelector("#user_CheckboxPanel");
while (elPanel.firstChild) elPanel.removeChild(elPanel.firstChild);
const files = qbpms.form.get(user_fieldFILES) || [];
console.log('=== #of Files: ' + files.length + ' ===');
if (files.length === 0) {
elPanel.innerText = '(No File Attached ...)';
return;
}
files.forEach((file, i) => {
const elCb = document.createElement('input');
elCb.type = 'checkbox';
elCb.id = `user_file_${i}`;
elCb.name = 'user_fileSelector';
elCb.value = String(i);
elCb.dataset.qfileId = file.id;
elCb.dataset.pdiId = file.processDataInstanceId;
elCb.dataset.contentType = file.contentType || 'application/octet-stream';
elCb.dataset.filename = file.name || 'input.bin';
const elLabel = document.createElement('label');
elLabel.htmlFor = elCb.id;
elLabel.style.cursor = 'pointer';
elLabel.style.marginRight = '15px';
elLabel.appendChild(elCb);
elLabel.appendChild(document.createTextNode(` ${file.name} (${file.contentType})`));
elPanel.appendChild(elLabel);
elPanel.appendChild(document.createElement("br"));
});
}
/** Questetra context path */
function user_getQcontextPath() {
const re = /https:\/\/[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9]\.questetra\.net|https:\/\/s\.questetra\.net\/\d{8}/;
const m = location.href.match(re);
if (!m) { console.error("DecorationError: Not Questetra BPM Suite URL"); return null; }
return m[0];
}
/** ArrayBuffer -> Base64 */
function user_arrayBufferToBase64(buffer) {
let binary = "";
const bytes = new Uint8Array(buffer);
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
return btoa(binary);
}
const aiButtons = document.querySelectorAll(".user_aiBtn");
aiButtons.forEach(button => {
button.addEventListener("click", async (event) => {
const strKey = qbpms.form.get("q_openAiApiKey");
const strInstruction = qbpms.form.get("q_instruction") || "";
const strInput = qbpms.form.get("q_input") || "";
const resultEl = document.getElementById("user_result");
const statusEl = document.getElementById("user_status");
const modelName = event.currentTarget.innerText.trim();
if (!strKey || !strInput) {
statusEl.innerText = "エラー: APIキーと入力内容は必須です。";
resultEl.innerText = "";
return;
}
aiButtons.forEach(btn => btn.disabled = true);
statusEl.innerText = "準備中...";
resultEl.innerHTML = '<span class="user_cursor"></span>';
const cursor = resultEl.querySelector(".user_cursor");
let streamDone = false;
try {
// --- 添付ファイルの準備 ---
const selectedCheckboxes = Array.from(document.querySelectorAll('input[name="user_fileSelector"]:checked'));
let contentParts = [{ type: "input_text", text: String(strInput) }];
for (const cb of selectedCheckboxes) {
const qctx = user_getQcontextPath();
if (!qctx) throw new Error("Questetraのコンテキストパスが取得できません。");
const qfileId = cb.dataset.qfileId;
const pdiId = cb.dataset.pdiId;
const contentType = cb.dataset.contentType || 'application/octet-stream';
const filename = cb.dataset.filename || 'input.bin';
const fetchUrl = `${qctx}/API/OR/ProcessInstance/File/download?id=${encodeURIComponent(qfileId)}&processDataInstanceId=${encodeURIComponent(pdiId)}`;
statusEl.innerText = `ファイル「${filename}」を取得中...`;
const dlRes = await fetch(fetchUrl, { credentials: 'same-origin' });
if (!dlRes.ok) throw new Error(`ファイルのダウンロード失敗: ${dlRes.status}`);
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 });
} else {
statusEl.innerText = `ファイル「${filename}」をアップロード中...`;
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();
contentParts.push({ type: 'input_file', file_id: upJson.id });
}
}
statusEl.innerText = "AIに接続中...";
const res = 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 (!res.ok) {
let msg = `API Error: ${res.status}`;
try { msg += `\n${JSON.stringify(await res.json(), null, 2)}`; } catch (_) {}
throw new Error(msg);
}
statusEl.innerText = "AIが文章を生成中...";
const reader = res.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]") { streamDone = true; 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") { streamDone = true; 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;
}
statusEl.innerText = "生成が完了しました。";
} catch (error) {
console.error("Request failed:", error);
statusEl.innerText = "エラーが発生しました。";
resultEl.innerText = String(error.message || error);
} finally {
const c = document.querySelector("#user_result .user_cursor");
if (c) c.remove();
aiButtons.forEach(btn => btn.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





