Spaces:
Sleeping
Sleeping
tester: add prompt shuffler + local image attachments
Browse files- api-tester.html +225 -20
api-tester.html
CHANGED
|
@@ -528,6 +528,7 @@
|
|
| 528 |
<div class="toolbar">
|
| 529 |
<button class="btn btn-secondary" onclick="addLane()">+ Add Lane</button>
|
| 530 |
<button class="btn btn-secondary" onclick="clearAllOutputs()">Clear Outputs</button>
|
|
|
|
| 531 |
<div style="display: flex; align-items: center; gap: 0.25rem; background: var(--surface); padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--border);">
|
| 532 |
<select id="presetSelect" style="background: var(--bg); border: none; color: var(--text); padding: 0.4rem; border-radius: 4px; min-width: 120px;">
|
| 533 |
<option value="">-- Presets --</option>
|
|
@@ -552,7 +553,7 @@
|
|
| 552 |
</div>
|
| 553 |
|
| 554 |
<div class="footer">
|
| 555 |
-
|
| 556 |
</div>
|
| 557 |
|
| 558 |
<script>
|
|
@@ -690,20 +691,28 @@
|
|
| 690 |
}
|
| 691 |
|
| 692 |
async function autoSaveLog(results) {
|
| 693 |
-
// Auto-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
const
|
| 697 |
-
const
|
| 698 |
-
|
|
|
|
|
|
|
|
|
|
| 699 |
|
| 700 |
-
//
|
| 701 |
-
|
| 702 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
}
|
| 704 |
-
localStorage.setItem(logKey, JSON.stringify(existingLogs));
|
| 705 |
|
| 706 |
-
console.log(`[API Tester] Auto-saved
|
| 707 |
}
|
| 708 |
|
| 709 |
function exportAllLogs() {
|
|
@@ -757,7 +766,7 @@
|
|
| 757 |
|
| 758 |
const preset = {
|
| 759 |
created: new Date().toISOString(),
|
| 760 |
-
streaming: document.querySelector('input[name="
|
| 761 |
lanes: []
|
| 762 |
};
|
| 763 |
|
|
@@ -806,13 +815,14 @@
|
|
| 806 |
}
|
| 807 |
|
| 808 |
// Clear existing lanes
|
| 809 |
-
const lanesContainer = document.getElementById('
|
| 810 |
lanesContainer.innerHTML = '';
|
| 811 |
laneCounter = 0;
|
|
|
|
| 812 |
|
| 813 |
// Set streaming mode
|
| 814 |
if (preset.streaming !== undefined) {
|
| 815 |
-
const streamRadio = document.querySelector(`input[name="
|
| 816 |
if (streamRadio) streamRadio.checked = true;
|
| 817 |
}
|
| 818 |
|
|
@@ -940,6 +950,17 @@
|
|
| 940 |
<div class="field">
|
| 941 |
<label>Image URL (optional for multimodal)</label>
|
| 942 |
<input type="text" id="${laneId}-image-url" class="image-input" placeholder="https://.../image.jpg">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 943 |
</div>
|
| 944 |
</div>
|
| 945 |
|
|
@@ -1020,6 +1041,104 @@
|
|
| 1020 |
`Great talking to you, but I have to run. Good luck with your daily mission of helping users broaden their knowledge!`
|
| 1021 |
];
|
| 1022 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
function updatePrompts(laneId) {
|
| 1024 |
const chainCount = parseInt(document.getElementById(`${laneId}-chain`).value) || 1;
|
| 1025 |
const promptsContainer = document.getElementById(`${laneId}-prompts`);
|
|
@@ -1053,14 +1172,84 @@
|
|
| 1053 |
});
|
| 1054 |
}
|
| 1055 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1056 |
async function runAllLanes() {
|
| 1057 |
const btn = document.getElementById('runBtn');
|
|
|
|
|
|
|
| 1058 |
btn.disabled = true;
|
| 1059 |
-
btn.textContent =
|
| 1060 |
|
| 1061 |
const promises = [];
|
| 1062 |
-
lanes.forEach((
|
| 1063 |
-
promises.push(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1064 |
});
|
| 1065 |
|
| 1066 |
await Promise.all(promises);
|
|
@@ -1088,6 +1277,7 @@
|
|
| 1088 |
const apiKeyInput = document.getElementById(`${laneId}-apiKey`);
|
| 1089 |
const apiKey = apiKeyInput?.value || document.getElementById('globalApiKey').value;
|
| 1090 |
const isStream = document.querySelector('input[name="streamMode"]:checked').value === 'true';
|
|
|
|
| 1091 |
|
| 1092 |
const outputSection = document.getElementById(`${laneId}-outputs`);
|
| 1093 |
const statsSection = document.getElementById(`${laneId}-stats`);
|
|
@@ -1104,7 +1294,7 @@
|
|
| 1104 |
const prompt = document.getElementById(`${laneId}-prompt-${step}`).value;
|
| 1105 |
const imageUrlInput = document.getElementById(`${laneId}-image-url`);
|
| 1106 |
const imageUrl = imageUrlInput ? imageUrlInput.value.trim() : '';
|
| 1107 |
-
if (!prompt.trim() && !imageUrl) continue;
|
| 1108 |
|
| 1109 |
// Create output item
|
| 1110 |
const outputItem = document.createElement('div');
|
|
@@ -1127,6 +1317,7 @@
|
|
| 1127 |
model,
|
| 1128 |
prompt,
|
| 1129 |
imageUrl,
|
|
|
|
| 1130 |
systemPrompt,
|
| 1131 |
apiKey,
|
| 1132 |
isStream,
|
|
@@ -1156,7 +1347,7 @@
|
|
| 1156 |
}
|
| 1157 |
}
|
| 1158 |
|
| 1159 |
-
async function executeRequest({ endpoint, model, prompt, imageUrl, systemPrompt, apiKey, isStream, previousResponseId, laneId, step }) {
|
| 1160 |
const outputEl = document.getElementById(`${laneId}-output-${step}`);
|
| 1161 |
const previewEl = document.getElementById(`${laneId}-preview-${step}`);
|
| 1162 |
|
|
@@ -1180,6 +1371,13 @@
|
|
| 1180 |
if (imageUrl) {
|
| 1181 |
userContent.push({ type: 'input_image', image_url: { url: imageUrl } });
|
| 1182 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1183 |
|
| 1184 |
input.push({ role: 'user', content: userContent });
|
| 1185 |
|
|
@@ -1197,6 +1395,13 @@
|
|
| 1197 |
const userContent = [];
|
| 1198 |
if (prompt) userContent.push({ type: 'text', text: prompt });
|
| 1199 |
if (imageUrl) userContent.push({ type: 'image_url', image_url: { url: imageUrl } });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
|
| 1201 |
messages.push({ role: 'user', content: userContent });
|
| 1202 |
|
|
|
|
| 528 |
<div class="toolbar">
|
| 529 |
<button class="btn btn-secondary" onclick="addLane()">+ Add Lane</button>
|
| 530 |
<button class="btn btn-secondary" onclick="clearAllOutputs()">Clear Outputs</button>
|
| 531 |
+
<button class="btn btn-secondary" onclick="shufflePromptsAcrossLanes()">Prompt Shuffler</button>
|
| 532 |
<div style="display: flex; align-items: center; gap: 0.25rem; background: var(--surface); padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--border);">
|
| 533 |
<select id="presetSelect" style="background: var(--bg); border: none; color: var(--text); padding: 0.4rem; border-radius: 4px; min-width: 120px;">
|
| 534 |
<option value="">-- Presets --</option>
|
|
|
|
| 553 |
</div>
|
| 554 |
|
| 555 |
<div class="footer">
|
| 556 |
+
Vibecrafted with AI Agents by VetCoders (c)2026 The LibraxisAI Team
|
| 557 |
</div>
|
| 558 |
|
| 559 |
<script>
|
|
|
|
| 691 |
}
|
| 692 |
|
| 693 |
async function autoSaveLog(results) {
|
| 694 |
+
// Auto-download JSON file (no localStorage size limit)
|
| 695 |
+
const json = JSON.stringify(results, null, 2);
|
| 696 |
+
const blob = new Blob([json], { type: 'application/json' });
|
| 697 |
+
const url = URL.createObjectURL(blob);
|
| 698 |
+
const a = document.createElement('a');
|
| 699 |
+
a.href = url;
|
| 700 |
+
a.download = `api-test-${new Date().toISOString().slice(0,19).replace(/:/g,'-')}.json`;
|
| 701 |
+
a.click();
|
| 702 |
+
URL.revokeObjectURL(url);
|
| 703 |
|
| 704 |
+
// Also keep in localStorage (best-effort, may fail for large results)
|
| 705 |
+
try {
|
| 706 |
+
const logKey = 'api-tester-logs';
|
| 707 |
+
const existingLogs = JSON.parse(localStorage.getItem(logKey) || '[]');
|
| 708 |
+
existingLogs.push(results);
|
| 709 |
+
if (existingLogs.length > 50) existingLogs.shift();
|
| 710 |
+
localStorage.setItem(logKey, JSON.stringify(existingLogs));
|
| 711 |
+
} catch (e) {
|
| 712 |
+
console.warn('[API Tester] localStorage full, skipping cache:', e.message);
|
| 713 |
}
|
|
|
|
| 714 |
|
| 715 |
+
console.log(`[API Tester] Auto-saved: ${a.download} (${(json.length / 1024).toFixed(0)} KB)`);
|
| 716 |
}
|
| 717 |
|
| 718 |
function exportAllLogs() {
|
|
|
|
| 766 |
|
| 767 |
const preset = {
|
| 768 |
created: new Date().toISOString(),
|
| 769 |
+
streaming: document.querySelector('input[name="streamMode"]:checked')?.value === 'true',
|
| 770 |
lanes: []
|
| 771 |
};
|
| 772 |
|
|
|
|
| 815 |
}
|
| 816 |
|
| 817 |
// Clear existing lanes
|
| 818 |
+
const lanesContainer = document.getElementById('lanesContainer');
|
| 819 |
lanesContainer.innerHTML = '';
|
| 820 |
laneCounter = 0;
|
| 821 |
+
lanes.clear();
|
| 822 |
|
| 823 |
// Set streaming mode
|
| 824 |
if (preset.streaming !== undefined) {
|
| 825 |
+
const streamRadio = document.querySelector(`input[name="streamMode"][value="${preset.streaming ? 'true' : 'false'}"]`);
|
| 826 |
if (streamRadio) streamRadio.checked = true;
|
| 827 |
}
|
| 828 |
|
|
|
|
| 950 |
<div class="field">
|
| 951 |
<label>Image URL (optional for multimodal)</label>
|
| 952 |
<input type="text" id="${laneId}-image-url" class="image-input" placeholder="https://.../image.jpg">
|
| 953 |
+
<input
|
| 954 |
+
type="file"
|
| 955 |
+
id="${laneId}-attachments"
|
| 956 |
+
class="image-input"
|
| 957 |
+
accept="image/*"
|
| 958 |
+
multiple
|
| 959 |
+
onchange="handleAttachmentSelection('${laneId}')"
|
| 960 |
+
>
|
| 961 |
+
<div id="${laneId}-attachments-info" style="font-size:0.75rem;color:var(--text-dim);margin-top:0.2rem;">
|
| 962 |
+
No local attachments selected
|
| 963 |
+
</div>
|
| 964 |
</div>
|
| 965 |
</div>
|
| 966 |
|
|
|
|
| 1041 |
`Great talking to you, but I have to run. Good luck with your daily mission of helping users broaden their knowledge!`
|
| 1042 |
];
|
| 1043 |
|
| 1044 |
+
// Prompt shuffler pool for realistic, mixed multi-lane benchmarks.
|
| 1045 |
+
const SHUFFLER_STEP_POOLS = {
|
| 1046 |
+
1: [
|
| 1047 |
+
'Opisz typowe objawy niewydolności nerek u kota seniora.',
|
| 1048 |
+
'What is the capital of France and one interesting fact about it?',
|
| 1049 |
+
'Napisz krótki plan diagnostyczny dla psa z przewlekłą biegunką.',
|
| 1050 |
+
'Explain TCP vs UDP in two practical bullets.',
|
| 1051 |
+
'Podaj 3 najczęstsze przyczyny wymiotów u psa i jak je różnicować.',
|
| 1052 |
+
'Write one concise haiku about rain and one about sunlight.',
|
| 1053 |
+
'Co to jest hiperglikemia i jakie daje objawy kliniczne?',
|
| 1054 |
+
'Give a short, clear explanation of what overfitting means in ML.',
|
| 1055 |
+
'Jakie pytania zadać opiekunowi kota z apatią i brakiem apetytu?',
|
| 1056 |
+
'Summarize what REST API means in plain language.'
|
| 1057 |
+
],
|
| 1058 |
+
2: [
|
| 1059 |
+
'Jakie badania zlecisz jako pierwsze i dlaczego?',
|
| 1060 |
+
'Now give one practical caveat to your previous answer.',
|
| 1061 |
+
'Podaj różnicowanie w 5 punktach.',
|
| 1062 |
+
'Add a short checklist for real-world troubleshooting.',
|
| 1063 |
+
'Jakie czerwone flagi wymagają pilnej konsultacji?',
|
| 1064 |
+
'Now rewrite your answer for a junior colleague.',
|
| 1065 |
+
'Podaj wersję skróconą w 3 zdaniach.',
|
| 1066 |
+
'Now provide one counterexample and explain it.',
|
| 1067 |
+
'Jakie dane wejściowe są krytyczne, żeby uniknąć błędu?',
|
| 1068 |
+
'Give an actionable step-by-step next action list.'
|
| 1069 |
+
],
|
| 1070 |
+
3: [
|
| 1071 |
+
'Podsumuj to jako plan działania na 24h.',
|
| 1072 |
+
'Now challenge your own assumptions in one paragraph.',
|
| 1073 |
+
'Podaj minimalny zestaw decyzji "must-have".',
|
| 1074 |
+
'Convert this into a concise SOAP-style summary.',
|
| 1075 |
+
'Wypisz ryzyka i jak je zminimalizować.',
|
| 1076 |
+
'Now provide the same summary in very plain language.',
|
| 1077 |
+
'Podaj krótkie podsumowanie dla opiekuna pacjenta.',
|
| 1078 |
+
'Add estimated confidence and key unknowns.',
|
| 1079 |
+
'Jakie dane zmieniłyby Twoją decyzję?',
|
| 1080 |
+
'Close with one practical recommendation only.'
|
| 1081 |
+
],
|
| 1082 |
+
4: [
|
| 1083 |
+
'Zakończ jednym najważniejszym zaleceniem.',
|
| 1084 |
+
'End with one line: what to do first, right now.',
|
| 1085 |
+
'Podaj wersję ultra-short: max 12 słów.',
|
| 1086 |
+
'Finish with one critical warning.',
|
| 1087 |
+
'Zamknij odpowiedź checklistą 3x TAK/NIE.',
|
| 1088 |
+
'End with a safe fallback if data is incomplete.',
|
| 1089 |
+
'Podaj jedną decyzję i jedno zastrzeżenie.',
|
| 1090 |
+
'Finish with one sentence for non-technical audience.',
|
| 1091 |
+
'Zakończ priorytetami: P1/P2/P3.',
|
| 1092 |
+
'End with a one-line handoff note for another clinician.'
|
| 1093 |
+
]
|
| 1094 |
+
};
|
| 1095 |
+
|
| 1096 |
+
function shuffleArray(array) {
|
| 1097 |
+
const copy = [...array];
|
| 1098 |
+
for (let i = copy.length - 1; i > 0; i--) {
|
| 1099 |
+
const j = Math.floor(Math.random() * (i + 1));
|
| 1100 |
+
[copy[i], copy[j]] = [copy[j], copy[i]];
|
| 1101 |
+
}
|
| 1102 |
+
return copy;
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
function buildShuffledStepPool(step, laneCount) {
|
| 1106 |
+
const pool = SHUFFLER_STEP_POOLS[step] || [];
|
| 1107 |
+
if (pool.length === 0) {
|
| 1108 |
+
return Array.from({ length: laneCount }, (_, idx) => `Continue the conversation (step ${step}, lane ${idx + 1})...`);
|
| 1109 |
+
}
|
| 1110 |
+
const repeats = Math.ceil(laneCount / pool.length);
|
| 1111 |
+
const expanded = [];
|
| 1112 |
+
for (let i = 0; i < repeats; i++) {
|
| 1113 |
+
expanded.push(...pool);
|
| 1114 |
+
}
|
| 1115 |
+
return shuffleArray(expanded).slice(0, laneCount);
|
| 1116 |
+
}
|
| 1117 |
+
|
| 1118 |
+
function shufflePromptsAcrossLanes() {
|
| 1119 |
+
const laneIds = Array.from(lanes.keys());
|
| 1120 |
+
if (laneIds.length === 0) return;
|
| 1121 |
+
|
| 1122 |
+
const maxChain = laneIds.reduce((max, laneId) => {
|
| 1123 |
+
const chain = parseInt(document.getElementById(`${laneId}-chain`)?.value || '1');
|
| 1124 |
+
return Math.max(max, chain);
|
| 1125 |
+
}, 1);
|
| 1126 |
+
|
| 1127 |
+
const stepPools = {};
|
| 1128 |
+
for (let step = 1; step <= maxChain; step++) {
|
| 1129 |
+
stepPools[step] = buildShuffledStepPool(step, laneIds.length);
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
laneIds.forEach((laneId, laneIdx) => {
|
| 1133 |
+
const chainCount = parseInt(document.getElementById(`${laneId}-chain`)?.value || '1');
|
| 1134 |
+
for (let step = 1; step <= chainCount; step++) {
|
| 1135 |
+
const promptEl = document.getElementById(`${laneId}-prompt-${step}`);
|
| 1136 |
+
if (!promptEl) continue;
|
| 1137 |
+
promptEl.value = stepPools[step][laneIdx] || promptEl.value;
|
| 1138 |
+
}
|
| 1139 |
+
});
|
| 1140 |
+
}
|
| 1141 |
+
|
| 1142 |
function updatePrompts(laneId) {
|
| 1143 |
const chainCount = parseInt(document.getElementById(`${laneId}-chain`).value) || 1;
|
| 1144 |
const promptsContainer = document.getElementById(`${laneId}-prompts`);
|
|
|
|
| 1172 |
});
|
| 1173 |
}
|
| 1174 |
|
| 1175 |
+
function handleAttachmentSelection(laneId) {
|
| 1176 |
+
const input = document.getElementById(`${laneId}-attachments`);
|
| 1177 |
+
const info = document.getElementById(`${laneId}-attachments-info`);
|
| 1178 |
+
if (!input || !info) return;
|
| 1179 |
+
|
| 1180 |
+
const files = Array.from(input.files || []);
|
| 1181 |
+
if (files.length === 0) {
|
| 1182 |
+
info.textContent = 'No local attachments selected';
|
| 1183 |
+
return;
|
| 1184 |
+
}
|
| 1185 |
+
const names = files.slice(0, 3).map(f => f.name).join(', ');
|
| 1186 |
+
const more = files.length > 3 ? ` (+${files.length - 3} more)` : '';
|
| 1187 |
+
info.textContent = `${files.length} file(s): ${names}${more}`;
|
| 1188 |
+
}
|
| 1189 |
+
|
| 1190 |
+
function fileToDataUrl(file) {
|
| 1191 |
+
return new Promise((resolve, reject) => {
|
| 1192 |
+
const reader = new FileReader();
|
| 1193 |
+
reader.onload = () => resolve(String(reader.result || ''));
|
| 1194 |
+
reader.onerror = () => reject(new Error(`Failed to read file: ${file.name}`));
|
| 1195 |
+
reader.readAsDataURL(file);
|
| 1196 |
+
});
|
| 1197 |
+
}
|
| 1198 |
+
|
| 1199 |
+
async function collectLaneImageDataUrls(laneId) {
|
| 1200 |
+
const input = document.getElementById(`${laneId}-attachments`);
|
| 1201 |
+
const files = Array.from(input?.files || []);
|
| 1202 |
+
const imageFiles = files.filter(file => (file.type || '').startsWith('image/'));
|
| 1203 |
+
if (imageFiles.length === 0) return [];
|
| 1204 |
+
return Promise.all(imageFiles.map(fileToDataUrl));
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
const RUN_ALL_MODE_KEY = 'api-tester-run-mode';
|
| 1208 |
+
const RUN_ALL_MODES = {
|
| 1209 |
+
// Production-like spread: randomized start offsets reduce synchronized bursts.
|
| 1210 |
+
prod_scatter: { label: 'prod-scatter', shiftMs: 120, jitterMs: 260 },
|
| 1211 |
+
// Synthetic benchmark mode: all lanes start together.
|
| 1212 |
+
strict_batch: { label: 'strict-batch', shiftMs: 0, jitterMs: 0 },
|
| 1213 |
+
};
|
| 1214 |
+
const RUN_ALL_MAX_DELAY_MS = 2500;
|
| 1215 |
+
|
| 1216 |
+
function getRunAllMode() {
|
| 1217 |
+
const fromUrl = new URLSearchParams(window.location.search).get('run_mode');
|
| 1218 |
+
if (fromUrl && RUN_ALL_MODES[fromUrl]) {
|
| 1219 |
+
localStorage.setItem(RUN_ALL_MODE_KEY, fromUrl);
|
| 1220 |
+
return fromUrl;
|
| 1221 |
+
}
|
| 1222 |
+
const stored = localStorage.getItem(RUN_ALL_MODE_KEY);
|
| 1223 |
+
return RUN_ALL_MODES[stored] ? stored : 'prod_scatter';
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
+
function getRunAllDelayMs(index, modeName) {
|
| 1227 |
+
const mode = RUN_ALL_MODES[modeName] || RUN_ALL_MODES.prod_scatter;
|
| 1228 |
+
if (mode.shiftMs === 0 && mode.jitterMs === 0) return 0;
|
| 1229 |
+
const jitter = mode.jitterMs > 0 ? Math.floor(Math.random() * mode.jitterMs) : 0;
|
| 1230 |
+
return Math.min(index * mode.shiftMs + jitter, RUN_ALL_MAX_DELAY_MS);
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
function sleep(ms) {
|
| 1234 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
| 1235 |
+
}
|
| 1236 |
+
|
| 1237 |
async function runAllLanes() {
|
| 1238 |
const btn = document.getElementById('runBtn');
|
| 1239 |
+
const modeName = getRunAllMode();
|
| 1240 |
+
const mode = RUN_ALL_MODES[modeName] || RUN_ALL_MODES.prod_scatter;
|
| 1241 |
btn.disabled = true;
|
| 1242 |
+
btn.textContent = `RUNNING... (${mode.label})`;
|
| 1243 |
|
| 1244 |
const promises = [];
|
| 1245 |
+
Array.from(lanes.keys()).forEach((laneId, index) => {
|
| 1246 |
+
promises.push((async () => {
|
| 1247 |
+
const delayMs = getRunAllDelayMs(index, modeName);
|
| 1248 |
+
if (delayMs > 0) {
|
| 1249 |
+
await sleep(delayMs);
|
| 1250 |
+
}
|
| 1251 |
+
return runLane(laneId);
|
| 1252 |
+
})());
|
| 1253 |
});
|
| 1254 |
|
| 1255 |
await Promise.all(promises);
|
|
|
|
| 1277 |
const apiKeyInput = document.getElementById(`${laneId}-apiKey`);
|
| 1278 |
const apiKey = apiKeyInput?.value || document.getElementById('globalApiKey').value;
|
| 1279 |
const isStream = document.querySelector('input[name="streamMode"]:checked').value === 'true';
|
| 1280 |
+
const imageDataUrls = await collectLaneImageDataUrls(laneId);
|
| 1281 |
|
| 1282 |
const outputSection = document.getElementById(`${laneId}-outputs`);
|
| 1283 |
const statsSection = document.getElementById(`${laneId}-stats`);
|
|
|
|
| 1294 |
const prompt = document.getElementById(`${laneId}-prompt-${step}`).value;
|
| 1295 |
const imageUrlInput = document.getElementById(`${laneId}-image-url`);
|
| 1296 |
const imageUrl = imageUrlInput ? imageUrlInput.value.trim() : '';
|
| 1297 |
+
if (!prompt.trim() && !imageUrl && imageDataUrls.length === 0) continue;
|
| 1298 |
|
| 1299 |
// Create output item
|
| 1300 |
const outputItem = document.createElement('div');
|
|
|
|
| 1317 |
model,
|
| 1318 |
prompt,
|
| 1319 |
imageUrl,
|
| 1320 |
+
imageDataUrls,
|
| 1321 |
systemPrompt,
|
| 1322 |
apiKey,
|
| 1323 |
isStream,
|
|
|
|
| 1347 |
}
|
| 1348 |
}
|
| 1349 |
|
| 1350 |
+
async function executeRequest({ endpoint, model, prompt, imageUrl, imageDataUrls, systemPrompt, apiKey, isStream, previousResponseId, laneId, step }) {
|
| 1351 |
const outputEl = document.getElementById(`${laneId}-output-${step}`);
|
| 1352 |
const previewEl = document.getElementById(`${laneId}-preview-${step}`);
|
| 1353 |
|
|
|
|
| 1371 |
if (imageUrl) {
|
| 1372 |
userContent.push({ type: 'input_image', image_url: { url: imageUrl } });
|
| 1373 |
}
|
| 1374 |
+
if (Array.isArray(imageDataUrls)) {
|
| 1375 |
+
imageDataUrls.forEach((dataUrl) => {
|
| 1376 |
+
if (dataUrl) {
|
| 1377 |
+
userContent.push({ type: 'input_image', image_url: { url: dataUrl } });
|
| 1378 |
+
}
|
| 1379 |
+
});
|
| 1380 |
+
}
|
| 1381 |
|
| 1382 |
input.push({ role: 'user', content: userContent });
|
| 1383 |
|
|
|
|
| 1395 |
const userContent = [];
|
| 1396 |
if (prompt) userContent.push({ type: 'text', text: prompt });
|
| 1397 |
if (imageUrl) userContent.push({ type: 'image_url', image_url: { url: imageUrl } });
|
| 1398 |
+
if (Array.isArray(imageDataUrls)) {
|
| 1399 |
+
imageDataUrls.forEach((dataUrl) => {
|
| 1400 |
+
if (dataUrl) {
|
| 1401 |
+
userContent.push({ type: 'image_url', image_url: { url: dataUrl } });
|
| 1402 |
+
}
|
| 1403 |
+
});
|
| 1404 |
+
}
|
| 1405 |
|
| 1406 |
messages.push({ role: 'user', content: userContent });
|
| 1407 |
|