@flamingo-stack/openframe-frontend-core 0.0.510 → 0.0.512-1640.4207.1
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-J2JUAS4C.cjs → chunk-24FYTTPB.cjs} +11 -11
- package/dist/{chunk-J2JUAS4C.cjs.map → chunk-24FYTTPB.cjs.map} +1 -1
- package/dist/{chunk-GE3JUTW2.js → chunk-27OKD7ZV.js} +44 -14
- package/dist/chunk-27OKD7ZV.js.map +1 -0
- package/dist/{chunk-SRYV4H3M.js → chunk-2SSVGMTE.js} +3 -3
- package/dist/{chunk-2AASLY6D.js → chunk-3KAZSJ2I.js} +3 -3
- package/dist/{chunk-Q4K3TGNP.cjs → chunk-3NECEXDF.cjs} +74 -44
- package/dist/chunk-3NECEXDF.cjs.map +1 -0
- package/dist/{chunk-5CDPRCPS.cjs → chunk-44ML4VMJ.cjs} +23 -23
- package/dist/{chunk-5CDPRCPS.cjs.map → chunk-44ML4VMJ.cjs.map} +1 -1
- package/dist/{chunk-WSAPKKDU.cjs → chunk-4NML2ZGN.cjs} +9 -9
- package/dist/{chunk-WSAPKKDU.cjs.map → chunk-4NML2ZGN.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-HX4CJYXL.js → chunk-5MC7ZKRX.js} +10 -10
- package/dist/{chunk-RHQ7BJKK.cjs → chunk-6I36GENY.cjs} +73 -73
- package/dist/{chunk-RHQ7BJKK.cjs.map → chunk-6I36GENY.cjs.map} +1 -1
- package/dist/{chunk-6FIDTXYC.js → chunk-7AQSXQEL.js} +3 -3
- package/dist/{chunk-EJ3EBYNV.cjs → chunk-AF2PYDNF.cjs} +32 -32
- package/dist/{chunk-EJ3EBYNV.cjs.map → chunk-AF2PYDNF.cjs.map} +1 -1
- package/dist/{chunk-2RLI5XVI.cjs → chunk-B2PCKLRF.cjs} +16 -16
- package/dist/{chunk-2RLI5XVI.cjs.map → chunk-B2PCKLRF.cjs.map} +1 -1
- package/dist/{chunk-Z35QWWJH.cjs → chunk-BQFMSIXM.cjs} +34 -34
- package/dist/{chunk-Z35QWWJH.cjs.map → chunk-BQFMSIXM.cjs.map} +1 -1
- package/dist/{chunk-6TL5XIAW.cjs → chunk-CS2MLJXE.cjs} +119 -119
- package/dist/{chunk-6TL5XIAW.cjs.map → chunk-CS2MLJXE.cjs.map} +1 -1
- package/dist/{chunk-JMBIGH4H.cjs → chunk-DON2EEFT.cjs} +83 -83
- package/dist/{chunk-JMBIGH4H.cjs.map → chunk-DON2EEFT.cjs.map} +1 -1
- package/dist/{chunk-RJ3D3PE2.cjs → chunk-DZR4XHZH.cjs} +9 -9
- package/dist/{chunk-RJ3D3PE2.cjs.map → chunk-DZR4XHZH.cjs.map} +1 -1
- package/dist/{chunk-UWPT23SA.js → chunk-EFTTERZ5.js} +3 -3
- package/dist/{chunk-BTUHMCYW.js → chunk-FS3YSNSU.js} +193 -6
- package/dist/chunk-FS3YSNSU.js.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-5AZZQQYS.cjs → chunk-HKRLXUU6.cjs} +40 -40
- package/dist/{chunk-5AZZQQYS.cjs.map → chunk-HKRLXUU6.cjs.map} +1 -1
- package/dist/{chunk-WNOGR5AA.js → chunk-IG3PRDHZ.js} +3 -3
- 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-XJVRAKEX.cjs → chunk-K6E2VQ4W.cjs} +56 -33
- package/dist/chunk-K6E2VQ4W.cjs.map +1 -0
- package/dist/{chunk-A62C4C4V.cjs → chunk-KI4EGKNV.cjs} +3 -2
- package/dist/chunk-KI4EGKNV.cjs.map +1 -0
- package/dist/{chunk-RB2EW6IZ.cjs → chunk-LCWCUFJJ.cjs} +3351 -3173
- package/dist/chunk-LCWCUFJJ.cjs.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-53JKCFFE.cjs → chunk-O37MV2JA.cjs} +12 -12
- package/dist/{chunk-53JKCFFE.cjs.map → chunk-O37MV2JA.cjs.map} +1 -1
- package/dist/{chunk-EYTXD62B.cjs → chunk-OGVIGAMZ.cjs} +232 -45
- package/dist/chunk-OGVIGAMZ.cjs.map +1 -0
- package/dist/{chunk-JOCFPAS6.js → chunk-OY6M4LX4.js} +6 -6
- package/dist/{chunk-HCZJZMU7.js → chunk-P5KFFOLJ.js} +3 -3
- package/dist/{chunk-FEGEC6FY.js → chunk-SQJXVADG.js} +5292 -5114
- package/dist/chunk-SQJXVADG.js.map +1 -0
- package/dist/{chunk-M4MKQKFF.js → chunk-UBTOOE55.js} +2 -2
- package/dist/{chunk-CTMLY2GN.js → chunk-UGOCHX36.js} +5 -5
- package/dist/{chunk-VDJF4ZAU.js → chunk-UGR7V2ZG.js} +3 -3
- package/dist/{chunk-TBGSQTJK.js → chunk-W2HEWV34.js} +6 -6
- package/dist/{chunk-LBOWSKGM.js → chunk-XE3OSQNH.js} +5 -5
- package/dist/{chunk-MLGGF24T.js → chunk-XRGYYEIX.js} +8 -8
- package/dist/{chunk-SZMA65OW.js → chunk-XURRAFIB.js} +2 -2
- package/dist/{chunk-DBR5JLX3.js → chunk-YNBRXJWC.js} +46 -23
- package/dist/chunk-YNBRXJWC.js.map +1 -0
- package/dist/{chunk-RG2WRBPT.js → chunk-Z3ADCYC5.js} +2 -2
- package/dist/{chunk-4GPWLGKT.js → chunk-ZRILYK5U.js} +2 -1
- 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-chat.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-chat.ts +174 -15
- 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-SRYV4H3M.js.map → chunk-2SSVGMTE.js.map} +0 -0
- /package/dist/{chunk-2AASLY6D.js.map → chunk-3KAZSJ2I.js.map} +0 -0
- /package/dist/{chunk-HX4CJYXL.js.map → chunk-5MC7ZKRX.js.map} +0 -0
- /package/dist/{chunk-6FIDTXYC.js.map → chunk-7AQSXQEL.js.map} +0 -0
- /package/dist/{chunk-UWPT23SA.js.map → chunk-EFTTERZ5.js.map} +0 -0
- /package/dist/{chunk-WNOGR5AA.js.map → chunk-IG3PRDHZ.js.map} +0 -0
- /package/dist/{chunk-JOCFPAS6.js.map → chunk-OY6M4LX4.js.map} +0 -0
- /package/dist/{chunk-HCZJZMU7.js.map → chunk-P5KFFOLJ.js.map} +0 -0
- /package/dist/{chunk-M4MKQKFF.js.map → chunk-UBTOOE55.js.map} +0 -0
- /package/dist/{chunk-CTMLY2GN.js.map → chunk-UGOCHX36.js.map} +0 -0
- /package/dist/{chunk-VDJF4ZAU.js.map → chunk-UGR7V2ZG.js.map} +0 -0
- /package/dist/{chunk-TBGSQTJK.js.map → chunk-W2HEWV34.js.map} +0 -0
- /package/dist/{chunk-LBOWSKGM.js.map → chunk-XE3OSQNH.js.map} +0 -0
- /package/dist/{chunk-MLGGF24T.js.map → chunk-XRGYYEIX.js.map} +0 -0
- /package/dist/{chunk-SZMA65OW.js.map → chunk-XURRAFIB.js.map} +0 -0
- /package/dist/{chunk-RG2WRBPT.js.map → chunk-Z3ADCYC5.js.map} +0 -0
- /package/dist/{chunk-4GPWLGKT.js.map → chunk-ZRILYK5U.js.map} +0 -0
|
@@ -56,6 +56,7 @@ import { createChatDialogStore, DEFAULT_DIALOG_SIDE } from '../stream/chat-dialo
|
|
|
56
56
|
import { useChatStreamReducer } from '../stream/use-chat-stream-reducer'
|
|
57
57
|
import { processHistoricalMessagesWithErrors } from '../utils/process-historical-messages'
|
|
58
58
|
import { extractIncompleteTailState } from '../utils/extract-incomplete-message-state'
|
|
59
|
+
import { buildDiscussPrompt } from '../utils/discuss-ref-prompt'
|
|
59
60
|
import type {
|
|
60
61
|
ChunkData,
|
|
61
62
|
FetchChunksFunction,
|
|
@@ -1076,10 +1077,33 @@ export function useNatsChatAdapter(
|
|
|
1076
1077
|
[],
|
|
1077
1078
|
)
|
|
1078
1079
|
|
|
1079
|
-
//
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1080
|
+
// "Ask Mingo" on an inline entity card. The agent transport has NO
|
|
1081
|
+
// entity-id-filtered retrieval (its `ContextItemType` enum covers DEVICE /
|
|
1082
|
+
// SCRIPT / TICKET / ORGANIZATION / USER / KB_ARTICLE / POLICY / QUERY /
|
|
1083
|
+
// SCHEDULED_SCRIPT — no content types), so unlike the SSE path there is no
|
|
1084
|
+
// `commandOverride.entityIdFilter` to send: the prompt carries the type + id
|
|
1085
|
+
// as text and the agent resolves the row itself. Same sentence as guide mode
|
|
1086
|
+
// (shared builder) so the affordance reads identically in both.
|
|
1087
|
+
//
|
|
1088
|
+
// This used to be a no-op, which made the card's "Ask Mingo" menu item dead
|
|
1089
|
+
// in Mingo mode. Swap `includeReference` for a structured filter once the
|
|
1090
|
+
// agent backend accepts one.
|
|
1091
|
+
const discussRef = useCallback(
|
|
1092
|
+
(reference: ChatRef) => {
|
|
1093
|
+
const prompt = buildDiscussPrompt(reference, { includeReference: true })
|
|
1094
|
+
// Menu-item click, so there is no composer promise for the rejection to
|
|
1095
|
+
// surface through — a bare `void` would leave it unhandled. Logged in the
|
|
1096
|
+
// same shape as `stopMessage`'s. NOTE: recovering the optimistic rows and
|
|
1097
|
+
// the stuck `thinking` phase after a failed publish is a `sendMessage`
|
|
1098
|
+
// concern (it affects composer sends identically) and is not done here.
|
|
1099
|
+
void sendMessage(prompt).catch((err) => {
|
|
1100
|
+
console.error('[useNatsChatAdapter] discussRef send failed:', err)
|
|
1101
|
+
})
|
|
1102
|
+
},
|
|
1103
|
+
[sendMessage],
|
|
1104
|
+
)
|
|
1105
|
+
// Display stays a no-op: it dispatches a `/… display "<x>"` slash command,
|
|
1106
|
+
// and the agent transport has no slash-command registry.
|
|
1083
1107
|
const displayRef = useCallback((_ref: ChatRef) => {
|
|
1084
1108
|
/* no-op in Mingo mode */
|
|
1085
1109
|
}, [])
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
} from '../utils/chat-conversation-storage'
|
|
68
68
|
import { useChatHistoryHydration } from './use-chat-history-hydration'
|
|
69
69
|
import { sanitizeTitleForChat } from '../utils/slash-dispatch-utils'
|
|
70
|
+
import { buildDiscussPrompt } from '../utils/discuss-ref-prompt'
|
|
70
71
|
import { defaultTableIdForDocumentType } from '../utils/source-icons'
|
|
71
72
|
import type { WireCommandOverride } from '../utils/slash-dispatch-utils'
|
|
72
73
|
import type { ChatAttachment } from '../utils/chat-attachment-markdown'
|
|
@@ -657,9 +658,10 @@ export function useSseChatAdapter(
|
|
|
657
658
|
}
|
|
658
659
|
// RETRIEVAL IS STRICTLY PRIMARY-KEY-DRIVEN. The visible prose
|
|
659
660
|
// ("Tell me more about <title>") is UX-only; retrieval narrows
|
|
660
|
-
// via `entityIdFilter` before the LLM is invoked.
|
|
661
|
-
|
|
662
|
-
|
|
661
|
+
// via `entityIdFilter` before the LLM is invoked. The sentence itself
|
|
662
|
+
// comes from the shared builder so the Mingo transport (which has no
|
|
663
|
+
// id filter and leans on the prose) phrases it identically.
|
|
664
|
+
const prompt = buildDiscussPrompt(reference)
|
|
663
665
|
sendMessage(prompt, {
|
|
664
666
|
commandOverride: { entityIdFilter: { tableId, id: refId } },
|
|
665
667
|
})
|
|
@@ -15,9 +15,12 @@
|
|
|
15
15
|
* intact, and the marker disappears before any rehype-raw HTML pass.
|
|
16
16
|
*
|
|
17
17
|
* Grammar:
|
|
18
|
-
* - marker: `[a-
|
|
18
|
+
* - marker: `[a-zA-Z]+` — the backend `ContextItemType.marker()` short form
|
|
19
19
|
* (`device`, `script`, `ticket`, `organization`, `user`, `kb`, `policy`,
|
|
20
|
-
* `query`, …).
|
|
20
|
+
* `query`, `scheduledScript`, …). Mostly lowercase, but NOT guaranteed to be:
|
|
21
|
+
* the marker is matched verbatim by the backend's `MentionParser`, and
|
|
22
|
+
* `SCHEDULED_SCRIPT` ships as camelCase `scheduledScript` — a lowercase-only
|
|
23
|
+
* grammar silently dropped that whole token (no chip, raw text in the bubble).
|
|
21
24
|
* - id: charset `[A-Za-z0-9_.+/=-]` (UUIDs with hyphens, fleet numeric ids,
|
|
22
25
|
* base64 global ids, etc.; matches the composer's `MENTION_TOKEN`) — but the
|
|
23
26
|
* id may NOT END in `.` or `-`, so a trailing SENTENCE period/dash isn't
|
|
@@ -35,7 +38,7 @@ import type { Plugin } from 'unified'
|
|
|
35
38
|
import type { Root, Text, Link } from 'mdast'
|
|
36
39
|
import { visit, SKIP } from 'unist-util-visit'
|
|
37
40
|
|
|
38
|
-
const MENTION_REGEX = /(^|[^\w@])@([a-
|
|
41
|
+
const MENTION_REGEX = /(^|[^\w@])@([a-zA-Z]+):([A-Za-z0-9_.+/=-]*[A-Za-z0-9_+/=])/g
|
|
39
42
|
|
|
40
43
|
export const remarkMentionChips: Plugin<[], Root> = () => {
|
|
41
44
|
return (tree: Root) => {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildDiscussPrompt } from '../discuss-ref-prompt'
|
|
3
|
+
import { readFetchedCardTitle } from '../resolve-fetched-card-href'
|
|
4
|
+
|
|
5
|
+
const ROADMAP_REF = {
|
|
6
|
+
type: 'roadmap_item',
|
|
7
|
+
id: '86ad3qvv5',
|
|
8
|
+
title: 'Deep Google Workspace tenant management',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('buildDiscussPrompt', () => {
|
|
12
|
+
it('guide/SSE shape — prose only, retrieval rides entityIdFilter', () => {
|
|
13
|
+
expect(buildDiscussPrompt(ROADMAP_REF)).toBe(
|
|
14
|
+
'Tell me more about Deep Google Workspace tenant management',
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('Mingo shape — appends the reference the agent has no filter for', () => {
|
|
19
|
+
expect(buildDiscussPrompt(ROADMAP_REF, { includeReference: true })).toBe(
|
|
20
|
+
'Tell me more about Deep Google Workspace tenant management (roadmap_item 86ad3qvv5)',
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('a synthetic ref (title === id) never leaks the id as the subject', () => {
|
|
25
|
+
const synthetic = { type: 'roadmap_item', id: '86ad3qvv5', title: '86ad3qvv5' }
|
|
26
|
+
expect(buildDiscussPrompt(synthetic, { includeReference: true })).toBe(
|
|
27
|
+
'Tell me more about this item (roadmap_item 86ad3qvv5)',
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('missing / blank title falls back to "this item"', () => {
|
|
32
|
+
expect(buildDiscussPrompt({ type: 'blog_post', id: 'b1' })).toBe('Tell me more about this item')
|
|
33
|
+
expect(buildDiscussPrompt({ type: 'blog_post', id: 'b1', title: ' ' })).toBe(
|
|
34
|
+
'Tell me more about this item',
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('no id → no reference suffix', () => {
|
|
39
|
+
expect(buildDiscussPrompt({ type: 'blog_post', id: '', title: 'Hello' }, { includeReference: true })).toBe(
|
|
40
|
+
'Tell me more about Hello',
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('readFetchedCardTitle — what upgrades a synthetic ref before the prompt', () => {
|
|
46
|
+
it('prefers title, then name', () => {
|
|
47
|
+
expect(readFetchedCardTitle({ title: 'Roadmap row' })).toBe('Roadmap row')
|
|
48
|
+
expect(readFetchedCardTitle({ name: 'Named row' })).toBe('Named row')
|
|
49
|
+
expect(readFetchedCardTitle({ title: ' ', name: 'Named row' })).toBe('Named row')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('null for rows with neither (and for non-objects)', () => {
|
|
53
|
+
expect(readFetchedCardTitle({ headline: 'nope' })).toBeNull()
|
|
54
|
+
expect(readFetchedCardTitle(null)).toBeNull()
|
|
55
|
+
expect(readFetchedCardTitle('string')).toBeNull()
|
|
56
|
+
expect(readFetchedCardTitle({ title: 42 })).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('end to end: fetched row title reaches the prompt', () => {
|
|
60
|
+
const synthetic = { type: 'roadmap_item', id: '86ad3qvv5', title: '86ad3qvv5' }
|
|
61
|
+
const row = { id: 'db-9', title: 'Deep Google Workspace tenant management' }
|
|
62
|
+
const enriched = { ...synthetic, title: readFetchedCardTitle(row) ?? synthetic.title }
|
|
63
|
+
expect(buildDiscussPrompt(enriched, { includeReference: true })).toBe(
|
|
64
|
+
'Tell me more about Deep Google Workspace tenant management (roadmap_item 86ad3qvv5)',
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -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
|
+
})
|