@flamingo-stack/openframe-frontend-core 0.0.690 → 0.0.691-2504.7650.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-22RSJ57Z.js → chunk-2YVTGPM7.js} +2 -2
- package/dist/{chunk-V5NOURSR.js → chunk-4HZXOQRM.js} +5 -5
- package/dist/{chunk-MDYTAKJ5.cjs → chunk-4N2JOPAQ.cjs} +22 -22
- package/dist/{chunk-MDYTAKJ5.cjs.map → chunk-4N2JOPAQ.cjs.map} +1 -1
- package/dist/{chunk-QNWQQ4RT.cjs → chunk-53O6PQ3F.cjs} +7 -7
- package/dist/{chunk-QNWQQ4RT.cjs.map → chunk-53O6PQ3F.cjs.map} +1 -1
- package/dist/{chunk-X4WBGDNW.cjs → chunk-5N2ZSB42.cjs} +18 -18
- package/dist/{chunk-X4WBGDNW.cjs.map → chunk-5N2ZSB42.cjs.map} +1 -1
- package/dist/{chunk-XRSJER2B.js → chunk-6Z26ZMR4.js} +4 -4
- package/dist/{chunk-HLEXYTOE.js → chunk-7EPGJOKN.js} +4 -4
- package/dist/{chunk-DOUG4UEZ.js → chunk-7WYWPELF.js} +11 -11
- package/dist/{chunk-ZBQZJAC7.cjs → chunk-BXAGR3PO.cjs} +7 -7
- package/dist/chunk-BXAGR3PO.cjs.map +1 -0
- package/dist/{chunk-QUGV2GT3.cjs → chunk-BXE2RZEV.cjs} +34 -34
- package/dist/{chunk-QUGV2GT3.cjs.map → chunk-BXE2RZEV.cjs.map} +1 -1
- package/dist/{chunk-DLRE5Y3G.js → chunk-C2SVJVEB.js} +2 -2
- package/dist/{chunk-OC3QH5WG.js → chunk-CMYDGVTZ.js} +2 -2
- package/dist/{chunk-VLWUKFQE.cjs → chunk-CWXRZVDI.cjs} +751 -710
- package/dist/chunk-CWXRZVDI.cjs.map +1 -0
- package/dist/{chunk-2PQKZWWP.js → chunk-DC3G4XGP.js} +6 -6
- package/dist/{chunk-KZXKT5AX.cjs → chunk-DDWLF2JV.cjs} +14 -14
- package/dist/{chunk-KZXKT5AX.cjs.map → chunk-DDWLF2JV.cjs.map} +1 -1
- package/dist/{chunk-NMXL34Z2.cjs → chunk-DVSPRY2V.cjs} +3 -3
- package/dist/{chunk-NMXL34Z2.cjs.map → chunk-DVSPRY2V.cjs.map} +1 -1
- package/dist/{chunk-OQVHGPHA.js → chunk-DZMDG7PC.js} +3 -3
- package/dist/{chunk-2MGTNYHW.cjs → chunk-ESFPY5FF.cjs} +100 -100
- package/dist/{chunk-2MGTNYHW.cjs.map → chunk-ESFPY5FF.cjs.map} +1 -1
- package/dist/{chunk-S5M5SEW2.js → chunk-FNMQ2J44.js} +5 -5
- package/dist/{chunk-UAKWSR3U.js → chunk-GDZ7IC5B.js} +2 -2
- package/dist/{chunk-OD2WMUAO.cjs → chunk-GZRU2A4L.cjs} +197 -197
- package/dist/{chunk-OD2WMUAO.cjs.map → chunk-GZRU2A4L.cjs.map} +1 -1
- package/dist/{chunk-3ZAIHRQH.cjs → chunk-H54RDLVC.cjs} +35 -35
- package/dist/{chunk-3ZAIHRQH.cjs.map → chunk-H54RDLVC.cjs.map} +1 -1
- package/dist/{chunk-Q76YVNCQ.js → chunk-HM7ZGABI.js} +2 -2
- package/dist/{chunk-YCKGUQA2.cjs → chunk-I27PWVWE.cjs} +17 -12
- package/dist/chunk-I27PWVWE.cjs.map +1 -0
- package/dist/{chunk-25X43LSD.js → chunk-JSG7WZKV.js} +4 -4
- package/dist/{chunk-AIXPQT3G.js → chunk-KMW2PXBU.js} +52 -11
- package/dist/chunk-KMW2PXBU.js.map +1 -0
- package/dist/{chunk-TBJWPNHU.js → chunk-KVDLL4LU.js} +4 -4
- package/dist/chunk-KVDLL4LU.js.map +1 -0
- package/dist/{chunk-54X75QTC.cjs → chunk-LCENRN2N.cjs} +14 -14
- package/dist/{chunk-54X75QTC.cjs.map → chunk-LCENRN2N.cjs.map} +1 -1
- package/dist/{chunk-BJHM2OEY.cjs → chunk-LNKLB7GM.cjs} +17 -17
- package/dist/{chunk-BJHM2OEY.cjs.map → chunk-LNKLB7GM.cjs.map} +1 -1
- package/dist/{chunk-JGVJRUGB.cjs → chunk-MIZU2OTO.cjs} +4 -4
- package/dist/{chunk-JGVJRUGB.cjs.map → chunk-MIZU2OTO.cjs.map} +1 -1
- package/dist/{chunk-TSENCZUL.js → chunk-MJI5GVT7.js} +11 -11
- package/dist/{chunk-BOM6MMWB.cjs → chunk-N3526CTH.cjs} +3 -3
- package/dist/{chunk-BOM6MMWB.cjs.map → chunk-N3526CTH.cjs.map} +1 -1
- package/dist/{chunk-D7BFS7N6.cjs → chunk-N4S7RBUM.cjs} +3 -3
- package/dist/{chunk-D7BFS7N6.cjs.map → chunk-N4S7RBUM.cjs.map} +1 -1
- package/dist/{chunk-4HBYEZ3I.cjs → chunk-SA4KQCNB.cjs} +82 -82
- package/dist/{chunk-4HBYEZ3I.cjs.map → chunk-SA4KQCNB.cjs.map} +1 -1
- package/dist/{chunk-RF2EYX3B.js → chunk-SC44YJCF.js} +2 -2
- package/dist/{chunk-2OD2PPNO.cjs → chunk-SDORFSFK.cjs} +37 -37
- package/dist/{chunk-2OD2PPNO.cjs.map → chunk-SDORFSFK.cjs.map} +1 -1
- package/dist/{chunk-42RQ4NIL.js → chunk-TSNGVWQO.js} +6 -6
- package/dist/{chunk-DMXI5D2Q.cjs → chunk-USKQURNF.cjs} +3 -3
- package/dist/{chunk-DMXI5D2Q.cjs.map → chunk-USKQURNF.cjs.map} +1 -1
- package/dist/{chunk-2KILTCND.js → chunk-UVUNBXS6.js} +6 -1
- package/dist/chunk-UVUNBXS6.js.map +1 -0
- package/dist/{chunk-5CBQBTGN.cjs → chunk-WZYIAKIH.cjs} +7 -7
- package/dist/{chunk-5CBQBTGN.cjs.map → chunk-WZYIAKIH.cjs.map} +1 -1
- package/dist/{chunk-2V4KPBNZ.js → chunk-XAOAYDGR.js} +3 -3
- package/dist/{chunk-U4OJCJTH.js → chunk-XBYF4SGD.js} +4 -4
- package/dist/{chunk-FCQR6VY4.cjs → chunk-Y3BC7TOJ.cjs} +26 -26
- package/dist/{chunk-FCQR6VY4.cjs.map → chunk-Y3BC7TOJ.cjs.map} +1 -1
- package/dist/{chunk-P27OGSQL.cjs → chunk-Y3Q76RAA.cjs} +37 -37
- package/dist/{chunk-P27OGSQL.cjs.map → chunk-Y3Q76RAA.cjs.map} +1 -1
- package/dist/{chunk-4BUPMSLY.js → chunk-Y7ZEVTLC.js} +3 -3
- package/dist/{chunk-Z622ACFD.js → chunk-Y7ZQZOE6.js} +5 -5
- package/dist/{chunk-5A3WSWZD.js → chunk-Z5FHXCXF.js} +8 -8
- package/dist/{chunk-UFXV64WQ.cjs → chunk-ZLYLHP64.cjs} +9 -9
- package/dist/{chunk-UFXV64WQ.cjs.map → chunk-ZLYLHP64.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +20 -20
- package/dist/components/case-studies/index.js +7 -7
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +8 -8
- package/dist/components/contact/index.js +7 -7
- package/dist/components/docs/index.cjs +11 -11
- package/dist/components/docs/index.js +10 -10
- 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 +101 -101
- package/dist/components/help-center-pages/index.js +21 -21
- package/dist/components/index.cjs +65 -65
- package/dist/components/index.js +21 -21
- package/dist/components/meeting-scheduler/index.cjs +58 -58
- package/dist/components/meeting-scheduler/index.js +8 -8
- package/dist/components/navigation/index.cjs +10 -10
- package/dist/components/navigation/index.js +9 -9
- 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 +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +12 -12
- package/dist/components/tickets/index.js +11 -11
- package/dist/components/tickets/ticket-live-provider.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +7 -7
- package/dist/components/ui/index.js +6 -6
- package/dist/hooks/index.cjs +4 -4
- package/dist/hooks/index.js +3 -3
- package/dist/index.cjs +12 -12
- package/dist/index.js +10 -10
- package/dist/utils/embed-authed-fetch.d.ts +18 -0
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/index.cjs +50 -6
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +50 -6
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/sse-subscription.d.ts +29 -11
- package/dist/utils/sse-subscription.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/tickets/ticket-live-provider.tsx +14 -12
- package/src/utils/embed-authed-fetch.ts +21 -0
- package/src/utils/sse-subscription.ts +107 -20
- package/dist/chunk-2KILTCND.js.map +0 -1
- package/dist/chunk-AIXPQT3G.js.map +0 -1
- package/dist/chunk-TBJWPNHU.js.map +0 -1
- package/dist/chunk-VLWUKFQE.cjs.map +0 -1
- package/dist/chunk-YCKGUQA2.cjs.map +0 -1
- package/dist/chunk-ZBQZJAC7.cjs.map +0 -1
- /package/dist/{chunk-22RSJ57Z.js.map → chunk-2YVTGPM7.js.map} +0 -0
- /package/dist/{chunk-V5NOURSR.js.map → chunk-4HZXOQRM.js.map} +0 -0
- /package/dist/{chunk-XRSJER2B.js.map → chunk-6Z26ZMR4.js.map} +0 -0
- /package/dist/{chunk-HLEXYTOE.js.map → chunk-7EPGJOKN.js.map} +0 -0
- /package/dist/{chunk-DOUG4UEZ.js.map → chunk-7WYWPELF.js.map} +0 -0
- /package/dist/{chunk-DLRE5Y3G.js.map → chunk-C2SVJVEB.js.map} +0 -0
- /package/dist/{chunk-OC3QH5WG.js.map → chunk-CMYDGVTZ.js.map} +0 -0
- /package/dist/{chunk-2PQKZWWP.js.map → chunk-DC3G4XGP.js.map} +0 -0
- /package/dist/{chunk-OQVHGPHA.js.map → chunk-DZMDG7PC.js.map} +0 -0
- /package/dist/{chunk-S5M5SEW2.js.map → chunk-FNMQ2J44.js.map} +0 -0
- /package/dist/{chunk-UAKWSR3U.js.map → chunk-GDZ7IC5B.js.map} +0 -0
- /package/dist/{chunk-Q76YVNCQ.js.map → chunk-HM7ZGABI.js.map} +0 -0
- /package/dist/{chunk-25X43LSD.js.map → chunk-JSG7WZKV.js.map} +0 -0
- /package/dist/{chunk-TSENCZUL.js.map → chunk-MJI5GVT7.js.map} +0 -0
- /package/dist/{chunk-RF2EYX3B.js.map → chunk-SC44YJCF.js.map} +0 -0
- /package/dist/{chunk-42RQ4NIL.js.map → chunk-TSNGVWQO.js.map} +0 -0
- /package/dist/{chunk-2V4KPBNZ.js.map → chunk-XAOAYDGR.js.map} +0 -0
- /package/dist/{chunk-U4OJCJTH.js.map → chunk-XBYF4SGD.js.map} +0 -0
- /package/dist/{chunk-4BUPMSLY.js.map → chunk-Y7ZEVTLC.js.map} +0 -0
- /package/dist/{chunk-Z622ACFD.js.map → chunk-Y7ZQZOE6.js.map} +0 -0
- /package/dist/{chunk-5A3WSWZD.js.map → chunk-Z5FHXCXF.js.map} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import {
|
|
3
3
|
createSseSubscription
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KMW2PXBU.js";
|
|
5
5
|
import {
|
|
6
6
|
useChatIdentity
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-Y7ZEVTLC.js";
|
|
8
8
|
import {
|
|
9
9
|
embedAuthedFetch
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-UVUNBXS6.js";
|
|
11
11
|
import {
|
|
12
12
|
useChatRuntime
|
|
13
13
|
} from "./chunk-3SMLYVM2.js";
|
|
@@ -275,4 +275,4 @@ export {
|
|
|
275
275
|
TICKET_MARK_READ_DEBOUNCE_MS,
|
|
276
276
|
TOAST_COPY
|
|
277
277
|
};
|
|
278
|
-
//# sourceMappingURL=chunk-
|
|
278
|
+
//# sourceMappingURL=chunk-KVDLL4LU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/tickets/ticket-live-provider.tsx","../src/components/tickets/types.ts"],"sourcesContent":["'use client';\n\n/**\n * TicketLiveProvider — the ONE client home for support-ticket realtime.\n *\n * DOMAIN logic only — connection lifecycle is owned entirely by the\n * common SSE client (`createSseSubscription`; its module header lists\n * the policies); this provider consumes its consolidated\n * `onConnectedChange` signal.\n *\n * Owns:\n * - the unread summary — delivered EXCLUSIVELY by the stream\n * (`ticket-summary` frames on connect + debounced after events) and\n * by `ticket-read` responses. The client holds NO summary fetch\n * path: resync-on-(re)connect is served by the SERVER (every\n * connect pushes a fresh summary);\n * - query invalidation on events (list + open drawer refresh live);\n * - `markRead` with local write-through zeroing for 0-latency,\n * reconciled by the response's server-computed summary;\n * - the ticket-contract `no-stream` (204) retry policy.\n *\n * There is NO polling anywhere and NO summary endpoint. Hosts whose\n * stream endpoint 404s or is absent get no unread indication — the\n * indication IS a realtime feature (a 404ing endpoint is re-probed on\n * presence, rate-limited by the client).\n *\n * Invalidation semantics on events — deliberately `invalidateQueries`,\n * NOT `setQueryData` patches: the ticket hooks run `gcTime: 0`, so\n * there is usually no cache entry to patch, and `TicketsCacheSlot`\n * shape rules apply when there is.\n */\n\nimport { useQueryClient } from '@tanstack/react-query';\nimport React from 'react';\nimport { useChatRuntime } from '../../contexts/chat-runtime-context';\nimport { embedAuthedFetch } from '../../utils/embed-authed-fetch';\nimport { createSseSubscription, type SseSubscription, type SseTransportStatus } from '../../utils/sse-subscription';\nimport { useChatIdentity } from '../chat/hooks/use-chat-identity';\nimport type { TicketMessageStreamData, TicketStreamEvent, TicketUnreadSummary } from './types';\n\n// Ticket realtime is its OWN api surface — deliberately NOT under\n// `/api/chat/agent/*` (that prefix is the chat agent's tool surface;\n// ticket streaming/read-receipts are unrelated to chat).\nconst TICKET_STREAM_ENDPOINT = '/api/tickets/stream';\nconst TICKET_READ_ENDPOINT = '/api/tickets/read';\n\nconst EMPTY_SUMMARY: TicketUnreadSummary = { totalUnread: 0, tickets: {}, latestUnreadAt: {} };\n\n/** ISO stamps can carry mixed timezone offsets (server `+00:00` vs an\n * optimistic local `Z`) — lexicographic string comparison mis-orders\n * those, so every stamp comparison goes through parsed epoch ms.\n * Missing/unparseable stamps sort oldest. */\nconst stampMs = (stamp: string | null | undefined): number => {\n if (!stamp) return 0;\n const ms = Date.parse(stamp);\n return Number.isNaN(ms) ? 0 : ms;\n};\n\n/** Trailing debounce for event-burst invalidations (an agent pasting 5\n * messages must not fire 5 list refetches). */\nconst INVALIDATE_DEBOUNCE_MS = 1_500;\n\nexport interface TicketLiveContextValue {\n /** Viewer has a resolved, non-anon chat identity. Surfaces (e.g. the\n * header cell) hide themselves when false — computed here so they\n * don't each spin up their own identity resolution. */\n authed: boolean;\n /** Realtime-subscribed (server-confirmed), not merely HTTP-open. */\n connected: boolean;\n /** Unread total across tickets — drives the header count badge. */\n unreadTotal: number;\n /** Per-ticket unread counts (missing key = 0) — drives row dots.\n * The currently-open ticket is masked to 0. */\n unreadByTicket: Record<string, number>;\n /** The ticket with the NEWEST unread reply (open ticket excluded) —\n * the header cell routes to it via the SSOT deep link\n * (`buildTicketOpenHref`). Null when nothing is unread. */\n nextUnreadTicketId: string | null;\n /** Mark a ticket read: POSTs the receipt, zeroes the ticket locally\n * (0-latency), then applies the response's server-computed summary. */\n markRead: (ticketExternalId: string) => Promise<void>;\n /** Drawer open/close registration — suppresses unread bumps for the\n * open ticket and masks it in the derived map. */\n setOpenTicketId: (ticketExternalId: string | null) => void;\n /** Hint that the viewer just created a ticket — reconnects the stream\n * (fresh ownership set + fresh server-pushed summary). */\n notifyTicketCreated: () => void;\n}\n\nconst TicketLiveContext = React.createContext<TicketLiveContextValue | null>(null);\n\nexport function useOptionalTicketLive(): TicketLiveContextValue | null {\n return React.useContext(TicketLiveContext);\n}\n\nexport function useTicketLive(): TicketLiveContextValue {\n const ctx = React.useContext(TicketLiveContext);\n if (!ctx) {\n throw new Error('useTicketLive must be used within <TicketLiveProvider>');\n }\n return ctx;\n}\n\nfunction isTicketMessageData(value: unknown): value is TicketMessageStreamData {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as TicketMessageStreamData).ticket_external_id === 'string'\n );\n}\n\nfunction isSummaryData(value: unknown): value is TicketUnreadSummary {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as TicketUnreadSummary).totalUnread === 'number' &&\n typeof (value as TicketUnreadSummary).tickets === 'object'\n );\n}\n\nexport interface TicketLiveProviderProps {\n children: React.ReactNode;\n /**\n * Whether ticket realtime is available to this host at all (feature flag,\n * session, entitlement). `false` provides NO context — `useOptionalTicketLive()`\n * reads `null`, `useTicketLive()` throws, and no identity or stream request is\n * made: byte-for-byte what a host got by not mounting this at all.\n *\n * It exists so a host never has to mount this CONDITIONALLY. A host that writes\n * `enabled ? <TicketLiveProvider>{children}</TicketLiveProvider> : <>{children}</>`\n * changes the element TYPE at that position the moment its flag answers, and React\n * tears down and remounts everything below. In openframe-frontend that wrapper sat\n * around the whole `AppLayout`, so every cold load remounted the app shell and the\n * page under it — visibly, as a chat drawer opened by a deep link replaying its\n * open animation. Mount it always and pass the answer here instead.\n *\n * One consequence of mounting it always: a `ChatRuntime` must be above it even\n * while disabled. The identity resolver reads one unconditionally (Rules of\n * Hooks) and throws without it; `enabled` gates the request, not the read.\n */\n enabled?: boolean;\n}\n\nexport function TicketLiveProvider({ children, enabled = true }: TicketLiveProviderProps) {\n const runtime = useChatRuntime();\n const identity = useChatIdentity(enabled);\n const queryClient = useQueryClient();\n\n const streamUrl = runtime?.endpoints.ticketStreamUrl ?? TICKET_STREAM_ENDPOINT;\n const readUrl = runtime?.endpoints.ticketReadUrl ?? TICKET_READ_ENDPOINT;\n\n // Gates the stream below (via `streamEnabled`) as well as every surface that\n // reads `authed` — one predicate, so a disabled provider is also a silent one.\n const authed = enabled && identity.authTier !== 'anon' && !!identity.user?.email;\n\n const [connected, setConnected] = React.useState(false);\n // The SINGLE unread source — written ONLY from server-computed\n // summaries (stream frames + markRead responses) plus the two\n // 0-latency optimistic writes (event bump, markRead zero), both\n // reconciled by the next server summary.\n const [summary, setSummary] = React.useState<TicketUnreadSummary>(EMPTY_SUMMARY);\n // State + ref for the open drawer: state drives the derived map,\n // the ref lets stream callbacks read it without re-subscribing.\n const [openTicketIdState, setOpenTicketIdState] = React.useState<string | null>(null);\n const openTicketIdRef = React.useRef<string | null>(null);\n\n // ---- Debounced (trailing) invalidation of ticket queries on events ----\n const pendingTicketIdsRef = React.useRef<Set<string>>(new Set());\n const invalidateTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const flushInvalidations = React.useCallback(() => {\n invalidateTimerRef.current = null;\n const ids = Array.from(pendingTicketIdsRef.current);\n pendingTicketIdsRef.current.clear();\n void queryClient.invalidateQueries({ queryKey: ['tickets'] });\n for (const id of ids) {\n void queryClient.invalidateQueries({ queryKey: ['ticket-engagements', id] });\n }\n }, [queryClient]);\n const scheduleInvalidation = React.useCallback(\n (ticketId?: string) => {\n if (ticketId) pendingTicketIdsRef.current.add(ticketId);\n if (invalidateTimerRef.current) return;\n invalidateTimerRef.current = setTimeout(flushInvalidations, INVALIDATE_DEBOUNCE_MS);\n },\n [flushInvalidations],\n );\n\n // ---- Optimistic unread writes (reconciled by server summaries) ----\n const bumpUnread = React.useCallback((ticketId: string, createdAt?: string | null) => {\n setSummary(prev => {\n const stamp = createdAt ?? new Date().toISOString();\n const existing = prev.latestUnreadAt[ticketId];\n return {\n totalUnread: prev.totalUnread + 1,\n tickets: { ...prev.tickets, [ticketId]: (prev.tickets[ticketId] ?? 0) + 1 },\n latestUnreadAt: {\n ...prev.latestUnreadAt,\n [ticketId]: existing && stampMs(existing) > stampMs(stamp) ? existing : stamp,\n },\n };\n });\n }, []);\n\n const zeroUnread = React.useCallback((ticketId: string) => {\n setSummary(prev => {\n const current = prev.tickets[ticketId] ?? 0;\n if (current === 0) return prev;\n const tickets = { ...prev.tickets };\n delete tickets[ticketId];\n const latestUnreadAt = { ...prev.latestUnreadAt };\n delete latestUnreadAt[ticketId];\n return { totalUnread: Math.max(0, prev.totalUnread - current), tickets, latestUnreadAt };\n });\n }, []);\n\n // ---- Stream lifecycle (owned by the SSE CLIENT — the provider only\n // consumes its consolidated signals) ----\n const subscriptionRef = React.useRef<SseSubscription | null>(null);\n const noStreamRef = React.useRef(false);\n\n // Stable handler refs so the subscription effect doesn't churn.\n const handlersRef = React.useRef<{\n onEvent: (eventName: string, data: unknown) => void;\n onStatus: (status: SseTransportStatus) => void;\n onConnected: (connected: boolean) => void;\n }>({ onEvent: () => {}, onStatus: () => {}, onConnected: () => {} });\n\n const handleConnected = (isConnected: boolean) => {\n setConnected(isConnected);\n if (isConnected) {\n // Event-gap healing on every (re)connect: the SERVER pushes a\n // fresh summary; the open drawer refetches through the ACL'd path.\n const openId = openTicketIdRef.current;\n if (openId) {\n void queryClient.invalidateQueries({ queryKey: ['ticket-engagements', openId] });\n }\n }\n };\n\n const handleEvent = (eventName: string, data: unknown) => {\n if (eventName === 'ticket-summary') {\n const frame = data as TicketStreamEvent | null;\n if (isSummaryData(frame?.data)) {\n setSummary({\n totalUnread: frame.data.totalUnread,\n tickets: frame.data.tickets ?? {},\n latestUnreadAt: frame.data.latestUnreadAt ?? {},\n });\n }\n return;\n }\n\n if (eventName === 'ticket-resync') {\n // Ownership gained / degraded payload — a fresh ticket-summary\n // frame follows from the server; refresh the query surfaces.\n scheduleInvalidation();\n return;\n }\n\n if (eventName === 'ticket-status') {\n // Meaningful ticket-row change (close/reopen/pipeline move). The\n // server pushes an updated summary right after (closures count as\n // unread server-side); here we just refresh the query surfaces so\n // the status badge flips live.\n const frame = data as TicketStreamEvent | null;\n const ticketId = (frame?.data as { ticket_external_id?: string } | undefined)?.ticket_external_id;\n scheduleInvalidation(ticketId);\n return;\n }\n\n if (eventName === 'ticket-message') {\n const frame = data as TicketStreamEvent | null;\n const payload = frame?.data;\n if (!isTicketMessageData(payload)) {\n scheduleInvalidation();\n return;\n }\n // Always invalidate (own replies from another tab render live);\n // bump unread ONLY on the server-stamped predicate, and never for\n // the ticket whose drawer is open. The server's debounced summary\n // push reconciles moments later.\n scheduleInvalidation(payload.ticket_external_id);\n if (payload.countsAsUnread && payload.ticket_external_id !== openTicketIdRef.current) {\n bumpUnread(payload.ticket_external_id, payload.hubspot_created_at);\n }\n }\n };\n\n const handleStatus = (status: SseTransportStatus) => {\n // The client owns connection lifecycle. The only transport status\n // with DOMAIN meaning is the ticket contract's `no-stream` (204 =\n // zero owned tickets): remember it so a domain signal (own\n // create_ticket, window focus) can retry.\n if (status === 'open') noStreamRef.current = false;\n if (status === 'no-stream') noStreamRef.current = true;\n };\n\n // Publish the three handlers into the stable ref. In an unconditional effect\n // rather than in the render body: every caller is an SSE transport callback,\n // which can only fire after a commit, and a render attempt React discards\n // must not be able to leave a handler behind that closes over state the user\n // never saw. Declared BEFORE the subscription effect so it is filled first\n // in the same flush.\n React.useEffect(() => {\n handlersRef.current.onConnected = handleConnected;\n handlersRef.current.onEvent = handleEvent;\n handlersRef.current.onStatus = handleStatus;\n });\n\n // Mount/unmount of the subscription — gated on auth + a usable URL.\n const streamEnabled = authed && !!streamUrl;\n React.useEffect(() => {\n if (!streamEnabled) return undefined;\n if (typeof window === 'undefined') return undefined;\n\n subscriptionRef.current = createSseSubscription({\n url: streamUrl,\n onEvent: (name, data) => handlersRef.current.onEvent(name, data),\n onStatusChange: status => handlersRef.current.onStatus(status),\n onConnectedChange: isConnected => handlersRef.current.onConnected(isConnected),\n // Long-lived per-user stream: suspend while hidden (scale relief),\n // resume + reconnect on visible/online — all inside the client.\n pauseWhenHidden: true,\n });\n\n const retryIfIdle = () => {\n // After a 204 (no-stream), a focus signal is the retry trigger. Only\n // 204 — the client's contract leaves that retry to the caller; it\n // retries terminal / 401 states on its own.\n if (noStreamRef.current) {\n noStreamRef.current = false;\n subscriptionRef.current?.reconnectNow();\n }\n };\n window.addEventListener('focus', retryIfIdle);\n\n return () => {\n window.removeEventListener('focus', retryIfIdle);\n if (invalidateTimerRef.current) {\n clearTimeout(invalidateTimerRef.current);\n invalidateTimerRef.current = null;\n }\n subscriptionRef.current?.close();\n subscriptionRef.current = null;\n setConnected(false);\n setSummary(EMPTY_SUMMARY);\n };\n // handlersRef is stable; streamUrl/auth changes remount the stream.\n }, [streamEnabled, streamUrl]);\n\n // ---- Public API ----\n const markRead = React.useCallback(\n async (ticketExternalId: string) => {\n try {\n const response = await embedAuthedFetch(readUrl, {\n method: 'POST',\n body: JSON.stringify({ ticket_id: ticketExternalId }),\n });\n if (!response.ok) return;\n // 0-latency local zero, then reconcile with the response's\n // server-computed summary (THE truth — includes closure\n // accounting and other tickets).\n zeroUnread(ticketExternalId);\n const body = (await response.json().catch(() => null)) as { summary?: TicketUnreadSummary } | null;\n if (isSummaryData(body?.summary)) {\n setSummary({\n totalUnread: body.summary.totalUnread,\n tickets: body.summary.tickets ?? {},\n latestUnreadAt: body.summary.latestUnreadAt ?? {},\n });\n }\n } catch {\n // Non-fatal — the receipt lands on the next markRead; unread\n // state reconciles via the next server summary push.\n }\n },\n [readUrl, zeroUnread],\n );\n\n const setOpenTicketId = React.useCallback((ticketExternalId: string | null) => {\n openTicketIdRef.current = ticketExternalId;\n setOpenTicketIdState(ticketExternalId);\n }, []);\n\n const notifyTicketCreated = React.useCallback(() => {\n // Fresh connect → fresh ownership set + fresh server summary\n // (covers both the no-stream 204 wait state and an already-live\n // stream whose set predates the new ticket).\n noStreamRef.current = false;\n subscriptionRef.current?.reconnectNow();\n }, []);\n\n // Derived unread map — open ticket masked to 0 so a server summary\n // landing inside the markRead debounce window can't transiently\n // re-badge it.\n const value = React.useMemo<TicketLiveContextValue>(() => {\n let unreadByTicket = summary.tickets;\n let unreadTotal = summary.totalUnread;\n if (openTicketIdState && unreadByTicket[openTicketIdState]) {\n const masked = unreadByTicket[openTicketIdState];\n unreadByTicket = { ...unreadByTicket };\n delete unreadByTicket[openTicketIdState];\n unreadTotal = Math.max(0, unreadTotal - masked);\n }\n // Ticket with the NEWEST unread reply (compared as parsed epoch ms\n // via `stampMs` — mixed timezone offsets break string ordering) —\n // header cell routes there. Falls back to any unread key if a\n // timestamp is missing.\n let nextUnreadTicketId: string | null = null;\n let nextStampMs = 0;\n for (const id of Object.keys(unreadByTicket)) {\n const ms = stampMs(summary.latestUnreadAt[id]);\n if (nextUnreadTicketId === null || ms > nextStampMs) {\n nextUnreadTicketId = id;\n nextStampMs = ms;\n }\n }\n return {\n authed,\n connected,\n unreadTotal,\n unreadByTicket,\n nextUnreadTicketId,\n markRead,\n setOpenTicketId,\n notifyTicketCreated,\n };\n }, [summary, authed, connected, openTicketIdState, markRead, setOpenTicketId, notifyTicketCreated]);\n\n return <TicketLiveContext.Provider value={enabled ? value : null}>{children}</TicketLiveContext.Provider>;\n}\n","/**\n * Wire shape of a row returned by `POST /api/chat/agent/find-ticket`.\n * Mirrors the executor's projection at `lib/data/hubspot-tools.ts`\n * (`FIND_TICKET_SELECT` / `FindTicketResult`).\n *\n * Cross-repo duplication is INTENTIONAL: this lib ships independently\n * of the hub, so we can't import `FindTicketResult` from\n * `hubspot-tools.ts` directly. If the server adds a column to\n * `FIND_TICKET_SELECT`, also add it here. The smoke test in §F of the\n * plan covers the happy path; a wire-contract test belongs in the hub.\n *\n * `find-ticket` returns `customer_emails: string[]` (jsonb array), NOT\n * a single `customer_email`. The list is server-self-scoped to the\n * caller's session email; the array is exposed for admin/staff\n * surfaces, which the ticket center doesn't render.\n */\nexport interface TicketData {\n id: string;\n /** HubSpot ticket id (display number, e.g. \"1234\"). */\n external_id: string;\n subject: string | null;\n /** Short (≤400 char) HTML-stripped preview of the ticket body —\n * used for the list-card subtitle when needed. */\n preview: string | null;\n /** Longer (≤4k char) sanitized body. INCLUDES every appended\n * `content_addendum` comment because the `update_ticket` executor\n * reads + re-writes the `content` property server-side with a\n * `---` separator. Render this in the drawer's description block\n * and the user sees both the original message + every comment they\n * (or staff) added. */\n body: string | null;\n /** Canonical OPEN | CLOSED (HubSpot pipeline derived). */\n status: string | null;\n /** Human label like \"New\" / \"Working on it\" / \"Waiting on contact\" /\n * \"Closed\". Drives the badge text; canonical status drives color. */\n pipeline_stage_label: string | null;\n clickup_task_id: string | null;\n /** Snapshot of the linked ClickUp delivery task — populated server-side\n * via the `clickup_tasks` mirror when `clickup_task_id` is set. Drives\n * the \"Linked delivery\" card surface on the ticket drawer (status\n * badge + ClickUp deep link). `null` when no link OR the ClickUp row\n * was deleted / not yet synced. */\n clickup: TicketClickupSummary | null;\n priority: string | null;\n customer_emails: string[];\n customer_company: string | null;\n /** HubSpot contact's display name. Drives the customer attribution\n * on the drawer when the viewer is NOT the customer themselves\n * (admin browsing / multi-contact second viewer). Conversations\n * API messages don't carry per-message sender info on Custom\n * Channels, so this is the only reliable source for \"what's the\n * customer's name.\" */\n customer_name: string | null;\n /** HubSpot owner id of the agent assigned to this ticket. Carried as\n * raw id for debugging; rendering goes through `assignedOwner`. Null\n * when unassigned. */\n assigned_to: string | null;\n /** Resolved assigned-owner profile — name + email + avatar. Populated\n * server-side via `attachOwnerProfiles` which joins through the\n * `hubspot_owners` mirror to `profiles` by email. Drives the\n * \"Assigned to\" attribution in the drawer header. Null when\n * unassigned OR the owner couldn't be resolved (rare — only when\n * the agent was deleted from HubSpot between the ticket update and\n * the next owners reconcile). */\n assignedOwner: TicketAssignedOwner | null;\n hubspot_updated_at: string;\n}\n\n/** Resolved profile of a ticket's assigned agent — surfaced in the\n * drawer header. Subset of the server's `MirroredOwnerProfile`\n * trimmed to just the rendering fields. */\nexport interface TicketAssignedOwner {\n name: string | null;\n email: string | null;\n avatarUrl: string | null;\n}\n\n/** Compact projection of a linked ClickUp task — matches the server's\n * `ClickupSummary` and aligns with `DeliveryItem` so the linked-card\n * on a ticket can render through the same `DeliveryRow` primitive used\n * on `/bug-fixes-and-enhancements`. */\nexport interface TicketClickupSummary {\n /** ClickUp task external_id (e.g. \"86ad4e022\"). Used as the\n * `?focus=<id>` URL param to scroll the public delivery page to\n * this row. */\n external_id: string;\n title: string | null;\n description: string | null;\n /** ClickUp status name — e.g. \"complete\" / \"working\" / \"design approved\"\n * / \"waiting for release\". Used as the badge label. */\n status: string | null;\n /** ClickUp's per-status hex color (e.g. \"#008844\"). Forwarded to the\n * badge so colors match the ClickUp board exactly. */\n status_color: string | null;\n /** Bucket — `'backlog' | 'working' | 'complete' | 'unknown'`. Used as\n * a fallback when status_color is missing. */\n status_category: string | null;\n /** ClickUp custom item label (`'Bug'` / `'Request'`) — drives the\n * type badge (\"BUG-FIX\" / \"ENHANCEMENT\"). */\n task_type: string | null;\n custom_item_id: number | null;\n /** Every ClickUp list the task is associated with. UI joins with \", \". */\n list_names: string[];\n /** Unix-ms timestamps so the row's \"ACTIVE X ago\" subtitle uses the\n * shared `getRelativeTime()` helper. */\n date_opened: number | null;\n date_updated: number | null;\n date_closed: number | null;\n /** Direct https://app.clickup.com/t/<id> deep link. Kept on the wire\n * for admin surfaces; the customer-facing linked card navigates\n * internally instead. */\n clickup_url: string | null;\n /** Composed server-side via the SAME `buildDevSectionUrl` helper the\n * chat-inline delivery card uses. Carries `?search=<id>` so the\n * delivery list filters to that single task on landing. */\n delivery_href: string;\n /** Target platform name for the host's `useNavLink` to decide\n * same-tab vs new-tab on cross-platform links. */\n delivery_target_platform: string;\n /** Release version label set by the delivery team, e.g. \"0.9\" / \"1.0\".\n * Shown beside the status when present. */\n target_version: string | null;\n}\n\n/**\n * Optimistic placeholder a `submitTicket` call prepends to the list\n * BEFORE the server roundtrip resolves. Drawer is hidden until the\n * real id arrives. The wrapper destructures `_optimistic` before\n * forwarding to `<ChatTicketItem>` so the DOM doesn't see an unknown\n * prop.\n */\nexport interface OptimisticTicket extends TicketData {\n _optimistic: true;\n}\n\nexport type AnyTicket = TicketData | OptimisticTicket;\n\nexport function isOptimistic(t: AnyTicket): t is OptimisticTicket {\n return (t as OptimisticTicket)._optimistic === true;\n}\n\n/**\n * Shape of a single `['tickets', …]` TanStack-Query cache slot.\n * Mirrors `FindTicketResponse` in `hooks/use-tickets-list.ts` — kept\n * here because the cache-mutation call sites in `useTicketActions` and\n * `<HelpCenterList>` would otherwise have to redeclare the shape inline.\n *\n * A 2026-05-29 prod regression (`t.map is not a function` on\n * close/reopen) was caused by assuming the cache held a bare\n * `TicketData[]` instead of this wrapper — every helper that calls\n * `queryClient.setQueriesData` / `getQueriesData` on `['tickets']`\n * MUST type the value through this shape and project / reassemble\n * `tickets` explicitly.\n */\nexport interface TicketsCacheSlot {\n tickets?: TicketData[];\n count?: number;\n totalCount?: number;\n page?: number;\n pageSize?: number;\n totalPages?: number;\n scope?: 'self' | 'all';\n}\n\n/**\n * Stable server-side error codes the ticket-action helpers route\n * through `mapTicketActionError`. Anything else is treated as a generic\n * server error.\n *\n * Reply-specific codes (`HUBSPOT_5XX` / `HUBSPOT_400_VALIDATION` /\n * `HUBSPOT_404_THREAD` / `HUBSPOT_REPLY_UNKNOWN`) drive the drawer\n * banner that appears above the composer when a customer reply fails.\n * Distinct from `HUBSPOT_DISCONNECTED` (whole-system) and\n * `TICKET_NOT_FOUND` (terminal-row) — the reply codes are per-attempt\n * + retryable except for `HUBSPOT_404_THREAD`.\n */\nexport type TicketActionErrorCode =\n | 'PROPOSAL_NOT_CLAIMABLE'\n | 'TICKET_NOT_FOUND'\n | 'TICKET_OWNERSHIP_DENIED'\n | 'HUBSPOT_DISCONNECTED'\n | 'RATE_LIMITED'\n | 'INVALID_TOOL_ARGS'\n | 'HUBSPOT_5XX'\n | 'HUBSPOT_400_VALIDATION'\n | 'HUBSPOT_404_THREAD'\n | 'HUBSPOT_REPLY_UNKNOWN'\n | 'UNKNOWN';\n\nexport interface MappedTicketActionError {\n code: TicketActionErrorCode;\n /** Human-readable copy safe to show in a toast. */\n message: string;\n /** When true, the form should disable submit + show the\n * support-down banner. Set only for HUBSPOT_DISCONNECTED. */\n supportSystemDown: boolean;\n /** When true, the helper should remove the affected row optimistically\n * (TICKET_NOT_FOUND). */\n removeRowFromCache: boolean;\n /** Retry hint surfaced from a 429 response. Caller decides whether\n * to mention it in the toast. */\n retryAfterSeconds?: number;\n}\n\n/**\n * Defensive client-side cap on ticket text (initial content + comment\n * addendums). HubSpot Note engagements accept more, but a 100KB paste\n * should fail fast at the UI rather than burning a server round-trip.\n * Both the open-ticket form and the per-row comment textarea import\n * this so a future server-side hardening only touches one place.\n */\nexport const TICKET_TEXT_MAX_CHARS = 5000;\n\n/**\n * Wire contract of the ticket live stream (`GET /api/chat/agent/\n * ticket-stream`, SSE). Producer (the hub route's `mapEvent`) imports\n * THIS type — same producer/consumer SSOT pattern as\n * `ConversationEngagementWire = TicketEngagement`. A schema drift here\n * would be a silent serialization bug; keep one named contract.\n *\n * Frames carry METADATA ONLY — never message bodies. Content is always\n * refetched through the ACL'd `list-engagements` path (one read path).\n *\n * Event names (`eventType`):\n * - `ticket-message` — a message row landed in the mirror for one of\n * the viewer's tickets. `data` carries the metadata.\n * - `ticket-status` — a MEANINGFUL ticket-row change (status /\n * pipeline stage / closure) landed in the mirror. Server-side\n * old-vs-new diffing drops no-op writes (sync stamps). Clients\n * invalidate + refetch the summary (closures count as unread\n * server-side via `closed_at` vs the read receipt).\n * - `ticket-summary` — the server-computed `TicketUnreadSummary`.\n * THE stream is the summary webservice: pushed on connect (server-\n * side resync-on-reconnect) and re-pushed (debounced) after every\n * delivered event. Clients hold NO summary fetch path.\n * - `ticket-resync` — degraded frame: something happened that the\n * server couldn't fully describe (truncated Realtime payload, or\n * the connection's ownership set just gained tickets). A fresh\n * `ticket-summary` follows it; clients invalidate queries.\n *\n * The server also emits `status` frames (`subscribed` / `retrying` /\n * `reconnect_failed`) from the shared SSE utility — clients derive\n * `connected` from those, NOT from the HTTP stream being open.\n */\nexport type TicketStreamEventType = 'ticket-message' | 'ticket-status' | 'ticket-summary' | 'ticket-resync';\n\n/** Metadata payload of a `ticket-message` frame. */\nexport type TicketMessageStreamData = {\n ticket_external_id: string;\n /** Mirror row's HubSpot message id. */\n message_id: string;\n /** Server-stamped via the same direction predicate the wire mapper\n * uses. Clients never re-derive author classification. */\n authorRole: 'customer' | 'support';\n /** Server-stamped via the visibility+direction unread predicate\n * (`countsAsUnread` in the hub's conversations DAL). ONLY frames\n * with `true` may bump client-side unread state. */\n countsAsUnread: boolean;\n hubspot_created_at: string | null;\n};\n\n/** Metadata payload of a `ticket-status` frame. */\nexport type TicketStatusStreamData = {\n ticket_external_id: string;\n /** Canonical OPEN | CLOSED after the change. */\n status: string | null;\n pipeline_stage_label: string | null;\n closed_at: string | null;\n};\n\nexport interface TicketStreamEvent {\n eventType: TicketStreamEventType;\n /** Human-readable label (SSE envelope compat) — clients ignore it. */\n message: string;\n /** `ticket-message` → TicketMessageStreamData; `ticket-status` →\n * TicketStatusStreamData; `ticket-summary` → TicketUnreadSummary;\n * absent on `ticket-resync`. */\n data?: TicketMessageStreamData | TicketStatusStreamData | TicketUnreadSummary;\n}\n\n/**\n * The SINGLE unread source, delivered EXCLUSIVELY over the stream\n * (`ticket-summary` frames on connect + after events) and in\n * `ticket-read` responses. There is no summary fetch endpoint. Header\n * badge total AND per-row dots both read this via the\n * `TicketLiveProvider` map; missing keys mean 0.\n */\nexport type TicketUnreadSummary = {\n totalUnread: number;\n tickets: Record<string, number>;\n /** ISO timestamp of the NEWEST unread message per ticket (same keys as\n * `tickets`). Drives \"route to the most recent update\" on the header\n * cell: the ticket with the max value here is `nextUnreadTicketId`. */\n latestUnreadAt: Record<string, string>;\n};\n\n/**\n * Trailing debounce for `markRead` while the drawer is open and new\n * messages stream in — flushed on drawer close so a fast open/close\n * still persists the receipt. Named beside its sibling cadences so\n * every timing knob lives in one file.\n */\nexport const TICKET_MARK_READ_DEBOUNCE_MS = 2000;\n\n/**\n * Ticket deep-link SSOT — DEFINED in the server-safe param-keys module\n * (`utils/dev-sections/dev-section-param-keys.ts`, beside its sibling\n * `devSectionAnchorId`) because producers span both worlds: the hub's\n * SERVER-side chat-ref builder and the client header cell. Re-exported\n * here so ticket-surface consumers keep one import home.\n */\nexport { TICKET_OPEN_PARAM, buildTicketOpenHref } from '../../utils/dev-sections/dev-section-param-keys';\n\n/**\n * Centralized toast copy. Keep all wording here so QA / localization\n * can find every user-visible string in one file.\n */\nexport const TOAST_COPY = {\n open_success: { title: 'Ticket opened', description: 'We received your message and will follow up shortly.' },\n open_mirror_pending: { title: 'Ticket opened', description: 'Syncing — your ticket will appear momentarily.' },\n close_success: { title: 'Ticket closed' },\n reopen_success: { title: 'Ticket reopened' },\n comment_success: { title: 'Comment added' },\n attach_success: { title: 'Files attached' },\n // Failure variants are constructed dynamically from MappedTicketActionError.\n} as const;\n"],"mappings":"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","names":[]}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkCWXRZVDIcjs = require('./chunk-CWXRZVDI.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
var
|
|
7
|
+
var _chunkMIZU2OTOcjs = require('./chunk-MIZU2OTO.cjs');
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ var _chunkJGVJRUGBcjs = require('./chunk-JGVJRUGB.cjs');
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
var
|
|
15
|
+
var _chunkGZRU2A4Lcjs = require('./chunk-GZRU2A4L.cjs');
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
var _chunkQRA2JXE4cjs = require('./chunk-QRA2JXE4.cjs');
|
|
@@ -21,7 +21,7 @@ var _chunkQRA2JXE4cjs = require('./chunk-QRA2JXE4.cjs');
|
|
|
21
21
|
var _chunkSADRZ4YIcjs = require('./chunk-SADRZ4YI.cjs');
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
var
|
|
24
|
+
var _chunk53O6PQ3Fcjs = require('./chunk-53O6PQ3F.cjs');
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
var _chunkU2YLMFJKcjs = require('./chunk-U2YLMFJK.cjs');
|
|
@@ -39,7 +39,7 @@ var _jsxruntime = require('react/jsx-runtime');
|
|
|
39
39
|
function DocSearchResultRow({ result, isHighlighted }) {
|
|
40
40
|
const docType = _optionalChain([result, 'access', _ => _.metadata, 'optionalAccess', _2 => _2.documentType]) || void 0;
|
|
41
41
|
const sourceRepo = _optionalChain([result, 'access', _3 => _3.metadata, 'optionalAccess', _4 => _4.sourceRepo]) || void 0;
|
|
42
|
-
const { Icon: SourceIcon, label: iconLabel } =
|
|
42
|
+
const { Icon: SourceIcon, label: iconLabel } = _chunkGZRU2A4Lcjs.resolveSourceIcon.call(void 0, {
|
|
43
43
|
sourceRepo,
|
|
44
44
|
documentType: docType
|
|
45
45
|
});
|
|
@@ -69,7 +69,7 @@ function DocSearchBar({
|
|
|
69
69
|
renderResult
|
|
70
70
|
}) {
|
|
71
71
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
72
|
-
|
|
72
|
+
_chunkCWXRZVDIcjs.SearchInput,
|
|
73
73
|
{
|
|
74
74
|
placeholder,
|
|
75
75
|
value: query,
|
|
@@ -119,7 +119,7 @@ function mapDocSearchResults(docs) {
|
|
|
119
119
|
results.push({
|
|
120
120
|
id: `group-${entry.repo}`,
|
|
121
121
|
// Same table label and count as the chat's grouped chip: one owner each.
|
|
122
|
-
title: `${
|
|
122
|
+
title: `${_chunkGZRU2A4Lcjs.getSourceLabel.call(void 0, entry.repo)} ${_chunkGZRU2A4Lcjs.recordCountLabel.call(void 0, rows.length)}`,
|
|
123
123
|
path: rows[0].path,
|
|
124
124
|
type: "file",
|
|
125
125
|
metadata: {
|
|
@@ -182,7 +182,7 @@ function resolveSearchResultAction(result, source, runtimeMode, composeContentUr
|
|
|
182
182
|
if (composed && !isAbsoluteHref(href)) {
|
|
183
183
|
return { kind: "navigate-same-tab", href };
|
|
184
184
|
}
|
|
185
|
-
const isNewTab =
|
|
185
|
+
const isNewTab = _chunk53O6PQ3Fcjs.decideNewTab.call(void 0, {
|
|
186
186
|
href,
|
|
187
187
|
targetPlatform: composed ? composed.targetPlatform : targetPlatform,
|
|
188
188
|
surface: "useUnifiedNav",
|
|
@@ -276,7 +276,7 @@ function useDocSearch(config) {
|
|
|
276
276
|
limit: "10"
|
|
277
277
|
});
|
|
278
278
|
if (tableIdsKey) params.set("tableIds", tableIdsKey);
|
|
279
|
-
const response = await
|
|
279
|
+
const response = await _chunkMIZU2OTOcjs.contentFetch.call(void 0, `${resolvedSearchEndpoint}?${params.toString()}`);
|
|
280
280
|
if (!response.ok) {
|
|
281
281
|
throw new Error(`Search request failed: ${response.status}`);
|
|
282
282
|
}
|
|
@@ -311,27 +311,27 @@ function useDocSearch(config) {
|
|
|
311
311
|
case "navigate-same-tab": {
|
|
312
312
|
if (wantsNewTab) {
|
|
313
313
|
setKeepOpen(true);
|
|
314
|
-
window.open(action.href, "_blank",
|
|
314
|
+
window.open(action.href, "_blank", _chunkGZRU2A4Lcjs.NEW_TAB_FEATURES);
|
|
315
315
|
return;
|
|
316
316
|
}
|
|
317
317
|
setKeepOpen(false);
|
|
318
318
|
const path = baseRoute && action.href.startsWith(`${baseRoute}/`) ? action.href.slice(baseRoute.length + 1) : null;
|
|
319
319
|
if (path && _optionalChain([onInPageSwap, 'optionalCall', _16 => _16(path)])) return;
|
|
320
|
-
router.push(
|
|
320
|
+
router.push(_chunkGZRU2A4Lcjs.stripSameOriginToPath.call(void 0, action.href));
|
|
321
321
|
return;
|
|
322
322
|
}
|
|
323
323
|
case "navigate-new-tab":
|
|
324
324
|
setKeepOpen(true);
|
|
325
325
|
if (_optionalChain([runtime, 'optionalAccess', _17 => _17.navigation, 'access', _18 => _18.mode]) === "embed") {
|
|
326
326
|
const targetPlatform = _nullishCoalesce(_optionalChain([result, 'access', _19 => _19.metadata, 'optionalAccess', _20 => _20.targetPlatform]), () => ( null));
|
|
327
|
-
|
|
327
|
+
_chunkGZRU2A4Lcjs.resolveExternalNavigation.call(void 0, {
|
|
328
328
|
href: action.href,
|
|
329
329
|
targetPlatform,
|
|
330
330
|
runtime
|
|
331
331
|
}).open();
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
|
-
window.open(action.href, "_blank",
|
|
334
|
+
window.open(action.href, "_blank", _chunkGZRU2A4Lcjs.NEW_TAB_FEATURES);
|
|
335
335
|
return;
|
|
336
336
|
case "ask-ai":
|
|
337
337
|
setKeepOpen(false);
|
|
@@ -370,4 +370,4 @@ function useDocSearch(config) {
|
|
|
370
370
|
|
|
371
371
|
|
|
372
372
|
exports.formatRelativePath = formatRelativePath; exports.DocSearchResultRow = DocSearchResultRow; exports.DocSearchBar = DocSearchBar; exports.mapDocSearchResults = mapDocSearchResults; exports.resolveSearchResultAction = resolveSearchResultAction; exports.useDocSearch = useDocSearch;
|
|
373
|
-
//# sourceMappingURL=chunk-
|
|
373
|
+
//# sourceMappingURL=chunk-LCENRN2N.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-54X75QTC.cjs","../src/components/shared/doc-search/format-relative-path.ts","../src/components/shared/doc-search/doc-search-result-row.tsx","../src/components/shared/doc-search/doc-search-bar.tsx","../src/components/shared/doc-search/map-doc-search-results.ts","../src/components/shared/doc-search/resolve-search-result-action.ts","../src/components/shared/doc-search/use-doc-search.ts"],"names":["jsx"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-54X75QTC.cjs","sourcesContent":[null,"/**\n * Format a full document path as a breadcrumb trail.\n * Shows parent folders only (excludes the last segment / filename).\n *\n * @example\n * formatRelativePath('openframe-oss-tenant/architecture/api-controllers.md')\n * // → 'Openframe oss tenant / Architecture'\n */\nexport function formatRelativePath(fullPath: string): string {\n if (!fullPath) return '';\n const segments = fullPath.replace(/\\.md$/, '').split('/');\n // Show only parent path (exclude the filename itself since the title already shows it)\n const parentSegments = segments.length > 1 ? segments.slice(0, -1) : segments;\n return parentSegments.map(seg => seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, ' ')).join(' / ');\n}\n","'use client';\n\n/**\n * Single row in the `<SearchInput>` dropdown — the standard layout\n * used by every doc-search-backed surface (company-hub data-room\n * search bar, onboarding-guide catalog search, …). Single source of\n * truth for the row appearance so search dropdowns are visually\n * identical everywhere.\n *\n * Resolves the source icon via the same `resolveSourceIcon()`\n * registry the inline chat-card refs use, so a row pointing at e.g.\n * an onboarding-guide surfaces the SAME `<GraduationCap>` glyph the\n * chat card surfaces — no cross-surface drift.\n */\n\nimport { resolveSourceIcon } from '../../chat/utils/source-row-cta';\nimport { formatRelativePath } from './format-relative-path';\n\n/**\n * Minimal result shape this row renders. Compatible with any\n * doc-search hook whose result type exposes `{ title?, path?,\n * metadata? }`. The two hub consumers (onboarding-guide catalog,\n * data-room sidebar) both satisfy this shape via their\n * `useDocSearch` hook result.\n */\nexport interface DocSearchResultRowEntry {\n title?: string;\n path?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface DocSearchResultRowProps {\n result: DocSearchResultRowEntry;\n isHighlighted: boolean;\n}\n\nexport function DocSearchResultRow({ result, isHighlighted }: DocSearchResultRowProps) {\n const docType = (result.metadata?.documentType as string) || undefined;\n const sourceRepo = (result.metadata?.sourceRepo as string) || undefined;\n const { Icon: SourceIcon, label: iconLabel } = resolveSourceIcon({\n sourceRepo,\n documentType: docType,\n });\n const isGroup = result.metadata?.isGroup as boolean | undefined;\n\n return (\n <div className=\"flex w-full min-w-0 items-center gap-3\">\n <span className=\"flex-shrink-0 text-ods-text-secondary\" title={iconLabel}>\n <SourceIcon className=\"size-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className={`truncate text-h6 ${isHighlighted ? 'text-ods-accent' : 'text-ods-text-primary'}`}>\n {result.title || result.path}\n </div>\n {!isGroup && result.path?.includes('/') && (\n <div className=\"mt-0.5 truncate text-ods-text-secondary text-h6\">{formatRelativePath(result.path)}</div>\n )}\n </div>\n </div>\n );\n}\n","'use client';\n\n/**\n * `<DocSearchBar>` — the canonical RAG-search dropdown surface.\n *\n * Mounted by every doc-search consumer (data-room sidebar, onboarding-\n * guide catalog, and any future surface that needs typeahead against\n * `/api/docs/search`). Wraps `<SearchInput>` with the lib's standard\n * `<DocSearchResultRow>` so the dropdown looks identical everywhere.\n *\n * ## Why a presentation component, not a \"search bar that owns its\n * own hook\"\n *\n * The data-fetching hook (`useDocSearch`) lives hub-side because it\n * depends on hub-only context (`useDocNavigation`, the rag-table-\n * config registry, the hub's `decideNewTab` helper). Moving the hook\n * would cascade ~5 more file migrations into the lib.\n *\n * Instead, the hook stays hub-side and callers pass its result into\n * this component as plain props. Both consumers shrink to ~5 lines.\n */\n\nimport type { ReactNode } from 'react';\nimport { SearchInput, type SearchResult } from '../../ui/search-input';\nimport { DocSearchResultRow } from './doc-search-result-row';\n\nexport interface DocSearchBarProps {\n placeholder: string;\n query: string;\n onQueryChange: (value: string) => void;\n /** Hook-fetched results. Reuses the lib's `<SearchInput>` `SearchResult`\n * shape directly so callers don't translate. */\n results: SearchResult[];\n isLoading: boolean;\n /** Result selection handler. Mirrors `<SearchInput>` — the second\n * `modifiers` argument is preserved so cmd-click / shift-click on\n * a result row still forces new-tab behavior. Hub `useDocSearch`\n * reads these to short-circuit to `window.open()`. */\n onResultSelect: (\n result: SearchResult,\n modifiers?: {\n metaKey?: boolean;\n ctrlKey?: boolean;\n shiftKey?: boolean;\n altKey?: boolean;\n button?: number;\n },\n ) => void;\n /** Lets the caller's hook force the dropdown open after a recent\n * internal action (e.g. result navigation). `undefined` falls back\n * to `<SearchInput>`'s built-in focus/hover heuristics. */\n showDropdown?: boolean;\n /** Defaults to 2 — matches the existing data-room and onboarding-\n * guide consumers. Override only if a surface needs different\n * typeahead semantics. */\n minQueryLength?: number;\n /** Defaults to 0 — both existing consumers debounce inside the\n * hook, not the input. */\n debounceMs?: number;\n className?: string;\n /** Optional row-renderer override. Defaults to the lib's standard\n * `<DocSearchResultRow>` (source icon + title + path breadcrumb).\n * Override only when a surface needs custom row chrome. */\n renderResult?: (result: SearchResult, isHighlighted: boolean) => ReactNode;\n}\n\nexport function DocSearchBar({\n placeholder,\n query,\n onQueryChange,\n results,\n isLoading,\n onResultSelect,\n showDropdown,\n minQueryLength = 2,\n debounceMs = 0,\n className = 'w-full',\n renderResult,\n}: DocSearchBarProps) {\n return (\n <SearchInput\n placeholder={placeholder}\n value={query}\n onChange={onQueryChange}\n results={results}\n isLoading={isLoading}\n onResultSelect={onResultSelect}\n showDropdown={showDropdown || undefined}\n debounceMs={debounceMs}\n minQueryLength={minQueryLength}\n className={className}\n renderResult={\n renderResult ??\n ((result, isHighlighted) => <DocSearchResultRow result={result} isHighlighted={isHighlighted} />)\n }\n />\n );\n}\n","/**\n * Map RAG `/api/docs/search` wire results into the `<DocSearchBar>`\n * dropdown's row shape, collapsing entity-table rows into grouped\n * results so the dropdown lists ONE \"Cap Table (12 records)\" row\n * instead of 12 individual rows.\n *\n * Pure transform — no telemetry, no navigation, no React deps. Lifted\n * from the hub's `hooks/use-docs.ts:mapDocSearchResults` (the hub's\n * `traceCompose` call was hub-only telemetry and is intentionally\n * dropped — callers that want logging can wrap this helper).\n */\n\nimport { recordCountLabel } from '../../../utils/source-grouping';\nimport { getSourceLabel } from '../../../utils/source-icons';\nimport type { SearchResult } from '../../ui/search-input';\nimport type { DocSearchResult } from './types';\n\n/** Source repos that should be collapsed into grouped results in the search bar.\n * A SEARCH rule, deliberately narrower than the chat strip's `groupsByTable`:\n * a search result is a destination, so only rows that share one are folded.\n * Only financial tables (all rows link to the same admin page).\n * Content tables (blog, webinar, podcast, etc.) stay individual since each has a unique URL. */\nconst SEARCH_GROUP_REPOS = new Set([\n 'financial-cap-table',\n 'financial-kpis',\n 'financial-pnl',\n 'financial-balance-sheet',\n 'financial-cash-flow',\n]);\n\nexport function mapDocSearchResults(docs: DocSearchResult[]): SearchResult[] {\n const entityGroups = new Map<string, DocSearchResult[]>();\n // Track insertion order — groups appear where the FIRST row of that\n // repo appeared in the response.\n const order: Array<{ type: 'entity'; repo: string } | { type: 'doc'; doc: DocSearchResult }> = [];\n const seenRepos = new Set<string>();\n\n for (const doc of docs) {\n if (doc.sourceRepo && SEARCH_GROUP_REPOS.has(doc.sourceRepo)) {\n const group = entityGroups.get(doc.sourceRepo) || [];\n group.push(doc);\n entityGroups.set(doc.sourceRepo, group);\n if (!seenRepos.has(doc.sourceRepo)) {\n seenRepos.add(doc.sourceRepo);\n order.push({ type: 'entity', repo: doc.sourceRepo });\n }\n } else {\n order.push({ type: 'doc', doc });\n }\n }\n\n const results: SearchResult[] = [];\n for (const entry of order) {\n if (entry.type === 'entity') {\n // An 'entity' entry is only pushed AFTER its group lands in the map, so\n // a miss is unreachable — but dropping one group beats throwing away the\n // whole search dropdown if that ever stops holding.\n const rows = entityGroups.get(entry.repo);\n if (!rows || rows.length === 0) continue;\n results.push({\n id: `group-${entry.repo}`,\n // Same table label and count as the chat's grouped chip: one owner each.\n title: `${getSourceLabel(entry.repo)} ${recordCountLabel(rows.length)}`,\n path: rows[0].path,\n type: 'file',\n metadata: {\n documentType: rows[0].documentType,\n externalUrl: rows[0].externalUrl,\n sourceRepo: entry.repo,\n id: rows[0].entityId,\n isGroup: true,\n items: rows.map(r => ({\n name: r.name,\n externalUrl: r.externalUrl,\n id: r.entityId,\n sourceRepo: r.sourceRepo,\n documentType: r.documentType,\n })),\n },\n });\n } else {\n const doc = entry.doc;\n const isNonMarkdown = doc.documentType && doc.documentType !== 'markdown';\n results.push({\n id: doc.path,\n title: doc.name,\n description: isNonMarkdown ? doc.name : doc.snippet,\n path: doc.path,\n type: doc.type,\n metadata: {\n matchType: doc.matchType,\n ...(doc.documentType ? { documentType: doc.documentType } : {}),\n ...(doc.externalUrl ? { externalUrl: doc.externalUrl } : {}),\n ...(doc.targetPlatform != null ? { targetPlatform: doc.targetPlatform } : {}),\n ...(doc.sourceRepo ? { sourceRepo: doc.sourceRepo } : {}),\n ...(doc.entityId ? { id: doc.entityId } : {}),\n },\n });\n }\n }\n\n return results;\n}\n","/**\n * Resolve what should happen when the user picks a search result.\n * Returns one of five typed actions so the caller is a single switch.\n *\n * Resolution order:\n * 1. `externalUrl` present → resolve the href through the host's\n * `composeContentUrl` seam (when wired), then use `decideNewTab` to\n * choose same-tab vs new-tab against the resolved `targetPlatform`.\n * 2. Row has `id` + `sourceRepo` + `documentType` → synth an Ask-AI\n * action (entity drill-in via primary key, no URL).\n * 3. Row has only `path` → legacy navigation fallback.\n * 4. Nothing actionable → noop.\n *\n * Lifted from the hub's `hooks/use-docs.ts:resolveSearchResultAction`.\n * Pure — no React, no telemetry.\n */\n\nimport type { ComposeContentUrl } from '../../../utils/content-href';\nimport type { ChatRef } from '../../chat/chat-ref.types';\nimport { decideNewTab } from '../../chat/utils/decide-new-tab';\nimport type { SearchResult } from '../../ui/search-input';\n\n/** True for `https://…` and protocol-relative `//…`. A composed href that is\n * NOT absolute is an in-app route the host claimed via `hostedTypes` /\n * `overrides`. */\nfunction isAbsoluteHref(href: string): boolean {\n return /^(?:[a-z][a-z0-9+.-]*:)?\\/\\//i.test(href);\n}\n\nexport type SearchResultAction =\n | { kind: 'navigate-same-tab'; href: string }\n | { kind: 'navigate-new-tab'; href: string }\n | { kind: 'ask-ai'; detail: { source: string; ref: ChatRef } }\n | { kind: 'route'; path: string }\n | { kind: 'noop' };\n\nexport function resolveSearchResultAction(\n result: SearchResult,\n source: string,\n runtimeMode?: 'host' | 'embed',\n /** The host's unified content-href seam (`runtime.composeContentUrl`).\n * When wired, an entity row's RAG `externalUrl` is re-resolved through it,\n * so a type the host serves in-app (`hostedTypes`) or deep-links itself\n * (`overrides`) lands on the SAME href the catalog cards and chat cards\n * use. Omitted → the legacy verbatim-`externalUrl` behavior. */\n composeContentUrl?: ComposeContentUrl,\n): SearchResultAction {\n const meta = result.metadata ?? {};\n const externalUrl = meta.externalUrl as string | undefined;\n const rowId = meta.id as string | undefined;\n const sourceRepo = meta.sourceRepo as string | undefined;\n const documentType = meta.documentType as string | undefined;\n if (externalUrl) {\n const targetPlatform = meta.targetPlatform as string | null | undefined;\n // Unified content-href seam — the same call `resolveSourceRowCTA` makes\n // for chat rows. Hosted types relativize the slug out of `externalUrl`;\n // everything else comes back verbatim, so an unwired host (or a row with\n // no `documentType`) keeps today's behavior byte-for-byte.\n const composed =\n composeContentUrl && documentType\n ? composeContentUrl({\n type: documentType,\n identifier: rowId ?? '',\n externalUrl,\n targetPlatform: targetPlatform ?? null,\n })\n : null;\n const href = composed?.href || externalUrl;\n // A RELATIVE composed href means the host claimed this row as in-app.\n // Short-circuit BEFORE `decideNewTab`: in embed mode that helper forces\n // new-tab unconditionally, which would fire `window.open()` on a path\n // that only resolves inside this app.\n if (composed && !isAbsoluteHref(href)) {\n return { kind: 'navigate-same-tab', href };\n }\n // Same pure helper `useNavLink` and `useUnifiedNav` call — single\n // decision rule across cards, chips, and autocomplete rows. Thread\n // the caller's `source` as `currentSource` so the platform-vs-\n // platform comparison matches the hub's pre-migration behavior.\n const isNewTab = decideNewTab({\n href,\n targetPlatform: composed ? composed.targetPlatform : targetPlatform,\n surface: 'useUnifiedNav',\n runtimeMode,\n currentSource: source,\n });\n return isNewTab ? { kind: 'navigate-new-tab', href } : { kind: 'navigate-same-tab', href };\n }\n if (rowId && sourceRepo && documentType) {\n return {\n kind: 'ask-ai',\n detail: {\n source,\n ref: { type: documentType, id: rowId, title: result.title, url: null },\n },\n };\n }\n if (result.path) {\n return { kind: 'route', path: result.path };\n }\n return { kind: 'noop' };\n}\n","'use client';\n\n/**\n * `useDocSearch` — debounced RAG-search hook against `/api/docs/search`.\n *\n * Pure fetch + navigation glue. Embedders can mount this directly\n * (any host with a reverse-proxy that exposes `/api/docs/search` will\n * work). Hub callers wire it into the lib `<DocSearchBar>` for the\n * canonical typeahead dropdown.\n *\n * ## What moved from hub to lib\n *\n * Lifted from `multi-platform-hub/hooks/use-docs.ts:useDocSearch`. Two\n * hub-only concerns are now optional injection points instead of\n * direct imports:\n *\n * - `useDocNavigation()` (hub's in-page doc-tree swap) → optional\n * `onInPageSwap?: (path: string) => boolean` config callback. When\n * present and returns true, the hook treats a same-origin result\n * click as \"handled in-page\"; when absent or returns false, the\n * hook falls back to `onNavigate(path)` (`router.push` on hub,\n * `window.location.assign` on bare embedders).\n * - `traceCompose` (hub-only telemetry) → dropped. The lib has no\n * equivalent runtime-context yet; bring it back when there is one.\n *\n * Everything else (debounce, `useChatRuntime` for embed-mode short-\n * circuit, embed-shim router, the action-resolver + result-mapper) is\n * now lib-resident.\n *\n * ## Result hrefs go through the content-href seam\n *\n * A row's href is NOT the RAG `externalUrl` verbatim any more: when the\n * host wires `runtime.composeContentUrl` (or passes `composeContentUrl`\n * here), the row is re-resolved through it — the SAME seam the catalog\n * cards and the chat entity cards use. So a host that serves onboarding\n * guides at `/help-center/onboarding-guides/<slug>` gets that path from\n * the dropdown too, instead of bouncing the user to the canonical hub\n * URL. Unwired hosts keep the verbatim-`externalUrl` behavior.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context';\nimport { useRouter } from '../../../embed-shims';\nimport { useDebounce } from '../../../hooks/ui/use-debounce';\nimport type { ComposeContentUrl } from '../../../utils/content-href';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport {\n resolveExternalNavigation,\n stripSameOriginToPath,\n NEW_TAB_FEATURES,\n} from '../../chat/utils/chat-nav-resolution';\nimport type { SearchResult } from '../../ui/search-input';\nimport { mapDocSearchResults } from './map-doc-search-results';\nimport { resolveSearchResultAction } from './resolve-search-result-action';\nimport type { DocSearchResult } from './types';\n\nexport interface UseDocSearchConfig {\n /** Discriminator passed to `/api/docs/search?source=` (e.g.\n * `'openframe'`). Embedders set it to whatever discriminator their\n * reverse-proxy expects. */\n source: string;\n /** Base route prefix this search lives under (e.g. `'/onboarding-guides'`).\n * When a result's href starts with `${baseRoute}/`, the hook\n * attempts the optional in-page swap path before falling through\n * to a full nav. */\n baseRoute: string;\n /** Imperative navigation fallback. Called when no override\n * (in-page swap, new-tab) applies. Hub callers pass\n * `(path) => router.push(path)`; embedders pass an equivalent. */\n onNavigate: (path: string) => void;\n /** Optional `RagTableConfig.id` list to narrow the search to specific\n * tables (e.g. `['onboarding-guides']`). Forwarded to\n * `/api/docs/search?tableIds=…` which intersects with the source's\n * standing set. */\n tableIds?: string[];\n /** Optional in-page swap callback. When the result's href is under\n * `baseRoute` AND this callback returns true, the hook treats the\n * click as handled in-page (no router push). Hub's\n * `<DocumentationSection>` wires this to\n * `useDocNavigation().navigate(path)`. */\n onInPageSwap?: (path: string) => boolean;\n /** Optional endpoint override. Defaults to `'/api/docs/search'`\n * (the hub's reverse-proxy route). Embedders with a different\n * path can override. */\n searchEndpoint?: string;\n /** Optional content-href seam override. Same fall-back chain as\n * `searchEndpoint`: prop → `runtime.composeContentUrl` → none. Wired,\n * it makes a result row honor the host's `hostedTypes` / `overrides`\n * instead of navigating to the RAG's canonical hub URL — so a row\n * lands where the catalog card for the same entity lands. */\n composeContentUrl?: ComposeContentUrl;\n}\n\n/** One shared empty array, so \"no matches\" keeps a stable identity. */\nconst NO_RESULTS: SearchResult[] = [];\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** `Array.isArray` narrows `unknown` to `any[]`; this keeps the elements `unknown`. */\nfunction isUnknownArray(value: unknown): value is readonly unknown[] {\n return Array.isArray(value);\n}\n\nfunction readString(source: Record<string, unknown>, key: string): string | undefined {\n const value = source[key];\n return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Decode `/api/docs/search`'s `{ success, data }` envelope.\n *\n * `Response.json()` is typed `any`, so the rows used to reach\n * `mapDocSearchResults` through an `as DocSearchResult[]` that asserted a\n * shape nothing had checked. `path` and `name` are the two that bite: the\n * mapper uses `path` as the row's React `id` and `name` as its title, so a row\n * missing either renders as a blank, key-colliding entry in the dropdown.\n * Such rows are dropped; the rest of the result set still lists.\n *\n * Returns null when the envelope itself is not a successful result set.\n */\nfunction toDocSearchResults(payload: unknown): DocSearchResult[] | null {\n if (!isRecord(payload) || !payload.success || !isUnknownArray(payload.data)) return null;\n\n const rows: DocSearchResult[] = [];\n for (const entry of payload.data) {\n if (!isRecord(entry)) continue;\n const path = readString(entry, 'path');\n const name = readString(entry, 'name');\n const type = entry.type;\n if (path === undefined || name === undefined) continue;\n\n const targetPlatform = entry.targetPlatform;\n rows.push({\n path,\n name,\n type: type === 'folder' ? 'folder' : 'file',\n snippet: readString(entry, 'snippet') ?? '',\n matchType: entry.matchType === 'title' ? 'title' : 'content',\n documentType: readString(entry, 'documentType'),\n externalUrl: readString(entry, 'externalUrl'),\n targetPlatform: typeof targetPlatform === 'string' ? targetPlatform : null,\n sourceRepo: readString(entry, 'sourceRepo'),\n entityId: readString(entry, 'entityId'),\n });\n }\n return rows;\n}\n\nexport function useDocSearch(config: UseDocSearchConfig) {\n const { source, baseRoute, onNavigate, tableIds, onInPageSwap, searchEndpoint, composeContentUrl } = config;\n const tableIdsKey = tableIds && tableIds.length > 0 ? tableIds.join(',') : '';\n\n const router = useRouter();\n // Optional chat-runtime read — when present and mode='embed' the\n // search-result row click short-circuits to a new-tab open against\n // the absolutized URL. Null/host preserves today's behavior.\n // Also used as the proxy-prefix fallback for `searchEndpoint`, matching\n // how tickets resolves `findTicketUrl`.\n const runtime = useChatRuntime();\n const resolvedSearchEndpoint = searchEndpoint ?? runtime?.endpoints.docsSearchUrl ?? '/api/docs/search';\n // Content-href seam, same prop → runtime → none chain. Threading it into\n // `resolveSearchResultAction` is what makes the dropdown obey the host's\n // route map; without it the row navigates to the RAG's canonical hub URL\n // even when the host serves that content in-app under a different path.\n const resolvedComposeContentUrl = composeContentUrl ?? runtime?.composeContentUrl;\n\n const [query, setQuery] = useState('');\n const [results, setResults] = useState<SearchResult[]>(NO_RESULTS);\n const [isFetching, setIsFetching] = useState(false);\n const debouncedQuery = useDebounce(query, 300);\n\n // Falling below the two-character floor empties the dropdown. Adjusted while\n // rendering (React's documented pattern for a reset the render already has\n // the answer to) rather than from an effect, which committed one more frame\n // of the previous matches under a query that no longer searches for them.\n const searchable = debouncedQuery.trim().length >= 2;\n const [wasSearchable, setWasSearchable] = useState(searchable);\n if (wasSearchable !== searchable) {\n setWasSearchable(searchable);\n if (!searchable) {\n setResults(NO_RESULTS);\n setIsFetching(false);\n }\n }\n\n useEffect(() => {\n if (!searchable) return undefined;\n\n let cancelled = false;\n\n async function fetchResults() {\n setIsFetching(true);\n try {\n const params = new URLSearchParams({\n q: debouncedQuery,\n source,\n limit: '10',\n });\n if (tableIdsKey) params.set('tableIds', tableIdsKey);\n\n const response = await contentFetch(`${resolvedSearchEndpoint}?${params.toString()}`);\n if (!response.ok) {\n throw new Error(`Search request failed: ${response.status}`);\n }\n\n const json: unknown = await response.json();\n\n if (!cancelled) {\n // An envelope that is not a successful result set now clears the\n // dropdown instead of leaving it untouched. Previously a 200 that\n // carried `success: false` (or a non-array `data`) fell through\n // every branch, so the PREVIOUS query's matches stayed on screen\n // under the new query — the throw path already reset them.\n const rows = toDocSearchResults(json);\n setResults(rows ? mapDocSearchResults(rows) : NO_RESULTS);\n }\n } catch (error) {\n console.error('Doc search error:', error);\n if (!cancelled) {\n setResults(NO_RESULTS);\n }\n } finally {\n if (!cancelled) {\n setIsFetching(false);\n }\n }\n }\n\n // Never rejects — try/catch/finally, every write gated on `cancelled`.\n void fetchResults();\n\n return () => {\n cancelled = true;\n };\n }, [debouncedQuery, searchable, source, tableIdsKey, resolvedSearchEndpoint]);\n\n // Derived loading state — single source of truth for \"should the\n // dropdown show 'Loading...' instead of 'No results found'\":\n const isLoading = query.trim().length >= 2 && (query !== debouncedQuery || isFetching);\n\n // Track whether dropdown should stay open (external link opened in new tab).\n const [keepOpen, setKeepOpen] = useState(false);\n\n const handleResultSelect = useCallback(\n (\n result: SearchResult,\n modifiers?: {\n metaKey?: boolean;\n ctrlKey?: boolean;\n shiftKey?: boolean;\n altKey?: boolean;\n button?: number;\n },\n ) => {\n const action = resolveSearchResultAction(result, source, runtime?.navigation.mode, resolvedComposeContentUrl);\n // Modifier / non-primary mouse click → force new tab regardless of\n // same-tab/new-tab decision. The dropdown row is a `<div>`, not an\n // `<a target=\"_blank\">`, so the browser doesn't background-tab\n // natively on cmd-click. Honor it explicitly here for parity with\n // the anchor-based surfaces (cards, chips, related-content). Plain\n // Enter from the keyboard passes `modifiers === undefined`.\n const wantsNewTab =\n modifiers &&\n (modifiers.metaKey ||\n modifiers.ctrlKey ||\n modifiers.shiftKey ||\n modifiers.altKey ||\n (typeof modifiers.button === 'number' && modifiers.button !== 0));\n switch (action.kind) {\n case 'navigate-same-tab': {\n if (wantsNewTab) {\n setKeepOpen(true);\n window.open(action.href, '_blank', NEW_TAB_FEATURES);\n return;\n }\n // Same-origin click:\n // 1. If the href is under the current doc-tree's baseRoute AND\n // an `onInPageSwap` callback is wired AND returns true →\n // consider in-page swap handled.\n // 2. Otherwise → embed-shim `router.push()` (soft RSC nav on\n // Next.js hosts, window.location.assign on bare hosts).\n setKeepOpen(false);\n const path =\n baseRoute && action.href.startsWith(`${baseRoute}/`) ? action.href.slice(baseRoute.length + 1) : null;\n if (path && onInPageSwap?.(path)) return;\n router.push(stripSameOriginToPath(action.href));\n return;\n }\n case 'navigate-new-tab':\n // Cross-origin (e.g. clicking a flamingo.run release from\n // product-hub) — open in a new tab. Keep dropdown open so the\n // user can pick another result without re-searching.\n setKeepOpen(true);\n // Embed-mode short-circuit — the row was clicked while the panel is\n // hosted inside an embedding app, so absolutize against the\n // embedder-supplied content origin and honor its `openExternal`\n // override. Not the only path an embed host takes: a composed\n // RELATIVE href resolves to `navigate-same-tab` above and never\n // reaches `decideNewTab`, which is what lets an embedder keep its\n // own in-app routes in-app.\n if (runtime?.navigation.mode === 'embed') {\n const targetPlatform = (result.metadata?.targetPlatform as string | null | undefined) ?? null;\n resolveExternalNavigation({\n href: action.href,\n targetPlatform,\n runtime,\n }).open();\n return;\n }\n window.open(action.href, '_blank', NEW_TAB_FEATURES);\n return;\n case 'ask-ai':\n // Row is searchable-but-not-openable (cap_table positions,\n // financial-kpi snapshots, anything backed by\n // `resolveUrl: () => null`). Dispatch a CustomEvent that\n // GlobalAskAI listens for — opens chat + drills via\n // `entityIdFilter` (primary-key only, same as inline-card Ask).\n setKeepOpen(false);\n window.dispatchEvent(new CustomEvent('ask-ai:open-with-ref', { detail: action.detail }));\n return;\n case 'route':\n // Final fallback: legacy navigation by path. Hits when a row\n // has neither URL nor pk metadata — a mapper/API regression.\n setKeepOpen(false);\n onNavigate(action.path);\n return;\n case 'noop':\n return;\n }\n },\n [onNavigate, source, baseRoute, router, onInPageSwap, runtime, resolvedComposeContentUrl],\n );\n\n // Reset keepOpen when query changes. Adjusted while rendering, not from an\n // effect: the dropdown's open state is read from this in the same render the\n // keystroke produced, so an effect held it pinned open for one extra frame.\n const [keepOpenForQuery, setKeepOpenForQuery] = useState(query);\n if (keepOpenForQuery !== query) {\n setKeepOpenForQuery(query);\n setKeepOpen(false);\n }\n\n return {\n query,\n setQuery,\n results,\n isLoading,\n handleResultSelect,\n keepDropdownOpen: keepOpen,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-LCENRN2N.cjs","../src/components/shared/doc-search/format-relative-path.ts","../src/components/shared/doc-search/doc-search-result-row.tsx","../src/components/shared/doc-search/doc-search-bar.tsx","../src/components/shared/doc-search/map-doc-search-results.ts","../src/components/shared/doc-search/resolve-search-result-action.ts","../src/components/shared/doc-search/use-doc-search.ts"],"names":["jsx"],"mappings":"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","file":"/home/runner/work/openframe-oss-lib/openframe-oss-lib/openframe-frontend-core/dist/chunk-LCENRN2N.cjs","sourcesContent":[null,"/**\n * Format a full document path as a breadcrumb trail.\n * Shows parent folders only (excludes the last segment / filename).\n *\n * @example\n * formatRelativePath('openframe-oss-tenant/architecture/api-controllers.md')\n * // → 'Openframe oss tenant / Architecture'\n */\nexport function formatRelativePath(fullPath: string): string {\n if (!fullPath) return '';\n const segments = fullPath.replace(/\\.md$/, '').split('/');\n // Show only parent path (exclude the filename itself since the title already shows it)\n const parentSegments = segments.length > 1 ? segments.slice(0, -1) : segments;\n return parentSegments.map(seg => seg.charAt(0).toUpperCase() + seg.slice(1).replace(/-/g, ' ')).join(' / ');\n}\n","'use client';\n\n/**\n * Single row in the `<SearchInput>` dropdown — the standard layout\n * used by every doc-search-backed surface (company-hub data-room\n * search bar, onboarding-guide catalog search, …). Single source of\n * truth for the row appearance so search dropdowns are visually\n * identical everywhere.\n *\n * Resolves the source icon via the same `resolveSourceIcon()`\n * registry the inline chat-card refs use, so a row pointing at e.g.\n * an onboarding-guide surfaces the SAME `<GraduationCap>` glyph the\n * chat card surfaces — no cross-surface drift.\n */\n\nimport { resolveSourceIcon } from '../../chat/utils/source-row-cta';\nimport { formatRelativePath } from './format-relative-path';\n\n/**\n * Minimal result shape this row renders. Compatible with any\n * doc-search hook whose result type exposes `{ title?, path?,\n * metadata? }`. The two hub consumers (onboarding-guide catalog,\n * data-room sidebar) both satisfy this shape via their\n * `useDocSearch` hook result.\n */\nexport interface DocSearchResultRowEntry {\n title?: string;\n path?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface DocSearchResultRowProps {\n result: DocSearchResultRowEntry;\n isHighlighted: boolean;\n}\n\nexport function DocSearchResultRow({ result, isHighlighted }: DocSearchResultRowProps) {\n const docType = (result.metadata?.documentType as string) || undefined;\n const sourceRepo = (result.metadata?.sourceRepo as string) || undefined;\n const { Icon: SourceIcon, label: iconLabel } = resolveSourceIcon({\n sourceRepo,\n documentType: docType,\n });\n const isGroup = result.metadata?.isGroup as boolean | undefined;\n\n return (\n <div className=\"flex w-full min-w-0 items-center gap-3\">\n <span className=\"flex-shrink-0 text-ods-text-secondary\" title={iconLabel}>\n <SourceIcon className=\"size-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className={`truncate text-h6 ${isHighlighted ? 'text-ods-accent' : 'text-ods-text-primary'}`}>\n {result.title || result.path}\n </div>\n {!isGroup && result.path?.includes('/') && (\n <div className=\"mt-0.5 truncate text-ods-text-secondary text-h6\">{formatRelativePath(result.path)}</div>\n )}\n </div>\n </div>\n );\n}\n","'use client';\n\n/**\n * `<DocSearchBar>` — the canonical RAG-search dropdown surface.\n *\n * Mounted by every doc-search consumer (data-room sidebar, onboarding-\n * guide catalog, and any future surface that needs typeahead against\n * `/api/docs/search`). Wraps `<SearchInput>` with the lib's standard\n * `<DocSearchResultRow>` so the dropdown looks identical everywhere.\n *\n * ## Why a presentation component, not a \"search bar that owns its\n * own hook\"\n *\n * The data-fetching hook (`useDocSearch`) lives hub-side because it\n * depends on hub-only context (`useDocNavigation`, the rag-table-\n * config registry, the hub's `decideNewTab` helper). Moving the hook\n * would cascade ~5 more file migrations into the lib.\n *\n * Instead, the hook stays hub-side and callers pass its result into\n * this component as plain props. Both consumers shrink to ~5 lines.\n */\n\nimport type { ReactNode } from 'react';\nimport { SearchInput, type SearchResult } from '../../ui/search-input';\nimport { DocSearchResultRow } from './doc-search-result-row';\n\nexport interface DocSearchBarProps {\n placeholder: string;\n query: string;\n onQueryChange: (value: string) => void;\n /** Hook-fetched results. Reuses the lib's `<SearchInput>` `SearchResult`\n * shape directly so callers don't translate. */\n results: SearchResult[];\n isLoading: boolean;\n /** Result selection handler. Mirrors `<SearchInput>` — the second\n * `modifiers` argument is preserved so cmd-click / shift-click on\n * a result row still forces new-tab behavior. Hub `useDocSearch`\n * reads these to short-circuit to `window.open()`. */\n onResultSelect: (\n result: SearchResult,\n modifiers?: {\n metaKey?: boolean;\n ctrlKey?: boolean;\n shiftKey?: boolean;\n altKey?: boolean;\n button?: number;\n },\n ) => void;\n /** Lets the caller's hook force the dropdown open after a recent\n * internal action (e.g. result navigation). `undefined` falls back\n * to `<SearchInput>`'s built-in focus/hover heuristics. */\n showDropdown?: boolean;\n /** Defaults to 2 — matches the existing data-room and onboarding-\n * guide consumers. Override only if a surface needs different\n * typeahead semantics. */\n minQueryLength?: number;\n /** Defaults to 0 — both existing consumers debounce inside the\n * hook, not the input. */\n debounceMs?: number;\n className?: string;\n /** Optional row-renderer override. Defaults to the lib's standard\n * `<DocSearchResultRow>` (source icon + title + path breadcrumb).\n * Override only when a surface needs custom row chrome. */\n renderResult?: (result: SearchResult, isHighlighted: boolean) => ReactNode;\n}\n\nexport function DocSearchBar({\n placeholder,\n query,\n onQueryChange,\n results,\n isLoading,\n onResultSelect,\n showDropdown,\n minQueryLength = 2,\n debounceMs = 0,\n className = 'w-full',\n renderResult,\n}: DocSearchBarProps) {\n return (\n <SearchInput\n placeholder={placeholder}\n value={query}\n onChange={onQueryChange}\n results={results}\n isLoading={isLoading}\n onResultSelect={onResultSelect}\n showDropdown={showDropdown || undefined}\n debounceMs={debounceMs}\n minQueryLength={minQueryLength}\n className={className}\n renderResult={\n renderResult ??\n ((result, isHighlighted) => <DocSearchResultRow result={result} isHighlighted={isHighlighted} />)\n }\n />\n );\n}\n","/**\n * Map RAG `/api/docs/search` wire results into the `<DocSearchBar>`\n * dropdown's row shape, collapsing entity-table rows into grouped\n * results so the dropdown lists ONE \"Cap Table (12 records)\" row\n * instead of 12 individual rows.\n *\n * Pure transform — no telemetry, no navigation, no React deps. Lifted\n * from the hub's `hooks/use-docs.ts:mapDocSearchResults` (the hub's\n * `traceCompose` call was hub-only telemetry and is intentionally\n * dropped — callers that want logging can wrap this helper).\n */\n\nimport { recordCountLabel } from '../../../utils/source-grouping';\nimport { getSourceLabel } from '../../../utils/source-icons';\nimport type { SearchResult } from '../../ui/search-input';\nimport type { DocSearchResult } from './types';\n\n/** Source repos that should be collapsed into grouped results in the search bar.\n * A SEARCH rule, deliberately narrower than the chat strip's `groupsByTable`:\n * a search result is a destination, so only rows that share one are folded.\n * Only financial tables (all rows link to the same admin page).\n * Content tables (blog, webinar, podcast, etc.) stay individual since each has a unique URL. */\nconst SEARCH_GROUP_REPOS = new Set([\n 'financial-cap-table',\n 'financial-kpis',\n 'financial-pnl',\n 'financial-balance-sheet',\n 'financial-cash-flow',\n]);\n\nexport function mapDocSearchResults(docs: DocSearchResult[]): SearchResult[] {\n const entityGroups = new Map<string, DocSearchResult[]>();\n // Track insertion order — groups appear where the FIRST row of that\n // repo appeared in the response.\n const order: Array<{ type: 'entity'; repo: string } | { type: 'doc'; doc: DocSearchResult }> = [];\n const seenRepos = new Set<string>();\n\n for (const doc of docs) {\n if (doc.sourceRepo && SEARCH_GROUP_REPOS.has(doc.sourceRepo)) {\n const group = entityGroups.get(doc.sourceRepo) || [];\n group.push(doc);\n entityGroups.set(doc.sourceRepo, group);\n if (!seenRepos.has(doc.sourceRepo)) {\n seenRepos.add(doc.sourceRepo);\n order.push({ type: 'entity', repo: doc.sourceRepo });\n }\n } else {\n order.push({ type: 'doc', doc });\n }\n }\n\n const results: SearchResult[] = [];\n for (const entry of order) {\n if (entry.type === 'entity') {\n // An 'entity' entry is only pushed AFTER its group lands in the map, so\n // a miss is unreachable — but dropping one group beats throwing away the\n // whole search dropdown if that ever stops holding.\n const rows = entityGroups.get(entry.repo);\n if (!rows || rows.length === 0) continue;\n results.push({\n id: `group-${entry.repo}`,\n // Same table label and count as the chat's grouped chip: one owner each.\n title: `${getSourceLabel(entry.repo)} ${recordCountLabel(rows.length)}`,\n path: rows[0].path,\n type: 'file',\n metadata: {\n documentType: rows[0].documentType,\n externalUrl: rows[0].externalUrl,\n sourceRepo: entry.repo,\n id: rows[0].entityId,\n isGroup: true,\n items: rows.map(r => ({\n name: r.name,\n externalUrl: r.externalUrl,\n id: r.entityId,\n sourceRepo: r.sourceRepo,\n documentType: r.documentType,\n })),\n },\n });\n } else {\n const doc = entry.doc;\n const isNonMarkdown = doc.documentType && doc.documentType !== 'markdown';\n results.push({\n id: doc.path,\n title: doc.name,\n description: isNonMarkdown ? doc.name : doc.snippet,\n path: doc.path,\n type: doc.type,\n metadata: {\n matchType: doc.matchType,\n ...(doc.documentType ? { documentType: doc.documentType } : {}),\n ...(doc.externalUrl ? { externalUrl: doc.externalUrl } : {}),\n ...(doc.targetPlatform != null ? { targetPlatform: doc.targetPlatform } : {}),\n ...(doc.sourceRepo ? { sourceRepo: doc.sourceRepo } : {}),\n ...(doc.entityId ? { id: doc.entityId } : {}),\n },\n });\n }\n }\n\n return results;\n}\n","/**\n * Resolve what should happen when the user picks a search result.\n * Returns one of five typed actions so the caller is a single switch.\n *\n * Resolution order:\n * 1. `externalUrl` present → resolve the href through the host's\n * `composeContentUrl` seam (when wired), then use `decideNewTab` to\n * choose same-tab vs new-tab against the resolved `targetPlatform`.\n * 2. Row has `id` + `sourceRepo` + `documentType` → synth an Ask-AI\n * action (entity drill-in via primary key, no URL).\n * 3. Row has only `path` → legacy navigation fallback.\n * 4. Nothing actionable → noop.\n *\n * Lifted from the hub's `hooks/use-docs.ts:resolveSearchResultAction`.\n * Pure — no React, no telemetry.\n */\n\nimport type { ComposeContentUrl } from '../../../utils/content-href';\nimport type { ChatRef } from '../../chat/chat-ref.types';\nimport { decideNewTab } from '../../chat/utils/decide-new-tab';\nimport type { SearchResult } from '../../ui/search-input';\n\n/** True for `https://…` and protocol-relative `//…`. A composed href that is\n * NOT absolute is an in-app route the host claimed via `hostedTypes` /\n * `overrides`. */\nfunction isAbsoluteHref(href: string): boolean {\n return /^(?:[a-z][a-z0-9+.-]*:)?\\/\\//i.test(href);\n}\n\nexport type SearchResultAction =\n | { kind: 'navigate-same-tab'; href: string }\n | { kind: 'navigate-new-tab'; href: string }\n | { kind: 'ask-ai'; detail: { source: string; ref: ChatRef } }\n | { kind: 'route'; path: string }\n | { kind: 'noop' };\n\nexport function resolveSearchResultAction(\n result: SearchResult,\n source: string,\n runtimeMode?: 'host' | 'embed',\n /** The host's unified content-href seam (`runtime.composeContentUrl`).\n * When wired, an entity row's RAG `externalUrl` is re-resolved through it,\n * so a type the host serves in-app (`hostedTypes`) or deep-links itself\n * (`overrides`) lands on the SAME href the catalog cards and chat cards\n * use. Omitted → the legacy verbatim-`externalUrl` behavior. */\n composeContentUrl?: ComposeContentUrl,\n): SearchResultAction {\n const meta = result.metadata ?? {};\n const externalUrl = meta.externalUrl as string | undefined;\n const rowId = meta.id as string | undefined;\n const sourceRepo = meta.sourceRepo as string | undefined;\n const documentType = meta.documentType as string | undefined;\n if (externalUrl) {\n const targetPlatform = meta.targetPlatform as string | null | undefined;\n // Unified content-href seam — the same call `resolveSourceRowCTA` makes\n // for chat rows. Hosted types relativize the slug out of `externalUrl`;\n // everything else comes back verbatim, so an unwired host (or a row with\n // no `documentType`) keeps today's behavior byte-for-byte.\n const composed =\n composeContentUrl && documentType\n ? composeContentUrl({\n type: documentType,\n identifier: rowId ?? '',\n externalUrl,\n targetPlatform: targetPlatform ?? null,\n })\n : null;\n const href = composed?.href || externalUrl;\n // A RELATIVE composed href means the host claimed this row as in-app.\n // Short-circuit BEFORE `decideNewTab`: in embed mode that helper forces\n // new-tab unconditionally, which would fire `window.open()` on a path\n // that only resolves inside this app.\n if (composed && !isAbsoluteHref(href)) {\n return { kind: 'navigate-same-tab', href };\n }\n // Same pure helper `useNavLink` and `useUnifiedNav` call — single\n // decision rule across cards, chips, and autocomplete rows. Thread\n // the caller's `source` as `currentSource` so the platform-vs-\n // platform comparison matches the hub's pre-migration behavior.\n const isNewTab = decideNewTab({\n href,\n targetPlatform: composed ? composed.targetPlatform : targetPlatform,\n surface: 'useUnifiedNav',\n runtimeMode,\n currentSource: source,\n });\n return isNewTab ? { kind: 'navigate-new-tab', href } : { kind: 'navigate-same-tab', href };\n }\n if (rowId && sourceRepo && documentType) {\n return {\n kind: 'ask-ai',\n detail: {\n source,\n ref: { type: documentType, id: rowId, title: result.title, url: null },\n },\n };\n }\n if (result.path) {\n return { kind: 'route', path: result.path };\n }\n return { kind: 'noop' };\n}\n","'use client';\n\n/**\n * `useDocSearch` — debounced RAG-search hook against `/api/docs/search`.\n *\n * Pure fetch + navigation glue. Embedders can mount this directly\n * (any host with a reverse-proxy that exposes `/api/docs/search` will\n * work). Hub callers wire it into the lib `<DocSearchBar>` for the\n * canonical typeahead dropdown.\n *\n * ## What moved from hub to lib\n *\n * Lifted from `multi-platform-hub/hooks/use-docs.ts:useDocSearch`. Two\n * hub-only concerns are now optional injection points instead of\n * direct imports:\n *\n * - `useDocNavigation()` (hub's in-page doc-tree swap) → optional\n * `onInPageSwap?: (path: string) => boolean` config callback. When\n * present and returns true, the hook treats a same-origin result\n * click as \"handled in-page\"; when absent or returns false, the\n * hook falls back to `onNavigate(path)` (`router.push` on hub,\n * `window.location.assign` on bare embedders).\n * - `traceCompose` (hub-only telemetry) → dropped. The lib has no\n * equivalent runtime-context yet; bring it back when there is one.\n *\n * Everything else (debounce, `useChatRuntime` for embed-mode short-\n * circuit, embed-shim router, the action-resolver + result-mapper) is\n * now lib-resident.\n *\n * ## Result hrefs go through the content-href seam\n *\n * A row's href is NOT the RAG `externalUrl` verbatim any more: when the\n * host wires `runtime.composeContentUrl` (or passes `composeContentUrl`\n * here), the row is re-resolved through it — the SAME seam the catalog\n * cards and the chat entity cards use. So a host that serves onboarding\n * guides at `/help-center/onboarding-guides/<slug>` gets that path from\n * the dropdown too, instead of bouncing the user to the canonical hub\n * URL. Unwired hosts keep the verbatim-`externalUrl` behavior.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { useChatRuntime } from '../../../contexts/chat-runtime-context';\nimport { useRouter } from '../../../embed-shims';\nimport { useDebounce } from '../../../hooks/ui/use-debounce';\nimport type { ComposeContentUrl } from '../../../utils/content-href';\nimport { contentFetch } from '../../../utils/embed-content-fetch';\nimport {\n resolveExternalNavigation,\n stripSameOriginToPath,\n NEW_TAB_FEATURES,\n} from '../../chat/utils/chat-nav-resolution';\nimport type { SearchResult } from '../../ui/search-input';\nimport { mapDocSearchResults } from './map-doc-search-results';\nimport { resolveSearchResultAction } from './resolve-search-result-action';\nimport type { DocSearchResult } from './types';\n\nexport interface UseDocSearchConfig {\n /** Discriminator passed to `/api/docs/search?source=` (e.g.\n * `'openframe'`). Embedders set it to whatever discriminator their\n * reverse-proxy expects. */\n source: string;\n /** Base route prefix this search lives under (e.g. `'/onboarding-guides'`).\n * When a result's href starts with `${baseRoute}/`, the hook\n * attempts the optional in-page swap path before falling through\n * to a full nav. */\n baseRoute: string;\n /** Imperative navigation fallback. Called when no override\n * (in-page swap, new-tab) applies. Hub callers pass\n * `(path) => router.push(path)`; embedders pass an equivalent. */\n onNavigate: (path: string) => void;\n /** Optional `RagTableConfig.id` list to narrow the search to specific\n * tables (e.g. `['onboarding-guides']`). Forwarded to\n * `/api/docs/search?tableIds=…` which intersects with the source's\n * standing set. */\n tableIds?: string[];\n /** Optional in-page swap callback. When the result's href is under\n * `baseRoute` AND this callback returns true, the hook treats the\n * click as handled in-page (no router push). Hub's\n * `<DocumentationSection>` wires this to\n * `useDocNavigation().navigate(path)`. */\n onInPageSwap?: (path: string) => boolean;\n /** Optional endpoint override. Defaults to `'/api/docs/search'`\n * (the hub's reverse-proxy route). Embedders with a different\n * path can override. */\n searchEndpoint?: string;\n /** Optional content-href seam override. Same fall-back chain as\n * `searchEndpoint`: prop → `runtime.composeContentUrl` → none. Wired,\n * it makes a result row honor the host's `hostedTypes` / `overrides`\n * instead of navigating to the RAG's canonical hub URL — so a row\n * lands where the catalog card for the same entity lands. */\n composeContentUrl?: ComposeContentUrl;\n}\n\n/** One shared empty array, so \"no matches\" keeps a stable identity. */\nconst NO_RESULTS: SearchResult[] = [];\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** `Array.isArray` narrows `unknown` to `any[]`; this keeps the elements `unknown`. */\nfunction isUnknownArray(value: unknown): value is readonly unknown[] {\n return Array.isArray(value);\n}\n\nfunction readString(source: Record<string, unknown>, key: string): string | undefined {\n const value = source[key];\n return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Decode `/api/docs/search`'s `{ success, data }` envelope.\n *\n * `Response.json()` is typed `any`, so the rows used to reach\n * `mapDocSearchResults` through an `as DocSearchResult[]` that asserted a\n * shape nothing had checked. `path` and `name` are the two that bite: the\n * mapper uses `path` as the row's React `id` and `name` as its title, so a row\n * missing either renders as a blank, key-colliding entry in the dropdown.\n * Such rows are dropped; the rest of the result set still lists.\n *\n * Returns null when the envelope itself is not a successful result set.\n */\nfunction toDocSearchResults(payload: unknown): DocSearchResult[] | null {\n if (!isRecord(payload) || !payload.success || !isUnknownArray(payload.data)) return null;\n\n const rows: DocSearchResult[] = [];\n for (const entry of payload.data) {\n if (!isRecord(entry)) continue;\n const path = readString(entry, 'path');\n const name = readString(entry, 'name');\n const type = entry.type;\n if (path === undefined || name === undefined) continue;\n\n const targetPlatform = entry.targetPlatform;\n rows.push({\n path,\n name,\n type: type === 'folder' ? 'folder' : 'file',\n snippet: readString(entry, 'snippet') ?? '',\n matchType: entry.matchType === 'title' ? 'title' : 'content',\n documentType: readString(entry, 'documentType'),\n externalUrl: readString(entry, 'externalUrl'),\n targetPlatform: typeof targetPlatform === 'string' ? targetPlatform : null,\n sourceRepo: readString(entry, 'sourceRepo'),\n entityId: readString(entry, 'entityId'),\n });\n }\n return rows;\n}\n\nexport function useDocSearch(config: UseDocSearchConfig) {\n const { source, baseRoute, onNavigate, tableIds, onInPageSwap, searchEndpoint, composeContentUrl } = config;\n const tableIdsKey = tableIds && tableIds.length > 0 ? tableIds.join(',') : '';\n\n const router = useRouter();\n // Optional chat-runtime read — when present and mode='embed' the\n // search-result row click short-circuits to a new-tab open against\n // the absolutized URL. Null/host preserves today's behavior.\n // Also used as the proxy-prefix fallback for `searchEndpoint`, matching\n // how tickets resolves `findTicketUrl`.\n const runtime = useChatRuntime();\n const resolvedSearchEndpoint = searchEndpoint ?? runtime?.endpoints.docsSearchUrl ?? '/api/docs/search';\n // Content-href seam, same prop → runtime → none chain. Threading it into\n // `resolveSearchResultAction` is what makes the dropdown obey the host's\n // route map; without it the row navigates to the RAG's canonical hub URL\n // even when the host serves that content in-app under a different path.\n const resolvedComposeContentUrl = composeContentUrl ?? runtime?.composeContentUrl;\n\n const [query, setQuery] = useState('');\n const [results, setResults] = useState<SearchResult[]>(NO_RESULTS);\n const [isFetching, setIsFetching] = useState(false);\n const debouncedQuery = useDebounce(query, 300);\n\n // Falling below the two-character floor empties the dropdown. Adjusted while\n // rendering (React's documented pattern for a reset the render already has\n // the answer to) rather than from an effect, which committed one more frame\n // of the previous matches under a query that no longer searches for them.\n const searchable = debouncedQuery.trim().length >= 2;\n const [wasSearchable, setWasSearchable] = useState(searchable);\n if (wasSearchable !== searchable) {\n setWasSearchable(searchable);\n if (!searchable) {\n setResults(NO_RESULTS);\n setIsFetching(false);\n }\n }\n\n useEffect(() => {\n if (!searchable) return undefined;\n\n let cancelled = false;\n\n async function fetchResults() {\n setIsFetching(true);\n try {\n const params = new URLSearchParams({\n q: debouncedQuery,\n source,\n limit: '10',\n });\n if (tableIdsKey) params.set('tableIds', tableIdsKey);\n\n const response = await contentFetch(`${resolvedSearchEndpoint}?${params.toString()}`);\n if (!response.ok) {\n throw new Error(`Search request failed: ${response.status}`);\n }\n\n const json: unknown = await response.json();\n\n if (!cancelled) {\n // An envelope that is not a successful result set now clears the\n // dropdown instead of leaving it untouched. Previously a 200 that\n // carried `success: false` (or a non-array `data`) fell through\n // every branch, so the PREVIOUS query's matches stayed on screen\n // under the new query — the throw path already reset them.\n const rows = toDocSearchResults(json);\n setResults(rows ? mapDocSearchResults(rows) : NO_RESULTS);\n }\n } catch (error) {\n console.error('Doc search error:', error);\n if (!cancelled) {\n setResults(NO_RESULTS);\n }\n } finally {\n if (!cancelled) {\n setIsFetching(false);\n }\n }\n }\n\n // Never rejects — try/catch/finally, every write gated on `cancelled`.\n void fetchResults();\n\n return () => {\n cancelled = true;\n };\n }, [debouncedQuery, searchable, source, tableIdsKey, resolvedSearchEndpoint]);\n\n // Derived loading state — single source of truth for \"should the\n // dropdown show 'Loading...' instead of 'No results found'\":\n const isLoading = query.trim().length >= 2 && (query !== debouncedQuery || isFetching);\n\n // Track whether dropdown should stay open (external link opened in new tab).\n const [keepOpen, setKeepOpen] = useState(false);\n\n const handleResultSelect = useCallback(\n (\n result: SearchResult,\n modifiers?: {\n metaKey?: boolean;\n ctrlKey?: boolean;\n shiftKey?: boolean;\n altKey?: boolean;\n button?: number;\n },\n ) => {\n const action = resolveSearchResultAction(result, source, runtime?.navigation.mode, resolvedComposeContentUrl);\n // Modifier / non-primary mouse click → force new tab regardless of\n // same-tab/new-tab decision. The dropdown row is a `<div>`, not an\n // `<a target=\"_blank\">`, so the browser doesn't background-tab\n // natively on cmd-click. Honor it explicitly here for parity with\n // the anchor-based surfaces (cards, chips, related-content). Plain\n // Enter from the keyboard passes `modifiers === undefined`.\n const wantsNewTab =\n modifiers &&\n (modifiers.metaKey ||\n modifiers.ctrlKey ||\n modifiers.shiftKey ||\n modifiers.altKey ||\n (typeof modifiers.button === 'number' && modifiers.button !== 0));\n switch (action.kind) {\n case 'navigate-same-tab': {\n if (wantsNewTab) {\n setKeepOpen(true);\n window.open(action.href, '_blank', NEW_TAB_FEATURES);\n return;\n }\n // Same-origin click:\n // 1. If the href is under the current doc-tree's baseRoute AND\n // an `onInPageSwap` callback is wired AND returns true →\n // consider in-page swap handled.\n // 2. Otherwise → embed-shim `router.push()` (soft RSC nav on\n // Next.js hosts, window.location.assign on bare hosts).\n setKeepOpen(false);\n const path =\n baseRoute && action.href.startsWith(`${baseRoute}/`) ? action.href.slice(baseRoute.length + 1) : null;\n if (path && onInPageSwap?.(path)) return;\n router.push(stripSameOriginToPath(action.href));\n return;\n }\n case 'navigate-new-tab':\n // Cross-origin (e.g. clicking a flamingo.run release from\n // product-hub) — open in a new tab. Keep dropdown open so the\n // user can pick another result without re-searching.\n setKeepOpen(true);\n // Embed-mode short-circuit — the row was clicked while the panel is\n // hosted inside an embedding app, so absolutize against the\n // embedder-supplied content origin and honor its `openExternal`\n // override. Not the only path an embed host takes: a composed\n // RELATIVE href resolves to `navigate-same-tab` above and never\n // reaches `decideNewTab`, which is what lets an embedder keep its\n // own in-app routes in-app.\n if (runtime?.navigation.mode === 'embed') {\n const targetPlatform = (result.metadata?.targetPlatform as string | null | undefined) ?? null;\n resolveExternalNavigation({\n href: action.href,\n targetPlatform,\n runtime,\n }).open();\n return;\n }\n window.open(action.href, '_blank', NEW_TAB_FEATURES);\n return;\n case 'ask-ai':\n // Row is searchable-but-not-openable (cap_table positions,\n // financial-kpi snapshots, anything backed by\n // `resolveUrl: () => null`). Dispatch a CustomEvent that\n // GlobalAskAI listens for — opens chat + drills via\n // `entityIdFilter` (primary-key only, same as inline-card Ask).\n setKeepOpen(false);\n window.dispatchEvent(new CustomEvent('ask-ai:open-with-ref', { detail: action.detail }));\n return;\n case 'route':\n // Final fallback: legacy navigation by path. Hits when a row\n // has neither URL nor pk metadata — a mapper/API regression.\n setKeepOpen(false);\n onNavigate(action.path);\n return;\n case 'noop':\n return;\n }\n },\n [onNavigate, source, baseRoute, router, onInPageSwap, runtime, resolvedComposeContentUrl],\n );\n\n // Reset keepOpen when query changes. Adjusted while rendering, not from an\n // effect: the dropdown's open state is read from this in the same render the\n // keystroke produced, so an effect held it pinned open for one extra frame.\n const [keepOpenForQuery, setKeepOpenForQuery] = useState(query);\n if (keepOpenForQuery !== query) {\n setKeepOpenForQuery(query);\n setKeepOpen(false);\n }\n\n return {\n query,\n setQuery,\n results,\n isLoading,\n handleResultSelect,\n keepDropdownOpen: keepOpen,\n };\n}\n"]}
|
|
@@ -9,7 +9,7 @@ var _chunkLCMT2GZKcjs = require('./chunk-LCMT2GZK.cjs');
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
var
|
|
12
|
+
var _chunkMIZU2OTOcjs = require('./chunk-MIZU2OTO.cjs');
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
|
|
@@ -36,7 +36,7 @@ var _chunk6TNOVWO3cjs = require('./chunk-6TNOVWO3.cjs');
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
var
|
|
39
|
+
var _chunkI27PWVWEcjs = require('./chunk-I27PWVWE.cjs');
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
|
|
@@ -482,7 +482,7 @@ function useLocalStorage(key, initialValue, options) {
|
|
|
482
482
|
});
|
|
483
483
|
const validate = _optionalChain([options, 'optionalAccess', _17 => _17.validate]);
|
|
484
484
|
const storage = _react.useMemo.call(void 0,
|
|
485
|
-
() =>
|
|
485
|
+
() => _chunkI27PWVWEcjs.createLocalStorageAdapter.call(void 0, { key, validate, logTag: `[useLocalStorage:${key}]` }),
|
|
486
486
|
[key, validate]
|
|
487
487
|
);
|
|
488
488
|
const [storedValue, setStoredValue] = _react.useState.call(void 0, () => _nullishCoalesce(storage.load(), () => ( initialValue)));
|
|
@@ -903,7 +903,7 @@ var _overlays = require('@react-aria/overlays');
|
|
|
903
903
|
var _lucidereact = require('lucide-react');
|
|
904
904
|
var _jsxruntime = require('react/jsx-runtime');
|
|
905
905
|
function renderPlatformIcon(platformName, className) {
|
|
906
|
-
const tinted = _chunkX7CEWD6Vcjs.cn.call(void 0, className,
|
|
906
|
+
const tinted = _chunkX7CEWD6Vcjs.cn.call(void 0, className, _chunkI27PWVWEcjs.getPlatformBrandClasses.call(void 0, platformName).accentText);
|
|
907
907
|
switch (platformName) {
|
|
908
908
|
case "openframe":
|
|
909
909
|
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
@@ -971,7 +971,7 @@ function getPlatformIconComponent(platformName, className = "h-6 w-6") {
|
|
|
971
971
|
return renderPlatformIcon(platformName, className);
|
|
972
972
|
}
|
|
973
973
|
function getPlatformColorClass(platformName) {
|
|
974
|
-
return
|
|
974
|
+
return _chunkI27PWVWEcjs.getPlatformBrandClasses.call(void 0, platformName).accentBg;
|
|
975
975
|
}
|
|
976
976
|
|
|
977
977
|
// src/hooks/platform/use-platform-config.ts
|
|
@@ -1410,7 +1410,7 @@ function useContactSubmission(options = {}) {
|
|
|
1410
1410
|
if (isSubmitting) return;
|
|
1411
1411
|
setIsSubmitting(true);
|
|
1412
1412
|
try {
|
|
1413
|
-
const response = await
|
|
1413
|
+
const response = await _chunkMIZU2OTOcjs.contentFetch.call(void 0, contactUrl, {
|
|
1414
1414
|
method: "POST",
|
|
1415
1415
|
headers: { "Content-Type": "application/json" },
|
|
1416
1416
|
body: JSON.stringify({
|
|
@@ -2105,7 +2105,7 @@ function variablesToUrlParams(variables, schema) {
|
|
|
2105
2105
|
const params = new URLSearchParams();
|
|
2106
2106
|
for (const [paramName, paramConfig] of Object.entries(schema)) {
|
|
2107
2107
|
const value = getNestedValue(variables, paramConfig.graphqlPath);
|
|
2108
|
-
if (!
|
|
2108
|
+
if (!_chunkMIZU2OTOcjs.shouldIncludeInUrl.call(void 0, value, paramConfig)) {
|
|
2109
2109
|
continue;
|
|
2110
2110
|
}
|
|
2111
2111
|
if (Array.isArray(value)) {
|
|
@@ -2134,7 +2134,7 @@ function coerceScalar(value, type) {
|
|
|
2134
2134
|
return isNaN(num) ? null : num;
|
|
2135
2135
|
}
|
|
2136
2136
|
case "int":
|
|
2137
|
-
return
|
|
2137
|
+
return _chunkMIZU2OTOcjs.positiveInt.call(void 0, value, null);
|
|
2138
2138
|
case "boolean":
|
|
2139
2139
|
return value === "true" || value === "1";
|
|
2140
2140
|
case "string":
|
|
@@ -2440,7 +2440,7 @@ function useApiParams(schema, options = {}) {
|
|
|
2440
2440
|
return flattened;
|
|
2441
2441
|
}, [stableSchema]);
|
|
2442
2442
|
const rawParams = _react.useMemo.call(void 0, () => {
|
|
2443
|
-
const result =
|
|
2443
|
+
const result = _chunkMIZU2OTOcjs.parseSchemaParams.call(void 0, stableSchema, new URLSearchParams(searchString), {
|
|
2444
2444
|
absent: options.absent === "null" ? "null" : "undefined"
|
|
2445
2445
|
});
|
|
2446
2446
|
if (debug) {
|
|
@@ -2456,14 +2456,14 @@ function useApiParams(schema, options = {}) {
|
|
|
2456
2456
|
}
|
|
2457
2457
|
const params = stableParams;
|
|
2458
2458
|
const addParamToSearchParams = _react.useCallback.call(void 0, (searchParams, key, value) => {
|
|
2459
|
-
for (const [k, v] of
|
|
2459
|
+
for (const [k, v] of _chunkMIZU2OTOcjs.createSearchParams.call(void 0, { [key]: value })) searchParams.append(k, v);
|
|
2460
2460
|
}, []);
|
|
2461
2461
|
const urlSearchParams = _react.useMemo.call(void 0, () => {
|
|
2462
2462
|
const newParams = new URLSearchParams();
|
|
2463
2463
|
for (const key of Object.keys(stableSchema)) {
|
|
2464
2464
|
const value = params[key];
|
|
2465
2465
|
const paramConfig = flattenedSchema[key];
|
|
2466
|
-
if (!
|
|
2466
|
+
if (!_chunkMIZU2OTOcjs.shouldIncludeInUrl.call(void 0, value, paramConfig)) {
|
|
2467
2467
|
continue;
|
|
2468
2468
|
}
|
|
2469
2469
|
addParamToSearchParams(newParams, key, value);
|
|
@@ -2520,7 +2520,7 @@ function useApiParams(schema, options = {}) {
|
|
|
2520
2520
|
return;
|
|
2521
2521
|
}
|
|
2522
2522
|
const newParams = new URLSearchParams();
|
|
2523
|
-
if (
|
|
2523
|
+
if (_chunkMIZU2OTOcjs.shouldIncludeInUrl.call(void 0, value, config)) {
|
|
2524
2524
|
addParamToSearchParams(newParams, key, value);
|
|
2525
2525
|
updateUrl(newParams);
|
|
2526
2526
|
} else {
|
|
@@ -2539,7 +2539,7 @@ function useApiParams(schema, options = {}) {
|
|
|
2539
2539
|
console.warn(`[useApiParams] Unknown parameter: ${key}`);
|
|
2540
2540
|
continue;
|
|
2541
2541
|
}
|
|
2542
|
-
if (
|
|
2542
|
+
if (_chunkMIZU2OTOcjs.shouldIncludeInUrl.call(void 0, value, flattenedSchema[key])) {
|
|
2543
2543
|
addParamToSearchParams(newParams, key, value);
|
|
2544
2544
|
} else {
|
|
2545
2545
|
keysToRemove.push(key);
|
|
@@ -2564,7 +2564,7 @@ function useApiParams(schema, options = {}) {
|
|
|
2564
2564
|
}, [router, debug]);
|
|
2565
2565
|
const pendingParams = _react.useMemo.call(void 0, () => {
|
|
2566
2566
|
if (pendingTail === void 0 || pendingTail === searchString) return params;
|
|
2567
|
-
return
|
|
2567
|
+
return _chunkMIZU2OTOcjs.parseSchemaParams.call(void 0, stableSchema, new URLSearchParams(pendingTail), {
|
|
2568
2568
|
absent: options.absent === "null" ? "null" : "undefined"
|
|
2569
2569
|
});
|
|
2570
2570
|
}, [pendingTail, searchString, params, stableSchema, options.absent]);
|
|
@@ -3316,7 +3316,7 @@ function useFormRescue({ form, fieldNames, onRestore, getSignals }) {
|
|
|
3316
3316
|
...resumeTokenRef.current ? { resume_token: resumeTokenRef.current } : {}
|
|
3317
3317
|
};
|
|
3318
3318
|
try {
|
|
3319
|
-
void
|
|
3319
|
+
void _chunkMIZU2OTOcjs.contentFetch.call(void 0, draftsUrl, {
|
|
3320
3320
|
method: "POST",
|
|
3321
3321
|
headers: { "Content-Type": "application/json" },
|
|
3322
3322
|
body: JSON.stringify({ ...body, ..._nullishCoalesce(_optionalChain([getSignalsRef, 'access', _68 => _68.current, 'optionalCall', _69 => _69()]), () => ( {})) }),
|
|
@@ -3345,7 +3345,7 @@ function useFormRescue({ form, fieldNames, onRestore, getSignals }) {
|
|
|
3345
3345
|
if (Object.keys(local.values).length) _optionalChain([onRestoreRef, 'access', _70 => _70.current, 'optionalCall', _71 => _71(local.values)]);
|
|
3346
3346
|
};
|
|
3347
3347
|
if (isResumeToken(token) && draftsUrl) {
|
|
3348
|
-
|
|
3348
|
+
_chunkMIZU2OTOcjs.contentFetch.call(void 0, `${draftsUrl}/resume/${encodeURIComponent(token)}`).then(async (res) => res.ok ? await res.json() : null).then((data) => {
|
|
3349
3349
|
if (cancelled) return;
|
|
3350
3350
|
if (!data || data.form_id !== formId) {
|
|
3351
3351
|
restoreLocal();
|
|
@@ -3597,4 +3597,4 @@ function useRescuedForm(form, { rescue, fields, getSignals, fieldPath }) {
|
|
|
3597
3597
|
|
|
3598
3598
|
|
|
3599
3599
|
exports.useAutoLimitTags = useAutoLimitTags; exports.useKeyboardCollisionPadding = useKeyboardCollisionPadding; exports.useDeferredError = useDeferredError; exports.DRAG_AND_DROP_MEDIA_QUERY = DRAG_AND_DROP_MEDIA_QUERY; exports.useDragAndDropEnabled = useDragAndDropEnabled; exports.useFocusTrap = useFocusTrap; exports.useHorizontalScrollbar = useHorizontalScrollbar; exports.useLocalStorage = useLocalStorage; exports.useMemoizedCallback = useMemoizedCallback; exports.useNotificationPermission = useNotificationPermission; exports.useOnboardingState = useOnboardingState; exports.useSearch = useSearch; exports.useTablePagination = useTablePagination; exports.useThrottle = useThrottle; exports.useWindowSize = useWindowSize; exports.usePreventScroll = _overlays.usePreventScroll; exports.platformIcons = platformIcons; exports.transformPlatformConfigsToOptions = transformPlatformConfigsToOptions; exports.getPlatformIcon = getPlatformIcon; exports.getSmallPlatformIcon = getSmallPlatformIcon; exports.getPlatformIconComponent = getPlatformIconComponent; exports.usePlatformConfig = usePlatformConfig; exports.usePlatformByValue = usePlatformByValue; exports.useValidatePlatform = useValidatePlatform; exports.dotColorByVariant = dotColorByVariant; exports.progressColorByVariant = progressColorByVariant; exports.ToastCard = ToastCard; exports.CommandApprovalToast = CommandApprovalToast; exports.Toaster = Toaster; exports.showToast = showToast; exports.showCommandApprovalToast = showCommandApprovalToast; exports.toast = toast; exports.useToast = useToast; exports.useContactSubmission = useContactSubmission; exports.useCopyToClipboard = useCopyToClipboard; exports.configureBatchImageFetch = configureBatchImageFetch; exports.batchFetchAuthenticatedImages = batchFetchAuthenticatedImages; exports.useBatchImages = useBatchImages; exports.configureAuthenticatedImage = configureAuthenticatedImage; exports.useAuthenticatedImage = useAuthenticatedImage; exports.flattenQueryVariables = flattenQueryVariables; exports.mergeDefaults = mergeDefaults; exports.validateSchema = validateSchema; exports.getArrayParams = getArrayParams; exports.getRequiredParams = getRequiredParams; exports.extractVariablesFromQuery = extractVariablesFromQuery; exports.isScalarType = isScalarType; exports.isInputObjectType = isInputObjectType; exports.GraphQLIntrospector = GraphQLIntrospector; exports.introspector = introspector; exports.urlParamsToVariables = urlParamsToVariables; exports.variablesToUrlParams = variablesToUrlParams; exports.coerceValue = coerceValue; exports.setNestedValue = setNestedValue; exports.getNestedValue = getNestedValue; exports.mergeVariables = mergeVariables; exports.clearParams = clearParams; exports.validateVariables = validateVariables; exports.useQueryParams = useQueryParams; exports.useApiParams = useApiParams; exports.useCursorPaginationState = useCursorPaginationState; exports.useNatsClient = useNatsClient; exports.validateAccessCode = validateAccessCode; exports.consumeAccessCode = consumeAccessCode; exports.validateAndConsumeAccessCode = validateAndConsumeAccessCode; exports.useAccessCodeIntegration = useAccessCodeIntegration; exports.toGoogleSheetsEmbedUrl = toGoogleSheetsEmbedUrl; exports.toGoogleSheetsOriginalUrl = toGoogleSheetsOriginalUrl; exports.toFigmaEmbedUrl = toFigmaEmbedUrl; exports.isFigmaSlidesUrl = isFigmaSlidesUrl; exports.toFigmaOriginalUrl = toFigmaOriginalUrl; exports.toClaudeMirrorPath = toClaudeMirrorPath; exports.toClaudeEmbedUrl = toClaudeEmbedUrl; exports.useClaudeMirrorSrc = useClaudeMirrorSrc; exports.useScrollToHash = useScrollToHash; exports.HONEYPOT_FIELD = HONEYPOT_FIELD; exports.ELAPSED_MS_FIELD = ELAPSED_MS_FIELD; exports.DEFAULT_MIN_FILL_MS = DEFAULT_MIN_FILL_MS; exports.HUMANITY_SIGNAL_KEYS = HUMANITY_SIGNAL_KEYS; exports.isHumanitySignalKey = isHumanitySignalKey; exports.extractHumanitySignals = extractHumanitySignals; exports.findHoneypotCopySource = findHoneypotCopySource; exports.evaluateHumanitySignals = evaluateHumanitySignals; exports.splitCsvEnv = splitCsvEnv; exports.useHumanitySignals = useHumanitySignals; exports.defineRescueForm = defineRescueForm; exports.getRescueForm = getRescueForm; exports.listRescueForms = listRescueForms; exports.RESCUE_FORMS = RESCUE_FORMS; exports.FORM_RESCUE_VALUE_FIELDS = FORM_RESCUE_VALUE_FIELDS; exports.isExcludedRescueField = isExcludedRescueField; exports.FORM_RESCUE_MAX_VALUE_CHARS = FORM_RESCUE_MAX_VALUE_CHARS; exports.FORM_RESCUE_DEBOUNCE_MS = FORM_RESCUE_DEBOUNCE_MS; exports.FORM_RESCUE_MAX_FIELDS = FORM_RESCUE_MAX_FIELDS; exports.FORM_RESCUE_ATTEMPT_FIELD = FORM_RESCUE_ATTEMPT_FIELD; exports.FORM_RESCUE_RESUME_FIELD = FORM_RESCUE_RESUME_FIELD; exports.FORM_RESCUE_SUBMIT_KEYS = FORM_RESCUE_SUBMIT_KEYS; exports.FORM_RESCUE_RESUME_PARAM = FORM_RESCUE_RESUME_PARAM; exports.isResumeToken = isResumeToken; exports.FORM_DRAFT_STATUSES = FORM_DRAFT_STATUSES; exports.FORM_RESCUE_SKIP_REASONS = FORM_RESCUE_SKIP_REASONS; exports.isRescueEmail = isRescueEmail; exports.isFormAttemptId = isFormAttemptId; exports.sanitizeRescueValues = sanitizeRescueValues; exports.filledRescueFields = filledRescueFields; exports.rescueCompletionPct = rescueCompletionPct; exports.sanitizeRescueFieldName = sanitizeRescueFieldName; exports.parseFormDraftSaveRequest = parseFormDraftSaveRequest; exports.FORM_RESCUE_EVENTS = FORM_RESCUE_EVENTS; exports.captureFormRescueEvent = captureFormRescueEvent; exports.completeFormRescue = completeFormRescue; exports.useFormRescue = useFormRescue; exports.useRescuedForm = useRescuedForm;
|
|
3600
|
-
//# sourceMappingURL=chunk-
|
|
3600
|
+
//# sourceMappingURL=chunk-LNKLB7GM.cjs.map
|