fix(deploy-modal): break down deploy modal into separate components (#837)

Co-authored-by: waleedlatif <waleedlatif@waleedlatifs-MacBook-Pro.local>
This commit is contained in:
Waleed Latif
2025-07-31 19:51:25 -07:00
committed by GitHub
parent 914f1cdd47
commit 5b53cc2be6
7 changed files with 1003 additions and 1363 deletions

View File

@@ -0,0 +1,268 @@
import { useState } from 'react'
import { Check, Copy, Eye, EyeOff, Plus, RefreshCw, Trash2 } from 'lucide-react'
import { Button, Card, CardContent, Input, Label } from '@/components/ui'
import { cn } from '@/lib/utils'
import type { AuthType } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/hooks/use-chat-form'
interface AuthSelectorProps {
authType: AuthType
password: string
emails: string[]
onAuthTypeChange: (type: AuthType) => void
onPasswordChange: (password: string) => void
onEmailsChange: (emails: string[]) => void
disabled?: boolean
isExistingChat?: boolean
error?: string
}
export function AuthSelector({
authType,
password,
emails,
onAuthTypeChange,
onPasswordChange,
onEmailsChange,
disabled = false,
isExistingChat = false,
error,
}: AuthSelectorProps) {
const [showPassword, setShowPassword] = useState(false)
const [newEmail, setNewEmail] = useState('')
const [emailError, setEmailError] = useState('')
const [copySuccess, setCopySuccess] = useState(false)
const generatePassword = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+='
let result = ''
const length = 24
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
onPasswordChange(result)
}
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
setCopySuccess(true)
setTimeout(() => setCopySuccess(false), 2000)
}
const handleAddEmail = () => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail) && !newEmail.startsWith('@')) {
setEmailError('Please enter a valid email or domain (e.g., user@example.com or @example.com)')
return
}
if (emails.includes(newEmail)) {
setEmailError('This email or domain is already in the list')
return
}
onEmailsChange([...emails, newEmail])
setNewEmail('')
setEmailError('')
}
const handleRemoveEmail = (email: string) => {
onEmailsChange(emails.filter((e) => e !== email))
}
return (
<div className='space-y-2'>
<Label className='font-medium text-sm'>Access Control</Label>
{/* Auth Type Selection */}
<div className='grid grid-cols-1 gap-3 md:grid-cols-3'>
{(['public', 'password', 'email'] as const).map((type) => (
<Card
key={type}
className={cn(
'cursor-pointer overflow-hidden shadow-none transition-colors hover:bg-accent/30',
authType === type
? 'border border-muted-foreground hover:bg-accent/50'
: 'border border-input'
)}
>
<CardContent className='relative flex flex-col items-center justify-center p-4 text-center'>
<button
type='button'
className='absolute inset-0 z-10 h-full w-full cursor-pointer'
onClick={() => !disabled && onAuthTypeChange(type)}
aria-label={`Select ${type} access`}
disabled={disabled}
/>
<div className='justify-center text-center align-middle'>
<h3 className='font-medium text-sm'>
{type === 'public' && 'Public Access'}
{type === 'password' && 'Password Protected'}
{type === 'email' && 'Email Access'}
</h3>
<p className='text-muted-foreground text-xs'>
{type === 'public' && 'Anyone can access your chat'}
{type === 'password' && 'Secure with a single password'}
{type === 'email' && 'Restrict to specific emails'}
</p>
</div>
</CardContent>
</Card>
))}
</div>
{/* Auth Settings */}
{authType === 'password' && (
<Card className='shadow-none'>
<CardContent className='p-4'>
<h3 className='mb-2 font-medium text-sm'>Password Settings</h3>
{isExistingChat && !password && (
<div className='mb-2 flex items-center text-muted-foreground text-xs'>
<div className='mr-2 rounded-full bg-primary/10 px-2 py-0.5 font-medium text-primary'>
Password set
</div>
<span>Current password is securely stored</span>
</div>
)}
<div className='relative'>
<Input
type={showPassword ? 'text' : 'password'}
placeholder={
isExistingChat
? 'Enter new password (leave empty to keep current)'
: 'Enter password'
}
value={password}
onChange={(e) => onPasswordChange(e.target.value)}
disabled={disabled}
className='pr-28'
required={!isExistingChat}
/>
<div className='absolute top-0 right-0 flex h-full'>
<Button
type='button'
variant='ghost'
size='icon'
onClick={generatePassword}
disabled={disabled}
className='px-2'
>
<RefreshCw className='h-4 w-4' />
<span className='sr-only'>Generate password</span>
</Button>
<Button
type='button'
variant='ghost'
size='icon'
onClick={() => copyToClipboard(password)}
disabled={!password || disabled}
className='px-2'
>
{copySuccess ? <Check className='h-4 w-4' /> : <Copy className='h-4 w-4' />}
<span className='sr-only'>Copy password</span>
</Button>
<Button
type='button'
variant='ghost'
size='icon'
onClick={() => setShowPassword(!showPassword)}
disabled={disabled}
className='px-2'
>
{showPassword ? <EyeOff className='h-4 w-4' /> : <Eye className='h-4 w-4' />}
<span className='sr-only'>
{showPassword ? 'Hide password' : 'Show password'}
</span>
</Button>
</div>
</div>
<p className='mt-2 text-muted-foreground text-xs'>
{isExistingChat
? 'Leaving this empty will keep the current password. Enter a new password to change it.'
: 'This password will be required to access your chat.'}
</p>
</CardContent>
</Card>
)}
{authType === 'email' && (
<Card className='shadow-none'>
<CardContent className='p-4'>
<h3 className='mb-2 font-medium text-sm'>Email Access Settings</h3>
<div className='flex gap-2'>
<Input
placeholder='user@example.com or @domain.com'
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
disabled={disabled}
className='flex-1'
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddEmail()
}
}}
/>
<Button
type='button'
onClick={handleAddEmail}
disabled={!newEmail.trim() || disabled}
className='shrink-0'
>
<Plus className='h-4 w-4' />
Add
</Button>
</div>
{emailError && <p className='mt-1 text-destructive text-sm'>{emailError}</p>}
{emails.length > 0 && (
<div className='mt-3 max-h-[150px] overflow-y-auto rounded-md border bg-background px-2 py-0 shadow-none'>
<ul className='divide-y divide-border'>
{emails.map((email) => (
<li key={email} className='relative'>
<div className='group my-1 flex items-center justify-between rounded-sm px-2 py-2 text-sm'>
<span className='font-medium text-foreground'>{email}</span>
<Button
type='button'
variant='ghost'
size='icon'
onClick={() => handleRemoveEmail(email)}
disabled={disabled}
className='h-7 w-7 opacity-70'
>
<Trash2 className='h-4 w-4' />
</Button>
</div>
</li>
))}
</ul>
</div>
)}
<p className='mt-2 text-muted-foreground text-xs'>
Add specific emails or entire domains (@example.com)
</p>
</CardContent>
</Card>
)}
{authType === 'public' && (
<Card className='shadow-none'>
<CardContent className='p-4'>
<h3 className='mb-2 font-medium text-sm'>Public Access Settings</h3>
<p className='text-muted-foreground text-xs'>
This chat will be publicly accessible to anyone with the link.
</p>
</CardContent>
</Card>
)}
{error && <p className='text-destructive text-sm'>{error}</p>}
</div>
)
}

View File

@@ -0,0 +1,69 @@
import { useEffect } from 'react'
import { Input, Label } from '@/components/ui'
import { getEmailDomain } from '@/lib/urls/utils'
import { cn } from '@/lib/utils'
import { useSubdomainValidation } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/hooks/use-subdomain-validation'
interface SubdomainInputProps {
value: string
onChange: (value: string) => void
originalSubdomain?: string
disabled?: boolean
onValidationChange?: (isValid: boolean) => void
}
const getDomainSuffix = (() => {
const suffix = `.${getEmailDomain()}`
return () => suffix
})()
export function SubdomainInput({
value,
onChange,
originalSubdomain,
disabled = false,
onValidationChange,
}: SubdomainInputProps) {
const { isChecking, error, isValid } = useSubdomainValidation(value, originalSubdomain)
// Notify parent of validation changes
useEffect(() => {
onValidationChange?.(isValid)
}, [isValid, onValidationChange])
const handleChange = (newValue: string) => {
const lowercaseValue = newValue.toLowerCase()
onChange(lowercaseValue)
}
return (
<div className='space-y-2'>
<Label htmlFor='subdomain' className='font-medium text-sm'>
Subdomain
</Label>
<div className='relative flex items-center rounded-md ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2'>
<Input
id='subdomain'
placeholder='company-name'
value={value}
onChange={(e) => handleChange(e.target.value)}
required
disabled={disabled}
className={cn(
'rounded-r-none border-r-0 focus-visible:ring-0 focus-visible:ring-offset-0',
error && 'border-destructive focus-visible:border-destructive'
)}
/>
<div className='flex h-10 items-center whitespace-nowrap rounded-r-md border border-l-0 bg-muted px-3 font-medium text-muted-foreground text-sm'>
{getDomainSuffix()}
</div>
{isChecking && (
<div className='absolute right-14 flex items-center'>
<div className='h-4 w-4 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600' />
</div>
)}
</div>
{error && <p className='mt-1 text-destructive text-sm'>{error}</p>}
</div>
)
}

View File

@@ -0,0 +1,154 @@
import { useCallback, useState } from 'react'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console/logger'
import type { ChatFormData } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/hooks/use-chat-form'
import type { OutputConfig } from '@/stores/panel/chat/types'
const logger = createLogger('ChatDeployment')
export interface ChatDeploymentState {
isLoading: boolean
error: string | null
deployedUrl: string | null
}
const chatSchema = z.object({
workflowId: z.string().min(1, 'Workflow ID is required'),
subdomain: z
.string()
.min(1, 'Subdomain is required')
.regex(/^[a-z0-9-]+$/, 'Subdomain can only contain lowercase letters, numbers, and hyphens'),
title: z.string().min(1, 'Title is required'),
description: z.string().optional(),
customizations: z.object({
primaryColor: z.string(),
welcomeMessage: z.string(),
}),
authType: z.enum(['public', 'password', 'email']).default('public'),
password: z.string().optional(),
allowedEmails: z.array(z.string()).optional().default([]),
outputConfigs: z
.array(
z.object({
blockId: z.string(),
path: z.string(),
})
)
.optional()
.default([]),
})
export function useChatDeployment() {
const [state, setState] = useState<ChatDeploymentState>({
isLoading: false,
error: null,
deployedUrl: null,
})
const deployChat = useCallback(
async (
workflowId: string,
formData: ChatFormData,
deploymentInfo: { apiKey: string } | null,
existingChatId?: string
) => {
setState({ isLoading: true, error: null, deployedUrl: null })
try {
// Prepare output configs
const outputConfigs: OutputConfig[] = formData.selectedOutputBlocks
.map((outputId) => {
const firstUnderscoreIndex = outputId.indexOf('_')
if (firstUnderscoreIndex !== -1) {
const blockId = outputId.substring(0, firstUnderscoreIndex)
const path = outputId.substring(firstUnderscoreIndex + 1)
if (blockId && path) {
return { blockId, path }
}
}
return null
})
.filter(Boolean) as OutputConfig[]
// Create request payload
const payload = {
workflowId,
subdomain: formData.subdomain.trim(),
title: formData.title.trim(),
description: formData.description.trim(),
customizations: {
primaryColor: '#802FFF',
welcomeMessage: formData.welcomeMessage.trim(),
},
authType: formData.authType,
password: formData.authType === 'password' ? formData.password : undefined,
allowedEmails: formData.authType === 'email' ? formData.emails : [],
outputConfigs,
apiKey: deploymentInfo?.apiKey,
deployApiEnabled: !existingChatId, // Only deploy API for new chats
}
// Validate with Zod
chatSchema.parse(payload)
// Determine endpoint and method
const endpoint = existingChatId ? `/api/chat/edit/${existingChatId}` : '/api/chat'
const method = existingChatId ? 'PATCH' : 'POST'
const response = await fetch(endpoint, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
const result = await response.json()
if (!response.ok) {
// Handle subdomain conflict specifically
if (result.error === 'Subdomain already in use') {
throw new Error('This subdomain is already in use')
}
throw new Error(result.error || `Failed to ${existingChatId ? 'update' : 'deploy'} chat`)
}
if (!result.chatUrl) {
throw new Error('Response missing chatUrl')
}
setState({
isLoading: false,
error: null,
deployedUrl: result.chatUrl,
})
logger.info(`Chat ${existingChatId ? 'updated' : 'deployed'} successfully:`, result.chatUrl)
return result.chatUrl
} catch (error: any) {
const errorMessage = error.message || 'An unexpected error occurred'
setState({
isLoading: false,
error: errorMessage,
deployedUrl: null,
})
logger.error(`Failed to ${existingChatId ? 'update' : 'deploy'} chat:`, error)
throw error
}
},
[]
)
const reset = useCallback(() => {
setState({
isLoading: false,
error: null,
deployedUrl: null,
})
}, [])
return {
...state,
deployChat,
reset,
}
}

View File

@@ -0,0 +1,112 @@
import { useCallback, useState } from 'react'
export type AuthType = 'public' | 'password' | 'email'
export interface ChatFormData {
subdomain: string
title: string
description: string
authType: AuthType
password: string
emails: string[]
welcomeMessage: string
selectedOutputBlocks: string[]
}
export interface ChatFormErrors {
subdomain?: string
title?: string
password?: string
emails?: string
outputBlocks?: string
general?: string
}
const initialFormData: ChatFormData = {
subdomain: '',
title: '',
description: '',
authType: 'public',
password: '',
emails: [],
welcomeMessage: 'Hi there! How can I help you today?',
selectedOutputBlocks: [],
}
export function useChatForm(initialData?: Partial<ChatFormData>) {
const [formData, setFormData] = useState<ChatFormData>({
...initialFormData,
...initialData,
})
const [errors, setErrors] = useState<ChatFormErrors>({})
const updateField = useCallback(
<K extends keyof ChatFormData>(field: K, value: ChatFormData[K]) => {
setFormData((prev) => ({ ...prev, [field]: value }))
// Clear error when user starts typing
if (field in errors && errors[field as keyof ChatFormErrors]) {
setErrors((prev) => ({ ...prev, [field]: undefined }))
}
},
[errors]
)
const setError = useCallback((field: keyof ChatFormErrors, message: string) => {
setErrors((prev) => ({ ...prev, [field]: message }))
}, [])
const clearError = useCallback((field: keyof ChatFormErrors) => {
setErrors((prev) => ({ ...prev, [field]: undefined }))
}, [])
const clearAllErrors = useCallback(() => {
setErrors({})
}, [])
const validateForm = useCallback((): boolean => {
const newErrors: ChatFormErrors = {}
if (!formData.subdomain.trim()) {
newErrors.subdomain = 'Subdomain is required'
} else if (!/^[a-z0-9-]+$/.test(formData.subdomain)) {
newErrors.subdomain = 'Subdomain can only contain lowercase letters, numbers, and hyphens'
}
if (!formData.title.trim()) {
newErrors.title = 'Title is required'
}
if (formData.authType === 'password' && !formData.password.trim()) {
newErrors.password = 'Password is required when using password protection'
}
if (formData.authType === 'email' && formData.emails.length === 0) {
newErrors.emails = 'At least one email or domain is required when using email access control'
}
if (formData.selectedOutputBlocks.length === 0) {
newErrors.outputBlocks = 'Please select at least one output block'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}, [formData])
const resetForm = useCallback(() => {
setFormData(initialFormData)
setErrors({})
}, [])
return {
formData,
errors,
updateField,
setError,
clearError,
clearAllErrors,
validateForm,
resetForm,
setFormData,
}
}

View File

@@ -0,0 +1,70 @@
import { useEffect, useRef, useState } from 'react'
export function useSubdomainValidation(subdomain: string, originalSubdomain?: string) {
const [isChecking, setIsChecking] = useState(false)
const [error, setError] = useState<string | null>(null)
const [isValid, setIsValid] = useState(false)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
// Clear previous timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// Reset states immediately when subdomain changes
setError(null)
setIsValid(false)
setIsChecking(false)
// Skip validation if empty or same as original
if (!subdomain.trim()) {
return
}
if (subdomain === originalSubdomain) {
setIsValid(true)
return
}
// Validate format first
if (!/^[a-z0-9-]+$/.test(subdomain)) {
setError('Subdomain can only contain lowercase letters, numbers, and hyphens')
return
}
// Debounce API call
setIsChecking(true)
timeoutRef.current = setTimeout(async () => {
try {
const response = await fetch(
`/api/chat/subdomains/validate?subdomain=${encodeURIComponent(subdomain)}`
)
const data = await response.json()
if (!response.ok || !data.available) {
setError(data.error || 'This subdomain is already in use')
setIsValid(false)
} else {
setError(null)
setIsValid(true)
}
} catch (error) {
setError('Error checking subdomain availability')
setIsValid(false)
} finally {
setIsChecking(false)
}
}, 500)
// Cleanup function
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [subdomain, originalSubdomain])
return { isChecking, error, isValid }
}

View File

@@ -1,30 +1,16 @@
'use client'
import { useEffect, useState } from 'react'
import { Info, Loader2, X } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { CopyButton } from '@/components/ui/copy-button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Loader2, X } from 'lucide-react'
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import {
ChatDeploy,
DeployForm,
DeploymentInfo,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components'
import { ChatDeploy } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -93,12 +79,10 @@ export function DeployModal({
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
const [keysLoaded, setKeysLoaded] = useState(false)
const [activeTab, setActiveTab] = useState<TabView>('api')
const [isChatDeploying, setIsChatDeploying] = useState(false)
const [chatSubmitting, setChatSubmitting] = useState(false)
const [apiDeployError, setApiDeployError] = useState<string | null>(null)
const [chatExists, setChatExists] = useState(false)
const [deployedChatUrl, setDeployedChatUrl] = useState<string | null>(null)
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false)
const [isChatFormValid, setIsChatFormValid] = useState(false)
// Generate an example input format for the API request
const getInputFormatExample = () => {
@@ -173,20 +157,16 @@ export function DeployModal({
if (response.ok) {
const data = await response.json()
if (data.isDeployed && data.deployment && data.deployment.chatUrl) {
setDeployedChatUrl(data.deployment.chatUrl)
if (data.isDeployed && data.deployment) {
setChatExists(true)
} else {
setDeployedChatUrl(null)
setChatExists(false)
}
} else {
setDeployedChatUrl(null)
setChatExists(false)
}
} catch (error) {
logger.error('Error fetching chat deployment info:', { error })
setDeployedChatUrl(null)
setChatExists(false)
} finally {
setIsLoading(false)
@@ -333,7 +313,6 @@ export function DeployModal({
setDeploymentStatus(workflowId, false)
// Reset chat deployment info
setDeployedChatUrl(null)
setChatExists(false)
// Close the modal
@@ -393,62 +372,18 @@ export function DeployModal({
// Custom close handler to ensure we clean up loading states
const handleCloseModal = () => {
setIsSubmitting(false)
setIsChatDeploying(false)
setChatSubmitting(false)
onOpenChange(false)
}
// Add a new handler for chat undeploy
const handleChatUndeploy = async () => {
try {
setIsUndeploying(true)
// First get the chat deployment info
const response = await fetch(`/api/workflows/${workflowId}/chat/status`)
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || 'Failed to get chat info')
}
const data = await response.json()
if (!data.isDeployed || !data.deployment || !data.deployment.id) {
throw new Error('No active chat deployment found')
}
// Delete the chat
const deleteResponse = await fetch(`/api/chat/edit/${data.deployment.id}`, {
method: 'DELETE',
})
if (!deleteResponse.ok) {
const errorData = await deleteResponse.json()
throw new Error(errorData.error || 'Failed to undeploy chat')
}
// Reset chat deployment info
setDeployedChatUrl(null)
setChatExists(false)
// Close the modal
onOpenChange(false)
} catch (error: any) {
logger.error('Error undeploying chat:', { error })
} finally {
setIsUndeploying(false)
setShowDeleteConfirmation(false)
}
}
// Find or create appropriate method to handle chat deployment
// Handle chat form submission
const handleChatSubmit = async () => {
// Check if workflow is deployed
if (!isDeployed) {
// Deploy workflow first
try {
setChatSubmitting(true)
setChatSubmitting(true)
// Call the API to deploy the workflow
try {
// Check if workflow is deployed first
if (!isDeployed) {
// Deploy workflow first
const response = await fetch(`/api/workflows/${workflowId}/deploy`, {
method: 'POST',
headers: {
@@ -474,58 +409,17 @@ export function DeployModal({
deployedAt ? new Date(deployedAt) : undefined,
apiKey
)
} catch (error: any) {
logger.error('Error auto-deploying workflow for chat:', { error })
setChatSubmitting(false)
return
}
// Trigger form submission in the ChatDeploy component
const form = document.querySelector('.chat-deploy-form') as HTMLFormElement
if (form) {
form.requestSubmit()
}
} catch (error: any) {
logger.error('Error auto-deploying workflow for chat:', { error })
setChatSubmitting(false)
}
// Now submit the chat deploy form
const form = document.querySelector('.chat-deploy-form') as HTMLFormElement
if (form) {
form.requestSubmit()
}
}
const handleChatDeploymentComplete = () => {
setChatSubmitting(false)
}
// Render deployed chat view
const _renderDeployedChatView = () => {
if (!deployedChatUrl) {
return (
<div className='flex items-center justify-center py-12 text-muted-foreground'>
<div className='flex flex-col items-center gap-2'>
<Info className='h-5 w-5' />
<p className='text-sm'>No chat deployment information available</p>
</div>
</div>
)
}
return (
<div className='space-y-4'>
<Card className='border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-900/20'>
<CardContent className='p-6 text-green-800 dark:text-green-400'>
<h3 className='mb-2 font-medium text-base'>Chat Deployment Active</h3>
<p className='mb-3'>Your chat is available at:</p>
<div className='group relative rounded-md border border-green-200 bg-white/50 p-3 dark:border-green-900/50 dark:bg-gray-900/50'>
<a
href={deployedChatUrl}
target='_blank'
rel='noopener noreferrer'
className='block break-all pr-8 font-medium text-primary text-sm underline'
>
{deployedChatUrl}
</a>
<CopyButton text={deployedChatUrl || ''} />
</div>
</CardContent>
</Card>
</div>
)
}
return (
@@ -614,12 +508,11 @@ export function DeployModal({
{activeTab === 'chat' && (
<ChatDeploy
workflowId={workflowId || ''}
onClose={() => onOpenChange(false)}
deploymentInfo={deploymentInfo}
onChatExistsChange={setChatExists}
showDeleteConfirmation={showDeleteConfirmation}
setShowDeleteConfirmation={setShowDeleteConfirmation}
onDeploymentComplete={handleChatDeploymentComplete}
chatSubmitting={chatSubmitting}
setChatSubmitting={setChatSubmitting}
onValidationChange={setIsChatFormValid}
/>
)}
</div>
@@ -636,7 +529,7 @@ export function DeployModal({
<Button
type='button'
onClick={() => onDeploy({ apiKey: apiKeys.length > 0 ? apiKeys[0].key : '' })}
disabled={isSubmitting || (!keysLoaded && !apiKeys.length) || isChatDeploying}
disabled={isSubmitting || (!keysLoaded && !apiKeys.length)}
className={cn(
'gap-2 font-medium',
'bg-[#802FFF] hover:bg-[#7028E6]',
@@ -663,69 +556,29 @@ export function DeployModal({
Cancel
</Button>
<div className='flex gap-2'>
{chatExists && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant='destructive' disabled={chatSubmitting || isUndeploying}>
{isUndeploying ? (
<>
<Loader2 className='mr-1.5 h-3.5 w-3.5 animate-spin' />
Undeploying...
</>
) : (
'Delete'
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Chat</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this chat? This will remove the chat
interface and make it unavailable to external users.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleChatUndeploy}
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Button
type='button'
onClick={handleChatSubmit}
disabled={chatSubmitting || !isChatFormValid}
className={cn(
'gap-2 font-medium',
'bg-[#802FFF] hover:bg-[#7028E6]',
'shadow-[0_0_0_0_#802FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'text-white transition-all duration-200',
'disabled:opacity-50 disabled:hover:bg-[#802FFF] disabled:hover:shadow-none'
)}
<Button
type='button'
onClick={handleChatSubmit}
disabled={chatSubmitting}
className={cn(
'gap-2 font-medium',
'bg-[#802FFF] hover:bg-[#7028E6]',
'shadow-[0_0_0_0_#802FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'text-white transition-all duration-200',
'disabled:opacity-50 disabled:hover:bg-[#802FFF] disabled:hover:shadow-none'
)}
>
{chatSubmitting ? (
<>
<Loader2 className='mr-1.5 h-3.5 w-3.5 animate-spin' />
{isDeployed
? chatExists
? 'Updating...'
: 'Deploying...'
: 'Deploying Workflow...'}
</>
) : chatExists ? (
'Update'
) : (
'Deploy Chat'
)}
</Button>
</div>
>
{chatSubmitting ? (
<>
<Loader2 className='mr-1.5 h-3.5 w-3.5 animate-spin' />
Deploying...
</>
) : chatExists ? (
'Update'
) : (
'Deploy Chat'
)}
</Button>
</div>
)}
</DialogContent>