Operator screen
AI Analysis
Alarm flood and trend analysis with the data reduction shown alongside the result.
index.htm as it is written with the smart-industrial package; industrial-demos.css is the screens' shared layout
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>AI Analysis - Smart.Industrial</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="industrial-demos.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial-dark">
<div class="hmi-screen">
<header class="hmi-header">
<div class="hmi-identity">
<span class="hmi-plant">AI ANALYSIS</span>
<span class="hmi-area">Alarm floods · shift trends · test reports</span>
</div>
<div class="hmi-header-actions">
<label class="hmi-field">
<span>Provider</span>
<select id="provider">
<option value="">Simulated (no model called)</option>
<option value="anthropic">Anthropic</option>
<option value="openai">OpenAI</option>
<option value="azure">Azure OpenAI</option>
</select>
</label>
<label class="hmi-field">
<span>API key</span>
<input id="apiKey" type="password" placeholder="optional" autocomplete="off" />
</label>
<button class="hmi-theme-toggle" id="themeToggle" type="button">Light</button>
</div>
</header>
<!-- The point of the screen, said once and plainly. -->
<section class="lede" aria-label="How this works">
<p>
Every panel here reduces its data <strong>before</strong> anything is sent. A shift of
1 Hz trend is 28,800 samples per pen; what leaves is a few hundred bytes of
statistics. You can read the exact payload under each panel before you send it.
</p>
<p class="lede-note" id="ledeNote">
Nothing is configured, so the answers below are produced locally from the same
reduction and marked <em>simulated</em>. Enter a key, or point
<code>configure({ endpoint })</code> at your own service, and the identical payload
goes to a real model instead.
</p>
</section>
<div class="panels">
<!-- ── Alarm flood ─────────────────────────────────────────── -->
<section class="hmi-panel">
<div class="hmi-panel-header">
<span>Alarm flood · 13:00–13:03</span>
<span class="hmi-panel-note" id="alarmNote">—</span>
</div>
<div class="hmi-panel-content panel-split">
<smart-alarm-grid id="alarmGrid"></smart-alarm-grid>
<div class="answer-column">
<button class="hmi-button hmi-button-primary" id="askAlarms" type="button">
Summarise for shift handover
</button>
<output class="answer" id="alarmAnswer" aria-live="polite">
<span class="answer-idle">Not asked yet.</span>
</output>
<details class="payload">
<summary>What would be sent <span class="payload-size" id="alarmSize"></span></summary>
<pre id="alarmPayload"></pre>
</details>
</div>
</div>
</section>
<!-- ── Shift trend ─────────────────────────────────────────── -->
<section class="hmi-panel">
<div class="hmi-panel-header">
<span>Shift trend · 06:00–14:00</span>
<span class="hmi-panel-note" id="trendNote">—</span>
</div>
<div class="hmi-panel-content panel-split">
<smart-strip-chart id="stripChart" mode="scroll" auto-scale="perPen"
history-length="1300" show-digital-display></smart-strip-chart>
<div class="answer-column">
<button class="hmi-button hmi-button-primary" id="askTrend" type="button">
Describe what happened
</button>
<output class="answer" id="trendAnswer" aria-live="polite">
<span class="answer-idle">Not asked yet.</span>
</output>
<details class="payload">
<summary>What would be sent <span class="payload-size" id="trendSize"></span></summary>
<pre id="trendPayload"></pre>
</details>
</div>
</div>
</section>
<!-- ── Test run ────────────────────────────────────────────── -->
<section class="hmi-panel">
<div class="hmi-panel-header">
<span>Test run · PA-2200 serial 004817</span>
<span class="hmi-panel-note" id="sequenceNote">—</span>
</div>
<div class="hmi-panel-content panel-split">
<smart-sequence-editor id="sequence" mode="run"></smart-sequence-editor>
<div class="answer-column">
<button class="hmi-button hmi-button-primary" id="askSequence" type="button">
Write the report
</button>
<output class="answer" id="sequenceAnswer" aria-live="polite">
<span class="answer-idle">Not asked yet.</span>
</output>
<details class="payload">
<summary>What would be sent <span class="payload-size" id="sequenceSize"></span></summary>
<pre id="sequencePayload"></pre>
</details>
</div>
</div>
</section>
</div>
</div>
<!-- scripts -->
<script type="module" src="node_modules/smart-industrial/source/modules/smart.alarmgrid.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.stripchart.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.sequenceeditor.js"></script>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.industrial.ai.js"></script>
<script type="text/javascript" src="data.js"></script>
<script type="text/javascript" src="responder.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
<script>
/* The site's light/dark switch is the reader's choice: a screen opened from the light site
in a new tab opens light too. Framed, the site's page sets the theme itself. */
addEventListener('load', function () {
var light = false,
toggle = document.querySelector('.hmi-theme-toggle');
try { light = window.top === window && localStorage.getItem('smart-industrial-dark') === 'false'; } catch (e) { }
/* after the screen's own onload, which is where its button starts listening */
if (light && toggle) { setTimeout(function () { if (document.body.getAttribute('theme') === 'industrial-dark') { toggle.click(); } }, 0); }
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* AI analysis.
*
* Three questions an engineer is actually asked - what happened in that flood, what did
* the shift do, what does this test run say - answered from data the components already
* hold. The screen's real subject is the payload: every panel shows exactly what would
* leave the browser, because that is the first question anyone in this industry asks. */
window.onload = function () {
const ai = window.Smart.Industrial.ai,
alarms = window.AIData.alarms(),
trendRows = window.AIData.trend(),
pens = window.AIData.trendPens;
/* ── Configuration ──────────────────────────────────────────────────── */
/* With nothing entered the client gets a sendRequest override, which replaces the
transport and leaves the prompt, the reduction and the promise exactly as they are.
Entering a key removes it, and the identical payload goes to the provider. */
function applyConfiguration() {
const provider = document.getElementById('provider').value,
key = document.getElementById('apiKey').value.trim(),
live = !!(provider && key),
note = document.getElementById('ledeNote');
ai.configure({
provider: provider || 'anthropic',
apiKey: live ? key : '',
sendRequest: live ? null : window.AIResponder.respond
});
note.classList.toggle('lede-note-live', live);
note.innerHTML = live ?
'Configured for <strong>' + provider + '</strong>. The payloads below are what ' +
'will be sent, and nothing else goes with them.' :
'Nothing is configured, so the answers below are written locally from the same ' +
'reduction and marked <em>simulated</em>. Enter a key, or point ' +
'<code>configure({ endpoint })</code> at your own service, and the identical ' +
'payload goes to a real model instead.';
return live;
}
/* ── Asking ─────────────────────────────────────────────────────────── */
function ask(id, run) {
const live = applyConfiguration(),
target = document.getElementById(id);
target.className = 'answer answer-pending';
target.textContent = 'Thinking…';
run().then(function (text) {
target.className = 'answer';
target.textContent = text;
if (!live) {
const mark = document.createElement('span');
//Never let a locally-written answer read as a model's.
mark.className = 'answer-mark';
mark.textContent = 'simulated — no model was called';
target.appendChild(mark);
}
}, function (error) {
target.className = 'answer answer-error';
target.textContent = error.message;
});
}
/* ── The payload, shown before it is sent ───────────────────────────── */
function bytes(count) {
if (count > 1048576) {
return (count / 1048576).toFixed(1) + ' MB';
}
return count > 1024 ? (count / 1024).toFixed(1) + ' KB' : count + ' B';
}
function showPayload(sizeId, payloadId, reduced, rawLength) {
const text = JSON.stringify(reduced, null, 2);
document.getElementById(payloadId).textContent = text;
/* Where the raw data is larger, say by how much - that ratio is the argument.
Where it is not, say so rather than printing a flattering "100 B of 100 B". */
document.getElementById(sizeId).textContent = rawLength > text.length * 1.5 ?
bytes(text.length) + ', reduced from ' + bytes(rawLength) :
bytes(text.length) + ', nothing stripped';
}
/* ── Alarm flood ────────────────────────────────────────────────────── */
const alarmGrid = document.getElementById('alarmGrid'),
alarmReduction = ai.reduceAlarms(alarms);
alarmGrid.alarms = alarms;
alarmGrid.defaultSort = 'time';
document.getElementById('alarmNote').textContent =
alarms.length + ' alarms · ' + alarmReduction.spanSeconds + ' s · ' +
(alarmReduction.isFlood ? 'flood by ISA-18.2' : 'within rate');
showPayload('alarmSize', 'alarmPayload', alarmReduction, JSON.stringify(alarms).length);
document.getElementById('askAlarms').addEventListener('click', function () {
ask('alarmAnswer', function () {
return ai.summariseAlarms(alarms, {
context: 'Continuous polymer line, areas Feed and Reactor. One operator on shift.'
});
});
});
/* ── Shift trend ────────────────────────────────────────────────────── */
const chart = document.getElementById('stripChart'),
trendReduction = ai.reduceTrend(trendRows, pens);
/* Colours from the shared palette rather than from the data file: the trend then
matches every other industrial screen. Re-applied on a theme change, because a
colour set in script is invisible to a stylesheet - without this the pens keep the
other theme's colours, and the readout prints each value in its pen colour. */
function tintPens() {
const tints = window.Smart.Industrial.paletteFor(document.body);
chart.pens = pens.map(function (pen, index) {
return Object.assign({}, pen, {
//From the series ramp only - an alarm colour on an ordinary pen says the
//process is in alarm, and the alarm fills are around 3:1 as text.
color: tints.series[index % tints.series.length]
});
});
}
tintPens();
/* Decimated for display only - every 24th sample, so the whole shift fits the width
and its shape is readable. The reduction reads all 28,800, which is the point: what
a chart can show and what an analysis can use are not the same window. */
const shown = [];
for (let i = 0; i < trendRows.length; i += 24) {
shown.push(trendRows[i]);
}
chart.pushMany(shown);
document.getElementById('trendNote').textContent =
trendRows.length.toLocaleString() + ' samples × ' + pens.length + ' pens';
showPayload('trendSize', 'trendPayload', trendReduction, JSON.stringify(trendRows).length);
document.getElementById('askTrend').addEventListener('click', function () {
ask('trendAnswer', function () {
return ai.describeTrend(trendRows, pens, {
context: 'Feed pump P-101 and reactor R-201, 06:00 to 14:00 shift.'
});
});
});
/* ── Test run ───────────────────────────────────────────────────────── */
const sequence = document.getElementById('sequence');
sequence.steps = window.AIData.steps();
const sequenceReport = sequence.report();
document.getElementById('sequenceNote').textContent =
(sequenceReport.passed ? 'PASS' : 'FAIL') + ' · ' +
sequenceReport.counts.passed + '/' + sequenceReport.total + ' steps passed';
showPayload('sequenceSize', 'sequencePayload', sequenceReport,
JSON.stringify(sequenceReport).length);
document.getElementById('askSequence').addEventListener('click', function () {
ask('sequenceAnswer', function () {
//Read the report again at click time, so a re-run is what gets written up.
return ai.explainSequence(sequence.report(), {
context: 'PA-2200 audio power amplifier, serial 004817, production test.'
});
});
});
/* ── Chrome ─────────────────────────────────────────────────────────── */
document.getElementById('provider').addEventListener('change', applyConfiguration);
document.getElementById('apiKey').addEventListener('input', applyConfiguration);
document.getElementById('themeToggle').addEventListener('click', function () {
const body = document.body,
dark = body.getAttribute('theme') === 'industrial-dark',
next = dark ? 'industrial' : 'industrial-dark';
body.setAttribute('theme', next);
this.textContent = dark ? 'Dark' : 'Light';
tintPens();
document.querySelectorAll('smart-alarm-grid, smart-strip-chart, smart-sequence-editor')
.forEach(function (element) {
element.theme = next;
});
});
applyConfiguration();
};
styles.css
/* AI analysis screen. Same surface steps as the other industrial screens - the only
saturated colour belongs to an alarm priority or a failed step, never to chrome. */
html,
body {
height: 100%;
margin: 0;
}
body.viewport {
background: var(--smart-industrial-canvas, var(--smart-body-background-color));
color: var(--smart-text-color);
font-family: var(--smart-font-family);
font-size: 13px;
overflow: auto;
}
.hmi-screen {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
min-height: 100%;
box-sizing: border-box;
}
/* ── Header ──────────────────────────────────────────────────────────── */
.hmi-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 10px 14px;
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
}
.hmi-identity {
display: flex;
flex-direction: column;
gap: 2px;
}
.hmi-plant {
font-size: 16px;
font-weight: 600;
letter-spacing: .08em;
}
.hmi-area {
color: var(--smart-text-tertiary-color);
font-size: 11px;
letter-spacing: .04em;
}
.hmi-header-actions {
display: flex;
align-items: flex-end;
gap: 10px;
}
.hmi-field {
display: flex;
flex-direction: column;
gap: 3px;
}
.hmi-field > span {
color: var(--smart-text-tertiary-color);
font-size: 10px;
letter-spacing: .06em;
text-transform: uppercase;
}
.hmi-field select,
.hmi-field input {
background: var(--smart-background);
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-color);
font-family: inherit;
font-size: 12px;
padding: 5px 7px;
min-width: 150px;
}
.hmi-button,
.hmi-theme-toggle {
background: transparent;
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
color: var(--smart-text-secondary-color);
cursor: pointer;
font-family: inherit;
font-size: 12px;
padding: 6px 12px;
}
.hmi-button:hover,
.hmi-theme-toggle:hover {
border-color: var(--smart-primary);
color: var(--smart-text-color);
}
.hmi-button-primary {
border-color: var(--smart-primary);
color: var(--smart-text-color);
font-weight: 600;
}
/* ── Lede ────────────────────────────────────────────────────────────── */
.lede {
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
padding: 12px 14px;
}
.lede p {
margin: 0;
max-width: 100ch;
line-height: 1.55;
}
.lede-note {
color: var(--smart-text-tertiary-color);
font-size: 12px;
margin-top: 6px !important;
}
.lede-note code {
font-family: var(--smart-font-family-monospace, monospace);
font-size: 11px;
}
/* The one state change worth colouring: something is actually configured. */
.lede-note-live {
color: var(--smart-text-secondary-color);
}
/* ── Panels ──────────────────────────────────────────────────────────── */
.panels {
display: flex;
flex-direction: column;
gap: 10px;
}
.hmi-panel {
background: var(--smart-industrial-panel-background);
border: 1px solid var(--smart-industrial-panel-border);
border-radius: 4px;
box-shadow: var(--smart-industrial-panel-shadow);
display: flex;
flex-direction: column;
overflow: hidden;
}
.hmi-panel-header {
align-items: baseline;
border-bottom: 1px solid var(--smart-industrial-panel-border);
display: flex;
justify-content: space-between;
font-size: 11px;
font-weight: 600;
letter-spacing: .06em;
padding: 8px 12px;
text-transform: uppercase;
}
.hmi-panel-note {
color: var(--smart-text-tertiary-color);
font-weight: 400;
letter-spacing: .03em;
text-transform: none;
}
.hmi-panel-content {
padding: 12px;
}
/* The component on the left, the answer on the right - so the reduction and what it
came from stay in one glance. */
.panel-split {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 1fr);
gap: 14px;
}
.panel-split > smart-alarm-grid,
.panel-split > smart-strip-chart,
.panel-split > smart-sequence-editor {
height: 320px;
width: 100%;
}
@media (max-width: 1100px) {
.panel-split {
grid-template-columns: minmax(0, 1fr);
}
}
/* ── The answer ──────────────────────────────────────────────────────── */
.answer-column {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
}
.answer {
background: var(--smart-background);
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
display: block;
flex: 1;
font-size: 13px;
line-height: 1.6;
min-height: 120px;
padding: 12px;
white-space: pre-wrap;
}
.answer-idle,
.answer-pending {
color: var(--smart-text-tertiary-color);
font-style: italic;
}
.answer-error {
border-color: var(--smart-error, #b3453a);
}
/* Never let a locally-written answer read as a model's. */
.answer-mark {
color: var(--smart-text-tertiary-color);
display: block;
font-size: 11px;
font-style: italic;
letter-spacing: .04em;
margin-top: 10px;
}
/* ── The payload ─────────────────────────────────────────────────────── */
.payload {
border: 1px solid var(--smart-border);
border-radius: var(--smart-border-radius);
padding: 8px 10px;
}
.payload > summary {
cursor: pointer;
font-size: 11px;
letter-spacing: .05em;
text-transform: uppercase;
}
.payload-size {
color: var(--smart-text-tertiary-color);
font-weight: 400;
letter-spacing: .02em;
text-transform: none;
}
.payload pre {
font-family: var(--smart-font-family-monospace, monospace);
font-size: 11px;
line-height: 1.5;
margin: 8px 0 0;
max-height: 220px;
overflow: auto;
}