qGuide: OpenAI API に画像付でリクエスト

qGuide: OpenAI API に画像付でリクエスト

translate 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>
warning 自由改変可能な HTML/JavaScript コードです (MIT License)。いかなる保証もありません。
(JavaScript を用いたデコレーションProfessional editionでのみ利用可能です: M213)

Capture

See Also

上部へスクロール

Questetra Supportをもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む