term-comparison / src /lib /DatasetViewer.svelte
Tobias Brugger
styling
1e9736c
<script lang="ts">
import {
fetchAllRows,
fetchDatasetConfigYaml,
type DatasetConfigYaml,
} from "./huggingfaceApi";
import type {
TermDefinition,
EquivalencyScore,
GradingTemplateEntry,
} from "./types";
import { marked } from "marked";
import { config, getDatasetName } from "./config";
// Configuration from centralized config
const availableJurisdictions = config.jurisdictions;
const dataset = getDatasetName("structured-answers");
const scoresDataset = getDatasetName("equivalency-scores");
const gradingTemplatesDataset = getDatasetName("grading-templates");
const directTranslations = config.directTranslations;
const equivalencyScoreLabels = config.equivalencyScoreLabels;
// Create bidirectional lookup map (both English->Swedish and Swedish->English)
const translationMap = new Map<string, string>();
Object.entries(directTranslations).forEach(([en, sv]) => {
translationMap.set(en.toLowerCase(), sv.toLowerCase());
translationMap.set(sv.toLowerCase(), en.toLowerCase());
});
// Form inputs
let jurisdiction1 = $state<string>(config.defaults.jurisdiction1);
let jurisdiction2 = $state<string>(config.defaults.jurisdiction2);
// Data state
let data1 = $state<TermDefinition[]>([]);
let data2 = $state<TermDefinition[]>([]);
let equivalencyScores = $state<EquivalencyScore[]>([]);
let selectedTerm1 = $state<string | null>(null);
let selectedTerm2 = $state<string | null>(null);
let datasetConfig = $state<DatasetConfigYaml | null>(null);
let gradingTemplates_j1 = $state<GradingTemplateEntry[]>([]);
let gradingTemplates_j2 = $state<GradingTemplateEntry[]>([]);
// Track previous selected terms for detecting user-initiated changes (loop prevention)
let prevSelectedTerm1 = $state<string | null>(null);
let prevSelectedTerm2 = $state<string | null>(null);
// UI state
let loading = $state(false);
let error = $state("");
let hasLoaded = $state(false);
let loadingDetails = $state(false);
let selectedCategory = $state<string>("All");
// Track previous values to detect changes
let prevJurisdiction1 = $state<string>(config.defaults.jurisdiction1);
let prevJurisdiction2 = $state<string>(config.defaults.jurisdiction2);
// Reload when jurisdictions change
$effect(() => {
const j1 = jurisdiction1;
const j2 = jurisdiction2;
if (hasLoaded && (j1 !== prevJurisdiction1 || j2 !== prevJurisdiction2)) {
prevJurisdiction1 = j1;
prevJurisdiction2 = j2;
loadComparison();
}
});
async function loadComparison() {
if (jurisdiction1 === jurisdiction2) {
error = "Please select two different jurisdictions";
return;
}
loading = true;
error = "";
data1 = [];
data2 = [];
equivalencyScores = [];
selectedTerm1 = null;
selectedTerm2 = null;
prevSelectedTerm1 = null;
prevSelectedTerm2 = null;
hasLoaded = false;
// Start loading grading templates/details concurrently to reduce wait time
const detailsPromise = loadDetails();
try {
// Determine the config name for equivalency scores (alphabetically sorted)
const sortedJurisdictions = [jurisdiction1, jurisdiction2].sort();
const scoresConfig = `${sortedJurisdictions[0]}_${sortedJurisdictions[1]}`;
// Fetch both jurisdictions and equivalency scores in parallel
const [rows1, rows2, scoresRows] = await Promise.all([
fetchAllRows(dataset, jurisdiction1, "train", undefined),
fetchAllRows(dataset, jurisdiction2, "train", undefined),
fetchAllRows(scoresDataset, scoresConfig, "train", undefined),
]);
data1 = (rows1 as TermDefinition[]).sort((a, b) =>
a.term.localeCompare(b.term)
);
data2 = (rows2 as TermDefinition[]).sort((a, b) =>
a.term.localeCompare(b.term)
);
equivalencyScores = scoresRows as EquivalencyScore[];
hasLoaded = true;
// Fetch config.yaml for this jurisdiction pair
try {
const configPath = `${jurisdiction1}_${jurisdiction2}/config.yaml`;
datasetConfig = await fetchDatasetConfigYaml(
scoresDataset,
configPath,
"main",
undefined
);
} catch (e) {
datasetConfig = null;
console.warn("Could not load dataset config.yaml", e);
}
// Auto-select first term from jurisdiction 1 and its equivalent in jurisdiction 2
if (data1.length > 0) {
const firstTerm = data1[0].term;
selectedTerm1 = firstTerm;
prevSelectedTerm1 = firstTerm;
// Try to find direct translation equivalent in jurisdiction 2
if (data2.length > 0) {
const translation = getDirectTranslation(firstTerm);
const matchingTerm = translation
? findTermInData(translation, data2)
: null;
if (matchingTerm) {
selectedTerm2 = matchingTerm;
prevSelectedTerm2 = matchingTerm;
} else {
selectedTerm2 = data2[0].term;
prevSelectedTerm2 = data2[0].term;
}
}
} else if (data2.length > 0) {
selectedTerm2 = data2[0].term;
prevSelectedTerm2 = data2[0].term;
}
} catch (e) {
error = e instanceof Error ? e.message : "Failed to load jurisdictions";
console.error("Jurisdiction loading error:", e);
} finally {
loading = false;
// Ensure details loading finishes (any error already handled in loadDetails)
try {
await detailsPromise;
} catch (e) {
console.warn("loadDetails failed:", e);
}
}
}
async function loadDetails() {
loadingDetails = true;
try {
const [rows_j1, rows_j2] = await Promise.all([
fetchAllRows(
gradingTemplatesDataset,
jurisdiction1,
"train",
undefined
),
fetchAllRows(
gradingTemplatesDataset,
jurisdiction2,
"train",
undefined
),
]);
gradingTemplates_j1 = rows_j1 as GradingTemplateEntry[];
gradingTemplates_j2 = rows_j2 as GradingTemplateEntry[];
} catch (e) {
// Non-fatal: templates may not exist for all jurisdictions
gradingTemplates_j1 = [];
gradingTemplates_j2 = [];
console.warn("Failed to load grading templates:", e);
} finally {
loadingDetails = false;
}
}
function getSelectedDefinition1(): string {
if (!selectedTerm1) return "";
const found = data1.find((item) => item.term === selectedTerm1);
return found?.definition || "";
}
function getSelectedDefinition2(): string {
if (!selectedTerm2) return "";
const found = data2.find((item) => item.term === selectedTerm2);
return found?.definition || "";
}
function normalizeTermForComparison(term: string | undefined): string {
if (term === undefined) return "";
// Replace underscores with spaces and normalize whitespace for comparison
return term.replace(/_/g, " ").replace(/\s+/g, " ").trim().toLowerCase();
}
function formatCategoryName(name: string): string {
// Replace underscores with spaces for display
return name.replace(/_/g, " ");
}
// Get direct translation equivalent for a term
function getDirectTranslation(term: string): string | null {
const normalized = normalizeTermForComparison(term);
return translationMap.get(normalized) || null;
}
// Find a term in a data array by its normalized form
function findTermInData(
normalizedTarget: string,
data: TermDefinition[]
): string | null {
const found = data.find(
(item) => normalizeTermForComparison(item.term) === normalizedTarget
);
return found?.term || null;
}
// Auto-select direct translation when selectedTerm1 changes (user-initiated)
$effect(() => {
const current = selectedTerm1;
if (current && current !== prevSelectedTerm1 && data2.length > 0) {
const translation = getDirectTranslation(current);
if (translation) {
const matchingTerm = findTermInData(translation, data2);
if (matchingTerm && matchingTerm !== selectedTerm2) {
selectedTerm2 = matchingTerm;
prevSelectedTerm2 = matchingTerm;
}
}
prevSelectedTerm1 = current;
}
});
// Auto-select direct translation when selectedTerm2 changes (user-initiated)
$effect(() => {
const current = selectedTerm2;
if (current && current !== prevSelectedTerm2 && data1.length > 0) {
const translation = getDirectTranslation(current);
if (translation) {
const matchingTerm = findTermInData(translation, data1);
if (matchingTerm && matchingTerm !== selectedTerm1) {
selectedTerm1 = matchingTerm;
prevSelectedTerm1 = matchingTerm;
}
}
prevSelectedTerm2 = current;
}
});
// Check if a term should be highlighted based on the selected term in the other jurisdiction
function shouldHighlightTerm(
term: string,
selectedInOtherJurisdiction: string | null
): boolean {
if (!selectedInOtherJurisdiction) return false;
const equivalentOfSelected = getDirectTranslation(
selectedInOtherJurisdiction
);
if (!equivalentOfSelected) return false;
const normalizedTerm = normalizeTermForComparison(term);
return normalizedTerm === equivalentOfSelected;
}
function getSimilarityScore(): EquivalencyScore | null {
if (!selectedTerm1 || !selectedTerm2 || equivalencyScores.length === 0) {
return null;
}
// Normalize selected terms for comparison
const normalizedTerm1 = normalizeTermForComparison(selectedTerm1);
const normalizedTerm2 = normalizeTermForComparison(selectedTerm2);
// Look for the score in both directions, comparing normalized terms
const score = equivalencyScores.find((s) => {
const normalizedJ1 = normalizeTermForComparison(s.term_j1);
const normalizedJ2 = normalizeTermForComparison(s.term_j2);
return (
(normalizedJ1 === normalizedTerm1 &&
normalizedJ2 === normalizedTerm2) ||
(normalizedJ1 === normalizedTerm2 && normalizedJ2 === normalizedTerm1)
);
});
return score || null;
}
// Get unique categories from comparisons
function getUniqueCategories(score: EquivalencyScore | null): string[] {
if (!score || !score.comparisons) return [];
const categories = new Set(score.comparisons.map((c) => c.category));
return Array.from(categories).sort();
}
// Get label for equivalency score based on configured ranges
function getScoreLabel(scoreValue: number): string {
const match = equivalencyScoreLabels.find(
(range) => scoreValue >= range.min && scoreValue < range.max
);
// Handle edge case for max score (5.0)
if (!match && scoreValue >= 5.0) {
return equivalencyScoreLabels[equivalencyScoreLabels.length - 1].label;
}
return match?.label || "";
}
// Return a CSS class name for coloring scores (low/medium/high)
function getScoreClass(scoreValue: number): string {
// More fine-grained buckets across the 1.0-5.0 range
// 1.0 - 1.99 -> very-low
// 2.0 - 2.49 -> low
// 2.5 - 2.99 -> medium-low
// 3.0 - 3.49 -> medium
// 3.5 - 3.99 -> medium-high
// 4.0 - 4.49 -> high
// 4.5 - 5.0+ -> very-high
if (scoreValue >= 4.5) return "score-very-high";
if (scoreValue >= 4.0) return "score-high";
if (scoreValue >= 3.5) return "score-medium-high";
if (scoreValue >= 3.0) return "score-medium";
if (scoreValue >= 2.5) return "score-medium-low";
if (scoreValue >= 2.0) return "score-low";
return "score-very-low";
}
// Filter comparisons by selected category
function getFilteredComparisons(score: EquivalencyScore | null) {
if (!score || !score.comparisons) return [];
if (selectedCategory === "All") return score.comparisons;
return score.comparisons.filter((c) => c.category === selectedCategory);
}
// Find a grading template entry matching a comparison's category/subcategory and optional term
function findGradingTemplate(
category: string,
subcategory: string | undefined,
templates: GradingTemplateEntry[],
term?: string
): GradingTemplateEntry | null {
if (!templates || templates.length === 0) return null;
const normalizedTarget = term ? normalizeTermForComparison(term) : null;
return (
templates.find((t) => {
const catMatch =
t.category === category &&
(t.subcategory || "") === (subcategory || "");
if (!catMatch) return false;
if (!normalizedTarget) return true;
return normalizeTermForComparison(t.term) === normalizedTarget;
}) || null
);
}
// Helper getters for templates + question text to avoid {@const} in template
function getTemplateJ1(comparison: {
category: string;
subcategory?: string;
term_j1?: string;
}) {
return findGradingTemplate(
comparison.category,
comparison.subcategory,
gradingTemplates_j1,
comparison.term_j1
);
}
function getTemplateJ2(comparison: {
category: string;
subcategory?: string;
term_j2?: string;
}) {
return findGradingTemplate(
comparison.category,
comparison.subcategory,
gradingTemplates_j2,
comparison.term_j2
);
}
function getQuestionText(comparison: {
category: string;
subcategory?: string;
term_j1?: string;
term_j2?: string;
}) {
return (
getTemplateJ1(comparison)?.question ||
getTemplateJ2(comparison)?.question ||
""
);
}
</script>
<div class="jurisdiction-comparison">
<h2>Jurisdiction Term Comparison</h2>
<!-- Model Info Display -->
{#if datasetConfig}
<div class="dataset-model-info subtle">
<span class="info-tooltip-inline">
<strong>Scoring Model:</strong>
{datasetConfig.scoring_model}
<span class="tooltip-content-inline">
<strong>Scoring Model</strong> is the model used to compute the equivalency
score between terms. It uses a language model to assess how closely terms
from different jurisdictions match in meaning.
</span>
</span>
<span class="sep">|</span>
<span class="info-tooltip-inline">
<strong>Synthesis Model:</strong>
{datasetConfig.synthesis_model}
<span class="tooltip-content-inline">
<strong>Synthesis Model</strong> is the model used to synthesize comparative
law notes. Generative AI is used to assess and explain the similarities
and differences between terms from different jurisdictions.
</span>
</span>
<span class="sep">|</span>
<span>
<strong>Last Updated:</strong>
{datasetConfig.generation_date || "N/A"}
</span>
</div>
{/if}
<div class="form">
<div class="form-row">
<div class="form-group">
<label for="jurisdiction1">Jurisdiction 1:</label>
<select
id="jurisdiction1"
bind:value={jurisdiction1}
disabled={loading}
>
{#each availableJurisdictions as jur}
<option value={jur.code}>{jur.label}</option>
{/each}
</select>
</div>
<div class="form-group">
<label for="jurisdiction2">Jurisdiction 2:</label>
<select
id="jurisdiction2"
bind:value={jurisdiction2}
disabled={loading}
>
{#each availableJurisdictions as jur}
<option value={jur.code}>{jur.label}</option>
{/each}
</select>
</div>
</div>
{#if !hasLoaded}
<button onclick={loadComparison} disabled={loading}>
{loading ? "Loading..." : "Load Comparison"}
</button>
{/if}
</div>
{#if error}
<div class="error">
<strong>Error:</strong>
{error}
</div>
{/if}
{#if hasLoaded}
<div class="comparison-container">
<!-- Jurisdiction 1 -->
<div class="jurisdiction-column">
<h3>
{availableJurisdictions.find((j) => j.code === jurisdiction1)?.label}
</h3>
<div class="term-selector">
<label for="term1">Select Term:</label>
<select id="term1" bind:value={selectedTerm1}>
{#each data1 as item}
<option value={item.term}>
{shouldHighlightTerm(item.term, selectedTerm2)
? "★ "
: ""}{item.term}
</option>
{/each}
</select>
<small class="translation-hint"
>★ = Direct translation of the term selected in the opposite
jurisdiction</small
>
</div>
{#if selectedTerm1}
<div class="definition-box">
<h4>Definition:</h4>
<div class="markdown-content">
{@html marked(getSelectedDefinition1())}
</div>
</div>
{/if}
<div class="info">
<small>{data1.length} terms available</small>
</div>
</div>
<!-- Jurisdiction 2 -->
<div class="jurisdiction-column">
<h3>
{availableJurisdictions.find((j) => j.code === jurisdiction2)?.label}
</h3>
<div class="term-selector">
<label for="term2">Select Term:</label>
<select id="term2" bind:value={selectedTerm2}>
{#each data2 as item}
<option value={item.term}>
{shouldHighlightTerm(item.term, selectedTerm1)
? "★ "
: ""}{item.term}
</option>
{/each}
</select>
<small class="translation-hint"
>★ = Direct translation of the term selected in the opposite
jurisdiction</small
>
</div>
{#if selectedTerm2}
<div class="definition-box">
<h4>Definition:</h4>
<div class="markdown-content">
{@html marked(getSelectedDefinition2())}
</div>
</div>
{/if}
<div class="info">
<small>{data2.length} terms available</small>
</div>
</div>
</div>
<!-- Similarity Score Display -->
{#if selectedTerm1 && selectedTerm2}
{@const score = getSimilarityScore()}
{#if score}
<div class="similarity-score">
<h3>Equivalency Score</h3>
<div class="score-display">
<span class="score-label">
{getScoreLabel(score.aggregated_similarity_score)}
<!-- Info icon with tooltip -->
<span class="info-tooltip">
<svg
class="info-icon"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
<span class="tooltip-content">
<strong>Score Legend (1 – 5)</strong>
{#each equivalencyScoreLabels as range}
<span class="tooltip-row">
<span class="tooltip-range"
>{range.min.toFixed(2)}–{range.max.toFixed(2)}</span
>
<span>{range.label}</span>
</span>
{/each}
</span>
</span>
</span>
<strong class={getScoreClass(score.aggregated_similarity_score)}
>{score.aggregated_similarity_score.toFixed(2)}</strong
>
</div>
{#if score.comparative_law_note}
<div class="comparative-note">
<div class="markdown-content">
{@html marked(score.comparative_law_note)}
</div>
</div>
{/if}
{#if score.comparisons && score.comparisons.length > 0}
{@const categories = getUniqueCategories(score)}
{@const filteredComparisons = getFilteredComparisons(score)}
<div class="comparisons-section">
<h3>Detailed Comparisons</h3>
{#if loadingDetails}
<div class="details-loading">
<div class="spinner" aria-hidden="true"></div>
<div class="spinner-label">Loading details…</div>
</div>
{:else}
<!-- Category Filter Pills -->
{#if categories.length > 0}
<div class="category-filters">
<button
class="filter-pill"
class:active={selectedCategory === "All"}
onclick={() => (selectedCategory = "All")}
>
All ({score.comparisons.length})
</button>
{#each categories as category}
{@const count = score.comparisons.filter(
(c) => c.category === category
).length}
<button
class="filter-pill"
class:active={selectedCategory === category}
onclick={() => (selectedCategory = category)}
>
{formatCategoryName(category)} ({count})
</button>
{/each}
</div>
{/if}
<div class="comparisons-list">
{#if selectedTerm1 || selectedTerm2}
<div class="comparisons-header">
<div class="header-column left">
<div class="detail-term">
{normalizeTermForComparison(selectedTerm1)}
</div>
</div>
<div class="header-column center">
<div class="detail-term">Comparison</div>
</div>
<div class="header-column right">
<div class="detail-term">
{normalizeTermForComparison(selectedTerm2)}
</div>
</div>
</div>
{/if}
{#each filteredComparisons as comparison}
<div class="comparison-item">
<div class="comparison-header">
<span class="comparison-category">
{formatCategoryName(comparison.category)}
</span>
{#if comparison.subcategory}
<span class="comparison-subcategory">
› {formatCategoryName(comparison.subcategory)}
</span>
{/if}
<div class="category-meta">
<span class="category-weight"
>Weight: {comparison.weight.toFixed(2)}</span
>
<span class="category-weighted"
>Weighted Score: {comparison.weighted_similarity_score.toFixed(
2
)}</span
>
</div>
<span
class={"comparison-score " +
getScoreClass(comparison.similarity_score)}
>{comparison.similarity_score.toFixed(2)}</span
>
</div>
{#if getQuestionText(comparison)}
<div class="detail-question-single">
{getQuestionText(comparison)}
</div>
{/if}
<div class="comparison-details">
<div class="detail-column left">
{#if getTemplateJ1(comparison)}
<div class="detail-answer">
{getTemplateJ1(comparison)?.answer}
</div>
{:else}
<div class="detail-empty">—</div>
{/if}
</div>
<div class="detail-column center">
{#if comparison.reasoning}
<div class="comparison-reasoning">
{comparison.reasoning}
</div>
{/if}
</div>
<div class="detail-column right">
{#if getTemplateJ2(comparison)}
<div class="detail-answer">
{getTemplateJ2(comparison)?.answer}
</div>
{:else}
<div class="detail-empty">—</div>
{/if}
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
{/if}
</div>
{:else}
<div class="similarity-score no-score">
<p>No equivalency score found for this term pair.</p>
</div>
{/if}
{/if}
{/if}
</div>
<style>
.jurisdiction-comparison {
max-width: 75%;
margin: 0 auto;
padding: 2rem;
color: #333;
}
h2 {
margin-bottom: 1.5rem;
color: #333;
text-align: center;
background: none;
font-weight: 700;
letter-spacing: 0.02em;
}
@media (prefers-color-scheme: dark) {
h2 {
color: #f7f7f7;
}
}
h3 {
margin: 0 0 1rem 0;
color: #444;
font-size: 1.25rem;
}
h4 {
margin: 0 0 0.5rem 0;
color: #555;
font-size: 1rem;
font-weight: 600;
}
.form {
background: #f5f5f5;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 2rem;
color: #333;
}
.form-group {
margin-bottom: 1rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #555;
}
select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
background: white;
color: #333;
}
select:disabled {
background: #e9e9e9;
cursor: not-allowed;
}
button {
background: #646cff;
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
width: 100%;
}
button:hover:not(:disabled) {
background: #535bf2;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.error {
background: #fee;
color: #c33;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
border: 1px solid #fcc;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-top: 2rem;
}
.jurisdiction-column {
background: white;
padding: 1.5rem;
border-radius: 8px;
border: 1px solid #ddd;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
color: #333;
}
.term-selector {
margin-bottom: 1.5rem;
}
.term-selector select {
font-size: 1rem;
padding: 0.75rem;
}
.definition-box {
background: #f9f9f9;
padding: 1.25rem;
border-radius: 6px;
border-left: 4px solid #646cff;
margin-bottom: 1rem;
min-height: 100px;
}
.info {
color: #666;
font-size: 0.875rem;
}
.info small {
font-size: 0.875rem;
}
.similarity-score {
background: white;
padding: 1.5rem;
border-radius: 8px;
border: 2px solid #646cff;
margin-top: 2rem;
color: #333;
}
.similarity-score h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #646cff;
text-align: center;
}
.similarity-score.no-score {
border-color: #ddd;
background: #f9f9f9;
text-align: center;
}
.similarity-score.no-score p {
margin: 0;
color: #666;
}
.score-display {
text-align: center;
}
.score-display .score-label {
display: block;
font-size: 1.1rem;
color: #555;
font-weight: 500;
margin-bottom: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.score-display strong {
display: block;
font-size: 3rem;
color: #646cff;
font-weight: 600;
}
/* Info Tooltip Styles */
.info-tooltip {
position: relative;
display: inline-flex;
align-items: center;
margin-left: 0.35rem;
cursor: help;
}
.info-icon {
color: #888;
vertical-align: middle;
transition: color 0.2s;
}
.info-tooltip:hover .info-icon {
color: #646cff;
}
.tooltip-content {
display: none;
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
background: #333;
color: white;
padding: 0.75rem 1rem;
border-radius: 6px;
font-size: 0.8rem;
white-space: nowrap;
z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
text-transform: none;
letter-spacing: normal;
text-align: left;
}
.tooltip-content::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #333;
}
.info-tooltip:hover .tooltip-content {
display: block;
}
.tooltip-content strong {
display: block;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: #fff;
border-bottom: 1px solid #555;
padding-bottom: 0.4rem;
}
.tooltip-row {
display: flex;
gap: 0.75rem;
padding: 0.2rem 0;
font-weight: normal;
}
.tooltip-range {
font-family: monospace;
color: #a5a8ff;
min-width: 70px;
}
.comparative-note {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid #e0e0e0;
}
.markdown-content {
line-height: 1.6;
color: #333;
text-align: left;
}
.markdown-content :global(h1),
.markdown-content :global(h2),
.markdown-content :global(h3),
.markdown-content :global(h4),
.markdown-content :global(h5),
.markdown-content :global(h6) {
margin-top: 1em;
margin-bottom: 0.5em;
font-weight: 600;
}
.markdown-content :global(p) {
margin: 0 0 1em 0;
}
.markdown-content :global(ul),
.markdown-content :global(ol) {
margin: 0 0 1em 0;
padding-left: 2em;
}
.markdown-content :global(code) {
background: #f5f5f5;
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: monospace;
font-size: 0.9em;
}
.markdown-content :global(pre) {
background: #f5f5f5;
padding: 1em;
border-radius: 4px;
overflow-x: auto;
}
.markdown-content :global(pre code) {
background: none;
padding: 0;
}
.markdown-content :global(blockquote) {
border-left: 4px solid #646cff;
padding-left: 1em;
margin: 0 0 1em 0;
color: #666;
}
.markdown-content :global(a) {
color: #646cff;
text-decoration: none;
}
.markdown-content :global(a:hover) {
text-decoration: underline;
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.form-row {
grid-template-columns: 1fr;
}
}
.translation-hint {
display: block;
margin-top: 0.25rem;
color: #666;
font-style: italic;
}
/* Comparisons section styling */
.comparisons-section {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid #e0e0e0;
}
.comparisons-header {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
margin-bottom: 0.75rem;
align-items: center;
}
.comparisons-header .header-column {
background: none;
padding: 0.5rem 0.75rem;
color: #333;
font-weight: 700;
}
.comparisons-header .header-column.left {
text-align: left;
padding-left: 0.5rem;
border-right: 1px solid #e0e0e0;
}
.comparisons-header .header-column.center {
text-align: center;
border-right: 1px solid #e0e0e0;
}
.comparisons-header .header-column.right {
text-align: right;
padding-right: 0.5rem;
}
.comparisons-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.comparison-item {
background: #f9f9f9;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 1rem;
transition: box-shadow 0.2s;
}
.comparison-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.comparison-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.comparison-details {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
margin-top: 0.75rem;
align-items: start;
}
.detail-column {
background: #fff;
padding: 0.75rem;
border-radius: 6px;
border: 1px solid #f0f0f0;
min-height: 70px;
}
.detail-column.left {
border-left: 3px solid #646cff;
}
.detail-column.right {
border-right: 3px solid #646cff;
}
.detail-term {
font-weight: 600;
margin-bottom: 0.35rem;
color: #333;
}
.detail-question {
font-size: 0.95rem;
color: #555;
margin-bottom: 0.5rem;
}
.detail-answer {
font-size: 0.9rem;
color: #444;
}
.detail-empty {
color: #888;
font-style: italic;
}
@media (max-width: 900px) {
.comparison-details {
grid-template-columns: 1fr;
}
.comparisons-header {
grid-template-columns: 1fr;
}
.comparisons-header .header-column {
border-right: none;
text-align: left;
padding: 0.25rem 0.5rem;
}
}
.comparison-category {
font-weight: 600;
color: #646cff;
font-size: 0.95rem;
}
.comparison-subcategory {
color: #464444;
font-size: 0.9rem;
}
.comparison-score {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-weight: 600;
font-size: 0.9rem;
}
.category-meta {
margin-left: auto;
display: flex;
gap: 0.5rem;
align-items: center;
color: #666;
font-size: 0.9rem;
}
.category-weight,
.category-weighted {
background: #f0f0f0;
padding: 0.15rem 0.5rem;
border-radius: 6px;
font-weight: 600;
color: #444;
}
/* Score color variants */
.comparison-score.score-high {
background: #1a7f37;
color: white;
}
.comparison-score.score-medium {
background: #d98200;
color: white;
}
.comparison-score.score-low {
background: #c33;
color: white;
}
.score-high {
color: #1a7f37;
}
.score-medium {
color: #d98200;
}
.score-low {
color: #c33;
}
/* Fine-grained score variants */
.comparison-score.score-very-high {
background: #0f6b2d;
color: white;
}
.comparison-score.score-medium-high {
background: #2e9a44;
color: white;
}
.comparison-score.score-medium-low {
background: #f39c12;
color: white;
}
.comparison-score.score-very-low {
background: #9b1f1f;
color: white;
}
.score-very-high {
color: #0f6b2d;
}
.score-medium-high {
color: #2e9a44;
}
.score-medium-low {
color: #f39c12;
}
.score-very-low {
color: #9b1f1f;
}
.comparison-meta {
display: flex;
gap: 1rem;
margin-bottom: 0.5rem;
font-size: 0.85rem;
color: #666;
}
.comparison-weight,
.comparison-weighted-score {
background: #f0f0f0;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.comparison-reasoning {
margin-top: 0.5rem;
padding: 0.75rem;
background: white;
border-radius: 4px;
font-size: 0.9rem;
line-height: 1.5;
color: #444;
text-align: left;
}
/* Category filter pills */
.category-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.filter-pill {
background: #f0f0f0;
color: #555;
border: 1px solid #ddd;
border-radius: 20px;
padding: 0.5rem 1rem;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
width: auto;
}
.filter-pill:hover {
background: #e0e0e0;
border-color: #999;
}
.filter-pill.active {
background: #646cff;
color: white;
border-color: #646cff;
font-weight: 600;
}
.filter-pill.active:hover {
background: #535bf2;
border-color: #535bf2;
}
.dataset-model-info.subtle {
background: none;
border: none;
padding: 2rem 0;
margin: 0 0 0.5rem 0;
max-width: 100%;
color: #888;
font-size: 0.92rem;
text-align: left;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.dataset-model-info.subtle strong {
color: #888;
font-weight: 500;
margin-right: 0.15em;
}
.dataset-model-info.subtle .sep {
color: #ccc;
font-size: 1.1em;
margin: 0 0.5em;
}
.info-tooltip-inline {
position: relative;
display: inline-flex;
align-items: center;
cursor: help;
}
.info-tooltip-inline strong {
margin-right: 0.15em;
}
.info-tooltip-inline:hover .tooltip-content-inline {
display: block;
}
.tooltip-content-inline {
display: none;
position: absolute;
bottom: calc(100% + 8px);
left: 0;
background: #333;
color: white;
padding: 0.75rem 1rem;
border-radius: 6px;
font-size: 0.8rem;
white-space: normal;
z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
text-align: left;
min-width: 220px;
max-width: 320px;
}
.tooltip-content-inline strong {
display: block;
font-size: 0.85rem;
margin-bottom: 0.5rem;
color: #fff;
border-bottom: 1px solid #555;
padding-bottom: 0.4rem;
}
/* Details loading spinner */
.details-loading {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 0;
justify-content: center;
color: #666;
}
.spinner {
width: 28px;
height: 28px;
border: 4px solid #e6e6ff;
border-top-color: #646cff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.spinner-label {
font-size: 0.95rem;
color: #444;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>