main
vue 88 lines 2.47 KB
Raw
1 <template>
2 <div ref="container" />
3 </template>
4
5 <script setup lang="ts">
6 // Vue wrapper around `<AppDetailDrawer>` (a React component from
7 // `@shuffleio/shuffle-mcps`). Same manual `react-dom/client` mount
8 // pattern as ShuffleMCPEmbed / TryMcpEmbed.
9 //
10 // AppDetailDrawer is the all-in-one panel for a single app: header
11 // + auth card (inline form for API-key apps, redirect handoff for
12 // OAuth apps) + MCP chat + Singul actions playground. We use it after
13 // picking an app from <ShuffleMCP preventDefault> so the user lands
14 // on a CoPilot-orchestrated drawer instead of a top-level redirect.
15
16 import type { Root } from "react-dom/client"
17 import { API_CONFIG, AppDetailDrawer } from "@shuffleio/shuffle-mcps"
18 import { storeToRefs } from "pinia"
19 import { createElement } from "react"
20 import { createRoot } from "react-dom/client"
21 import { onBeforeUnmount, onMounted, ref, watch } from "vue"
22 import { useThemeStore } from "@/stores/theme"
23 import { fetchShuffleConnectorCredentials } from "@/utils/shuffle/shuffleConnectorCredentials"
24 import { MuiProvider } from "@/utils/shuffle/shuffleMuiTheme"
25
26 interface Props {
27 appName: string | null
28 width?: number
29 anchor?: "left" | "right"
30 }
31
32 const props = withDefaults(defineProps<Props>(), {
33 width: 720,
34 anchor: "right"
35 })
36
37 const emit = defineEmits<{
38 (e: "refresh"): void
39 }>()
40
41 const show = defineModel<boolean>("show", { required: true, default: false })
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 function render() {
50 if (!root) return
51 root.render(
52 createElement(
53 MuiProvider as never,
54 { isDark: isThemeDark.value },
55 createElement(AppDetailDrawer as never, {
56 open: show.value,
57 onClose: () => (show.value = false),
58 appName: props.appName,
59 anchor: props.anchor,
60 width: props.width,
61 onRefresh: () => emit("refresh")
62 })
63 )
64 )
65 }
66
67 watch([show, () => props.appName, () => props.width, () => props.anchor, isThemeDark], () => render())
68
69 onBeforeUnmount(() => {
70 if (root) {
71 root.unmount()
72 root = null
73 }
74 })
75
76 onMounted(async () => {
77 if (!container.value) return
78 root = createRoot(container.value)
79 // Same gating as the other embeds — wait for connector creds before
80 // the first render so the drawer's internal fetches go through the
81 // proxy with the right Bearer token.
82 const creds = await fetchShuffleConnectorCredentials()
83 if (creds?.api_key) {
84 API_CONFIG.setApiKey(creds.api_key)
85 }
86 render()
87 })
88 </script>