@supericons/mcp 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/auth.js +69 -0
- package/converter.js +8 -0
- package/generated/mcp-output-locales.json +12436 -0
- package/generated/motion-lab-baseline.json +886 -0
- package/hosted-search-client.js +198 -0
- package/index.js +1240 -0
- package/material-export.js +174 -0
- package/mcp-output-localization.js +132 -0
- package/motion-lab-client.js +347 -0
- package/motion-lab.js +21 -0
- package/package.json +63 -0
- package/public/cjk-search-terms.json +63474 -0
- package/public/multilingual-search-aliases.json +4307 -0
- package/public/product-facts.json +25 -0
- package/recommend-icons.js +707 -0
- package/remote-server.js +465 -0
- package/runtime/cjk-search-core.js +82 -0
- package/runtime/converter-workflow.js +593 -0
- package/runtime/generated-search-intent-rules.js +1190 -0
- package/runtime/icon-semantic-aliases.js +330 -0
- package/runtime/icon-taxonomy-seed.js +461 -0
- package/runtime/public-metadata-sanitizer.js +171 -0
- package/runtime/search-intent-core.js +130 -0
- package/search.js +375 -0
- package/semantic-registry.js +212 -0
- package/server.json +27 -0
- package/telemetry.js +85 -0
- package/variant-support.js +236 -0
- package/workflow-access.js +65 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPublicSemanticPayload,
|
|
3
|
+
getSemanticRecordForIcon,
|
|
4
|
+
scoreSemanticAlignment,
|
|
5
|
+
} from './semantic-registry.js';
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import {
|
|
10
|
+
expandCjkQuery,
|
|
11
|
+
normalizeCjkSearchText,
|
|
12
|
+
} from './runtime/cjk-search-core.js';
|
|
13
|
+
import {
|
|
14
|
+
buildIntentQueryVariants,
|
|
15
|
+
buildSearchIntentProfile,
|
|
16
|
+
getIntentCandidateAdjustment,
|
|
17
|
+
} from './runtime/search-intent-core.js';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const cjkTermsPath = join(__dirname, 'public', 'cjk-search-terms.json');
|
|
21
|
+
const multilingualAliasesPath = join(__dirname, 'public', 'multilingual-search-aliases.json');
|
|
22
|
+
const cjkSearchTerms = existsSync(cjkTermsPath)
|
|
23
|
+
? JSON.parse(readFileSync(cjkTermsPath, 'utf8')).terms || []
|
|
24
|
+
: [];
|
|
25
|
+
const multilingualSearchAliases = existsSync(multilingualAliasesPath)
|
|
26
|
+
? JSON.parse(readFileSync(multilingualAliasesPath, 'utf8')).aliases || []
|
|
27
|
+
: [];
|
|
28
|
+
const multilingualExpansionTerms = [...cjkSearchTerms, ...multilingualSearchAliases];
|
|
29
|
+
|
|
30
|
+
const GENERIC_SLOT_WORDS = new Set([
|
|
31
|
+
'action',
|
|
32
|
+
'button',
|
|
33
|
+
'buttons',
|
|
34
|
+
'control',
|
|
35
|
+
'controls',
|
|
36
|
+
'icon',
|
|
37
|
+
'icons',
|
|
38
|
+
'item',
|
|
39
|
+
'items',
|
|
40
|
+
'nav',
|
|
41
|
+
'navigation',
|
|
42
|
+
'slot',
|
|
43
|
+
'tab',
|
|
44
|
+
'tabs',
|
|
45
|
+
'ui',
|
|
46
|
+
'view',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const VARIANT_PENALTIES = Object.freeze([
|
|
50
|
+
{ pattern: /circle/i, penalty: 5 },
|
|
51
|
+
{ pattern: /square/i, penalty: 4 },
|
|
52
|
+
{ pattern: /dash/i, penalty: 5 },
|
|
53
|
+
{ pattern: /badge/i, penalty: 4 },
|
|
54
|
+
{ pattern: /off/i, penalty: 6 },
|
|
55
|
+
{ pattern: /slash/i, penalty: 6 },
|
|
56
|
+
{ pattern: /warning/i, penalty: 4 },
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const COMMON_SLOT_PREFERENCE_RULES = Object.freeze([
|
|
60
|
+
{
|
|
61
|
+
slotPatterns: [
|
|
62
|
+
/profile/i,
|
|
63
|
+
/account/i,
|
|
64
|
+
/\buser\b/i,
|
|
65
|
+
/\busers\b/i,
|
|
66
|
+
/avatar/i,
|
|
67
|
+
/\u8d26\u6237|\u5e10\u6237|\u5e33\u6236|\u500b\u4eba\u8cc7\u6599|\u4e2a\u4eba\u8d44\u6599|\u7528\u6237|\u4f7f\u7528\u8005/u,
|
|
68
|
+
/\u30a2\u30ab\u30a6\u30f3\u30c8|\u30d7\u30ed\u30d5\u30a3\u30fc\u30eb|\u30e6\u30fc\u30b6\u30fc/u,
|
|
69
|
+
/\uacc4\uc815|\ud504\ub85c\ud544|\uc0ac\uc6a9\uc790/u,
|
|
70
|
+
/cuenta|perfil|usuario/i,
|
|
71
|
+
/konto|profil|benutzer/i,
|
|
72
|
+
/conta|perfil|usu[aá]rio|utilizador/i,
|
|
73
|
+
/\u0627\u0644\u062d\u0633\u0627\u0628|\u0645\u0644\u0641\s+\u0634\u062e\u0635\u064a|\u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645/u,
|
|
74
|
+
/\u0916\u093e\u0924\u093e|\u092a\u094d\u0930\u094b\u092b\u093c\u093e\u0907\u0932|\u092a\u094d\u0930\u094b\u092b\u093e\u0907\u0932|\u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e/u,
|
|
75
|
+
/t[aà]i kho[aả]n|tai khoan|h[oồ] s[oơ]|ho so|ng[uư][oờ]i d[uù]ng|nguoi dung/i,
|
|
76
|
+
/\u0e1a\u0e31\u0e0d\u0e0a\u0e35|\u0e42\u0e1b\u0e23\u0e44\u0e1f\u0e25\u0e4c|\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49/u,
|
|
77
|
+
],
|
|
78
|
+
queryVariants: ['user profile', 'account user', 'avatar person', 'user'],
|
|
79
|
+
iconPreferences: [
|
|
80
|
+
{ pattern: /^user(?:_|-|$)|(?:_|-)user(?:_|-|$)|users|profile|avatar|circle-user|user-circle/i, bonus: 42 },
|
|
81
|
+
{ pattern: /person|contact/i, bonus: 12 },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
slotPatterns: [
|
|
86
|
+
/\bhome\b/i,
|
|
87
|
+
/\bmain\b/i,
|
|
88
|
+
/\u9996\u9875|\u9996\u9801|\u4e3b\u9875|\u4e3b\u9801/u,
|
|
89
|
+
/\u30db\u30fc\u30e0|\u30e1\u30a4\u30f3/u,
|
|
90
|
+
/\ud648|\uba54\uc778/u,
|
|
91
|
+
/inicio|principal/i,
|
|
92
|
+
/startseite|hauptseite/i,
|
|
93
|
+
/\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629|\u0627\u0644\u0635\u0641\u062d\u0629\s+\u0627\u0644\u0631\u0626\u064a\u0633\u064a\u0629/u,
|
|
94
|
+
/\u0e2b\u0e19\u0e49\u0e32\u0e2b\u0e25\u0e31\u0e01/u,
|
|
95
|
+
],
|
|
96
|
+
queryVariants: ['home', 'house'],
|
|
97
|
+
iconPreferences: [
|
|
98
|
+
{ pattern: /^home(?:_|-|$)|(?:_|-)home(?:_|-|$)|house/i, bonus: 40 },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
slotPatterns: [
|
|
103
|
+
/alerts?/i,
|
|
104
|
+
/notifications?/i,
|
|
105
|
+
/bell/i,
|
|
106
|
+
/\u901a\u77e5|\u63d0\u9192/u,
|
|
107
|
+
/\u30a2\u30e9\u30fc\u30c8|\u304a\u77e5\u3089\u305b|\u901a\u77e5/u,
|
|
108
|
+
/\uc54c\ub9bc/u,
|
|
109
|
+
/notificaci[oó]n|notificaciones|alerta/i,
|
|
110
|
+
/benachrichtigung|benachrichtigungen/i,
|
|
111
|
+
/notifica(?:ç|c)[aã]o|notifica(?:ç|c)[oõ]es|notificacoes|alerta/i,
|
|
112
|
+
/\u0627\u0644\u0625\u0634\u0639\u0627\u0631|\u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a/u,
|
|
113
|
+
/\u0938\u0942\u091a\u0928\u093e|\u0938\u0942\u091a\u0928\u093e\u090f\u0901|\u0905\u0927\u093f\u0938\u0942\u091a\u0928\u093e/u,
|
|
114
|
+
/th[oô]ng b[aá]o|thong bao|c[aả]nh b[aá]o|canh bao/i,
|
|
115
|
+
/\u0e41\u0e08\u0e49\u0e07\u0e40\u0e15\u0e37\u0e2d\u0e19|\u0e01\u0e32\u0e23\u0e41\u0e08\u0e49\u0e07\u0e40\u0e15\u0e37\u0e2d\u0e19/u,
|
|
116
|
+
],
|
|
117
|
+
queryVariants: ['notification', 'bell', 'alert', 'alarm'],
|
|
118
|
+
iconPreferences: [
|
|
119
|
+
{ pattern: /notification|bell/i, bonus: 46 },
|
|
120
|
+
{ pattern: /alarm|alert/i, bonus: 18 },
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
slotPatterns: [
|
|
125
|
+
/privacy/i,
|
|
126
|
+
/security/i,
|
|
127
|
+
/private/i,
|
|
128
|
+
/safe/i,
|
|
129
|
+
/protection/i,
|
|
130
|
+
/\u9690\u79c1|\u96b1\u79c1|\u5b89\u5168/u,
|
|
131
|
+
/\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc|\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3|\u5b89\u5168/u,
|
|
132
|
+
/\uac1c\uc778\uc815\ubcf4|\uac1c\uc778\s+\uc815\ubcf4|\ubcf4\uc548|\uc548\uc804/u,
|
|
133
|
+
/privacidad|seguridad/i,
|
|
134
|
+
/datenschutz|sicherheit/i,
|
|
135
|
+
/privacidade|seguran[cç]a|seguranca/i,
|
|
136
|
+
/\u0627\u0644\u062e\u0635\u0648\u0635\u064a\u0629|\u0627\u0644\u0623\u0645\u0627\u0646|\u0627\u0644\u0627\u0645\u0627\u0646|\u0627\u0644\u0623\u0645\u0646|\u0627\u0644\u0627\u0645\u0646/u,
|
|
137
|
+
/\u0917\u094b\u092a\u0928\u0940\u092f\u0924\u093e|\u0938\u0941\u0930\u0915\u094d\u0937\u093e/u,
|
|
138
|
+
/quy[eề]n ri[eê]ng t[uư]|quyen rieng tu|b[aả]o m[aậ]t|bao mat|an to[aà]n|an toan/i,
|
|
139
|
+
/\u0e04\u0e27\u0e32\u0e21\u0e40\u0e1b\u0e47\u0e19\u0e2a\u0e48\u0e27\u0e19\u0e15\u0e31\u0e27|\u0e04\u0e27\u0e32\u0e21\u0e1b\u0e25\u0e2d\u0e14\u0e20\u0e31\u0e22/u,
|
|
140
|
+
],
|
|
141
|
+
queryVariants: ['shield lock', 'lock', 'shield', 'privacy security', 'security'],
|
|
142
|
+
iconPreferences: [
|
|
143
|
+
{ pattern: /shield.*lock|lock.*shield|shield-check|shield-alert|shield/i, bonus: 58 },
|
|
144
|
+
{ pattern: /^lock$|(?:_|-)lock(?:_|-|$)|key|fingerprint/i, bonus: 28 },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
slotPatterns: [
|
|
149
|
+
/appearance/i,
|
|
150
|
+
/theme/i,
|
|
151
|
+
/color/i,
|
|
152
|
+
/palette/i,
|
|
153
|
+
/dark mode/i,
|
|
154
|
+
/light mode/i,
|
|
155
|
+
/\u5916\u89c2|\u5916\u89c0|\u4e3b\u9898|\u4e3b\u984c|\u989c\u8272|\u984f\u8272/u,
|
|
156
|
+
/\u5916\u89b3|\u30c6\u30fc\u30de|\u8868\u793a|\u914d\u8272/u,
|
|
157
|
+
/\uc678\uad00|\ud14c\ub9c8|\uc0c9\uc0c1|\ud654\uba74/u,
|
|
158
|
+
/apariencia|tema|colou?r|modo/i,
|
|
159
|
+
/erscheinungsbild|design|darstellung|thema/i,
|
|
160
|
+
/apar[eê]ncia|tema|cor/i,
|
|
161
|
+
/\u0627\u0644\u0645\u0638\u0647\u0631|\u0627\u0644\u0633\u0645\u0629|\u0627\u0644\u0648\u0636\u0639|\u0627\u0644\u0623\u0644\u0648\u0627\u0646/u,
|
|
162
|
+
/\u0930\u0942\u092a|\u0925\u0940\u092e|\u0926\u093f\u0916\u093e\u0935\u091f|\u0930\u0902\u0917/u,
|
|
163
|
+
/giao di[eệ]n|giao dien|ch[uủ] \u0111[eề]|chu de|m[aà]u|mau/i,
|
|
164
|
+
/\u0e23\u0e39\u0e1b\u0e25\u0e31\u0e01\u0e29\u0e13\u0e4c|\u0e18\u0e35\u0e21|\u0e2a\u0e35|\u0e2b\u0e19\u0e49\u0e32\u0e15\u0e32/u,
|
|
165
|
+
],
|
|
166
|
+
queryVariants: ['palette', 'moon', 'theme', 'sun moon', 'appearance'],
|
|
167
|
+
iconPreferences: [
|
|
168
|
+
{ pattern: /^palette$|paint|brush|color|swatch/i, bonus: 54 },
|
|
169
|
+
{ pattern: /^moon$|sun-moon|sun|theme/i, bonus: 28 },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
slotPatterns: [
|
|
174
|
+
/language/i,
|
|
175
|
+
/locale/i,
|
|
176
|
+
/translate/i,
|
|
177
|
+
/translation/i,
|
|
178
|
+
/\u8bed\u8a00/u,
|
|
179
|
+
/\u8a9e\u8a00/u,
|
|
180
|
+
/\u8a00\u8a9e/u,
|
|
181
|
+
/\uc5b8\uc5b4/u,
|
|
182
|
+
/idioma/i,
|
|
183
|
+
/sprache/i,
|
|
184
|
+
/l[ií]ngua/i,
|
|
185
|
+
/langue/i,
|
|
186
|
+
/\u0627\u0644\u0644\u063a\u0629|\u0644\u063a\u0629/u,
|
|
187
|
+
/\u092d\u093e\u0937\u093e/u,
|
|
188
|
+
/ng[oô]n ng[uữ]|ngon ngu/i,
|
|
189
|
+
/\u0e20\u0e32\u0e29\u0e32/u,
|
|
190
|
+
],
|
|
191
|
+
queryVariants: ['globe', 'languages', 'translate', 'language'],
|
|
192
|
+
iconPreferences: [
|
|
193
|
+
{ pattern: /^globe$|^languages?$|^translate$|(?:_|-)(globe|languages?|translate)(?:_|-|$)/i, bonus: 70 },
|
|
194
|
+
{ pattern: /globe|language|translate/i, bonus: 28 },
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
slotPatterns: [
|
|
199
|
+
/language/i,
|
|
200
|
+
/locale/i,
|
|
201
|
+
/translate/i,
|
|
202
|
+
/translation/i,
|
|
203
|
+
/语言/u,
|
|
204
|
+
/語言/u,
|
|
205
|
+
/言語/u,
|
|
206
|
+
/언어/u,
|
|
207
|
+
/idioma/i,
|
|
208
|
+
/sprache/i,
|
|
209
|
+
/língua/i,
|
|
210
|
+
/langue/i,
|
|
211
|
+
/اللغة/u,
|
|
212
|
+
/भाषा/u,
|
|
213
|
+
/ngôn ngữ/i,
|
|
214
|
+
/ภาษา/u,
|
|
215
|
+
],
|
|
216
|
+
queryVariants: ['globe', 'languages', 'translate', 'language'],
|
|
217
|
+
iconPreferences: [
|
|
218
|
+
{ pattern: /^globe$|^languages?$|^translate$|(?:_|-)(globe|languages?|translate)(?:_|-|$)/i, bonus: 70 },
|
|
219
|
+
{ pattern: /globe|language|translate/i, bonus: 28 },
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
slotPatterns: [/create/i, /\badd\b/i, /\bplus\b/i, /compose/i, /new item/i],
|
|
224
|
+
queryVariants: ['add', 'plus', 'create new', 'compose'],
|
|
225
|
+
iconPreferences: [
|
|
226
|
+
{ pattern: /^(add|plus)(?:_|-|$)/i, bonus: 48 },
|
|
227
|
+
{ pattern: /(?:_|-)(add|plus)(?:_|-|$)/i, bonus: 18 },
|
|
228
|
+
{ pattern: /compose|edit|pencil/i, bonus: 8 },
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
slotPatterns: [/archive/i],
|
|
233
|
+
queryVariants: ['archive', 'archive box', 'box archive'],
|
|
234
|
+
iconPreferences: [
|
|
235
|
+
{ pattern: /^archive(?:_|-|$)|(?:_|-)archive(?:_|-|$)/i, bonus: 40 },
|
|
236
|
+
{ pattern: /box|tray/i, bonus: 8 },
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
slotPatterns: [/alerts?/i, /notifications?/i, /bell/i, /通知/u, /알림/u, /通知/u],
|
|
241
|
+
queryVariants: ['notification', 'bell', 'alert', 'alarm'],
|
|
242
|
+
iconPreferences: [
|
|
243
|
+
{ pattern: /notification|bell/i, bonus: 42 },
|
|
244
|
+
{ pattern: /alarm|alert/i, bonus: 16 },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
slotPatterns: [/profile/i, /account/i, /\buser\b/i, /\busers\b/i, /avatar/i],
|
|
249
|
+
queryVariants: ['user profile', 'account user', 'avatar person'],
|
|
250
|
+
iconPreferences: [
|
|
251
|
+
{ pattern: /^user(?:_|-|$)|(?:_|-)user(?:_|-|$)|users|profile|avatar|circle-user|user-circle/i, bonus: 36 },
|
|
252
|
+
{ pattern: /person|contact/i, bonus: 10 },
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
slotPatterns: [/model/i, /\bai\b/i, /\bml\b/i, /machine learning/i],
|
|
257
|
+
queryVariants: ['brain circuit', 'brain cog', 'neural network', 'model'],
|
|
258
|
+
iconPreferences: [
|
|
259
|
+
{ pattern: /brain-circuit|brain_circuit/i, bonus: 44 },
|
|
260
|
+
{ pattern: /brain|circuit|nodes/i, bonus: 24 },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
slotPatterns: [/prompt/i],
|
|
265
|
+
queryVariants: ['message text', 'text input', 'terminal', 'text cursor'],
|
|
266
|
+
iconPreferences: [
|
|
267
|
+
{ pattern: /message.*text|text.*input|text-cursor-input|terminal/i, bonus: 36 },
|
|
268
|
+
{ pattern: /text|prompt|keyboard/i, bonus: 14 },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
slotPatterns: [/dataset/i, /\bdata\b/i, /table/i],
|
|
273
|
+
queryVariants: ['database', 'data table', 'grid rows columns', 'table'],
|
|
274
|
+
iconPreferences: [
|
|
275
|
+
{ pattern: /^database$|^table-2$|^table_2$|table-cells|table-columns|table-rows/i, bonus: 36 },
|
|
276
|
+
{ pattern: /database|table|grid/i, bonus: 18 },
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
slotPatterns: [/evaluation/i, /benchmark/i, /score/i, /metrics?/i],
|
|
281
|
+
queryVariants: ['bar chart', 'metrics chart', 'gauge', 'checklist', 'benchmark'],
|
|
282
|
+
iconPreferences: [
|
|
283
|
+
{ pattern: /bar-chart-3|chart-bar|bar-chart|gauge|clipboard-check|list-check/i, bonus: 38 },
|
|
284
|
+
{ pattern: /chart|metrics|check/i, bonus: 16 },
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
slotPatterns: [/deployment/i, /deploy/i, /release/i, /ship/i, /publish/i],
|
|
289
|
+
queryVariants: ['cloud upload', 'upload', 'rocket launch', 'server upload', 'send'],
|
|
290
|
+
iconPreferences: [
|
|
291
|
+
{ pattern: /cloud-upload|upload-cloud|rocket|send/i, bonus: 40 },
|
|
292
|
+
{ pattern: /upload|server|package/i, bonus: 18 },
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
slotPatterns: [/monitoring/i, /monitor/i, /observability/i, /telemetry/i, /activity/i],
|
|
297
|
+
queryVariants: ['chart line', 'activity', 'pulse', 'gauge', 'signal'],
|
|
298
|
+
iconPreferences: [
|
|
299
|
+
{ pattern: /chart-line|line-chart|activity|pulse|gauge/i, bonus: 42 },
|
|
300
|
+
{ pattern: /chart|signal|radar/i, bonus: 16 },
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
slotPatterns: [/billing/i, /payment/i, /invoice/i, /subscription/i],
|
|
305
|
+
queryVariants: ['credit card', 'receipt', 'invoice', 'payment', 'wallet'],
|
|
306
|
+
iconPreferences: [
|
|
307
|
+
{ pattern: /credit-card|receipt|wallet|invoice/i, bonus: 44 },
|
|
308
|
+
{ pattern: /card|banknote|currency|dollar/i, bonus: 18 },
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
slotPatterns: [/reports?/i, /analytics/i, /insights?/i],
|
|
313
|
+
queryVariants: ['bar chart', 'file chart', 'analytics chart', 'report document'],
|
|
314
|
+
iconPreferences: [
|
|
315
|
+
{ pattern: /file-.*chart|chart-bar|bar-chart-3|bar-chart|chart-line|line-chart/i, bonus: 40 },
|
|
316
|
+
{ pattern: /chart|report|analytics/i, bonus: 16 },
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
slotPatterns: [/settings?/i, /preferences?/i, /configure/i],
|
|
321
|
+
queryVariants: ['settings', 'cog', 'sliders'],
|
|
322
|
+
iconPreferences: [
|
|
323
|
+
{ pattern: /^settings$|^cog$|settings-2|sliders/i, bonus: 34 },
|
|
324
|
+
{ pattern: /settings|cog|adjustments/i, bonus: 16 },
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
slotPatterns: [/database/i, /storage/i],
|
|
329
|
+
queryVariants: ['database', 'server database', 'data storage'],
|
|
330
|
+
iconPreferences: [
|
|
331
|
+
{ pattern: /^database$|database-stack/i, bonus: 36 },
|
|
332
|
+
{ pattern: /database|server/i, bonus: 16 },
|
|
333
|
+
],
|
|
334
|
+
},
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
const SLOT_PREFERENCE_RULES = Object.freeze({
|
|
338
|
+
mingcute: [
|
|
339
|
+
{
|
|
340
|
+
slotPatterns: [/home/i],
|
|
341
|
+
iconPreferences: [
|
|
342
|
+
{ pattern: /^home_3_line$/i, bonus: 14 },
|
|
343
|
+
{ pattern: /^home_2_line$/i, bonus: 8 },
|
|
344
|
+
{ pattern: /^home_1_line$/i, bonus: 4 },
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
slotPatterns: [/create/i, /add/i, /plus/i, /compose/i],
|
|
349
|
+
iconPreferences: [
|
|
350
|
+
{ pattern: /^add_line$/i, bonus: 42 },
|
|
351
|
+
{ pattern: /^plus_line$/i, bonus: 10 },
|
|
352
|
+
{ pattern: /^add_circle_line$/i, bonus: 6 },
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
slotPatterns: [/alerts?/i, /notification/i],
|
|
357
|
+
iconPreferences: [{ pattern: /^notification_line$/i, bonus: 12 }],
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
slotPatterns: [/profile/i, /user/i, /account/i],
|
|
361
|
+
iconPreferences: [{ pattern: /^user_1_line$/i, bonus: 12 }],
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
function normalizeText(value) {
|
|
367
|
+
return String(value || '')
|
|
368
|
+
.toLowerCase()
|
|
369
|
+
.replace(/[_:]+/g, ' ')
|
|
370
|
+
.replace(/[^a-z0-9\s-]/g, ' ')
|
|
371
|
+
.replace(/-/g, ' ')
|
|
372
|
+
.replace(/\s+/g, ' ')
|
|
373
|
+
.trim();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function tokenizeText(value) {
|
|
377
|
+
const normalized = normalizeText(value);
|
|
378
|
+
return normalized ? normalized.split(' ') : [];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function dedupe(values) {
|
|
382
|
+
return [...new Set(values.filter(Boolean))];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function buildLocalizedVariants(value, locale) {
|
|
386
|
+
if (!locale) return [];
|
|
387
|
+
const expanded = expandCjkQuery(value, {
|
|
388
|
+
locale,
|
|
389
|
+
terms: multilingualExpansionTerms,
|
|
390
|
+
});
|
|
391
|
+
const variants = expanded.matched.length > 0 ? expanded.variants.slice(1) : [];
|
|
392
|
+
const normalizedValue = normalizeCjkSearchText(value);
|
|
393
|
+
const containedMatches = [];
|
|
394
|
+
|
|
395
|
+
if (normalizedValue) {
|
|
396
|
+
for (const record of multilingualExpansionTerms) {
|
|
397
|
+
if (record.locale !== locale || record.gate !== 'auto_accept') continue;
|
|
398
|
+
|
|
399
|
+
const recordValues = [record.term, ...(record.variants || [])]
|
|
400
|
+
.map((term) => normalizeCjkSearchText(term))
|
|
401
|
+
.filter(Boolean);
|
|
402
|
+
const isContainedMatch = recordValues.some((term) => (
|
|
403
|
+
term.length >= 2
|
|
404
|
+
&& (normalizedValue.includes(term) || term.includes(normalizedValue))
|
|
405
|
+
));
|
|
406
|
+
if (!isContainedMatch) continue;
|
|
407
|
+
|
|
408
|
+
const firstIndex = Math.min(
|
|
409
|
+
...recordValues
|
|
410
|
+
.map((term) => normalizedValue.indexOf(term))
|
|
411
|
+
.filter((index) => index >= 0)
|
|
412
|
+
);
|
|
413
|
+
containedMatches.push({
|
|
414
|
+
index: Number.isFinite(firstIndex) ? firstIndex : Number.MAX_SAFE_INTEGER,
|
|
415
|
+
concepts: record.maps_to || [],
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
containedMatches
|
|
421
|
+
.sort((left, right) => left.index - right.index)
|
|
422
|
+
.forEach((match) => {
|
|
423
|
+
for (const concept of match.concepts) {
|
|
424
|
+
const normalizedConcept = normalizeCjkSearchText(concept);
|
|
425
|
+
if (normalizedConcept) variants.push(normalizedConcept);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return dedupe(variants);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function buildSlotIntentTerms(task, slot, locale = null) {
|
|
433
|
+
const taskTokens = tokenizeText(task);
|
|
434
|
+
const slotTokens = tokenizeText(slot);
|
|
435
|
+
const usefulSlotTokens = slotTokens.filter((token) => !GENERIC_SLOT_WORDS.has(token));
|
|
436
|
+
|
|
437
|
+
const expanded = [...usefulSlotTokens];
|
|
438
|
+
|
|
439
|
+
const usefulTaskTokens = taskTokens.filter((token) => !GENERIC_SLOT_WORDS.has(token));
|
|
440
|
+
expanded.push(...usefulTaskTokens);
|
|
441
|
+
expanded.push(...buildLocalizedVariants(slot, locale).flatMap(tokenizeText));
|
|
442
|
+
expanded.push(...buildLocalizedVariants(task, locale).flatMap(tokenizeText));
|
|
443
|
+
|
|
444
|
+
const variants = buildIntentQueryVariants(`${slot} ${task}`, {
|
|
445
|
+
baseQuery: slot,
|
|
446
|
+
maxVariants: 8,
|
|
447
|
+
});
|
|
448
|
+
for (const variant of variants) {
|
|
449
|
+
expanded.push(...tokenizeText(variant));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
for (const rule of getMatchingSlotRules(slot, expanded)) {
|
|
453
|
+
for (const variant of rule.queryVariants || []) {
|
|
454
|
+
expanded.push(...tokenizeText(variant));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return dedupe(expanded);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function buildSlotQueryVariants(task, slot, locale = null) {
|
|
462
|
+
const localizedVariants = [
|
|
463
|
+
...buildLocalizedVariants(slot, locale),
|
|
464
|
+
...buildLocalizedVariants(`${slot} ${task}`, locale),
|
|
465
|
+
];
|
|
466
|
+
const variants = buildIntentQueryVariants(`${slot} ${task}`, {
|
|
467
|
+
baseQuery: slot,
|
|
468
|
+
maxVariants: 8,
|
|
469
|
+
});
|
|
470
|
+
variants.unshift(...localizedVariants);
|
|
471
|
+
const usefulSlotTokens = tokenizeText(slot).filter((token) => !GENERIC_SLOT_WORDS.has(token));
|
|
472
|
+
variants.push(...usefulSlotTokens);
|
|
473
|
+
const intentTerms = tokenizeText(`${slot} ${task} ${variants.join(' ')}`);
|
|
474
|
+
const ruleVariants = getMatchingSlotRules(slot, intentTerms)
|
|
475
|
+
.flatMap((rule) => rule.queryVariants || []);
|
|
476
|
+
variants.unshift(...ruleVariants);
|
|
477
|
+
return dedupe(variants).slice(0, 12);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function scoreLexicalFit(icon, intentTerms, slotLabel) {
|
|
481
|
+
const tokens = new Set([
|
|
482
|
+
...tokenizeText(icon.id),
|
|
483
|
+
...tokenizeText(icon.name),
|
|
484
|
+
...tokenizeText(`${icon.lib}:${icon.id}`),
|
|
485
|
+
]);
|
|
486
|
+
const normalizedId = normalizeText(icon.id);
|
|
487
|
+
const normalizedName = normalizeText(icon.name);
|
|
488
|
+
const normalizedSlot = normalizeText(slotLabel);
|
|
489
|
+
|
|
490
|
+
let score = 0;
|
|
491
|
+
for (const term of intentTerms) {
|
|
492
|
+
if (tokens.has(term)) score += 14;
|
|
493
|
+
else if (normalizedId.includes(term) || normalizedName.includes(term)) score += 8;
|
|
494
|
+
|
|
495
|
+
if (normalizedId === term || normalizedName === term) {
|
|
496
|
+
score += 20;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (normalizedSlot && (normalizedId === normalizedSlot || normalizedName === normalizedSlot)) {
|
|
501
|
+
score += 20;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return score;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function getVariantPenalty(icon) {
|
|
508
|
+
const normalizedId = normalizeText(icon.id);
|
|
509
|
+
let penalty = 0;
|
|
510
|
+
for (const rule of VARIANT_PENALTIES) {
|
|
511
|
+
if (rule.pattern.test(normalizedId)) {
|
|
512
|
+
penalty += rule.penalty;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return penalty;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function getMatchingSlotRules(slotLabel, intentTerms = []) {
|
|
519
|
+
const rawSlotText = String(slotLabel || '');
|
|
520
|
+
const slotText = normalizeText(slotLabel);
|
|
521
|
+
return COMMON_SLOT_PREFERENCE_RULES.filter((rule) => rule.slotPatterns.some((pattern) => (
|
|
522
|
+
pattern.test(slotText) || pattern.test(rawSlotText)
|
|
523
|
+
)));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function scoreSlotPreferenceRules(icon, rules = [], slotText = '') {
|
|
527
|
+
let bonus = 0;
|
|
528
|
+
|
|
529
|
+
for (const rule of rules) {
|
|
530
|
+
for (const preference of rule.iconPreferences) {
|
|
531
|
+
if (preference.pattern.test(icon.id)) {
|
|
532
|
+
bonus += preference.bonus;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return bonus;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function getSlotPreferenceBonus(icon, slotLabel, intentTerms, library) {
|
|
541
|
+
const slotText = `${slotLabel} ${intentTerms.join(' ')}`;
|
|
542
|
+
const commonRules = getMatchingSlotRules(slotLabel, intentTerms);
|
|
543
|
+
const libraryRules = (SLOT_PREFERENCE_RULES[library] || [])
|
|
544
|
+
.filter((rule) => rule.slotPatterns.some((pattern) => pattern.test(slotText)));
|
|
545
|
+
|
|
546
|
+
return scoreSlotPreferenceRules(icon, commonRules, slotText) +
|
|
547
|
+
scoreSlotPreferenceRules(icon, libraryRules, slotText);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function summarizeSemanticFit(slotLabel, semanticRecord, intentTerms) {
|
|
551
|
+
if (semanticRecord?.depicts && semanticRecord?.use_when) {
|
|
552
|
+
return `Strong fit for ${slotLabel}: visually reads as ${String(semanticRecord.depicts).toLowerCase()}. ${semanticRecord.use_when}`;
|
|
553
|
+
}
|
|
554
|
+
if (semanticRecord?.depicts) {
|
|
555
|
+
return `Good fit for ${slotLabel}: visually reads as ${String(semanticRecord.depicts).toLowerCase()}.`;
|
|
556
|
+
}
|
|
557
|
+
if (semanticRecord?.use_when) {
|
|
558
|
+
return `Strong fit for ${slotLabel}: ${semanticRecord.use_when}`;
|
|
559
|
+
}
|
|
560
|
+
if (intentTerms.length > 0) {
|
|
561
|
+
return `Best lexical match for ${slotLabel} from the current library.`;
|
|
562
|
+
}
|
|
563
|
+
return `Best available match for ${slotLabel}.`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function buildWhySelected(slotLabel, semanticRecord, iconResult) {
|
|
567
|
+
const label = semanticRecord?.label || iconResult.name;
|
|
568
|
+
if (semanticRecord?.depicts) {
|
|
569
|
+
return `${label} matches ${slotLabel} and visually reads as ${String(semanticRecord.depicts).toLowerCase()}.`;
|
|
570
|
+
}
|
|
571
|
+
if (semanticRecord?.use_when) {
|
|
572
|
+
return `${label} matches ${slotLabel}. ${semanticRecord.use_when}`;
|
|
573
|
+
}
|
|
574
|
+
return `${label} is the clearest match for ${slotLabel} from the current library.`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function buildCandidatePayload(slotLabel, iconResult, semanticRecord, intentTerms) {
|
|
578
|
+
return {
|
|
579
|
+
id: iconResult.id,
|
|
580
|
+
library: iconResult.library,
|
|
581
|
+
name: iconResult.name,
|
|
582
|
+
style: iconResult.style || 'outline',
|
|
583
|
+
label: semanticRecord?.label || iconResult.semantic?.label || iconResult.name,
|
|
584
|
+
semantic_fit: summarizeSemanticFit(slotLabel, semanticRecord, intentTerms),
|
|
585
|
+
why_selected: buildWhySelected(slotLabel, semanticRecord, iconResult),
|
|
586
|
+
svg: iconResult.svg,
|
|
587
|
+
semantic: buildPublicSemanticPayload(semanticRecord) || iconResult.semantic || null,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function mapWithConcurrency(items, limit, mapper) {
|
|
592
|
+
const results = new Array(items.length);
|
|
593
|
+
let nextIndex = 0;
|
|
594
|
+
|
|
595
|
+
async function worker() {
|
|
596
|
+
while (nextIndex < items.length) {
|
|
597
|
+
const currentIndex = nextIndex;
|
|
598
|
+
nextIndex += 1;
|
|
599
|
+
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const workerCount = Math.min(Math.max(1, limit), items.length);
|
|
604
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
605
|
+
return results;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export async function recommendIconsForTask({
|
|
609
|
+
task,
|
|
610
|
+
library,
|
|
611
|
+
style = 'any',
|
|
612
|
+
locale = null,
|
|
613
|
+
slots,
|
|
614
|
+
limitPerSlot = 3,
|
|
615
|
+
searchIconsForQuery,
|
|
616
|
+
buildIconResult,
|
|
617
|
+
semanticMap,
|
|
618
|
+
}) {
|
|
619
|
+
const slotResults = await mapWithConcurrency(slots, 6, async (slotLabel) => {
|
|
620
|
+
const intentTerms = buildSlotIntentTerms(task, slotLabel, locale);
|
|
621
|
+
const queryVariants = buildSlotQueryVariants(task, slotLabel, locale).slice(0, locale ? 8 : 2);
|
|
622
|
+
const pooledIcons = [];
|
|
623
|
+
const seen = new Set();
|
|
624
|
+
|
|
625
|
+
const resultGroups = await mapWithConcurrency(queryVariants, 2, async (queryVariant) => {
|
|
626
|
+
try {
|
|
627
|
+
return await searchIconsForQuery({
|
|
628
|
+
query: queryVariant,
|
|
629
|
+
library,
|
|
630
|
+
style,
|
|
631
|
+
limit: Math.max(limitPerSlot * 5, 10),
|
|
632
|
+
locale,
|
|
633
|
+
});
|
|
634
|
+
} catch {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
for (const results of resultGroups) {
|
|
640
|
+
for (const icon of results) {
|
|
641
|
+
const key = `${icon.lib}:${icon.id}`;
|
|
642
|
+
if (seen.has(key)) continue;
|
|
643
|
+
seen.add(key);
|
|
644
|
+
pooledIcons.push(icon);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const scored = pooledIcons
|
|
649
|
+
.map((icon, index) => {
|
|
650
|
+
const semanticRecord = getSemanticRecordForIcon(semanticMap, icon);
|
|
651
|
+
const semanticQuery = queryVariants.join(' ');
|
|
652
|
+
const semanticScore = semanticRecord ? scoreSemanticAlignment(semanticQuery, semanticRecord) * 3 : 0;
|
|
653
|
+
const lexicalScore = scoreLexicalFit(icon, intentTerms, slotLabel);
|
|
654
|
+
const semanticBonus = semanticRecord ? 6 : 0;
|
|
655
|
+
const variantPenalty = getVariantPenalty(icon);
|
|
656
|
+
const slotPreferenceBonus = getSlotPreferenceBonus(icon, slotLabel, intentTerms, library);
|
|
657
|
+
const intentProfile = buildSearchIntentProfile(`${slotLabel} ${task}`);
|
|
658
|
+
const intentAdjustment = getIntentCandidateAdjustment(icon, intentProfile);
|
|
659
|
+
|
|
660
|
+
return {
|
|
661
|
+
icon,
|
|
662
|
+
index,
|
|
663
|
+
semanticRecord,
|
|
664
|
+
slotPreferenceBonus,
|
|
665
|
+
totalScore:
|
|
666
|
+
semanticScore +
|
|
667
|
+
lexicalScore +
|
|
668
|
+
semanticBonus +
|
|
669
|
+
slotPreferenceBonus +
|
|
670
|
+
intentAdjustment.boost -
|
|
671
|
+
intentAdjustment.penalty -
|
|
672
|
+
variantPenalty,
|
|
673
|
+
};
|
|
674
|
+
})
|
|
675
|
+
.filter((entry) => entry.totalScore > 0)
|
|
676
|
+
.sort((left, right) => {
|
|
677
|
+
if (right.slotPreferenceBonus !== left.slotPreferenceBonus) {
|
|
678
|
+
return right.slotPreferenceBonus - left.slotPreferenceBonus;
|
|
679
|
+
}
|
|
680
|
+
if (right.totalScore !== left.totalScore) return right.totalScore - left.totalScore;
|
|
681
|
+
return left.index - right.index;
|
|
682
|
+
})
|
|
683
|
+
.slice(0, limitPerSlot);
|
|
684
|
+
|
|
685
|
+
const preparedCandidates = [];
|
|
686
|
+
for (const entry of scored) {
|
|
687
|
+
const iconResult = await buildIconResult(entry.icon, { style });
|
|
688
|
+
if (!iconResult?.svg) continue;
|
|
689
|
+
preparedCandidates.push(buildCandidatePayload(slotLabel, iconResult, entry.semanticRecord, intentTerms));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
slot: slotLabel,
|
|
694
|
+
queries_used: queryVariants,
|
|
695
|
+
recommended: preparedCandidates[0] || null,
|
|
696
|
+
alternatives: preparedCandidates.slice(1),
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
task,
|
|
702
|
+
library: library || 'all',
|
|
703
|
+
style,
|
|
704
|
+
slot_count: slots.length,
|
|
705
|
+
results: slotResults,
|
|
706
|
+
};
|
|
707
|
+
}
|