setCategory and setSearch called loadTemplates() with no arguments, dropping the mine/shared filter set by the active tab — causing the list to show all templates instead of the current user's scripts after any filter interaction. Store now persists tabFilters whenever loadTemplates is called with explicit filters, and reuses them for subsequent no-arg calls from setCategory/setSearch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
6.3 KiB
TypeScript
201 lines
6.3 KiB
TypeScript
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"
|
|
tabFilters: { mine?: boolean; shared?: boolean } // current tab's ownership filter
|
|
isLoadingTemplates: boolean // drives skeleton in ScriptTemplateList
|
|
isLoadingDetail: boolean // drives spinner in ScriptConfigurePane
|
|
|
|
// Form
|
|
paramValues: Record<string, string> // keyed by ScriptParameter.key; booleans as 'true'/'false'
|
|
formErrors: Record<string, string> // keyed by ScriptParameter.key
|
|
|
|
// Output
|
|
generatedScript: string | null
|
|
generationId: string | null
|
|
generationWarnings: string[]
|
|
isGenerating: boolean
|
|
generateError: string | null
|
|
|
|
// Actions
|
|
loadCategories: () => Promise<void>
|
|
loadTemplates: (filters?: { mine?: boolean; shared?: boolean }) => Promise<void>
|
|
selectTemplate: (id: string) => Promise<void>
|
|
setCategory: (id: string | null) => void
|
|
setSearch: (query: string) => void
|
|
setParamValue: (key: string, value: string) => void
|
|
validate: () => boolean
|
|
generate: (sessionId?: string) => Promise<void>
|
|
clearOutput: () => void
|
|
reset: () => void
|
|
}
|
|
|
|
export const useScriptGeneratorStore = create<ScriptGeneratorState>()((set, get) => ({
|
|
// Initial state
|
|
categories: [],
|
|
templates: [],
|
|
selectedTemplate: null,
|
|
searchQuery: '',
|
|
activeCategoryId: null,
|
|
tabFilters: {},
|
|
isLoadingTemplates: false,
|
|
isLoadingDetail: false,
|
|
paramValues: {},
|
|
formErrors: {},
|
|
generatedScript: null,
|
|
generationId: null,
|
|
generationWarnings: [],
|
|
isGenerating: false,
|
|
generateError: null,
|
|
|
|
loadCategories: async () => {
|
|
try {
|
|
const categories = await scriptsApi.getCategories()
|
|
set({ categories })
|
|
} catch {
|
|
// silently ignore — categories remain empty, UI degrades gracefully
|
|
}
|
|
},
|
|
|
|
loadTemplates: async (filters) => {
|
|
// When filters are provided (e.g. tab change), persist them so that
|
|
// subsequent setCategory/setSearch calls reuse the same ownership filter.
|
|
const resolvedFilters = filters !== undefined ? filters : get().tabFilters
|
|
if (filters !== undefined) set({ tabFilters: filters })
|
|
set({ isLoadingTemplates: true })
|
|
try {
|
|
const { activeCategoryId, categories, searchQuery } = get()
|
|
const category = categories.find(c => c.id === activeCategoryId)
|
|
const params: { category_slug?: string; search?: string; mine?: boolean; shared?: boolean } = {}
|
|
if (category) params.category_slug = category.slug
|
|
if (searchQuery) params.search = searchQuery
|
|
if (resolvedFilters.mine) params.mine = true
|
|
if (resolvedFilters.shared) params.shared = true
|
|
const templates = await scriptsApi.getTemplates(params)
|
|
set({ templates, isLoadingTemplates: false })
|
|
} catch {
|
|
set({ isLoadingTemplates: false })
|
|
}
|
|
},
|
|
|
|
selectTemplate: async (id: string) => {
|
|
set({ isLoadingDetail: true })
|
|
try {
|
|
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<string, string> = {}
|
|
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,
|
|
})
|
|
} catch {
|
|
set({ 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<string, string> = {}
|
|
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,
|
|
})
|
|
},
|
|
}))
|