qGuide: OpenAI API に画像付でリクエスト
qGuide: Request to OpenAI API with Image
“投入ファイル” のファイル(Image/PDF)と “投入データ” の文字列と “APIキー” を Responses API に CORS 送信します。レスポンス(モデルが生成した文章)は、タスク処理画面上でストリーミング表示されます。プロンプト設定次第で、「古文書の解析」「故障箇所の発見」「個体数のカウント」「RGBカラーコードの抽出」など様々な支援機能(タスク処理者を支援する機能)を提供することが可能です。
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>
自由改変可能な HTML/JavaScript コードです (MIT License)。いかなる保証もありません。
(JavaScript を用いたデコレーションは Professional editionでのみ利用可能です: M213)
(JavaScript を用いたデコレーションは Professional editionでのみ利用可能です: M213)
Capture





