Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 127 additions & 24 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,15 @@ const visualsState = {
fallbackMessage: null,
urchin: null,
useLegacy: false,
statusOverlay: null,
statusText: null,
metaBar: null,
runLabel: null,
};
let lastVisualPayload = null;
let isGeneratingCalendar = false;
const GENERATE_BUTTON_DEFAULT_LABEL = 'Generate schedule';
const GENERATE_BUTTON_LOADING_LABEL = 'Generating…';

const CALENDAR_HISTORY_LIMIT = 20;
const calendarHistoryState = {
Expand Down Expand Up @@ -133,6 +140,28 @@ function syncVisualsVisibility() {
}
}

function showVisualsOverlay(message, { loading = false } = {}) {
const overlay = visualsState.statusOverlay;
const text = visualsState.statusText;
if (!overlay || !text) {
return;
}
overlay.hidden = false;
overlay.classList.toggle('is-loading', Boolean(loading));
text.textContent = message || '';
}

function hideVisualsOverlay() {
const overlay = visualsState.statusOverlay;
const text = visualsState.statusText;
if (!overlay || !text) {
return;
}
overlay.hidden = true;
overlay.classList.remove('is-loading');
text.textContent = '';
}

function resolveLegacyPng(payload) {
if (!payload || typeof payload !== 'object') {
return '';
Expand Down Expand Up @@ -230,6 +259,16 @@ function initVisualsMount() {
layout.append(mainPanel);
visualsState.mainPanel = mainPanel;

const metaBar = document.createElement('div');
metaBar.className = 'visuals-meta';
metaBar.hidden = true;
const runLabel = document.createElement('span');
runLabel.className = 'visuals-meta__run';
metaBar.append(runLabel);
mainPanel.append(metaBar);
visualsState.metaBar = metaBar;
visualsState.runLabel = runLabel;

const mount = document.createElement('div');
mount.className = 'visuals-mount';
mainPanel.append(mount);
Expand All @@ -251,6 +290,18 @@ function initVisualsMount() {
visualsState.fallbackImg = fallbackImg;
visualsState.fallbackMessage = fallbackMessage;

const overlay = document.createElement('div');
overlay.className = 'visuals-status-overlay';
overlay.hidden = true;
const overlaySpinner = document.createElement('div');
overlaySpinner.className = 'visuals-status-overlay__spinner';
const overlayText = document.createElement('div');
overlayText.className = 'visuals-status-overlay__text';
overlay.append(overlaySpinner, overlayText);
mainPanel.append(overlay);
visualsState.statusOverlay = overlay;
visualsState.statusText = overlayText;

const historyPanel = createCalendarHistoryPanel();
if (historyPanel) {
layout.append(historyPanel);
Expand Down Expand Up @@ -393,6 +444,40 @@ function formatHistoryHours(value) {
return rounded.toFixed(1);
}

function updateActiveRunLabel() {
const metaBar = visualsState.metaBar;
const runLabel = visualsState.runLabel;
if (!metaBar || !runLabel) {
return;
}

const { activeId, runHistory } = calendarHistoryState;
if (!activeId) {
runLabel.textContent = '';
metaBar.hidden = true;
return;
}

const index = runHistory.findIndex((entry) => entry.id === activeId);
if (index === -1) {
runLabel.textContent = '';
metaBar.hidden = true;
return;
}

const entry = runHistory[index];
const runNumber = runHistory.length - index;
const parts = [`Run #${runNumber}`];
if (entry.timestamp) {
const formatted = formatHistoryTimestamp(entry.timestamp);
if (formatted) {
parts.push(formatted);
}
}
runLabel.textContent = parts.join(' · ');
metaBar.hidden = false;
}

function renderCalendarRunHistory() {
const list = calendarHistoryState.list;
if (!list) {
Expand All @@ -405,6 +490,7 @@ function renderCalendarRunHistory() {
emptyItem.className = 'visuals-history-empty';
emptyItem.textContent = 'No runs yet. Generate a schedule to build history.';
list.append(emptyItem);
updateActiveRunLabel();
return;
}

Expand Down Expand Up @@ -468,6 +554,7 @@ function renderCalendarRunHistory() {
});

list.append(fragment);
updateActiveRunLabel();
}

function createCalendarHistoryPanel() {
Expand Down Expand Up @@ -552,6 +639,7 @@ function restoreCalendarHistoryEntry(entryId) {
const validation = validateWebV1Calendar(payload);
setJsonValidationBadge(validation.ok ? 'ok' : 'err');
renderCalendarRunHistory();
hideVisualsOverlay();
dispatchIntent({
type: INTENT_TYPES.NAVIGATE_TAB,
payload: { tab: 'visuals' },
Expand Down Expand Up @@ -3554,9 +3642,23 @@ function hydrateConfigPanel() {
if (generateButton) {
styleRuntimeButton(generateButton);
generateButton.disabled = false;
generateButton.textContent = GENERATE_BUTTON_DEFAULT_LABEL;
updateRuntimeButtonState(generateButton);

const setGenerateButtonState = (loading) => {
isGeneratingCalendar = Boolean(loading);
generateButton.disabled = isGeneratingCalendar;
generateButton.textContent = isGeneratingCalendar
? GENERATE_BUTTON_LOADING_LABEL
: GENERATE_BUTTON_DEFAULT_LABEL;
updateRuntimeButtonState(generateButton);
};

const handleGenerate = async () => {
if (isGeneratingCalendar) {
return;
}

const snapshot = typeof getConfigSnapshot === 'function' ? getConfigSnapshot() : {};
const variantId = snapshot.variant || 'mk1';
const rigId = snapshot.rig || 'default';
Expand All @@ -3568,9 +3670,11 @@ function hydrateConfigPanel() {
? calendarConfig?.mk2?.workforce?.budgetText || ''
: '';

generateButton.disabled = true;
generateButton.textContent = 'Generating…';
updateRuntimeButtonState(generateButton);
setGenerateButtonState(true);
updateVisuals(null);
showVisualsOverlay('Generating schedule…', { loading: true });
calendarHistoryState.activeId = null;
renderCalendarRunHistory();

beginConsoleRun('Generating payload…');

Expand All @@ -3582,23 +3686,23 @@ function hydrateConfigPanel() {
const seedNumber = Number.parseInt(seedValue, 10);
const normalizedSeed = Number.isFinite(seedNumber) ? seedNumber : seedValue;

const workerArgs = {
class: 'calendar',
variant: variantId,
rig: rigId,
archetype,
week_start: weekStartValue,
seed: normalizedSeed,
};
if (budgetText && budgetText.trim()) {
try {
workerArgs.yearly_budget = JSON.parse(budgetText);
} catch (parseError) {
throw { error: 'Invalid yearly budget JSON.', stdout: '', stderr: '' };
try {
const workerArgs = {
class: 'calendar',
variant: variantId,
rig: rigId,
archetype,
week_start: weekStartValue,
seed: normalizedSeed,
};
if (budgetText && budgetText.trim()) {
try {
workerArgs.yearly_budget = JSON.parse(budgetText);
} catch (parseError) {
throw { error: 'Invalid yearly budget JSON.', stdout: '', stderr: '' };
}
}
}

try {
const { result = null, stdout = '', stderr = '', fallback = false } =
await sendWorkerMessage('run', {
fn: selectedFn || 'mock_run',
Expand All @@ -3612,6 +3716,7 @@ function hydrateConfigPanel() {

if (!result || typeof result !== 'object') {
appendConsoleLog('error: No result returned from worker.');
showVisualsOverlay('No result returned from worker.', { loading: false });
dispatchIntent({
type: INTENT_TYPES.SHOW_TOAST,
payload: {
Expand All @@ -3630,6 +3735,7 @@ function hydrateConfigPanel() {
weekStart: result.week_start || weekStartValue,
});
updateJsonActionsState();
hideVisualsOverlay();

const eventsCount = Array.isArray(result.events) ? result.events.length : 0;
const inputsSnapshot = {
Expand Down Expand Up @@ -3693,6 +3799,7 @@ function hydrateConfigPanel() {
? error.message
: 'Generation failed.';
console.error('Generation failed:', error);
showVisualsOverlay(description, { loading: false });
dispatchIntent({
type: INTENT_TYPES.SHOW_TOAST,
payload: {
Expand All @@ -3703,15 +3810,11 @@ function hydrateConfigPanel() {
},
});
} finally {
generateButton.disabled = false;
generateButton.textContent = 'Generate';
updateRuntimeButtonState(generateButton);
setGenerateButtonState(false);
}
};

generateButton.addEventListener('click', () => {
handleGenerate();
});
generateButton.addEventListener('click', handleGenerate);
}

if (initializeRuntimeButton && runtimeReady) {
Expand Down
77 changes: 77 additions & 0 deletions web/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,30 @@ body {
display: flex;
flex-direction: column;
gap: 16px;
position: relative;
}

.visuals-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
border-radius: 12px;
border: 1px solid rgba(148, 163, 184, 0.25);
background: rgba(15, 23, 42, 0.45);
color: #e2e8f0;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.01em;
}

.visuals-meta[hidden] {
display: none;
}

.visuals-meta__run {
font-weight: 600;
}

.visuals-history-panel {
Expand Down Expand Up @@ -831,6 +855,59 @@ body {
text-align: center;
}

.visuals-status-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 24px;
border-radius: 16px;
background: rgba(15, 23, 42, 0.75);
color: #e2e8f0;
font-size: 16px;
font-weight: 600;
text-align: center;
pointer-events: none;
backdrop-filter: blur(2px);
z-index: 5;
}

.visuals-status-overlay[hidden] {
display: none;
}

.visuals-status-overlay__spinner {
width: 32px;
height: 32px;
border-radius: 50%;
border: 3px solid rgba(226, 232, 240, 0.3);
border-top-color: #38bdf8;
animation: visuals-spinner 0.85s linear infinite;
display: none;
}

.visuals-status-overlay.is-loading .visuals-status-overlay__spinner {
display: block;
}

.visuals-status-overlay__text {
max-width: 320px;
font-size: 15px;
line-height: 1.4;
}

@keyframes visuals-spinner {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

.radial-urchin-root {
display: flex;
flex-direction: column;
Expand Down