Predictor / index.html
ThisIsAnAnonymousUser's picture
Update index.html
0e50595 verified
Raw History Blame Contribute Delete
18.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light" />
<meta name="description" content="Estimate the minimalism–maximalism stylistic position of a visual design." />
<title>Commitment over Compromise — Stylistic Commitment Research</title>
<style>
:root {
--navy: #193B63;
--navy-dark: #112C4B;
--ink: #202B3B;
--muted: #617085;
--line: #DDE4EB;
--background: #F8FAFC;
--white: #FFFFFF;
--blue: #9AB4CC;
--gold: #B99A63;
}
*, *::before, *::after { box-sizing: border-box; }
html { font-size: 16px; }
body {
margin: 0;
background: var(--background);
color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont,
"Segoe UI", Arial, sans-serif;
line-height: 1.55;
}
button, input { font: inherit; }
button:focus-visible, .dropzone:focus-visible {
outline: 3px solid #7AA8D0; outline-offset: 3px;
}
.container { width: min(100% - 40px, 1200px); margin: 0 auto; }
header { text-align: center; padding: 9px 0 11px; }
h1 {
max-width: 1060px;
margin: 0 auto 7px;
color: var(--navy-dark);
font-size: clamp(1.85rem, 2.55vw, 2.19rem);
line-height: 1.18;
letter-spacing: -.032em;
font-weight: 750;
}
.intro {
max-width: 920px;
margin: 6px auto 0;
color: #455366;
font-size: 1.02rem;
}
main { padding-bottom: 16px; }
.workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; align-items: stretch; }
.panel {
min-width: 0;
padding: 16px 19px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 14px;
}
h2 { margin: 0 0 9px; font-size: 1.16rem; color: var(--navy-dark); font-weight: 750; letter-spacing: -.013em; }
#upload-heading, #results-heading { font-size: 1.42rem; line-height: 1.3; }
.dropzone {
position: relative;
display: flex;
min-height: 219px;
align-items: center;
justify-content: center;
padding: 22px;
border: 1.5px dashed #AEBBC8;
border-radius: 10px;
background: #FCFDFE;
cursor: pointer;
overflow: hidden;
transition: border-color 120ms ease, background 120ms ease;
}
.dropzone:hover, .dropzone.dragging { border-color: var(--navy); background: #F1F6FA; }
.dropzone input { display: none; }
.drop-prompt { text-align: center; }
.upload-icon { color: var(--navy); display: block; margin: 0 auto 14px; }
.upload-title { margin: 0; color: var(--navy-dark); font-weight: 650; }
.upload-detail { margin: 5px 0 0; color: var(--muted); font-size: .9rem; }
.preview {
display: none;
max-width: 100%;
width: 100%;
height: 205px;
object-fit: contain;
}
.dropzone.has-image { background: #FFFFFF; }
.dropzone.has-image .drop-prompt { display: none; }
.dropzone.has-image .preview { display: block; }
.filename { min-height: 21px; margin: 7px 1px 9px; font-size: .81rem; color: var(--muted); overflow-wrap: anywhere; }
.primary {
display: block;
width: 100%;
padding: 10px 16px;
border: 1px solid var(--navy);
border-radius: 9px;
color: white;
background: var(--navy);
font-weight: 650;
cursor: pointer;
transition: background 120ms ease;
}
.primary:hover:not(:disabled) { background: var(--navy-dark); }
.primary:disabled { opacity: .48; cursor: not-allowed; }
.message { min-height: 18px; margin: 7px 0 0; color: var(--muted); font-size: .84rem; }
.message.error { color: #A43636; }
.empty-result {
display: flex;
align-items: center;
justify-content: center;
min-height: 235px;
border: 1px dashed var(--line);
border-radius: 10px;
text-align: center;
color: var(--muted);
padding: 28px;
}
/* Match the upload card's height; distribute result blocks vertically. */
.workspace > .panel:last-child { display: flex; flex-direction: column; }
.empty-result { flex: 1; }
.results {
display: none;
flex: 1;
flex-direction: column;
justify-content: space-between;
gap: 12px;
padding: 2px 0 7px;
}
.score-label { margin: 0 0 3px; font-size: .9rem; color: var(--muted); font-weight: 650; }
.score {
color: var(--navy);
font-size: clamp(2.08rem, 2.7vw, 2.35rem);
font-variant-numeric: tabular-nums;
font-weight: 760;
line-height: 1.17;
letter-spacing: -.04em;
}
.continuum { margin-top: 0; }
.continuum-label { font-size: .85rem; font-weight: 650; color: var(--navy-dark); margin-bottom: 11px; }
.scale {
position: relative;
height: 13px;
border: 1px solid #C7D2DC;
border-radius: 20px;
background: linear-gradient(to right, #8FAEC9 0%, #B9CBD8 29%, #DBE0E4 50%, #D9C49D 73%, #B99A63 100%);
}
.scale-center {
position: absolute;
left: 50%;
top: -4px;
height: 21px;
border-left: 1px solid rgba(44, 58, 70, .52);
}
.marker {
position: absolute;
top: -9px;
left: 50%;
width: 7px;
height: 29px;
transform: translateX(-50%);
background: var(--navy-dark);
border: 1px solid white;
border-radius: 4px;
box-shadow: 0 0 0 1px rgba(18, 45, 75, .38);
transition: left 180ms ease;
}
.scale-ticks {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
margin-top: 7px;
color: #445368;
font-size: .77rem;
font-weight: 650;
line-height: 1.38;
}
.scale-ticks span { min-width: 0; }
.scale-ticks span:nth-child(2) { text-align: center; }
.scale-ticks span:nth-child(3) { text-align: right; }
.scale-ticks small { display: block; font-size: .72rem; font-weight: 500; color: var(--muted); }
.scale-caption { margin: 7px 0 0; color: #67768A; font-size: .72rem; line-height: 1.4; }
.outside { display: none; color: #805D25; font-weight: 650; margin-top: 9px; font-size: .78rem; }
.divider { border: 0; border-top: 1px solid var(--line); margin: 0; width: 100%; }
.secondary-label { font-size: .86rem; color: var(--muted); font-weight: 650; }
.secondary-value { color: var(--navy-dark); font-size: 1.35rem; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 5px; }
.interpretation { margin: 8px 0 0; font-size: .85rem; line-height: 1.43; color: #526174; }
.model-info {
margin-top: 20px;
padding: 15px 20px 16px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 14px;
}
.model-info h2 { margin: 0 0 9px; }
.model-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; }
.model-info h3 { margin: 0 0 5px; color: var(--navy-dark); font-size: .98rem; }
.model-info p { margin: 0; color: #526174; font-size: .85rem; line-height: 1.45; }
@media (max-width: 790px) {
.container { width: min(100% - 30px, 1200px); }
header { padding: 30px 0 23px; }
.workspace { grid-template-columns: 1fr; gap: 15px; align-items: start; }
.results { gap: 16px; padding-bottom: 0; }
.workspace > .panel:last-child { min-height: 0; }
.panel { padding: 22px; }
.dropzone { min-height: 258px; }
.empty-result { min-height: 185px; }
.preview { height: 242px; }
.model-info { padding: 20px; }
.model-info-grid { grid-template-columns: 1fr; gap: 25px; }
.scale-ticks { gap: 5px; font-size: .7rem; }
.scale-ticks small { font-size: .68rem; }
}
/* On shorter desktop screens, prioritize keeping both explanatory sections visible.
Very small screens still scroll naturally rather than reducing type to unreadable sizes. */
@media (min-width: 791px) and (max-height: 780px) {
header { padding-top: 14px; padding-bottom: 15px; }
h1 { font-size: clamp(1.85rem, 2.6vw, 2.23rem); }
.intro { font-size: 1.02rem; margin-top: 7px; }
.dropzone { min-height: 195px; }
.preview { height: 182px; }
.empty-result { min-height: 225px; }
.model-info { margin-top: 18px; padding-top: 14px; padding-bottom: 16px; }
.model-info p { font-size: .85rem; line-height: 1.45; }
}
@media (prefers-reduced-motion: reduce) {
.marker, .dropzone, .primary { transition: none; }
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Commitment over Compromise: The Unique Aesthetic Rewards of Stylistic Commitment</h1>
<p class="intro">Upload a design image to estimate its position along the minimalism–maximalism stylistic continuum using a trained computer-vision model.</p>
</header>
<main>
<div class="workspace">
<section class="panel" aria-labelledby="upload-heading">
<h2 id="upload-heading">Upload a Design Image</h2>
<div id="dropzone" class="dropzone" tabindex="0" role="button" aria-label="Select or drop a design image">
<input id="image-file" type="file" accept="image/png,image/jpeg,image/webp,image/bmp,image/gif" />
<div class="drop-prompt">
<svg class="upload-icon" aria-hidden="true" width="39" height="39" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 16V3m0 0L7 8m5-5 5 5"/><path d="M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/>
</svg>
<p class="upload-title">Drag and drop an image here</p>
<p class="upload-detail">or click to browse</p>
</div>
<img id="image-preview" class="preview" alt="Selected design image preview" />
</div>
<p id="filename" class="filename">PNG, JPG, WebP, BMP, or GIF</p>
<button id="analyze-button" class="primary" type="button" disabled>Analyze Design</button>
<p id="status" class="message" role="status" aria-live="polite"></p>
</section>
<section class="panel" aria-labelledby="results-heading">
<h2 id="results-heading">Style Analysis</h2>
<div id="empty-result" class="empty-result"></div>
<div id="results" class="results" aria-live="polite">
<div class="primary-result">
<p class="score-label">Predicted Style Score</p>
<div id="style-score" class="score">—</div>
</div>
<hr class="divider" />
<div class="continuum">
<div class="continuum-label">Minimalism–Maximalism Continuum</div>
<div class="scale" role="img" aria-label="Stylistic continuum, minimalism on the left, moderate in the center, maximalism on the right">
<div class="scale-center" aria-hidden="true"></div>
<div id="marker" class="marker" aria-hidden="true"></div>
</div>
<div class="scale-ticks"><span>Stylistic Commitment<small>(Minimal)</small></span><span>Stylistically Compromised<small>(Moderate)</small></span><span>Stylistic Commitment<small>(Maximal)</small></span></div>
<p id="outside" class="outside">The prediction is beyond the visual window; its marker is shown at the boundary. The numerical score is unchanged.</p>
</div>
<hr class="divider" />
<div class="secondary-result">
<div class="secondary-label">Estimated Expert Rating (1–7)</div>
<div id="expert-rating" class="secondary-value">—</div>
</div>
</div>
</section>
</div>
<section class="model-info" aria-labelledby="model-heading">
<h2 id="model-heading">About the Model</h2>
<div class="model-info-grid">
<div>
<h3>Model Overview</h3>
<p>This tool accompanies the research manuscript <em>Commitment over Compromise: The Unique Aesthetic Rewards of Stylistic Commitment</em>, which examines how clearly expressing a design style, rather than occupying a stylistic middle ground, shapes aesthetic appeal. Using minimalism and maximalism as two distinct design styles, the tool employs a trained computer-vision model (ResNet-18) to estimate where a design falls along the minimalism–maximalism stylistic continuum.</p>
</div>
<div>
<h3>Interpreting the Style Score</h3>
<p>The predicted Style Score indicates a design’s position along the minimalism–maximalism stylistic continuum. Increasingly negative scores indicate stronger stylistic commitment to minimalism, whereas increasingly positive scores indicate stronger stylistic commitment to maximalism. Scores closer to zero reflect a more moderate position along the continuum, corresponding to stylistically compromised designs that do not clearly express either style. Because the scores are mean-centered, zero represents the mean of the expert ratings used to train the model rather than an absolute boundary between stylistic categories.</p>
</div>
</div>
</section>
</main>
</div>
<script type="module">
// The original Space hosts the trained model. This page changes only the interface.
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
const MODEL_SPACE = "ThisIsAnAnonymousUser/Minimal-Maximal.7.0";
const API_ENDPOINT = "/predict";
// This is a visualization window, not a bound on the model's possible outputs.
const DISPLAY_HALF_RANGE = 3.454;
const dropzone = document.getElementById("dropzone");
const input = document.getElementById("image-file");
const preview = document.getElementById("image-preview");
const filename = document.getElementById("filename");
const button = document.getElementById("analyze-button");
const status = document.getElementById("status");
const empty = document.getElementById("empty-result");
const results = document.getElementById("results");
const styleScore = document.getElementById("style-score");
const expertRating = document.getElementById("expert-rating");
const marker = document.getElementById("marker");
const outside = document.getElementById("outside");
let selectedFile = null;
let previewUrl = null;
let clientPromise = null;
let isAnalyzing = false;
function clearPrediction() {
results.style.display = "none";
empty.style.display = "flex";
styleScore.textContent = "—";
expertRating.textContent = "—";
}
function selectImage(file) {
if (!file || isAnalyzing) return;
const allowedTypes = new Set(["image/png", "image/jpeg", "image/webp", "image/bmp", "image/gif"]);
if (!allowedTypes.has(file.type)) {
status.textContent = "Please select a PNG, JPG, WebP, BMP, or GIF image.";
status.classList.add("error");
return;
}
selectedFile = file;
if (previewUrl) URL.revokeObjectURL(previewUrl);
previewUrl = URL.createObjectURL(file);
preview.src = previewUrl;
filename.textContent = file.name;
dropzone.classList.add("has-image");
status.textContent = "";
status.classList.remove("error");
clearPrediction();
button.disabled = isAnalyzing;
}
dropzone.addEventListener("click", (event) => {
// Ignore the click generated by input.click() so it does not bubble back.
if (event.target !== input && !isAnalyzing) input.click();
});
dropzone.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
if (!isAnalyzing) input.click();
}
});
input.addEventListener("change", () => selectImage(input.files?.[0]));
dropzone.addEventListener("dragover", (event) => { event.preventDefault(); dropzone.classList.add("dragging"); });
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("dragging"));
dropzone.addEventListener("drop", (event) => {
event.preventDefault();
dropzone.classList.remove("dragging");
selectImage(event.dataTransfer.files?.[0]);
});
button.addEventListener("click", async () => {
if (!selectedFile || isAnalyzing) return;
const submittedFile = selectedFile;
isAnalyzing = true;
button.disabled = true;
button.textContent = "Analyzing…";
status.textContent = "Connecting to the model and analyzing your image…";
status.classList.remove("error");
try {
if (!clientPromise) clientPromise = Client.connect(MODEL_SPACE);
const client = await clientPromise;
// The API expects an image blob in the parameter named "img".
const response = await client.predict(API_ENDPOINT, { img: submittedFile });
// From the documented /predict API:
// [0] estimated expert rating (1–7), [1] mean-centered style score,
// [2] original HTML visualization (unused by the redesigned UI).
const values = response.data;
if (!Array.isArray(values) || values.length < 2 || values[0] == null || values[1] == null) {
throw new Error("The model returned an unexpected response.");
}
const expert = Number(values[0]);
const centered = Number(values[1]);
if (!Number.isFinite(expert) || !Number.isFinite(centered)) {
throw new Error("The model returned a non-numeric prediction.");
}
// Do not change or recompute model outputs; round only the visible labels.
styleScore.textContent = centered.toFixed(2);
expertRating.textContent = expert.toFixed(2);
const pct = Math.max(0, Math.min(100, ((centered + DISPLAY_HALF_RANGE) / (2 * DISPLAY_HALF_RANGE)) * 100));
marker.style.left = `${pct}%`;
outside.style.display = Math.abs(centered) > DISPLAY_HALF_RANGE ? "block" : "none";
empty.style.display = "none";
results.style.display = "flex";
status.textContent = "Analysis complete.";
} catch (error) {
clientPromise = null; // allow a fresh connection on retry
status.textContent = "Prediction unavailable. Please retry; the model may be restarting or temporarily offline.";
status.classList.add("error");
console.error("Model prediction failed:", error);
} finally {
isAnalyzing = false;
button.disabled = !selectedFile;
button.textContent = "Analyze Design";
}
});
</script>
</body>
</html>