- Add language field to ScriptTemplateListItem frontend type - Add mine/shared params to scriptsApi.getTemplates - Update scriptGeneratorStore.loadTemplates to accept filter params - Reorganize ScriptLibraryPage with tab bar (My Scripts / Team Scripts) - Add "Build a New Script" button linking to /script-builder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
195 lines
5.9 KiB
TypeScript
195 lines
5.9 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"
|
|
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,
|
|
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) => {
|
|
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 (filters?.mine) params.mine = true
|
|
if (filters?.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,
|
|
})
|
|
},
|
|
}))
|