Spaces:
Running
Running
Refine demo navigation
Browse files- static/script.js +15 -98
- static/style.css +63 -18
- templates/index.html +32 -23
- test_app.py +49 -0
static/script.js
CHANGED
|
@@ -14,15 +14,7 @@ import { initClearButton, updateClearButtonState } from './reset.js';
|
|
| 14 |
|
| 15 |
document.addEventListener('DOMContentLoaded', function () {
|
| 16 |
// === CONFIGURATION CONSTANTS ===
|
| 17 |
-
const
|
| 18 |
-
MOBILE_MIN_WIDTH: 120,
|
| 19 |
-
DESKTOP_MIN_WIDTH: 160,
|
| 20 |
-
MOBILE_BREAKPOINT: 768,
|
| 21 |
-
NARROW_BREAKPOINT: 360,
|
| 22 |
-
MAX_LABEL_LENGTH: 60,
|
| 23 |
-
BALANCE_DELAY: 100,
|
| 24 |
-
RESIZE_DEBOUNCE: 250,
|
| 25 |
-
};
|
| 26 |
|
| 27 |
const UI_CONFIG = {
|
| 28 |
SCROLL_SMOOTH_BEHAVIOR: 'smooth',
|
|
@@ -75,6 +67,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 75 |
const outputViewTabs = Array.from(
|
| 76 |
document.querySelectorAll('[data-output-view]'),
|
| 77 |
);
|
|
|
|
| 78 |
const instructionsEl = document.querySelector('.instructions');
|
| 79 |
const loadingOverlay = document.getElementById('loading-overlay');
|
| 80 |
let processingLoadingTimer = null;
|
|
@@ -322,10 +315,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 322 |
}
|
| 323 |
loadSampleReport(defaultSample, { scroll: false });
|
| 324 |
}
|
| 325 |
-
|
| 326 |
-
setTimeout(() => {
|
| 327 |
-
balanceByColumnCount();
|
| 328 |
-
}, GRID_CONFIG.BALANCE_DELAY);
|
| 329 |
}
|
| 330 |
|
| 331 |
/**
|
|
@@ -353,73 +342,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 353 |
buttons[nextIndex].scrollIntoView({ block: 'nearest', inline: 'center' });
|
| 354 |
}
|
| 355 |
|
| 356 |
-
/**
|
| 357 |
-
* Balances sample button rows by calculating optimal column count for even distribution.
|
| 358 |
-
* Keeps row-wise reading order while achieving visual balance (e.g., 5+5 instead of 6+4).
|
| 359 |
-
* Uses responsive sizing for better mobile experience.
|
| 360 |
-
*/
|
| 361 |
-
function balanceByColumnCount() {
|
| 362 |
-
const container = document.querySelector('.sample-buttons');
|
| 363 |
-
if (!container) {
|
| 364 |
-
console.warn('Sample buttons container not found');
|
| 365 |
-
return;
|
| 366 |
-
}
|
| 367 |
-
|
| 368 |
-
const cards = container.querySelectorAll('.sample-button').length;
|
| 369 |
-
const styles = getComputedStyle(container);
|
| 370 |
-
const gap = parseFloat(styles.columnGap) || 12;
|
| 371 |
-
|
| 372 |
-
const viewport = window.innerWidth;
|
| 373 |
-
const minWidth =
|
| 374 |
-
viewport <= GRID_CONFIG.MOBILE_BREAKPOINT
|
| 375 |
-
? GRID_CONFIG.MOBILE_MIN_WIDTH
|
| 376 |
-
: GRID_CONFIG.DESKTOP_MIN_WIDTH;
|
| 377 |
-
|
| 378 |
-
const containerWidth = container.clientWidth;
|
| 379 |
-
const columnsFit = Math.max(
|
| 380 |
-
1,
|
| 381 |
-
Math.floor((containerWidth + gap) / (minWidth + gap)),
|
| 382 |
-
);
|
| 383 |
-
|
| 384 |
-
if (viewport <= GRID_CONFIG.NARROW_BREAKPOINT) {
|
| 385 |
-
return;
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
// Find the column count that provides the most even distribution
|
| 389 |
-
let bestCols = columnsFit;
|
| 390 |
-
let bestRem = cards % columnsFit;
|
| 391 |
-
|
| 392 |
-
for (let cols = columnsFit - 1; cols >= 1; cols--) {
|
| 393 |
-
const rem = cards % cols;
|
| 394 |
-
if (rem === 0) {
|
| 395 |
-
bestCols = cols;
|
| 396 |
-
break; // Perfect distribution found
|
| 397 |
-
}
|
| 398 |
-
if (rem > bestRem) continue; // Worse distribution, skip
|
| 399 |
-
bestCols = cols;
|
| 400 |
-
bestRem = rem;
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
// Mobile-specific logic: prefer 2-3 columns for better touch targets
|
| 404 |
-
if (viewport <= GRID_CONFIG.MOBILE_BREAKPOINT) {
|
| 405 |
-
if (bestCols === 1 && columnsFit >= 2) {
|
| 406 |
-
bestCols = 2; // Force at least 2 columns on mobile
|
| 407 |
-
} else if (bestCols > 3 && cards >= 6) {
|
| 408 |
-
// If we have many columns, prefer 2-3 for mobile UX
|
| 409 |
-
const cols2Rem = cards % 2;
|
| 410 |
-
const cols3Rem = cards % 3;
|
| 411 |
-
if (cols2Rem <= cols3Rem) {
|
| 412 |
-
bestCols = 2;
|
| 413 |
-
} else {
|
| 414 |
-
bestCols = 3;
|
| 415 |
-
}
|
| 416 |
-
}
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
// Always apply the balanced column count for optimal visual distribution
|
| 420 |
-
container.style.gridTemplateColumns = `repeat(${bestCols}, minmax(${minWidth}px, 1fr))`;
|
| 421 |
-
}
|
| 422 |
-
|
| 423 |
/**
|
| 424 |
* Loads a sample report into the input area and automatically processes it.
|
| 425 |
* @param {Object} sample - The sample report data object
|
|
@@ -467,14 +389,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 467 |
|
| 468 |
loadSampleReports();
|
| 469 |
|
| 470 |
-
let resizeTimeout;
|
| 471 |
-
window.addEventListener('resize', () => {
|
| 472 |
-
clearTimeout(resizeTimeout);
|
| 473 |
-
resizeTimeout = setTimeout(() => {
|
| 474 |
-
balanceByColumnCount();
|
| 475 |
-
}, GRID_CONFIG.RESIZE_DEBOUNCE);
|
| 476 |
-
});
|
| 477 |
-
|
| 478 |
/**
|
| 479 |
* Updates the cache status display in the UI.
|
| 480 |
* @returns {Promise<void>}
|
|
@@ -563,8 +477,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 563 |
}
|
| 564 |
const mobilePromptToggle = document.getElementById('prompt-toggle-mobile');
|
| 565 |
if (mobilePromptToggle) mobilePromptToggle.checked = false;
|
| 566 |
-
setOutputViewsAvailable(false);
|
| 567 |
selectOutputView('cards');
|
|
|
|
| 568 |
updateLXToggleStates(); // Disable toggles when output is cleared
|
| 569 |
updateCopyButtonState();
|
| 570 |
|
|
@@ -729,7 +643,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 729 |
showPromptView(false);
|
| 730 |
}
|
| 731 |
|
| 732 |
-
selectOutputView(
|
| 733 |
|
| 734 |
const hasIntervals = data.segments.some(
|
| 735 |
(segment) => segment.intervals && segment.intervals.length > 0,
|
|
@@ -1171,8 +1085,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 1171 |
*/
|
| 1172 |
function extractLabelInfo(content) {
|
| 1173 |
const colonIndex = content.indexOf(':');
|
| 1174 |
-
const hasLabel =
|
| 1175 |
-
colonIndex > 0 && colonIndex < GRID_CONFIG.MAX_LABEL_LENGTH;
|
| 1176 |
|
| 1177 |
return {
|
| 1178 |
hasLabel,
|
|
@@ -1477,7 +1390,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 1477 |
* Selects one of the three output representations.
|
| 1478 |
* @param {'cards'|'report'|'raw'} view - View to make visible
|
| 1479 |
*/
|
| 1480 |
-
function selectOutputView(view) {
|
| 1481 |
const requestedTab = outputViewTabs.find(
|
| 1482 |
(tab) => tab.dataset.outputView === view,
|
| 1483 |
);
|
|
@@ -1508,6 +1421,8 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 1508 |
tab.setAttribute('aria-selected', String(selected));
|
| 1509 |
tab.tabIndex = selected ? 0 : -1;
|
| 1510 |
});
|
|
|
|
|
|
|
| 1511 |
}
|
| 1512 |
|
| 1513 |
/** Enables alternate representations only when structured output exists. */
|
|
@@ -1519,10 +1434,10 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 1519 |
});
|
| 1520 |
}
|
| 1521 |
|
| 1522 |
-
outputViewTabs.forEach((tab
|
| 1523 |
-
tab.tabIndex =
|
| 1524 |
tab.addEventListener('click', () =>
|
| 1525 |
-
selectOutputView(tab.dataset.outputView),
|
| 1526 |
);
|
| 1527 |
tab.addEventListener('keydown', (event) => {
|
| 1528 |
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
|
|
@@ -1543,7 +1458,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 1543 |
if (event.key === 'Home') nextIndex = 0;
|
| 1544 |
if (event.key === 'End') nextIndex = enabledTabs.length - 1;
|
| 1545 |
enabledTabs[nextIndex].focus();
|
| 1546 |
-
selectOutputView(enabledTabs[nextIndex].dataset.outputView
|
|
|
|
|
|
|
| 1547 |
});
|
| 1548 |
});
|
| 1549 |
|
|
@@ -1562,7 +1479,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
| 1562 |
if (rawToggle) {
|
| 1563 |
rawToggle.addEventListener('change', () => {
|
| 1564 |
const showRaw = rawToggle.checked;
|
| 1565 |
-
selectOutputView(showRaw ? 'raw' : 'cards');
|
| 1566 |
|
| 1567 |
const mobileRawToggle = document.getElementById('raw-toggle-mobile');
|
| 1568 |
if (mobileRawToggle) {
|
|
|
|
| 14 |
|
| 15 |
document.addEventListener('DOMContentLoaded', function () {
|
| 16 |
// === CONFIGURATION CONSTANTS ===
|
| 17 |
+
const MAX_LABEL_LENGTH = 60;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
const UI_CONFIG = {
|
| 20 |
SCROLL_SMOOTH_BEHAVIOR: 'smooth',
|
|
|
|
| 67 |
const outputViewTabs = Array.from(
|
| 68 |
document.querySelectorAll('[data-output-view]'),
|
| 69 |
);
|
| 70 |
+
let preferredStructuredView = 'report';
|
| 71 |
const instructionsEl = document.querySelector('.instructions');
|
| 72 |
const loadingOverlay = document.getElementById('loading-overlay');
|
| 73 |
let processingLoadingTimer = null;
|
|
|
|
| 315 |
}
|
| 316 |
loadSampleReport(defaultSample, { scroll: false });
|
| 317 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
}
|
| 319 |
|
| 320 |
/**
|
|
|
|
| 342 |
buttons[nextIndex].scrollIntoView({ block: 'nearest', inline: 'center' });
|
| 343 |
}
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
/**
|
| 346 |
* Loads a sample report into the input area and automatically processes it.
|
| 347 |
* @param {Object} sample - The sample report data object
|
|
|
|
| 389 |
|
| 390 |
loadSampleReports();
|
| 391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
/**
|
| 393 |
* Updates the cache status display in the UI.
|
| 394 |
* @returns {Promise<void>}
|
|
|
|
| 477 |
}
|
| 478 |
const mobilePromptToggle = document.getElementById('prompt-toggle-mobile');
|
| 479 |
if (mobilePromptToggle) mobilePromptToggle.checked = false;
|
|
|
|
| 480 |
selectOutputView('cards');
|
| 481 |
+
setOutputViewsAvailable(false);
|
| 482 |
updateLXToggleStates(); // Disable toggles when output is cleared
|
| 483 |
updateCopyButtonState();
|
| 484 |
|
|
|
|
| 643 |
showPromptView(false);
|
| 644 |
}
|
| 645 |
|
| 646 |
+
selectOutputView(preferredStructuredView);
|
| 647 |
|
| 648 |
const hasIntervals = data.segments.some(
|
| 649 |
(segment) => segment.intervals && segment.intervals.length > 0,
|
|
|
|
| 1085 |
*/
|
| 1086 |
function extractLabelInfo(content) {
|
| 1087 |
const colonIndex = content.indexOf(':');
|
| 1088 |
+
const hasLabel = colonIndex > 0 && colonIndex < MAX_LABEL_LENGTH;
|
|
|
|
| 1089 |
|
| 1090 |
return {
|
| 1091 |
hasLabel,
|
|
|
|
| 1390 |
* Selects one of the three output representations.
|
| 1391 |
* @param {'cards'|'report'|'raw'} view - View to make visible
|
| 1392 |
*/
|
| 1393 |
+
function selectOutputView(view, { remember = false } = {}) {
|
| 1394 |
const requestedTab = outputViewTabs.find(
|
| 1395 |
(tab) => tab.dataset.outputView === view,
|
| 1396 |
);
|
|
|
|
| 1421 |
tab.setAttribute('aria-selected', String(selected));
|
| 1422 |
tab.tabIndex = selected ? 0 : -1;
|
| 1423 |
});
|
| 1424 |
+
|
| 1425 |
+
if (remember) preferredStructuredView = view;
|
| 1426 |
}
|
| 1427 |
|
| 1428 |
/** Enables alternate representations only when structured output exists. */
|
|
|
|
| 1434 |
});
|
| 1435 |
}
|
| 1436 |
|
| 1437 |
+
outputViewTabs.forEach((tab) => {
|
| 1438 |
+
tab.tabIndex = tab.getAttribute('aria-selected') === 'true' ? 0 : -1;
|
| 1439 |
tab.addEventListener('click', () =>
|
| 1440 |
+
selectOutputView(tab.dataset.outputView, { remember: true }),
|
| 1441 |
);
|
| 1442 |
tab.addEventListener('keydown', (event) => {
|
| 1443 |
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
|
|
|
|
| 1458 |
if (event.key === 'Home') nextIndex = 0;
|
| 1459 |
if (event.key === 'End') nextIndex = enabledTabs.length - 1;
|
| 1460 |
enabledTabs[nextIndex].focus();
|
| 1461 |
+
selectOutputView(enabledTabs[nextIndex].dataset.outputView, {
|
| 1462 |
+
remember: true,
|
| 1463 |
+
});
|
| 1464 |
});
|
| 1465 |
});
|
| 1466 |
|
|
|
|
| 1479 |
if (rawToggle) {
|
| 1480 |
rawToggle.addEventListener('change', () => {
|
| 1481 |
const showRaw = rawToggle.checked;
|
| 1482 |
+
selectOutputView(showRaw ? 'raw' : 'cards', { remember: true });
|
| 1483 |
|
| 1484 |
const mobileRawToggle = document.getElementById('raw-toggle-mobile');
|
| 1485 |
if (mobileRawToggle) {
|
static/style.css
CHANGED
|
@@ -2378,6 +2378,24 @@ body.page-wrapper {
|
|
| 2378 |
text-decoration: none;
|
| 2379 |
}
|
| 2380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2381 |
.hero-attribution .material-symbols-outlined {
|
| 2382 |
font-size: 14px;
|
| 2383 |
}
|
|
@@ -2442,40 +2460,29 @@ body.page-wrapper {
|
|
| 2442 |
}
|
| 2443 |
|
| 2444 |
.sample-rail-shell::after {
|
| 2445 |
-
|
| 2446 |
-
top: 0;
|
| 2447 |
-
right: 0;
|
| 2448 |
-
bottom: 7px;
|
| 2449 |
-
width: 34px;
|
| 2450 |
-
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.96));
|
| 2451 |
-
content: '';
|
| 2452 |
-
pointer-events: none;
|
| 2453 |
}
|
| 2454 |
|
| 2455 |
.sample-buttons {
|
| 2456 |
-
display:
|
| 2457 |
-
|
| 2458 |
gap: 8px;
|
| 2459 |
margin: 0;
|
| 2460 |
-
padding: 2px
|
| 2461 |
-
overflow-x: auto;
|
| 2462 |
-
overscroll-behavior-inline: contain;
|
| 2463 |
-
scrollbar-color: #c5d0e1 transparent;
|
| 2464 |
-
scrollbar-width: thin;
|
| 2465 |
}
|
| 2466 |
|
| 2467 |
.sample-button {
|
| 2468 |
-
|
| 2469 |
-
min-width: 136px;
|
| 2470 |
padding: 9px 11px;
|
| 2471 |
border-radius: 10px;
|
| 2472 |
box-shadow: none;
|
| 2473 |
}
|
| 2474 |
|
| 2475 |
.sample-title {
|
|
|
|
| 2476 |
margin-bottom: 4px;
|
| 2477 |
font-size: 13px;
|
| 2478 |
-
|
| 2479 |
}
|
| 2480 |
|
| 2481 |
.sample-modality {
|
|
@@ -2939,6 +2946,11 @@ body.page-wrapper {
|
|
| 2939 |
width: 130px;
|
| 2940 |
}
|
| 2941 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2942 |
.safety-strip {
|
| 2943 |
align-items: flex-start;
|
| 2944 |
}
|
|
@@ -2953,6 +2965,39 @@ body.page-wrapper {
|
|
| 2953 |
display: none;
|
| 2954 |
}
|
| 2955 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2956 |
.transformation-studio.text-area-container {
|
| 2957 |
display: flex !important;
|
| 2958 |
flex-direction: column;
|
|
|
|
| 2378 |
text-decoration: none;
|
| 2379 |
}
|
| 2380 |
|
| 2381 |
+
.hero-links {
|
| 2382 |
+
display: flex;
|
| 2383 |
+
justify-content: flex-end;
|
| 2384 |
+
flex-wrap: wrap;
|
| 2385 |
+
gap: 5px 13px;
|
| 2386 |
+
}
|
| 2387 |
+
|
| 2388 |
+
.hero-attribution a:hover {
|
| 2389 |
+
text-decoration: underline;
|
| 2390 |
+
text-underline-offset: 3px;
|
| 2391 |
+
}
|
| 2392 |
+
|
| 2393 |
+
.hero-attribution a:focus-visible {
|
| 2394 |
+
border-radius: 4px;
|
| 2395 |
+
outline: 3px solid rgba(26, 115, 232, 0.35);
|
| 2396 |
+
outline-offset: 3px;
|
| 2397 |
+
}
|
| 2398 |
+
|
| 2399 |
.hero-attribution .material-symbols-outlined {
|
| 2400 |
font-size: 14px;
|
| 2401 |
}
|
|
|
|
| 2460 |
}
|
| 2461 |
|
| 2462 |
.sample-rail-shell::after {
|
| 2463 |
+
content: none;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2464 |
}
|
| 2465 |
|
| 2466 |
.sample-buttons {
|
| 2467 |
+
display: grid;
|
| 2468 |
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
| 2469 |
gap: 8px;
|
| 2470 |
margin: 0;
|
| 2471 |
+
padding: 2px;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2472 |
}
|
| 2473 |
|
| 2474 |
.sample-button {
|
| 2475 |
+
min-width: 0;
|
|
|
|
| 2476 |
padding: 9px 11px;
|
| 2477 |
border-radius: 10px;
|
| 2478 |
box-shadow: none;
|
| 2479 |
}
|
| 2480 |
|
| 2481 |
.sample-title {
|
| 2482 |
+
min-height: 2.4em;
|
| 2483 |
margin-bottom: 4px;
|
| 2484 |
font-size: 13px;
|
| 2485 |
+
line-height: 1.2;
|
| 2486 |
}
|
| 2487 |
|
| 2488 |
.sample-modality {
|
|
|
|
| 2946 |
width: 130px;
|
| 2947 |
}
|
| 2948 |
|
| 2949 |
+
.hero-links {
|
| 2950 |
+
width: 100%;
|
| 2951 |
+
justify-content: flex-start;
|
| 2952 |
+
}
|
| 2953 |
+
|
| 2954 |
.safety-strip {
|
| 2955 |
align-items: flex-start;
|
| 2956 |
}
|
|
|
|
| 2965 |
display: none;
|
| 2966 |
}
|
| 2967 |
|
| 2968 |
+
.sample-rail-shell::after {
|
| 2969 |
+
position: absolute;
|
| 2970 |
+
top: 0;
|
| 2971 |
+
right: 0;
|
| 2972 |
+
bottom: 7px;
|
| 2973 |
+
width: 34px;
|
| 2974 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.96));
|
| 2975 |
+
content: '';
|
| 2976 |
+
pointer-events: none;
|
| 2977 |
+
}
|
| 2978 |
+
|
| 2979 |
+
.sample-buttons {
|
| 2980 |
+
display: flex;
|
| 2981 |
+
justify-content: flex-start;
|
| 2982 |
+
padding: 2px 28px 7px 2px;
|
| 2983 |
+
overflow-x: auto;
|
| 2984 |
+
overscroll-behavior-inline: contain;
|
| 2985 |
+
scrollbar-color: #c5d0e1 transparent;
|
| 2986 |
+
scrollbar-width: thin;
|
| 2987 |
+
}
|
| 2988 |
+
|
| 2989 |
+
.sample-button {
|
| 2990 |
+
flex: 0 0 auto;
|
| 2991 |
+
min-width: 136px;
|
| 2992 |
+
}
|
| 2993 |
+
|
| 2994 |
+
.sample-title {
|
| 2995 |
+
min-height: 0;
|
| 2996 |
+
overflow: hidden;
|
| 2997 |
+
text-overflow: ellipsis;
|
| 2998 |
+
white-space: nowrap;
|
| 2999 |
+
}
|
| 3000 |
+
|
| 3001 |
.transformation-studio.text-area-container {
|
| 3002 |
display: flex !important;
|
| 3003 |
flex-direction: column;
|
templates/index.html
CHANGED
|
@@ -95,15 +95,26 @@
|
|
| 95 |
width="174"
|
| 96 |
height="25"
|
| 97 |
/>
|
| 98 |
-
<
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
</div>
|
| 108 |
<div class="safety-strip" role="note">
|
| 109 |
<span class="material-symbols-outlined" aria-hidden="true"
|
|
@@ -121,11 +132,9 @@
|
|
| 121 |
<div class="sample-heading-row">
|
| 122 |
<div>
|
| 123 |
<p class="section-kicker">Start with a study</p>
|
| 124 |
-
<h2 id="sample-heading">Explore
|
| 125 |
</div>
|
| 126 |
-
<p>
|
| 127 |
-
Choose any modality. Results appear instantly from the demo cache.
|
| 128 |
-
</p>
|
| 129 |
</div>
|
| 130 |
<div class="sample-rail-shell">
|
| 131 |
<div class="sample-buttons" aria-label="Sample radiology reports"></div>
|
|
@@ -217,16 +226,6 @@
|
|
| 217 |
<h2>Structured extraction</h2>
|
| 218 |
</div>
|
| 219 |
<div class="output-view-tabs" role="tablist" aria-label="Output view">
|
| 220 |
-
<button
|
| 221 |
-
class="output-view-tab active"
|
| 222 |
-
id="findings-tab"
|
| 223 |
-
data-output-view="cards"
|
| 224 |
-
role="tab"
|
| 225 |
-
aria-selected="true"
|
| 226 |
-
aria-controls="findings-cards"
|
| 227 |
-
>
|
| 228 |
-
Findings
|
| 229 |
-
</button>
|
| 230 |
<button
|
| 231 |
class="output-view-tab"
|
| 232 |
id="report-tab"
|
|
@@ -239,6 +238,16 @@
|
|
| 239 |
>
|
| 240 |
Report
|
| 241 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
<button
|
| 243 |
class="output-view-tab"
|
| 244 |
id="raw-tab"
|
|
|
|
| 95 |
width="174"
|
| 96 |
height="25"
|
| 97 |
/>
|
| 98 |
+
<div class="hero-links">
|
| 99 |
+
<a
|
| 100 |
+
href="https://github.com/google/langextract"
|
| 101 |
+
target="_blank"
|
| 102 |
+
rel="noopener noreferrer"
|
| 103 |
+
>LangExtract on GitHub
|
| 104 |
+
<span class="material-symbols-outlined" aria-hidden="true"
|
| 105 |
+
>open_in_new</span
|
| 106 |
+
></a
|
| 107 |
+
>
|
| 108 |
+
<a
|
| 109 |
+
href="https://developers.googleblog.com/en/introducing-langextract-a-gemini-powered-information-extraction-library"
|
| 110 |
+
target="_blank"
|
| 111 |
+
rel="noopener noreferrer"
|
| 112 |
+
>LangExtract release blog
|
| 113 |
+
<span class="material-symbols-outlined" aria-hidden="true"
|
| 114 |
+
>open_in_new</span
|
| 115 |
+
></a
|
| 116 |
+
>
|
| 117 |
+
</div>
|
| 118 |
</div>
|
| 119 |
<div class="safety-strip" role="note">
|
| 120 |
<span class="material-symbols-outlined" aria-hidden="true"
|
|
|
|
| 132 |
<div class="sample-heading-row">
|
| 133 |
<div>
|
| 134 |
<p class="section-kicker">Start with a study</p>
|
| 135 |
+
<h2 id="sample-heading">Explore sample reports</h2>
|
| 136 |
</div>
|
| 137 |
+
<p>Choose a study. Built-in examples can use saved or live results.</p>
|
|
|
|
|
|
|
| 138 |
</div>
|
| 139 |
<div class="sample-rail-shell">
|
| 140 |
<div class="sample-buttons" aria-label="Sample radiology reports"></div>
|
|
|
|
| 226 |
<h2>Structured extraction</h2>
|
| 227 |
</div>
|
| 228 |
<div class="output-view-tabs" role="tablist" aria-label="Output view">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
<button
|
| 230 |
class="output-view-tab"
|
| 231 |
id="report-tab"
|
|
|
|
| 238 |
>
|
| 239 |
Report
|
| 240 |
</button>
|
| 241 |
+
<button
|
| 242 |
+
class="output-view-tab active"
|
| 243 |
+
id="findings-tab"
|
| 244 |
+
data-output-view="cards"
|
| 245 |
+
role="tab"
|
| 246 |
+
aria-selected="true"
|
| 247 |
+
aria-controls="findings-cards"
|
| 248 |
+
>
|
| 249 |
+
Findings
|
| 250 |
+
</button>
|
| 251 |
<button
|
| 252 |
class="output-view-tab"
|
| 253 |
id="raw-tab"
|
test_app.py
CHANGED
|
@@ -494,6 +494,55 @@ class TestFrontendAndProjectConfiguration(unittest.TestCase):
|
|
| 494 |
self.assertIn('data-output-view="raw"', template)
|
| 495 |
self.assertIn('id="prompt-toggle"', template)
|
| 496 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
def test_mobile_and_reduced_motion_studio_rules_are_defined(self):
|
| 498 |
script = Path('static/script.js').read_text()
|
| 499 |
stylesheet = Path('static/style.css').read_text()
|
|
|
|
| 494 |
self.assertIn('data-output-view="raw"', template)
|
| 495 |
self.assertIn('id="prompt-toggle"', template)
|
| 496 |
|
| 497 |
+
def test_langextract_links_and_sample_copy_are_professional(self):
|
| 498 |
+
template = Path('templates/index.html').read_text()
|
| 499 |
+
stylesheet = Path('static/style.css').read_text()
|
| 500 |
+
|
| 501 |
+
self.assertIn('href="https://github.com/google/langextract"', template)
|
| 502 |
+
self.assertIn('LangExtract on GitHub', template)
|
| 503 |
+
self.assertIn('LangExtract release blog', template)
|
| 504 |
+
self.assertGreaterEqual(template.count('rel="noopener noreferrer"'), 2)
|
| 505 |
+
self.assertIn('<h2 id="sample-heading">Explore sample reports</h2>', template)
|
| 506 |
+
self.assertIn('Choose a study.', template)
|
| 507 |
+
self.assertNotIn('Explore a cached report', template)
|
| 508 |
+
self.assertNotIn('Results appear instantly from the demo cache.', template)
|
| 509 |
+
self.assertIn('.hero-attribution a:focus-visible', stylesheet)
|
| 510 |
+
|
| 511 |
+
def test_sample_grid_and_report_default_preserve_responsive_fallbacks(self):
|
| 512 |
+
script = Path('static/script.js').read_text()
|
| 513 |
+
stylesheet = Path('static/style.css').read_text()
|
| 514 |
+
template = Path('templates/index.html').read_text()
|
| 515 |
+
|
| 516 |
+
self.assertIn('grid-template-columns: repeat(5, minmax(0, 1fr));', stylesheet)
|
| 517 |
+
mobile_rules = stylesheet[stylesheet.index('@media (max-width: 768px)') :]
|
| 518 |
+
self.assertIn('.sample-buttons {', mobile_rules)
|
| 519 |
+
self.assertIn('display: flex;', mobile_rules)
|
| 520 |
+
self.assertIn('overflow-x: auto;', mobile_rules)
|
| 521 |
+
self.assertLess(
|
| 522 |
+
template.index('id="report-tab"'), template.index('id="findings-tab"')
|
| 523 |
+
)
|
| 524 |
+
self.assertIn("let preferredStructuredView = 'report';", script)
|
| 525 |
+
self.assertIn('selectOutputView(preferredStructuredView);', script)
|
| 526 |
+
self.assertGreaterEqual(script.count('remember: true'), 3)
|
| 527 |
+
self.assertIn(
|
| 528 |
+
"selectOutputView(showRaw ? 'raw' : 'cards', { remember: true });",
|
| 529 |
+
script,
|
| 530 |
+
)
|
| 531 |
+
self.assertIn("tab.getAttribute('aria-selected') === 'true'", script)
|
| 532 |
+
self.assertLess(
|
| 533 |
+
script.index(
|
| 534 |
+
"selectOutputView('cards');",
|
| 535 |
+
script.index('predictButton.addEventListener'),
|
| 536 |
+
),
|
| 537 |
+
script.index(
|
| 538 |
+
'setOutputViewsAvailable(false);',
|
| 539 |
+
script.index('predictButton.addEventListener'),
|
| 540 |
+
),
|
| 541 |
+
)
|
| 542 |
+
self.assertNotIn('balanceByColumnCount', script)
|
| 543 |
+
self.assertNotIn('BALANCE_DELAY', script)
|
| 544 |
+
self.assertNotIn('RESIZE_DEBOUNCE', script)
|
| 545 |
+
|
| 546 |
def test_mobile_and_reduced_motion_studio_rules_are_defined(self):
|
| 547 |
script = Path('static/script.js').read_text()
|
| 548 |
stylesheet = Path('static/style.css').read_text()
|