@app-connect/core 1.7.18 → 1.7.19
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/connector/proxy/index.js +2 -1
- package/handlers/log.js +181 -10
- package/handlers/plugin.js +27 -0
- package/handlers/user.js +31 -2
- package/index.js +99 -22
- package/lib/authSession.js +21 -12
- package/lib/callLogComposer.js +1 -1
- package/lib/debugTracer.js +20 -2
- package/lib/util.js +21 -4
- package/mcp/README.md +392 -0
- package/mcp/mcpHandler.js +293 -82
- package/mcp/tools/checkAuthStatus.js +27 -34
- package/mcp/tools/createCallLog.js +13 -9
- package/mcp/tools/createContact.js +2 -6
- package/mcp/tools/doAuth.js +27 -157
- package/mcp/tools/findContactByName.js +6 -9
- package/mcp/tools/findContactByPhone.js +2 -6
- package/mcp/tools/getGoogleFilePicker.js +5 -9
- package/mcp/tools/getHelp.js +2 -3
- package/mcp/tools/getPublicConnectors.js +41 -28
- package/mcp/tools/index.js +11 -36
- package/mcp/tools/logout.js +5 -10
- package/mcp/tools/rcGetCallLogs.js +3 -20
- package/mcp/ui/App/App.tsx +361 -0
- package/mcp/ui/App/components/AuthInfoForm.tsx +113 -0
- package/mcp/ui/App/components/AuthSuccess.tsx +22 -0
- package/mcp/ui/App/components/ConnectorList.tsx +82 -0
- package/mcp/ui/App/components/DebugPanel.tsx +43 -0
- package/mcp/ui/App/components/OAuthConnect.tsx +270 -0
- package/mcp/ui/App/lib/callTool.ts +130 -0
- package/mcp/ui/App/lib/debugLog.ts +41 -0
- package/mcp/ui/App/lib/developerPortal.ts +111 -0
- package/mcp/ui/App/main.css +6 -0
- package/mcp/ui/App/root.tsx +13 -0
- package/mcp/ui/dist/index.html +53 -0
- package/mcp/ui/index.html +13 -0
- package/mcp/ui/package-lock.json +6356 -0
- package/mcp/ui/package.json +25 -0
- package/mcp/ui/tsconfig.json +26 -0
- package/mcp/ui/vite.config.ts +16 -0
- package/models/llmSessionModel.js +14 -0
- package/package.json +72 -72
- package/releaseNotes.json +12 -0
- package/test/handlers/plugin.test.js +287 -0
- package/test/lib/util.test.js +379 -1
- package/test/mcp/tools/createCallLog.test.js +3 -3
- package/test/mcp/tools/doAuth.test.js +40 -303
- package/test/mcp/tools/findContactByName.test.js +3 -3
- package/test/mcp/tools/findContactByPhone.test.js +3 -3
- package/test/mcp/tools/getGoogleFilePicker.test.js +7 -7
- package/test/mcp/tools/getPublicConnectors.test.js +49 -70
- package/test/mcp/tools/logout.test.js +2 -2
- package/mcp/SupportedPlatforms.md +0 -12
- package/mcp/tools/collectAuthInfo.js +0 -91
- package/mcp/tools/setConnector.js +0 -69
- package/test/mcp/tools/collectAuthInfo.test.js +0 -234
- package/test/mcp/tools/setConnector.test.js +0 -177
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import { ConnectorList, type Connector } from './components/ConnectorList'
|
|
3
|
+
import { AuthInfoForm } from './components/AuthInfoForm'
|
|
4
|
+
import { OAuthConnect } from './components/OAuthConnect'
|
|
5
|
+
import { AuthSuccess } from './components/AuthSuccess'
|
|
6
|
+
import { setServerUrl } from './lib/callTool'
|
|
7
|
+
import { fetchConnectors, fetchManifest } from './lib/developerPortal'
|
|
8
|
+
import { dbg } from './lib/debugLog'
|
|
9
|
+
import { DebugPanel } from './components/DebugPanel'
|
|
10
|
+
|
|
11
|
+
// Initial structuredContent from getPublicConnectors — serverUrl, rcAccountId, rcExtensionId, openaiSessionId
|
|
12
|
+
interface ToolOutput {
|
|
13
|
+
serverUrl?: string
|
|
14
|
+
rcAccountId?: string | null
|
|
15
|
+
rcExtensionId?: string | null
|
|
16
|
+
openaiSessionId?: string | null
|
|
17
|
+
error?: boolean
|
|
18
|
+
errorMessage?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Step = 'loadingConnectors' | 'select' | 'loading' | 'authInfo' | 'oauth' | 'success' | 'error'
|
|
22
|
+
|
|
23
|
+
interface FlowState {
|
|
24
|
+
connectorManifest: any | null
|
|
25
|
+
connectorName: string | null
|
|
26
|
+
connectorDisplayName: string | null
|
|
27
|
+
hostname: string
|
|
28
|
+
userInfo: any | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const INITIAL_FLOW_STATE: FlowState = {
|
|
32
|
+
connectorManifest: null,
|
|
33
|
+
connectorName: null,
|
|
34
|
+
connectorDisplayName: null,
|
|
35
|
+
hostname: '',
|
|
36
|
+
userInfo: null,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract and apply serverUrl from tool output as early as possible.
|
|
41
|
+
* Must be called synchronously so it's set before any tool calls.
|
|
42
|
+
*/
|
|
43
|
+
function applyServerUrl(output: ToolOutput | null) {
|
|
44
|
+
dbg.info('applyServerUrl: received output keys:', output ? Object.keys(output).join(', ') : 'null');
|
|
45
|
+
dbg.info('applyServerUrl: serverUrl =', output?.serverUrl ?? '(none)');
|
|
46
|
+
if (output?.serverUrl) {
|
|
47
|
+
setServerUrl(output.serverUrl);
|
|
48
|
+
}
|
|
49
|
+
return output;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Hook to receive the initial tool output (serverUrl + rcAccountId).
|
|
54
|
+
*
|
|
55
|
+
* Listens via three mechanisms per the ChatGPT Apps SDK docs:
|
|
56
|
+
* 1. window.openai.toolOutput — synchronous read on mount
|
|
57
|
+
* 2. openai:set_globals event — ChatGPT pushes globals into the iframe
|
|
58
|
+
* 3. ui/notifications/tool-result postMessage — MCP Apps bridge notification
|
|
59
|
+
* 4. Polling window.openai.toolOutput — fallback for async population
|
|
60
|
+
*/
|
|
61
|
+
function useToolResult() {
|
|
62
|
+
const [toolResult, setToolResult] = useState<ToolOutput | null>(() => {
|
|
63
|
+
const openai = (window as any).openai
|
|
64
|
+
dbg.info('init: window.openai exists?', !!openai);
|
|
65
|
+
dbg.info('init: window.openai.toolOutput?', JSON.stringify(openai?.toolOutput)?.slice(0, 200) ?? 'null');
|
|
66
|
+
const output = openai?.toolOutput
|
|
67
|
+
if (output && Object.keys(output).length > 0) {
|
|
68
|
+
return applyServerUrl(output)
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (toolResult) return
|
|
75
|
+
|
|
76
|
+
// MCP Apps bridge: tool-result notification
|
|
77
|
+
const onMessage = (event: MessageEvent) => {
|
|
78
|
+
if (event.source !== window.parent) return
|
|
79
|
+
const message = event.data
|
|
80
|
+
if (!message || message.jsonrpc !== '2.0') return
|
|
81
|
+
if (message.method !== 'ui/notifications/tool-result') return
|
|
82
|
+
const payload = message.params?.structuredContent ?? message.params ?? null
|
|
83
|
+
if (payload) setToolResult(applyServerUrl(payload))
|
|
84
|
+
}
|
|
85
|
+
window.addEventListener('message', onMessage, { passive: true })
|
|
86
|
+
|
|
87
|
+
// ChatGPT extension: openai:set_globals event
|
|
88
|
+
const onSetGlobals = (event: Event) => {
|
|
89
|
+
const detail = (event as CustomEvent).detail
|
|
90
|
+
const output = detail?.globals?.toolOutput ?? (window as any).openai?.toolOutput
|
|
91
|
+
if (output && Object.keys(output).length > 0) {
|
|
92
|
+
setToolResult(applyServerUrl(output))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
window.addEventListener('openai:set_globals', onSetGlobals, { passive: true })
|
|
96
|
+
|
|
97
|
+
// Fallback: poll window.openai.toolOutput for async population
|
|
98
|
+
const interval = setInterval(() => {
|
|
99
|
+
const output = (window as any).openai?.toolOutput
|
|
100
|
+
if (output && Object.keys(output).length > 0) {
|
|
101
|
+
setToolResult(applyServerUrl(output))
|
|
102
|
+
clearInterval(interval)
|
|
103
|
+
}
|
|
104
|
+
}, 100)
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
window.removeEventListener('message', onMessage)
|
|
108
|
+
window.removeEventListener('openai:set_globals', onSetGlobals)
|
|
109
|
+
clearInterval(interval)
|
|
110
|
+
}
|
|
111
|
+
}, [toolResult])
|
|
112
|
+
|
|
113
|
+
return toolResult
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function App() {
|
|
117
|
+
const data = useToolResult()
|
|
118
|
+
const [step, setStep] = useState<Step>('loadingConnectors')
|
|
119
|
+
const [connectors, setConnectors] = useState<Connector[]>([])
|
|
120
|
+
const [flow, setFlow] = useState<FlowState>(INITIAL_FLOW_STATE)
|
|
121
|
+
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
|
122
|
+
|
|
123
|
+
// Fetch connector list from developer portal once serverUrl (and optionally rcAccountId) is known
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!data || data.error) return
|
|
126
|
+
|
|
127
|
+
async function loadConnectors() {
|
|
128
|
+
try {
|
|
129
|
+
dbg.info('loadConnectors: fetching with rcAccountId=', data!.rcAccountId ?? '(none)')
|
|
130
|
+
const list = await fetchConnectors(data!.rcAccountId)
|
|
131
|
+
setConnectors(list)
|
|
132
|
+
setStep('select')
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
dbg.error('loadConnectors failed:', err.message)
|
|
135
|
+
setStep('error')
|
|
136
|
+
setErrorMsg(err.message || 'Failed to load connectors')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
loadConnectors()
|
|
141
|
+
}, [data])
|
|
142
|
+
|
|
143
|
+
const resetToSelect = useCallback(() => {
|
|
144
|
+
setStep('select')
|
|
145
|
+
setFlow(INITIAL_FLOW_STATE)
|
|
146
|
+
setErrorMsg(null)
|
|
147
|
+
}, [])
|
|
148
|
+
|
|
149
|
+
const handleConnectorSelect = useCallback(async (connector: Connector) => {
|
|
150
|
+
dbg.info('connector selected:', JSON.stringify(connector))
|
|
151
|
+
setStep('loading')
|
|
152
|
+
setErrorMsg(null)
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const manifest = await fetchManifest(
|
|
156
|
+
connector.id,
|
|
157
|
+
connector.status === 'private',
|
|
158
|
+
data?.rcAccountId,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if (!manifest) {
|
|
162
|
+
setStep('error')
|
|
163
|
+
setErrorMsg('Failed to load connector configuration')
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const connectorName = connector.name
|
|
168
|
+
const platform = manifest?.platforms?.[connectorName]
|
|
169
|
+
|
|
170
|
+
if (!platform) {
|
|
171
|
+
setStep('error')
|
|
172
|
+
setErrorMsg('Invalid connector configuration')
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setFlow((prev) => ({
|
|
177
|
+
...prev,
|
|
178
|
+
connectorManifest: manifest,
|
|
179
|
+
connectorName,
|
|
180
|
+
connectorDisplayName: connector.displayName,
|
|
181
|
+
}))
|
|
182
|
+
|
|
183
|
+
const envType = platform.environment?.type
|
|
184
|
+
const authType = platform.auth?.type
|
|
185
|
+
|
|
186
|
+
// For oauth with dynamic/selectable environments, collect auth info first
|
|
187
|
+
if (authType === 'oauth' && (envType === 'dynamic' || envType === 'selectable')) {
|
|
188
|
+
setStep('authInfo')
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// For fixed environments, extract hostname from the fixed URL
|
|
193
|
+
if (envType === 'fixed' && platform.environment?.url) {
|
|
194
|
+
try {
|
|
195
|
+
const url = new URL(platform.environment.url)
|
|
196
|
+
setFlow((prev) => ({ ...prev, hostname: url.hostname }))
|
|
197
|
+
} catch {
|
|
198
|
+
// hostname stays empty
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setStep('oauth')
|
|
203
|
+
} catch (err: any) {
|
|
204
|
+
setStep('error')
|
|
205
|
+
setErrorMsg(err.message || 'Failed to load connector')
|
|
206
|
+
}
|
|
207
|
+
}, [data?.rcAccountId])
|
|
208
|
+
|
|
209
|
+
const handleAuthInfoSubmit = useCallback(
|
|
210
|
+
(value: { hostname?: string; selection?: string }) => {
|
|
211
|
+
try {
|
|
212
|
+
const platform = flow.connectorManifest?.platforms?.[flow.connectorName!]
|
|
213
|
+
const envType = platform?.environment?.type
|
|
214
|
+
let resolvedHostname = ''
|
|
215
|
+
|
|
216
|
+
if (envType === 'dynamic' && value.hostname) {
|
|
217
|
+
resolvedHostname = new URL(value.hostname).hostname
|
|
218
|
+
} else if (envType === 'selectable' && value.selection) {
|
|
219
|
+
const sel = platform.environment.selections?.find(
|
|
220
|
+
(s: { name: string; const: string }) => s.name === value.selection,
|
|
221
|
+
)
|
|
222
|
+
if (sel?.const) {
|
|
223
|
+
resolvedHostname = new URL(sel.const).hostname
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
setFlow((prev) => ({ ...prev, hostname: resolvedHostname }))
|
|
228
|
+
setStep('oauth')
|
|
229
|
+
} catch (err: any) {
|
|
230
|
+
setStep('error')
|
|
231
|
+
setErrorMsg(err.message || 'Failed to process environment selection')
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
[flow.connectorManifest, flow.connectorName],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
const handleAuthSuccess = useCallback(
|
|
238
|
+
(authData: { jwtToken?: string; userInfo?: any }) => {
|
|
239
|
+
setFlow((prev) => ({ ...prev, userInfo: authData.userInfo ?? null }))
|
|
240
|
+
setStep('success')
|
|
241
|
+
},
|
|
242
|
+
[],
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const handleAuthError = useCallback(
|
|
246
|
+
(error: string) => {
|
|
247
|
+
if (error === 'retry') {
|
|
248
|
+
setStep('oauth')
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
setStep('error')
|
|
252
|
+
setErrorMsg(error)
|
|
253
|
+
},
|
|
254
|
+
[],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
// Waiting for initial tool output from ChatGPT
|
|
258
|
+
if (!data) {
|
|
259
|
+
return (
|
|
260
|
+
<div className="flex items-center justify-center min-h-[150px] p-4">
|
|
261
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
|
|
262
|
+
</div>
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Error from initial tool call
|
|
267
|
+
if (data.error) {
|
|
268
|
+
return (
|
|
269
|
+
<div className="p-4">
|
|
270
|
+
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
|
|
271
|
+
<p className="text-red-700">{data.errorMessage || 'An error occurred'}</p>
|
|
272
|
+
</div>
|
|
273
|
+
<DebugPanel />
|
|
274
|
+
</div>
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div className="p-4">
|
|
280
|
+
{step === 'loadingConnectors' && (
|
|
281
|
+
<div className="flex items-center justify-center min-h-[150px]">
|
|
282
|
+
<div className="text-center space-y-3">
|
|
283
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
|
284
|
+
<p className="text-sm text-secondary">Loading connectors...</p>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{step === 'select' && (
|
|
290
|
+
<ConnectorList connectors={connectors} onSelect={handleConnectorSelect} />
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
{step === 'loading' && (
|
|
294
|
+
<div className="flex items-center justify-center min-h-[150px]">
|
|
295
|
+
<div className="text-center space-y-3">
|
|
296
|
+
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary mx-auto" />
|
|
297
|
+
<p className="text-sm text-secondary">Loading...</p>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{step === 'authInfo' && flow.connectorManifest && flow.connectorName && (
|
|
303
|
+
<AuthInfoForm
|
|
304
|
+
environmentType={
|
|
305
|
+
flow.connectorManifest.platforms[flow.connectorName].environment.type
|
|
306
|
+
}
|
|
307
|
+
urlIdentifier={
|
|
308
|
+
flow.connectorManifest.platforms[flow.connectorName].environment.urlIdentifier
|
|
309
|
+
}
|
|
310
|
+
instructions={
|
|
311
|
+
flow.connectorManifest.platforms[flow.connectorName].environment.instructions
|
|
312
|
+
}
|
|
313
|
+
selections={
|
|
314
|
+
flow.connectorManifest.platforms[flow.connectorName].environment.selections
|
|
315
|
+
}
|
|
316
|
+
connectorDisplayName={flow.connectorDisplayName!}
|
|
317
|
+
onSubmit={handleAuthInfoSubmit}
|
|
318
|
+
onBack={resetToSelect}
|
|
319
|
+
/>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{step === 'oauth' && flow.connectorManifest && flow.connectorName && (
|
|
323
|
+
<OAuthConnect
|
|
324
|
+
connectorManifest={flow.connectorManifest}
|
|
325
|
+
connectorName={flow.connectorName}
|
|
326
|
+
connectorDisplayName={flow.connectorDisplayName!}
|
|
327
|
+
hostname={flow.hostname}
|
|
328
|
+
openaiSessionId={data?.openaiSessionId ?? null}
|
|
329
|
+
rcExtensionId={data?.rcExtensionId ?? null}
|
|
330
|
+
onSuccess={handleAuthSuccess}
|
|
331
|
+
onError={handleAuthError}
|
|
332
|
+
onBack={resetToSelect}
|
|
333
|
+
/>
|
|
334
|
+
)}
|
|
335
|
+
|
|
336
|
+
{step === 'success' && (
|
|
337
|
+
<AuthSuccess
|
|
338
|
+
connectorDisplayName={flow.connectorDisplayName!}
|
|
339
|
+
userInfo={flow.userInfo ?? undefined}
|
|
340
|
+
/>
|
|
341
|
+
)}
|
|
342
|
+
|
|
343
|
+
{step === 'error' && (
|
|
344
|
+
<div className="w-full max-w-md">
|
|
345
|
+
<div className="rounded-lg border border-red-200 bg-red-50 p-4 mb-4">
|
|
346
|
+
<p className="text-red-700 text-sm font-semibold">Error</p>
|
|
347
|
+
<p className="text-red-700 text-sm mt-1">{errorMsg || 'An error occurred'}</p>
|
|
348
|
+
</div>
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
onClick={resetToSelect}
|
|
352
|
+
className="text-sm text-primary hover:underline cursor-pointer"
|
|
353
|
+
>
|
|
354
|
+
← Back to connector list
|
|
355
|
+
</button>
|
|
356
|
+
<DebugPanel />
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
)
|
|
361
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Button } from '@openai/apps-sdk-ui/components/Button'
|
|
3
|
+
import { Input } from '@openai/apps-sdk-ui/components/Input'
|
|
4
|
+
|
|
5
|
+
interface Selection {
|
|
6
|
+
name: string
|
|
7
|
+
const: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface AuthInfoFormProps {
|
|
11
|
+
environmentType: 'dynamic' | 'selectable'
|
|
12
|
+
urlIdentifier?: string
|
|
13
|
+
instructions?: string
|
|
14
|
+
selections?: Selection[]
|
|
15
|
+
connectorDisplayName: string
|
|
16
|
+
onSubmit: (value: { hostname?: string; selection?: string }) => void
|
|
17
|
+
onBack: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function AuthInfoForm({
|
|
21
|
+
environmentType,
|
|
22
|
+
urlIdentifier,
|
|
23
|
+
instructions,
|
|
24
|
+
selections,
|
|
25
|
+
connectorDisplayName,
|
|
26
|
+
onSubmit,
|
|
27
|
+
onBack,
|
|
28
|
+
}: AuthInfoFormProps) {
|
|
29
|
+
const [hostname, setHostname] = useState('')
|
|
30
|
+
const [selectedName, setSelectedName] = useState<string | null>(null)
|
|
31
|
+
|
|
32
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
if (environmentType === 'dynamic') {
|
|
35
|
+
onSubmit({ hostname })
|
|
36
|
+
} else if (selectedName) {
|
|
37
|
+
onSubmit({ selection: selectedName })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="w-full max-w-md">
|
|
43
|
+
<div className="mb-4">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={onBack}
|
|
47
|
+
className="text-sm text-secondary hover:text-primary transition-colors mb-2 cursor-pointer"
|
|
48
|
+
>
|
|
49
|
+
← Back
|
|
50
|
+
</button>
|
|
51
|
+
<h2 className="heading-lg">Connect to {connectorDisplayName}</h2>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
55
|
+
{environmentType === 'dynamic' && (
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<p className="text-secondary text-sm">
|
|
58
|
+
{instructions || `Enter your ${connectorDisplayName} hostname`}
|
|
59
|
+
</p>
|
|
60
|
+
{urlIdentifier && (
|
|
61
|
+
<p className="text-xs text-secondary opacity-70">
|
|
62
|
+
Example: {urlIdentifier}
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
<Input
|
|
66
|
+
type="url"
|
|
67
|
+
value={hostname}
|
|
68
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setHostname(e.target.value)}
|
|
69
|
+
placeholder="https://your-instance.example.com"
|
|
70
|
+
size="md"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{environmentType === 'selectable' && selections && (
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
<p className="text-secondary text-sm">
|
|
78
|
+
Select your {connectorDisplayName} environment
|
|
79
|
+
</p>
|
|
80
|
+
<div className="space-y-2">
|
|
81
|
+
{selections.map((sel) => (
|
|
82
|
+
<button
|
|
83
|
+
key={sel.name}
|
|
84
|
+
type="button"
|
|
85
|
+
onClick={() => setSelectedName(sel.name)}
|
|
86
|
+
className={`w-full text-left rounded-xl border p-3 transition-colors cursor-pointer ${
|
|
87
|
+
selectedName === sel.name
|
|
88
|
+
? 'border-primary bg-surface-hover'
|
|
89
|
+
: 'border-default bg-surface hover:border-primary'
|
|
90
|
+
}`}
|
|
91
|
+
>
|
|
92
|
+
<span className="font-medium text-sm">{sel.name}</span>
|
|
93
|
+
</button>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
<Button
|
|
100
|
+
type="submit"
|
|
101
|
+
color="primary"
|
|
102
|
+
size="md"
|
|
103
|
+
disabled={
|
|
104
|
+
(environmentType === 'dynamic' && !hostname.trim()) ||
|
|
105
|
+
(environmentType === 'selectable' && !selectedName)
|
|
106
|
+
}
|
|
107
|
+
>
|
|
108
|
+
Continue
|
|
109
|
+
</Button>
|
|
110
|
+
</form>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface AuthSuccessProps {
|
|
2
|
+
connectorDisplayName: string
|
|
3
|
+
userInfo?: { id?: string; name?: string }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function AuthSuccess({ connectorDisplayName, userInfo }: AuthSuccessProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="w-full max-w-md">
|
|
9
|
+
<div className="rounded-xl border border-green-200 bg-green-50 p-5 text-center space-y-2">
|
|
10
|
+
<div className="text-2xl text-green-700">✓</div>
|
|
11
|
+
<h2 className="heading-lg text-green-800">Connected</h2>
|
|
12
|
+
<p className="text-sm text-green-700">
|
|
13
|
+
Successfully connected to <strong>{connectorDisplayName}</strong>
|
|
14
|
+
{userInfo?.name ? ` as ${userInfo.name}` : ''}.
|
|
15
|
+
</p>
|
|
16
|
+
<p className="text-xs text-green-600 mt-2">
|
|
17
|
+
You can now use the AI assistant to interact with your CRM.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Button } from "@openai/apps-sdk-ui/components/Button"
|
|
3
|
+
import { Badge } from "@openai/apps-sdk-ui/components/Badge"
|
|
4
|
+
|
|
5
|
+
export interface Connector {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
displayName: string
|
|
9
|
+
description?: string
|
|
10
|
+
status?: 'public' | 'private'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ConnectorListProps {
|
|
14
|
+
connectors: Connector[]
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
onSelect: (connector: Connector) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ConnectorList({ connectors, disabled, onSelect }: ConnectorListProps) {
|
|
20
|
+
const [selectedName, setSelectedName] = useState<string | null>(null)
|
|
21
|
+
|
|
22
|
+
if (!connectors || connectors.length === 0) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="p-4 text-center text-secondary">
|
|
25
|
+
<p>No connectors available</p>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const handleSelect = (connector: Connector) => {
|
|
31
|
+
setSelectedName(connector.displayName)
|
|
32
|
+
onSelect(connector)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="w-full max-w-md">
|
|
37
|
+
<div className="mb-4">
|
|
38
|
+
<h2 className="heading-lg">Available Connectors</h2>
|
|
39
|
+
<p className="text-secondary text-sm mt-1">
|
|
40
|
+
Select a CRM to connect with RingCentral
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="space-y-3">
|
|
45
|
+
{connectors.map((connector) => {
|
|
46
|
+
const isSelected = selectedName === connector.displayName
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
key={connector.displayName}
|
|
50
|
+
className="rounded-xl border border-default bg-surface p-4 hover:border-primary transition-colors"
|
|
51
|
+
>
|
|
52
|
+
<div className="flex items-center justify-between gap-3">
|
|
53
|
+
<div className="flex-1">
|
|
54
|
+
<div className="flex items-center gap-2">
|
|
55
|
+
<h3 className="font-medium">{connector.displayName}</h3>
|
|
56
|
+
{connector.status === 'private' && (
|
|
57
|
+
<Badge color="secondary" size="sm">Private</Badge>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
{connector.description && (
|
|
61
|
+
<p className="text-secondary text-sm mt-1">
|
|
62
|
+
{connector.description}
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<Button
|
|
67
|
+
color={isSelected ? 'secondary' : 'primary'}
|
|
68
|
+
size="sm"
|
|
69
|
+
onClick={() => handleSelect(connector)}
|
|
70
|
+
disabled={disabled || isSelected}
|
|
71
|
+
>
|
|
72
|
+
{isSelected ? 'Connecting...' : 'Connect'}
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
})}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { getEntries, subscribe, type DebugEntry } from '../lib/debugLog'
|
|
3
|
+
|
|
4
|
+
const LEVEL_STYLE: Record<DebugEntry['level'], string> = {
|
|
5
|
+
info: 'text-blue-700',
|
|
6
|
+
warn: 'text-yellow-700',
|
|
7
|
+
error: 'text-red-700 font-semibold',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DebugPanel() {
|
|
11
|
+
const [entries, setEntries] = useState(getEntries)
|
|
12
|
+
const [open, setOpen] = useState(false)
|
|
13
|
+
|
|
14
|
+
useEffect(() => subscribe(() => setEntries(getEntries())), [])
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="mt-4 border border-default rounded-lg overflow-hidden text-xs">
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={() => setOpen((v) => !v)}
|
|
21
|
+
className="w-full flex items-center justify-between px-3 py-2 bg-surface hover:bg-surface-hover cursor-pointer text-secondary"
|
|
22
|
+
>
|
|
23
|
+
<span>Debug log ({entries.length} entries)</span>
|
|
24
|
+
<span>{open ? '▲' : '▼'}</span>
|
|
25
|
+
</button>
|
|
26
|
+
|
|
27
|
+
{open && (
|
|
28
|
+
<div className="max-h-60 overflow-y-auto bg-black/5 p-2 space-y-0.5 font-mono">
|
|
29
|
+
{entries.length === 0 && (
|
|
30
|
+
<p className="text-secondary italic">No entries yet.</p>
|
|
31
|
+
)}
|
|
32
|
+
{entries.map((e, i) => (
|
|
33
|
+
<div key={i} className={`${LEVEL_STYLE[e.level]} leading-tight`}>
|
|
34
|
+
<span className="opacity-50 mr-1">{e.time}</span>
|
|
35
|
+
<span>[{e.level}]</span>{' '}
|
|
36
|
+
<span className="break-all">{e.msg}</span>
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|