PluginProbe
Extendify / 3.0.6
Extendify v3.0.6
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 / suggestions.js

suggestions.js in Extendify 3.0.6, at src/Agent/state/suggestions.js

134 lines 4.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { create } from 'zustand';
2 import { devtools, persist } from 'zustand/middleware';
3
4 const pluginSuggestions = window.extAgentData?.suggestions || [];
5
6 const state = (set, get) => ({
7 suggestions: pluginSuggestions,
8 addSuggestions: (suggestions) => {
9 const mapped = (Array.isArray(suggestions) ? suggestions : [])
10 .filter((s) => s.workflowId && s.label)
11 .map((s) => ({
12 workflowId: s.workflowId,
13 message: s.label,
14 icon: s.icon ?? 'sparkle',
15 source: s.source ?? 'workflow',
16 available: s.available,
17 addedAt: Date.now(),
18 seenAt: null,
19 clickedAt: null,
20 }));
21 if (mapped.length === 0) return;
22 set((prev) => {
23 // Remove any duplicated suggestions based on the message.
24 const messagesSet = new Set(mapped.map((s) => s.message));
25 const filtered = prev.suggestions.filter(
26 (item) => !messagesSet.has(item.message),
27 );
28 return { suggestions: [...mapped, ...filtered] };
29 });
30 },
31 // Returns sorted suggestions for display.
32 // Note: workflow suggestions are not persistent by design, they are removed from the state when seen.
33 // Sort order: workflow first (newest added), then plugin (unseen first, then oldest seen).
34 getNextSuggestions: ({ exclude = [] } = {}) => {
35 const excludeIds = new Set(exclude.map((s) => s.workflowId));
36 const { suggestions } = get();
37 return (
38 suggestions
39 // Exclude any passed in
40 .filter((i) => !excludeIds.has(i.workflowId))
41 // Remove any that arent available
42 .filter((s) => get().isAvailable(s))
43 // Oldest seen first.
44 .toSorted((a, b) => (a.seenAt ?? 0) - (b.seenAt ?? 0))
45 // Unseen before seen.
46 .toSorted((a, b) => (a.seenAt ? 1 : 0) - (b.seenAt ? 1 : 0))
47 // Workflows sorted by newest added.
48 .toSorted((a, b) => {
49 if ([a.source, b.source].includes('plugin')) return 0;
50 return (b.addedAt ?? 0) - (a.addedAt ?? 0);
51 })
52 // Workflows before plugins.
53 .toSorted(
54 (a, b) =>
55 (a.source === 'plugin' ? 1 : 0) - (b.source === 'plugin' ? 1 : 0),
56 )
57 );
58 },
59 // Returns the top N suggestions and marks them as seen.
60 getSuggestions: ({ slice = 3, exclude = [] } = {}) => {
61 const results = get().getNextSuggestions({ exclude }).slice(0, slice);
62 get().markAsSeen(results);
63 return results;
64 },
65 isAvailable: (s) => {
66 const { context, abilities } = window.extAgentData ?? {};
67 const { context: reqContext, abilities: reqAbilities } = s.available ?? {};
68
69 // Skip ability checks if abilities are not yet known.
70 if (reqAbilities && !reqAbilities.every((key) => abilities?.[key])) {
71 return false;
72 }
73
74 // If not context check were good.
75 if (!reqContext) return true;
76
77 const checks = Array.isArray(reqContext)
78 ? // If it's an array, truthy check
79 reqContext.every((key) => context?.[key])
80 : // If it's an object, check the value
81 Object.entries(reqContext).every(
82 ([key, val]) => context?.[key] === val,
83 );
84
85 return checks;
86 },
87 markAsSeen: (items) => {
88 const messages = new Set(items.map((s) => s.message));
89 set((prev) => ({
90 suggestions: prev.suggestions.map((s) => {
91 if (s.seenAt || !messages.has(s.message)) return s;
92 return { ...s, seenAt: Date.now() };
93 }),
94 }));
95 },
96 markAsClicked: (suggestion) =>
97 set((prev) => ({
98 suggestions: prev.suggestions.map((item) => {
99 if (item.message !== suggestion.message) return item;
100 return { ...item, clickedAt: Date.now() };
101 }),
102 })),
103 });
104
105 export const useSuggestionsStore = create()(
106 persist(
107 devtools(state, {
108 name: 'Extendify Agent Suggestions',
109 enabled: window.extSharedData.devbuild,
110 }),
111 {
112 name: `extendify-agent-suggestions-${window.extSharedData.siteId}`,
113 // Reconcile persisted state with fresh plugin suggestions on hydration.
114 merge: (persisted, current) => {
115 const persistedSuggestions = persisted?.suggestions ?? [];
116
117 // 1. for plugin suggestions, the plugin is the source of truth,
118 // but we keep any dynamic state
119 // 2. This also filters out the workflow suggestions and any others
120 // which we might want to have an expire prop instead
121 const suggestions = pluginSuggestions.map((s) => {
122 const p = persistedSuggestions.find((ps) => ps.id === s.id);
123 return {
124 ...s,
125 seenAt: p?.seenAt,
126 clickedAt: p?.clickedAt,
127 };
128 });
129 return { ...current, suggestions };
130 },
131 },
132 ),
133 );
134