From 9d10aa90ad745454c8846305ab3f9339debb8a6b Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 13 Mar 2026 02:01:02 -0400 Subject: [PATCH] feat: add scriptGeneratorStore Zustand store Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/store/scriptGeneratorStore.ts | 180 +++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 frontend/src/store/scriptGeneratorStore.ts diff --git a/frontend/src/store/scriptGeneratorStore.ts b/frontend/src/store/scriptGeneratorStore.ts new file mode 100644 index 00000000..fb6dadf5 --- /dev/null +++ b/frontend/src/store/scriptGeneratorStore.ts @@ -0,0 +1,180 @@ +import { create } from 'zustand' +import { scriptsApi } from '@/api' +import type { + ScriptCategoryResponse, + ScriptTemplateListItem, + ScriptTemplateDetail, + ScriptParametersSchema, +} from '@/types' + +interface ScriptGeneratorState { + // Template browsing + categories: ScriptCategoryResponse[] + templates: ScriptTemplateListItem[] + selectedTemplate: ScriptTemplateDetail | null + searchQuery: string + activeCategoryId: string | null // null = "All" + isLoadingTemplates: boolean // drives skeleton in ScriptTemplateList + isLoadingDetail: boolean // drives spinner in ScriptGeneratorPanel + + // Form + paramValues: Record // keyed by ScriptParameter.key; booleans as 'true'/'false' + formErrors: Record // keyed by ScriptParameter.key + + // Output + generatedScript: string | null + generationId: string | null + generationWarnings: string[] + isGenerating: boolean + generateError: string | null + + // Actions + loadCategories: () => Promise + loadTemplates: () => Promise + selectTemplate: (id: string) => Promise + setCategory: (id: string | null) => void + setSearch: (query: string) => void + setParamValue: (key: string, value: string) => void + validate: () => boolean + generate: (sessionId?: string) => Promise + clearOutput: () => void + reset: () => void +} + +export const useScriptGeneratorStore = create()((set, get) => ({ + // Initial state + categories: [], + templates: [], + selectedTemplate: null, + searchQuery: '', + activeCategoryId: null, + isLoadingTemplates: false, + isLoadingDetail: false, + paramValues: {}, + formErrors: {}, + generatedScript: null, + generationId: null, + generationWarnings: [], + isGenerating: false, + generateError: null, + + loadCategories: async () => { + const categories = await scriptsApi.getCategories() + set({ categories }) + }, + + loadTemplates: async () => { + set({ isLoadingTemplates: true }) + const { activeCategoryId, categories, searchQuery } = get() + const category = categories.find(c => c.id === activeCategoryId) + const params: { category_slug?: string; search?: string } = {} + if (category) params.category_slug = category.slug + if (searchQuery) params.search = searchQuery + const templates = await scriptsApi.getTemplates(params) + set({ templates, isLoadingTemplates: false }) + }, + + selectTemplate: async (id: string) => { + set({ isLoadingDetail: true }) + const detail = await scriptsApi.getTemplateDetail(id) + // Populate paramValues from parameter defaults + const schema = detail.parameters_schema as ScriptParametersSchema + const parameters = schema?.parameters ?? [] + const paramValues: Record = {} + for (const param of parameters) { + const d = param.default + if (d === null || d === undefined) paramValues[param.key] = '' + else if (typeof d === 'boolean') paramValues[param.key] = d ? 'true' : 'false' + else paramValues[param.key] = String(d) + } + set({ + selectedTemplate: detail, + paramValues, + formErrors: {}, + generatedScript: null, + generationId: null, + generationWarnings: [], + generateError: null, + isLoadingDetail: false, + }) + }, + + setCategory: (id: string | null) => { + set({ activeCategoryId: id }) + get().loadTemplates() + }, + + setSearch: (query: string) => { + set({ searchQuery: query }) + get().loadTemplates() + }, + + setParamValue: (key: string, value: string) => { + set(state => ({ + paramValues: { ...state.paramValues, [key]: value }, + formErrors: { ...state.formErrors, [key]: '' }, // clear error on change + })) + }, + + validate: () => { + const { selectedTemplate, paramValues } = get() + if (!selectedTemplate) return true + const schema = selectedTemplate.parameters_schema as ScriptParametersSchema + const parameters = schema?.parameters ?? [] + const errors: Record = {} + for (const param of parameters) { + if (param.required && !paramValues[param.key]) { + errors[param.key] = `${param.label} is required` + } + } + set({ formErrors: errors }) + return Object.keys(errors).length === 0 + }, + + generate: async (sessionId?: string) => { + const { selectedTemplate, paramValues } = get() + if (!selectedTemplate) return + if (!get().validate()) return + + set({ isGenerating: true, generateError: null }) + try { + const response = await scriptsApi.generate({ + template_id: selectedTemplate.id, + parameters: paramValues, + ...(sessionId ? { session_id: sessionId } : {}), + }) + set({ + generatedScript: response.script, + generationId: response.id, + generationWarnings: response.warnings, + isGenerating: false, + }) + } catch (error: unknown) { + const axiosErr = error as { response?: { data?: { detail?: string } } } + const message = axiosErr.response?.data?.detail ?? 'Failed to generate script' + set({ generateError: message, isGenerating: false }) + } + }, + + clearOutput: () => { + set({ + generatedScript: null, + generationId: null, + generationWarnings: [], + generateError: null, + }) + }, + + // Exposed for Phase 3 callers (session execution context). + // Does NOT clear selectedTemplate, categories, templates, or browsing state. + reset: () => { + set({ + paramValues: {}, + formErrors: {}, + generatedScript: null, + generationId: null, + generationWarnings: [], + generateError: null, + }) + }, +}))