2026-03-16 11:08:02 +05:30
|
|
|
import { useState, useEffect, useCallback } from "react"
|
|
|
|
|
import { getGoogleClientId, setGoogleClientId } from "@/lib/google-client-id-store"
|
|
|
|
|
import { toast } from "sonner"
|
|
|
|
|
|
|
|
|
|
export interface ProviderState {
|
|
|
|
|
isConnected: boolean
|
|
|
|
|
isLoading: boolean
|
|
|
|
|
isConnecting: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type Step = 0 | 1 | 2 | 3
|
|
|
|
|
|
|
|
|
|
export type OnboardingPath = 'rowboat' | 'byok' | null
|
|
|
|
|
|
|
|
|
|
export type LlmProviderFlavor = "openai" | "anthropic" | "google" | "openrouter" | "aigateway" | "ollama" | "openai-compatible"
|
|
|
|
|
|
|
|
|
|
export interface LlmModelOption {
|
|
|
|
|
id: string
|
|
|
|
|
name?: string
|
|
|
|
|
release_date?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function useOnboardingState(open: boolean, onComplete: () => void) {
|
|
|
|
|
const [currentStep, setCurrentStep] = useState<Step>(0)
|
|
|
|
|
const [onboardingPath, setOnboardingPath] = useState<OnboardingPath>(null)
|
|
|
|
|
|
|
|
|
|
// LLM setup state
|
|
|
|
|
const [llmProvider, setLlmProvider] = useState<LlmProviderFlavor>("openai")
|
|
|
|
|
const [modelsCatalog, setModelsCatalog] = useState<Record<string, LlmModelOption[]>>({})
|
|
|
|
|
const [modelsLoading, setModelsLoading] = useState(false)
|
|
|
|
|
const [modelsError, setModelsError] = useState<string | null>(null)
|
|
|
|
|
const [providerConfigs, setProviderConfigs] = useState<Record<LlmProviderFlavor, { apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>>({
|
|
|
|
|
openai: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
|
|
|
|
anthropic: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
|
|
|
|
google: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
|
|
|
|
openrouter: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
|
|
|
|
aigateway: { apiKey: "", baseURL: "", model: "", knowledgeGraphModel: "" },
|
|
|
|
|
ollama: { apiKey: "", baseURL: "http://localhost:11434", model: "", knowledgeGraphModel: "" },
|
|
|
|
|
"openai-compatible": { apiKey: "", baseURL: "http://localhost:1234/v1", model: "", knowledgeGraphModel: "" },
|
|
|
|
|
})
|
|
|
|
|
const [testState, setTestState] = useState<{ status: "idle" | "testing" | "success" | "error"; error?: string }>({
|
|
|
|
|
status: "idle",
|
|
|
|
|
})
|
|
|
|
|
const [showMoreProviders, setShowMoreProviders] = useState(false)
|
|
|
|
|
|
|
|
|
|
// OAuth provider states
|
|
|
|
|
const [providers, setProviders] = useState<string[]>([])
|
|
|
|
|
const [providersLoading, setProvidersLoading] = useState(true)
|
|
|
|
|
const [providerStates, setProviderStates] = useState<Record<string, ProviderState>>({})
|
|
|
|
|
const [googleClientIdOpen, setGoogleClientIdOpen] = useState(false)
|
|
|
|
|
|
|
|
|
|
// Granola state
|
|
|
|
|
const [granolaEnabled, setGranolaEnabled] = useState(false)
|
|
|
|
|
const [granolaLoading, setGranolaLoading] = useState(true)
|
|
|
|
|
|
|
|
|
|
// Slack state (agent-slack CLI)
|
|
|
|
|
const [slackEnabled, setSlackEnabled] = useState(false)
|
|
|
|
|
const [slackLoading, setSlackLoading] = useState(true)
|
|
|
|
|
const [slackWorkspaces, setSlackWorkspaces] = useState<Array<{ url: string; name: string }>>([])
|
|
|
|
|
const [slackAvailableWorkspaces, setSlackAvailableWorkspaces] = useState<Array<{ url: string; name: string }>>([])
|
|
|
|
|
const [slackSelectedUrls, setSlackSelectedUrls] = useState<Set<string>>(new Set())
|
|
|
|
|
const [slackPickerOpen, setSlackPickerOpen] = useState(false)
|
|
|
|
|
const [slackDiscovering, setSlackDiscovering] = useState(false)
|
|
|
|
|
const [slackDiscoverError, setSlackDiscoverError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
// Inline upsell callout dismissed
|
|
|
|
|
const [upsellDismissed, setUpsellDismissed] = useState(false)
|
|
|
|
|
|
2026-03-16 16:17:33 +05:30
|
|
|
// Composio/Gmail state (used when signed in with Rowboat account)
|
|
|
|
|
const [useComposioForGoogle, setUseComposioForGoogle] = useState(false)
|
|
|
|
|
const [gmailConnected, setGmailConnected] = useState(false)
|
|
|
|
|
const [gmailLoading, setGmailLoading] = useState(true)
|
|
|
|
|
const [gmailConnecting, setGmailConnecting] = useState(false)
|
|
|
|
|
const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false)
|
|
|
|
|
const [composioApiKeyTarget, setComposioApiKeyTarget] = useState<'slack' | 'gmail'>('gmail')
|
|
|
|
|
|
2026-03-16 11:08:02 +05:30
|
|
|
const updateProviderConfig = useCallback(
|
|
|
|
|
(provider: LlmProviderFlavor, updates: Partial<{ apiKey: string; baseURL: string; model: string; knowledgeGraphModel: string }>) => {
|
|
|
|
|
setProviderConfigs(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[provider]: { ...prev[provider], ...updates },
|
|
|
|
|
}))
|
|
|
|
|
setTestState({ status: "idle" })
|
|
|
|
|
},
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const activeConfig = providerConfigs[llmProvider]
|
|
|
|
|
const showApiKey = llmProvider === "openai" || llmProvider === "anthropic" || llmProvider === "google" || llmProvider === "openrouter" || llmProvider === "aigateway" || llmProvider === "openai-compatible"
|
|
|
|
|
const requiresApiKey = llmProvider === "openai" || llmProvider === "anthropic" || llmProvider === "google" || llmProvider === "openrouter" || llmProvider === "aigateway"
|
|
|
|
|
const requiresBaseURL = llmProvider === "ollama" || llmProvider === "openai-compatible"
|
|
|
|
|
const showBaseURL = llmProvider === "ollama" || llmProvider === "openai-compatible" || llmProvider === "aigateway"
|
|
|
|
|
const isLocalProvider = llmProvider === "ollama" || llmProvider === "openai-compatible"
|
|
|
|
|
const canTest =
|
|
|
|
|
activeConfig.model.trim().length > 0 &&
|
|
|
|
|
(!requiresApiKey || activeConfig.apiKey.trim().length > 0) &&
|
|
|
|
|
(!requiresBaseURL || activeConfig.baseURL.trim().length > 0)
|
|
|
|
|
|
|
|
|
|
// Track connected providers for the completion step
|
|
|
|
|
const connectedProviders = Object.entries(providerStates)
|
|
|
|
|
.filter(([, state]) => state.isConnected)
|
|
|
|
|
.map(([provider]) => provider)
|
|
|
|
|
|
2026-03-16 16:17:33 +05:30
|
|
|
// Load available providers and composio-for-google flag on mount
|
2026-03-16 11:08:02 +05:30
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return
|
|
|
|
|
|
|
|
|
|
async function loadProviders() {
|
|
|
|
|
try {
|
|
|
|
|
setProvidersLoading(true)
|
|
|
|
|
const result = await window.ipc.invoke('oauth:list-providers', null)
|
|
|
|
|
setProviders(result.providers || [])
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to get available providers:', error)
|
|
|
|
|
setProviders([])
|
|
|
|
|
} finally {
|
|
|
|
|
setProvidersLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 16:17:33 +05:30
|
|
|
async function loadComposioForGoogleFlag() {
|
|
|
|
|
try {
|
|
|
|
|
const result = await window.ipc.invoke('composio:use-composio-for-google', null)
|
|
|
|
|
setUseComposioForGoogle(result.enabled)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to check composio-for-google flag:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 11:08:02 +05:30
|
|
|
loadProviders()
|
2026-03-16 16:17:33 +05:30
|
|
|
loadComposioForGoogleFlag()
|
2026-03-16 11:08:02 +05:30
|
|
|
}, [open])
|
|
|
|
|
|
|
|
|
|
// Load LLM models catalog on open
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return
|
|
|
|
|
|
|
|
|
|
async function loadModels() {
|
|
|
|
|
try {
|
|
|
|
|
setModelsLoading(true)
|
|
|
|
|
setModelsError(null)
|
|
|
|
|
const result = await window.ipc.invoke("models:list", null)
|
|
|
|
|
const catalog: Record<string, LlmModelOption[]> = {}
|
|
|
|
|
for (const provider of result.providers || []) {
|
|
|
|
|
catalog[provider.id] = provider.models || []
|
|
|
|
|
}
|
|
|
|
|
setModelsCatalog(catalog)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to load models catalog:", error)
|
|
|
|
|
setModelsError("Failed to load models list")
|
|
|
|
|
setModelsCatalog({})
|
|
|
|
|
} finally {
|
|
|
|
|
setModelsLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadModels()
|
|
|
|
|
}, [open])
|
|
|
|
|
|
|
|
|
|
// Preferred default models for each provider
|
|
|
|
|
const preferredDefaults: Partial<Record<LlmProviderFlavor, string>> = {
|
|
|
|
|
openai: "gpt-5.2",
|
|
|
|
|
anthropic: "claude-opus-4-6-20260202",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize default models from catalog
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (Object.keys(modelsCatalog).length === 0) return
|
|
|
|
|
setProviderConfigs(prev => {
|
|
|
|
|
const next = { ...prev }
|
|
|
|
|
const cloudProviders: LlmProviderFlavor[] = ["openai", "anthropic", "google"]
|
|
|
|
|
for (const provider of cloudProviders) {
|
|
|
|
|
const models = modelsCatalog[provider]
|
|
|
|
|
if (models?.length && !next[provider].model) {
|
|
|
|
|
const preferredModel = preferredDefaults[provider]
|
|
|
|
|
const hasPreferred = preferredModel && models.some(m => m.id === preferredModel)
|
|
|
|
|
next[provider] = { ...next[provider], model: hasPreferred ? preferredModel : (models[0]?.id || "") }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
}, [modelsCatalog])
|
|
|
|
|
|
|
|
|
|
// Load Granola config
|
|
|
|
|
const refreshGranolaConfig = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setGranolaLoading(true)
|
|
|
|
|
const result = await window.ipc.invoke('granola:getConfig', null)
|
|
|
|
|
setGranolaEnabled(result.enabled)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to load Granola config:', error)
|
|
|
|
|
setGranolaEnabled(false)
|
|
|
|
|
} finally {
|
|
|
|
|
setGranolaLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Update Granola config
|
|
|
|
|
const handleGranolaToggle = useCallback(async (enabled: boolean) => {
|
|
|
|
|
try {
|
|
|
|
|
setGranolaLoading(true)
|
|
|
|
|
await window.ipc.invoke('granola:setConfig', { enabled })
|
|
|
|
|
setGranolaEnabled(enabled)
|
|
|
|
|
toast.success(enabled ? 'Granola sync enabled' : 'Granola sync disabled')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to update Granola config:', error)
|
|
|
|
|
toast.error('Failed to update Granola sync settings')
|
|
|
|
|
} finally {
|
|
|
|
|
setGranolaLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Load Slack config
|
|
|
|
|
const refreshSlackConfig = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setSlackLoading(true)
|
|
|
|
|
const result = await window.ipc.invoke('slack:getConfig', null)
|
|
|
|
|
setSlackEnabled(result.enabled)
|
|
|
|
|
setSlackWorkspaces(result.workspaces || [])
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to load Slack config:', error)
|
|
|
|
|
setSlackEnabled(false)
|
|
|
|
|
setSlackWorkspaces([])
|
|
|
|
|
} finally {
|
|
|
|
|
setSlackLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Enable Slack: discover workspaces
|
|
|
|
|
const handleSlackEnable = useCallback(async () => {
|
|
|
|
|
setSlackDiscovering(true)
|
|
|
|
|
setSlackDiscoverError(null)
|
|
|
|
|
try {
|
|
|
|
|
const result = await window.ipc.invoke('slack:listWorkspaces', null)
|
|
|
|
|
if (result.error || result.workspaces.length === 0) {
|
|
|
|
|
setSlackDiscoverError(result.error || 'No Slack workspaces found. Set up with: agent-slack auth import-desktop')
|
|
|
|
|
setSlackAvailableWorkspaces([])
|
|
|
|
|
setSlackPickerOpen(true)
|
|
|
|
|
} else {
|
|
|
|
|
setSlackAvailableWorkspaces(result.workspaces)
|
|
|
|
|
setSlackSelectedUrls(new Set(result.workspaces.map((w: { url: string }) => w.url)))
|
|
|
|
|
setSlackPickerOpen(true)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to discover Slack workspaces:', error)
|
|
|
|
|
setSlackDiscoverError('Failed to discover Slack workspaces')
|
|
|
|
|
setSlackPickerOpen(true)
|
|
|
|
|
} finally {
|
|
|
|
|
setSlackDiscovering(false)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Save selected Slack workspaces
|
|
|
|
|
const handleSlackSaveWorkspaces = useCallback(async () => {
|
|
|
|
|
const selected = slackAvailableWorkspaces.filter(w => slackSelectedUrls.has(w.url))
|
|
|
|
|
try {
|
|
|
|
|
setSlackLoading(true)
|
|
|
|
|
await window.ipc.invoke('slack:setConfig', { enabled: true, workspaces: selected })
|
|
|
|
|
setSlackEnabled(true)
|
|
|
|
|
setSlackWorkspaces(selected)
|
|
|
|
|
setSlackPickerOpen(false)
|
|
|
|
|
toast.success('Slack enabled')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to save Slack config:', error)
|
|
|
|
|
toast.error('Failed to save Slack settings')
|
|
|
|
|
} finally {
|
|
|
|
|
setSlackLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}, [slackAvailableWorkspaces, slackSelectedUrls])
|
|
|
|
|
|
|
|
|
|
// Disable Slack
|
|
|
|
|
const handleSlackDisable = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setSlackLoading(true)
|
|
|
|
|
await window.ipc.invoke('slack:setConfig', { enabled: false, workspaces: [] })
|
|
|
|
|
setSlackEnabled(false)
|
|
|
|
|
setSlackWorkspaces([])
|
|
|
|
|
setSlackPickerOpen(false)
|
|
|
|
|
toast.success('Slack disabled')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to update Slack config:', error)
|
|
|
|
|
toast.error('Failed to update Slack settings')
|
|
|
|
|
} finally {
|
|
|
|
|
setSlackLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
2026-03-16 16:17:33 +05:30
|
|
|
// Load Gmail connection status (Composio)
|
|
|
|
|
const refreshGmailStatus = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setGmailLoading(true)
|
|
|
|
|
const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'gmail' })
|
|
|
|
|
setGmailConnected(result.isConnected)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to load Gmail status:', error)
|
|
|
|
|
setGmailConnected(false)
|
|
|
|
|
} finally {
|
|
|
|
|
setGmailLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Connect to Gmail via Composio
|
|
|
|
|
const startGmailConnect = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
setGmailConnecting(true)
|
|
|
|
|
const result = await window.ipc.invoke('composio:initiate-connection', { toolkitSlug: 'gmail' })
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
toast.error(result.error || 'Failed to connect to Gmail')
|
|
|
|
|
setGmailConnecting(false)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to connect to Gmail:', error)
|
|
|
|
|
toast.error('Failed to connect to Gmail')
|
|
|
|
|
setGmailConnecting(false)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Handle Gmail connect button click (checks Composio config first)
|
|
|
|
|
const handleConnectGmail = useCallback(async () => {
|
|
|
|
|
const configResult = await window.ipc.invoke('composio:is-configured', null)
|
|
|
|
|
if (!configResult.configured) {
|
|
|
|
|
setComposioApiKeyTarget('gmail')
|
|
|
|
|
setComposioApiKeyOpen(true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
await startGmailConnect()
|
|
|
|
|
}, [startGmailConnect])
|
|
|
|
|
|
|
|
|
|
// Handle Composio API key submission
|
|
|
|
|
const handleComposioApiKeySubmit = useCallback(async (apiKey: string) => {
|
|
|
|
|
try {
|
|
|
|
|
await window.ipc.invoke('composio:set-api-key', { apiKey })
|
|
|
|
|
setComposioApiKeyOpen(false)
|
|
|
|
|
toast.success('Composio API key saved')
|
|
|
|
|
await startGmailConnect()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to save Composio API key:', error)
|
|
|
|
|
toast.error('Failed to save API key')
|
|
|
|
|
}
|
|
|
|
|
}, [startGmailConnect])
|
|
|
|
|
|
2026-03-16 11:08:02 +05:30
|
|
|
// New step flow:
|
|
|
|
|
// Rowboat path: 0 (welcome) → 2 (connect) → 3 (done)
|
|
|
|
|
// BYOK path: 0 (welcome) → 1 (llm setup) → 2 (connect) → 3 (done)
|
|
|
|
|
const handleNext = useCallback(() => {
|
|
|
|
|
if (currentStep === 0) {
|
|
|
|
|
if (onboardingPath === 'byok') {
|
|
|
|
|
setCurrentStep(1)
|
|
|
|
|
} else {
|
|
|
|
|
setCurrentStep(2)
|
|
|
|
|
}
|
|
|
|
|
} else if (currentStep === 1) {
|
|
|
|
|
setCurrentStep(2)
|
|
|
|
|
} else if (currentStep === 2) {
|
|
|
|
|
setCurrentStep(3)
|
|
|
|
|
}
|
|
|
|
|
}, [currentStep, onboardingPath])
|
|
|
|
|
|
|
|
|
|
const handleBack = useCallback(() => {
|
|
|
|
|
if (currentStep === 1) {
|
|
|
|
|
setCurrentStep(0)
|
|
|
|
|
setOnboardingPath(null)
|
|
|
|
|
} else if (currentStep === 2) {
|
|
|
|
|
if (onboardingPath === 'rowboat') {
|
|
|
|
|
setCurrentStep(0)
|
|
|
|
|
} else {
|
|
|
|
|
setCurrentStep(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [currentStep, onboardingPath])
|
|
|
|
|
|
|
|
|
|
const handleComplete = useCallback(() => {
|
|
|
|
|
onComplete()
|
|
|
|
|
}, [onComplete])
|
|
|
|
|
|
|
|
|
|
const handleTestAndSaveLlmConfig = useCallback(async () => {
|
|
|
|
|
if (!canTest) return
|
|
|
|
|
setTestState({ status: "testing" })
|
|
|
|
|
try {
|
|
|
|
|
const apiKey = activeConfig.apiKey.trim() || undefined
|
|
|
|
|
const baseURL = activeConfig.baseURL.trim() || undefined
|
|
|
|
|
const model = activeConfig.model.trim()
|
|
|
|
|
const knowledgeGraphModel = activeConfig.knowledgeGraphModel.trim() || undefined
|
|
|
|
|
const providerConfig = {
|
|
|
|
|
provider: {
|
|
|
|
|
flavor: llmProvider,
|
|
|
|
|
apiKey,
|
|
|
|
|
baseURL,
|
|
|
|
|
},
|
|
|
|
|
model,
|
|
|
|
|
knowledgeGraphModel,
|
|
|
|
|
}
|
|
|
|
|
const result = await window.ipc.invoke("models:test", providerConfig)
|
|
|
|
|
if (result.success) {
|
|
|
|
|
setTestState({ status: "success" })
|
|
|
|
|
await window.ipc.invoke("models:saveConfig", providerConfig)
|
|
|
|
|
handleNext()
|
|
|
|
|
} else {
|
|
|
|
|
setTestState({ status: "error", error: result.error })
|
|
|
|
|
toast.error(result.error || "Connection test failed")
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Connection test failed:", error)
|
|
|
|
|
setTestState({ status: "error", error: "Connection test failed" })
|
|
|
|
|
toast.error("Connection test failed")
|
|
|
|
|
}
|
|
|
|
|
}, [activeConfig.apiKey, activeConfig.baseURL, activeConfig.model, activeConfig.knowledgeGraphModel, canTest, llmProvider, handleNext])
|
|
|
|
|
|
|
|
|
|
// Check connection status for all providers
|
|
|
|
|
const refreshAllStatuses = useCallback(async () => {
|
|
|
|
|
refreshGranolaConfig()
|
|
|
|
|
refreshSlackConfig()
|
|
|
|
|
|
2026-03-16 16:17:33 +05:30
|
|
|
// Refresh Gmail Composio status if enabled
|
|
|
|
|
if (useComposioForGoogle) {
|
|
|
|
|
refreshGmailStatus()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 11:08:02 +05:30
|
|
|
if (providers.length === 0) return
|
|
|
|
|
|
|
|
|
|
const newStates: Record<string, ProviderState> = {}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await window.ipc.invoke('oauth:getState', null)
|
|
|
|
|
const config = result.config || {}
|
|
|
|
|
for (const provider of providers) {
|
|
|
|
|
newStates[provider] = {
|
|
|
|
|
isConnected: config[provider]?.connected ?? false,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
isConnecting: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to check connection status for providers:', error)
|
|
|
|
|
for (const provider of providers) {
|
|
|
|
|
newStates[provider] = {
|
|
|
|
|
isConnected: false,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
isConnecting: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setProviderStates(newStates)
|
2026-03-16 16:17:33 +05:30
|
|
|
}, [providers, refreshGranolaConfig, refreshSlackConfig, refreshGmailStatus, useComposioForGoogle])
|
2026-03-16 11:08:02 +05:30
|
|
|
|
|
|
|
|
// Refresh statuses when modal opens or providers list changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (open && providers.length > 0) {
|
|
|
|
|
refreshAllStatuses()
|
|
|
|
|
}
|
|
|
|
|
}, [open, providers, refreshAllStatuses])
|
|
|
|
|
|
|
|
|
|
// Listen for OAuth completion events
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const cleanup = window.ipc.on('oauth:didConnect', (event) => {
|
|
|
|
|
const { provider, success, error } = event
|
|
|
|
|
|
|
|
|
|
setProviderStates(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[provider]: {
|
|
|
|
|
isConnected: success,
|
|
|
|
|
isLoading: false,
|
|
|
|
|
isConnecting: false,
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
const displayName = provider === 'fireflies-ai' ? 'Fireflies' : provider.charAt(0).toUpperCase() + provider.slice(1)
|
|
|
|
|
toast.success(`Connected to ${displayName}`)
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(error || `Failed to connect to ${provider}`)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return cleanup
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Auto-advance from Rowboat sign-in step when OAuth completes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (onboardingPath !== 'rowboat' || currentStep !== 0) return
|
|
|
|
|
|
2026-03-16 16:17:33 +05:30
|
|
|
const cleanup = window.ipc.on('oauth:didConnect', async (event) => {
|
2026-03-16 11:08:02 +05:30
|
|
|
if (event.provider === 'rowboat' && event.success) {
|
2026-03-16 16:17:33 +05:30
|
|
|
// Re-check the composio-for-google flag now that the account is connected
|
|
|
|
|
try {
|
|
|
|
|
const result = await window.ipc.invoke('composio:use-composio-for-google', null)
|
|
|
|
|
setUseComposioForGoogle(result.enabled)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to re-check composio-for-google flag:', error)
|
|
|
|
|
}
|
2026-03-16 11:08:02 +05:30
|
|
|
setCurrentStep(2) // Go to Connect Accounts
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return cleanup
|
|
|
|
|
}, [onboardingPath, currentStep])
|
|
|
|
|
|
|
|
|
|
// Listen for Composio connection events
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const cleanup = window.ipc.on('composio:didConnect', (event) => {
|
|
|
|
|
const { toolkitSlug, success, error } = event
|
|
|
|
|
|
|
|
|
|
if (toolkitSlug === 'slack') {
|
|
|
|
|
setSlackEnabled(success)
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
toast.success('Connected to Slack')
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(error || 'Failed to connect to Slack')
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 16:17:33 +05:30
|
|
|
|
|
|
|
|
if (toolkitSlug === 'gmail') {
|
|
|
|
|
setGmailConnected(success)
|
|
|
|
|
setGmailConnecting(false)
|
|
|
|
|
|
|
|
|
|
if (success) {
|
|
|
|
|
toast.success('Connected to Gmail')
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(error || 'Failed to connect to Gmail')
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-16 11:08:02 +05:30
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return cleanup
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const startConnect = useCallback(async (provider: string, clientId?: string) => {
|
|
|
|
|
setProviderStates(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[provider]: { ...prev[provider], isConnecting: true }
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await window.ipc.invoke('oauth:connect', { provider, clientId })
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
toast.error(result.error || `Failed to connect to ${provider}`)
|
|
|
|
|
setProviderStates(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[provider]: { ...prev[provider], isConnecting: false }
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to connect:', error)
|
|
|
|
|
toast.error(`Failed to connect to ${provider}`)
|
|
|
|
|
setProviderStates(prev => ({
|
|
|
|
|
...prev,
|
|
|
|
|
[provider]: { ...prev[provider], isConnecting: false }
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Connect to a provider
|
|
|
|
|
const handleConnect = useCallback(async (provider: string) => {
|
|
|
|
|
if (provider === 'google') {
|
|
|
|
|
const existingClientId = getGoogleClientId()
|
|
|
|
|
if (!existingClientId) {
|
|
|
|
|
setGoogleClientIdOpen(true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
await startConnect(provider, existingClientId)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await startConnect(provider)
|
|
|
|
|
}, [startConnect])
|
|
|
|
|
|
|
|
|
|
const handleGoogleClientIdSubmit = useCallback((clientId: string) => {
|
|
|
|
|
setGoogleClientId(clientId)
|
|
|
|
|
setGoogleClientIdOpen(false)
|
|
|
|
|
startConnect('google', clientId)
|
|
|
|
|
}, [startConnect])
|
|
|
|
|
|
|
|
|
|
// Switch to rowboat path from BYOK inline callout
|
|
|
|
|
const handleSwitchToRowboat = useCallback(() => {
|
|
|
|
|
setOnboardingPath('rowboat')
|
|
|
|
|
setCurrentStep(0)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
// Step state
|
|
|
|
|
currentStep,
|
|
|
|
|
setCurrentStep,
|
|
|
|
|
onboardingPath,
|
|
|
|
|
setOnboardingPath,
|
|
|
|
|
|
|
|
|
|
// LLM state
|
|
|
|
|
llmProvider,
|
|
|
|
|
setLlmProvider,
|
|
|
|
|
modelsCatalog,
|
|
|
|
|
modelsLoading,
|
|
|
|
|
modelsError,
|
|
|
|
|
providerConfigs,
|
|
|
|
|
activeConfig,
|
|
|
|
|
testState,
|
|
|
|
|
setTestState,
|
|
|
|
|
showApiKey,
|
|
|
|
|
requiresApiKey,
|
|
|
|
|
requiresBaseURL,
|
|
|
|
|
showBaseURL,
|
|
|
|
|
isLocalProvider,
|
|
|
|
|
canTest,
|
|
|
|
|
showMoreProviders,
|
|
|
|
|
setShowMoreProviders,
|
|
|
|
|
updateProviderConfig,
|
|
|
|
|
handleTestAndSaveLlmConfig,
|
|
|
|
|
|
|
|
|
|
// OAuth state
|
|
|
|
|
providers,
|
|
|
|
|
providersLoading,
|
|
|
|
|
providerStates,
|
|
|
|
|
googleClientIdOpen,
|
|
|
|
|
setGoogleClientIdOpen,
|
|
|
|
|
connectedProviders,
|
|
|
|
|
handleConnect,
|
|
|
|
|
handleGoogleClientIdSubmit,
|
|
|
|
|
startConnect,
|
|
|
|
|
|
|
|
|
|
// Granola state
|
|
|
|
|
granolaEnabled,
|
|
|
|
|
granolaLoading,
|
|
|
|
|
handleGranolaToggle,
|
|
|
|
|
|
|
|
|
|
// Slack state
|
|
|
|
|
slackEnabled,
|
|
|
|
|
slackLoading,
|
|
|
|
|
slackWorkspaces,
|
|
|
|
|
slackAvailableWorkspaces,
|
|
|
|
|
slackSelectedUrls,
|
|
|
|
|
setSlackSelectedUrls,
|
|
|
|
|
slackPickerOpen,
|
|
|
|
|
slackDiscovering,
|
|
|
|
|
slackDiscoverError,
|
|
|
|
|
handleSlackEnable,
|
|
|
|
|
handleSlackSaveWorkspaces,
|
|
|
|
|
handleSlackDisable,
|
|
|
|
|
|
|
|
|
|
// Upsell
|
|
|
|
|
upsellDismissed,
|
|
|
|
|
setUpsellDismissed,
|
|
|
|
|
|
2026-03-16 16:17:33 +05:30
|
|
|
// Composio/Gmail state
|
|
|
|
|
useComposioForGoogle,
|
|
|
|
|
gmailConnected,
|
|
|
|
|
gmailLoading,
|
|
|
|
|
gmailConnecting,
|
|
|
|
|
composioApiKeyOpen,
|
|
|
|
|
setComposioApiKeyOpen,
|
|
|
|
|
composioApiKeyTarget,
|
|
|
|
|
handleConnectGmail,
|
|
|
|
|
handleComposioApiKeySubmit,
|
|
|
|
|
|
2026-03-16 11:08:02 +05:30
|
|
|
// Navigation
|
|
|
|
|
handleNext,
|
|
|
|
|
handleBack,
|
|
|
|
|
handleComplete,
|
|
|
|
|
handleSwitchToRowboat,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type OnboardingState = ReturnType<typeof useOnboardingState>
|