Files
resolutionflow/frontend/src/store/scriptGeneratorStore.ts
chihlasm 1637d7de8f fix: preserve tab ownership filter across category and search changes in script library
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>
2026-04-01 05:57:01 +00:00

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,
})
},
}))