gpt-5.4 @ xhigh · api
apixhigh
- config
- gpt-5.4-eff-xhigh--api--official--dev
- batch
- 2026-07-19--unified
- transport
- openai-official-api
- protocol
- api · metered
- served model
- gpt-5.4-2026-03-05
- 采集于
- 2026年7月18日 11:54 · N=4
variant
live
slot 0 · P-q · sandboxed iframe

截图是 similarity 通道测量时的原始像素。运行 live 会在 sandboxed iframe 中重新执行原始输出;渲染结果取决于你的浏览器和字体,不保证与测量像素一致。
similarity 邻居
最近
claude-opus-4-8 @medium · kiro0.70gpt-5.4 @none · api0.69claude-fable-5 @xhigh · CC0.67claude-opus-4-7 @low · kiro0.67gpt-5.6-luna @high · codex-oauth0.67最远
claude-haiku-4-5 @xhigh · CC0.44minimax-m2.7 · go0.45gpt-5.6-luna @medium · codex-oauth0.45gpt-5.6-luna @high · kiro0.45gpt-5.5 @medium · codex-oauth0.45限于 session 2026-07-19--unified
similarity 衡量本 session 内的趋同程度 —— 不是质量。
Fidelity —— 卡片展示的数据对不对?
- namematch
- conditionmatch
- datematch
- max tempmatch展示值:25.3
- min tempmatch展示值:16
- hourlyambiguouscoverage · 0 命中 · 0 不符 · 24 未展示
- request sidematch0 处违规
extractor fidelity-extractor-v1+Berlin-2026-07-15
源码 —— 原始模型输出,一字节未改
下载 card.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather Card</title>
<style>
:root {
color-scheme: dark;
--bg1:#09162f;
--bg2:#1d67c2;
--glow1:255,197,92;
--glow2:119,209,255;
--accent-rgb:255,209,102;
--accent2-rgb:110,193,255;
--line-rgb:150,215,255;
--text-rgb:245,247,251;
}
*{box-sizing:border-box}
html,body{width:100%;height:100%;margin:0}
body{
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
color: rgb(var(--text-rgb));
background:
radial-gradient(1000px 700px at 14% 14%, rgba(var(--glow1), .30), transparent 60%),
radial-gradient(900px 620px at 86% 18%, rgba(var(--glow2), .28), transparent 58%),
linear-gradient(135deg, var(--bg1), var(--bg2));
display:flex;
align-items:center;
justify-content:center;
overflow:hidden;
}
body::before{
content:"";
position:fixed;
inset:-10%;
background:
radial-gradient(circle at 20% 30%, rgba(255,255,255,.06) 0 1px, transparent 1.2px) 0 0/18px 18px,
radial-gradient(circle at 70% 65%, rgba(255,255,255,.04) 0 1px, transparent 1.2px) 9px 9px/24px 24px;
opacity:.18;
mix-blend-mode:soft-light;
pointer-events:none;
}
body::after{
content:"";
position:fixed;
inset:0;
background:radial-gradient(circle at center, transparent 48%, rgba(0,0,0,.32) 100%);
pointer-events:none;
}
.frame{
width:100%;
height:100%;
padding:40px;
display:flex;
align-items:center;
justify-content:center;
position:relative;
}
.orb{
position:absolute;
border-radius:50%;
filter:blur(72px);
opacity:.35;
pointer-events:none;
animation:drift 18s ease-in-out infinite alternate;
}
.orb.one{width:260px;height:260px;left:9%;top:10%;background:rgba(var(--glow1),.45)}
.orb.two{width:320px;height:320px;right:10%;bottom:12%;background:rgba(var(--glow2),.35);animation-duration:22s}
.card{
position:relative;
width:min(1140px, calc(100vw - 80px));
height:min(680px, calc(100vh - 80px));
border-radius:32px;
background:
linear-gradient(180deg, rgba(255,255,255,.16), rgba(255,255,255,.06)),
linear-gradient(135deg, rgba(12,20,34,.66), rgba(11,19,31,.42));
border:1px solid rgba(255,255,255,.16);
box-shadow:
0 40px 100px rgba(0,0,0,.36),
inset 0 1px 0 rgba(255,255,255,.14),
inset 0 -1px 0 rgba(255,255,255,.05);
overflow:hidden;
backdrop-filter:blur(24px) saturate(130%);
}
.card::before{
content:"";
position:absolute;
inset:0;
background:
radial-gradient(520px 260px at 84% 10%, rgba(var(--glow2), .18), transparent 60%),
radial-gradient(420px 240px at 12% 12%, rgba(var(--glow1), .15), transparent 62%);
pointer-events:none;
}
.card::after{
content:"";
position:absolute;
inset:0;
border-radius:32px;
box-shadow:inset 0 0 0 1px rgba(255,255,255,.04);
pointer-events:none;
}
.status{
position:absolute;
inset:0;
z-index:3;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:16px;
padding:40px;
text-align:center;
}
.hidden{display:none!important}
.spinner{
width:54px;
height:54px;
border-radius:50%;
border:3px solid rgba(255,255,255,.16);
border-top-color:rgba(var(--accent2-rgb), .95);
animation:spin .9s linear infinite;
box-shadow:0 0 0 6px rgba(255,255,255,.02);
}
.status-title{
font-size:30px;
line-height:1.05;
font-weight:780;
letter-spacing:-0.04em;
}
.status-sub{
max-width:480px;
font-size:15px;
line-height:1.6;
color:rgba(245,247,251,.72);
}
.status.error .spinner{display:none}
.content{
position:relative;
z-index:1;
height:100%;
padding:34px;
display:grid;
grid-template-rows:286px 1fr;
gap:26px;
}
.top{
display:grid;
grid-template-columns:1.08fr 0.92fr;
gap:24px;
}
.panel{
border-radius:28px;
background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.04));
border:1px solid rgba(255,255,255,.12);
box-shadow:
inset 0 1px 0 rgba(255,255,255,.08),
0 14px 36px rgba(0,0,0,.08);
}
.summary{
padding:30px 30px 28px;
display:flex;
flex-direction:column;
}
.eyebrow{
font-size:11px;
font-weight:800;
letter-spacing:.18em;
text-transform:uppercase;
color:rgba(245,247,251,.58);
}
.location{
margin:14px 0 0;
font-size:clamp(40px, 4.2vw, 56px);
line-height:.92;
letter-spacing:-0.06em;
font-weight:820;
max-width:520px;
overflow-wrap:anywhere;
}
.date-line{
margin-top:18px;
display:flex;
align-items:center;
gap:10px;
flex-wrap:wrap;
}
.pill{
padding:10px 14px;
border-radius:999px;
background:rgba(255,255,255,.08);
border:1px solid rgba(255,255,255,.12);
font-size:14px;
color:rgba(245,247,251,.92);
white-space:nowrap;
}
.pill.subtle{
color:rgba(245,247,251,.68);
}
.condition-block{
margin-top:30px;
}
.label{
font-size:12px;
font-weight:800;
letter-spacing:.16em;
text-transform:uppercase;
color:rgba(245,247,251,.56);
}
.condition{
margin-top:12px;
font-size:40px;
line-height:1.03;
letter-spacing:-0.04em;
font-weight:780;
max-width:450px;
}
.summary-copy{
margin-top:14px;
max-width:480px;
font-size:16px;
line-height:1.65;
color:rgba(245,247,251,.72);
}
.meta-row{
margin-top:auto;
display:flex;
gap:12px;
flex-wrap:wrap;
}
.meta-chip{
padding:11px 14px;
border-radius:16px;
background:rgba(255,255,255,.06);
border:1px solid rgba(255,255,255,.10);
box-shadow:inset 0 1px 0 rgba(255,255,255,.04);
font-size:13px;
color:rgba(245,247,251,.74);
}
.details{
padding:22px;
display:grid;
grid-template-rows:1fr 118px;
gap:18px;
}
.hero-visual{
position:relative;
overflow:hidden;
border-radius:24px;
background:
linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.02)),
radial-gradient(circle at 50% 30%, rgba(var(--glow1), .12), transparent 55%);
border:1px solid rgba(255,255,255,.12);
display:flex;
align-items:center;
justify-content:center;
min-height:0;
}
.hero-visual::before{
content:"";
position:absolute;
width:320px;
height:320px;
border-radius:50%;
background:
radial-gradient(circle, rgba(var(--glow1), .42), rgba(var(--glow2), .14) 52%, transparent 70%);
filter:blur(18px);
}
.hero-badges{
position:absolute;
top:16px;
left:16px;
right:16px;
display:flex;
align-items:center;
justify-content:space-between;
gap:10px;
z-index:2;
}
.hero-chip{
padding:9px 12px;
border-radius:999px;
background:rgba(7,12,22,.22);
border:1px solid rgba(255,255,255,.12);
box-shadow:inset 0 1px 0 rgba(255,255,255,.05);
font-size:12px;
font-weight:700;
color:rgba(245,247,251,.82);
backdrop-filter:blur(8px);
white-space:nowrap;
}
.hero-range{
position:absolute;
left:18px;
bottom:18px;
padding:12px 14px;
border-radius:18px;
background:rgba(7,12,22,.26);
border:1px solid rgba(255,255,255,.12);
box-shadow:inset 0 1px 0 rgba(255,255,255,.05);
backdrop-filter:blur(10px);
}
.hero-range .small{
font-size:11px;
font-weight:800;
letter-spacing:.16em;
text-transform:uppercase;
color:rgba(245,247,251,.56);
}
.hero-range .big{
margin-top:6px;
font-size:30px;
line-height:1;
font-weight:820;
letter-spacing:-0.05em;
}
.metric-grid{
display:grid;
grid-template-columns:1fr 1fr;
gap:18px;
}
.temp-card{
padding:18px 20px;
border-radius:22px;
background:rgba(255,255,255,.07);
border:1px solid rgba(255,255,255,.12);
display:flex;
flex-direction:column;
justify-content:space-between;
box-shadow:inset 0 1px 0 rgba(255,255,255,.06);
}
.temp-card .value{
margin-top:10px;
font-size:54px;
line-height:.95;
font-weight:830;
letter-spacing:-0.06em;
}
.temp-card.max .value{color:rgba(var(--accent-rgb), .98)}
.temp-card.min .value{color:rgba(var(--accent2-rgb), .96)}
.bottom{
padding:24px 26px 22px;
display:grid;
grid-template-rows:auto 1fr auto;
gap:18px;
}
.section-head{
display:flex;
align-items:flex-end;
justify-content:space-between;
gap:18px;
}
.section-title{
margin:0;
font-size:24px;
line-height:1;
font-weight:780;
letter-spacing:-0.04em;
}
.section-sub{
font-size:14px;
color:rgba(245,247,251,.66);
white-space:nowrap;
}
.chart-wrap{
position:relative;
border-radius:24px;
overflow:hidden;
background:
linear-gradient(180deg, rgba(5,8,14,.18), rgba(255,255,255,.03)),
radial-gradient(circle at 50% 0%, rgba(var(--line-rgb), .08), transparent 45%);
border:1px solid rgba(255,255,255,.10);
}
.chart-wrap::after{
content:"";
position:absolute;
left:0; right:0; bottom:0;
height:88px;
background:linear-gradient(180deg, transparent, rgba(8,12,20,.22));
pointer-events:none;
}
#chart{
width:100%;
height:232px;
display:block;
}
.chart-empty{
position:absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
font-size:15px;
color:rgba(245,247,251,.64);
}
.hourly-strip{
display:grid;
gap:8px;
}
.hour-cell{
position:relative;
overflow:hidden;
padding:11px 4px 12px;
border-radius:16px;
text-align:center;
background:linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.03));
border:1px solid rgba(255,255,255,.08);
box-shadow:inset 0 1px 0 rgba(255,255,255,.04);
}
.hour-cell::before{
content:"";
position:absolute;
inset:0;
background:linear-gradient(180deg, rgba(var(--cell-rgb, 255,255,255), var(--cell-alpha, .08)), rgba(255,255,255,.01));
opacity:.9;
pointer-events:none;
}
.hour-cell > *{position:relative;z-index:1}
.hour-cell.warm{outline:1px solid rgba(var(--accent-rgb), .42)}
.hour-cell.cold{outline:1px solid rgba(var(--accent2-rgb), .38)}
.hour-time{
font-size:12px;
font-weight:760;
color:rgba(245,247,251,.58);
letter-spacing:.02em;
}
.hour-temp{
margin-top:8px;
font-size:18px;
line-height:1;
font-weight:790;
letter-spacing:-0.04em;
}
.hour-marker{
margin:10px auto 0;
width:8px;
height:8px;
border-radius:50%;
background:rgba(255,255,255,.85);
box-shadow:0 0 0 4px rgba(255,255,255,.08);
}
.weather-icon{
position:relative;
z-index:1;
width:186px;
height:186px;
filter:drop-shadow(0 16px 34px rgba(0,0,0,.18));
}
.weather-icon .sun-ray{
stroke:#ffd66b;
stroke-width:8;
stroke-linecap:round;
}
.weather-icon .cloud-fill{fill:#f7fbff}
.weather-icon .cloud-shadow{fill:rgba(196,210,228,.68)}
.weather-icon .raindrop{
stroke:#8ed1ff;
stroke-width:9;
stroke-linecap:round;
}
.weather-icon .snowflake{
stroke:#e7f7ff;
stroke-width:6.5;
stroke-linecap:round;
}
.weather-icon .bolt{fill:#ffe680}
.weather-icon .fog-line{
stroke:#eef4fb;
stroke-width:8;
stroke-linecap:round;
opacity:.95;
}
.weather-icon .sun-group{
transform-origin:100px 100px;
animation:spinSlow 24s linear infinite;
}
.weather-icon .cloud-group{animation:floatSoft 4.8s ease-in-out infinite alternate}
.weather-icon .drop-group{animation:bob 2.6s ease-in-out infinite alternate}
.weather-icon .bolt{
animation:flicker 2.8s ease-in-out infinite;
transform-origin:100px 128px;
}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes drift{
from{transform:translate3d(0,0,0) scale(1)}
to{transform:translate3d(18px,-16px,0) scale(1.08)}
}
@keyframes spinSlow{to{transform:rotate(360deg)}}
@keyframes floatSoft{
from{transform:translateY(-2px)}
to{transform:translateY(4px)}
}
@keyframes bob{
from{transform:translateY(0)}
to{transform:translateY(6px)}
}
@keyframes flicker{
0%,68%,100%{transform:scale(1);opacity:1}
72%{transform:scale(1.03);opacity:.92}
76%{transform:scale(.98);opacity:.86}
}
@media (max-width:1080px){
.content{grid-template-rows:auto auto}
.top{grid-template-columns:1fr}
.details{grid-template-rows:260px 118px}
#chart{height:210px}
}
</style>
</head>
<body>
<div class="frame">
<div class="orb one"></div>
<div class="orb two"></div>
<main class="card" role="main" aria-label="Weather card">
<div id="status" class="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<div class="status-title">Loading weather…</div>
<div class="status-sub" id="statusSub">Preparing the daily summary and hourly temperatures.</div>
</div>
<div id="content" class="content hidden">
<div class="top">
<section class="panel summary">
<div>
<div class="eyebrow">Weather card</div>
<h1 class="location" id="locationName">Berlin</h1>
<div class="date-line">
<div class="pill" id="prettyDate">—</div>
<div class="pill subtle" id="rawDate">—</div>
<div class="pill subtle">UTC</div>
</div>
<div class="condition-block">
<div class="label">Condition</div>
<div class="condition" id="conditionText">—</div>
<div class="summary-copy" id="summaryCopy">Daily highs, lows, and the full hourly temperature curve for the selected day.</div>
</div>
</div>
<div class="meta-row">
<div class="meta-chip" id="coordChip">Coords 52.520, 13.405</div>
<div class="meta-chip" id="modeChip">Forecast data</div>
</div>
</section>
<section class="panel details">
<div class="hero-visual">
<div class="hero-badges">
<div class="hero-chip" id="codeChip">WMO 0</div>
<div class="hero-chip">UTC day</div>
</div>
<div id="heroIcon" aria-hidden="true"></div>
<div class="hero-range">
<div class="small">Daily span</div>
<div class="big" id="rangeValue">—</div>
</div>
</div>
<div class="metric-grid">
<div class="temp-card max">
<div class="label">Maximum</div>
<div class="value" id="tempMax">—</div>
</div>
<div class="temp-card min">
<div class="label">Minimum</div>
<div class="value" id="tempMin">—</div>
</div>
</div>
</section>
</div>
<section class="panel bottom">
<div class="section-head">
<h2 class="section-title">Hourly temperatures</h2>
<div class="section-sub" id="hourlyMeta">—</div>
</div>
<div class="chart-wrap">
<svg id="chart" viewBox="0 0 1000 232" preserveAspectRatio="none" aria-label="Hourly temperature chart"></svg>
<div id="chartEmpty" class="chart-empty hidden">Hourly detail unavailable for this date.</div>
</div>
<div class="hourly-strip" id="hourlyStrip"></div>
</section>
</div>
</main>
</div>
<script>
(() => {
const DEFAULTS = {
lat: 52.52,
lon: 13.405,
name: 'Berlin',
date: todayUTCString()
};
const THEMES = {
clear: {
bg1: '#09162f',
bg2: '#1d67c2',
glow1: '255,197,92',
glow2: '119,209,255',
accent: '255,209,102',
accent2: '110,193,255',
line: '150,215,255'
},
partly: {
bg1: '#0b1730',
bg2: '#3567b7',
glow1: '255,209,122',
glow2: '158,220,255',
accent: '255,215,126',
accent2: '136,207,255',
line: '165,223,255'
},
cloudy: {
bg1: '#121927',
bg2: '#48576d',
glow1: '215,223,234',
glow2: '166,181,203',
accent: '225,235,247',
accent2: '166,191,220',
line: '189,211,233'
},
fog: {
bg1: '#1a1f29',
bg2: '#5f6979',
glow1: '230,236,244',
glow2: '185,192,202',
accent: '228,236,246',
accent2: '188,202,219',
line: '210,220,231'
},
rain: {
bg1: '#071321',
bg2: '#214f7a',
glow1: '104,187,255',
glow2: '55,116,182',
accent: '120,203,255',
accent2: '173,224,255',
line: '136,206,255'
},
snow: {
bg1: '#091827',
bg2: '#5483aa',
glow1: '233,247,255',
glow2: '162,212,255',
accent: '239,249,255',
accent2: '173,224,255',
line: '199,236,255'
},
storm: {
bg1: '#140c27',
bg2: '#35205e',
glow1: '187,142,255',
glow2: '255,221,94',
accent: '199,167,255',
accent2: '255,226,123',
line: '189,190,255'
}
};
const status = document.getElementById('status');
const statusSub = document.getElementById('statusSub');
const content = document.getElementById('content');
const chartEmpty = document.getElementById('chartEmpty');
const el = {
locationName: document.getElementById('locationName'),
prettyDate: document.getElementById('prettyDate'),
rawDate: document.getElementById('rawDate'),
conditionText: document.getElementById('conditionText'),
summaryCopy: document.getElementById('summaryCopy'),
coordChip: document.getElementById('coordChip'),
modeChip: document.getElementById('modeChip'),
codeChip: document.getElementById('codeChip'),
heroIcon: document.getElementById('heroIcon'),
rangeValue: document.getElementById('rangeValue'),
tempMax: document.getElementById('tempMax'),
tempMin: document.getElementById('tempMin'),
hourlyMeta: document.getElementById('hourlyMeta'),
chart: document.getElementById('chart'),
hourlyStrip: document.getElementById('hourlyStrip')
};
init().catch((err) => {
console.error(err);
showError("Weather data couldn’t be loaded.", "Please verify the location parameters and try again.");
});
async function init() {
const params = readParams();
document.title = `${params.name} — Weather for ${params.date}`;
statusSub.textContent = `Fetching ${params.name} for ${params.date} (UTC).`;
const today = todayUTCString();
const isPast = params.date < today;
const endpoint = isPast ? '/api/om/archive' : '/api/om/forecast';
const query = new URLSearchParams({
latitude: String(params.lat),
longitude: String(params.lon),
daily: 'temperature_2m_max,temperature_2m_min,weather_code',
hourly: 'temperature_2m',
timezone: 'UTC',
temperature_unit: 'celsius',
start_date: params.date,
end_date: params.date
});
const url = `${endpoint}?${query.toString()}`;
const response = await fetchWithTimeout(url, 12000);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
const parsed = normalizeWeatherData(data, params.date);
const weather = describeWeatherCode(parsed.weatherCode);
applyTheme(weather.theme);
render({
params,
endpoint,
weather,
max: parsed.max,
min: parsed.min,
weatherCode: parsed.weatherCode,
hourly: parsed.hourly
});
}
function readParams() {
const sp = new URLSearchParams(window.location.search);
const lat = toFiniteNumber(sp.get('lat'), DEFAULTS.lat);
const lon = toFiniteNumber(sp.get('lon'), DEFAULTS.lon);
const nameRaw = (sp.get('name') || DEFAULTS.name).trim();
let date = (sp.get('date') || DEFAULTS.date).trim();
if (!isValidISODate(date)) date = DEFAULTS.date;
return {
lat,
lon,
name: nameRaw || DEFAULTS.name,
date
};
}
function normalizeWeatherData(data, date) {
const daily = data && data.daily;
const hourlyBlock = data && data.hourly;
const max = firstFinite(daily && daily.temperature_2m_max);
const min = firstFinite(daily && daily.temperature_2m_min);
const weatherCode = firstFinite(daily && daily.weather_code);
if (![max, min, weatherCode].every(Number.isFinite)) {
throw new Error('Missing daily values');
}
const times = Array.isArray(hourlyBlock && hourlyBlock.time) ? hourlyBlock.time : [];
const temps = Array.isArray(hourlyBlock && hourlyBlock.temperature_2m) ? hourlyBlock.temperature_2m : [];
const hourly = [];
for (let i = 0; i < Math.min(times.length, temps.length); i++) {
const stamp = String(times[i] || '');
const tempNum = Number(temps[i]);
if (!stamp.startsWith(date) || !Number.isFinite(tempNum)) continue;
hourly.push({
time: stamp,
hour: stamp.slice(11, 13),
temp: tempNum
});
}
return { max, min, weatherCode, hourly };
}
function render(state) {
const { params, endpoint, weather, max, min, weatherCode, hourly } = state;
const isArchive = endpoint.includes('/archive');
const modeLabel = isArchive ? 'Historical snapshot' : 'Forecast snapshot';
const hoursLabel = hourly.length === 24
? '24 hours'
: `${hourly.length} hourly ${hourly.length === 1 ? 'point' : 'points'}`;
el.locationName.textContent = params.name;
el.prettyDate.textContent = formatDisplayDate(params.date);
el.rawDate.textContent = params.date;
el.conditionText.textContent = weather.label;
el.summaryCopy.textContent = `${modeLabel} for the selected UTC day: high ${formatTemp(max, true)}, low ${formatTemp(min, true)}. Hourly temperatures appear below.`;
el.coordChip.textContent = `Coords ${formatCoord(params.lat)}, ${formatCoord(params.lon)}`;
el.modeChip.textContent = isArchive ? 'Archive data' : 'Forecast data';
el.codeChip.textContent = `WMO ${weatherCode}`;
el.rangeValue.textContent = formatTemp(Math.abs(max - min), true);
el.tempMax.textContent = formatTemp(max, true);
el.tempMin.textContent = formatTemp(min, true);
el.hourlyMeta.textContent = `${hoursLabel} • UTC • °C`;
el.heroIcon.innerHTML = iconForWeather(weather.icon);
renderChart(hourly);
renderHourlyStrip(hourly);
status.classList.add('hidden');
content.classList.remove('hidden');
}
function showError(title, sub) {
applyTheme('fog');
status.className = 'status error';
content.classList.add('hidden');
status.innerHTML = `
<div class="status-title">${escapeHtml(title)}</div>
<div class="status-sub">${escapeHtml(sub)}</div>
`;
}
function renderChart(hourly) {
const svg = el.chart;
svg.innerHTML = '';
chartEmpty.classList.toggle('hidden', hourly.length > 0);
if (!hourly.length) return;
const W = 1000;
const H = 232;
const m = { top: 22, right: 28, bottom: 28, left: 56 };
const plotW = W - m.left - m.right;
const plotH = H - m.top - m.bottom;
const temps = hourly.map((d) => d.temp);
const minT = Math.min(...temps);
const maxT = Math.max(...temps);
const span = maxT - minT;
const pad = Math.max(2, span * 0.2 || 2);
let domainMin = minT - pad;
let domainMax = maxT + pad;
if (domainMax === domainMin) {
domainMax += 2;
domainMin -= 2;
}
const points = hourly.map((d, i) => {
const x = m.left + (plotW * i) / Math.max(hourly.length - 1, 1);
const y = m.top + ((domainMax - d.temp) / (domainMax - domainMin)) * plotH;
return { x, y, temp: d.temp, hour: d.hour };
});
const baseline = H - m.bottom + 2;
const linePath = smoothSvgPath(points);
const areaPath = `${linePath} L ${points[points.length - 1].x.toFixed(2)} ${baseline.toFixed(2)} L ${points[0].x.toFixed(2)} ${baseline.toFixed(2)} Z`;
const defs = `
<defs>
<linearGradient id="areaGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="${rgbaCssVar('--line-rgb', 0.34)}"/>
<stop offset="72%" stop-color="${rgbaCssVar('--line-rgb', 0.08)}"/>
<stop offset="100%" stop-color="${rgbaCssVar('--line-rgb', 0)}"/>
</linearGradient>
<linearGradient id="lineGradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="${rgbaCssVar('--accent2-rgb', 0.98)}"/>
<stop offset="100%" stop-color="${rgbaCssVar('--accent-rgb', 0.98)}"/>
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="7" result="coloredBlur"></feGaussianBlur>
<feMerge>
<feMergeNode in="coloredBlur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
`;
const gridLines = [];
const gridSteps = 4;
for (let i = 0; i <= gridSteps; i++) {
const y = m.top + (plotH * i) / gridSteps;
const value = domainMax - ((domainMax - domainMin) * i) / gridSteps;
gridLines.push(`
<line x1="${m.left}" y1="${y.toFixed(2)}" x2="${W - m.right}" y2="${y.toFixed(2)}"
stroke="rgba(255,255,255,0.12)" stroke-dasharray="4 7" />
<text x="${m.left - 12}" y="${(y + 4).toFixed(2)}" text-anchor="end"
font-size="12" font-weight="700" fill="rgba(245,247,251,0.55)" font-family="system-ui, sans-serif">
${svgEscape(formatTemp(value, false, true))}
</text>
`);
}
const timeGuides = [];
const labelEvery = hourly.length > 12 ? 3 : 2;
points.forEach((p, i) => {
const isMajor = i === 0 || i === points.length - 1 || i % labelEvery === 0;
if (isMajor) {
timeGuides.push(`
<line x1="${p.x.toFixed(2)}" y1="${m.top}" x2="${p.x.toFixed(2)}" y2="${baseline.toFixed(2)}"
stroke="rgba(255,255,255,0.05)" />
<text x="${p.x.toFixed(2)}" y="${H - 8}" text-anchor="middle"
font-size="12" font-weight="700" fill="rgba(245,247,251,0.55)" font-family="system-ui, sans-serif">
${p.hour}:00
</text>
`);
}
});
const pointsMarkup = points.map((p) => {
const r = (p.temp === maxT || p.temp === minT) ? 5.5 : 3.6;
return `
<circle cx="${p.x.toFixed(2)}" cy="${p.y.toFixed(2)}" r="${r}"
fill="${rgbaCssVar('--bg1', 1, true)}" stroke="url(#lineGradient)" stroke-width="3" />
`;
}).join('');
const first = points[0];
const last = points[points.length - 1];
const firstLast = `
<g>
<circle cx="${first.x.toFixed(2)}" cy="${first.y.toFixed(2)}" r="7.5" fill="rgba(255,255,255,0.12)" />
<circle cx="${last.x.toFixed(2)}" cy="${last.y.toFixed(2)}" r="7.5" fill="rgba(255,255,255,0.12)" />
</g>
`;
const extremes = buildExtremeBadges(points, minT, maxT, W, m);
svg.innerHTML = `
${defs}
<rect x="0" y="0" width="${W}" height="${H}" fill="transparent"></rect>
${gridLines.join('')}
${timeGuides.join('')}
<path d="${areaPath}" fill="url(#areaGradient)"></path>
<path d="${linePath}" fill="none" stroke="rgba(255,255,255,0.12)" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="${linePath}" fill="none" stroke="url(#lineGradient)" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round" filter="url(#glow)"></path>
${firstLast}
${pointsMarkup}
${extremes}
`;
}
function buildExtremeBadges(points, minT, maxT, W, m) {
if (!points.length) return '';
if (maxT === minT) {
return badge(points[Math.floor(points.length / 2)], formatTemp(maxT, false, true), 'neutral', W, m);
}
const maxIndex = points.findIndex((p) => p.temp === maxT);
const minIndex = points.findIndex((p) => p.temp === minT);
return badge(points[maxIndex], formatTemp(maxT, false, true), 'warm', W, m) +
badge(points[minIndex], formatTemp(minT, false, true), 'cool', W, m);
}
function badge(point, text, kind, W, m) {
if (!point) return '';
const width = Math.max(46, text.length * 8 + 24);
const x = clamp(point.x - width / 2, m.left, W - m.right - width);
const above = point.y > 52;
const y = above ? point.y - 36 : point.y + 14;
let fill, stroke;
if (kind === 'warm') {
fill = rgbaCssVar('--accent-rgb', 0.18);
stroke = rgbaCssVar('--accent-rgb', 0.58);
} else if (kind === 'cool') {
fill = rgbaCssVar('--accent2-rgb', 0.18);
stroke = rgbaCssVar('--accent2-rgb', 0.58);
} else {
fill = rgbaCssVar('--line-rgb', 0.18);
stroke = rgbaCssVar('--line-rgb', 0.52);
}
return `
<g>
<rect x="${x.toFixed(2)}" y="${y.toFixed(2)}" width="${width}" height="28" rx="14"
fill="${fill}" stroke="${stroke}" />
<text x="${(x + width / 2).toFixed(2)}" y="${(y + 18.5).toFixed(2)}" text-anchor="middle"
font-size="12" font-weight="800" fill="rgba(245,247,251,0.92)" font-family="system-ui, sans-serif">${svgEscape(text)}</text>
</g>
`;
}
function renderHourlyStrip(hourly) {
const container = el.hourlyStrip;
container.innerHTML = '';
if (!hourly.length) {
container.style.gridTemplateColumns = '1fr';
container.innerHTML = '<div class="hour-cell"><div class="hour-time">—</div><div class="hour-temp">No data</div></div>';
return;
}
const temps = hourly.map((h) => h.temp);
const minT = Math.min(...temps);
const maxT = Math.max(...temps);
const span = Math.max(maxT - minT, 1);
const uniform = maxT === minT;
container.style.gridTemplateColumns = `repeat(${hourly.length}, minmax(0, 1fr))`;
hourly.forEach((h) => {
const norm = uniform ? 0.5 : (h.temp - minT) / span;
const rgb = mixRgb([110, 193, 255], [255, 209, 102], norm);
const cell = document.createElement('div');
cell.className = 'hour-cell';
if (!uniform && h.temp === maxT) cell.classList.add('warm');
if (!uniform && h.temp === minT) cell.classList.add('cold');
cell.style.setProperty('--cell-rgb', `${rgb[0]},${rgb[1]},${rgb[2]}`);
cell.style.setProperty('--cell-alpha', (0.08 + norm * 0.18).toFixed(2));
cell.innerHTML = `
<div class="hour-time">${escapeHtml(h.hour)}:00</div>
<div class="hour-temp">${escapeHtml(formatTemp(h.temp, false, true))}</div>
<div class="hour-marker"></div>
`;
container.appendChild(cell);
});
}
function describeWeatherCode(code) {
switch (code) {
case 0: return { label: 'Clear sky', theme: 'clear', icon: 'clear' };
case 1: return { label: 'Mainly clear', theme: 'partly', icon: 'partly' };
case 2: return { label: 'Partly cloudy', theme: 'partly', icon: 'partly' };
case 3: return { label: 'Overcast', theme: 'cloudy', icon: 'cloudy' };
case 45: return { label: 'Fog', theme: 'fog', icon: 'fog' };
case 48: return { label: 'Depositing rime fog', theme: 'fog', icon: 'fog' };
case 51: return { label: 'Light drizzle', theme: 'rain', icon: 'rain' };
case 53: return { label: 'Moderate drizzle', theme: 'rain', icon: 'rain' };
case 55: return { label: 'Dense drizzle', theme: 'rain', icon: 'rain' };
case 56: return { label: 'Light freezing drizzle', theme: 'rain', icon: 'rain' };
case 57: return { label: 'Dense freezing drizzle', theme: 'rain', icon: 'rain' };
case 61: return { label: 'Slight rain', theme: 'rain', icon: 'rain' };
case 63: return { label: 'Moderate rain', theme: 'rain', icon: 'rain' };
case 65: return { label: 'Heavy rain', theme: 'rain', icon: 'rain' };
case 66: return { label: 'Light freezing rain', theme: 'rain', icon: 'rain' };
case 67: return { label: 'Heavy freezing rain', theme: 'rain', icon: 'rain' };
case 71: return { label: 'Slight snowfall', theme: 'snow', icon: 'snow' };
case 73: return { label: 'Moderate snowfall', theme: 'snow', icon: 'snow' };
case 75: return { label: 'Heavy snowfall', theme: 'snow', icon: 'snow' };
case 77: return { label: 'Snow grains', theme: 'snow', icon: 'snow' };
case 80: return { label: 'Slight rain showers', theme: 'rain', icon: 'rain' };
case 81: return { label: 'Moderate rain showers', theme: 'rain', icon: 'rain' };
case 82: return { label: 'Violent rain showers', theme: 'rain', icon: 'rain' };
case 85: return { label: 'Slight snow showers', theme: 'snow', icon: 'snow' };
case 86: return { label: 'Heavy snow showers', theme: 'snow', icon: 'snow' };
case 95: return { label: 'Thunderstorm', theme: 'storm', icon: 'storm' };
case 96: return { label: 'Thunderstorm with slight hail', theme: 'storm', icon: 'storm' };
case 99: return { label: 'Thunderstorm with heavy hail', theme: 'storm', icon: 'storm' };
default: return { label: 'Unknown conditions', theme: 'cloudy', icon: 'cloudy' };
}
}
function applyTheme(themeKey) {
const theme = THEMES[themeKey] || THEMES.clear;
const root = document.documentElement.style;
root.setProperty('--bg1', theme.bg1);
root.setProperty('--bg2', theme.bg2);
root.setProperty('--glow1', theme.glow1);
root.setProperty('--glow2', theme.glow2);
root.setProperty('--accent-rgb', theme.accent);
root.setProperty('--accent2-rgb', theme.accent2);
root.setProperty('--line-rgb', theme.line);
}
function iconForWeather(kind) {
switch (kind) {
case 'clear':
return `
<svg class="weather-icon" viewBox="0 0 200 200" fill="none" aria-hidden="true">
<defs>
<radialGradient id="sunGrad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#fff7c8"/>
<stop offset="60%" stop-color="#ffd96e"/>
<stop offset="100%" stop-color="#ffb24a"/>
</radialGradient>
</defs>
<g class="sun-group">
<line class="sun-ray" x1="100" y1="28" x2="100" y2="10"></line>
<line class="sun-ray" x1="100" y1="190" x2="100" y2="172"></line>
<line class="sun-ray" x1="28" y1="100" x2="10" y2="100"></line>
<line class="sun-ray" x1="190" y1="100" x2="172" y2="100"></line>
<line class="sun-ray" x1="48.5" y1="48.5" x2="36" y2="36"></line>
<line class="sun-ray" x1="151.5" y1="151.5" x2="164" y2="164"></line>
<line class="sun-ray" x1="151.5" y1="48.5" x2="164" y2="36"></line>
<line class="sun-ray" x1="48.5" y1="151.5" x2="36" y2="164"></line>
</g>
<circle cx="100" cy="100" r="36" fill="url(#sunGrad)"></circle>
<circle cx="100" cy="100" r="44" stroke="#ffe59a" stroke-opacity=".5" stroke-width="6"></circle>
</svg>
`;
case 'partly':
return `
<svg class="weather-icon" viewBox="0 0 200 200" fill="none" aria-hidden="true">
<defs>
<radialGradient id="sunGrad2" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#fff8cd"/>
<stop offset="60%" stop-color="#ffd86b"/>
<stop offset="100%" stop-color="#ffb047"/>
</radialGradient>
</defs>
<g class="sun-group" transform="translate(-8 -8)">
<line class="sun-ray" x1="78" y1="45" x2="78" y2="28"></line>
<line class="sun-ray" x1="78" y1="113" x2="78" y2="96"></line>
<line class="sun-ray" x1="45" y1="78" x2="28" y2="78"></line>
<line class="sun-ray" x1="113" y1="78" x2="96" y2="78"></line>
<line class="sun-ray" x1="54.5" y1="54.5" x2="43" y2="43"></line>
<line class="sun-ray" x1="101.5" y1="101.5" x2="113" y2="113"></line>
<line class="sun-ray" x1="101.5" y1="54.5" x2="113" y2="43"></line>
<line class="sun-ray" x1="54.5" y1="101.5" x2="43" y2="113"></line>
<circle cx="78" cy="78" r="28" fill="url(#sunGrad2)"></circle>
</g>
<g class="cloud-group">
<ellipse class="cloud-shadow" cx="116" cy="128" rx="42" ry="25"></ellipse>
<path class="cloud-fill" d="M56 129c0-18.2 14.8-33 33-33 2.4 0 4.8.2 7 .7 5.7-14.5 19.8-24.7 36.2-24.7 21.3 0 38.8 17 39.6 38.1 12 1.2 21.2 11.3 21.2 23.6 0 13.2-10.8 23.9-24 23.9H80.6c-13.6 0-24.6-11-24.6-24.6 0-1.3.1-2.7.4-4.1z"></path>
</g>
</svg>
`;
case 'cloudy':
return `
<svg class="weather-icon" viewBox="0 0 200 200" fill="none" aria-hidden="true">
<g class="cloud-group">
<ellipse class="cloud-shadow" cx="108" cy="126" rx="44" ry="26"></ellipse>
<path class="cloud-fill" d="M44 127c0-17.5 14.2-31.7 31.7-31.7 3.1 0 6 .4 8.8 1.2 6.5-18.4 24-31.5 44.4-31.5 26.1 0 47.4 20.6 48.4 46.5 13.2 1.6 23.4 12.8 23.4 26.4 0 14.8-12 26.8-26.8 26.8H70.4C55.8 164.7 44 152.8 44 138.2c0-3.8.8-7.4 2.2-10.8z"></path>
</g>
</svg>
`;
case 'fog':
return `
<svg class="weather-icon" viewBox="0 0 200 200" fill="none" aria-hidden="true">
<g class="cloud-group">
<ellipse class="cloud-shadow" cx="110" cy="102" rx="38" ry="22"></ellipse>
<path class="cloud-fill" d="M56 103c0-15.6 12.7-28.3 28.3-28.3 2.7 0 5.3.4 7.8 1.1 5.8-15.7 20.8-26.8 38.3-26.8 22.4 0 40.8 17.7 41.7 39.9 11.4 1.4 20.2 11.2 20.2 22.9 0 12.8-10.4 23.2-23.2 23.2H78.5c-12.9 0-23.4-10.5-23.4-23.4 0-3 .6-5.8 1.7-8.6z"></path>
</g>
<g transform="translate(0 24)">
<line class="fog-line" x1="44" y1="112" x2="156" y2="112"></line>
<line class="fog-line" x1="58" y1="132" x2="148" y2="132"></line>
<line class="fog-line" x1="72" y1="152" x2="136" y2="152"></line>
</g>
</svg>
`;
case 'snow':
return `
<svg class="weather-icon" viewBox="0 0 200 200" fill="none" aria-hidden="true">
<g class="cloud-group">
<ellipse class="cloud-shadow" cx="108" cy="100" rx="42" ry="24"></ellipse>
<path class="cloud-fill" d="M50 103c0-16.8 13.6-30.4 30.4-30.4 2.8 0 5.5.4 8.1 1.1 6.2-16.9 22.4-28.8 41.4-28.8 24.2 0 44.1 19 45 43 12.2 1.5 21.6 11.8 21.6 24.4 0 13.7-11.1 24.8-24.8 24.8H73.6c-13.6 0-24.6-11-24.6-24.6 0-3.3.7-6.5 2-9.5z"></path>
</g>
<g class="drop-group">
<g transform="translate(74 140)">
<line class="snowflake" x1="0" y1="-9" x2="0" y2="9"></line>
<line class="snowflake" x1="-7.8" y1="-4.5" x2="7.8" y2="4.5"></line>
<line class="snowflake" x1="-7.8" y1="4.5" x2="7.8" y2="-4.5"></line>
</g>
<g transform="translate(102 154)">
<line class="snowflake" x1="0" y1="-9" x2="0" y2="9"></line>
<line class="snowflake" x1="-7.8" y1="-4.5" x2="7.8" y2="4.5"></line>
<line class="snowflake" x1="-7.8" y1="4.5" x2="7.8" y2="-4.5"></line>
</g>
<g transform="translate(130 142)">
<line class="snowflake" x1="0" y1="-9" x2="0" y2="9"></line>
<line class="snowflake" x1="-7.8" y1="-4.5" x2="7.8" y2="4.5"></line>
<line class="snowflake" x1="-7.8" y1="4.5" x2="7.8" y2="-4.5"></line>
</g>
</g>
</svg>
`;
case 'storm':
return `
<svg class="weather-icon" viewBox="0 0 200 200" fill="none" aria-hidden="true">
<g class="cloud-group">
<ellipse class="cloud-shadow" cx="108" cy="100" rx="42" ry="24"></ellipse>
<path class="cloud-fill" d="M50 103c0-16.8 13.6-30.4 30.4-30.4 2.8 0 5.5.4 8.1 1.1 6.2-16.9 22.4-28.8 41.4-28.8 24.2 0 44.1 19 45 43 12.2 1.5 21.6 11.8 21.6 24.4 0 13.7-11.1 24.8-24.8 24.8H73.6c-13.6 0-24.6-11-24.6-24.6 0-3.3.7-6.5 2-9.5z"></path>
</g>
<g class="drop-group">
<line class="raindrop" x1="74" y1="142" x2="68" y2="160"></line>
<line class="raindrop" x1="132" y1="142" x2="126" y2="160"></line>
</g>
<path class="bolt" d="M106 136h20l-18 22h18l-34 36 11-25H86z"></path>
</svg>
`;
case 'rain':
default:
return `
<svg class="weather-icon" viewBox="0 0 200 200" fill="none" aria-hidden="true">
<g class="cloud-group">
<ellipse class="cloud-shadow" cx="108" cy="100" rx="42" ry="24"></ellipse>
<path class="cloud-fill" d="M50 103c0-16.8 13.6-30.4 30.4-30.4 2.8 0 5.5.4 8.1 1.1 6.2-16.9 22.4-28.8 41.4-28.8 24.2 0 44.1 19 45 43 12.2 1.5 21.6 11.8 21.6 24.4 0 13.7-11.1 24.8-24.8 24.8H73.6c-13.6 0-24.6-11-24.6-24.6 0-3.3.7-6.5 2-9.5z"></path>
</g>
<g class="drop-group">
<line class="raindrop" x1="72" y1="142" x2="66" y2="160"></line>
<line class="raindrop" x1="100" y1="150" x2="94" y2="168"></line>
<line class="raindrop" x1="128" y1="142" x2="122" y2="160"></line>
</g>
</svg>
`;
}
}
async function fetchWithTimeout(url, timeoutMs) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, {
method: 'GET',
headers: { 'Accept': 'application/json' },
signal: controller.signal
});
} finally {
clearTimeout(id);
}
}
function todayUTCString() {
return new Date().toISOString().slice(0, 10);
}
function isValidISODate(value) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
const d = new Date(`${value}T00:00:00Z`);
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === value;
}
function toFiniteNumber(value, fallback) {
if (value == null) return fallback;
const s = String(value).trim();
if (s === '') return fallback;
const n = Number(s);
return Number.isFinite(n) ? n : fallback;
}
function firstFinite(arr) {
if (!Array.isArray(arr)) return NaN;
for (const value of arr) {
const n = Number(value);
if (Number.isFinite(n)) return n;
}
return NaN;
}
function formatDisplayDate(dateStr) {
const d = new Date(`${dateStr}T00:00:00Z`);
return new Intl.DateTimeFormat(undefined, {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC'
}).format(d);
}
function formatCoord(value) {
const n = Number(value);
if (!Number.isFinite(n)) return '—';
return n.toFixed(3);
}
function formatTemp(value, withUnit, compact) {
const n = Number(value);
if (!Number.isFinite(n)) return '—';
const rounded = Math.round(n * 10) / 10;
const whole = Math.abs(rounded - Math.round(rounded)) < 0.05;
const text = compact
? (whole ? String(Math.round(rounded)) : rounded.toFixed(1))
: (whole ? String(Math.round(rounded)) : rounded.toFixed(1));
return `${text}°${withUnit ? 'C' : ''}`;
}
function mixRgb(a, b, t) {
return [
Math.round(a[0] + (b[0] - a[0]) * t),
Math.round(a[1] + (b[1] - a[1]) * t),
Math.round(a[2] + (b[2] - a[2]) * t)
];
}
function clamp(v, min, max) {
return Math.min(max, Math.max(min, v));
}
function smoothSvgPath(points) {
if (!points.length) return '';
if (points.length === 1) {
const p = points[0];
return `M ${p.x.toFixed(2)} ${p.y.toFixed(2)}`;
}
const smoothing = 0.18;
return points.reduce((acc, point, i, arr) => {
if (i === 0) return `M ${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
const cps = controlPoint(arr[i - 1], arr[i - 2], point, false, smoothing);
const cpe = controlPoint(point, arr[i - 1], arr[i + 1], true, smoothing);
return `${acc} C ${cps.x.toFixed(2)} ${cps.y.toFixed(2)}, ${cpe.x.toFixed(2)} ${cpe.y.toFixed(2)}, ${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
}, '');
}
function controlPoint(current, previous, next, reverse, smoothing) {
const p = previous || current;
const n = next || current;
const o = lineProperties(p, n);
const angle = o.angle + (reverse ? Math.PI : 0);
const length = o.length * smoothing;
const x = current.x + Math.cos(angle) * length;
const y = current.y + Math.sin(angle) * length;
return { x, y };
}
function lineProperties(a, b) {
const length = Math.hypot(b.x - a.x, b.y - a.y);
const angle = Math.atan2(b.y - a.y, b.x - a.x);
return { length, angle };
}
function rgbaCssVar(varName, alpha, rawColor) {
const root = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
if (rawColor) {
if (root.startsWith('#')) return root;
return `rgba(${root}, ${alpha})`;
}
if (root.startsWith('#')) {
const hex = root.replace('#', '');
const full = hex.length === 3 ? hex.split('').map((c) => c + c).join('') : hex;
const bigint = parseInt(full, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
return `rgba(${root}, ${alpha})`;
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (ch) => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[ch]));
}
function svgEscape(value) {
return escapeHtml(value);
}
})();
</script>
</body>
</html>L1 —— 廉价的确定性描述量
| bytes 总计 | 45,113 | html / css / js | 3,146 / 11,696 / 30,271 |
|---|---|---|---|
| dom 节点 | 57 | dom 深度 | 10 |
| css 规则 | 98 | 含 js | 是 |
| brightness | 69.0 | contrast | 30.5 |
| colorfulness | 23.0 | 留白 | 33.8% |
该 slot 的 telemetry
| prompt tokens | 312 | completion tokens | 106,704 |
|---|---|---|---|
| total tokens | 107,016 | wall | 522.2 s |
| cost | — | request id | — |
config.json574 B
{
"N": 4,
"auth": {
"credential_ref": "WCB_OPENAI_OFFICIAL_KEY",
"method": "api-key"
},
"billing": "metered",
"config_id": "gpt-5.4-eff-xhigh--api--official--dev",
"effort": "xhigh",
"family": "gpt",
"m": {
"min": 4,
"q": 4
},
"model_id": "gpt-5.4",
"protocol": "api",
"served_model": "gpt-5.4-2026-03-05",
"telemetry": {
"completion_tokens": 1011406,
"cost_usd": null,
"prompt_tokens": 73263,
"total_tokens": 1084669,
"wall_ms": 5745574
},
"transport": "openai-official-api",
"vendor_sanction_ref": null
}send-log4.6 KB
{
"N": 4,
"batch_id": "2026-07-18--official-gpt",
"config_id": "gpt-5.4-eff-xhigh--api--official--dev",
"positions": [
{
"attempts": [
{
"attempt_index": 0,
"backoff_ms": null,
"charged": true,
"ended_at": "2026-07-17T17:34:56.852555+00:00",
"http_status": 200,
"outcome": "valid",
"reached_model": true,
"reason": "ok",
"request_id": null,
"started_at": "2026-07-17T17:21:12.302367+00:00"
}
],
"block_index": 0,
"model_reaching_attempt_index": 0,
"slot_index": 0,
"terminal_state": "valid",
"variant": "P-min"
},
{
"attempts": [
{
"attempt_index": 0,
"backoff_ms": null,
"charged": true,
"ended_at": "2026-07-17T17:43:43.021585+00:00",
"http_status": 200,
"outcome": "valid",
"reached_model": true,
"reason": "ok",
"request_id": null,
"started_at": "2026-07-17T17:35:00.810213+00:00"
}
],
"block_index": 0,
"model_reaching_attempt_index": 0,
"slot_index": 0,
"terminal_state": "valid",
"variant": "P-q"
},
{
"attempts": [
{
"attempt_index": 0,
"backoff_ms": null,
"charged": true,
"ended_at": "2026-07-17T17:56:33.970059+00:00",
"http_status": 200,
"outcome": "valid",
"reached_model": true,
"reason": "ok",
"request_id": null,
"started_at": "2026-07-17T17:43:47.999690+00:00"
}
],
"block_index": 1,
"model_reaching_attempt_index": 0,
"slot_index": 1,
"terminal_state": "valid",
"variant": "P-q"
},
{
"attempts": [
{
"attempt_index": 0,
"backoff_ms": null,
"charged": true,
"ended_at": "2026-07-17T18:03:33.182436+00:00",
"http_status": 200,
"outcome": "valid",
"reached_model": true,
"reason": "ok",
"request_id": null,
"started_at": "2026-07-17T17:56:38.525584+00:00"
}
],
"block_index": 1,
"model_reaching_attempt_index": 0,
"slot_index": 1,
"terminal_state": "valid",
"variant": "P-min"
},
{
"attempts": [
{
"attempt_index": 0,
"backoff_ms": null,
"charged": true,
"ended_at": "2026-07-17T18:20:54.396520+00:00",
"http_status": 200,
"outcome": "valid",
"reached_model": true,
"reason": "ok",
"request_id": null,
"started_at": "2026-07-17T18:03:37.488771+00:00"
}
],
"block_index": 2,
"model_reaching_attempt_index": 0,
"slot_index": 2,
"terminal_state": "valid",
"variant": "P-q"
},
{
"attempts": [
{
"attempt_index": 0,
"backoff_ms": null,
"charged": true,
"ended_at": "2026-07-17T18:32:12.335112+00:00",
"http_status": 200,
"outcome": "valid",
"reached_model": true,
"reason": "ok",
"request_id": null,
"started_at": "2026-07-17T18:20:58.951108+00:00"
}
],
"block_index": 2,
"model_reaching_attempt_index": 0,
"slot_index": 2,
"terminal_state": "valid",
"variant": "P-min"
},
{
"attempts": [
{
"attempt_index": 0,
"backoff_ms": null,
"charged": true,
"ended_at": "2026-07-17T18:40:01.298976+00:00",
"http_status": 200,
"outcome": "valid",
"reached_model": true,
"reason": "ok",
"request_id": null,
"started_at": "2026-07-17T18:32:16.233971+00:00"
}
],
"block_index": 3,
"model_reaching_attempt_index": 0,
"slot_index": 3,
"terminal_state": "valid",
"variant": "P-min"
},
{
"attempts": [
{
"attempt_index": 0,
"backoff_ms": null,
"charged": true,
"ended_at": "2026-07-17T18:57:28.064600+00:00",
"http_status": 200,
"outcome": "valid",
"reached_model": true,
"reason": "ok",
"request_id": null,
"started_at": "2026-07-17T18:40:05.187639+00:00"
}
],
"block_index": 3,
"model_reaching_attempt_index": 0,
"slot_index": 3,
"terminal_state": "valid",
"variant": "P-q"
}
],
"retry_policy": {
"max_unreachable_retries": 5,
"rate_limit_backoff": {
"max_attempts": 4,
"max_total_ms": 60000
}
}
}