# 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 ```typescript interface ScriptGeneratorState { // Template browsing categories: ScriptCategoryResponse[]; templates: ScriptTemplateListItem[]; selectedTemplate: ScriptTemplateDetail | null; searchQuery: string; activeCategoryId: string | null; isLoadingTemplates: boolean; // Form paramValues: Record; // keyed by variable_name formErrors: Record; // keyed by variable_name // Output generatedScript: string | null; generationId: string | null; isGenerating: boolean; generateError: string | null; // Actions loadCategories: () => Promise; loadTemplates: () => Promise; selectTemplate: (id: string) => Promise; setCategory: (id: string | null) => void; setSearch: (query: string) => void; setParamValue: (variableName: string, value: string) => void; generate: (sessionId?: string) => Promise; 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`: ```typescript 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; session_id?: string; } export interface ScriptGenerateResponse { generation_id: string; generated_script: string; template_name: string; } ``` --- ## API Client **File:** `frontend/src/api/scripts.ts` ```typescript getCategories(): Promise getTemplates(params?: { category_id?: string; search?: string }): Promise getTemplateDetail(id: string): Promise generate(req: ScriptGenerateRequest): Promise getGenerations(): Promise ``` 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 → ``, password → ``, select → ``, 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 `
` 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