@wix/web5-core 1.63.36 → 1.63.38
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/dist/cjs/components/ui/PoweredByBadge.js +12 -4
- package/dist/cjs/components/ui/PoweredByBadge.js.map +1 -1
- package/dist/cjs/image/composeSemantic.js +76 -12
- package/dist/cjs/image/composeSemantic.js.map +1 -1
- package/dist/cjs/styles/components.css +0 -1
- package/dist/esm/components/ui/PoweredByBadge.js +8 -0
- package/dist/esm/components/ui/PoweredByBadge.js.map +1 -1
- package/dist/esm/image/composeSemantic.js +76 -12
- package/dist/esm/image/composeSemantic.js.map +1 -1
- package/dist/esm/styles/components.css +0 -1
- package/dist/types/components/ui/PoweredByBadge.d.ts +8 -0
- package/dist/types/components/ui/PoweredByBadge.d.ts.map +1 -1
- package/dist/types/image/composeSemantic.d.ts +31 -0
- package/dist/types/image/composeSemantic.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/styles/components.css +0 -1
- package/dist/cjs/components/ui/PoweredByBadge.css +0 -68
- package/dist/esm/components/ui/PoweredByBadge.css +0 -68
- package/src/components/ui/PoweredByBadge.css +0 -68
|
@@ -15,6 +15,14 @@ const DEFAULT_POWERED_BY_HREF = exports.DEFAULT_POWERED_BY_HREF = 'https://www.n
|
|
|
15
15
|
* Rendering is the caller's decision — this component says nothing about plans.
|
|
16
16
|
* The AI page mounts it only when the site is on the Free tier (see
|
|
17
17
|
* `shouldShowPoweredBy`); any paid package removes it.
|
|
18
|
+
*
|
|
19
|
+
* NOTE: unlike every other component here, its CSS is NOT in
|
|
20
|
+
* `styles/components.css`. That file ships in the client bundle, and this
|
|
21
|
+
* component is mounted from the app bundle — the two are deployed
|
|
22
|
+
* independently, and splitting it across them shipped a naked, unstyled credit
|
|
23
|
+
* to a live storefront. Its stylesheet lives with the shell that mounts it, in
|
|
24
|
+
* `web50-server-ui/src/components/App.css`. A future client that mounts this
|
|
25
|
+
* itself has to bring its own copy, or move the rules somewhere both can see.
|
|
18
26
|
*/
|
|
19
27
|
const PoweredByBadge = ({
|
|
20
28
|
label = 'Powered by',
|
|
@@ -30,7 +38,7 @@ const PoweredByBadge = ({
|
|
|
30
38
|
__self: void 0,
|
|
31
39
|
__source: {
|
|
32
40
|
fileName: _jsxFileName,
|
|
33
|
-
lineNumber:
|
|
41
|
+
lineNumber: 47,
|
|
34
42
|
columnNumber: 5
|
|
35
43
|
}
|
|
36
44
|
}, /*#__PURE__*/_react.default.createElement("a", {
|
|
@@ -47,7 +55,7 @@ const PoweredByBadge = ({
|
|
|
47
55
|
__self: void 0,
|
|
48
56
|
__source: {
|
|
49
57
|
fileName: _jsxFileName,
|
|
50
|
-
lineNumber:
|
|
58
|
+
lineNumber: 48,
|
|
51
59
|
columnNumber: 7
|
|
52
60
|
}
|
|
53
61
|
}, /*#__PURE__*/_react.default.createElement("span", {
|
|
@@ -55,7 +63,7 @@ const PoweredByBadge = ({
|
|
|
55
63
|
__self: void 0,
|
|
56
64
|
__source: {
|
|
57
65
|
fileName: _jsxFileName,
|
|
58
|
-
lineNumber:
|
|
66
|
+
lineNumber: 61,
|
|
59
67
|
columnNumber: 9
|
|
60
68
|
}
|
|
61
69
|
}, label), ' ', /*#__PURE__*/_react.default.createElement("span", {
|
|
@@ -63,7 +71,7 @@ const PoweredByBadge = ({
|
|
|
63
71
|
__self: void 0,
|
|
64
72
|
__source: {
|
|
65
73
|
fileName: _jsxFileName,
|
|
66
|
-
lineNumber:
|
|
74
|
+
lineNumber: 62,
|
|
67
75
|
columnNumber: 9
|
|
68
76
|
}
|
|
69
77
|
}, brand)));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","_interopRequireDefault","require","_utils","_jsxFileName","DEFAULT_POWERED_BY_HREF","exports","PoweredByBadge","label","brand","href","target","onClick","className","rel","undefined","default","createElement","cn","__self","__source","fileName","lineNumber","columnNumber"],"sources":["../../../../src/components/ui/PoweredByBadge.tsx"],"sourcesContent":["import React, { FC } from 'react';\nimport { cn } from '../../lib/utils';\n\nexport interface PoweredByBadgeProps {\n /** Muted lead-in. Default: \"Powered by\". */\n label?: string;\n /** The emphasised brand name, and the link's visible anchor. Default: \"Niso\". */\n brand?: string;\n /** URL the badge opens. Default: the Niso marketing site. */\n href?: string;\n /** Override the link target. Default: `_blank` (opens in a new tab). */\n target?: '_blank' | '_self' | '_parent' | '_top';\n /** Click handler invoked before navigation. Use to fire analytics. */\n onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;\n className?: string;\n}\n\nexport const DEFAULT_POWERED_BY_HREF = 'https://www.nisoweb.ai';\n\n/**\n * The free-tier credit bar (B2B-4574): a full-width strip at the very bottom of\n * the AI page reading \"Powered by Niso\", with the brand name linking out.\n *\n * Rendering is the caller's decision — this component says nothing about plans.\n * The AI page mounts it only when the site is on the Free tier (see\n * `shouldShowPoweredBy`); any paid package removes it.\n */\nexport const PoweredByBadge: FC<PoweredByBadgeProps> = ({\n label = 'Powered by',\n brand = 'Niso',\n href = DEFAULT_POWERED_BY_HREF,\n target = '_blank',\n onClick,\n className,\n}) => {\n const rel = target === '_blank' ? 'noopener noreferrer' : undefined;\n\n return (\n <div className={cn('web5-powered-by', className)}>\n <a\n className=\"web5-powered-by__link\"\n href={href}\n target={target}\n rel={rel}\n onClick={onClick}\n // The visible text splits across two spans for the two-tone treatment,\n // so spell the whole thing out once for assistive tech — including the\n // new tab, which is otherwise unannounced.\n aria-label={`${label} ${brand}${\n target === '_blank' ? ' (opens in a new tab)' : ''\n }`}\n >\n <span className=\"web5-powered-by__label\">{label}</span>{' '}\n <span className=\"web5-powered-by__brand\">{brand}</span>\n </a>\n </div>\n );\n};\n"],"mappings":";;;;;AAAA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AACA,IAAAC,MAAA,GAAAD,OAAA;AAAqC,IAAAE,YAAA;AAgB9B,MAAMC,uBAAuB,GAAAC,OAAA,CAAAD,uBAAA,GAAG,wBAAwB;;AAE/D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAME,cAAuC,GAAGA,CAAC;EACtDC,KAAK,GAAG,YAAY;EACpBC,KAAK,GAAG,MAAM;EACdC,IAAI,GAAGL,uBAAuB;EAC9BM,MAAM,GAAG,QAAQ;EACjBC,OAAO;EACPC;AACF,CAAC,KAAK;EACJ,MAAMC,GAAG,GAAGH,MAAM,KAAK,QAAQ,GAAG,qBAAqB,GAAGI,SAAS;EAEnE,oBACEf,MAAA,CAAAgB,OAAA,CAAAC,aAAA;IAAKJ,SAAS,EAAE,IAAAK,SAAE,EAAC,iBAAiB,EAAEL,SAAS,CAAE;IAAAM,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAjB,YAAA;MAAAkB,UAAA;MAAAC,YAAA;IAAA;EAAA,gBAC/CvB,MAAA,CAAAgB,OAAA,CAAAC,aAAA;IACEJ,SAAS,EAAC,uBAAuB;IACjCH,IAAI,EAAEA,IAAK;IACXC,MAAM,EAAEA,MAAO;IACfG,GAAG,EAAEA,GAAI;IACTF,OAAO,EAAEA;IACT;IACA;IACA;IAAA;IACA,cAAY,GAAGJ,KAAK,IAAIC,KAAK,GAC3BE,MAAM,KAAK,QAAQ,GAAG,uBAAuB,GAAG,EAAE,EACjD;IAAAQ,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAjB,YAAA;MAAAkB,UAAA;MAAAC,YAAA;IAAA;EAAA,gBAEHvB,MAAA,CAAAgB,OAAA,CAAAC,aAAA;IAAMJ,SAAS,EAAC,wBAAwB;IAAAM,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAjB,YAAA;MAAAkB,UAAA;MAAAC,YAAA;IAAA;EAAA,GAAEf,KAAY,CAAC,EAAC,GAAG,eAC3DR,MAAA,CAAAgB,OAAA,CAAAC,aAAA;IAAMJ,SAAS,EAAC,wBAAwB;IAAAM,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAjB,YAAA;MAAAkB,UAAA;MAAAC,YAAA;IAAA;EAAA,GAAEd,KAAY,CACrD,CACA,CAAC;AAEV,CAAC;AAACH,OAAA,CAAAC,cAAA,GAAAA,cAAA","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireDefault","require","_utils","_jsxFileName","DEFAULT_POWERED_BY_HREF","exports","PoweredByBadge","label","brand","href","target","onClick","className","rel","undefined","default","createElement","cn","__self","__source","fileName","lineNumber","columnNumber"],"sources":["../../../../src/components/ui/PoweredByBadge.tsx"],"sourcesContent":["import React, { FC } from 'react';\nimport { cn } from '../../lib/utils';\n\nexport interface PoweredByBadgeProps {\n /** Muted lead-in. Default: \"Powered by\". */\n label?: string;\n /** The emphasised brand name, and the link's visible anchor. Default: \"Niso\". */\n brand?: string;\n /** URL the badge opens. Default: the Niso marketing site. */\n href?: string;\n /** Override the link target. Default: `_blank` (opens in a new tab). */\n target?: '_blank' | '_self' | '_parent' | '_top';\n /** Click handler invoked before navigation. Use to fire analytics. */\n onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;\n className?: string;\n}\n\nexport const DEFAULT_POWERED_BY_HREF = 'https://www.nisoweb.ai';\n\n/**\n * The free-tier credit bar (B2B-4574): a full-width strip at the very bottom of\n * the AI page reading \"Powered by Niso\", with the brand name linking out.\n *\n * Rendering is the caller's decision — this component says nothing about plans.\n * The AI page mounts it only when the site is on the Free tier (see\n * `shouldShowPoweredBy`); any paid package removes it.\n *\n * NOTE: unlike every other component here, its CSS is NOT in\n * `styles/components.css`. That file ships in the client bundle, and this\n * component is mounted from the app bundle — the two are deployed\n * independently, and splitting it across them shipped a naked, unstyled credit\n * to a live storefront. Its stylesheet lives with the shell that mounts it, in\n * `web50-server-ui/src/components/App.css`. A future client that mounts this\n * itself has to bring its own copy, or move the rules somewhere both can see.\n */\nexport const PoweredByBadge: FC<PoweredByBadgeProps> = ({\n label = 'Powered by',\n brand = 'Niso',\n href = DEFAULT_POWERED_BY_HREF,\n target = '_blank',\n onClick,\n className,\n}) => {\n const rel = target === '_blank' ? 'noopener noreferrer' : undefined;\n\n return (\n <div className={cn('web5-powered-by', className)}>\n <a\n className=\"web5-powered-by__link\"\n href={href}\n target={target}\n rel={rel}\n onClick={onClick}\n // The visible text splits across two spans for the two-tone treatment,\n // so spell the whole thing out once for assistive tech — including the\n // new tab, which is otherwise unannounced.\n aria-label={`${label} ${brand}${\n target === '_blank' ? ' (opens in a new tab)' : ''\n }`}\n >\n <span className=\"web5-powered-by__label\">{label}</span>{' '}\n <span className=\"web5-powered-by__brand\">{brand}</span>\n </a>\n </div>\n );\n};\n"],"mappings":";;;;;AAAA,IAAAA,MAAA,GAAAC,sBAAA,CAAAC,OAAA;AACA,IAAAC,MAAA,GAAAD,OAAA;AAAqC,IAAAE,YAAA;AAgB9B,MAAMC,uBAAuB,GAAAC,OAAA,CAAAD,uBAAA,GAAG,wBAAwB;;AAE/D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAME,cAAuC,GAAGA,CAAC;EACtDC,KAAK,GAAG,YAAY;EACpBC,KAAK,GAAG,MAAM;EACdC,IAAI,GAAGL,uBAAuB;EAC9BM,MAAM,GAAG,QAAQ;EACjBC,OAAO;EACPC;AACF,CAAC,KAAK;EACJ,MAAMC,GAAG,GAAGH,MAAM,KAAK,QAAQ,GAAG,qBAAqB,GAAGI,SAAS;EAEnE,oBACEf,MAAA,CAAAgB,OAAA,CAAAC,aAAA;IAAKJ,SAAS,EAAE,IAAAK,SAAE,EAAC,iBAAiB,EAAEL,SAAS,CAAE;IAAAM,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAjB,YAAA;MAAAkB,UAAA;MAAAC,YAAA;IAAA;EAAA,gBAC/CvB,MAAA,CAAAgB,OAAA,CAAAC,aAAA;IACEJ,SAAS,EAAC,uBAAuB;IACjCH,IAAI,EAAEA,IAAK;IACXC,MAAM,EAAEA,MAAO;IACfG,GAAG,EAAEA,GAAI;IACTF,OAAO,EAAEA;IACT;IACA;IACA;IAAA;IACA,cAAY,GAAGJ,KAAK,IAAIC,KAAK,GAC3BE,MAAM,KAAK,QAAQ,GAAG,uBAAuB,GAAG,EAAE,EACjD;IAAAQ,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAjB,YAAA;MAAAkB,UAAA;MAAAC,YAAA;IAAA;EAAA,gBAEHvB,MAAA,CAAAgB,OAAA,CAAAC,aAAA;IAAMJ,SAAS,EAAC,wBAAwB;IAAAM,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAjB,YAAA;MAAAkB,UAAA;MAAAC,YAAA;IAAA;EAAA,GAAEf,KAAY,CAAC,EAAC,GAAG,eAC3DR,MAAA,CAAAgB,OAAA,CAAAC,aAAA;IAAMJ,SAAS,EAAC,wBAAwB;IAAAM,MAAA;IAAAC,QAAA;MAAAC,QAAA,EAAAjB,YAAA;MAAAkB,UAAA;MAAAC,YAAA;IAAA;EAAA,GAAEd,KAAY,CACrD,CACA,CAAC;AAEV,CAAC;AAACH,OAAA,CAAAC,cAAA,GAAAA,cAAA","ignoreList":[]}
|
|
@@ -22,9 +22,55 @@ exports.composeSemantic = composeSemantic;
|
|
|
22
22
|
* CDN build externalises core, so a core change could not be seen through
|
|
23
23
|
* `?clientBundleUrl=` until it had been published. 0223 always assigned it
|
|
24
24
|
* here, and this is that move.
|
|
25
|
+
*
|
|
26
|
+
* WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)
|
|
27
|
+
* ------------------------------------------------------
|
|
28
|
+
* It buys **recall, and only recall**. In media-manager an editorial slot
|
|
29
|
+
* retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates
|
|
30
|
+
* (`CandidateGather.planFor`), scoped by a filter that is just "this site's
|
|
31
|
+
* images" (`buildFilter`) — so the phrase ranks the corpus rather than gating
|
|
32
|
+
* it. Which picture then WINS is decided by `SlotScorer`, whose every term is
|
|
33
|
+
* geometric (crop fit, background, subject, text safety, resolution). Vespa's
|
|
34
|
+
* relevance score is not one of them, and the pool's order is discarded by the
|
|
35
|
+
* assignment solve. The phrase's entire job is choosing which 30 images reach
|
|
36
|
+
* the scorer.
|
|
37
|
+
*
|
|
38
|
+
* That is why it is budgeted in WORDS. The documents it is matched against are
|
|
39
|
+
* short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,
|
|
40
|
+
* label…`, usually under ten words — and retrieval is hybrid, BM25 blended with
|
|
41
|
+
* vector closeness. A headline plus a clause of marketing prose is the wrong
|
|
42
|
+
* shape against that on both halves: the vector drifts toward the mean of
|
|
43
|
+
* several topics instead of sitting on the subject, and BM25 spends term weight
|
|
44
|
+
* on filler that pulls in whatever else on the site happens to carry those
|
|
45
|
+
* words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)
|
|
46
|
+
* happen on the document side only.
|
|
47
|
+
*
|
|
48
|
+
* A phrase that is too specific costs a worse 30, never a blank section: the
|
|
49
|
+
* site-wide widening in `CandidateGather.poolFor` is entity-only because an
|
|
50
|
+
* editorial pool empties only when the site has no images at all.
|
|
51
|
+
*
|
|
52
|
+
* CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text
|
|
53
|
+
* slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —
|
|
54
|
+
* every text-derived slot in the product gets a new id. Not a place for
|
|
55
|
+
* cosmetic edits.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The phrase's real budget, in whole words — the unit that matters, because
|
|
60
|
+
* what it is matched against is a short bag of nouns and not a paragraph.
|
|
61
|
+
*/
|
|
62
|
+
const WORD_BUDGET = 10;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A title of this many words already names its own subject, so the lead adds
|
|
66
|
+
* dilution rather than signal and is dropped. Below it the title may be as thin
|
|
67
|
+
* as "Our picks" or "Why it matters", which names nothing retrievable on its
|
|
68
|
+
* own — that is the case the lead exists for.
|
|
25
69
|
*/
|
|
70
|
+
const TITLE_ENOUGH = 5;
|
|
26
71
|
|
|
27
|
-
/**
|
|
72
|
+
/** A character backstop under the word budget: ten words is normally well
|
|
73
|
+
* inside it, but one pathological unbroken "word" is not. */
|
|
28
74
|
const MAX_LENGTH = 120;
|
|
29
75
|
|
|
30
76
|
/** Below this there is no query worth sending — better to render no picture
|
|
@@ -39,9 +85,18 @@ const stripMarkdown = raw => raw
|
|
|
39
85
|
.replace(/^\s*#{1,6}\s*/gm, '').replace(/^\s*>\s?/gm, '')
|
|
40
86
|
// html tags an author or the parser may have left behind
|
|
41
87
|
.replace(/<[^>]+>/g, ' ');
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
.
|
|
88
|
+
const trimTail = raw => raw.replace(/[\s.,;:!?—–-]+$/g, '').trim();
|
|
89
|
+
const tidy = raw => trimTail(stripMarkdown(raw).replace(/\s+/g, ' ').trim());
|
|
90
|
+
const wordsOf = raw => raw.split(' ').filter(word => word.length > 0);
|
|
91
|
+
|
|
92
|
+
/** Keep at most `max` whole words. The unit the budget is actually stated in. */
|
|
93
|
+
const clampToWords = (raw, max) => {
|
|
94
|
+
const words = wordsOf(raw);
|
|
95
|
+
if (words.length <= max) {
|
|
96
|
+
return raw;
|
|
97
|
+
}
|
|
98
|
+
return trimTail(words.slice(0, max).join(' '));
|
|
99
|
+
};
|
|
45
100
|
|
|
46
101
|
/**
|
|
47
102
|
* Clamp without cutting a word in half. Slicing at a fixed length leaves
|
|
@@ -58,9 +113,12 @@ const clampToWord = (raw, max) => {
|
|
|
58
113
|
const cut = raw.slice(0, max + 1);
|
|
59
114
|
const lastBreak = cut.lastIndexOf(' ');
|
|
60
115
|
const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);
|
|
61
|
-
return out
|
|
116
|
+
return trimTail(out);
|
|
62
117
|
};
|
|
63
118
|
|
|
119
|
+
/** The word budget first, then the character backstop under it. */
|
|
120
|
+
const budgeted = raw => clampToWord(clampToWords(raw, WORD_BUDGET), MAX_LENGTH);
|
|
121
|
+
|
|
64
122
|
/** The first clause carries the subject; what follows is usually qualification. */
|
|
65
123
|
const firstClause = raw => {
|
|
66
124
|
const m = raw.match(/^[^.!?;]+/);
|
|
@@ -78,17 +136,23 @@ function composeSemantic({
|
|
|
78
136
|
// A mission, when there is one, is the whole answer.
|
|
79
137
|
const mission = tidy(sectionSemantic ?? '');
|
|
80
138
|
if (mission.length >= MIN_LENGTH) {
|
|
81
|
-
return
|
|
139
|
+
return budgeted(mission);
|
|
82
140
|
}
|
|
83
141
|
const head = tidy(title ?? '');
|
|
84
|
-
const
|
|
142
|
+
const headWords = wordsOf(head).length;
|
|
143
|
+
|
|
144
|
+
// The title is the stronger signal, so it is spent first and never truncated
|
|
145
|
+
// to make room for the lead. Once it names a subject on its own, it IS the
|
|
146
|
+
// phrase.
|
|
147
|
+
if (headWords >= TITLE_ENOUGH) {
|
|
148
|
+
const out = budgeted(head);
|
|
149
|
+
return out.length < MIN_LENGTH ? '' : out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// A thin title spends what it did not use on the lead's first clause.
|
|
153
|
+
const body = clampToWords(firstClause(tidy(lead ?? '')), WORD_BUDGET - headWords);
|
|
85
154
|
const joined = [head, body].filter(Boolean).join('. ');
|
|
86
155
|
if (joined.length < MIN_LENGTH) return '';
|
|
87
|
-
if (joined.length <= MAX_LENGTH) return joined;
|
|
88
|
-
|
|
89
|
-
// Over budget: keep the title whole if it fits, since it is the stronger
|
|
90
|
-
// signal, and spend whatever is left on the lead.
|
|
91
|
-
if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);
|
|
92
156
|
return clampToWord(joined, MAX_LENGTH);
|
|
93
157
|
}
|
|
94
158
|
//# sourceMappingURL=composeSemantic.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["MAX_LENGTH","MIN_LENGTH","stripMarkdown","raw","replace","tidy","trim","clampToWord","max","length","cut","slice","lastBreak","lastIndexOf","out","firstClause","m","match","composeSemantic","title","lead","sectionSemantic","mission","head","body","joined","filter","Boolean","join"],"sources":["../../../src/image/composeSemantic.ts"],"sourcesContent":["/**\n * Turn a section's own words into a retrieval phrase.\n *\n * The section hands over its RAW props — a title, a lead paragraph — not a\n * query string. Composing is this module's job, in one place, because the two\n * call sites that do it today invent their own phrase, do not agree with each\n * other, and neither is tested:\n *\n * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144\n * `web5://image/${title}` — raw, so `**Nike** collections`\n * is searched WITH its asterisks\n * w5-client-circana/src/components/sections/ResearchSection.tsx:68\n * `web5://image/${encodeURIComponent(title)}` — same idea, escaped\n *\n * Centralising it is most of what the text-derived case is worth.\n *\n * Lifted from the seed client package (ADR 0225): it started there because the\n * CDN build externalises core, so a core change could not be seen through\n * `?clientBundleUrl=` until it had been published. 0223 always assigned it\n * here, and this is that move.\n */\n\n/** Longer than this and the tail stops helping retrieval and starts diluting it. */\nconst MAX_LENGTH = 120;\n\n/** Below this there is no query worth sending — better to render no picture\n * than to retrieve on a word like \"More\". */\nconst MIN_LENGTH = 3;\n\nconst stripMarkdown = (raw: string): string =>\n raw\n // images before links:  would otherwise leave a stray `!`\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // emphasis / strong / strike / inline code — keep the words, drop the marks\n .replace(/(\\*\\*\\*|\\*\\*|\\*|___|__|_|~~|`)/g, '')\n // leading heading hashes and blockquote marks\n .replace(/^\\s*#{1,6}\\s*/gm, '')\n .replace(/^\\s*>\\s?/gm, '')\n // html tags an author or the parser may have left behind\n .replace(/<[^>]+>/g, ' ');\n\nconst tidy = (raw: string): string =>\n stripMarkdown(raw)\n .replace(/\\s+/g, ' ')\n .trim()\n // trailing punctuation is noise in a retrieval phrase\n .replace(/[\\s.,;:!?—–-]+$/g, '')\n .trim();\n\n/**\n * Clamp without cutting a word in half. Slicing at a fixed length leaves\n * fragments like \"the kinds of product\" — observed in the first real run — and\n * a dangling partial word is noise in a retrieval phrase, not a shorter\n * version of it. Falls back to a hard slice only when the budget cannot fit\n * even one word.\n */\nconst clampToWord = (raw: string, max: number): string => {\n if (raw.length <= max) {\n return raw;\n }\n // +1 so a boundary landing exactly on the budget still counts as a break.\n const cut = raw.slice(0, max + 1);\n const lastBreak = cut.lastIndexOf(' ');\n const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);\n return out.replace(/[\\s.,;:!?—–-]+$/g, '').trim();\n};\n\n/** The first clause carries the subject; what follows is usually qualification. */\nconst firstClause = (raw: string): string => {\n const m = raw.match(/^[^.!?;]+/);\n return (m ? m[0] : raw).trim();\n};\n\nexport interface ComposeSemanticInput {\n /** The section's headline. */\n title?: string;\n /** Its lead paragraph or body copy. */\n lead?: string;\n /** The descriptor's own `semantic` / mission, when the host supplies one.\n * Outranks the prose: it is what the orchestrator meant the section to be\n * about, which is a better subject than what it happened to say. */\n sectionSemantic?: string;\n}\n\n/**\n * Returns the phrase, or `''` when what survives is too thin to retrieve on —\n * the caller then declares no slot at all rather than sending a bad query.\n */\nexport function composeSemantic({\n title,\n lead,\n sectionSemantic,\n}: ComposeSemanticInput): string {\n // A mission, when there is one, is the whole answer.\n const mission = tidy(sectionSemantic ?? '');\n if (mission.length >= MIN_LENGTH) {\n return clampToWord(mission, MAX_LENGTH);\n }\n\n const head = tidy(title ?? '');\n const body = firstClause(tidy(lead ?? ''));\n\n const joined = [head, body].filter(Boolean).join('. ');\n if (joined.length < MIN_LENGTH) return '';\n\n if (joined.length <= MAX_LENGTH) return joined;\n\n // Over budget: keep the title whole if it fits, since it is the stronger\n // signal, and spend whatever is left on the lead.\n if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);\n return clampToWord(joined, MAX_LENGTH);\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["WORD_BUDGET","TITLE_ENOUGH","MAX_LENGTH","MIN_LENGTH","stripMarkdown","raw","replace","trimTail","trim","tidy","wordsOf","split","filter","word","length","clampToWords","max","words","slice","join","clampToWord","cut","lastBreak","lastIndexOf","out","budgeted","firstClause","m","match","composeSemantic","title","lead","sectionSemantic","mission","head","headWords","body","joined","Boolean"],"sources":["../../../src/image/composeSemantic.ts"],"sourcesContent":["/**\n * Turn a section's own words into a retrieval phrase.\n *\n * The section hands over its RAW props — a title, a lead paragraph — not a\n * query string. Composing is this module's job, in one place, because the two\n * call sites that do it today invent their own phrase, do not agree with each\n * other, and neither is tested:\n *\n * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144\n * `web5://image/${title}` — raw, so `**Nike** collections`\n * is searched WITH its asterisks\n * w5-client-circana/src/components/sections/ResearchSection.tsx:68\n * `web5://image/${encodeURIComponent(title)}` — same idea, escaped\n *\n * Centralising it is most of what the text-derived case is worth.\n *\n * Lifted from the seed client package (ADR 0225): it started there because the\n * CDN build externalises core, so a core change could not be seen through\n * `?clientBundleUrl=` until it had been published. 0223 always assigned it\n * here, and this is that move.\n *\n * WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)\n * ------------------------------------------------------\n * It buys **recall, and only recall**. In media-manager an editorial slot\n * retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates\n * (`CandidateGather.planFor`), scoped by a filter that is just \"this site's\n * images\" (`buildFilter`) — so the phrase ranks the corpus rather than gating\n * it. Which picture then WINS is decided by `SlotScorer`, whose every term is\n * geometric (crop fit, background, subject, text safety, resolution). Vespa's\n * relevance score is not one of them, and the pool's order is discarded by the\n * assignment solve. The phrase's entire job is choosing which 30 images reach\n * the scorer.\n *\n * That is why it is budgeted in WORDS. The documents it is matched against are\n * short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,\n * label…`, usually under ten words — and retrieval is hybrid, BM25 blended with\n * vector closeness. A headline plus a clause of marketing prose is the wrong\n * shape against that on both halves: the vector drifts toward the mean of\n * several topics instead of sitting on the subject, and BM25 spends term weight\n * on filler that pulls in whatever else on the site happens to carry those\n * words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)\n * happen on the document side only.\n *\n * A phrase that is too specific costs a worse 30, never a blank section: the\n * site-wide widening in `CandidateGather.poolFor` is entity-only because an\n * editorial pool empties only when the site has no images at all.\n *\n * CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text\n * slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —\n * every text-derived slot in the product gets a new id. Not a place for\n * cosmetic edits.\n */\n\n/**\n * The phrase's real budget, in whole words — the unit that matters, because\n * what it is matched against is a short bag of nouns and not a paragraph.\n */\nconst WORD_BUDGET = 10;\n\n/**\n * A title of this many words already names its own subject, so the lead adds\n * dilution rather than signal and is dropped. Below it the title may be as thin\n * as \"Our picks\" or \"Why it matters\", which names nothing retrievable on its\n * own — that is the case the lead exists for.\n */\nconst TITLE_ENOUGH = 5;\n\n/** A character backstop under the word budget: ten words is normally well\n * inside it, but one pathological unbroken \"word\" is not. */\nconst MAX_LENGTH = 120;\n\n/** Below this there is no query worth sending — better to render no picture\n * than to retrieve on a word like \"More\". */\nconst MIN_LENGTH = 3;\n\nconst stripMarkdown = (raw: string): string =>\n raw\n // images before links:  would otherwise leave a stray `!`\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // emphasis / strong / strike / inline code — keep the words, drop the marks\n .replace(/(\\*\\*\\*|\\*\\*|\\*|___|__|_|~~|`)/g, '')\n // leading heading hashes and blockquote marks\n .replace(/^\\s*#{1,6}\\s*/gm, '')\n .replace(/^\\s*>\\s?/gm, '')\n // html tags an author or the parser may have left behind\n .replace(/<[^>]+>/g, ' ');\n\nconst trimTail = (raw: string): string =>\n raw.replace(/[\\s.,;:!?—–-]+$/g, '').trim();\n\nconst tidy = (raw: string): string =>\n trimTail(stripMarkdown(raw).replace(/\\s+/g, ' ').trim());\n\nconst wordsOf = (raw: string): string[] =>\n raw.split(' ').filter((word) => word.length > 0);\n\n/** Keep at most `max` whole words. The unit the budget is actually stated in. */\nconst clampToWords = (raw: string, max: number): string => {\n const words = wordsOf(raw);\n if (words.length <= max) {\n return raw;\n }\n return trimTail(words.slice(0, max).join(' '));\n};\n\n/**\n * Clamp without cutting a word in half. Slicing at a fixed length leaves\n * fragments like \"the kinds of product\" — observed in the first real run — and\n * a dangling partial word is noise in a retrieval phrase, not a shorter\n * version of it. Falls back to a hard slice only when the budget cannot fit\n * even one word.\n */\nconst clampToWord = (raw: string, max: number): string => {\n if (raw.length <= max) {\n return raw;\n }\n // +1 so a boundary landing exactly on the budget still counts as a break.\n const cut = raw.slice(0, max + 1);\n const lastBreak = cut.lastIndexOf(' ');\n const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);\n return trimTail(out);\n};\n\n/** The word budget first, then the character backstop under it. */\nconst budgeted = (raw: string): string =>\n clampToWord(clampToWords(raw, WORD_BUDGET), MAX_LENGTH);\n\n/** The first clause carries the subject; what follows is usually qualification. */\nconst firstClause = (raw: string): string => {\n const m = raw.match(/^[^.!?;]+/);\n return (m ? m[0] : raw).trim();\n};\n\nexport interface ComposeSemanticInput {\n /** The section's headline. */\n title?: string;\n /** Its lead paragraph or body copy. */\n lead?: string;\n /** The descriptor's own `semantic` / mission, when the host supplies one.\n * Outranks the prose: it is what the orchestrator meant the section to be\n * about, which is a better subject than what it happened to say. */\n sectionSemantic?: string;\n}\n\n/**\n * Returns the phrase, or `''` when what survives is too thin to retrieve on —\n * the caller then declares no slot at all rather than sending a bad query.\n */\nexport function composeSemantic({\n title,\n lead,\n sectionSemantic,\n}: ComposeSemanticInput): string {\n // A mission, when there is one, is the whole answer.\n const mission = tidy(sectionSemantic ?? '');\n if (mission.length >= MIN_LENGTH) {\n return budgeted(mission);\n }\n\n const head = tidy(title ?? '');\n const headWords = wordsOf(head).length;\n\n // The title is the stronger signal, so it is spent first and never truncated\n // to make room for the lead. Once it names a subject on its own, it IS the\n // phrase.\n if (headWords >= TITLE_ENOUGH) {\n const out = budgeted(head);\n return out.length < MIN_LENGTH ? '' : out;\n }\n\n // A thin title spends what it did not use on the lead's first clause.\n const body = clampToWords(\n firstClause(tidy(lead ?? '')),\n WORD_BUDGET - headWords,\n );\n\n const joined = [head, body].filter(Boolean).join('. ');\n if (joined.length < MIN_LENGTH) return '';\n\n return clampToWord(joined, MAX_LENGTH);\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
@import '../components/ui/PromptEntryEmptyState.css';
|
|
3
3
|
@import '../components/ui/FeedbackBar.css';
|
|
4
4
|
@import '../components/ui/AiDisclosure.css';
|
|
5
|
-
@import '../components/ui/PoweredByBadge.css';
|
|
6
5
|
@import '../components/ui/UserQuery.css';
|
|
7
6
|
@import '../components/ui/PlacementLoader.css';
|
|
8
7
|
@import '../components/placement/PlacementResponseRenderer.css';
|
|
@@ -9,6 +9,14 @@ export const DEFAULT_POWERED_BY_HREF = 'https://www.nisoweb.ai';
|
|
|
9
9
|
* Rendering is the caller's decision — this component says nothing about plans.
|
|
10
10
|
* The AI page mounts it only when the site is on the Free tier (see
|
|
11
11
|
* `shouldShowPoweredBy`); any paid package removes it.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: unlike every other component here, its CSS is NOT in
|
|
14
|
+
* `styles/components.css`. That file ships in the client bundle, and this
|
|
15
|
+
* component is mounted from the app bundle — the two are deployed
|
|
16
|
+
* independently, and splitting it across them shipped a naked, unstyled credit
|
|
17
|
+
* to a live storefront. Its stylesheet lives with the shell that mounts it, in
|
|
18
|
+
* `web50-server-ui/src/components/App.css`. A future client that mounts this
|
|
19
|
+
* itself has to bring its own copy, or move the rules somewhere both can see.
|
|
12
20
|
*/
|
|
13
21
|
export const PoweredByBadge = _ref => {
|
|
14
22
|
let {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","cn","DEFAULT_POWERED_BY_HREF","PoweredByBadge","_ref","label","brand","href","target","onClick","className","rel","undefined","createElement"],"sources":["../../../../src/components/ui/PoweredByBadge.tsx"],"sourcesContent":["import React, { FC } from 'react';\nimport { cn } from '../../lib/utils';\n\nexport interface PoweredByBadgeProps {\n /** Muted lead-in. Default: \"Powered by\". */\n label?: string;\n /** The emphasised brand name, and the link's visible anchor. Default: \"Niso\". */\n brand?: string;\n /** URL the badge opens. Default: the Niso marketing site. */\n href?: string;\n /** Override the link target. Default: `_blank` (opens in a new tab). */\n target?: '_blank' | '_self' | '_parent' | '_top';\n /** Click handler invoked before navigation. Use to fire analytics. */\n onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;\n className?: string;\n}\n\nexport const DEFAULT_POWERED_BY_HREF = 'https://www.nisoweb.ai';\n\n/**\n * The free-tier credit bar (B2B-4574): a full-width strip at the very bottom of\n * the AI page reading \"Powered by Niso\", with the brand name linking out.\n *\n * Rendering is the caller's decision — this component says nothing about plans.\n * The AI page mounts it only when the site is on the Free tier (see\n * `shouldShowPoweredBy`); any paid package removes it.\n */\nexport const PoweredByBadge: FC<PoweredByBadgeProps> = ({\n label = 'Powered by',\n brand = 'Niso',\n href = DEFAULT_POWERED_BY_HREF,\n target = '_blank',\n onClick,\n className,\n}) => {\n const rel = target === '_blank' ? 'noopener noreferrer' : undefined;\n\n return (\n <div className={cn('web5-powered-by', className)}>\n <a\n className=\"web5-powered-by__link\"\n href={href}\n target={target}\n rel={rel}\n onClick={onClick}\n // The visible text splits across two spans for the two-tone treatment,\n // so spell the whole thing out once for assistive tech — including the\n // new tab, which is otherwise unannounced.\n aria-label={`${label} ${brand}${\n target === '_blank' ? ' (opens in a new tab)' : ''\n }`}\n >\n <span className=\"web5-powered-by__label\">{label}</span>{' '}\n <span className=\"web5-powered-by__brand\">{brand}</span>\n </a>\n </div>\n );\n};\n"],"mappings":"AAAA,OAAOA,KAAK,MAAc,OAAO;AACjC,SAASC,EAAE,QAAQ,iBAAiB;AAgBpC,OAAO,MAAMC,uBAAuB,GAAG,wBAAwB;;AAE/D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,cAAuC,GAAGC,IAAA,IAOjD;EAAA,IAPkD;IACtDC,KAAK,GAAG,YAAY;IACpBC,KAAK,GAAG,MAAM;IACdC,IAAI,GAAGL,uBAAuB;IAC9BM,MAAM,GAAG,QAAQ;IACjBC,OAAO;IACPC;EACF,CAAC,GAAAN,IAAA;EACC,MAAMO,GAAG,GAAGH,MAAM,KAAK,QAAQ,GAAG,qBAAqB,GAAGI,SAAS;EAEnE,oBACEZ,KAAA,CAAAa,aAAA;IAAKH,SAAS,EAAET,EAAE,CAAC,iBAAiB,EAAES,SAAS;EAAE,gBAC/CV,KAAA,CAAAa,aAAA;IACEH,SAAS,EAAC,uBAAuB;IACjCH,IAAI,EAAEA,IAAK;IACXC,MAAM,EAAEA,MAAO;IACfG,GAAG,EAAEA,GAAI;IACTF,OAAO,EAAEA;IACT;IACA;IACA;IAAA;IACA,cAAY,GAAGJ,KAAK,IAAIC,KAAK,GAC3BE,MAAM,KAAK,QAAQ,GAAG,uBAAuB,GAAG,EAAE;EACjD,gBAEHR,KAAA,CAAAa,aAAA;IAAMH,SAAS,EAAC;EAAwB,GAAEL,KAAY,CAAC,EAAC,GAAG,eAC3DL,KAAA,CAAAa,aAAA;IAAMH,SAAS,EAAC;EAAwB,GAAEJ,KAAY,CACrD,CACA,CAAC;AAEV,CAAC","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["React","cn","DEFAULT_POWERED_BY_HREF","PoweredByBadge","_ref","label","brand","href","target","onClick","className","rel","undefined","createElement"],"sources":["../../../../src/components/ui/PoweredByBadge.tsx"],"sourcesContent":["import React, { FC } from 'react';\nimport { cn } from '../../lib/utils';\n\nexport interface PoweredByBadgeProps {\n /** Muted lead-in. Default: \"Powered by\". */\n label?: string;\n /** The emphasised brand name, and the link's visible anchor. Default: \"Niso\". */\n brand?: string;\n /** URL the badge opens. Default: the Niso marketing site. */\n href?: string;\n /** Override the link target. Default: `_blank` (opens in a new tab). */\n target?: '_blank' | '_self' | '_parent' | '_top';\n /** Click handler invoked before navigation. Use to fire analytics. */\n onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;\n className?: string;\n}\n\nexport const DEFAULT_POWERED_BY_HREF = 'https://www.nisoweb.ai';\n\n/**\n * The free-tier credit bar (B2B-4574): a full-width strip at the very bottom of\n * the AI page reading \"Powered by Niso\", with the brand name linking out.\n *\n * Rendering is the caller's decision — this component says nothing about plans.\n * The AI page mounts it only when the site is on the Free tier (see\n * `shouldShowPoweredBy`); any paid package removes it.\n *\n * NOTE: unlike every other component here, its CSS is NOT in\n * `styles/components.css`. That file ships in the client bundle, and this\n * component is mounted from the app bundle — the two are deployed\n * independently, and splitting it across them shipped a naked, unstyled credit\n * to a live storefront. Its stylesheet lives with the shell that mounts it, in\n * `web50-server-ui/src/components/App.css`. A future client that mounts this\n * itself has to bring its own copy, or move the rules somewhere both can see.\n */\nexport const PoweredByBadge: FC<PoweredByBadgeProps> = ({\n label = 'Powered by',\n brand = 'Niso',\n href = DEFAULT_POWERED_BY_HREF,\n target = '_blank',\n onClick,\n className,\n}) => {\n const rel = target === '_blank' ? 'noopener noreferrer' : undefined;\n\n return (\n <div className={cn('web5-powered-by', className)}>\n <a\n className=\"web5-powered-by__link\"\n href={href}\n target={target}\n rel={rel}\n onClick={onClick}\n // The visible text splits across two spans for the two-tone treatment,\n // so spell the whole thing out once for assistive tech — including the\n // new tab, which is otherwise unannounced.\n aria-label={`${label} ${brand}${\n target === '_blank' ? ' (opens in a new tab)' : ''\n }`}\n >\n <span className=\"web5-powered-by__label\">{label}</span>{' '}\n <span className=\"web5-powered-by__brand\">{brand}</span>\n </a>\n </div>\n );\n};\n"],"mappings":"AAAA,OAAOA,KAAK,MAAc,OAAO;AACjC,SAASC,EAAE,QAAQ,iBAAiB;AAgBpC,OAAO,MAAMC,uBAAuB,GAAG,wBAAwB;;AAE/D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,cAAuC,GAAGC,IAAA,IAOjD;EAAA,IAPkD;IACtDC,KAAK,GAAG,YAAY;IACpBC,KAAK,GAAG,MAAM;IACdC,IAAI,GAAGL,uBAAuB;IAC9BM,MAAM,GAAG,QAAQ;IACjBC,OAAO;IACPC;EACF,CAAC,GAAAN,IAAA;EACC,MAAMO,GAAG,GAAGH,MAAM,KAAK,QAAQ,GAAG,qBAAqB,GAAGI,SAAS;EAEnE,oBACEZ,KAAA,CAAAa,aAAA;IAAKH,SAAS,EAAET,EAAE,CAAC,iBAAiB,EAAES,SAAS;EAAE,gBAC/CV,KAAA,CAAAa,aAAA;IACEH,SAAS,EAAC,uBAAuB;IACjCH,IAAI,EAAEA,IAAK;IACXC,MAAM,EAAEA,MAAO;IACfG,GAAG,EAAEA,GAAI;IACTF,OAAO,EAAEA;IACT;IACA;IACA;IAAA;IACA,cAAY,GAAGJ,KAAK,IAAIC,KAAK,GAC3BE,MAAM,KAAK,QAAQ,GAAG,uBAAuB,GAAG,EAAE;EACjD,gBAEHR,KAAA,CAAAa,aAAA;IAAMH,SAAS,EAAC;EAAwB,GAAEL,KAAY,CAAC,EAAC,GAAG,eAC3DL,KAAA,CAAAa,aAAA;IAAMH,SAAS,EAAC;EAAwB,GAAEJ,KAAY,CACrD,CACA,CAAC;AAEV,CAAC","ignoreList":[]}
|
|
@@ -18,9 +18,55 @@
|
|
|
18
18
|
* CDN build externalises core, so a core change could not be seen through
|
|
19
19
|
* `?clientBundleUrl=` until it had been published. 0223 always assigned it
|
|
20
20
|
* here, and this is that move.
|
|
21
|
+
*
|
|
22
|
+
* WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)
|
|
23
|
+
* ------------------------------------------------------
|
|
24
|
+
* It buys **recall, and only recall**. In media-manager an editorial slot
|
|
25
|
+
* retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates
|
|
26
|
+
* (`CandidateGather.planFor`), scoped by a filter that is just "this site's
|
|
27
|
+
* images" (`buildFilter`) — so the phrase ranks the corpus rather than gating
|
|
28
|
+
* it. Which picture then WINS is decided by `SlotScorer`, whose every term is
|
|
29
|
+
* geometric (crop fit, background, subject, text safety, resolution). Vespa's
|
|
30
|
+
* relevance score is not one of them, and the pool's order is discarded by the
|
|
31
|
+
* assignment solve. The phrase's entire job is choosing which 30 images reach
|
|
32
|
+
* the scorer.
|
|
33
|
+
*
|
|
34
|
+
* That is why it is budgeted in WORDS. The documents it is matched against are
|
|
35
|
+
* short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,
|
|
36
|
+
* label…`, usually under ten words — and retrieval is hybrid, BM25 blended with
|
|
37
|
+
* vector closeness. A headline plus a clause of marketing prose is the wrong
|
|
38
|
+
* shape against that on both halves: the vector drifts toward the mean of
|
|
39
|
+
* several topics instead of sitting on the subject, and BM25 spends term weight
|
|
40
|
+
* on filler that pulls in whatever else on the site happens to carry those
|
|
41
|
+
* words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)
|
|
42
|
+
* happen on the document side only.
|
|
43
|
+
*
|
|
44
|
+
* A phrase that is too specific costs a worse 30, never a blank section: the
|
|
45
|
+
* site-wide widening in `CandidateGather.poolFor` is entity-only because an
|
|
46
|
+
* editorial pool empties only when the site has no images at all.
|
|
47
|
+
*
|
|
48
|
+
* CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text
|
|
49
|
+
* slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —
|
|
50
|
+
* every text-derived slot in the product gets a new id. Not a place for
|
|
51
|
+
* cosmetic edits.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The phrase's real budget, in whole words — the unit that matters, because
|
|
56
|
+
* what it is matched against is a short bag of nouns and not a paragraph.
|
|
57
|
+
*/
|
|
58
|
+
const WORD_BUDGET = 10;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A title of this many words already names its own subject, so the lead adds
|
|
62
|
+
* dilution rather than signal and is dropped. Below it the title may be as thin
|
|
63
|
+
* as "Our picks" or "Why it matters", which names nothing retrievable on its
|
|
64
|
+
* own — that is the case the lead exists for.
|
|
21
65
|
*/
|
|
66
|
+
const TITLE_ENOUGH = 5;
|
|
22
67
|
|
|
23
|
-
/**
|
|
68
|
+
/** A character backstop under the word budget: ten words is normally well
|
|
69
|
+
* inside it, but one pathological unbroken "word" is not. */
|
|
24
70
|
const MAX_LENGTH = 120;
|
|
25
71
|
|
|
26
72
|
/** Below this there is no query worth sending — better to render no picture
|
|
@@ -35,9 +81,18 @@ const stripMarkdown = raw => raw
|
|
|
35
81
|
.replace(/^\s*#{1,6}\s*/gm, '').replace(/^\s*>\s?/gm, '')
|
|
36
82
|
// html tags an author or the parser may have left behind
|
|
37
83
|
.replace(/<[^>]+>/g, ' ');
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
.
|
|
84
|
+
const trimTail = raw => raw.replace(/[\s.,;:!?—–-]+$/g, '').trim();
|
|
85
|
+
const tidy = raw => trimTail(stripMarkdown(raw).replace(/\s+/g, ' ').trim());
|
|
86
|
+
const wordsOf = raw => raw.split(' ').filter(word => word.length > 0);
|
|
87
|
+
|
|
88
|
+
/** Keep at most `max` whole words. The unit the budget is actually stated in. */
|
|
89
|
+
const clampToWords = (raw, max) => {
|
|
90
|
+
const words = wordsOf(raw);
|
|
91
|
+
if (words.length <= max) {
|
|
92
|
+
return raw;
|
|
93
|
+
}
|
|
94
|
+
return trimTail(words.slice(0, max).join(' '));
|
|
95
|
+
};
|
|
41
96
|
|
|
42
97
|
/**
|
|
43
98
|
* Clamp without cutting a word in half. Slicing at a fixed length leaves
|
|
@@ -54,9 +109,12 @@ const clampToWord = (raw, max) => {
|
|
|
54
109
|
const cut = raw.slice(0, max + 1);
|
|
55
110
|
const lastBreak = cut.lastIndexOf(' ');
|
|
56
111
|
const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);
|
|
57
|
-
return out
|
|
112
|
+
return trimTail(out);
|
|
58
113
|
};
|
|
59
114
|
|
|
115
|
+
/** The word budget first, then the character backstop under it. */
|
|
116
|
+
const budgeted = raw => clampToWord(clampToWords(raw, WORD_BUDGET), MAX_LENGTH);
|
|
117
|
+
|
|
60
118
|
/** The first clause carries the subject; what follows is usually qualification. */
|
|
61
119
|
const firstClause = raw => {
|
|
62
120
|
const m = raw.match(/^[^.!?;]+/);
|
|
@@ -75,17 +133,23 @@ export function composeSemantic(_ref) {
|
|
|
75
133
|
// A mission, when there is one, is the whole answer.
|
|
76
134
|
const mission = tidy(sectionSemantic ?? '');
|
|
77
135
|
if (mission.length >= MIN_LENGTH) {
|
|
78
|
-
return
|
|
136
|
+
return budgeted(mission);
|
|
79
137
|
}
|
|
80
138
|
const head = tidy(title ?? '');
|
|
81
|
-
const
|
|
139
|
+
const headWords = wordsOf(head).length;
|
|
140
|
+
|
|
141
|
+
// The title is the stronger signal, so it is spent first and never truncated
|
|
142
|
+
// to make room for the lead. Once it names a subject on its own, it IS the
|
|
143
|
+
// phrase.
|
|
144
|
+
if (headWords >= TITLE_ENOUGH) {
|
|
145
|
+
const out = budgeted(head);
|
|
146
|
+
return out.length < MIN_LENGTH ? '' : out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A thin title spends what it did not use on the lead's first clause.
|
|
150
|
+
const body = clampToWords(firstClause(tidy(lead ?? '')), WORD_BUDGET - headWords);
|
|
82
151
|
const joined = [head, body].filter(Boolean).join('. ');
|
|
83
152
|
if (joined.length < MIN_LENGTH) return '';
|
|
84
|
-
if (joined.length <= MAX_LENGTH) return joined;
|
|
85
|
-
|
|
86
|
-
// Over budget: keep the title whole if it fits, since it is the stronger
|
|
87
|
-
// signal, and spend whatever is left on the lead.
|
|
88
|
-
if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);
|
|
89
153
|
return clampToWord(joined, MAX_LENGTH);
|
|
90
154
|
}
|
|
91
155
|
//# sourceMappingURL=composeSemantic.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["MAX_LENGTH","MIN_LENGTH","stripMarkdown","raw","replace","tidy","trim","clampToWord","max","length","cut","slice","lastBreak","lastIndexOf","out","firstClause","m","match","composeSemantic","_ref","title","lead","sectionSemantic","mission","head","body","joined","filter","Boolean","join"],"sources":["../../../src/image/composeSemantic.ts"],"sourcesContent":["/**\n * Turn a section's own words into a retrieval phrase.\n *\n * The section hands over its RAW props — a title, a lead paragraph — not a\n * query string. Composing is this module's job, in one place, because the two\n * call sites that do it today invent their own phrase, do not agree with each\n * other, and neither is tested:\n *\n * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144\n * `web5://image/${title}` — raw, so `**Nike** collections`\n * is searched WITH its asterisks\n * w5-client-circana/src/components/sections/ResearchSection.tsx:68\n * `web5://image/${encodeURIComponent(title)}` — same idea, escaped\n *\n * Centralising it is most of what the text-derived case is worth.\n *\n * Lifted from the seed client package (ADR 0225): it started there because the\n * CDN build externalises core, so a core change could not be seen through\n * `?clientBundleUrl=` until it had been published. 0223 always assigned it\n * here, and this is that move.\n */\n\n/** Longer than this and the tail stops helping retrieval and starts diluting it. */\nconst MAX_LENGTH = 120;\n\n/** Below this there is no query worth sending — better to render no picture\n * than to retrieve on a word like \"More\". */\nconst MIN_LENGTH = 3;\n\nconst stripMarkdown = (raw: string): string =>\n raw\n // images before links:  would otherwise leave a stray `!`\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // emphasis / strong / strike / inline code — keep the words, drop the marks\n .replace(/(\\*\\*\\*|\\*\\*|\\*|___|__|_|~~|`)/g, '')\n // leading heading hashes and blockquote marks\n .replace(/^\\s*#{1,6}\\s*/gm, '')\n .replace(/^\\s*>\\s?/gm, '')\n // html tags an author or the parser may have left behind\n .replace(/<[^>]+>/g, ' ');\n\nconst tidy = (raw: string): string =>\n stripMarkdown(raw)\n .replace(/\\s+/g, ' ')\n .trim()\n // trailing punctuation is noise in a retrieval phrase\n .replace(/[\\s.,;:!?—–-]+$/g, '')\n .trim();\n\n/**\n * Clamp without cutting a word in half. Slicing at a fixed length leaves\n * fragments like \"the kinds of product\" — observed in the first real run — and\n * a dangling partial word is noise in a retrieval phrase, not a shorter\n * version of it. Falls back to a hard slice only when the budget cannot fit\n * even one word.\n */\nconst clampToWord = (raw: string, max: number): string => {\n if (raw.length <= max) {\n return raw;\n }\n // +1 so a boundary landing exactly on the budget still counts as a break.\n const cut = raw.slice(0, max + 1);\n const lastBreak = cut.lastIndexOf(' ');\n const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);\n return out.replace(/[\\s.,;:!?—–-]+$/g, '').trim();\n};\n\n/** The first clause carries the subject; what follows is usually qualification. */\nconst firstClause = (raw: string): string => {\n const m = raw.match(/^[^.!?;]+/);\n return (m ? m[0] : raw).trim();\n};\n\nexport interface ComposeSemanticInput {\n /** The section's headline. */\n title?: string;\n /** Its lead paragraph or body copy. */\n lead?: string;\n /** The descriptor's own `semantic` / mission, when the host supplies one.\n * Outranks the prose: it is what the orchestrator meant the section to be\n * about, which is a better subject than what it happened to say. */\n sectionSemantic?: string;\n}\n\n/**\n * Returns the phrase, or `''` when what survives is too thin to retrieve on —\n * the caller then declares no slot at all rather than sending a bad query.\n */\nexport function composeSemantic({\n title,\n lead,\n sectionSemantic,\n}: ComposeSemanticInput): string {\n // A mission, when there is one, is the whole answer.\n const mission = tidy(sectionSemantic ?? '');\n if (mission.length >= MIN_LENGTH) {\n return clampToWord(mission, MAX_LENGTH);\n }\n\n const head = tidy(title ?? '');\n const body = firstClause(tidy(lead ?? ''));\n\n const joined = [head, body].filter(Boolean).join('. ');\n if (joined.length < MIN_LENGTH) return '';\n\n if (joined.length <= MAX_LENGTH) return joined;\n\n // Over budget: keep the title whole if it fits, since it is the stronger\n // signal, and spend whatever is left on the lead.\n if (head.length >= MAX_LENGTH) return clampToWord(head, MAX_LENGTH);\n return clampToWord(joined, MAX_LENGTH);\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["WORD_BUDGET","TITLE_ENOUGH","MAX_LENGTH","MIN_LENGTH","stripMarkdown","raw","replace","trimTail","trim","tidy","wordsOf","split","filter","word","length","clampToWords","max","words","slice","join","clampToWord","cut","lastBreak","lastIndexOf","out","budgeted","firstClause","m","match","composeSemantic","_ref","title","lead","sectionSemantic","mission","head","headWords","body","joined","Boolean"],"sources":["../../../src/image/composeSemantic.ts"],"sourcesContent":["/**\n * Turn a section's own words into a retrieval phrase.\n *\n * The section hands over its RAW props — a title, a lead paragraph — not a\n * query string. Composing is this module's job, in one place, because the two\n * call sites that do it today invent their own phrase, do not agree with each\n * other, and neither is tested:\n *\n * w5-client-circana/src/components/sections/GenericEntitySection.tsx:144\n * `web5://image/${title}` — raw, so `**Nike** collections`\n * is searched WITH its asterisks\n * w5-client-circana/src/components/sections/ResearchSection.tsx:68\n * `web5://image/${encodeURIComponent(title)}` — same idea, escaped\n *\n * Centralising it is most of what the text-derived case is worth.\n *\n * Lifted from the seed client package (ADR 0225): it started there because the\n * CDN build externalises core, so a core change could not be seen through\n * `?clientBundleUrl=` until it had been published. 0223 always assigned it\n * here, and this is that move.\n *\n * WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)\n * ------------------------------------------------------\n * It buys **recall, and only recall**. In media-manager an editorial slot\n * retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates\n * (`CandidateGather.planFor`), scoped by a filter that is just \"this site's\n * images\" (`buildFilter`) — so the phrase ranks the corpus rather than gating\n * it. Which picture then WINS is decided by `SlotScorer`, whose every term is\n * geometric (crop fit, background, subject, text safety, resolution). Vespa's\n * relevance score is not one of them, and the pool's order is discarded by the\n * assignment solve. The phrase's entire job is choosing which 30 images reach\n * the scorer.\n *\n * That is why it is budgeted in WORDS. The documents it is matched against are\n * short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,\n * label…`, usually under ten words — and retrieval is hybrid, BM25 blended with\n * vector closeness. A headline plus a clause of marketing prose is the wrong\n * shape against that on both halves: the vector drifts toward the mean of\n * several topics instead of sitting on the subject, and BM25 spends term weight\n * on filler that pulls in whatever else on the site happens to carry those\n * words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)\n * happen on the document side only.\n *\n * A phrase that is too specific costs a worse 30, never a blank section: the\n * site-wide widening in `CandidateGather.poolFor` is entity-only because an\n * editorial pool empties only when the site has no images at all.\n *\n * CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text\n * slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —\n * every text-derived slot in the product gets a new id. Not a place for\n * cosmetic edits.\n */\n\n/**\n * The phrase's real budget, in whole words — the unit that matters, because\n * what it is matched against is a short bag of nouns and not a paragraph.\n */\nconst WORD_BUDGET = 10;\n\n/**\n * A title of this many words already names its own subject, so the lead adds\n * dilution rather than signal and is dropped. Below it the title may be as thin\n * as \"Our picks\" or \"Why it matters\", which names nothing retrievable on its\n * own — that is the case the lead exists for.\n */\nconst TITLE_ENOUGH = 5;\n\n/** A character backstop under the word budget: ten words is normally well\n * inside it, but one pathological unbroken \"word\" is not. */\nconst MAX_LENGTH = 120;\n\n/** Below this there is no query worth sending — better to render no picture\n * than to retrieve on a word like \"More\". */\nconst MIN_LENGTH = 3;\n\nconst stripMarkdown = (raw: string): string =>\n raw\n // images before links:  would otherwise leave a stray `!`\n .replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1')\n // emphasis / strong / strike / inline code — keep the words, drop the marks\n .replace(/(\\*\\*\\*|\\*\\*|\\*|___|__|_|~~|`)/g, '')\n // leading heading hashes and blockquote marks\n .replace(/^\\s*#{1,6}\\s*/gm, '')\n .replace(/^\\s*>\\s?/gm, '')\n // html tags an author or the parser may have left behind\n .replace(/<[^>]+>/g, ' ');\n\nconst trimTail = (raw: string): string =>\n raw.replace(/[\\s.,;:!?—–-]+$/g, '').trim();\n\nconst tidy = (raw: string): string =>\n trimTail(stripMarkdown(raw).replace(/\\s+/g, ' ').trim());\n\nconst wordsOf = (raw: string): string[] =>\n raw.split(' ').filter((word) => word.length > 0);\n\n/** Keep at most `max` whole words. The unit the budget is actually stated in. */\nconst clampToWords = (raw: string, max: number): string => {\n const words = wordsOf(raw);\n if (words.length <= max) {\n return raw;\n }\n return trimTail(words.slice(0, max).join(' '));\n};\n\n/**\n * Clamp without cutting a word in half. Slicing at a fixed length leaves\n * fragments like \"the kinds of product\" — observed in the first real run — and\n * a dangling partial word is noise in a retrieval phrase, not a shorter\n * version of it. Falls back to a hard slice only when the budget cannot fit\n * even one word.\n */\nconst clampToWord = (raw: string, max: number): string => {\n if (raw.length <= max) {\n return raw;\n }\n // +1 so a boundary landing exactly on the budget still counts as a break.\n const cut = raw.slice(0, max + 1);\n const lastBreak = cut.lastIndexOf(' ');\n const out = lastBreak > 0 ? cut.slice(0, lastBreak) : raw.slice(0, max);\n return trimTail(out);\n};\n\n/** The word budget first, then the character backstop under it. */\nconst budgeted = (raw: string): string =>\n clampToWord(clampToWords(raw, WORD_BUDGET), MAX_LENGTH);\n\n/** The first clause carries the subject; what follows is usually qualification. */\nconst firstClause = (raw: string): string => {\n const m = raw.match(/^[^.!?;]+/);\n return (m ? m[0] : raw).trim();\n};\n\nexport interface ComposeSemanticInput {\n /** The section's headline. */\n title?: string;\n /** Its lead paragraph or body copy. */\n lead?: string;\n /** The descriptor's own `semantic` / mission, when the host supplies one.\n * Outranks the prose: it is what the orchestrator meant the section to be\n * about, which is a better subject than what it happened to say. */\n sectionSemantic?: string;\n}\n\n/**\n * Returns the phrase, or `''` when what survives is too thin to retrieve on —\n * the caller then declares no slot at all rather than sending a bad query.\n */\nexport function composeSemantic({\n title,\n lead,\n sectionSemantic,\n}: ComposeSemanticInput): string {\n // A mission, when there is one, is the whole answer.\n const mission = tidy(sectionSemantic ?? '');\n if (mission.length >= MIN_LENGTH) {\n return budgeted(mission);\n }\n\n const head = tidy(title ?? '');\n const headWords = wordsOf(head).length;\n\n // The title is the stronger signal, so it is spent first and never truncated\n // to make room for the lead. Once it names a subject on its own, it IS the\n // phrase.\n if (headWords >= TITLE_ENOUGH) {\n const out = budgeted(head);\n return out.length < MIN_LENGTH ? '' : out;\n }\n\n // A thin title spends what it did not use on the lead's first clause.\n const body = clampToWords(\n firstClause(tidy(lead ?? '')),\n WORD_BUDGET - headWords,\n );\n\n const joined = [head, body].filter(Boolean).join('. ');\n if (joined.length < MIN_LENGTH) return '';\n\n return clampToWord(joined, MAX_LENGTH);\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
@import '../components/ui/PromptEntryEmptyState.css';
|
|
3
3
|
@import '../components/ui/FeedbackBar.css';
|
|
4
4
|
@import '../components/ui/AiDisclosure.css';
|
|
5
|
-
@import '../components/ui/PoweredByBadge.css';
|
|
6
5
|
@import '../components/ui/UserQuery.css';
|
|
7
6
|
@import '../components/ui/PlacementLoader.css';
|
|
8
7
|
@import '../components/placement/PlacementResponseRenderer.css';
|
|
@@ -20,6 +20,14 @@ export declare const DEFAULT_POWERED_BY_HREF = "https://www.nisoweb.ai";
|
|
|
20
20
|
* Rendering is the caller's decision — this component says nothing about plans.
|
|
21
21
|
* The AI page mounts it only when the site is on the Free tier (see
|
|
22
22
|
* `shouldShowPoweredBy`); any paid package removes it.
|
|
23
|
+
*
|
|
24
|
+
* NOTE: unlike every other component here, its CSS is NOT in
|
|
25
|
+
* `styles/components.css`. That file ships in the client bundle, and this
|
|
26
|
+
* component is mounted from the app bundle — the two are deployed
|
|
27
|
+
* independently, and splitting it across them shipped a naked, unstyled credit
|
|
28
|
+
* to a live storefront. Its stylesheet lives with the shell that mounts it, in
|
|
29
|
+
* `web50-server-ui/src/components/App.css`. A future client that mounts this
|
|
30
|
+
* itself has to bring its own copy, or move the rules somewhere both can see.
|
|
23
31
|
*/
|
|
24
32
|
export declare const PoweredByBadge: FC<PoweredByBadgeProps>;
|
|
25
33
|
//# sourceMappingURL=PoweredByBadge.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PoweredByBadge.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/PoweredByBadge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAGlC,MAAM,WAAW,mBAAmB;IAClC,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACjD,sEAAsE;IACtE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,uBAAuB,2BAA2B,CAAC;AAEhE
|
|
1
|
+
{"version":3,"file":"PoweredByBadge.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/PoweredByBadge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAGlC,MAAM,WAAW,mBAAmB;IAClC,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACjD,sEAAsE;IACtE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,uBAAuB,2BAA2B,CAAC;AAEhE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CA8BlD,CAAC"}
|
|
@@ -18,6 +18,37 @@
|
|
|
18
18
|
* CDN build externalises core, so a core change could not be seen through
|
|
19
19
|
* `?clientBundleUrl=` until it had been published. 0223 always assigned it
|
|
20
20
|
* here, and this is that move.
|
|
21
|
+
*
|
|
22
|
+
* WHAT THIS PHRASE IS FOR, AND WHY IT IS SHORT (ADR 0238)
|
|
23
|
+
* ------------------------------------------------------
|
|
24
|
+
* It buys **recall, and only recall**. In media-manager an editorial slot
|
|
25
|
+
* retrieves `SEMANTIC` over `semantic_text` capped at 30 candidates
|
|
26
|
+
* (`CandidateGather.planFor`), scoped by a filter that is just "this site's
|
|
27
|
+
* images" (`buildFilter`) — so the phrase ranks the corpus rather than gating
|
|
28
|
+
* it. Which picture then WINS is decided by `SlotScorer`, whose every term is
|
|
29
|
+
* geometric (crop fit, background, subject, text safety, resolution). Vespa's
|
|
30
|
+
* relevance score is not one of them, and the pool's order is discarded by the
|
|
31
|
+
* assignment solve. The phrase's entire job is choosing which 30 images reach
|
|
32
|
+
* the scorer.
|
|
33
|
+
*
|
|
34
|
+
* That is why it is budgeted in WORDS. The documents it is matched against are
|
|
35
|
+
* short — `ImageDocumentWriter.semanticText` embeds `alt, caption, label,
|
|
36
|
+
* label…`, usually under ten words — and retrieval is hybrid, BM25 blended with
|
|
37
|
+
* vector closeness. A headline plus a clause of marketing prose is the wrong
|
|
38
|
+
* shape against that on both halves: the vector drifts toward the mean of
|
|
39
|
+
* several topics instead of sitting on the subject, and BM25 spends term weight
|
|
40
|
+
* on filler that pulls in whatever else on the site happens to carry those
|
|
41
|
+
* words. Nothing normalises the query — case folding and dedupe (`LabelMerge`)
|
|
42
|
+
* happen on the document side only.
|
|
43
|
+
*
|
|
44
|
+
* A phrase that is too specific costs a worse 30, never a blank section: the
|
|
45
|
+
* site-wide widening in `CandidateGather.poolFor` is entity-only because an
|
|
46
|
+
* editorial pool empties only when the site has no images at all.
|
|
47
|
+
*
|
|
48
|
+
* CHANGING ANY OF THIS INVALIDATES SLOT IDS. `useImageSlot` derives a text
|
|
49
|
+
* slot's id as `s:${semantic}` (ADR 0223), so the phrase IS the cache key —
|
|
50
|
+
* every text-derived slot in the product gets a new id. Not a place for
|
|
51
|
+
* cosmetic edits.
|
|
21
52
|
*/
|
|
22
53
|
export interface ComposeSemanticInput {
|
|
23
54
|
/** The section's headline. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composeSemantic.d.ts","sourceRoot":"","sources":["../../../src/image/composeSemantic.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"composeSemantic.d.ts","sourceRoot":"","sources":["../../../src/image/composeSemantic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAmFH,MAAM,WAAW,oBAAoB;IACnC,8BAA8B;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;yEAEqE;IACrE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,IAAI,EACJ,eAAe,GAChB,EAAE,oBAAoB,GAAG,MAAM,CA4B/B"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/web5-core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.63.
|
|
4
|
+
"version": "1.63.38",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tsachis",
|
|
7
7
|
"email": "tsachis@wix.com"
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"wallaby": {
|
|
101
101
|
"autoDetect": true
|
|
102
102
|
},
|
|
103
|
-
"falconPackageHash": "
|
|
103
|
+
"falconPackageHash": "527f52ff447d5d61666f8af72e2f79ab513eea188fe4f1a640a38cb2"
|
|
104
104
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
@import '../components/ui/PromptEntryEmptyState.css';
|
|
3
3
|
@import '../components/ui/FeedbackBar.css';
|
|
4
4
|
@import '../components/ui/AiDisclosure.css';
|
|
5
|
-
@import '../components/ui/PoweredByBadge.css';
|
|
6
5
|
@import '../components/ui/UserQuery.css';
|
|
7
6
|
@import '../components/ui/PlacementLoader.css';
|
|
8
7
|
@import '../components/placement/PlacementResponseRenderer.css';
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/* "Powered by Niso" free-tier credit bar (B2B-4574).
|
|
2
|
-
|
|
3
|
-
A full-width strip pinned under the prompt dock at the very bottom of the AI
|
|
4
|
-
page. Colour and type are vars with fallbacks so a client can re-skin the bar
|
|
5
|
-
without forking the component — but see the two values below that are
|
|
6
|
-
deliberately fixed, and `App.tsx` on why the bar itself is not a UI slot. */
|
|
7
|
-
|
|
8
|
-
.web5-powered-by {
|
|
9
|
-
/*
|
|
10
|
-
* The bar's lift, straight from the design (Figma 751-2729): a soft drop
|
|
11
|
-
* plus a wider ambient glow, the latter being what actually reads above a
|
|
12
|
-
* bar pinned to the bottom edge.
|
|
13
|
-
*
|
|
14
|
-
* Its own value rather than one of the `--shadow-*` steps in
|
|
15
|
-
* `base-tokens.css`: that is a general-purpose scale clients legitimately
|
|
16
|
-
* re-map — feature.com flattens `--shadow-sm` to `none` — and the bar needs
|
|
17
|
-
* a lift that survives it. Declared here rather than in `base-tokens.css`
|
|
18
|
-
* because that file ships in the *client* bundle, which is versioned apart
|
|
19
|
-
* from this stylesheet; a token defined there would resolve to nothing on
|
|
20
|
-
* any client built before this change.
|
|
21
|
-
*/
|
|
22
|
-
--web5-credit-bar-shadow:
|
|
23
|
-
0px 6px 6px hsl(230 100% 7% / 0.03), 0px 0px 18px hsl(230 100% 7% / 0.07);
|
|
24
|
-
|
|
25
|
-
display: flex;
|
|
26
|
-
align-items: center;
|
|
27
|
-
justify-content: center;
|
|
28
|
-
width: 100%;
|
|
29
|
-
/*
|
|
30
|
-
* Fixed, not a var. The host docks this bar `position: fixed` and lifts the
|
|
31
|
-
* prompt input by the same 30px (`--web5-credit-bar-height` in `App.css`);
|
|
32
|
-
* a client able to resize it could only ever push the two out of step. The
|
|
33
|
-
* colour and type vars below are safe precisely because they cannot.
|
|
34
|
-
*/
|
|
35
|
-
height: 30px;
|
|
36
|
-
padding-inline: var(--web5-powered-by-padding-inline, 16px);
|
|
37
|
-
background: var(--web5-powered-by-background, hsl(var(--background)));
|
|
38
|
-
box-shadow: var(--web5-powered-by-shadow, var(--web5-credit-bar-shadow));
|
|
39
|
-
font-size: var(--web5-powered-by-font-size, 14px);
|
|
40
|
-
line-height: var(--web5-powered-by-line-height, 1);
|
|
41
|
-
font-weight: 400;
|
|
42
|
-
text-align: center;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.web5-powered-by__link {
|
|
46
|
-
display: inline-flex;
|
|
47
|
-
align-items: center;
|
|
48
|
-
gap: 0.25em;
|
|
49
|
-
/* WCAG 2.1 AA target size — the 30px bar is taller than the text alone. */
|
|
50
|
-
min-height: 24px;
|
|
51
|
-
color: inherit;
|
|
52
|
-
text-decoration: none;
|
|
53
|
-
cursor: pointer;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.web5-powered-by__label {
|
|
57
|
-
color: var(--web5-powered-by-label-color, hsl(var(--muted-foreground)));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.web5-powered-by__brand {
|
|
61
|
-
color: var(--web5-powered-by-brand-color, hsl(var(--foreground)));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.web5-powered-by__link:hover .web5-powered-by__brand,
|
|
65
|
-
.web5-powered-by__link:focus-visible .web5-powered-by__brand {
|
|
66
|
-
text-decoration: underline;
|
|
67
|
-
text-decoration-skip-ink: none;
|
|
68
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/* "Powered by Niso" free-tier credit bar (B2B-4574).
|
|
2
|
-
|
|
3
|
-
A full-width strip pinned under the prompt dock at the very bottom of the AI
|
|
4
|
-
page. Colour and type are vars with fallbacks so a client can re-skin the bar
|
|
5
|
-
without forking the component — but see the two values below that are
|
|
6
|
-
deliberately fixed, and `App.tsx` on why the bar itself is not a UI slot. */
|
|
7
|
-
|
|
8
|
-
.web5-powered-by {
|
|
9
|
-
/*
|
|
10
|
-
* The bar's lift, straight from the design (Figma 751-2729): a soft drop
|
|
11
|
-
* plus a wider ambient glow, the latter being what actually reads above a
|
|
12
|
-
* bar pinned to the bottom edge.
|
|
13
|
-
*
|
|
14
|
-
* Its own value rather than one of the `--shadow-*` steps in
|
|
15
|
-
* `base-tokens.css`: that is a general-purpose scale clients legitimately
|
|
16
|
-
* re-map — feature.com flattens `--shadow-sm` to `none` — and the bar needs
|
|
17
|
-
* a lift that survives it. Declared here rather than in `base-tokens.css`
|
|
18
|
-
* because that file ships in the *client* bundle, which is versioned apart
|
|
19
|
-
* from this stylesheet; a token defined there would resolve to nothing on
|
|
20
|
-
* any client built before this change.
|
|
21
|
-
*/
|
|
22
|
-
--web5-credit-bar-shadow:
|
|
23
|
-
0px 6px 6px hsl(230 100% 7% / 0.03), 0px 0px 18px hsl(230 100% 7% / 0.07);
|
|
24
|
-
|
|
25
|
-
display: flex;
|
|
26
|
-
align-items: center;
|
|
27
|
-
justify-content: center;
|
|
28
|
-
width: 100%;
|
|
29
|
-
/*
|
|
30
|
-
* Fixed, not a var. The host docks this bar `position: fixed` and lifts the
|
|
31
|
-
* prompt input by the same 30px (`--web5-credit-bar-height` in `App.css`);
|
|
32
|
-
* a client able to resize it could only ever push the two out of step. The
|
|
33
|
-
* colour and type vars below are safe precisely because they cannot.
|
|
34
|
-
*/
|
|
35
|
-
height: 30px;
|
|
36
|
-
padding-inline: var(--web5-powered-by-padding-inline, 16px);
|
|
37
|
-
background: var(--web5-powered-by-background, hsl(var(--background)));
|
|
38
|
-
box-shadow: var(--web5-powered-by-shadow, var(--web5-credit-bar-shadow));
|
|
39
|
-
font-size: var(--web5-powered-by-font-size, 14px);
|
|
40
|
-
line-height: var(--web5-powered-by-line-height, 1);
|
|
41
|
-
font-weight: 400;
|
|
42
|
-
text-align: center;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.web5-powered-by__link {
|
|
46
|
-
display: inline-flex;
|
|
47
|
-
align-items: center;
|
|
48
|
-
gap: 0.25em;
|
|
49
|
-
/* WCAG 2.1 AA target size — the 30px bar is taller than the text alone. */
|
|
50
|
-
min-height: 24px;
|
|
51
|
-
color: inherit;
|
|
52
|
-
text-decoration: none;
|
|
53
|
-
cursor: pointer;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.web5-powered-by__label {
|
|
57
|
-
color: var(--web5-powered-by-label-color, hsl(var(--muted-foreground)));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.web5-powered-by__brand {
|
|
61
|
-
color: var(--web5-powered-by-brand-color, hsl(var(--foreground)));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.web5-powered-by__link:hover .web5-powered-by__brand,
|
|
65
|
-
.web5-powered-by__link:focus-visible .web5-powered-by__brand {
|
|
66
|
-
text-decoration: underline;
|
|
67
|
-
text-decoration-skip-ink: none;
|
|
68
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/* "Powered by Niso" free-tier credit bar (B2B-4574).
|
|
2
|
-
|
|
3
|
-
A full-width strip pinned under the prompt dock at the very bottom of the AI
|
|
4
|
-
page. Colour and type are vars with fallbacks so a client can re-skin the bar
|
|
5
|
-
without forking the component — but see the two values below that are
|
|
6
|
-
deliberately fixed, and `App.tsx` on why the bar itself is not a UI slot. */
|
|
7
|
-
|
|
8
|
-
.web5-powered-by {
|
|
9
|
-
/*
|
|
10
|
-
* The bar's lift, straight from the design (Figma 751-2729): a soft drop
|
|
11
|
-
* plus a wider ambient glow, the latter being what actually reads above a
|
|
12
|
-
* bar pinned to the bottom edge.
|
|
13
|
-
*
|
|
14
|
-
* Its own value rather than one of the `--shadow-*` steps in
|
|
15
|
-
* `base-tokens.css`: that is a general-purpose scale clients legitimately
|
|
16
|
-
* re-map — feature.com flattens `--shadow-sm` to `none` — and the bar needs
|
|
17
|
-
* a lift that survives it. Declared here rather than in `base-tokens.css`
|
|
18
|
-
* because that file ships in the *client* bundle, which is versioned apart
|
|
19
|
-
* from this stylesheet; a token defined there would resolve to nothing on
|
|
20
|
-
* any client built before this change.
|
|
21
|
-
*/
|
|
22
|
-
--web5-credit-bar-shadow:
|
|
23
|
-
0px 6px 6px hsl(230 100% 7% / 0.03), 0px 0px 18px hsl(230 100% 7% / 0.07);
|
|
24
|
-
|
|
25
|
-
display: flex;
|
|
26
|
-
align-items: center;
|
|
27
|
-
justify-content: center;
|
|
28
|
-
width: 100%;
|
|
29
|
-
/*
|
|
30
|
-
* Fixed, not a var. The host docks this bar `position: fixed` and lifts the
|
|
31
|
-
* prompt input by the same 30px (`--web5-credit-bar-height` in `App.css`);
|
|
32
|
-
* a client able to resize it could only ever push the two out of step. The
|
|
33
|
-
* colour and type vars below are safe precisely because they cannot.
|
|
34
|
-
*/
|
|
35
|
-
height: 30px;
|
|
36
|
-
padding-inline: var(--web5-powered-by-padding-inline, 16px);
|
|
37
|
-
background: var(--web5-powered-by-background, hsl(var(--background)));
|
|
38
|
-
box-shadow: var(--web5-powered-by-shadow, var(--web5-credit-bar-shadow));
|
|
39
|
-
font-size: var(--web5-powered-by-font-size, 14px);
|
|
40
|
-
line-height: var(--web5-powered-by-line-height, 1);
|
|
41
|
-
font-weight: 400;
|
|
42
|
-
text-align: center;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.web5-powered-by__link {
|
|
46
|
-
display: inline-flex;
|
|
47
|
-
align-items: center;
|
|
48
|
-
gap: 0.25em;
|
|
49
|
-
/* WCAG 2.1 AA target size — the 30px bar is taller than the text alone. */
|
|
50
|
-
min-height: 24px;
|
|
51
|
-
color: inherit;
|
|
52
|
-
text-decoration: none;
|
|
53
|
-
cursor: pointer;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.web5-powered-by__label {
|
|
57
|
-
color: var(--web5-powered-by-label-color, hsl(var(--muted-foreground)));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.web5-powered-by__brand {
|
|
61
|
-
color: var(--web5-powered-by-brand-color, hsl(var(--foreground)));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.web5-powered-by__link:hover .web5-powered-by__brand,
|
|
65
|
-
.web5-powered-by__link:focus-visible .web5-powered-by__brand {
|
|
66
|
-
text-decoration: underline;
|
|
67
|
-
text-decoration-skip-ink: none;
|
|
68
|
-
}
|