PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / state / workflows.js

workflows.js in Extendify 3.1.5, at src/Agent/state/workflows.js

157 lines 5.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { isAbilityWorkflow } from '@agent/lib/abilities';
2 import { isChangeSiteDesignWorkflowAvailable } from '@agent/lib/util';
3 import { AbilityRun } from '@agent/workflows/abilities/components/run';
4 import changeSiteDesignWorkflow from '@agent/workflows/theme/change-site-design';
5 import variationsWorkflow from '@agent/workflows/theme/change-theme-variation';
6 import { abilityWorkflows, workflows } from '@agent/workflows/workflows';
7 import { useQuickEditStore } from '@quick-edit/state/store';
8 import { deepMerge } from '@shared/lib/utils';
9 import { create } from 'zustand';
10 import { devtools, persist } from 'zustand/middleware';
11
12 const onboarding = window.extAgentData.chatHistory?.length === 0;
13 const agentResponse = isChangeSiteDesignWorkflowAvailable()
14 ? changeSiteDesignWorkflow.example?.agentResponse
15 : variationsWorkflow.example?.agentResponse;
16 const onboardingToolProps = {
17 ...agentResponse.whenFinishedTool,
18 agentResponse,
19 };
20
21 // Used to check case-insensitive matches for workflow examples
22 const collator = new Intl.Collator(undefined, {
23 sensitivity: 'base',
24 usage: 'search',
25 });
26
27 const state = (set, get) => ({
28 workflow: null,
29 // Data for the tool component that shows up at the end of a workflow
30 whenFinishedToolProps: null,
31 getWorkflow: () => {
32 const curr = get().workflow;
33 // Workflows may define a "parent" workflow via templateId
34 const currId = curr?.templateId || curr?.id;
35 const wf = workflows.find(({ id }) => id === currId);
36 if (!wf?.id) {
37 if (!curr) return null;
38 if (!isAbilityWorkflow(curr.id)) return curr;
39 // The backend never sends whenFinished, so this supplies the gate.
40 const ability = deepMerge(
41 { whenFinished: { component: AbilityRun } },
42 abilityWorkflows[curr.id] ?? {},
43 );
44 return { ...deepMerge(curr, ability), id: curr.id };
45 }
46 return { ...deepMerge(curr, wf || {}), id: curr?.id };
47 },
48 getWorkflowByExample: (example) => {
49 const wf = workflows.find(
50 ({ example: ex }) => collator.compare(ex?.text, example) === 0,
51 );
52 if (!wf?.id) return null;
53 if (!wf.available()) return null;
54 return wf;
55 },
56 // Gets the workflows available to the user
57 // TODO: maybe we need to have a way to include a
58 // workflow regardless of the block being active?
59 getAvailableWorkflows: () => {
60 const wfs = workflows.filter(({ available }) => available());
61 // If a block is set, only include those with 'block'
62 const blockWorkflows = wfs.filter(({ requires }) =>
63 requires?.includes('block'),
64 );
65 if (useQuickEditStore.getState().agentBlock) return blockWorkflows;
66 // otherwise remove all of the above
67 return wfs.filter(({ id }) => !blockWorkflows.some((w) => w.id === id));
68 },
69 getWorkflowsByFeature: ({ requires } = {}) => {
70 if (!requires) return workflows.filter(({ available }) => available());
71 // e.g. requires: ['block']
72 return workflows.filter(
73 ({ available, requires: workflowRequires }) =>
74 available() &&
75 (!requires || workflowRequires?.some((s) => requires.includes(s))),
76 );
77 },
78 workflowData: null,
79 mergeWorkflowData: (data) => {
80 set((state) => {
81 if (!state.workflowData) return { workflowData: data };
82 return {
83 workflowData: { ...state.workflowData, ...data },
84 };
85 });
86 },
87 setWorkflow: (workflow) => {
88 const agentBlockCode = useQuickEditStore.getState().agentBlockCode;
89 const started = workflow
90 ? { ...workflow, startingPage: window.location.href }
91 : null;
92 set({
93 workflow: started,
94 // If a block is selected, add it to the workflow data
95 // previousContent is named this way for legacy reasons
96 workflowData: agentBlockCode ? { previousContent: agentBlockCode } : null,
97 whenFinishedToolProps: null,
98 });
99 if (!started) return;
100 const workflowStarted = get().getWorkflow();
101 window.dispatchEvent(
102 new CustomEvent('extendify-workflow::started', {
103 detail: workflowStarted,
104 }),
105 );
106 try {
107 workflowStarted?.onStarted?.(workflowStarted);
108 } catch {
109 // A workflow that can't read the page still has to start.
110 }
111 },
112 setWhenFinishedToolProps: (whenFinishedToolProps) =>
113 set({ whenFinishedToolProps }),
114 // Declared up front it would hide the workflow while nothing is staged.
115 // Not setWorkflow: that resets workflowData and drops the run's data.
116 requireBlock: () =>
117 set((state) =>
118 state.workflow
119 ? { workflow: { ...state.workflow, requires: ['block'] } }
120 : {},
121 ),
122 });
123
124 export const useWorkflowStore = create()(
125 persist(devtools(state, { name: 'Extendify Agent Workflows' }), {
126 name: `extendify-agent-workflows-${window.extSharedData.siteId}`,
127 merge: (persistedState, currentState) => {
128 // if we are in onboarding mode, add the starting workflow
129 if (onboarding) {
130 return {
131 ...currentState,
132 ...persistedState,
133 workflow: isChangeSiteDesignWorkflowAvailable()
134 ? changeSiteDesignWorkflow
135 : variationsWorkflow,
136 whenFinishedToolProps: onboardingToolProps,
137 };
138 }
139 const merged = { ...currentState, ...persistedState };
140 // A reload can't carry the staged block a block-patching workflow
141 // depends on, so a rehydrated-open one is always stale. A canvas
142 // session is stale too: its field values never persist.
143 const id = merged.workflow?.templateId || merged.workflow?.id;
144 const registered = workflows.find((wf) => wf.id === id);
145 if (id === 'block-patching' || registered?.whenFinished?.canvas) {
146 return {
147 ...merged,
148 workflow: null,
149 whenFinishedToolProps: null,
150 workflowData: null,
151 };
152 }
153 return merged;
154 },
155 }),
156 );
157