/** * Inference Comparator - カスタムJavaScript * モーダル表示とズーム機能を提供 */ /** * ズームボタンを作成するヘルパー関数 * UI: 「−」「+」「100%表示」ボタン * 初期を100%として、+/-で10%刻みで拡大縮小(70%〜170%) */ function createZoomControls(targetElement) { var baseSize = 14; // 初期フォントサイズ(px)= 100% var zoomPercent = 100; // 現在のズーム率(%) var minPercent = 70; var maxPercent = 170; var step = 10; var zoomArea = document.createElement('div'); zoomArea.style.cssText = ` display: flex; gap: 8px; align-items: center; margin-right: auto; `; // −ボタン(縮小) var zoomOutBtn = document.createElement('button'); zoomOutBtn.style.cssText = ` background: #fff; color: #374151; border: 1px solid #d1d5db; border-radius: 8px; padding: 8px 16px; font-size: 1rem; font-weight: 600; cursor: pointer; min-width: 44px; `; zoomOutBtn.textContent = '−'; zoomOutBtn.onclick = function() { if (zoomPercent > minPercent) { zoomPercent -= step; var newSize = baseSize * zoomPercent / 100; targetElement.style.fontSize = newSize + 'px'; } }; // +ボタン(拡大) var zoomInBtn = document.createElement('button'); zoomInBtn.style.cssText = ` background: #fff; color: #374151; border: 1px solid #d1d5db; border-radius: 8px; padding: 8px 16px; font-size: 1rem; font-weight: 600; cursor: pointer; min-width: 44px; `; zoomInBtn.textContent = '+'; zoomInBtn.onclick = function() { if (zoomPercent < maxPercent) { zoomPercent += step; var newSize = baseSize * zoomPercent / 100; targetElement.style.fontSize = newSize + 'px'; } }; // 100%表示ボタン(リセット) var resetBtn = document.createElement('button'); resetBtn.style.cssText = ` background: #fff; color: #374151; border: 1px solid #d1d5db; border-radius: 8px; padding: 8px 16px; font-size: 0.9rem; font-weight: 500; cursor: pointer; `; resetBtn.textContent = '100%表示'; resetBtn.onclick = function() { zoomPercent = 100; targetElement.style.fontSize = baseSize + 'px'; }; zoomArea.appendChild(zoomOutBtn); zoomArea.appendChild(zoomInBtn); zoomArea.appendChild(resetBtn); return zoomArea; } /** * クエリモーダルを表示する * @param {string} tid_b64 - Base64エンコードされたtask_id * @param {string} query_b64 - Base64エンコードされたクエリ */ function showQueryModal(tid_b64, query_b64) { // Base64デコード(UTF-8対応) function b64decode(str) { return decodeURIComponent(escape(atob(str))); } var tid = b64decode(tid_b64); var query = b64decode(query_b64); // 既存のモーダルを削除 var existing = document.getElementById('query-modal-overlay'); if (existing) existing.remove(); // オーバーレイ作成 var overlay = document.createElement('div'); overlay.id = 'query-modal-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 10000; `; overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); }; // モーダルコンテンツ var modal = document.createElement('div'); modal.style.cssText = ` background: #fff; border-radius: 16px; padding: 32px; max-width: 80vw; width: 900px; max-height: 85vh; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); display: flex; flex-direction: column; font-family: Inter, Noto Sans JP, system-ui, sans-serif; `; // タイトル var title = document.createElement('div'); title.style.cssText = ` font-size: 1.25rem; font-weight: 700; color: #1f2937; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 2px solid #e5e7eb; `; title.textContent = 'task_id: ' + tid + ' のクエリ'; // クエリ表示エリア var queryPre = document.createElement('pre'); queryPre.style.cssText = ` background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; padding: 20px; margin: 0; font-family: JetBrains Mono, Fira Code, monospace; font-size: 14px; line-height: 1.7; color: #0c4a6e; white-space: pre-wrap; word-break: break-all; overflow-y: auto; flex: 1; min-height: 200px; user-select: text; cursor: text; `; queryPre.textContent = query; // ボタンエリア var btnArea = document.createElement('div'); btnArea.style.cssText = ` margin-top: 20px; display: flex; gap: 12px; align-items: center; `; // ズームコントロールを追加 var zoomControls = createZoomControls(queryPre); btnArea.appendChild(zoomControls); // コピーボタン var copyBtn = document.createElement('button'); copyBtn.style.cssText = ` background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; border-radius: 8px; padding: 12px 24px; font-size: 0.95rem; font-weight: 600; cursor: pointer; transition: all 0.2s; `; copyBtn.textContent = '📋 コピー'; copyBtn.onmouseover = function() { this.style.background = '#e5e7eb'; }; copyBtn.onmouseout = function() { this.style.background = '#f3f4f6'; }; copyBtn.onclick = function() { navigator.clipboard.writeText(query).then(function() { copyBtn.textContent = '✓ コピーしました'; setTimeout(function() { copyBtn.textContent = '📋 コピー'; }, 2000); }); }; // 閉じるボタン var closeBtn = document.createElement('button'); closeBtn.style.cssText = ` background: #3b82f6; color: #fff; border: none; border-radius: 8px; padding: 12px 32px; font-size: 0.95rem; font-weight: 600; cursor: pointer; transition: background 0.2s; `; closeBtn.textContent = '閉じる'; closeBtn.onmouseover = function() { this.style.background = '#2563eb'; }; closeBtn.onmouseout = function() { this.style.background = '#3b82f6'; }; closeBtn.onclick = function() { overlay.remove(); }; btnArea.appendChild(copyBtn); btnArea.appendChild(closeBtn); modal.appendChild(title); modal.appendChild(queryPre); modal.appendChild(btnArea); overlay.appendChild(modal); document.body.appendChild(overlay); } /** * エラーモーダルを表示する * @param {string} tid_b64 - Base64エンコードされたtask_id * @param {string} err_b64 - Base64エンコードされたエラーメッセージ * @param {string} gen_b64 - Base64エンコードされた推論結果 */ function showErrorModal(tid_b64, err_b64, gen_b64) { // Base64デコード(UTF-8対応) function b64decode(str) { return decodeURIComponent(escape(atob(str))); } var tid = b64decode(tid_b64); var err = b64decode(err_b64); var gen = b64decode(gen_b64); // 既存のモーダルを削除 var existing = document.getElementById('error-modal-overlay'); if (existing) existing.remove(); // オーバーレイ作成 var overlay = document.createElement('div'); overlay.id = 'error-modal-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 10000; `; overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); }; // モーダルコンテンツ var modal = document.createElement('div'); modal.style.cssText = ` background: #fff; border-radius: 16px; padding: 32px; max-width: 80vw; width: 900px; max-height: 85vh; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); display: flex; flex-direction: column; font-family: 'Inter', 'Noto Sans JP', system-ui, sans-serif; `; // タイトル var title = document.createElement('div'); title.style.cssText = ` font-size: 1.25rem; font-weight: 700; color: #1f2937; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 2px solid #e5e7eb; `; title.textContent = 'task_id: ' + tid + ' のエラー内容'; // スクロールエリア var scrollArea = document.createElement('div'); scrollArea.style.cssText = ` flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; `; // エラーセクション var errSection = document.createElement('div'); var errLabel = document.createElement('div'); errLabel.style.cssText = ` font-size: 0.875rem; font-weight: 600; color: #6b7280; margin-bottom: 8px; `; errLabel.textContent = 'エラー'; var errPre = document.createElement('pre'); errPre.style.cssText = ` background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 16px; margin: 0; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 0.875rem; line-height: 1.6; color: #991b1b; white-space: pre-wrap; word-break: break-all; overflow-y: auto; max-height: 150px; user-select: text; cursor: text; `; errPre.textContent = err; errSection.appendChild(errLabel); errSection.appendChild(errPre); // 推論結果セクション var genSection = document.createElement('div'); genSection.style.cssText = 'flex: 1; display: flex; flex-direction: column;'; var genLabel = document.createElement('div'); genLabel.style.cssText = ` font-size: 0.875rem; font-weight: 600; color: #6b7280; margin-bottom: 8px; `; genLabel.textContent = '推論結果'; var genPre = document.createElement('pre'); genPre.style.cssText = ` background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin: 0; font-family: JetBrains Mono, Fira Code, monospace; font-size: 14px; line-height: 1.5; color: #374151; white-space: pre-wrap; word-break: break-all; overflow-y: auto; flex: 1; min-height: 200px; user-select: text; cursor: text; `; genPre.textContent = gen; genSection.appendChild(genLabel); genSection.appendChild(genPre); scrollArea.appendChild(errSection); scrollArea.appendChild(genSection); // ボタンエリア var btnArea = document.createElement('div'); btnArea.style.cssText = ` margin-top: 20px; display: flex; gap: 12px; align-items: center; `; // ズームコントロールを追加(推論結果用) var zoomControls = createZoomControls(genPre); btnArea.appendChild(zoomControls); // コピーボタン var copyBtn = document.createElement('button'); copyBtn.style.cssText = ` background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; border-radius: 8px; padding: 12px 24px; font-size: 0.95rem; font-weight: 600; cursor: pointer; transition: all 0.2s; `; copyBtn.textContent = '📋 全てコピー'; copyBtn.onmouseover = function() { this.style.background = '#e5e7eb'; }; copyBtn.onmouseout = function() { this.style.background = '#f3f4f6'; }; copyBtn.onclick = function() { var text = 'task_id: ' + tid + ' のエラー内容\n\n'; text += '【エラー】\n' + err + '\n\n'; text += '【推論結果】\n' + gen; navigator.clipboard.writeText(text).then(function() { copyBtn.textContent = '✓ コピーしました'; setTimeout(function() { copyBtn.textContent = '📋 全てコピー'; }, 2000); }); }; // 閉じるボタン var closeBtn = document.createElement('button'); closeBtn.style.cssText = ` background: #3b82f6; color: #fff; border: none; border-radius: 8px; padding: 12px 32px; font-size: 0.95rem; font-weight: 600; cursor: pointer; transition: background 0.2s; `; closeBtn.textContent = '閉じる'; closeBtn.onmouseover = function() { this.style.background = '#2563eb'; }; closeBtn.onmouseout = function() { this.style.background = '#3b82f6'; }; closeBtn.onclick = function() { overlay.remove(); }; btnArea.appendChild(copyBtn); btnArea.appendChild(closeBtn); modal.appendChild(title); modal.appendChild(scrollArea); modal.appendChild(btnArea); overlay.appendChild(modal); document.body.appendChild(overlay); }