div0-space commited on
Commit
07df0cf
·
verified ·
1 Parent(s): dc6c8c4

tester: add prompt shuffler + local image attachments

Browse files
Files changed (1) hide show
  1. 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
- Created by M&K (c)2026 The LibraxisAI Team
556
  </div>
557
 
558
  <script>
@@ -690,20 +691,28 @@
690
  }
691
 
692
  async function autoSaveLog(results) {
693
- // Auto-save to tools/logs/ via local file download
694
- // Since we can't write directly to filesystem from browser,
695
- // we append to localStorage and provide batch export
696
- const logKey = 'api-tester-logs';
697
- const existingLogs = JSON.parse(localStorage.getItem(logKey) || '[]');
698
- existingLogs.push(results);
 
 
 
699
 
700
- // Keep last 100 tests
701
- if (existingLogs.length > 100) {
702
- existingLogs.shift();
 
 
 
 
 
 
703
  }
704
- localStorage.setItem(logKey, JSON.stringify(existingLogs));
705
 
706
- console.log(`[API Tester] Auto-saved test #${existingLogs.length} to localStorage`);
707
  }
708
 
709
  function exportAllLogs() {
@@ -757,7 +766,7 @@
757
 
758
  const preset = {
759
  created: new Date().toISOString(),
760
- streaming: document.querySelector('input[name="mode"]:checked')?.value === 'stream',
761
  lanes: []
762
  };
763
 
@@ -806,13 +815,14 @@
806
  }
807
 
808
  // Clear existing lanes
809
- const lanesContainer = document.getElementById('lanes');
810
  lanesContainer.innerHTML = '';
811
  laneCounter = 0;
 
812
 
813
  // Set streaming mode
814
  if (preset.streaming !== undefined) {
815
- const streamRadio = document.querySelector(`input[name="mode"][value="${preset.streaming ? 'stream' : 'non-stream'}"]`);
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 = 'RUNNING...';
1060
 
1061
  const promises = [];
1062
- lanes.forEach((_, laneId) => {
1063
- promises.push(runLane(laneId));
 
 
 
 
 
 
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