@flamingo-stack/openframe-frontend-core 0.0.510 → 0.0.511
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/{chunk-GE3JUTW2.js → chunk-27OKD7ZV.js} +44 -14
- package/dist/chunk-27OKD7ZV.js.map +1 -0
- package/dist/{chunk-2RLI5XVI.cjs → chunk-2CE725NN.cjs} +16 -16
- package/dist/{chunk-2RLI5XVI.cjs.map → chunk-2CE725NN.cjs.map} +1 -1
- package/dist/{chunk-Q4K3TGNP.cjs → chunk-3NECEXDF.cjs} +74 -44
- package/dist/chunk-3NECEXDF.cjs.map +1 -0
- package/dist/{chunk-RJ3D3PE2.cjs → chunk-4SG55XVA.cjs} +9 -9
- package/dist/{chunk-RJ3D3PE2.cjs.map → chunk-4SG55XVA.cjs.map} +1 -1
- package/dist/{chunk-SYZFFRVD.cjs → chunk-5BAGE7AI.cjs} +13 -13
- package/dist/{chunk-SYZFFRVD.cjs.map → chunk-5BAGE7AI.cjs.map} +1 -1
- package/dist/{chunk-6FIDTXYC.js → chunk-7AQSXQEL.js} +3 -3
- package/dist/{chunk-6TL5XIAW.cjs → chunk-7YV67K3I.cjs} +119 -119
- package/dist/{chunk-6TL5XIAW.cjs.map → chunk-7YV67K3I.cjs.map} +1 -1
- package/dist/{chunk-5CDPRCPS.cjs → chunk-A5YZUV6B.cjs} +23 -23
- package/dist/{chunk-5CDPRCPS.cjs.map → chunk-A5YZUV6B.cjs.map} +1 -1
- package/dist/{chunk-WSAPKKDU.cjs → chunk-C4EUXZSA.cjs} +9 -9
- package/dist/{chunk-WSAPKKDU.cjs.map → chunk-C4EUXZSA.cjs.map} +1 -1
- package/dist/{chunk-5AZZQQYS.cjs → chunk-DPLXHG7H.cjs} +40 -40
- package/dist/{chunk-5AZZQQYS.cjs.map → chunk-DPLXHG7H.cjs.map} +1 -1
- package/dist/{chunk-BTUHMCYW.js → chunk-FS3YSNSU.js} +193 -6
- package/dist/chunk-FS3YSNSU.js.map +1 -0
- package/dist/{chunk-RB2EW6IZ.cjs → chunk-GHXTF35X.cjs} +3231 -3165
- package/dist/chunk-GHXTF35X.cjs.map +1 -0
- package/dist/{chunk-7VUKIWF6.cjs → chunk-HIV2GIFP.cjs} +3 -3
- package/dist/{chunk-7VUKIWF6.cjs.map → chunk-HIV2GIFP.cjs.map} +1 -1
- package/dist/{chunk-R4HKB3YD.cjs → chunk-IIPBTWQR.cjs} +22 -22
- package/dist/{chunk-R4HKB3YD.cjs.map → chunk-IIPBTWQR.cjs.map} +1 -1
- package/dist/{chunk-JMBIGH4H.cjs → chunk-IUZMWMVX.cjs} +83 -83
- package/dist/{chunk-JMBIGH4H.cjs.map → chunk-IUZMWMVX.cjs.map} +1 -1
- package/dist/{chunk-XJVRAKEX.cjs → chunk-IXCBVJRU.cjs} +56 -33
- package/dist/chunk-IXCBVJRU.cjs.map +1 -0
- package/dist/{chunk-TBGSQTJK.js → chunk-J4VB4EHW.js} +6 -6
- package/dist/{chunk-UWPT23SA.js → chunk-JXMNLH7F.js} +3 -3
- package/dist/{chunk-A62C4C4V.cjs → chunk-KI4EGKNV.cjs} +3 -2
- package/dist/chunk-KI4EGKNV.cjs.map +1 -0
- package/dist/{chunk-RHQ7BJKK.cjs → chunk-LEWQRAZ4.cjs} +73 -73
- package/dist/{chunk-RHQ7BJKK.cjs.map → chunk-LEWQRAZ4.cjs.map} +1 -1
- package/dist/{chunk-DBR5JLX3.js → chunk-LVAJK2U2.js} +46 -23
- package/dist/chunk-LVAJK2U2.js.map +1 -0
- package/dist/{chunk-OB7YDOTK.cjs → chunk-NJADHL2G.cjs} +8 -8
- package/dist/{chunk-OB7YDOTK.cjs.map → chunk-NJADHL2G.cjs.map} +1 -1
- package/dist/{chunk-HCZJZMU7.js → chunk-NKA3O4L6.js} +3 -3
- package/dist/{chunk-SRYV4H3M.js → chunk-NNWSGOUP.js} +3 -3
- package/dist/{chunk-EYTXD62B.cjs → chunk-OGVIGAMZ.cjs} +232 -45
- package/dist/chunk-OGVIGAMZ.cjs.map +1 -0
- package/dist/{chunk-EJ3EBYNV.cjs → chunk-OMN7A6RP.cjs} +32 -32
- package/dist/{chunk-EJ3EBYNV.cjs.map → chunk-OMN7A6RP.cjs.map} +1 -1
- package/dist/{chunk-WNOGR5AA.js → chunk-PRMR6WKC.js} +3 -3
- package/dist/{chunk-J2JUAS4C.cjs → chunk-PXQQFRUD.cjs} +11 -11
- package/dist/{chunk-J2JUAS4C.cjs.map → chunk-PXQQFRUD.cjs.map} +1 -1
- package/dist/{chunk-VDJF4ZAU.js → chunk-RU3SCECX.js} +3 -3
- package/dist/{chunk-53JKCFFE.cjs → chunk-RXFBW2EJ.cjs} +12 -12
- package/dist/{chunk-53JKCFFE.cjs.map → chunk-RXFBW2EJ.cjs.map} +1 -1
- package/dist/{chunk-JOCFPAS6.js → chunk-TNZOUHHN.js} +6 -6
- package/dist/{chunk-HX4CJYXL.js → chunk-TR7AMHGB.js} +10 -10
- package/dist/{chunk-MLGGF24T.js → chunk-TYVHM67X.js} +8 -8
- package/dist/{chunk-Z35QWWJH.cjs → chunk-U4WPFX5K.cjs} +34 -34
- package/dist/{chunk-Z35QWWJH.cjs.map → chunk-U4WPFX5K.cjs.map} +1 -1
- package/dist/{chunk-M4MKQKFF.js → chunk-UBTOOE55.js} +2 -2
- package/dist/{chunk-FEGEC6FY.js → chunk-X7AJ3J42.js} +5170 -5104
- package/dist/chunk-X7AJ3J42.js.map +1 -0
- package/dist/{chunk-SZMA65OW.js → chunk-XURRAFIB.js} +2 -2
- package/dist/{chunk-CTMLY2GN.js → chunk-YGS7BLSN.js} +5 -5
- package/dist/{chunk-RG2WRBPT.js → chunk-Z3ADCYC5.js} +2 -2
- package/dist/{chunk-2AASLY6D.js → chunk-ZBQV7WH7.js} +3 -3
- package/dist/{chunk-4GPWLGKT.js → chunk-ZRILYK5U.js} +2 -1
- package/dist/{chunk-LBOWSKGM.js → chunk-ZSIEM7CL.js} +5 -5
- package/dist/components/calendar.cjs +4 -4
- package/dist/components/calendar.js +3 -3
- package/dist/components/case-studies/index.cjs +17 -17
- package/dist/components/case-studies/index.js +7 -7
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +13 -7
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +12 -6
- package/dist/components/chat/remark-mention-chips.d.ts +5 -2
- package/dist/components/chat/remark-mention-chips.d.ts.map +1 -1
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts +48 -0
- package/dist/components/chat/utils/discuss-ref-prompt.d.ts.map +1 -0
- package/dist/components/chat/utils/index.d.ts +2 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +63 -0
- package/dist/components/chat/utils/resolve-fetched-card-href.d.ts.map +1 -0
- package/dist/components/chat/utils/source-row-cta.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/docs/index.cjs +10 -10
- package/dist/components/docs/index.js +9 -9
- package/dist/components/embeds/index.cjs +8 -8
- package/dist/components/embeds/index.js +7 -7
- package/dist/components/faq/index.cjs +9 -9
- package/dist/components/faq/index.js +8 -8
- package/dist/components/features/index.cjs +7 -7
- package/dist/components/features/index.js +6 -6
- package/dist/components/help-center-pages/index.cjs +28 -28
- package/dist/components/help-center-pages/index.js +19 -19
- package/dist/components/icons/index.cjs +3 -3
- package/dist/components/icons/index.js +2 -2
- package/dist/components/index.cjs +253 -247
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -17
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +92 -92
- package/dist/components/meeting-scheduler/index.js +9 -9
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +11 -11
- package/dist/components/onboarding-guides/index.js +10 -10
- package/dist/components/related-content/index.cjs +9 -9
- package/dist/components/related-content/index.js +8 -8
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts +11 -3
- package/dist/components/shared/doc-search/resolve-search-result-action.d.ts.map +1 -1
- package/dist/components/shared/doc-search/use-doc-search.d.ts +7 -0
- package/dist/components/shared/doc-search/use-doc-search.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +11 -11
- package/dist/components/tickets/index.js +10 -10
- package/dist/components/ui/actions-menu.d.ts.map +1 -1
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +70 -70
- package/dist/components/ui/file-manager/index.js +3 -3
- package/dist/components/ui/index.cjs +13 -7
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +12 -6
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -1
- package/dist/components/ui/overlay-open-registry.d.ts +50 -0
- package/dist/components/ui/overlay-open-registry.d.ts.map +1 -0
- package/dist/components/ui/portal-container.d.ts +24 -0
- package/dist/components/ui/portal-container.d.ts.map +1 -1
- package/dist/hooks/index.cjs +4 -4
- package/dist/hooks/index.js +3 -3
- package/dist/index.cjs +20 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -14
- package/dist/utils/anchor-id.d.ts +32 -0
- package/dist/utils/anchor-id.d.ts.map +1 -0
- package/dist/utils/content-href.d.ts +27 -6
- package/dist/utils/content-href.d.ts.map +1 -1
- package/dist/utils/index.cjs +213 -152
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +212 -153
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/list-url.d.ts.map +1 -1
- package/dist/utils/same-page-hash-nav.d.ts +17 -6
- package/dist/utils/same-page-hash-nav.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/__tests__/remark-mention-chips.test.ts +67 -0
- package/src/components/chat/chat-message-enhanced.tsx +3 -2
- package/src/components/chat/chat-message-list.tsx +49 -5
- package/src/components/chat/embeddable-chat.tsx +19 -2
- package/src/components/chat/entity-cards/dispatch.tsx +65 -4
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +28 -4
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +5 -3
- package/src/components/chat/remark-mention-chips.ts +6 -3
- package/src/components/chat/utils/__tests__/discuss-ref-prompt.test.ts +67 -0
- package/src/components/chat/utils/__tests__/resolve-fetched-card-href.test.ts +156 -0
- package/src/components/chat/utils/discuss-ref-prompt.ts +63 -0
- package/src/components/chat/utils/index.ts +16 -0
- package/src/components/chat/utils/resolve-fetched-card-href.ts +104 -0
- package/src/components/chat/utils/source-row-cta.ts +5 -1
- package/src/components/shared/doc-search/__tests__/resolve-search-result-action.test.ts +145 -0
- package/src/components/shared/doc-search/resolve-search-result-action.ts +46 -10
- package/src/components/shared/doc-search/use-doc-search.ts +50 -14
- package/src/components/ui/__tests__/overlay-open-registry.test.tsx +87 -0
- package/src/components/ui/actions-menu.tsx +65 -2
- package/src/components/ui/dropdown-menu.tsx +62 -12
- package/src/components/ui/markdown/base-components.tsx +26 -0
- package/src/components/ui/overlay-open-registry.tsx +99 -0
- package/src/components/ui/portal-container.tsx +31 -0
- package/src/utils/__tests__/anchor-id.test.ts +139 -0
- package/src/utils/__tests__/content-href.test.ts +74 -0
- package/src/utils/__tests__/list-url.test.ts +11 -1
- package/src/utils/anchor-id.ts +106 -0
- package/src/utils/content-href.ts +113 -13
- package/src/utils/index.ts +8 -0
- package/src/utils/list-url.ts +6 -1
- package/src/utils/same-page-hash-nav.ts +28 -9
- package/dist/chunk-A62C4C4V.cjs.map +0 -1
- package/dist/chunk-BTUHMCYW.js.map +0 -1
- package/dist/chunk-DBR5JLX3.js.map +0 -1
- package/dist/chunk-EYTXD62B.cjs.map +0 -1
- package/dist/chunk-FEGEC6FY.js.map +0 -1
- package/dist/chunk-GE3JUTW2.js.map +0 -1
- package/dist/chunk-Q4K3TGNP.cjs.map +0 -1
- package/dist/chunk-RB2EW6IZ.cjs.map +0 -1
- package/dist/chunk-XJVRAKEX.cjs.map +0 -1
- /package/dist/{chunk-6FIDTXYC.js.map → chunk-7AQSXQEL.js.map} +0 -0
- /package/dist/{chunk-TBGSQTJK.js.map → chunk-J4VB4EHW.js.map} +0 -0
- /package/dist/{chunk-UWPT23SA.js.map → chunk-JXMNLH7F.js.map} +0 -0
- /package/dist/{chunk-HCZJZMU7.js.map → chunk-NKA3O4L6.js.map} +0 -0
- /package/dist/{chunk-SRYV4H3M.js.map → chunk-NNWSGOUP.js.map} +0 -0
- /package/dist/{chunk-WNOGR5AA.js.map → chunk-PRMR6WKC.js.map} +0 -0
- /package/dist/{chunk-VDJF4ZAU.js.map → chunk-RU3SCECX.js.map} +0 -0
- /package/dist/{chunk-JOCFPAS6.js.map → chunk-TNZOUHHN.js.map} +0 -0
- /package/dist/{chunk-HX4CJYXL.js.map → chunk-TR7AMHGB.js.map} +0 -0
- /package/dist/{chunk-MLGGF24T.js.map → chunk-TYVHM67X.js.map} +0 -0
- /package/dist/{chunk-M4MKQKFF.js.map → chunk-UBTOOE55.js.map} +0 -0
- /package/dist/{chunk-SZMA65OW.js.map → chunk-XURRAFIB.js.map} +0 -0
- /package/dist/{chunk-CTMLY2GN.js.map → chunk-YGS7BLSN.js.map} +0 -0
- /package/dist/{chunk-RG2WRBPT.js.map → chunk-Z3ADCYC5.js.map} +0 -0
- /package/dist/{chunk-2AASLY6D.js.map → chunk-ZBQV7WH7.js.map} +0 -0
- /package/dist/{chunk-4GPWLGKT.js.map → chunk-ZRILYK5U.js.map} +0 -0
- /package/dist/{chunk-LBOWSKGM.js.map → chunk-ZSIEM7CL.js.map} +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { makeComposeContentUrl } from '../../../../utils/content-href'
|
|
3
|
+
import { resolveFetchedCardHref } from '../resolve-fetched-card-href'
|
|
4
|
+
|
|
5
|
+
const HUB = 'https://www.flamingo.run'
|
|
6
|
+
|
|
7
|
+
/** The OpenFrame Help Center seam, verbatim in shape from
|
|
8
|
+
* `openframe-oss-frontend/src/app/(app)/help-center/help-center-content-href.ts`. */
|
|
9
|
+
const compose = makeComposeContentUrl({
|
|
10
|
+
hostedTypes: new Set(['onboarding_guide', 'product_release']),
|
|
11
|
+
contentOrigin: HUB,
|
|
12
|
+
suffixes: {
|
|
13
|
+
onboarding_guide: 'help-center/onboarding-guides',
|
|
14
|
+
product_release: 'help-center/releases',
|
|
15
|
+
blog_post: 'blog',
|
|
16
|
+
case_study: 'case-studies',
|
|
17
|
+
},
|
|
18
|
+
overrides: {
|
|
19
|
+
roadmap_item: (id) => ({
|
|
20
|
+
href: `/help-center/roadmap?search=${encodeURIComponent(id)}`,
|
|
21
|
+
targetPlatform: null,
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('resolveFetchedCardHref — Mingo cards with no ref metadata', () => {
|
|
27
|
+
it('override type deep-links on the MARKER id, not the fetched row id', () => {
|
|
28
|
+
// The `[card://roadmap_item:86ad3qvv5]` id is the ClickUp task id the
|
|
29
|
+
// roadmap list filters by; the fetched row carries its own db id.
|
|
30
|
+
expect(
|
|
31
|
+
resolveFetchedCardHref({
|
|
32
|
+
contentRefType: 'roadmap_item',
|
|
33
|
+
id: '86ad3qvv5',
|
|
34
|
+
item: { id: 'db-uuid-9', title: 'Deep Google Workspace tenant management' },
|
|
35
|
+
composeContentUrl: compose,
|
|
36
|
+
}),
|
|
37
|
+
).toEqual({ href: '/help-center/roadmap?search=86ad3qvv5', targetPlatform: null })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('hosted type uses the fetched row slug for its in-app detail route', () => {
|
|
41
|
+
expect(
|
|
42
|
+
resolveFetchedCardHref({
|
|
43
|
+
contentRefType: 'product_release',
|
|
44
|
+
id: 'row-uuid-1',
|
|
45
|
+
item: { id: 'row-uuid-1', slug: 'v1-2-0' },
|
|
46
|
+
composeContentUrl: compose,
|
|
47
|
+
}),
|
|
48
|
+
).toEqual({ href: '/help-center/releases/v1-2-0', targetPlatform: null })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('hosted type with no slug falls back to the marker id', () => {
|
|
52
|
+
expect(
|
|
53
|
+
resolveFetchedCardHref({
|
|
54
|
+
contentRefType: 'onboarding_guide',
|
|
55
|
+
id: 'guide-7',
|
|
56
|
+
item: { id: 'guide-7' },
|
|
57
|
+
composeContentUrl: compose,
|
|
58
|
+
}),
|
|
59
|
+
).toEqual({ href: '/help-center/onboarding-guides/guide-7', targetPlatform: null })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('non-hosted type resolves to the content hub', () => {
|
|
63
|
+
expect(
|
|
64
|
+
resolveFetchedCardHref({
|
|
65
|
+
contentRefType: 'case_study',
|
|
66
|
+
id: 'cs-1',
|
|
67
|
+
item: { id: 'cs-1', slug: 'acme-migration' },
|
|
68
|
+
composeContentUrl: compose,
|
|
69
|
+
}),
|
|
70
|
+
).toEqual({ href: `${HUB}/case-studies/acme-migration`, targetPlatform: null })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('legacy rail-vocab alias resolves to the SAME url as the canonical type', () => {
|
|
74
|
+
// The card registry keys blog entries by `blog_post_existing` (what
|
|
75
|
+
// `buildListUrl` wants). Both spellings must produce one url — the reported
|
|
76
|
+
// split was `/blog/what-is-mdr` vs `/blog_post_existing/bitwarden-…`.
|
|
77
|
+
const canonical = resolveFetchedCardHref({
|
|
78
|
+
contentRefType: 'blog_post',
|
|
79
|
+
id: 'p1',
|
|
80
|
+
item: { id: 'p1', slug: 'bitwarden-review-for-msps' },
|
|
81
|
+
composeContentUrl: compose,
|
|
82
|
+
})
|
|
83
|
+
const alias = resolveFetchedCardHref({
|
|
84
|
+
contentRefType: 'blog_post_existing',
|
|
85
|
+
id: 'p1',
|
|
86
|
+
item: { id: 'p1', slug: 'bitwarden-review-for-msps' },
|
|
87
|
+
composeContentUrl: compose,
|
|
88
|
+
})
|
|
89
|
+
expect(alias).toEqual(canonical)
|
|
90
|
+
expect(alias).toEqual({
|
|
91
|
+
href: `${HUB}/blog/bitwarden-review-for-msps`,
|
|
92
|
+
targetPlatform: null,
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('no seam wired → null (card stays unlinked, pre-existing behavior)', () => {
|
|
97
|
+
expect(
|
|
98
|
+
resolveFetchedCardHref({
|
|
99
|
+
contentRefType: 'roadmap_item',
|
|
100
|
+
id: '86ad3qvv5',
|
|
101
|
+
item: { id: '86ad3qvv5' },
|
|
102
|
+
}),
|
|
103
|
+
).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('guards a missing type / id / malformed item', () => {
|
|
107
|
+
expect(
|
|
108
|
+
resolveFetchedCardHref({ contentRefType: '', id: 'x', item: {}, composeContentUrl: compose }),
|
|
109
|
+
).toBeNull()
|
|
110
|
+
expect(
|
|
111
|
+
resolveFetchedCardHref({
|
|
112
|
+
contentRefType: 'case_study',
|
|
113
|
+
id: '',
|
|
114
|
+
item: {},
|
|
115
|
+
composeContentUrl: compose,
|
|
116
|
+
}),
|
|
117
|
+
).toBeNull()
|
|
118
|
+
// A non-object item (or a non-string slug) must not throw — it just loses
|
|
119
|
+
// the slug hint and falls back to the marker id.
|
|
120
|
+
expect(
|
|
121
|
+
resolveFetchedCardHref({
|
|
122
|
+
contentRefType: 'case_study',
|
|
123
|
+
id: 'cs-2',
|
|
124
|
+
item: { slug: 42 },
|
|
125
|
+
composeContentUrl: compose,
|
|
126
|
+
}),
|
|
127
|
+
).toEqual({ href: `${HUB}/case-studies/cs-2`, targetPlatform: null })
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('makeComposeContentUrl — the new `slug` hint', () => {
|
|
132
|
+
it('externalUrl still wins over the hint (SSE path unchanged)', () => {
|
|
133
|
+
expect(
|
|
134
|
+
compose({
|
|
135
|
+
type: 'product_release',
|
|
136
|
+
identifier: 'row-uuid-1',
|
|
137
|
+
slug: 'from-the-row',
|
|
138
|
+
externalUrl: `${HUB}/releases/from-the-url`,
|
|
139
|
+
}),
|
|
140
|
+
).toEqual({ href: '/help-center/releases/from-the-url', targetPlatform: null })
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('applies to non-hosted types too — hub detail routes are slug-based', () => {
|
|
144
|
+
expect(compose({ type: 'blog_post', identifier: 'id-1', slug: 'hello-world' })).toEqual({
|
|
145
|
+
href: `${HUB}/blog/hello-world`,
|
|
146
|
+
targetPlatform: null,
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('no hint → identifier, unchanged for page-view and SSE callers', () => {
|
|
151
|
+
expect(compose({ type: 'blog_post', identifier: 'hello-world' })).toEqual({
|
|
152
|
+
href: `${HUB}/blog/hello-world`,
|
|
153
|
+
targetPlatform: null,
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE place the "Ask Mingo" (discuss) prompt for an inline entity card is
|
|
3
|
+
* composed — shared by every transport so the affordance reads identically
|
|
4
|
+
* wherever it is clicked.
|
|
5
|
+
*
|
|
6
|
+
* ## Why the two transports still differ underneath
|
|
7
|
+
*
|
|
8
|
+
* The prose is UX only. What narrows retrieval differs:
|
|
9
|
+
*
|
|
10
|
+
* - **SSE / guide** — `sendMessage(prompt, { commandOverride: { entityIdFilter:
|
|
11
|
+
* { tableId, id } } })`. Retrieval is strictly primary-key-driven; the
|
|
12
|
+
* sentence is decoration.
|
|
13
|
+
* - **Mingo / NATS** — the agent backend has no entity-id-filtered retrieval
|
|
14
|
+
* (`ContextItemType` covers DEVICE / SCRIPT / TICKET / ORGANIZATION / USER /
|
|
15
|
+
* KB_ARTICLE / POLICY / QUERY / SCHEDULED_SCRIPT — no content types), so the
|
|
16
|
+
* prompt is ALL the agent gets. Hence `includeReference`, which appends the
|
|
17
|
+
* type + id so the agent can still identify the exact row by text.
|
|
18
|
+
*
|
|
19
|
+
* When the backend grows an id-filter for the agent transport, the Mingo callers
|
|
20
|
+
* switch to it and drop `includeReference` — the prose stays put.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { sanitizeTitleForChat } from './slash-dispatch-utils'
|
|
24
|
+
|
|
25
|
+
export interface DiscussPromptRef {
|
|
26
|
+
/** documentType, e.g. `'roadmap_item'`. */
|
|
27
|
+
type: string
|
|
28
|
+
/** Primary key — the id from the `[card://type:id]` marker. */
|
|
29
|
+
id: string
|
|
30
|
+
/** Display title. May be the bare id on a synthetic ref (Mingo). */
|
|
31
|
+
title?: string | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface BuildDiscussPromptOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Append `(<type> <id>)` so a transport WITHOUT structured entity filtering
|
|
37
|
+
* still points the agent at the exact row. Default `false` — the SSE path
|
|
38
|
+
* sends `entityIdFilter` and must keep the sentence clean.
|
|
39
|
+
*/
|
|
40
|
+
includeReference?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `Tell me more about <title>` — plus `(<type> <id>)` when
|
|
45
|
+
* `includeReference` is set and the title alone is ambiguous.
|
|
46
|
+
*
|
|
47
|
+
* Falls back to `this item` when the ref has no usable title, matching the
|
|
48
|
+
* long-standing SSE behavior. A title that is just the id (the synthetic ref a
|
|
49
|
+
* Mingo `[card://…]` marker produces before its row loads) is treated as
|
|
50
|
+
* missing, so the prompt never degrades to `Tell me more about 86ad3qvv5`.
|
|
51
|
+
*/
|
|
52
|
+
export function buildDiscussPrompt(
|
|
53
|
+
reference: DiscussPromptRef,
|
|
54
|
+
options: BuildDiscussPromptOptions = {},
|
|
55
|
+
): string {
|
|
56
|
+
const id = (reference.id ?? '').trim()
|
|
57
|
+
const rawTitle = (reference.title ?? '').trim()
|
|
58
|
+
const usableTitle = rawTitle && rawTitle !== id ? sanitizeTitleForChat(rawTitle) : ''
|
|
59
|
+
const prompt = `Tell me more about ${usableTitle || 'this item'}`
|
|
60
|
+
if (!options.includeReference || !id) return prompt
|
|
61
|
+
// Only worth the noise when the reference adds something the title doesn't.
|
|
62
|
+
return `${prompt} (${reference.type} ${id})`
|
|
63
|
+
}
|
|
@@ -196,4 +196,20 @@ export {
|
|
|
196
196
|
resolveSourceIcon,
|
|
197
197
|
} from './source-row-cta'
|
|
198
198
|
|
|
199
|
+
// Post-fetch enrichment for the synthetic ChatRef a `[card://type:id]` marker
|
|
200
|
+
// produces when the transport ships no refs metadata (Mingo/NATS).
|
|
201
|
+
export {
|
|
202
|
+
resolveFetchedCardHref,
|
|
203
|
+
readFetchedCardTitle,
|
|
204
|
+
type FetchedCardHrefInput,
|
|
205
|
+
} from './resolve-fetched-card-href'
|
|
206
|
+
|
|
207
|
+
// The ONE "Ask Mingo" prompt builder — shared by the SSE adapter, the NATS
|
|
208
|
+
// adapter, and hosts that inject their own Mingo state (openframe-frontend).
|
|
209
|
+
export {
|
|
210
|
+
buildDiscussPrompt,
|
|
211
|
+
type DiscussPromptRef,
|
|
212
|
+
type BuildDiscussPromptOptions,
|
|
213
|
+
} from './discuss-ref-prompt'
|
|
214
|
+
|
|
199
215
|
export * from './scripted-stream'
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-fetch href fallback for an inline chat card whose `ChatRef` carried no
|
|
3
|
+
* `externalUrl`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a fetch-mode card can arrive without a URL
|
|
6
|
+
*
|
|
7
|
+
* The two chat transports deliver entity references differently:
|
|
8
|
+
*
|
|
9
|
+
* - **SSE / guide transport** — the server ships a `refs` metadata frame, so
|
|
10
|
+
* each `[card://type:id]` marker expands into a FULL `ChatRef`
|
|
11
|
+
* (`title` / `url` / `targetPlatform` / `sourceRepo`). `resolveSourceRowCTA`
|
|
12
|
+
* then routes that `externalUrl` through the host's `composeContentUrl`
|
|
13
|
+
* seam and the card is clickable.
|
|
14
|
+
* - **Mingo / NATS transport** (`messageData: [{ type: 'GUIDE' | 'TEXT', text }]`)
|
|
15
|
+
* — the body carries bare `[card://type:id]` markers and NOTHING else.
|
|
16
|
+
* `chat-message-enhanced.tsx` synthesizes a minimal `{ type, id, title: id,
|
|
17
|
+
* url: null }` ref so fetch-mode cards can still self-fetch their row, but
|
|
18
|
+
* `resolveSourceRowCTA` has no `externalUrl`, no `path` and no
|
|
19
|
+
* `targetPlatform` to work with → `href: null` → the card renders with its
|
|
20
|
+
* real title and description but goes nowhere when clicked.
|
|
21
|
+
*
|
|
22
|
+
* This resolver closes that gap WITHOUT a second URL vocabulary: it calls the
|
|
23
|
+
* exact same `composeContentUrl` seam the page cards (`resolveContentHref`) and
|
|
24
|
+
* the SSE cards (`resolveSourceRowCTA`) use, so one host config —
|
|
25
|
+
* `hostedTypes` / `suffixes` / `overrides` — governs all three.
|
|
26
|
+
*
|
|
27
|
+
* `identifier` stays the marker's primary key (that is what `overrides` such as
|
|
28
|
+
* roadmap's `?search=<task id>` deep-link on); the row's `slug` rides the
|
|
29
|
+
* separate `slug` hint for hosted types whose detail route is slug-based.
|
|
30
|
+
*
|
|
31
|
+
* Pure — no React, no DOM. The caller owns `safeHref` validation.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { ComposeContentUrl } from '../../../utils/content-href'
|
|
35
|
+
import { canonicalContentRefType } from '../../../utils/list-url'
|
|
36
|
+
|
|
37
|
+
export interface FetchedCardHrefInput {
|
|
38
|
+
/** Canonical `documentType` for the seam (registry `contentRefType`). */
|
|
39
|
+
contentRefType: string
|
|
40
|
+
/** The marker's primary key — `ChatRef.id`, NOT the fetched row's id. */
|
|
41
|
+
id: string
|
|
42
|
+
/** The row the card fetched. Shape is per-type and opaque here; only
|
|
43
|
+
* `slug` and `platforms` are read, both defensively. */
|
|
44
|
+
item: unknown
|
|
45
|
+
/** Host seam. Absent (unwired runtime) → no fallback, same as today. */
|
|
46
|
+
composeContentUrl?: ComposeContentUrl
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Read `item.slug` when it is a non-empty string. */
|
|
50
|
+
function readSlug(item: unknown): string | null {
|
|
51
|
+
if (!item || typeof item !== 'object') return null
|
|
52
|
+
const slug = (item as { slug?: unknown }).slug
|
|
53
|
+
return typeof slug === 'string' && slug.trim() ? slug.trim() : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Human title of a fetched row — `title`, else `name`, else null.
|
|
58
|
+
*
|
|
59
|
+
* A Mingo synthetic ref carries `title: <the marker id>` because that is all
|
|
60
|
+
* the transport shipped. Everything downstream that prints or sends the title
|
|
61
|
+
* (the "Ask Mingo" prompt, host renderers falling back to `ref.title`) would
|
|
62
|
+
* otherwise say `86ad3qvv5`. Once the row loads, this is the real title.
|
|
63
|
+
*/
|
|
64
|
+
export function readFetchedCardTitle(item: unknown): string | null {
|
|
65
|
+
if (!item || typeof item !== 'object') return null
|
|
66
|
+
for (const key of ['title', 'name'] as const) {
|
|
67
|
+
const value = (item as Record<string, unknown>)[key]
|
|
68
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Read `item.platforms` when it is an array — the hub composer keys its
|
|
74
|
+
* cross-platform topology off it; the embedder default ignores it. */
|
|
75
|
+
function readPlatforms(item: unknown): Array<{ name?: string }> | undefined {
|
|
76
|
+
if (!item || typeof item !== 'object') return undefined
|
|
77
|
+
const platforms = (item as { platforms?: unknown }).platforms
|
|
78
|
+
return Array.isArray(platforms) ? (platforms as Array<{ name?: string }>) : undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve `{href, targetPlatform}` for a freshly fetched card row, or `null`
|
|
83
|
+
* when the host wired no seam (the caller then keeps the card unlinked, which
|
|
84
|
+
* is the pre-existing behavior).
|
|
85
|
+
*/
|
|
86
|
+
export function resolveFetchedCardHref({
|
|
87
|
+
contentRefType,
|
|
88
|
+
id,
|
|
89
|
+
item,
|
|
90
|
+
composeContentUrl,
|
|
91
|
+
}: FetchedCardHrefInput): { href: string; targetPlatform: string | null } | null {
|
|
92
|
+
if (!composeContentUrl || !contentRefType || !id) return null
|
|
93
|
+
return composeContentUrl({
|
|
94
|
+
// The card registry keys some entries by the legacy rail vocabulary
|
|
95
|
+
// (`blog_post_existing`) because that is what `buildListUrl` expects.
|
|
96
|
+
// The seam speaks the CANONICAL vocabulary — a host's `hostedTypes` /
|
|
97
|
+
// `overrides` are written against `blog_post`, so an un-aliased type
|
|
98
|
+
// misses them all and lands on `<origin>/blog_post_existing/<slug>`.
|
|
99
|
+
type: canonicalContentRefType(contentRefType),
|
|
100
|
+
identifier: id,
|
|
101
|
+
slug: readSlug(item),
|
|
102
|
+
platforms: readPlatforms(item),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
@@ -29,6 +29,7 @@ import { getBaseUrl } from '../../../utils/cn'
|
|
|
29
29
|
import { safeHref } from './compact-card-classes'
|
|
30
30
|
import type { ChatRef } from '../chat-ref.types'
|
|
31
31
|
import type { ComposeContentUrl } from '../../../utils/content-href'
|
|
32
|
+
import { canonicalContentRefType } from '../../../utils/list-url'
|
|
32
33
|
|
|
33
34
|
/** Path sanitization — keep alphanumerics, slash, dash, dot, underscore;
|
|
34
35
|
* strip everything else. Defends against a hostile mapper returning a
|
|
@@ -233,7 +234,10 @@ export function resolveSourceRowCTA(
|
|
|
233
234
|
// from the externalUrl) and everything else to the externalUrl verbatim —
|
|
234
235
|
// the SAME resolver the page-view cards use, so chat + page links match.
|
|
235
236
|
const composed = ctx.composeContentUrl({
|
|
236
|
-
|
|
237
|
+
// Canonical vocabulary only — a rail-vocab alias would miss the host's
|
|
238
|
+
// `hostedTypes` / `overrides` and fall through to the seam's default
|
|
239
|
+
// `<origin>/<type>/<id>` branch. Same canonicalizer `buildListUrl` uses.
|
|
240
|
+
type: canonicalContentRefType(row.documentType),
|
|
237
241
|
identifier: idValue,
|
|
238
242
|
externalUrl: row.externalUrl,
|
|
239
243
|
targetPlatform: row.targetPlatform ?? null,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { makeComposeContentUrl } from '../../../../utils/content-href'
|
|
3
|
+
import type { SearchResult } from '../../../ui/search-input'
|
|
4
|
+
import { resolveSearchResultAction } from '../resolve-search-result-action'
|
|
5
|
+
|
|
6
|
+
const HUB = 'https://www.flamingo.run'
|
|
7
|
+
|
|
8
|
+
/** A RAG row for a guide the hub owns, shaped like `mapDocSearchResults` emits. */
|
|
9
|
+
const guideRow: SearchResult = {
|
|
10
|
+
id: 'onboarding-guides/deploy-first-device',
|
|
11
|
+
title: 'Deploy your first device',
|
|
12
|
+
path: 'onboarding-guides/deploy-first-device',
|
|
13
|
+
metadata: {
|
|
14
|
+
documentType: 'onboarding_guide',
|
|
15
|
+
externalUrl: `${HUB}/onboarding-guides/deploy-first-device`,
|
|
16
|
+
sourceRepo: 'onboarding-guides',
|
|
17
|
+
id: 'guide-123',
|
|
18
|
+
targetPlatform: 'openframe',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The OpenFrame Help Center seam: the two hosted types live under
|
|
23
|
+
* `/help-center/...`, list-filter types deep-link with `?search=<id>`. */
|
|
24
|
+
const helpCenterCompose = makeComposeContentUrl({
|
|
25
|
+
hostedTypes: new Set(['onboarding_guide', 'product_release']),
|
|
26
|
+
contentOrigin: HUB,
|
|
27
|
+
suffixes: {
|
|
28
|
+
onboarding_guide: 'help-center/onboarding-guides',
|
|
29
|
+
product_release: 'help-center/releases',
|
|
30
|
+
blog_post: 'blog',
|
|
31
|
+
},
|
|
32
|
+
overrides: {
|
|
33
|
+
delivery_item: (id) => ({ href: `/help-center/bug-fixes?search=${id}`, targetPlatform: null }),
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('resolveSearchResultAction — composeContentUrl seam', () => {
|
|
38
|
+
it('no seam wired → the RAG externalUrl verbatim (legacy behavior)', () => {
|
|
39
|
+
expect(resolveSearchResultAction(guideRow, 'openframe')).toEqual({
|
|
40
|
+
kind: 'navigate-same-tab',
|
|
41
|
+
href: `${HUB}/onboarding-guides/deploy-first-device`,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('hosted type → the host in-app route, slug recovered from externalUrl', () => {
|
|
46
|
+
expect(
|
|
47
|
+
resolveSearchResultAction(guideRow, 'openframe', 'host', helpCenterCompose),
|
|
48
|
+
).toEqual({
|
|
49
|
+
kind: 'navigate-same-tab',
|
|
50
|
+
href: '/help-center/onboarding-guides/deploy-first-device',
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('relative composed href stays same-tab even in embed mode', () => {
|
|
55
|
+
// `decideNewTab` forces new-tab under embed; an in-app path must not be
|
|
56
|
+
// handed to window.open — the short-circuit runs before that decision.
|
|
57
|
+
expect(
|
|
58
|
+
resolveSearchResultAction(guideRow, 'openframe', 'embed', helpCenterCompose),
|
|
59
|
+
).toEqual({
|
|
60
|
+
kind: 'navigate-same-tab',
|
|
61
|
+
href: '/help-center/onboarding-guides/deploy-first-device',
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('override type → the host deep-link, primary key as identifier', () => {
|
|
66
|
+
const row: SearchResult = {
|
|
67
|
+
id: 'delivery-1',
|
|
68
|
+
title: 'Fix the thing',
|
|
69
|
+
metadata: {
|
|
70
|
+
documentType: 'delivery_item',
|
|
71
|
+
externalUrl: `${HUB}/bug-fixes-and-enhancements?search=abc123`,
|
|
72
|
+
sourceRepo: 'clickup-delivery',
|
|
73
|
+
id: 'abc123',
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
expect(resolveSearchResultAction(row, 'openframe', 'host', helpCenterCompose)).toEqual({
|
|
77
|
+
kind: 'navigate-same-tab',
|
|
78
|
+
href: '/help-center/bug-fixes?search=abc123',
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('non-hosted type → the seam returns externalUrl verbatim, opens out', () => {
|
|
83
|
+
const row: SearchResult = {
|
|
84
|
+
id: 'blog-1',
|
|
85
|
+
title: 'Hello',
|
|
86
|
+
metadata: {
|
|
87
|
+
documentType: 'blog_post',
|
|
88
|
+
externalUrl: `${HUB}/blog/hello`,
|
|
89
|
+
sourceRepo: 'blog-posts',
|
|
90
|
+
id: 'blog-1',
|
|
91
|
+
targetPlatform: 'flamingo',
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
expect(resolveSearchResultAction(row, 'openframe', 'host', helpCenterCompose)).toEqual({
|
|
95
|
+
kind: 'navigate-new-tab',
|
|
96
|
+
href: `${HUB}/blog/hello`,
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('row without documentType → seam skipped, externalUrl verbatim', () => {
|
|
101
|
+
const row: SearchResult = {
|
|
102
|
+
id: 'x',
|
|
103
|
+
title: 'Untyped',
|
|
104
|
+
metadata: { externalUrl: `${HUB}/somewhere`, targetPlatform: 'openframe' },
|
|
105
|
+
}
|
|
106
|
+
expect(resolveSearchResultAction(row, 'openframe', 'host', helpCenterCompose)).toEqual({
|
|
107
|
+
kind: 'navigate-same-tab',
|
|
108
|
+
href: `${HUB}/somewhere`,
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('no externalUrl → unchanged ask-ai / route / noop fallbacks', () => {
|
|
113
|
+
expect(
|
|
114
|
+
resolveSearchResultAction(
|
|
115
|
+
{
|
|
116
|
+
id: 'cap-1',
|
|
117
|
+
title: 'Cap Table',
|
|
118
|
+
metadata: { documentType: 'cap_table', sourceRepo: 'financial-cap-table', id: 'row-9' },
|
|
119
|
+
},
|
|
120
|
+
'company-hub',
|
|
121
|
+
'host',
|
|
122
|
+
helpCenterCompose,
|
|
123
|
+
),
|
|
124
|
+
).toEqual({
|
|
125
|
+
kind: 'ask-ai',
|
|
126
|
+
detail: {
|
|
127
|
+
source: 'company-hub',
|
|
128
|
+
ref: { type: 'cap_table', id: 'row-9', title: 'Cap Table', url: null },
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
resolveSearchResultAction(
|
|
134
|
+
{ id: 'doc', title: 'Doc', path: 'repo/architecture/api.md' },
|
|
135
|
+
'openframe',
|
|
136
|
+
'host',
|
|
137
|
+
helpCenterCompose,
|
|
138
|
+
),
|
|
139
|
+
).toEqual({ kind: 'route', path: 'repo/architecture/api.md' })
|
|
140
|
+
|
|
141
|
+
expect(
|
|
142
|
+
resolveSearchResultAction({ id: 'empty', title: 'Empty' }, 'openframe', 'host', helpCenterCompose),
|
|
143
|
+
).toEqual({ kind: 'noop' })
|
|
144
|
+
})
|
|
145
|
+
})
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* Returns one of five typed actions so the caller is a single switch.
|
|
4
4
|
*
|
|
5
5
|
* Resolution order:
|
|
6
|
-
* 1. `externalUrl` present →
|
|
7
|
-
*
|
|
6
|
+
* 1. `externalUrl` present → resolve the href through the host's
|
|
7
|
+
* `composeContentUrl` seam (when wired), then use `decideNewTab` to
|
|
8
|
+
* choose same-tab vs new-tab against the resolved `targetPlatform`.
|
|
8
9
|
* 2. Row has `id` + `sourceRepo` + `documentType` → synth an Ask-AI
|
|
9
10
|
* action (entity drill-in via primary key, no URL).
|
|
10
11
|
* 3. Row has only `path` → legacy navigation fallback.
|
|
@@ -16,8 +17,16 @@
|
|
|
16
17
|
|
|
17
18
|
import type { SearchResult } from '../../ui/search-input'
|
|
18
19
|
import type { ChatRef } from '../../chat/chat-ref.types'
|
|
20
|
+
import type { ComposeContentUrl } from '../../../utils/content-href'
|
|
19
21
|
import { decideNewTab } from '../../chat/utils/decide-new-tab'
|
|
20
22
|
|
|
23
|
+
/** True for `https://…` and protocol-relative `//…`. A composed href that is
|
|
24
|
+
* NOT absolute is an in-app route the host claimed via `hostedTypes` /
|
|
25
|
+
* `overrides`. */
|
|
26
|
+
function isAbsoluteHref(href: string): boolean {
|
|
27
|
+
return /^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(href)
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
export type SearchResultAction =
|
|
22
31
|
| { kind: 'navigate-same-tab'; href: string }
|
|
23
32
|
| { kind: 'navigate-new-tab'; href: string }
|
|
@@ -29,29 +38,56 @@ export function resolveSearchResultAction(
|
|
|
29
38
|
result: SearchResult,
|
|
30
39
|
source: string,
|
|
31
40
|
runtimeMode?: 'host' | 'embed',
|
|
41
|
+
/** The host's unified content-href seam (`runtime.composeContentUrl`).
|
|
42
|
+
* When wired, an entity row's RAG `externalUrl` is re-resolved through it,
|
|
43
|
+
* so a type the host serves in-app (`hostedTypes`) or deep-links itself
|
|
44
|
+
* (`overrides`) lands on the SAME href the catalog cards and chat cards
|
|
45
|
+
* use. Omitted → the legacy verbatim-`externalUrl` behavior. */
|
|
46
|
+
composeContentUrl?: ComposeContentUrl,
|
|
32
47
|
): SearchResultAction {
|
|
33
48
|
const meta = result.metadata ?? {}
|
|
34
49
|
const externalUrl = meta.externalUrl as string | undefined
|
|
50
|
+
const rowId = meta.id as string | undefined
|
|
51
|
+
const sourceRepo = meta.sourceRepo as string | undefined
|
|
52
|
+
const documentType = meta.documentType as string | undefined
|
|
35
53
|
if (externalUrl) {
|
|
54
|
+
const targetPlatform = meta.targetPlatform as string | null | undefined
|
|
55
|
+
// Unified content-href seam — the same call `resolveSourceRowCTA` makes
|
|
56
|
+
// for chat rows. Hosted types relativize the slug out of `externalUrl`;
|
|
57
|
+
// everything else comes back verbatim, so an unwired host (or a row with
|
|
58
|
+
// no `documentType`) keeps today's behavior byte-for-byte.
|
|
59
|
+
const composed =
|
|
60
|
+
composeContentUrl && documentType
|
|
61
|
+
? composeContentUrl({
|
|
62
|
+
type: documentType,
|
|
63
|
+
identifier: rowId ?? '',
|
|
64
|
+
externalUrl,
|
|
65
|
+
targetPlatform: targetPlatform ?? null,
|
|
66
|
+
})
|
|
67
|
+
: null
|
|
68
|
+
const href = composed?.href || externalUrl
|
|
69
|
+
// A RELATIVE composed href means the host claimed this row as in-app.
|
|
70
|
+
// Short-circuit BEFORE `decideNewTab`: in embed mode that helper forces
|
|
71
|
+
// new-tab unconditionally, which would fire `window.open()` on a path
|
|
72
|
+
// that only resolves inside this app.
|
|
73
|
+
if (composed && !isAbsoluteHref(href)) {
|
|
74
|
+
return { kind: 'navigate-same-tab', href }
|
|
75
|
+
}
|
|
36
76
|
// Same pure helper `useNavLink` and `useUnifiedNav` call — single
|
|
37
77
|
// decision rule across cards, chips, and autocomplete rows. Thread
|
|
38
78
|
// the caller's `source` as `currentSource` so the platform-vs-
|
|
39
79
|
// platform comparison matches the hub's pre-migration behavior.
|
|
40
|
-
const targetPlatform = meta.targetPlatform as string | null | undefined
|
|
41
80
|
const isNewTab = decideNewTab({
|
|
42
|
-
href
|
|
43
|
-
targetPlatform,
|
|
81
|
+
href,
|
|
82
|
+
targetPlatform: composed ? composed.targetPlatform : targetPlatform,
|
|
44
83
|
surface: 'useUnifiedNav',
|
|
45
84
|
runtimeMode,
|
|
46
85
|
currentSource: source,
|
|
47
86
|
})
|
|
48
87
|
return isNewTab
|
|
49
|
-
? { kind: 'navigate-new-tab', href
|
|
50
|
-
: { kind: 'navigate-same-tab', href
|
|
88
|
+
? { kind: 'navigate-new-tab', href }
|
|
89
|
+
: { kind: 'navigate-same-tab', href }
|
|
51
90
|
}
|
|
52
|
-
const rowId = meta.id as string | undefined
|
|
53
|
-
const sourceRepo = meta.sourceRepo as string | undefined
|
|
54
|
-
const documentType = meta.documentType as string | undefined
|
|
55
91
|
if (rowId && sourceRepo && documentType) {
|
|
56
92
|
return {
|
|
57
93
|
kind: 'ask-ai',
|