Files
resolutionflow/docs/superpowers/specs/2026-03-13-script-generator-phase2-design.md
2026-03-13 01:05:39 -04:00

11 KiB

Script Generator Phase 2 — Frontend Design

Date: 2026-03-13 Status: Approved Phase: 2 of Script Generator feature Builds on: Phase 1 backend (feat/script-generator PR #105)


Goal

Build the Script Library frontend: a three-panel page where MSP engineers can browse PowerShell script templates by category, fill in parameters, get a live preview, and generate + copy/download the final script.


Architecture

Layout: Top filter bar + two-column layout (C). Category tabs and search across the top, template list on the left, generator panel on the right. Follows the existing glassmorphism design system.

State: Zustand store (scriptGeneratorStore) chosen over a local hook because script generation will soon be embeddable in session execution (Script Output Node). A store allows any component anywhere in the tree to read/write generation state without prop drilling. Survives navigation.

Preview: Client-side lightweight substitution for live preview as the user types ({{variable_name}} replacement only). The real POST /scripts/generate endpoint is called on Generate — it applies filters (as_secure_string, as_array, as_bool), conditionals, and PowerShell-safe sanitization.


Zustand Store — scriptGeneratorStore

File: frontend/src/store/scriptGeneratorStore.ts

State shape

interface ScriptGeneratorState {
  // Template browsing
  categories: ScriptCategoryResponse[];
  templates: ScriptTemplateListItem[];
  selectedTemplate: ScriptTemplateDetail | null;
  searchQuery: string;
  activeCategoryId: string | null;
  isLoadingTemplates: boolean;

  // Form
  paramValues: Record<string, string>;       // keyed by variable_name
  formErrors: Record<string, string>;        // keyed by variable_name

  // Output
  generatedScript: string | null;
  generationId: string | null;
  isGenerating: boolean;
  generateError: string | null;

  // Actions
  loadCategories: () => Promise<void>;
  loadTemplates: () => Promise<void>;
  selectTemplate: (id: string) => Promise<void>;
  setCategory: (id: string | null) => void;
  setSearch: (query: string) => void;
  setParamValue: (variableName: string, value: string) => void;
  generate: (sessionId?: string) => Promise<void>;
  clearOutput: () => void;
  reset: () => void;
}

Behaviour notes

  • setCategory and setSearch both call loadTemplates() after updating state — they compose (category + search both applied to the API call)
  • selectTemplate fetches the full ScriptTemplateDetail (including parameters_schema and script_template), then calls reset() to clear previous form/output state
  • reset() clears paramValues, formErrors, generatedScript, generationId, generateError — does not clear template selection or browsing state
  • generate() validates required params client-side first (populates formErrors), then calls POST /scripts/generate

Types

Added to frontend/src/types/index.ts:

export interface ScriptCategoryResponse {
  id: string;
  name: string;
  slug: string;
  description: string | null;
  template_count: number;
}

export interface ScriptTemplateListItem {
  id: string;
  category_id: string;
  name: string;
  description: string | null;
  script_complexity: 'simple' | 'moderate' | 'complex';
  usage_count: number;
  tags: string[];
}

export interface ScriptParameter {
  variable_name: string;
  label: string;
  field_type: 'text' | 'password' | 'select' | 'multiselect' | 'checkbox' | 'number';
  required: boolean;
  default_value?: string;
  options?: string[];
  is_sensitive: boolean;
  display_order: number;
  help_text?: string;
}

export interface ScriptParametersSchema {
  parameters: ScriptParameter[];
}

export interface ScriptTemplateDetail extends ScriptTemplateListItem {
  parameters_schema: ScriptParametersSchema;
  script_template: string;
}

export interface ScriptGenerateRequest {
  template_id: string;
  parameters: Record<string, string>;
  session_id?: string;
}

export interface ScriptGenerateResponse {
  generation_id: string;
  generated_script: string;
  template_name: string;
}

API Client

File: frontend/src/api/scripts.ts

getCategories(): Promise<ScriptCategoryResponse[]>
getTemplates(params?: { category_id?: string; search?: string }): Promise<ScriptTemplateListItem[]>
getTemplateDetail(id: string): Promise<ScriptTemplateDetail>
generate(req: ScriptGenerateRequest): Promise<ScriptGenerateResponse>
getGenerations(): Promise<ScriptGenerationRecord[]>

All methods use the existing apiClient (base URL /api/v1, auth interceptor handles token refresh).

Exported from frontend/src/api/index.ts as scriptsApi.


Component Tree

ScriptLibraryPage                    pages/ScriptLibraryPage.tsx
├── ScriptFilterBar                  components/scripts/ScriptFilterBar.tsx
├── ScriptTemplateList               components/scripts/ScriptTemplateList.tsx
│   └── TemplateCard                 components/scripts/TemplateCard.tsx
└── ScriptGeneratorPanel             components/scripts/ScriptGeneratorPanel.tsx
    ├── ScriptParameterForm          components/scripts/ScriptParameterForm.tsx
    │   └── ScriptParameterField     components/scripts/ScriptParameterField.tsx
    └── ScriptPreview                components/scripts/ScriptPreview.tsx
        └── PowerShellHighlighter    components/scripts/PowerShellHighlighter.tsx

Component responsibilities

ScriptLibraryPage

  • Bootstraps store on mount: calls loadCategories() then loadTemplates()
  • Renders two-column layout (template list left, generator panel right)
  • Renders ScriptFilterBar above the two columns
  • No business logic — pure layout + bootstrap

ScriptFilterBar

  • Category tabs (pill style, bg-primary/10 + cyan accent on active)
  • Search input (debounced 300ms) — searches names, descriptions, and tags (backend handles it)
  • Reads categories, activeCategoryId, searchQuery from store
  • Calls setCategory and setSearch

ScriptTemplateList

  • Scrollable list of TemplateCard components
  • Reads templates, isLoadingTemplates, selectedTemplate from store
  • Shows skeleton loaders while loading
  • Shows empty state when no templates match

TemplateCard

  • Displays: name, description (truncated to 2 lines), complexity badge, usage count, tags
  • Active state: bg-primary/10 background + 3px left cyan accent bar (matches existing nav active pattern)
  • Calls selectTemplate(template.id) on click
  • Complexity badge colors: simple → emerald-400, moderate → amber-400, complex → rose-500

ScriptGeneratorPanel

  • Shows placeholder ("Select a template to get started") when selectedTemplate is null
  • When template selected: renders template name/description header, ScriptParameterForm, ScriptPreview, and action bar
  • Action bar: Generate button (bg-gradient-brand) + Download .ps1 button
  • Download triggers new Blob([generatedScript], { type: 'text/plain' }) → anchor click
  • Shows generateError inline below Generate button
  • Viewers see Generate button disabled with tooltip "Engineer access required" (checked via usePermissions())

ScriptParameterForm

  • Iterates selectedTemplate.parameters_schema.parameters sorted by display_order
  • Renders a ScriptParameterField per parameter
  • Client-side required validation on Generate (marks formErrors in store)
  • Disabled entirely for viewers

ScriptParameterField

  • Renders input by field_type: text → <Input>, password → <Input type="password">, select → <select>, checkbox → <input type="checkbox">, number → <Input type="number">
  • Shows help_text as a small muted line below the field
  • Shows formErrors[variable_name] as an inline error
  • Password fields show a show/hide toggle
  • Calls setParamValue(variable_name, value) on change

ScriptPreview

  • Two modes:
    • Draft mode (before Generate): client-side substitution of {{variable_name}} in selectedTemplate.script_template using current paramValues. Unfilled params render as {{variable_name}} placeholders (visually dimmed).
    • Generated mode (after Generate): shows generatedScript from store
  • Copy icon (Lucide Copy) in top-right corner of code block — calls navigator.clipboard.writeText(); shows a brief "Copied!" tooltip on success
  • Passes script string to PowerShellHighlighter

PowerShellHighlighter

  • Pure component: ({ script: string }) => JSX.Element
  • Regex-based syntax highlighting (no external library):
    • Comments (#...) → text-[#8b949e]
    • Cmdlets (Verb-Noun pattern) → text-[#22d3ee] (cyan)
    • String literals ("...", '...') → text-[#a5d6ff]
    • Variables ($VarName) → text-[#79c0ff]
    • Parameters (-ParamName) → text-[#d2a8ff] (purple)
    • Keywords (if, foreach, function, etc.) → text-[#ff7b72]
    • Unfilled placeholders ({{variable_name}}) → text-amber-400 with dashed underline
  • Renders as <pre><code> with font-label (JetBrains Mono), bg-card, rounded corners

Routing & Navigation

  • Route: /scripts added to frontend/src/router.tsx inside the ProtectedRoute/AppLayout children
  • Sidebar nav entry: "Scripts" with a Terminal icon (Lucide), grouped under the main nav
  • No sub-routes needed for Phase 2

Permissions

Action Minimum role
View Script Library page Any authenticated user
Browse templates, see preview Any authenticated user
Fill form, generate, copy, download Engineer or above

Implemented via usePermissions() hook. Viewer-blocking applied to: ScriptParameterForm (disabled), Generate button (disabled + tooltip), Download button (disabled + tooltip).


Search Behaviour

  • Search query sent as ?search= param to GET /scripts/templates
  • Backend searches name, description, and tags (already implemented in Phase 1)
  • Debounced 300ms in ScriptFilterBar before calling setSearch
  • Category filter and search compose: both params sent simultaneously

Empty & Loading States

Scenario Treatment
Templates loading Skeleton cards (3 placeholder TemplateCards)
No templates in category Illustration + "No templates found" message
No search results "No templates match your search" with clear button
No template selected Right panel: centered placeholder with Terminal icon + "Select a template to get started"
Generating Generate button shows spinner, disabled
Generate error Inline error text below Generate button in rose-500

Out of Scope (Phase 2)

  • Session-embedded script generation (Script Output Node) — Phase 3
  • Template creation/editing UI — admin-only, deferred
  • Generation history page — deferred
  • Admin template management — deferred
  • Script execution / RMM integration — long-term roadmap