/ src / components / auth / setup-wizard / utils.ts
utils.ts
  1  import {
  2    STARTER_KITS,
  3    getDefaultModelForProvider,
  4    type OnboardingPath,
  5    type StarterKit,
  6    type SetupProvider,
  7    type StarterKitAgentTemplate,
  8  } from '@/lib/setup-defaults'
  9  import type { ConfiguredProvider, SetupStep, StarterDraftAgent } from './types'
 10  import { STEP_ORDER } from './types'
 11  
 12  export function stepIndex(step: SetupStep): number {
 13    if (step === 'connect') return STEP_ORDER.indexOf('providers')
 14    return STEP_ORDER.indexOf(step)
 15  }
 16  
 17  export function defaultKitForPath(path: OnboardingPath): string {
 18    if (path === 'manual') return 'blank_workspace'
 19    return 'personal_assistant'
 20  }
 21  
 22  export function getStarterKitsForPath(path: OnboardingPath): StarterKit[] {
 23    if (path === 'quick') {
 24      const quickIds = new Set(['personal_assistant', 'builder_studio', 'research_copilot'])
 25      return STARTER_KITS.filter((kit) => quickIds.has(kit.id))
 26    }
 27    if (path === 'intent') {
 28      const intentIds = new Set([
 29        'personal_assistant',
 30        'builder_studio',
 31        'research_copilot',
 32        'operator_swarm',
 33        'inbox_triage',
 34        'data_analyst',
 35      ])
 36      return STARTER_KITS.filter((kit) => intentIds.has(kit.id))
 37    }
 38    return STARTER_KITS
 39  }
 40  
 41  export function applyIntentContext(prompt: string, intentText: string): string {
 42    const trimmed = intentText.trim()
 43    if (!trimmed) return prompt
 44    return `${prompt}
 45  
 46  Current user intent:
 47  - ${trimmed}
 48  
 49  Keep your help aligned to this intent unless the user changes direction.`
 50  }
 51  
 52  export function formatAgentCount(count: number): string {
 53    if (count === 0) return 'Blank'
 54    if (count === 1) return '1 agent'
 55    return `${count} agents`
 56  }
 57  
 58  export function withHttpScheme(value: string): string {
 59    return /^(https?|wss?):\/\//i.test(value) ? value : `http://${value}`
 60  }
 61  
 62  export function parseProviderUrl(value: string | null | undefined): URL | null {
 63    const trimmed = typeof value === 'string' ? value.trim() : ''
 64    if (!trimmed) return null
 65    try {
 66      return new URL(withHttpScheme(trimmed))
 67    } catch {
 68      return null
 69    }
 70  }
 71  
 72  export function formatEndpointHost(value: string | null | undefined): string | null {
 73    const parsed = parseProviderUrl(value)
 74    if (!parsed) return null
 75    return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname
 76  }
 77  
 78  export function isLocalOpenClawEndpoint(value: string | null | undefined): boolean {
 79    const parsed = parseProviderUrl(value)
 80    if (!parsed) return false
 81    const host = parsed.hostname.trim().toLowerCase()
 82    return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0'
 83  }
 84  
 85  export function resolveOpenClawDashboardUrl(value: string | null | undefined): string {
 86    const parsed = parseProviderUrl(value)
 87    if (!parsed) return 'http://localhost:18789'
 88    const next = new URL(parsed.toString())
 89    if (next.protocol === 'wss:') next.protocol = 'https:'
 90    if (next.protocol === 'ws:') next.protocol = 'http:'
 91    next.pathname = ''
 92    next.search = ''
 93    next.hash = ''
 94    return next.toString().replace(/\/+$/, '')
 95  }
 96  
 97  export function getOpenClawErrorHint(message: string): string | null {
 98    const lower = message.toLowerCase()
 99    if (lower.includes('timeout') || lower.includes('timed out')) {
100      return 'Ensure the port is open and reachable from this machine.'
101    }
102    if (lower.includes('401') || lower.includes('unauthorized')) {
103      return 'Check your gateway auth token.'
104    }
105    if (lower.includes('405') || lower.includes('method not allowed')) {
106      return 'Enable chatCompletions in your OpenClaw config: openclaw config set gateway.http.endpoints.chatCompletions.enabled true'
107    }
108    if (lower.includes('econnrefused') || lower.includes('connection refused') || lower.includes('connect econnrefused')) {
109      return 'Verify that the OpenClaw gateway is running on the target host.'
110    }
111    return null
112  }
113  
114  export function requiresSetupProviderVerification(provider: SetupProvider | null | undefined): boolean {
115    return provider != null && provider !== 'openclaw' && provider !== 'custom'
116  }
117  
118  export function preferredConfiguredProvider(
119    template: StarterKitAgentTemplate,
120    configuredProviders: ConfiguredProvider[],
121    fallbackProviderConfigId?: string | null,
122    fallbackProvider?: SetupProvider | null,
123  ): ConfiguredProvider | null {
124    if (fallbackProviderConfigId) {
125      const exact = configuredProviders.find((candidate) => candidate.id === fallbackProviderConfigId)
126      if (exact) return exact
127    }
128  
129    if (fallbackProvider) {
130      const exact = configuredProviders.find((candidate) => candidate.setupProvider === fallbackProvider)
131      if (exact) return exact
132    }
133  
134    for (const provider of template.recommendedProviders || []) {
135      const exact = configuredProviders.find((candidate) => candidate.setupProvider === provider)
136      if (exact) return exact
137    }
138  
139    return configuredProviders[0] || null
140  }
141  
142  export function buildStarterDrafts(args: {
143    starterKitId: string | null
144    intentText: string
145    configuredProviders: ConfiguredProvider[]
146    previousDrafts?: StarterDraftAgent[]
147  }): StarterDraftAgent[] {
148    const { starterKitId, intentText, configuredProviders, previousDrafts = [] } = args
149    const starterKit = STARTER_KITS.find((kit) => kit.id === starterKitId)
150    if (!starterKit) return []
151  
152    const previousById = new Map(previousDrafts.map((draft) => [draft.id, draft]))
153  
154    return starterKit.agents.map((template) => {
155      const id = `${starterKit.id}:${template.id}`
156      const previous = previousById.get(id)
157      const configuredProvider = preferredConfiguredProvider(
158        template,
159        configuredProviders,
160        previous?.providerConfigId,
161        previous?.setupProvider,
162      )
163      const oldProvider = previous?.setupProvider || null
164      const previousModel = previous?.model || ''
165      const oldProviderDefault = oldProvider ? getDefaultModelForProvider(oldProvider) : ''
166      const nextProviderDefault = configuredProvider?.defaultModel || ''
167      const shouldRefreshModel =
168        !previousModel.trim()
169        || (oldProvider !== configuredProvider?.setupProvider && previousModel === oldProviderDefault)
170  
171      return {
172        id,
173        templateId: template.id,
174        name: previous?.name || template.name,
175        description: previous?.description || template.description,
176        systemPrompt: previous?.systemPrompt || applyIntentContext(template.systemPrompt, intentText),
177        soul: previous?.soul || '',
178        providerConfigId: configuredProvider?.id || null,
179        setupProvider: configuredProvider?.setupProvider || null,
180        provider: configuredProvider?.provider || null,
181        model: shouldRefreshModel ? nextProviderDefault : previousModel,
182        credentialId: configuredProvider?.credentialId || null,
183        apiEndpoint: configuredProvider?.endpoint || null,
184        gatewayProfileId: configuredProvider?.gatewayProfileId || null,
185        tools: template.tools,
186        capabilities: previous?.capabilities || template.capabilities || [],
187        delegationEnabled: previous?.delegationEnabled ?? template.delegationEnabled ?? false,
188        delegationTargetMode: previous?.delegationTargetMode || 'all',
189        delegationTargetAgentIds: previous?.delegationTargetAgentIds || [],
190        autoDraftSkillSuggestions: previous?.autoDraftSkillSuggestions ?? true,
191        orchestratorEnabled: previous?.orchestratorEnabled ?? false,
192        orchestratorMission: previous?.orchestratorMission || '',
193        avatarSeed: previous?.avatarSeed || Math.random().toString(36).slice(2, 10),
194        avatarUrl: previous?.avatarUrl || null,
195        enabled: previous?.enabled ?? true,
196      }
197    })
198  }