PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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.2.1, at src/Agent/state/suggestions.js

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