feat: Stage 4 & 5 - Profile Calculation (colprof) & Verification (profcheck) #14
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Mock script for colprof
|
||||
# Simulates colprof execution and outputs progress log
|
||||
|
||||
basename="$1"
|
||||
# Find last argument if -D or other flags are used
|
||||
for arg in "$@"; do
|
||||
basename="$arg"
|
||||
done
|
||||
|
||||
echo "colprof: Starting profile calculation for $basename"
|
||||
sleep 1
|
||||
echo "Gamut mapping calculation..."
|
||||
sleep 1
|
||||
echo "Fitting cLUT grid points..."
|
||||
sleep 1
|
||||
echo "Writing ICC profile $basename.icc..."
|
||||
touch "$basename.icc"
|
||||
echo "Done."
|
||||
exit 0
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Mock script for profcheck
|
||||
# Simulates profcheck verification output
|
||||
|
||||
echo "profcheck: Checking profile accuracy..."
|
||||
sleep 1
|
||||
cat << 'EOF'
|
||||
{"event": "profcheck_complete", "avg_de": 0.85, "max_de": 2.41, "rms_de": 1.02}
|
||||
EOF
|
||||
echo "Summary:"
|
||||
echo " avg. dE = 0.85"
|
||||
echo " max. dE = 2.41"
|
||||
echo " rms. dE = 1.02"
|
||||
exit 0
|
||||
@@ -204,6 +204,94 @@ pub async fn run_chartread(
|
||||
state.spawn(app, id, binary, args, cwd).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ColprofConfig {
|
||||
pub quality: String,
|
||||
pub algorithm: String,
|
||||
pub description: String,
|
||||
pub copyright: Option<String>,
|
||||
pub basename: String,
|
||||
pub cwd: String,
|
||||
}
|
||||
|
||||
pub fn build_colprof_args(config: &ColprofConfig) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"-v".to_string(),
|
||||
"-u".to_string(),
|
||||
"-q".to_string(),
|
||||
config.quality.clone(),
|
||||
"-a".to_string(),
|
||||
config.algorithm.clone(),
|
||||
"-D".to_string(),
|
||||
config.description.clone(),
|
||||
];
|
||||
|
||||
if let Some(copyright) = &config.copyright {
|
||||
if !copyright.trim().is_empty() {
|
||||
args.push("-C".to_string());
|
||||
args.push(copyright.clone());
|
||||
}
|
||||
}
|
||||
|
||||
args.push(config.basename.clone());
|
||||
args
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_colprof(
|
||||
app: AppHandle,
|
||||
state: State<'_, ProcessManager>,
|
||||
config: ColprofConfig,
|
||||
) -> Result<(), String> {
|
||||
let binary = resolve_binary(app.clone(), "colprof".to_string()).await?;
|
||||
let args = build_colprof_args(&config);
|
||||
let id = format!("colprof_{}", config.basename);
|
||||
|
||||
let cwd = if config.cwd.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(config.cwd.clone())
|
||||
};
|
||||
|
||||
state.spawn(app, id, binary, args, cwd).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ProfcheckConfig {
|
||||
pub ti3_path: String,
|
||||
pub icc_path: String,
|
||||
pub cwd: String,
|
||||
}
|
||||
|
||||
pub fn build_profcheck_args(config: &ProfcheckConfig) -> Vec<String> {
|
||||
vec![
|
||||
"-v".to_string(),
|
||||
"-k".to_string(),
|
||||
"-u".to_string(),
|
||||
config.ti3_path.clone(),
|
||||
config.icc_path.clone(),
|
||||
]
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn run_profcheck(
|
||||
app: AppHandle,
|
||||
state: State<'_, ProcessManager>,
|
||||
config: ProfcheckConfig,
|
||||
) -> Result<(), String> {
|
||||
let binary = resolve_binary(app.clone(), "profcheck".to_string()).await?;
|
||||
let args = build_profcheck_args(&config);
|
||||
let id = format!("profcheck_{}", config.ti3_path);
|
||||
|
||||
let cwd = if config.cwd.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(config.cwd.clone())
|
||||
};
|
||||
|
||||
state.spawn(app, id, binary, args, cwd).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -287,4 +375,32 @@ mod tests {
|
||||
let args = build_chartread_args(&config);
|
||||
assert_eq!(args, vec!["-v", "-u", "my_profile"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_colprof_args() {
|
||||
let config = ColprofConfig {
|
||||
quality: "h".to_string(),
|
||||
algorithm: "l".to_string(),
|
||||
description: "My Profile".to_string(),
|
||||
copyright: Some("2026 ACME".to_string()),
|
||||
basename: "my_profile".to_string(),
|
||||
cwd: "/home/user".to_string(),
|
||||
};
|
||||
let args = build_colprof_args(&config);
|
||||
assert_eq!(
|
||||
args,
|
||||
vec!["-v", "-u", "-q", "h", "-a", "l", "-D", "My Profile", "-C", "2026 ACME", "my_profile"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_profcheck_args() {
|
||||
let config = ProfcheckConfig {
|
||||
ti3_path: "my_profile.ti3".to_string(),
|
||||
icc_path: "my_profile.icc".to_string(),
|
||||
cwd: "/home/user".to_string(),
|
||||
};
|
||||
let args = build_profcheck_args(&config);
|
||||
assert_eq!(args, vec!["-v", "-k", "-u", "my_profile.ti3", "my_profile.icc"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ pub fn run() {
|
||||
commands::run_printtarg,
|
||||
commands::read_file_base64,
|
||||
commands::run_chartread,
|
||||
commands::run_colprof,
|
||||
commands::run_profcheck,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
+91
-5
@@ -218,15 +218,101 @@
|
||||
<!-- Stage 4: colprof -->
|
||||
<section id="stage-4" class="stage hidden">
|
||||
<h2>Create Profile</h2>
|
||||
<p>Calculate the ICC profile.</p>
|
||||
<button class="primary" id="btnProfile">Create Profile (.icc)</button>
|
||||
<p>Calculate the ICC profile from your target measurement data.</p>
|
||||
|
||||
<div class="form-container">
|
||||
<div class="form-group">
|
||||
<label>Profile Quality</label>
|
||||
<select id="colprofQuality">
|
||||
<option value="l">Low (Fast draft)</option>
|
||||
<option value="m" selected>Medium (Standard)</option>
|
||||
<option value="h">High (Recommended)</option>
|
||||
<option value="u">Ultra (Maximum precision - slow)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Profile Description</label>
|
||||
<input type="text" id="colprofDescription" placeholder="e.g. Epson P600 Lustre Photo">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Copyright / Creator (Optional)</label>
|
||||
<input type="text" id="colprofCopyright" placeholder="e.g. Copyright (c) 2026 My Studio">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Algorithm Type</label>
|
||||
<select id="colprofAlgorithm">
|
||||
<option value="l" selected>Lab cLUT (Standard Lookup Table)</option>
|
||||
<option value="x">XYZ cLUT</option>
|
||||
<option value="X">Display XYZ cLUT + Matrix</option>
|
||||
<option value="m">Matrix Only (Simple)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="primary" id="btnCreateProfile">Calculate Profile (.icc)</button>
|
||||
|
||||
<!-- Spinner and Progress Stage -->
|
||||
<div class="spinner-container hidden" id="colprofSpinnerContainer">
|
||||
<div class="spinner"></div>
|
||||
<div class="stage-label" id="colprofStageLabel">Calculating ICC Profile...</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Card -->
|
||||
<div class="success-card hidden" id="colprofSuccessCard">
|
||||
<h3>🎉 Profile Calculation Complete</h3>
|
||||
<p id="colprofSuccessInfo"></p>
|
||||
<button class="primary" id="btnGoToVerify">Proceed to Verification →</button>
|
||||
</div>
|
||||
|
||||
<!-- Process Log -->
|
||||
<div class="log-container hidden" id="colprofLogContainer">
|
||||
<h4>Process Output</h4>
|
||||
<pre id="colprofLog"></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stage 5: profcheck -->
|
||||
<section id="stage-5" class="stage hidden">
|
||||
<h2>Verify</h2>
|
||||
<p>Analyze the generated profile quality.</p>
|
||||
<button class="primary" id="btnVerify">Verify Profile</button>
|
||||
<h2>Verify Profile</h2>
|
||||
<p>Check the numerical accuracy of your profile against the original measurement data.</p>
|
||||
|
||||
<button class="primary" id="btnVerify">Run Profile Verification</button>
|
||||
|
||||
<!-- Report Card -->
|
||||
<div class="report-card hidden" id="profcheckReportCard">
|
||||
<div class="report-header">
|
||||
<h3>Profile Quality Report</h3>
|
||||
<div id="profcheckBadge" class="report-badge">GOOD</div>
|
||||
</div>
|
||||
|
||||
<div class="report-metrics">
|
||||
<div class="metric-box">
|
||||
<span class="metric-value" id="profcheckAvgDe">0.00</span>
|
||||
<span class="metric-label">Average ΔE₀₀</span>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span class="metric-value" id="profcheckMaxDe">0.00</span>
|
||||
<span class="metric-label">Peak ΔE₀₀</span>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span class="metric-value" id="profcheckRmsDe">0.00</span>
|
||||
<span class="metric-label">RMS ΔE₀₀</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="report-footnote">
|
||||
Professional printer profiles typically achieve average ΔE₀₀ < 1.0, and acceptable profiles < 2.0.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process Log -->
|
||||
<div class="log-container hidden" id="profcheckLogContainer">
|
||||
<h4>Verification Output</h4>
|
||||
<pre id="profcheckLog"></pre>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { initTargen } from './targen.js';
|
||||
import { initPrinttarg } from './printtarg.js';
|
||||
import { initChartread } from './chartread.js';
|
||||
import { initColprof } from './colprof.js';
|
||||
import { initProfcheck } from './profcheck.js';
|
||||
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
|
||||
@@ -35,4 +37,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Initialize Stage 3
|
||||
initChartread();
|
||||
|
||||
// Initialize Stage 4
|
||||
initColprof();
|
||||
|
||||
// Initialize Stage 5
|
||||
initProfcheck();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
import { startSwatchListener, stopSwatchListener } from './swatch_grid.js';
|
||||
import { setStage3Result } from './colprof.js';
|
||||
|
||||
// Module-level state: set by Stage 2 when it completes
|
||||
let stage2Basename = "";
|
||||
@@ -150,6 +151,7 @@ export function initChartread() {
|
||||
setState(STATE.FINISHED);
|
||||
setPrompt("✅ Measurement complete! .ti3 file has been saved.");
|
||||
logPre.textContent += "\n[SUCCESS] chartread completed. .ti3 file written.\n";
|
||||
setStage3Result(stage2Basename, stage2Cwd);
|
||||
advanceToStage4();
|
||||
} else {
|
||||
setState(STATE.FINISHED);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
import { setStage4Result } from './profcheck.js';
|
||||
|
||||
let chartreadBasename = "";
|
||||
let chartreadCwd = "";
|
||||
|
||||
/**
|
||||
* Called by chartread.js after Stage 3 completes.
|
||||
*/
|
||||
export function setStage3Result(basename, cwd) {
|
||||
chartreadBasename = basename;
|
||||
chartreadCwd = cwd;
|
||||
}
|
||||
|
||||
export function initColprof() {
|
||||
const qualitySelect = document.getElementById("colprofQuality");
|
||||
const algorithmSelect = document.getElementById("colprofAlgorithm");
|
||||
const descInput = document.getElementById("colprofDescription");
|
||||
const copyrightInput = document.getElementById("colprofCopyright");
|
||||
const btnCreateProfile = document.getElementById("btnCreateProfile");
|
||||
const spinnerContainer = document.getElementById("colprofSpinnerContainer");
|
||||
const stageLabel = document.getElementById("colprofStageLabel");
|
||||
const logContainer = document.getElementById("colprofLogContainer");
|
||||
const logPre = document.getElementById("colprofLog");
|
||||
const successCard = document.getElementById("colprofSuccessCard");
|
||||
const successInfo = document.getElementById("colprofSuccessInfo");
|
||||
const btnGoToVerify = document.getElementById("btnGoToVerify");
|
||||
|
||||
if (!btnCreateProfile) return;
|
||||
|
||||
btnCreateProfile.addEventListener("click", async () => {
|
||||
const basename = chartreadBasename || "test_target";
|
||||
const cwd = chartreadCwd || "";
|
||||
|
||||
const description = descInput.value.trim() || basename;
|
||||
|
||||
logPre.textContent = "";
|
||||
logContainer.classList.remove("hidden");
|
||||
spinnerContainer.classList.remove("hidden");
|
||||
successCard.classList.add("hidden");
|
||||
btnCreateProfile.disabled = true;
|
||||
stageLabel.textContent = "Initializing colprof...";
|
||||
|
||||
const config = {
|
||||
quality: qualitySelect.value,
|
||||
algorithm: algorithmSelect.value,
|
||||
description: description,
|
||||
copyright: copyrightInput.value.trim() || null,
|
||||
basename: basename,
|
||||
cwd: cwd,
|
||||
};
|
||||
|
||||
const processId = `colprof_${basename}`;
|
||||
|
||||
try {
|
||||
const unlistenStdout = await listen("process:stdout", (event) => {
|
||||
if (event.payload.id === processId && event.payload.line) {
|
||||
const line = event.payload.line;
|
||||
logPre.textContent += line + "\n";
|
||||
logPre.scrollTop = logPre.scrollHeight;
|
||||
|
||||
// Parse coarse progress stages from stdout
|
||||
const lineLower = line.toLowerCase();
|
||||
if (lineLower.includes("gamut mapping")) {
|
||||
stageLabel.textContent = "Gamut mapping calculation in progress...";
|
||||
} else if (lineLower.includes("fitting") || lineLower.includes("clut")) {
|
||||
stageLabel.textContent = "Fitting cLUT grid points...";
|
||||
} else if (lineLower.includes("writing") || lineLower.includes("icc profile")) {
|
||||
stageLabel.textContent = "Writing ICC profile header & tags...";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const unlistenStderr = await listen("process:stderr", (event) => {
|
||||
if (event.payload.id === processId && event.payload.line) {
|
||||
logPre.textContent += "ERR: " + event.payload.line + "\n";
|
||||
logPre.scrollTop = logPre.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
const unlistenExit = await listen("process:exit", (event) => {
|
||||
if (event.payload.id === processId) {
|
||||
unlistenStdout();
|
||||
unlistenStderr();
|
||||
unlistenExit();
|
||||
spinnerContainer.classList.add("hidden");
|
||||
btnCreateProfile.disabled = false;
|
||||
|
||||
if (event.payload.code === 0) {
|
||||
logPre.textContent += "\n[SUCCESS] colprof completed. Profile generated.\n";
|
||||
const iccFilename = `${basename}.icc`;
|
||||
successInfo.textContent = `Profile: ${iccFilename} (${description})`;
|
||||
successCard.classList.remove("hidden");
|
||||
|
||||
setStage4Result(basename, cwd);
|
||||
} else {
|
||||
logPre.textContent += `\n[ERROR] colprof exited with code ${event.payload.code}.\n`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await invoke("run_colprof", { config });
|
||||
} catch (err) {
|
||||
logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
|
||||
spinnerContainer.classList.add("hidden");
|
||||
btnCreateProfile.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (btnGoToVerify) {
|
||||
btnGoToVerify.addEventListener("click", () => {
|
||||
advanceToStage5();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function advanceToStage5() {
|
||||
const steps = document.querySelectorAll('.step');
|
||||
const stages = document.querySelectorAll('.stage');
|
||||
|
||||
steps.forEach(s => s.classList.remove('active'));
|
||||
if (steps[4]) steps[4].classList.add('active');
|
||||
|
||||
stages.forEach(s => {
|
||||
s.classList.remove('active');
|
||||
s.classList.add('hidden');
|
||||
});
|
||||
if (stages[4]) {
|
||||
stages[4].classList.remove('hidden');
|
||||
stages[4].classList.add('active');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const { listen } = window.__TAURI__.event;
|
||||
|
||||
let profileBasename = "";
|
||||
let profileCwd = "";
|
||||
|
||||
/**
|
||||
* Called by colprof.js after Stage 4 completes.
|
||||
*/
|
||||
export function setStage4Result(basename, cwd) {
|
||||
profileBasename = basename;
|
||||
profileCwd = cwd;
|
||||
}
|
||||
|
||||
export function initProfcheck() {
|
||||
const btnVerify = document.getElementById("btnVerify");
|
||||
const logContainer = document.getElementById("profcheckLogContainer");
|
||||
const logPre = document.getElementById("profcheckLog");
|
||||
const reportCard = document.getElementById("profcheckReportCard");
|
||||
const avgDeEl = document.getElementById("profcheckAvgDe");
|
||||
const maxDeEl = document.getElementById("profcheckMaxDe");
|
||||
const rmsDeEl = document.getElementById("profcheckRmsDe");
|
||||
const badgeEl = document.getElementById("profcheckBadge");
|
||||
|
||||
if (!btnVerify) return;
|
||||
|
||||
btnVerify.addEventListener("click", async () => {
|
||||
const basename = profileBasename || "test_target";
|
||||
const cwd = profileCwd || "";
|
||||
|
||||
const sep = cwd.includes('\\') ? '\\' : '/';
|
||||
const ti3Path = cwd ? `${cwd}${sep}${basename}.ti3` : `${basename}.ti3`;
|
||||
const iccPath = cwd ? `${cwd}${sep}${basename}.icc` : `${basename}.icc`;
|
||||
|
||||
logPre.textContent = "";
|
||||
logContainer.classList.remove("hidden");
|
||||
reportCard.classList.add("hidden");
|
||||
btnVerify.disabled = true;
|
||||
|
||||
const config = {
|
||||
ti3_path: ti3Path,
|
||||
icc_path: iccPath,
|
||||
cwd: cwd,
|
||||
};
|
||||
|
||||
const processId = `profcheck_${ti3Path}`;
|
||||
let stdoutAccumulator = "";
|
||||
|
||||
try {
|
||||
const unlistenStdout = await listen("process:stdout", (event) => {
|
||||
if (event.payload.id === processId && event.payload.line) {
|
||||
stdoutAccumulator += event.payload.line + "\n";
|
||||
logPre.textContent += event.payload.line + "\n";
|
||||
logPre.scrollTop = logPre.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
const unlistenStderr = await listen("process:stderr", (event) => {
|
||||
if (event.payload.id === processId && event.payload.line) {
|
||||
logPre.textContent += "ERR: " + event.payload.line + "\n";
|
||||
logPre.scrollTop = logPre.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
const unlistenExit = await listen("process:exit", (event) => {
|
||||
if (event.payload.id === processId) {
|
||||
unlistenStdout();
|
||||
unlistenStderr();
|
||||
unlistenExit();
|
||||
btnVerify.disabled = false;
|
||||
|
||||
if (event.payload.code === 0) {
|
||||
logPre.textContent += "\n[SUCCESS] profcheck verification finished.\n";
|
||||
parseAndRenderReport(stdoutAccumulator);
|
||||
} else {
|
||||
logPre.textContent += `\n[ERROR] profcheck exited with code ${event.payload.code}.\n`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await invoke("run_profcheck", { config });
|
||||
} catch (err) {
|
||||
logPre.textContent += `\n[INVOKE ERROR] ${err}\n`;
|
||||
btnVerify.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function parseAndRenderReport(stdout) {
|
||||
reportCard.classList.remove("hidden");
|
||||
|
||||
let avgDe = 0.0;
|
||||
let maxDe = 0.0;
|
||||
let rmsDe = 0.0;
|
||||
|
||||
// Check if JSON output is present
|
||||
const jsonMatch = stdout.match(/\{[\s\S]*"avg_de"[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
try {
|
||||
const json = JSON.parse(jsonMatch[0]);
|
||||
avgDe = json.avg_de || 0;
|
||||
maxDe = json.max_de || json.peak_de || 0;
|
||||
rmsDe = json.rms_de || 0;
|
||||
} catch (e) {
|
||||
console.error("JSON parse error:", e);
|
||||
}
|
||||
} else {
|
||||
// Regex fallbacks for standard profcheck output
|
||||
const avgMatch = stdout.match(/avg\.\s*dE\s*=\s*([\d\.]+)/i) || stdout.match(/average\s*dE\s*:\s*([\d\.]+)/i);
|
||||
const maxMatch = stdout.match(/max\.\s*dE\s*=\s*([\d\.]+)/i) || stdout.match(/peak\s*dE\s*:\s*([\d\.]+)/i);
|
||||
const rmsMatch = stdout.match(/rms\.\s*dE\s*=\s*([\d\.]+)/i) || stdout.match(/rms\s*dE\s*:\s*([\d\.]+)/i);
|
||||
|
||||
if (avgMatch) avgDe = parseFloat(avgMatch[1]);
|
||||
if (maxMatch) maxDe = parseFloat(maxMatch[1]);
|
||||
if (rmsMatch) rmsDe = parseFloat(rmsMatch[1]);
|
||||
}
|
||||
|
||||
avgDeEl.textContent = avgDe.toFixed(2);
|
||||
maxDeEl.textContent = maxDe.toFixed(2);
|
||||
rmsDeEl.textContent = rmsDe.toFixed(2);
|
||||
|
||||
// Quality verdict
|
||||
badgeEl.className = "report-badge";
|
||||
if (avgDe < 1.0) {
|
||||
badgeEl.textContent = "EXCELLENT";
|
||||
badgeEl.classList.add("badge-excellent");
|
||||
} else if (avgDe < 2.0) {
|
||||
badgeEl.textContent = "GOOD";
|
||||
badgeEl.classList.add("badge-good");
|
||||
} else if (avgDe < 4.0) {
|
||||
badgeEl.textContent = "ACCEPTABLE";
|
||||
badgeEl.classList.add("badge-acceptable");
|
||||
} else {
|
||||
badgeEl.textContent = "POOR";
|
||||
badgeEl.classList.add("badge-poor");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -499,3 +499,146 @@ button.danger:hover {
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Stage 4: Spinner & Success Card
|
||||
======================================== */
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: var(--panel-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: var(--accent-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.stage-label {
|
||||
font-size: 1rem;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.success-card {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border: 1px solid #4caf50;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.success-card h3 {
|
||||
color: #4caf50;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.success-card p {
|
||||
color: #ccc;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Stage 5: Report Card
|
||||
======================================== */
|
||||
.report-card {
|
||||
background: var(--panel-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.report-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.report-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.report-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.badge-excellent {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #4caf50;
|
||||
border: 1px solid #4caf50;
|
||||
}
|
||||
|
||||
.badge-good {
|
||||
background: rgba(33, 150, 243, 0.2);
|
||||
color: #2196f3;
|
||||
border: 1px solid #2196f3;
|
||||
}
|
||||
|
||||
.badge-acceptable {
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ff9800;
|
||||
border: 1px solid #ff9800;
|
||||
}
|
||||
|
||||
.badge-poor {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
color: #f44336;
|
||||
border: 1px solid #f44336;
|
||||
}
|
||||
|
||||
.report-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric-box {
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
display: block;
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.report-footnote {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
border-top: 1px dashed var(--border-color);
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user