main
vue 118 lines 3.74 KB
Raw
1 <template>
2 <div ref="container" class="w-full" />
3 </template>
4
5 <script setup lang="ts">
6 // Vue wrapper around `<TryMcpSection>` (a React component from
7 // `@shuffleio/shuffle-mcps`). Same manual react-dom/client mount
8 // strategy as ShuffleMCPEmbed.
9 //
10 // Difference from ShuffleMCPEmbed: TryMcpSection requires a *resolved*
11 // app id (Algolia objectID), not just a name. The package's
12 // `useAppLookup(name)` hook does the resolution, so we wrap both in a
13 // tiny inline React component and let the hook drive what gets rendered.
14 //
15 // Auth: TryMcpSection reads from the package's global API_CONFIG rather
16 // than props. We call `API_CONFIG.setApiKey(authToken)` once on mount —
17 // good enough for a single-customer prototype. Multi-customer scoping
18 // (per-org headers via `getAuthHeader(orgId)`) is a follow-up.
19
20 import type { FC } from "react"
21 import type { Root } from "react-dom/client"
22 import { API_CONFIG, TryMcpSection, useAppLookup } from "@shuffleio/shuffle-mcps"
23 import { storeToRefs } from "pinia"
24 import { createElement } from "react"
25 import { createRoot } from "react-dom/client"
26 import { onBeforeUnmount, onMounted, ref, watch } from "vue"
27 import { useThemeStore } from "@/stores/theme"
28 import { fetchShuffleConnectorCredentials } from "@/utils/shuffle/shuffleConnectorCredentials"
29 import { MuiProvider } from "@/utils/shuffle/shuffleMuiTheme"
30
31 interface Props {
32 appName: string
33 /**
34 * Per-customer Shuffle org auth token. Forwarded to TryMcpSection's
35 * underlying chat so per-customer scoping holds even when the
36 * deployment-wide connector API key is also set.
37 */
38 authToken: string
39 }
40
41 const props = defineProps<Props>()
42
43 const container = ref<HTMLElement | null>(null)
44 let root: Root | null = null
45
46 const themeStore = useThemeStore()
47 const { isThemeDark } = storeToRefs(themeStore)
48
49 // Inline React component that resolves appName → algoliaId via the
50 // package's hook, then mounts TryMcpSection. Lives inline (not a
51 // separate .tsx file) because it's tightly coupled to this wrapper and
52 // has no other consumers.
53 const TryMcpInline: FC<{ appName: string }> = ({ appName }) => {
54 const { displayName, image, categories, algoliaId, loading } = useAppLookup(appName)
55 if (loading) {
56 return createElement("div", { className: "text-tertiary text-center text-sm" }, "Resolving app…")
57 }
58 if (!algoliaId) {
59 return createElement(
60 "div",
61 { className: "text-error p-4 text-sm" },
62 `Could not resolve "${appName}" against Shuffle's catalog.`
63 )
64 }
65 return createElement(TryMcpSection as never, {
66 appName: displayName,
67 appIcon: image,
68 appId: algoliaId,
69 categories
70 })
71 }
72
73 function render() {
74 if (!root) return
75 root.render(
76 createElement(
77 MuiProvider as never,
78 { isDark: isThemeDark.value },
79 createElement(TryMcpInline, { appName: props.appName })
80 )
81 )
82 }
83
84 onMounted(async () => {
85 if (!container.value) return
86 root = createRoot(container.value)
87 // Resolve creds before the first render — same reason as
88 // ShuffleMCPEmbed. The package's hooks (useAppLookup, the chat fetch)
89 // fire on mount; rendering them with the wrong API key burns
90 // unauthenticated requests to shuffler.io.
91 const creds = await fetchShuffleConnectorCredentials()
92 API_CONFIG.setApiKey(creds?.api_key ?? props.authToken)
93 render()
94 })
95
96 watch(
97 () => [props.appName, props.authToken] as const,
98 () => {
99 // Don't clobber the connector key if it's already loaded —
100 // fetchShuffleConnectorCredentials caches across the session.
101 fetchShuffleConnectorCredentials().then(creds => {
102 API_CONFIG.setApiKey(creds?.api_key ?? props.authToken)
103 render()
104 })
105 }
106 )
107
108 // Re-render when the host theme toggles so the MUI provider swaps to
109 // the matching light/dark palette.
110 watch(isThemeDark, () => render())
111
112 onBeforeUnmount(() => {
113 if (root) {
114 root.unmount()
115 root = null
116 }
117 })
118 </script>