@matterfact/embed 0.7.0 → 0.9.0
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/README.md +171 -1
- package/dist/chunk-6EM7T2JV.js +816 -0
- package/dist/chunk-6EM7T2JV.js.map +1 -0
- package/dist/{chunk-4Q2ROXLR.js → chunk-BKKXHSYU.js} +2 -2
- package/dist/chunk-FEXG4LQJ.js +3 -0
- package/dist/chunk-FEXG4LQJ.js.map +7 -0
- package/dist/chunk-NWNMS34P.js +2 -0
- package/dist/chunk-URGQBG4I.js +800 -0
- package/dist/chunk-URGQBG4I.js.map +1 -0
- package/dist/context-ACFBWIFH.js +3 -0
- package/dist/{context-YX2KXFLI.js.map → context-ACFBWIFH.js.map} +1 -1
- package/dist/{context-WU2F5CBC.js → context-U2HJJN2S.js} +4 -2
- package/dist/embed.js +1 -1
- package/dist/embed.js.map +4 -4
- package/dist/index.cjs +1006 -159
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +192 -32
- package/dist/index.d.ts +192 -32
- package/dist/index.js +537 -130
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1042 -165
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +81 -1
- package/dist/react.d.ts +81 -1
- package/dist/react.js +566 -132
- package/dist/react.js.map +1 -1
- package/dist/{snapshot-JOGZWESK.js → snapshot-4GXT6PKZ.js} +2 -2
- package/dist/{snapshot-MUXE7KXX.js → snapshot-Y75SCGCM.js} +3 -3
- package/dist/{snapshot-MUXE7KXX.js.map → snapshot-Y75SCGCM.js.map} +1 -1
- package/examples/embed-demo/README.md +37 -0
- package/examples/embed-demo/src/App.tsx +55 -21
- package/examples/embed-demo/src/HoistDemo.tsx +390 -0
- package/examples/embed-demo/src/main.tsx +7 -0
- package/examples/embed-demo/src/mockXH.ts +492 -0
- package/examples/embed-demo/src/placement.tsx +43 -0
- package/examples/embed-demo/src/styles.css +147 -2
- package/examples/embed-demo/vite.config.ts +2 -2
- package/package.json +1 -1
- package/dist/chunk-7I37ZFAJ.js +0 -2
- package/dist/chunk-AXKXNYRT.js +0 -381
- package/dist/chunk-AXKXNYRT.js.map +0 -1
- package/dist/chunk-D7R6WVHG.js +0 -2
- package/dist/chunk-D7R6WVHG.js.map +0 -7
- package/dist/chunk-RS6RMZ77.js +0 -396
- package/dist/chunk-RS6RMZ77.js.map +0 -1
- package/dist/context-YX2KXFLI.js +0 -3
- /package/dist/{chunk-4Q2ROXLR.js.map → chunk-BKKXHSYU.js.map} +0 -0
- /package/dist/{chunk-7I37ZFAJ.js.map → chunk-NWNMS34P.js.map} +0 -0
- /package/dist/{context-WU2F5CBC.js.map → context-U2HJJN2S.js.map} +0 -0
- /package/dist/{snapshot-JOGZWESK.js.map → snapshot-4GXT6PKZ.js.map} +0 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { MatterfactAgent } from '@matterfact/embed/react';
|
|
4
|
+
import { config } from './config';
|
|
5
|
+
import { PlacementSwitch, usePlacement } from './placement';
|
|
6
|
+
import {
|
|
7
|
+
useMockXH,
|
|
8
|
+
mockNavigate,
|
|
9
|
+
selectHolding,
|
|
10
|
+
getHoldings,
|
|
11
|
+
getOwnershipRows,
|
|
12
|
+
getPriceSeries,
|
|
13
|
+
} from './mockXH';
|
|
14
|
+
|
|
15
|
+
type NavPolicy = 'off' | 'confirm' | 'auto';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The `?hoist=1` demo: the SAME Meridian Capital portfolio App.tsx renders, but
|
|
19
|
+
* with NOTHING declared to the agent by hand — `window.XH` (installed by
|
|
20
|
+
* mockXH.ts, before this ever mounts) is all `<MatterfactAgent>` needs. No
|
|
21
|
+
* `getPageContext` prop here on purpose: leaving it unset is what lets the SDK's
|
|
22
|
+
* automatic Hoist detection run (see @matterfact/embed's context.ts —
|
|
23
|
+
* `resolveDeclaredContext` only reaches the Hoist fallback when no declared
|
|
24
|
+
* context source is present).
|
|
25
|
+
*/
|
|
26
|
+
export function HoistDemo() {
|
|
27
|
+
const { name, params, selection } = useMockXH();
|
|
28
|
+
const [panelOpen, setPanelOpen] = useState(true);
|
|
29
|
+
const [placement, setPlacement] = usePlacement();
|
|
30
|
+
const [navPolicy, setNavPolicy] = useState<NavPolicy>('confirm');
|
|
31
|
+
const ticker = params.ticker;
|
|
32
|
+
const onCompanyPage = name.startsWith('default.company.ticker');
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className="app"
|
|
37
|
+
data-panel={panelOpen ? 'open' : 'closed'}
|
|
38
|
+
data-placement={placement}
|
|
39
|
+
>
|
|
40
|
+
<header>
|
|
41
|
+
<div className="brand">
|
|
42
|
+
<span className="mark">M</span> Meridian Capital{' '}
|
|
43
|
+
<small>· Hoist mock</small>
|
|
44
|
+
</div>
|
|
45
|
+
<nav>
|
|
46
|
+
<NavButton
|
|
47
|
+
active={name === 'default.holdings'}
|
|
48
|
+
onClick={() => mockNavigate('default.holdings')}
|
|
49
|
+
>
|
|
50
|
+
Holdings
|
|
51
|
+
</NavButton>
|
|
52
|
+
<NavButton
|
|
53
|
+
active={name === 'default.research'}
|
|
54
|
+
onClick={() => mockNavigate('default.research')}
|
|
55
|
+
>
|
|
56
|
+
Research
|
|
57
|
+
</NavButton>
|
|
58
|
+
{onCompanyPage && (
|
|
59
|
+
<>
|
|
60
|
+
<span className="nav-sep">/</span>
|
|
61
|
+
<NavButton active onClick={() => {}}>
|
|
62
|
+
{ticker}
|
|
63
|
+
</NavButton>
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
</nav>
|
|
67
|
+
<span className="spacer" />
|
|
68
|
+
<a className="mode-link" href={location.pathname}>
|
|
69
|
+
← Hand-declared mode
|
|
70
|
+
</a>
|
|
71
|
+
<span className="env-chip">
|
|
72
|
+
{config.env} · {config.widgetOrigin.replace(/^https?:\/\//, '')}
|
|
73
|
+
</span>
|
|
74
|
+
<label className="hoist-policy">
|
|
75
|
+
hoist.navigate
|
|
76
|
+
<select
|
|
77
|
+
value={navPolicy}
|
|
78
|
+
onChange={(e) => setNavPolicy(e.target.value as NavPolicy)}
|
|
79
|
+
>
|
|
80
|
+
<option value="off">Off</option>
|
|
81
|
+
<option value="confirm">Confirm</option>
|
|
82
|
+
<option value="auto">Auto</option>
|
|
83
|
+
</select>
|
|
84
|
+
</label>
|
|
85
|
+
<PlacementSwitch placement={placement} onChange={setPlacement} />
|
|
86
|
+
{placement === 'panel' && (
|
|
87
|
+
<button className="toggle" onClick={() => setPanelOpen((v) => !v)}>
|
|
88
|
+
<span className="dot" /> Assistant
|
|
89
|
+
</button>
|
|
90
|
+
)}
|
|
91
|
+
</header>
|
|
92
|
+
|
|
93
|
+
{/* THE CORNER — the widget's own box: it positions and sizes itself, so it
|
|
94
|
+
sits outside the layout with nothing wrapped around it. */}
|
|
95
|
+
{placement === 'corner' && (
|
|
96
|
+
<MatterfactAgent
|
|
97
|
+
publishableKey={config.publishableKey}
|
|
98
|
+
widgetOrigin={config.widgetOrigin}
|
|
99
|
+
surface={config.surface}
|
|
100
|
+
theme={config.theme}
|
|
101
|
+
actions={{ navigate: navPolicy }}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
<div className="layout">
|
|
106
|
+
<main>
|
|
107
|
+
<HoistPage
|
|
108
|
+
routeName={name}
|
|
109
|
+
ticker={ticker}
|
|
110
|
+
selection={selection}
|
|
111
|
+
onCompanyPage={onCompanyPage}
|
|
112
|
+
/>
|
|
113
|
+
</main>
|
|
114
|
+
|
|
115
|
+
{/* THE PANEL — same inline-agent pattern as App.tsx's hand-declared demo. */}
|
|
116
|
+
{placement === 'panel' && (
|
|
117
|
+
<aside>
|
|
118
|
+
<div className="panel-head">
|
|
119
|
+
<span className="mf">✦</span> matterfact assistant
|
|
120
|
+
</div>
|
|
121
|
+
<div className="agent-slot">
|
|
122
|
+
<MatterfactAgent
|
|
123
|
+
inline
|
|
124
|
+
publishableKey={config.publishableKey}
|
|
125
|
+
widgetOrigin={config.widgetOrigin}
|
|
126
|
+
surface={config.surface}
|
|
127
|
+
theme={config.theme}
|
|
128
|
+
actions={{ navigate: navPolicy }}
|
|
129
|
+
style={{ width: '100%', height: '100%' }}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</aside>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function NavButton({
|
|
140
|
+
active,
|
|
141
|
+
onClick,
|
|
142
|
+
children,
|
|
143
|
+
}: {
|
|
144
|
+
active?: boolean;
|
|
145
|
+
onClick: () => void;
|
|
146
|
+
children: ReactNode;
|
|
147
|
+
}) {
|
|
148
|
+
return (
|
|
149
|
+
<button className={active ? 'active' : undefined} onClick={onClick}>
|
|
150
|
+
{children}
|
|
151
|
+
</button>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function HoistPage({
|
|
156
|
+
routeName,
|
|
157
|
+
ticker,
|
|
158
|
+
selection,
|
|
159
|
+
onCompanyPage,
|
|
160
|
+
}: {
|
|
161
|
+
routeName: string;
|
|
162
|
+
ticker: string;
|
|
163
|
+
selection: string | null;
|
|
164
|
+
onCompanyPage: boolean;
|
|
165
|
+
}) {
|
|
166
|
+
if (onCompanyPage) {
|
|
167
|
+
const leaf = routeName.split('.').pop() ?? 'thesis';
|
|
168
|
+
return <CompanyPage ticker={ticker} leaf={leaf} />;
|
|
169
|
+
}
|
|
170
|
+
if (routeName === 'default.research') return <ResearchPage />;
|
|
171
|
+
return <HoldingsPage selection={selection} />;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function HoldingsPage({ selection }: { selection: string | null }) {
|
|
175
|
+
const holdings = getHoldings();
|
|
176
|
+
const selected = holdings.find((h) => h.ticker === selection);
|
|
177
|
+
return (
|
|
178
|
+
<>
|
|
179
|
+
<div className="eyebrow">Portfolio</div>
|
|
180
|
+
<h1>Holdings</h1>
|
|
181
|
+
<div className="sub">
|
|
182
|
+
{holdings.length} holdings · read live from a mock{' '}
|
|
183
|
+
<code>window.XH</code> GridModel
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<div className="section-title">
|
|
187
|
+
Holdings{' '}
|
|
188
|
+
<span className="hint">
|
|
189
|
+
(click a row to select it · click a ticker to drill in)
|
|
190
|
+
</span>
|
|
191
|
+
</div>
|
|
192
|
+
<table>
|
|
193
|
+
<thead>
|
|
194
|
+
<tr>
|
|
195
|
+
<th>Ticker</th>
|
|
196
|
+
<th>Weight</th>
|
|
197
|
+
<th>Day</th>
|
|
198
|
+
<th>YTD</th>
|
|
199
|
+
</tr>
|
|
200
|
+
</thead>
|
|
201
|
+
<tbody>
|
|
202
|
+
{holdings.map((h) => (
|
|
203
|
+
<tr
|
|
204
|
+
key={h.ticker}
|
|
205
|
+
aria-pressed={h.ticker === selection}
|
|
206
|
+
onClick={() => selectHolding(h.ticker)}
|
|
207
|
+
style={{
|
|
208
|
+
cursor: 'pointer',
|
|
209
|
+
background:
|
|
210
|
+
h.ticker === selection ? 'var(--surface-2)' : undefined,
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
<td className="tick">
|
|
214
|
+
{h.ticker === selection ? '▸ ' : ''}
|
|
215
|
+
<button
|
|
216
|
+
className="ticker-link"
|
|
217
|
+
onClick={(e) => {
|
|
218
|
+
e.stopPropagation();
|
|
219
|
+
mockNavigate('default.company.ticker.thesis', {
|
|
220
|
+
ticker: h.ticker,
|
|
221
|
+
});
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
{h.ticker}
|
|
225
|
+
</button>
|
|
226
|
+
</td>
|
|
227
|
+
<td>{h.weight}</td>
|
|
228
|
+
<td className={h.dayNeg ? 'neg' : 'pos'}>{h.day}</td>
|
|
229
|
+
<td className={h.ytdNeg ? 'neg' : 'pos'}>{h.ytd}</td>
|
|
230
|
+
</tr>
|
|
231
|
+
))}
|
|
232
|
+
</tbody>
|
|
233
|
+
</table>
|
|
234
|
+
<p className="artifact-caption">
|
|
235
|
+
<strong>Automatic context:</strong> this table, the selection, and the
|
|
236
|
+
site map in the header are all read live out of <code>window.XH</code> —
|
|
237
|
+
nothing on this page is declared to the agent by hand. Select a row,
|
|
238
|
+
then ask <em>"what's selected?"</em>; ask the assistant to navigate to
|
|
239
|
+
Research or to a holding's Ownership tab and watch the approval card.
|
|
240
|
+
</p>
|
|
241
|
+
{selected && (
|
|
242
|
+
<p className="artifact-caption">
|
|
243
|
+
<strong>Selected:</strong> {selected.ticker} — {selected.thesis}
|
|
244
|
+
</p>
|
|
245
|
+
)}
|
|
246
|
+
</>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function ResearchPage() {
|
|
251
|
+
return (
|
|
252
|
+
<>
|
|
253
|
+
<div className="eyebrow">Research</div>
|
|
254
|
+
<h1>Research</h1>
|
|
255
|
+
<div className="sub">
|
|
256
|
+
No grid or chart is mounted on this page — just a route and a site-map.
|
|
257
|
+
</div>
|
|
258
|
+
<p className="artifact-caption">
|
|
259
|
+
With no GridModel or ChartModel present, the agent's page context comes
|
|
260
|
+
from the route and nav alone (<code>buildHoistContext</code> in the
|
|
261
|
+
adapter) — a good page to ask <em>"what page am I on?"</em> against.
|
|
262
|
+
</p>
|
|
263
|
+
</>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function CompanyPage({ ticker, leaf }: { ticker: string; leaf: string }) {
|
|
268
|
+
const holding = getHoldings().find((h) => h.ticker === ticker);
|
|
269
|
+
return (
|
|
270
|
+
<>
|
|
271
|
+
<div className="eyebrow">Company · {ticker}</div>
|
|
272
|
+
<h1>{ticker}</h1>
|
|
273
|
+
<div className="company-subnav">
|
|
274
|
+
<SubTab
|
|
275
|
+
active={leaf === 'thesis'}
|
|
276
|
+
onClick={() =>
|
|
277
|
+
mockNavigate('default.company.ticker.thesis', { ticker })
|
|
278
|
+
}
|
|
279
|
+
>
|
|
280
|
+
Thesis
|
|
281
|
+
</SubTab>
|
|
282
|
+
<SubTab
|
|
283
|
+
active={leaf === 'financials'}
|
|
284
|
+
onClick={() =>
|
|
285
|
+
mockNavigate('default.company.ticker.financials', { ticker })
|
|
286
|
+
}
|
|
287
|
+
>
|
|
288
|
+
Financials
|
|
289
|
+
</SubTab>
|
|
290
|
+
<SubTab
|
|
291
|
+
active={leaf === 'ownership'}
|
|
292
|
+
onClick={() =>
|
|
293
|
+
mockNavigate('default.company.ticker.ownership', { ticker })
|
|
294
|
+
}
|
|
295
|
+
>
|
|
296
|
+
Ownership
|
|
297
|
+
</SubTab>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
{leaf === 'thesis' && (
|
|
301
|
+
<p className="artifact-caption" style={{ fontSize: 14 }}>
|
|
302
|
+
{holding?.thesis ?? 'No thesis on file for this ticker.'}
|
|
303
|
+
</p>
|
|
304
|
+
)}
|
|
305
|
+
{leaf === 'financials' && (
|
|
306
|
+
<>
|
|
307
|
+
<div className="section-title">Indexed price</div>
|
|
308
|
+
<Sparkline points={getPriceSeries(ticker)} />
|
|
309
|
+
<p className="artifact-caption">
|
|
310
|
+
Backed by a mock <code>ChartModel</code> with{' '}
|
|
311
|
+
<code>[timestamp, value]</code> series points — the agent sees a
|
|
312
|
+
points/min/max/last summary, never the raw series.
|
|
313
|
+
</p>
|
|
314
|
+
</>
|
|
315
|
+
)}
|
|
316
|
+
{leaf === 'ownership' && (
|
|
317
|
+
<>
|
|
318
|
+
<div className="section-title">Top holders</div>
|
|
319
|
+
<table>
|
|
320
|
+
<thead>
|
|
321
|
+
<tr>
|
|
322
|
+
<th>Holder</th>
|
|
323
|
+
<th>Shares</th>
|
|
324
|
+
<th>% Owned</th>
|
|
325
|
+
</tr>
|
|
326
|
+
</thead>
|
|
327
|
+
<tbody>
|
|
328
|
+
{getOwnershipRows(ticker).map((r) => (
|
|
329
|
+
<tr key={r.holder}>
|
|
330
|
+
<td>{r.holder}</td>
|
|
331
|
+
<td>{r.shares}</td>
|
|
332
|
+
<td>{r.pct}</td>
|
|
333
|
+
</tr>
|
|
334
|
+
))}
|
|
335
|
+
</tbody>
|
|
336
|
+
</table>
|
|
337
|
+
</>
|
|
338
|
+
)}
|
|
339
|
+
</>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function SubTab({
|
|
344
|
+
active,
|
|
345
|
+
onClick,
|
|
346
|
+
children,
|
|
347
|
+
}: {
|
|
348
|
+
active: boolean;
|
|
349
|
+
onClick: () => void;
|
|
350
|
+
children: ReactNode;
|
|
351
|
+
}) {
|
|
352
|
+
return (
|
|
353
|
+
<button className={`subtab${active ? ' active' : ''}`} onClick={onClick}>
|
|
354
|
+
{children}
|
|
355
|
+
</button>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** A dependency-free inline line chart — visual proof that the page the agent
|
|
360
|
+
* navigated to is a real, different view, not just a route-name change. */
|
|
361
|
+
function Sparkline({ points }: { points: number[] }) {
|
|
362
|
+
if (!points.length) return null;
|
|
363
|
+
const width = 480;
|
|
364
|
+
const height = 120;
|
|
365
|
+
const min = Math.min(...points);
|
|
366
|
+
const max = Math.max(...points);
|
|
367
|
+
const span = max - min || 1;
|
|
368
|
+
const stepX = points.length > 1 ? width / (points.length - 1) : 0;
|
|
369
|
+
const d = points
|
|
370
|
+
.map(
|
|
371
|
+
(v, i) =>
|
|
372
|
+
`${i === 0 ? 'M' : 'L'} ${(i * stepX).toFixed(1)} ${(
|
|
373
|
+
height -
|
|
374
|
+
((v - min) / span) * height
|
|
375
|
+
).toFixed(1)}`,
|
|
376
|
+
)
|
|
377
|
+
.join(' ');
|
|
378
|
+
return (
|
|
379
|
+
<svg
|
|
380
|
+
width={width}
|
|
381
|
+
height={height}
|
|
382
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
383
|
+
className="hoist-chart"
|
|
384
|
+
role="img"
|
|
385
|
+
aria-label="Indexed price chart"
|
|
386
|
+
>
|
|
387
|
+
<path d={d} fill="none" stroke="var(--accent)" strokeWidth={2} />
|
|
388
|
+
</svg>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
3
|
import { App } from './App';
|
|
4
|
+
import { installMockXH, isHoistModeEnabled } from './mockXH';
|
|
4
5
|
import './styles.css';
|
|
5
6
|
|
|
7
|
+
// Installed BEFORE React renders anything, not from an effect: the widget's mount
|
|
8
|
+
// effect reads window.XH synchronously (see @matterfact/embed's context.ts —
|
|
9
|
+
// start() calls advertiseTools() at the end of mount), so window.XH has to exist
|
|
10
|
+
// before <MatterfactAgent> ever mounts. See mockXH.ts's doc comment.
|
|
11
|
+
if (isHoistModeEnabled()) installMockXH();
|
|
12
|
+
|
|
6
13
|
createRoot(document.getElementById('root')!).render(
|
|
7
14
|
<StrictMode>
|
|
8
15
|
<App />
|