Co-Study4Grid / docs /features /frontend-ui-improvements.md
github-actions[bot]
Deploy 7688ef1
13d4e44
|
Raw
History Blame Contribute Delete
17.3 kB

Frontend UI Improvements Plan

Status: Implemented
Scope: frontend/src/ components. The auto-generated frontend/dist-standalone/standalone.html (PR #101) inherits from the React source via npm run build:standalone; the legacy hand-maintained standalone_interface.html has been decommissioned.
Date: 2026-04-01

Note (historical): this is an early UI-improvements plan. Several of these items have since been superseded or extended by later work — notably the shared Action Filter rings strip (severity + action-type + Max-loading, replacing the per-surface category toggles here), the collapsible readability sidebar, the tiered Notices pill + DiagramLegend, and light / dark theme (all 0.8.0). For current behavior see docs/README.md, the relevant feature docs (e.g. dark-mode.md) and the CHANGELOG. The content below is preserved as the original plan of record.


Table of Contents

  1. Move & Rename "Run Analysis" Button
  2. Always-Visible Visualization Tabs with Placeholder Messages
  3. Collapsible Voltage Filter
  4. Revised Color Code for Highlights

1. Move & Rename "Run Analysis" Button

Current State

The "Run Analysis" button lives inside the contingency selector card in App.tsx (line ~812–829), directly below the contingency <input> field. It is rendered as:

<button onClick={wrappedRunAnalysis} disabled={!selectedBranch || analysisLoading}>
  {analysisLoading ? '⚙️ Running...' : '🚀 Run Analysis'}
</button>

The ActionFeed panel (below the OverloadPanel in the sidebar) already has its own button zone where:

  • A processing spinner appears while analysisLoading is true (line ~916)
  • A "Display N prioritized actions" button appears when pendingAnalysisResult is ready (line ~934)

Target State

Move the analysis trigger into the ActionFeed panel header area, at the location where the processing indicator and "Display" button appear. The three states become a single slot:

State What is shown
Idle (no analysis running, no pending result) 🔍 Analyze & Suggest button (green, enabled only if selectedBranch is set)
Running (analysisLoading === true) ⚙️ Analyzing… button (yellow, disabled)
Pending (pendingAnalysisResult !== null) 📊 Display N prioritized actions button (green gradient, enabled)

Files to Change

App.tsx

  • Remove the <button onClick={wrappedRunAnalysis}> block (lines ~812–829) from the contingency selector card.
  • Pass onRunAnalysis={wrappedRunAnalysis} as a new prop to <ActionFeed>.
  • The selectedBranch value is already passed (indirectly via analysisLoading disable logic). Also pass canRunAnalysis={!!selectedBranch && !analysisLoading} so ActionFeed can enable/disable.

ActionFeed.tsx

  • Add props: onRunAnalysis: () => void, canRunAnalysis: boolean.
  • In the Suggested Actions section (around line ~916–956), replace the current conditional rendering with the unified three-state slot:
{/* Unified analysis action slot */}
<div style={{ padding: '10px 15px' }}>
  {analysisLoading ? (
    <button disabled style={{ /* yellow processing style */ }}>
      ⚙️ Analyzing…
    </button>
  ) : pendingAnalysisResult ? (
    <button onClick={onDisplayPrioritizedActions} style={{ /* green gradient */ }}>
      📊 Display {count} prioritized actions
    </button>
  ) : (
    <button onClick={onRunAnalysis} disabled={!canRunAnalysis} style={{ /* green style */ }}>
      🔍 Analyze & Suggest
    </button>
  )}
</div>

standalone_interface.html

  • Apply the same relocation: remove the analysis button from the contingency selector area and add it into the action feed panel header.

Test Files

  • Update App.settings.test.tsx and App.session.test.tsx: the '🚀 Run Analysis' text selector changes to '🔍 Analyze & Suggest'. Locate the button inside the action feed panel instead of the sidebar header.

2. Always-Visible Visualization Tabs with Placeholder Messages

Current State

In VisualizationPanel.tsx (lines ~808–858), tabs are conditionally rendered:

  • Network (N): Always shown
  • Contingency (N-1): Only when selectedBranch is set
  • Action: Only when selectedActionId is set
  • Overflow Analysis: Only when result?.pdf_url exists

Target State

All four tabs are always visible. When a tab's content is not yet available, clicking it shows a placeholder message guiding the user on what to do. Additionally, the Action tab dynamically updates its label to include the selected action ID.

Tab Definitions

Tab Label (default) Label (populated) Placeholder message
N Network (N) (always populated after config) Configure a network path in Settings to view the base-case diagram.
N-1 Contingency (N-1) Select a contingency element from the dropdown above to view the N-1 state.
Action Remedial Action Remedial Action: {actionId} Select an action card from the suggestions panel to view its effect on the network.
Overflow Overflow Analysis Run an analysis to generate the overflow graph.

Tab Styling

  • Populated tabs: Current active/inactive styling (bold + colored bottom border when active).
  • Unpopulated tabs: Slightly dimmer text (#aab), italic label, no colored border even when active. Still clickable.
  • Active but unpopulated: White background + placeholder message in content area (centered, grey italic text with a subtle icon).

Files to Change

VisualizationPanel.tsx

  • Remove conditional rendering of tab buttons. Render all four always.
  • Add availability flags:
    const tabAvailable = {
      'n': !!nDiagram?.svg,
      'n-1': !!n1Diagram?.svg,
      'action': !!actionDiagram?.svg,
      'overflow': !!result?.pdf_url,
    };
    
  • Update the Action tab label:
    const actionTabLabel = selectedActionId
      ? `Remedial Action: ${selectedActionId}`
      : 'Remedial Action';
    
    Truncate long action IDs with CSS text-overflow: ellipsis (max ~200px) and show full ID on hover via title attribute.
  • In the tab content area, wrap each tab's body:
    {activeTab === 'action' && (
      tabAvailable['action']
        ? <MemoizedSvgContainer ... />
        : <TabPlaceholder message="Select an action card from the suggestions panel to view its effect on the network." />
    )}
    
  • Add a small TabPlaceholder component (inline or local):
    const TabPlaceholder: React.FC<{ message: string }> = ({ message }) => (
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        height: '100%', color: '#999', fontStyle: 'italic', padding: '40px',
        textAlign: 'center', fontSize: '0.95rem'
      }}>
        {message}
      </div>
    );
    

standalone_interface.html

  • Mirror the same logic: always render all 4 tab buttons, show placeholder text when content is unavailable.

App.tsx

  • Remove any logic that prevents switching to a tab when its content is unavailable (if present). The onTabChange handler should allow selecting any tab at any time.

3. Collapsible Voltage Filter

Current State

The voltage filter is a vertical sidebar on the right side of the visualization panel (VisualizationPanel.tsx lines ~1074–1125, CSS class .voltage-sidebar). It is always visible when uniqueVoltages.length > 1, consuming ~62px of horizontal space.

Target State

The voltage filter sidebar is collapsed by default, showing only a small toggle button. When expanded, it shows the full slider UI as today.

Collapsed State

  • A small vertical button on the right edge: ▸ kV (or a filter icon from lucide-react).
  • Width: ~24px, just enough for the icon/label.
  • Click expands the filter.

Expanded State

  • Full current sidebar (62px width) with an additional collapse button ( or ) at the top.
  • Clicking the collapse button or clicking outside collapses it back.

Files to Change

VisualizationPanel.tsx

  • Add local state: const [voltageFilterExpanded, setVoltageFilterExpanded] = useState(false);
  • Wrap the existing voltage sidebar in a conditional:
    {uniqueVoltages.length > 1 && (
      voltageFilterExpanded ? (
        <div className="voltage-sidebar">
          <button onClick={() => setVoltageFilterExpanded(false)}
            style={{ alignSelf: 'flex-end', background: 'none', border: 'none', cursor: 'pointer', fontSize: '14px' }}>
    
          </button>
          {/* ... existing slider content ... */}
        </div>
      ) : (
        <button
          className="voltage-sidebar-toggle"
          onClick={() => setVoltageFilterExpanded(true)}
          title="Show voltage filter"
        >
          <span style={{ writingMode: 'vertical-rl' }}>kV ▸</span>
        </button>
      )
    )}
    

App.css

  • Add .voltage-sidebar-toggle style:
    .voltage-sidebar-toggle {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      width: 24px;
      background: rgba(244, 244, 244, 0.85);
      border: none;
      border-left: 1px solid #ccc;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 15;
      color: #666;
      font-size: 12px;
    }
    .voltage-sidebar-toggle:hover {
      background: rgba(230, 230, 230, 0.95);
      color: #333;
    }
    

standalone_interface.html

  • Add equivalent collapse/expand toggle with the same CSS and JS logic.

4. Revised Color Code for Highlights

Current State

Element NAD Color SLD Color CSS Class
Action targets Yellow #fffb00 Yellow #fffb00 .nad-action-target, .sld-highlight-action
Contingency Orange #ff9800 Orange #ff9800 .nad-contingency-highlight, .sld-highlight-contingency
Overloads Orange #ff8c00 Orange #ff8c00 dashed .nad-overloaded, .sld-highlight-overloaded
Breakers/switches Purple #e040fb .sld-highlight-breaker

Problem: Contingency and overloads both use orange, making them hard to distinguish. Actions use yellow which doesn't contrast well with orange on bright backgrounds.

Target Color Scheme

Element New Color Hex Rationale
Remedial Actions Purple-pink #e040fb Distinctive, stands out against network blues/greens
Contingency Yellow #f5c542 Warm "warning" tone, clearly different from orange
Overloads Orange #ff8c00 Kept — strong "danger" association, universally understood
Breakers/switches Purple (lighter) #ce93d8 Softer shade to distinguish from action purple-pink

Visual Hierarchy

  🟣 Purple-pink (#e040fb)  →  Actions (what the operator chose)
  🟡 Yellow (#f5c542)       →  Contingency (the triggering event)
  🟠 Orange (#ff8c00)       →  Overloads (the problem to solve)
  🟣 Light purple (#ce93d8) →  Breakers (topology detail)

Files to Change

App.css — NAD Highlights

Action targets: Yellow → Purple-pink

/* Before */
.nad-action-target {
    filter: drop-shadow(0 0 8px #fffb00) drop-shadow(0 0 15px #fffb00) !important;
}
.nad-action-target path, .nad-action-target line, ... {
    stroke: #fffb00 !important;
}
.nad-action-target circle, .nad-action-target rect {
    stroke: #ffe600 !important;
    fill: #ffe600 !important;
}

/* After */
.nad-action-target {
    filter: drop-shadow(0 0 8px #e040fb) drop-shadow(0 0 15px #e040fb) !important;
}
.nad-action-target path, .nad-action-target line, ... {
    stroke: #e040fb !important;
}
.nad-action-target circle, .nad-action-target rect {
    stroke: #e040fb !important;
    fill: #e040fb !important;
    fill-opacity: 0.6 !important;
}

Contingency: Orange → Yellow

/* Before */
.nad-contingency-highlight {
    filter: drop-shadow(0 0 8px #ff9800) drop-shadow(0 0 15px #ff9800) !important;
}
.nad-contingency-highlight path, ... {
    stroke: #ff9800 !important;
}

/* After */
.nad-contingency-highlight {
    filter: drop-shadow(0 0 8px #f5c542) drop-shadow(0 0 15px #f5c542) !important;
}
.nad-contingency-highlight path, ... {
    stroke: #f5c542 !important;
}
.nad-contingency-highlight circle, .nad-contingency-highlight rect {
    stroke: #f5c542 !important;
    fill: #f5c542 !important;
}

Overloads: No change (already orange #ff8c00). But add NAD highlight support for overloads using the same clone-based glow pattern used for contingency and actions, so overloaded lines get a visible orange halo instead of just a stroke-width bump.

App.css — SLD Highlights

/* Action: yellow → purple-pink */
.sld-highlight-clone.sld-highlight-action {
    filter: drop-shadow(0 0 4px #e040fb) drop-shadow(0 0 8px #e040fb);
}
.sld-highlight-clone.sld-highlight-action path, ... {
    stroke: #e040fb !important;
}

/* Contingency: orange → yellow */
.sld-highlight-clone.sld-highlight-contingency {
    filter: drop-shadow(0 0 4px #f5c542) drop-shadow(0 0 8px #f5c542);
}
.sld-highlight-clone.sld-highlight-contingency path, ... {
    stroke: #f5c542 !important;
}

/* Overloads: unchanged (orange #ff8c00, dashed) */

/* Breakers: bold purple → lighter purple */
.sld-highlight-clone.sld-highlight-breaker {
    filter: drop-shadow(0 0 3px #ce93d8) drop-shadow(0 0 6px #ce93d8);
}
.sld-highlight-clone.sld-highlight-breaker path, ... {
    stroke: #ce93d8 !important;
}

App.css — Overload Highlight Enhancement

Add clone-based glow for overloaded lines (currently overloads only get a stroke-width increase, no halo/glow like actions and contingency do):

.nad-overloaded {
    filter: drop-shadow(0 0 6px #ff8c00) drop-shadow(0 0 12px #ff8c00) !important;
}

svgUtils.ts — Overload Highlighting

Currently highlightOverloadedLines() (line ~186) only adds the nad-overloaded class to existing elements. To match the clone-based approach used by actions and contingency (for consistent glow rendering), consider upgrading it to also create cloned background elements with the .nad-overloaded class. This gives overloads the same visual treatment (halo behind the original element).

Alternatively, the simpler CSS-only filter: drop-shadow() addition above may be sufficient if the existing stroke-width increase already makes overloads visible enough. Start with the CSS-only approach and upgrade to clones only if needed.

standalone_interface.html

  • Update all matching CSS rules with the same color changes.
  • The standalone file has its own embedded <style> block — search for #fffb00, #ff9800, #e040fb and apply the same replacements.

cssRegression.test.ts

  • Update expected color values in assertions:
    • nad-action-target → expect #e040fb instead of #fffb00
    • nad-contingency-highlight → expect #f5c542 instead of #ff9800
    • standalone equivalents likewise

Color Legend

Consider adding a small legend to the visualization panel (bottom-left corner, semi-transparent) so users understand the color coding:

 ● Purple-pink — Remedial action targets
 ● Yellow — Contingency element
 ● Orange — Overloaded lines

This is optional but recommended for discoverability.


Implementation Order

Recommended sequence (each item is independently deployable):

  1. Color code revision (item 4) — CSS-only changes, low risk, high visual impact. Update tests.
  2. Collapsible voltage filter (item 3) — Self-contained UI change, no cross-component dependencies.
  3. Always-visible tabs (item 2) — Moderate refactor of VisualizationPanel tab rendering.
  4. Move analysis button (item 1) — Cross-component change (App.tsx ↔ ActionFeed.tsx), needs test updates.

Summary of All Files Affected

File Changes
frontend/src/App.tsx Remove analysis button from contingency card, pass new props to ActionFeed
frontend/src/components/ActionFeed.tsx Add onRunAnalysis/canRunAnalysis props, unified three-state button slot
frontend/src/components/VisualizationPanel.tsx Always-visible tabs with placeholders, dynamic action tab label, collapsible voltage filter
frontend/src/App.css Color scheme update (actions → purple-pink, contingency → yellow), overload glow, voltage toggle style
frontend/src/utils/svgUtils.ts (Optional) Upgrade overload highlighting to clone-based approach
frontend/src/utils/cssRegression.test.ts Update expected color values
frontend/src/App.settings.test.tsx Update button text selector
frontend/src/App.session.test.tsx Update button text selector
standalone_interface.html Mirror all four changes in embedded CSS/JS