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

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