mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement: modal UI (#2202)
* fix: trigger-save delete modal * improvement: old modal styling
This commit is contained in:
+88
-107
@@ -1,9 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { AlertCircle, Loader2, X } from 'lucide-react'
|
||||
import { AlertCircle, Loader2 } from 'lucide-react'
|
||||
import { Button, Textarea } from '@/components/emcn'
|
||||
import { Modal, ModalContent, ModalTitle } from '@/components/emcn/components/modal/modal'
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@/components/emcn/components/modal/modal'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ChunkData, DocumentData } from '@/stores/knowledge/store'
|
||||
@@ -113,132 +119,107 @@ export function CreateChunkModal({
|
||||
return (
|
||||
<>
|
||||
<Modal open={open} onOpenChange={handleCloseAttempt}>
|
||||
<ModalContent
|
||||
className='flex h-[74vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'
|
||||
showClose={false}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Create Chunk
|
||||
</ModalTitle>
|
||||
<Button variant='ghost' className='h-8 w-8 p-0' onClick={handleCloseAttempt}>
|
||||
<X className='h-4 w-4' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ModalContent className='h-[74vh] sm:max-w-[600px]'>
|
||||
<ModalHeader>Create Chunk</ModalHeader>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden'>
|
||||
<form className='flex min-h-0 flex-1 flex-col'>
|
||||
{/* Scrollable Content */}
|
||||
<div className='scrollbar-hide min-h-0 flex-1 overflow-y-auto pb-20'>
|
||||
<div className='flex min-h-full flex-col px-6'>
|
||||
<div className='flex flex-1 flex-col space-y-[12px] pt-0 pb-6'>
|
||||
{/* Document Info Section */}
|
||||
<div className='flex-shrink-0 space-y-[8px]'>
|
||||
<div className='flex items-center gap-3 rounded-lg border bg-muted/30 p-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='font-medium text-sm'>
|
||||
{document?.filename || 'Unknown Document'}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Adding chunk to this document
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-red-600' />
|
||||
<p className='text-red-800 text-sm'>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Input Section - Expands to fill space */}
|
||||
<div className='flex min-h-0 flex-1 flex-col space-y-[8px]'>
|
||||
<Label
|
||||
htmlFor='content'
|
||||
className='font-medium text-[13px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'
|
||||
>
|
||||
Chunk Content
|
||||
</Label>
|
||||
<Textarea
|
||||
id='content'
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder='Enter the content for this chunk...'
|
||||
className='min-h-0 flex-1 resize-none'
|
||||
disabled={isCreating}
|
||||
/>
|
||||
</div>
|
||||
<form className='flex min-h-0 flex-1 flex-col'>
|
||||
<ModalBody>
|
||||
<div className='space-y-[12px]'>
|
||||
{/* Document Info Section */}
|
||||
<div className='flex items-center gap-3 rounded-lg border p-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='font-medium text-[var(--text-primary)] text-sm'>
|
||||
{document?.filename || 'Unknown Document'}
|
||||
</p>
|
||||
<p className='text-[var(--text-tertiary)] text-xs'>
|
||||
Adding chunk to this document
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed Footer with Actions */}
|
||||
<div className='absolute inset-x-0 bottom-0 bg-[var(--surface-1)] dark:bg-[var(--surface-1)]'>
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleCloseAttempt}
|
||||
type='button'
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-[var(--text-error)]' />
|
||||
<p className='text-[var(--text-error)] text-sm'>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Input Section */}
|
||||
<div className='space-y-[8px]'>
|
||||
<Label
|
||||
htmlFor='content'
|
||||
className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'
|
||||
>
|
||||
Chunk Content
|
||||
</Label>
|
||||
<Textarea
|
||||
id='content'
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder='Enter the content for this chunk...'
|
||||
rows={10}
|
||||
disabled={isCreating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleCreateChunk}
|
||||
type='button'
|
||||
disabled={!isFormValid || isCreating}
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create Chunk'
|
||||
)}
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleCloseAttempt}
|
||||
type='button'
|
||||
disabled={isCreating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleCreateChunk}
|
||||
type='button'
|
||||
disabled={!isFormValid || isCreating}
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
'Create Chunk'
|
||||
)}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* Unsaved Changes Alert */}
|
||||
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
|
||||
<ModalContent className='flex flex-col gap-0 p-0'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Discard changes?
|
||||
</ModalTitle>
|
||||
<p className='mt-2 text-[12px] text-[var(--text-secondary)] dark:text-[var(--text-secondary)]'>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Discard Changes</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
You have unsaved changes. Are you sure you want to close without saving?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => setShowUnsavedChangesAlert(false)}
|
||||
type='button'
|
||||
>
|
||||
Keep editing
|
||||
Keep Editing
|
||||
</Button>
|
||||
<Button variant='primary' onClick={handleConfirmDiscard} type='button'>
|
||||
Discard changes
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleConfirmDiscard}
|
||||
type='button'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
+11
-25
@@ -2,15 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ChunkData } from '@/stores/knowledge/store'
|
||||
@@ -76,29 +68,23 @@ export function DeleteChunkModal({
|
||||
|
||||
return (
|
||||
<Modal open={isOpen} onOpenChange={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete Chunk</ModalTitle>
|
||||
<ModalDescription>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Delete Chunk</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Are you sure you want to delete this chunk?{' '}
|
||||
<span className='text-[var(--text-error)] dark:text-[var(--text-error)]'>
|
||||
This action cannot be undone.
|
||||
</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
disabled={isDeleting}
|
||||
onClick={onClose}
|
||||
className='h-[32px] px-[12px]'
|
||||
>
|
||||
<Button variant='active' disabled={isDeleting} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleDeleteChunk}
|
||||
disabled={isDeleting}
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
|
||||
+130
-152
@@ -6,8 +6,10 @@ import {
|
||||
Button,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalTitle,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
@@ -169,179 +171,155 @@ export function EditChunkModal({
|
||||
return (
|
||||
<>
|
||||
<Modal open={isOpen} onOpenChange={handleCloseAttempt}>
|
||||
<ModalContent
|
||||
className='flex h-[74vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[600px]'
|
||||
showClose={false}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Edit Chunk
|
||||
</ModalTitle>
|
||||
<ModalContent className='h-[74vh] sm:max-w-[600px]'>
|
||||
<div className='flex items-center justify-between px-[16px] py-[10px]'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<span className='font-medium text-[16px] text-[var(--text-primary)]'>Edit Chunk</span>
|
||||
|
||||
{/* Navigation Controls */}
|
||||
<div className='flex items-center gap-1'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
asChild
|
||||
onFocus={(e) => e.preventDefault()}
|
||||
onBlur={(e) => e.preventDefault()}
|
||||
{/* Navigation Controls */}
|
||||
<div className='flex items-center gap-1'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
asChild
|
||||
onFocus={(e) => e.preventDefault()}
|
||||
onBlur={(e) => e.preventDefault()}
|
||||
>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => handleNavigate('prev')}
|
||||
disabled={!canNavigatePrev || isNavigating || isSaving}
|
||||
className='h-8 w-8 p-0'
|
||||
>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => handleNavigate('prev')}
|
||||
disabled={!canNavigatePrev || isNavigating || isSaving}
|
||||
className='h-8 w-8 p-0'
|
||||
>
|
||||
<ChevronUp className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom'>
|
||||
Previous chunk{' '}
|
||||
{currentPage > 1 && currentChunkIndex === 0 ? '(previous page)' : ''}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<ChevronUp className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom'>
|
||||
Previous chunk{' '}
|
||||
{currentPage > 1 && currentChunkIndex === 0 ? '(previous page)' : ''}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
asChild
|
||||
onFocus={(e) => e.preventDefault()}
|
||||
onBlur={(e) => e.preventDefault()}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger
|
||||
asChild
|
||||
onFocus={(e) => e.preventDefault()}
|
||||
onBlur={(e) => e.preventDefault()}
|
||||
>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => handleNavigate('next')}
|
||||
disabled={!canNavigateNext || isNavigating || isSaving}
|
||||
className='h-8 w-8 p-0'
|
||||
>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => handleNavigate('next')}
|
||||
disabled={!canNavigateNext || isNavigating || isSaving}
|
||||
className='h-8 w-8 p-0'
|
||||
>
|
||||
<ChevronDown className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom'>
|
||||
Next chunk{' '}
|
||||
{currentPage < totalPages && currentChunkIndex === allChunks.length - 1
|
||||
? '(next page)'
|
||||
: ''}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
<ChevronDown className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='bottom'>
|
||||
Next chunk{' '}
|
||||
{currentPage < totalPages && currentChunkIndex === allChunks.length - 1
|
||||
? '(next page)'
|
||||
: ''}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
|
||||
<Button variant='ghost' className='h-8 w-8 p-0' onClick={handleCloseAttempt}>
|
||||
<X className='h-4 w-4' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant='ghost' className='h-[16px] w-[16px] p-0' onClick={handleCloseAttempt}>
|
||||
<X className='h-[16px] w-[16px]' />
|
||||
<span className='sr-only'>Close</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden'>
|
||||
<form className='flex min-h-0 flex-1 flex-col'>
|
||||
{/* Scrollable Content */}
|
||||
<div className='scrollbar-hide min-h-0 flex-1 overflow-y-auto pb-20'>
|
||||
<div className='flex min-h-full flex-col px-6'>
|
||||
<div className='flex flex-1 flex-col space-y-[12px] pt-0 pb-6'>
|
||||
{/* Document Info Section */}
|
||||
<div className='flex-shrink-0 space-y-[8px]'>
|
||||
<div className='flex items-center gap-3 rounded-lg border bg-muted/30 p-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='font-medium text-sm'>
|
||||
{document?.filename || 'Unknown Document'}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Editing chunk #{chunk.chunkIndex} • Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-red-200 bg-red-50 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-red-600' />
|
||||
<p className='text-red-800 text-sm'>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Input Section - Expands to fill space */}
|
||||
<div className='flex min-h-0 flex-1 flex-col space-y-[8px]'>
|
||||
<Label
|
||||
htmlFor='content'
|
||||
className='font-medium text-[13px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'
|
||||
>
|
||||
Chunk Content
|
||||
</Label>
|
||||
<Textarea
|
||||
id='content'
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
placeholder={
|
||||
userPermissions.canEdit ? 'Enter chunk content...' : 'Read-only view'
|
||||
}
|
||||
className='min-h-0 flex-1 resize-none'
|
||||
disabled={isSaving || isNavigating || !userPermissions.canEdit}
|
||||
readOnly={!userPermissions.canEdit}
|
||||
/>
|
||||
</div>
|
||||
<form className='flex min-h-0 flex-1 flex-col'>
|
||||
<ModalBody>
|
||||
<div className='space-y-[12px]'>
|
||||
{/* Document Info Section */}
|
||||
<div className='flex items-center gap-3 rounded-lg border p-4'>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<p className='font-medium text-[var(--text-primary)] text-sm'>
|
||||
{document?.filename || 'Unknown Document'}
|
||||
</p>
|
||||
<p className='text-[var(--text-tertiary)] text-xs'>
|
||||
Editing chunk #{chunk.chunkIndex} • Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed Footer with Actions */}
|
||||
<div className='absolute inset-x-0 bottom-0 bg-[var(--surface-1)] dark:bg-[var(--surface-1)]'>
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleCloseAttempt}
|
||||
type='button'
|
||||
disabled={isSaving || isNavigating}
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className='flex items-center gap-2 rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 p-3'>
|
||||
<AlertCircle className='h-4 w-4 text-[var(--text-error)]' />
|
||||
<p className='text-[var(--text-error)] text-sm'>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Input Section */}
|
||||
<div className='space-y-[8px]'>
|
||||
<Label
|
||||
htmlFor='content'
|
||||
className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{userPermissions.canEdit && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSaveContent}
|
||||
type='button'
|
||||
disabled={!isFormValid || isSaving || !hasUnsavedChanges || isNavigating}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
Chunk Content
|
||||
</Label>
|
||||
<Textarea
|
||||
id='content'
|
||||
value={editedContent}
|
||||
onChange={(e) => setEditedContent(e.target.value)}
|
||||
placeholder={
|
||||
userPermissions.canEdit ? 'Enter chunk content...' : 'Read-only view'
|
||||
}
|
||||
rows={10}
|
||||
disabled={isSaving || isNavigating || !userPermissions.canEdit}
|
||||
readOnly={!userPermissions.canEdit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleCloseAttempt}
|
||||
type='button'
|
||||
disabled={isSaving || isNavigating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
{userPermissions.canEdit && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSaveContent}
|
||||
type='button'
|
||||
disabled={!isFormValid || isSaving || !hasUnsavedChanges || isNavigating}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
{/* Unsaved Changes Alert */}
|
||||
<Modal open={showUnsavedChangesAlert} onOpenChange={setShowUnsavedChangesAlert}>
|
||||
<ModalContent className='flex flex-col gap-0 p-0'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Unsaved Changes
|
||||
</ModalTitle>
|
||||
<p className='mt-2 text-[12px] text-[var(--text-secondary)] dark:text-[var(--text-secondary)]'>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Unsaved Changes</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
You have unsaved changes to this chunk content.
|
||||
{pendingNavigation
|
||||
? ' Do you want to discard your changes and navigate to the next chunk?'
|
||||
: ' Are you sure you want to discard your changes and close the editor?'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => {
|
||||
@@ -356,11 +334,11 @@ export function EditChunkModal({
|
||||
variant='primary'
|
||||
onClick={handleConfirmDiscard}
|
||||
type='button'
|
||||
className='bg-[var(--text-error)] hover:bg-[var(--text-error)] dark:bg-[var(--text-error)] dark:hover:bg-[var(--text-error)]'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@@ -19,11 +19,10 @@ import { useParams, useRouter } from 'next/navigation'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
@@ -1143,31 +1142,29 @@ export function KnowledgeBase({
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete Knowledge Base</ModalTitle>
|
||||
<ModalDescription>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Delete Knowledge Base</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Are you sure you want to delete "{knowledgeBaseName}"? This will permanently delete
|
||||
the knowledge base and all {totalItems} document
|
||||
{totalItems === 1 ? '' : 's'} within it.{' '}
|
||||
<span className='text-[var(--text-error)] dark:text-[var(--text-error)]'>
|
||||
This action cannot be undone.
|
||||
</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
className='h-[32px] px-[12px]'
|
||||
variant='outline'
|
||||
variant='active'
|
||||
onClick={() => setShowDeleteDialog(false)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
variant='primary'
|
||||
onClick={handleDeleteKnowledgeBase}
|
||||
disabled={isDeleting}
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete Knowledge Base'}
|
||||
</Button>
|
||||
|
||||
+36
-42
@@ -3,14 +3,7 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { AlertCircle, Check, Loader2, X } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -156,15 +149,14 @@ export function UploadModal({
|
||||
|
||||
return (
|
||||
<Modal open={open} onOpenChange={handleClose}>
|
||||
<ModalContent className='flex max-h-[95vh] flex-col overflow-hidden sm:max-w-[600px]'>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Upload Documents</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalContent className='max-h-[95vh] sm:max-w-[600px]'>
|
||||
<ModalHeader>Upload Documents</ModalHeader>
|
||||
|
||||
<div className='flex-1 space-y-6 overflow-auto'>
|
||||
{/* File Upload Section */}
|
||||
<div className='space-y-3'>
|
||||
<Label>Select Files</Label>
|
||||
<ModalBody>
|
||||
<div className='space-y-[12px]'>
|
||||
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Select Files
|
||||
</Label>
|
||||
|
||||
{files.length === 0 ? (
|
||||
<div
|
||||
@@ -172,10 +164,10 @@ export function UploadModal({
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`relative flex cursor-pointer items-center justify-center rounded-lg border-2 border-dashed p-8 text-center transition-colors ${
|
||||
className={`relative flex cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-dashed p-8 text-center transition-colors ${
|
||||
isDragging
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-muted-foreground/25 hover:border-muted-foreground/40 hover:bg-muted/10'
|
||||
? 'border-[var(--brand-primary-hex)] bg-[var(--brand-primary-hex)]/5'
|
||||
: 'border-[var(--c-575757)] hover:border-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
@@ -187,10 +179,10 @@ export function UploadModal({
|
||||
multiple
|
||||
/>
|
||||
<div className='space-y-2'>
|
||||
<p className='font-medium text-sm'>
|
||||
<p className='font-medium text-[var(--text-primary)] text-sm'>
|
||||
{isDragging ? 'Drop files here!' : 'Drop files here or click to browse'}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
<p className='text-[var(--text-tertiary)] text-xs'>
|
||||
Supports PDF, DOC, DOCX, TXT, CSV, XLS, XLSX, MD, PPT, PPTX, HTML, JSON, YAML,
|
||||
YML (max 100MB each)
|
||||
</p>
|
||||
@@ -205,8 +197,8 @@ export function UploadModal({
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`cursor-pointer rounded-md border border-dashed p-3 text-center transition-colors ${
|
||||
isDragging
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-muted-foreground/25 hover:border-muted-foreground/40'
|
||||
? 'border-[var(--brand-primary-hex)] bg-[var(--brand-primary-hex)]/5'
|
||||
: 'border-[var(--c-575757)] hover:border-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
@@ -217,7 +209,7 @@ export function UploadModal({
|
||||
className='hidden'
|
||||
multiple
|
||||
/>
|
||||
<p className='text-sm'>
|
||||
<p className='text-[var(--text-primary)] text-sm'>
|
||||
{isDragging ? 'Drop more files here!' : 'Drop more files or click to browse'}
|
||||
</p>
|
||||
</div>
|
||||
@@ -238,12 +230,16 @@ export function UploadModal({
|
||||
{isCurrentlyUploading && (
|
||||
<Loader2 className='h-4 w-4 animate-spin text-[var(--brand-primary-hex)]' />
|
||||
)}
|
||||
{isCompleted && <Check className='h-4 w-4 text-green-500' />}
|
||||
{isFailed && <X className='h-4 w-4 text-red-500' />}
|
||||
<p className='truncate font-medium text-sm'>{file.name}</p>
|
||||
{isCompleted && (
|
||||
<Check className='h-4 w-4 text-[var(--text-success)]' />
|
||||
)}
|
||||
{isFailed && <X className='h-4 w-4 text-[var(--text-error)]' />}
|
||||
<p className='truncate font-medium text-[var(--text-primary)] text-sm'>
|
||||
{file.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
<p className='text-[var(--text-tertiary)] text-xs'>
|
||||
{formatFileSize(file.size)}
|
||||
</p>
|
||||
{isCurrentlyUploading && (
|
||||
@@ -253,7 +249,9 @@ export function UploadModal({
|
||||
)}
|
||||
</div>
|
||||
{isFailed && fileStatus?.error && (
|
||||
<p className='mt-1 text-red-500 text-xs'>{fileStatus.error}</p>
|
||||
<p className='mt-1 text-[var(--text-error)] text-xs'>
|
||||
{fileStatus.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
@@ -261,7 +259,7 @@ export function UploadModal({
|
||||
variant='ghost'
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={isUploading}
|
||||
className='h-8 w-8 p-0 text-muted-foreground hover:text-destructive'
|
||||
className='h-8 w-8 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-error)]'
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
</Button>
|
||||
@@ -275,36 +273,32 @@ export function UploadModal({
|
||||
|
||||
{/* Show upload error first, then file error only if no upload error */}
|
||||
{uploadError && (
|
||||
<div className='rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2'>
|
||||
<div className='rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 px-3 py-2'>
|
||||
<div className='flex items-start gap-2'>
|
||||
<AlertCircle className='mt-0.5 h-4 w-4 shrink-0 text-destructive' />
|
||||
<div className='flex-1 text-destructive text-sm'>{uploadError.message}</div>
|
||||
<AlertCircle className='mt-0.5 h-4 w-4 shrink-0 text-[var(--text-error)]' />
|
||||
<div className='flex-1 text-[var(--text-error)] text-sm'>
|
||||
{uploadError.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{fileError && !uploadError && (
|
||||
<div className='rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-destructive text-sm'>
|
||||
<div className='rounded-md border border-[var(--text-error)]/50 bg-[var(--text-error)]/10 px-3 py-2 text-[var(--text-error)] text-sm'>
|
||||
{fileError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={handleClose}
|
||||
disabled={isUploading}
|
||||
className='h-[32px] px-[12px]'
|
||||
>
|
||||
<Button variant='default' onClick={handleClose} disabled={isUploading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleUpload}
|
||||
disabled={files.length === 0 || isUploading}
|
||||
className='h-[32px] px-[12px]'
|
||||
>
|
||||
{isUploading
|
||||
? uploadProgress.stage === 'uploading'
|
||||
|
||||
+316
-293
@@ -7,7 +7,13 @@ import { useParams } from 'next/navigation'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
import { Button, Input, Label, Textarea } from '@/components/emcn'
|
||||
import { Modal, ModalContent, ModalTitle } from '@/components/emcn/components/modal/modal'
|
||||
import {
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@/components/emcn/components/modal/modal'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -310,317 +316,334 @@ export function CreateModal({ open, onOpenChange, onKnowledgeBaseCreated }: Crea
|
||||
|
||||
return (
|
||||
<Modal open={open} onOpenChange={handleClose}>
|
||||
<ModalContent className='flex h-[78vh] max-h-[95vh] flex-col gap-0 overflow-hidden p-0 sm:max-w-[750px]'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex-shrink-0 px-6 py-5'>
|
||||
<ModalTitle className='font-medium text-[14px] text-[var(--text-primary)] dark:text-[var(--text-primary)]'>
|
||||
Create Knowledge Base
|
||||
</ModalTitle>
|
||||
</div>
|
||||
<ModalContent className='h-[78vh] max-h-[95vh] sm:max-w-[750px]'>
|
||||
<ModalHeader>Create Knowledge Base</ModalHeader>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className='relative flex min-h-0 flex-1 flex-col overflow-hidden'>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
|
||||
{/* Scrollable Content */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className='scrollbar-hide min-h-0 flex-1 overflow-y-auto pb-20'
|
||||
>
|
||||
<div className='px-6'>
|
||||
{/* Show upload error first, then submit error only if no upload error */}
|
||||
{uploadError && (
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertTitle>Upload Error</AlertTitle>
|
||||
<AlertDescription>{uploadError.message}</AlertDescription>
|
||||
</Alert>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className='flex min-h-0 flex-1 flex-col'>
|
||||
<ModalBody>
|
||||
<div ref={scrollContainerRef} className='space-y-[12px]'>
|
||||
{/* Show upload error first, then submit error only if no upload error */}
|
||||
{uploadError && (
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertTitle>Upload Error</AlertTitle>
|
||||
<AlertDescription>{uploadError.message}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{submitStatus && submitStatus.type === 'error' && !uploadError && (
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{submitStatus.message}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Form Fields Section */}
|
||||
<div className='space-y-[8px]'>
|
||||
<Label
|
||||
htmlFor='name'
|
||||
className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'
|
||||
>
|
||||
Name *
|
||||
</Label>
|
||||
<Input
|
||||
id='name'
|
||||
placeholder='Enter knowledge base name'
|
||||
{...register('name')}
|
||||
className={errors.name ? 'border-[var(--text-error)]' : ''}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className='mt-1 text-[var(--text-error)] text-sm'>{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{submitStatus && submitStatus.type === 'error' && !uploadError && (
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{submitStatus.message}</AlertDescription>
|
||||
</Alert>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label
|
||||
htmlFor='description'
|
||||
className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'
|
||||
>
|
||||
Description
|
||||
</Label>
|
||||
<Textarea
|
||||
id='description'
|
||||
placeholder='Describe what this knowledge base contains (optional)'
|
||||
rows={3}
|
||||
{...register('description')}
|
||||
className={errors.description ? 'border-[var(--text-error)]' : ''}
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className='mt-1 text-[var(--text-error)] text-sm'>
|
||||
{errors.description.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Form Fields Section */}
|
||||
<div className='space-y-[12px]'>
|
||||
{/* Chunk Configuration Section */}
|
||||
<div className='space-y-[12px] rounded-lg border p-5'>
|
||||
<h3 className='font-medium text-[var(--text-primary)] text-sm'>
|
||||
Chunking Configuration
|
||||
</h3>
|
||||
|
||||
{/* Min and Max Chunk Size Row */}
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='name'>Name *</Label>
|
||||
<Label
|
||||
htmlFor='minChunkSize'
|
||||
className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'
|
||||
>
|
||||
Min Chunk Size
|
||||
</Label>
|
||||
<Input
|
||||
id='name'
|
||||
placeholder='Enter knowledge base name'
|
||||
{...register('name')}
|
||||
className={errors.name ? 'border-red-500' : ''}
|
||||
id='minChunkSize'
|
||||
type='number'
|
||||
placeholder='1'
|
||||
{...register('minChunkSize', { valueAsNumber: true })}
|
||||
className={errors.minChunkSize ? 'border-[var(--text-error)]' : ''}
|
||||
autoComplete='off'
|
||||
data-form-type='other'
|
||||
name='min-chunk-size'
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className='mt-1 text-red-500 text-sm'>{errors.name.message}</p>
|
||||
{errors.minChunkSize && (
|
||||
<p className='mt-1 text-[var(--text-error)] text-xs'>
|
||||
{errors.minChunkSize.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='description'>Description</Label>
|
||||
<Textarea
|
||||
id='description'
|
||||
placeholder='Describe what this knowledge base contains (optional)'
|
||||
rows={3}
|
||||
{...register('description')}
|
||||
className={errors.description ? 'border-red-500' : ''}
|
||||
<Label
|
||||
htmlFor='maxChunkSize'
|
||||
className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'
|
||||
>
|
||||
Max Chunk Size
|
||||
</Label>
|
||||
<Input
|
||||
id='maxChunkSize'
|
||||
type='number'
|
||||
placeholder='1024'
|
||||
{...register('maxChunkSize', { valueAsNumber: true })}
|
||||
className={errors.maxChunkSize ? 'border-[var(--text-error)]' : ''}
|
||||
autoComplete='off'
|
||||
data-form-type='other'
|
||||
name='max-chunk-size'
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className='mt-1 text-red-500 text-sm'>{errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Chunk Configuration Section */}
|
||||
<div className='space-y-[12px] rounded-lg border p-5'>
|
||||
<h3 className='font-medium text-foreground text-sm'>Chunking Configuration</h3>
|
||||
|
||||
{/* Min and Max Chunk Size Row */}
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='minChunkSize'>Min Chunk Size</Label>
|
||||
<Input
|
||||
id='minChunkSize'
|
||||
type='number'
|
||||
placeholder='1'
|
||||
{...register('minChunkSize', { valueAsNumber: true })}
|
||||
className={errors.minChunkSize ? 'border-red-500' : ''}
|
||||
autoComplete='off'
|
||||
data-form-type='other'
|
||||
name='min-chunk-size'
|
||||
/>
|
||||
{errors.minChunkSize && (
|
||||
<p className='mt-1 text-red-500 text-xs'>{errors.minChunkSize.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='maxChunkSize'>Max Chunk Size</Label>
|
||||
<Input
|
||||
id='maxChunkSize'
|
||||
type='number'
|
||||
placeholder='1024'
|
||||
{...register('maxChunkSize', { valueAsNumber: true })}
|
||||
className={errors.maxChunkSize ? 'border-red-500' : ''}
|
||||
autoComplete='off'
|
||||
data-form-type='other'
|
||||
name='max-chunk-size'
|
||||
/>
|
||||
{errors.maxChunkSize && (
|
||||
<p className='mt-1 text-red-500 text-xs'>{errors.maxChunkSize.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overlap Size */}
|
||||
<div className='space-y-[8px]'>
|
||||
<Label htmlFor='overlapSize'>Overlap Size</Label>
|
||||
<Input
|
||||
id='overlapSize'
|
||||
type='number'
|
||||
placeholder='200'
|
||||
{...register('overlapSize', { valueAsNumber: true })}
|
||||
className={errors.overlapSize ? 'border-red-500' : ''}
|
||||
autoComplete='off'
|
||||
data-form-type='other'
|
||||
name='overlap-size'
|
||||
/>
|
||||
{errors.overlapSize && (
|
||||
<p className='mt-1 text-red-500 text-xs'>{errors.overlapSize.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Configure how documents are split into chunks for processing. Smaller chunks
|
||||
provide more precise retrieval but may lose context.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* File Upload Section */}
|
||||
<div className='space-y-[12px]'>
|
||||
<Label>Upload Documents</Label>
|
||||
{files.length === 0 ? (
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`relative flex cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-dashed py-8 text-center transition-all duration-200 ${
|
||||
isDragging
|
||||
? 'border-purple-300 bg-purple-50 shadow-sm'
|
||||
: 'border-muted-foreground/25 hover:border-muted-foreground/40 hover:bg-muted/10'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept={ACCEPT_ATTRIBUTE}
|
||||
onChange={handleFileChange}
|
||||
className='hidden'
|
||||
multiple
|
||||
/>
|
||||
<div className='flex flex-col items-center gap-3'>
|
||||
<div className='space-y-1'>
|
||||
<p
|
||||
className={`font-medium text-sm transition-colors duration-200 ${
|
||||
isDragging ? 'text-purple-700' : ''
|
||||
}`}
|
||||
>
|
||||
{isDragging
|
||||
? 'Drop files here!'
|
||||
: 'Drop files here or click to browse'}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Supports PDF, DOC, DOCX, TXT, CSV, XLS, XLSX, MD, PPT, PPTX, HTML,
|
||||
JSON, YAML, YML (max 100MB each)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-2'>
|
||||
{/* Compact drop area at top of file list */}
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`cursor-pointer rounded-md border border-dashed p-3 text-center transition-all duration-200 ${
|
||||
isDragging
|
||||
? 'border-purple-300 bg-purple-50'
|
||||
: 'border-muted-foreground/25 hover:border-muted-foreground/40 hover:bg-muted/10'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept={ACCEPT_ATTRIBUTE}
|
||||
onChange={handleFileChange}
|
||||
className='hidden'
|
||||
multiple
|
||||
/>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<div>
|
||||
<p
|
||||
className={`font-medium text-sm transition-colors duration-200 ${
|
||||
isDragging ? 'text-purple-700' : ''
|
||||
}`}
|
||||
>
|
||||
{isDragging
|
||||
? 'Drop more files here!'
|
||||
: 'Drop more files or click to browse'}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
PDF, DOC, DOCX, TXT, CSV, XLS, XLSX, MD, PPT, PPTX, HTML (max 100MB
|
||||
each)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File list */}
|
||||
<div className='space-y-2'>
|
||||
{files.map((file, index) => {
|
||||
const fileStatus = uploadProgress.fileStatuses?.[index]
|
||||
const isCurrentlyUploading = fileStatus?.status === 'uploading'
|
||||
const isCompleted = fileStatus?.status === 'completed'
|
||||
const isFailed = fileStatus?.status === 'failed'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='flex items-center gap-3 rounded-md border p-3'
|
||||
>
|
||||
{getFileIcon(file.type, file.name)}
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{isCurrentlyUploading && (
|
||||
<Loader2 className='h-4 w-4 animate-spin text-[var(--brand-primary-hex)]' />
|
||||
)}
|
||||
{isCompleted && <Check className='h-4 w-4 text-green-500' />}
|
||||
{isFailed && <X className='h-4 w-4 text-red-500' />}
|
||||
<p className='truncate font-medium text-sm'>{file.name}</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{formatFileSize(file.size)}
|
||||
</p>
|
||||
{isCurrentlyUploading && (
|
||||
<div className='min-w-0 max-w-32 flex-1'>
|
||||
<Progress
|
||||
value={fileStatus?.progress || 0}
|
||||
className='h-1'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isFailed && fileStatus?.error && (
|
||||
<p className='mt-1 text-red-500 text-xs'>{fileStatus.error}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={isUploading}
|
||||
className='h-8 w-8 p-0 text-muted-foreground hover:text-destructive'
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{fileError && (
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{fileError}</AlertDescription>
|
||||
</Alert>
|
||||
{errors.maxChunkSize && (
|
||||
<p className='mt-1 text-[var(--text-error)] text-xs'>
|
||||
{errors.maxChunkSize.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed Footer with Actions */}
|
||||
<div className='absolute inset-x-0 bottom-0 bg-[var(--surface-1)] dark:bg-[var(--surface-1)]'>
|
||||
<div className='flex w-full items-center justify-between gap-[8px] px-6 py-4'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => handleClose(false)}
|
||||
type='button'
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
type='submit'
|
||||
disabled={isSubmitting || !nameValue?.trim()}
|
||||
>
|
||||
{isSubmitting
|
||||
? isUploading
|
||||
? uploadProgress.stage === 'uploading'
|
||||
? `Uploading ${uploadProgress.filesCompleted}/${uploadProgress.totalFiles}...`
|
||||
: uploadProgress.stage === 'processing'
|
||||
? 'Processing...'
|
||||
: 'Creating...'
|
||||
: 'Creating...'
|
||||
: 'Create Knowledge Base'}
|
||||
</Button>
|
||||
{/* Overlap Size */}
|
||||
<div className='space-y-[8px]'>
|
||||
<Label
|
||||
htmlFor='overlapSize'
|
||||
className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'
|
||||
>
|
||||
Overlap Size
|
||||
</Label>
|
||||
<Input
|
||||
id='overlapSize'
|
||||
type='number'
|
||||
placeholder='200'
|
||||
{...register('overlapSize', { valueAsNumber: true })}
|
||||
className={errors.overlapSize ? 'border-[var(--text-error)]' : ''}
|
||||
autoComplete='off'
|
||||
data-form-type='other'
|
||||
name='overlap-size'
|
||||
/>
|
||||
{errors.overlapSize && (
|
||||
<p className='mt-1 text-[var(--text-error)] text-xs'>
|
||||
{errors.overlapSize.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className='text-[var(--text-tertiary)] text-xs'>
|
||||
Configure how documents are split into chunks for processing. Smaller chunks
|
||||
provide more precise retrieval but may lose context.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* File Upload Section */}
|
||||
<div className='space-y-[12px]'>
|
||||
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
|
||||
Upload Documents
|
||||
</Label>
|
||||
{files.length === 0 ? (
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`relative flex cursor-pointer items-center justify-center rounded-lg border-[1.5px] border-dashed py-8 text-center transition-all duration-200 ${
|
||||
isDragging
|
||||
? 'border-[var(--brand-primary-hex)] bg-[var(--brand-primary-hex)]/5'
|
||||
: 'border-[var(--c-575757)] hover:border-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept={ACCEPT_ATTRIBUTE}
|
||||
onChange={handleFileChange}
|
||||
className='hidden'
|
||||
multiple
|
||||
/>
|
||||
<div className='flex flex-col items-center gap-3'>
|
||||
<div className='space-y-1'>
|
||||
<p
|
||||
className={`font-medium text-[var(--text-primary)] text-sm transition-colors duration-200 ${
|
||||
isDragging ? 'text-[var(--brand-primary-hex)]' : ''
|
||||
}`}
|
||||
>
|
||||
{isDragging ? 'Drop files here!' : 'Drop files here or click to browse'}
|
||||
</p>
|
||||
<p className='text-[var(--text-tertiary)] text-xs'>
|
||||
Supports PDF, DOC, DOCX, TXT, CSV, XLS, XLSX, MD, PPT, PPTX, HTML, JSON,
|
||||
YAML, YML (max 100MB each)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-2'>
|
||||
{/* Compact drop area at top of file list */}
|
||||
<div
|
||||
ref={dropZoneRef}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`cursor-pointer rounded-md border border-dashed p-3 text-center transition-all duration-200 ${
|
||||
isDragging
|
||||
? 'border-[var(--brand-primary-hex)] bg-[var(--brand-primary-hex)]/5'
|
||||
: 'border-[var(--c-575757)] hover:border-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept={ACCEPT_ATTRIBUTE}
|
||||
onChange={handleFileChange}
|
||||
className='hidden'
|
||||
multiple
|
||||
/>
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<div>
|
||||
<p
|
||||
className={`font-medium text-[var(--text-primary)] text-sm transition-colors duration-200 ${
|
||||
isDragging ? 'text-[var(--brand-primary-hex)]' : ''
|
||||
}`}
|
||||
>
|
||||
{isDragging
|
||||
? 'Drop more files here!'
|
||||
: 'Drop more files or click to browse'}
|
||||
</p>
|
||||
<p className='text-[var(--text-tertiary)] text-xs'>
|
||||
PDF, DOC, DOCX, TXT, CSV, XLS, XLSX, MD, PPT, PPTX, HTML (max 100MB
|
||||
each)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File list */}
|
||||
<div className='space-y-2'>
|
||||
{files.map((file, index) => {
|
||||
const fileStatus = uploadProgress.fileStatuses?.[index]
|
||||
const isCurrentlyUploading = fileStatus?.status === 'uploading'
|
||||
const isCompleted = fileStatus?.status === 'completed'
|
||||
const isFailed = fileStatus?.status === 'failed'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='flex items-center gap-3 rounded-md border p-3'
|
||||
>
|
||||
{getFileIcon(file.type, file.name)}
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{isCurrentlyUploading && (
|
||||
<Loader2 className='h-4 w-4 animate-spin text-[var(--brand-primary-hex)]' />
|
||||
)}
|
||||
{isCompleted && (
|
||||
<Check className='h-4 w-4 text-[var(--text-success)]' />
|
||||
)}
|
||||
{isFailed && <X className='h-4 w-4 text-[var(--text-error)]' />}
|
||||
<p className='truncate font-medium text-[var(--text-primary)] text-sm'>
|
||||
{file.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<p className='text-[var(--text-tertiary)] text-xs'>
|
||||
{formatFileSize(file.size)}
|
||||
</p>
|
||||
{isCurrentlyUploading && (
|
||||
<div className='min-w-0 max-w-32 flex-1'>
|
||||
<Progress value={fileStatus?.progress || 0} className='h-1' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isFailed && fileStatus?.error && (
|
||||
<p className='mt-1 text-[var(--text-error)] text-xs'>
|
||||
{fileStatus.error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
onClick={() => removeFile(index)}
|
||||
disabled={isUploading}
|
||||
className='h-8 w-8 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-error)]'
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{fileError && (
|
||||
<Alert variant='destructive'>
|
||||
<AlertCircle className='h-4 w-4' />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{fileError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => handleClose(false)}
|
||||
type='button'
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='primary' type='submit' disabled={isSubmitting || !nameValue?.trim()}>
|
||||
{isSubmitting
|
||||
? isUploading
|
||||
? uploadProgress.stage === 'uploading'
|
||||
? `Uploading ${uploadProgress.filesCompleted}/${uploadProgress.totalFiles}...`
|
||||
: uploadProgress.stage === 'processing'
|
||||
? 'Processing...'
|
||||
: 'Creating...'
|
||||
: 'Creating...'
|
||||
: 'Create Knowledge Base'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
+10
-21
@@ -1,14 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
@@ -479,26 +471,23 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
)}
|
||||
|
||||
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete schedule?</ModalTitle>
|
||||
<ModalDescription>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Delete Schedule</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Are you sure you want to delete this schedule configuration? This will stop the
|
||||
workflow from running automatically.{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => setShowDeleteDialog(false)}
|
||||
className='h-[32px] px-[12px]'
|
||||
>
|
||||
<Button variant='active' onClick={() => setShowDeleteDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleDeleteConfirm}
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)]'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
+14
-15
@@ -2,10 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalTitle,
|
||||
ModalHeader,
|
||||
} from '@/components/emcn/components'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
@@ -452,24 +452,23 @@ export function TriggerSave({
|
||||
)}
|
||||
|
||||
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<ModalContent>
|
||||
<ModalTitle>Delete trigger?</ModalTitle>
|
||||
<ModalDescription>
|
||||
Are you sure you want to delete this trigger configuration? This will remove the webhook
|
||||
and stop all incoming triggers.{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</ModalDescription>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Delete Trigger</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Are you sure you want to delete this trigger configuration? This will remove the
|
||||
webhook and stop all incoming triggers.{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => setShowDeleteDialog(false)}
|
||||
className='h-[32px] px-[12px]'
|
||||
>
|
||||
<Button variant='active' onClick={() => setShowDeleteDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleDeleteConfirm}
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)]'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
@@ -9,11 +9,10 @@ import {
|
||||
Copy,
|
||||
Layout,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
MoreHorizontal,
|
||||
Play,
|
||||
Popover,
|
||||
@@ -532,28 +531,28 @@ export function Panel() {
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<Modal open={isDeleteModalOpen} onOpenChange={setIsDeleteModalOpen}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete workflow?</ModalTitle>
|
||||
<ModalDescription>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Delete Workflow</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Deleting this workflow will permanently remove all associated blocks, executions, and
|
||||
configuration.{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
className='h-[32px] px-[12px]'
|
||||
variant='outline'
|
||||
variant='active'
|
||||
onClick={() => setIsDeleteModalOpen(false)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)]'
|
||||
variant='primary'
|
||||
onClick={handleDeleteWorkflow}
|
||||
disabled={isDeleting}
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
|
||||
+10
-15
@@ -17,11 +17,10 @@ import {
|
||||
import {
|
||||
Button as EmcnButton,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import {
|
||||
@@ -1165,27 +1164,23 @@ export function WebhookSettings({ workflowId, open, onOpenChange }: WebhookSetti
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Modal open={showDeleteDialog} onOpenChange={handleDeleteDialogClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete webhook?</ModalTitle>
|
||||
<ModalDescription>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Delete Webhook</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
This will permanently remove the webhook configuration and stop all notifications.{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<EmcnButton
|
||||
variant='outline'
|
||||
className='h-[32px] px-[12px]'
|
||||
disabled={isDeleting}
|
||||
onClick={handleDeleteDialogClose}
|
||||
>
|
||||
<EmcnButton variant='active' disabled={isDeleting} onClick={handleDeleteDialogClose}>
|
||||
Cancel
|
||||
</EmcnButton>
|
||||
<EmcnButton
|
||||
variant='primary'
|
||||
onClick={confirmDeleteWebhook}
|
||||
disabled={isDeleting}
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)]'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||
</EmcnButton>
|
||||
|
||||
+21
-33
@@ -2,15 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
import { useSession, useSubscription } from '@/lib/auth/auth-client'
|
||||
import { getSubscriptionStatus } from '@/lib/billing/client/utils'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
@@ -234,42 +226,38 @@ export function CancelSubscription({ subscription, subscriptionData }: CancelSub
|
||||
</div>
|
||||
|
||||
<Modal open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<ModalContent>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>
|
||||
<ModalTitle>
|
||||
{isCancelAtPeriodEnd ? 'Restore' : 'Cancel'} {subscription.plan} subscription?
|
||||
</ModalTitle>
|
||||
<ModalDescription>
|
||||
{isCancelAtPeriodEnd ? 'Restore' : 'Cancel'} {subscription.plan} Subscription
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
{isCancelAtPeriodEnd
|
||||
? 'Your subscription is set to cancel at the end of the billing period. Would you like to keep your subscription active?'
|
||||
: `You'll be redirected to Stripe to manage your subscription. You'll keep access until ${formatDate(
|
||||
periodEndDate
|
||||
)}, then downgrade to free plan.`}{' '}
|
||||
{!isCancelAtPeriodEnd && (
|
||||
<span className='text-[var(--text-error)] dark:text-[var(--text-error)]'>
|
||||
This action cannot be undone.
|
||||
</span>
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
)}
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
</p>
|
||||
|
||||
{!isCancelAtPeriodEnd && (
|
||||
<div className='py-2'>
|
||||
<div className='rounded-[8px] bg-[var(--surface-3)] p-3 text-sm'>
|
||||
<ul className='space-y-1 text-[var(--text-muted)] text-xs'>
|
||||
<li>• Keep all features until {formatDate(periodEndDate)}</li>
|
||||
<li>• No more charges</li>
|
||||
<li>• Data preserved</li>
|
||||
<li>• Can reactivate anytime</li>
|
||||
</ul>
|
||||
{!isCancelAtPeriodEnd && (
|
||||
<div className='mt-3'>
|
||||
<div className='rounded-[8px] bg-[var(--surface-3)] p-3 text-sm'>
|
||||
<ul className='space-y-1 text-[var(--text-muted)] text-xs'>
|
||||
<li>• Keep all features until {formatDate(periodEndDate)}</li>
|
||||
<li>• No more charges</li>
|
||||
<li>• Data preserved</li>
|
||||
<li>• Can reactivate anytime</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='h-[32px] px-[12px]'
|
||||
variant='active'
|
||||
onClick={isCancelAtPeriodEnd ? () => setIsDialogOpen(false) : handleKeep}
|
||||
disabled={isLoading}
|
||||
>
|
||||
|
||||
+36
-43
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
|
||||
interface RemoveMemberDialogProps {
|
||||
open: boolean
|
||||
@@ -33,52 +25,53 @@ export function RemoveMemberDialog({
|
||||
}: RemoveMemberDialogProps) {
|
||||
return (
|
||||
<Modal open={open} onOpenChange={onOpenChange}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>{isSelfRemoval ? 'Leave Organization' : 'Remove Team Member'}</ModalTitle>
|
||||
<ModalDescription>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>{isSelfRemoval ? 'Leave Organization' : 'Remove Team Member'}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
{isSelfRemoval
|
||||
? 'Are you sure you want to leave this organization? You will lose access to all team resources.'
|
||||
: `Are you sure you want to remove ${memberName} from the team?`}{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
</p>
|
||||
|
||||
{!isSelfRemoval && (
|
||||
<div className='py-4'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
id='reduce-seats'
|
||||
className='rounded-[4px]'
|
||||
checked={shouldReduceSeats}
|
||||
onChange={(e) => onShouldReduceSeatsChange(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor='reduce-seats' className='text-xs'>
|
||||
Also reduce seat count in my subscription
|
||||
</label>
|
||||
{!isSelfRemoval && (
|
||||
<div className='mt-4'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<input
|
||||
type='checkbox'
|
||||
id='reduce-seats'
|
||||
className='rounded-[4px]'
|
||||
checked={shouldReduceSeats}
|
||||
onChange={(e) => onShouldReduceSeatsChange(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor='reduce-seats' className='text-xs'>
|
||||
Also reduce seat count in my subscription
|
||||
</label>
|
||||
</div>
|
||||
<p className='mt-1 text-[var(--text-muted)] text-xs'>
|
||||
If selected, your team seat count will be reduced by 1, lowering your monthly
|
||||
billing.
|
||||
</p>
|
||||
</div>
|
||||
<p className='mt-1 text-[var(--text-muted)] text-xs'>
|
||||
If selected, your team seat count will be reduced by 1, lowering your monthly billing.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className='pb-2'>
|
||||
<p className='text-[#DC2626] text-[11px] leading-tight dark:text-[#F87171]'>
|
||||
{error instanceof Error && error.message ? error.message : String(error)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className='mt-2'>
|
||||
<p className='text-[#DC2626] text-[11px] leading-tight dark:text-[#F87171]'>
|
||||
{error instanceof Error && error.message ? error.message : String(error)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='outline' onClick={onCancel} className='h-[32px] px-[12px]'>
|
||||
<Button variant='active' onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={() => onConfirmRemove(shouldReduceSeats)}
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)]'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
{isSelfRemoval ? 'Leave Organization' : 'Remove'}
|
||||
</Button>
|
||||
|
||||
+23
-30
@@ -5,11 +5,10 @@ import { Eye, MoreHorizontal, Plus, Trash2, X } from 'lucide-react'
|
||||
import {
|
||||
Button as EmcnButton,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import {
|
||||
Button,
|
||||
@@ -449,46 +448,40 @@ export function KnowledgeBaseTags({ knowledgeBaseId }: KnowledgeBaseTagsProps) {
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Modal open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Delete Tag</ModalTitle>
|
||||
<ModalDescription asChild>
|
||||
<div>
|
||||
<div className='mb-2'>
|
||||
Are you sure you want to delete the "{selectedTag?.displayName}" tag? This will
|
||||
remove this tag from {selectedTagUsage?.documentCount || 0} document
|
||||
{selectedTagUsage?.documentCount !== 1 ? 's' : ''}.{' '}
|
||||
<span className='text-[var(--text-error)] dark:text-[var(--text-error)]'>
|
||||
This action cannot be undone.
|
||||
</span>
|
||||
</div>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Delete Tag</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Are you sure you want to delete the "{selectedTag?.displayName}" tag? This will remove
|
||||
this tag from {selectedTagUsage?.documentCount || 0} document
|
||||
{selectedTagUsage?.documentCount !== 1 ? 's' : ''}.{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</p>
|
||||
|
||||
{selectedTagUsage && selectedTagUsage.documentCount > 0 && (
|
||||
<div className='mt-4'>
|
||||
<div className='mb-2 font-medium text-sm'>Affected documents:</div>
|
||||
<DocumentList
|
||||
documents={selectedTagUsage.documents}
|
||||
totalCount={selectedTagUsage.documentCount}
|
||||
maxHeight='max-h-32'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedTagUsage && selectedTagUsage.documentCount > 0 && (
|
||||
<div className='mt-4'>
|
||||
<div className='mb-2 font-medium text-sm'>Affected documents:</div>
|
||||
<DocumentList
|
||||
documents={selectedTagUsage.documents}
|
||||
totalCount={selectedTagUsage.documentCount}
|
||||
maxHeight='max-h-32'
|
||||
/>
|
||||
</div>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<EmcnButton
|
||||
variant='outline'
|
||||
className='h-[32px] px-[12px]'
|
||||
variant='active'
|
||||
disabled={isDeleting}
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</EmcnButton>
|
||||
<EmcnButton
|
||||
variant='primary'
|
||||
onClick={confirmDeleteTag}
|
||||
disabled={isDeleting}
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete Tag'}
|
||||
</EmcnButton>
|
||||
|
||||
+65
-80
@@ -2,15 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Download, LogOut, Pencil, Plus, Send, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalDescription,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalTitle,
|
||||
} from '@/components/emcn'
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'
|
||||
import { Button as UIButton } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
@@ -632,14 +624,12 @@ export function WorkspaceSelector({
|
||||
|
||||
{/* Centralized Delete Workspace Dialog */}
|
||||
<Modal open={isDeleteDialogOpen} onOpenChange={handleDialogClose}>
|
||||
<ModalContent>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>
|
||||
<ModalTitle>
|
||||
{showTemplateChoice
|
||||
? 'Delete workspace with published templates?'
|
||||
: 'Delete workspace?'}
|
||||
</ModalTitle>
|
||||
<ModalDescription>
|
||||
{showTemplateChoice ? 'Delete Workspace with Published Templates' : 'Delete Workspace'}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
{showTemplateChoice ? (
|
||||
<>
|
||||
This workspace contains {templatesInfo?.count} published template
|
||||
@@ -658,52 +648,49 @@ export function WorkspaceSelector({
|
||||
<>
|
||||
Deleting this workspace will permanently remove all associated workflows, logs,
|
||||
and knowledge bases.{' '}
|
||||
<span className='text-[var(--text-error)] dark:text-[var(--text-error)]'>
|
||||
This action cannot be undone.
|
||||
</span>
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</>
|
||||
)}
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
|
||||
{showTemplateChoice ? (
|
||||
<div className='flex gap-2 py-2'>
|
||||
<Button
|
||||
onClick={() => handleTemplateAction('keep')}
|
||||
className='h-[32px] flex-1 px-[12px]'
|
||||
variant='outline'
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Keep Templates'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleTemplateAction('delete')}
|
||||
className='h-[32px] flex-1 bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete Templates'}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className='py-2'>
|
||||
<p className='mb-2 font-[360] text-sm'>
|
||||
Enter the workspace name{' '}
|
||||
<span className='font-semibold'>{workspaceToDelete?.name}</span> to confirm.
|
||||
</p>
|
||||
<Input
|
||||
value={deleteConfirmationName}
|
||||
onChange={(e) => setDeleteConfirmationName(e.target.value)}
|
||||
placeholder={workspaceToDelete?.name}
|
||||
className='h-9 rounded-[8px]'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{showTemplateChoice ? (
|
||||
<div className='mt-3 flex gap-2'>
|
||||
<Button
|
||||
onClick={() => handleTemplateAction('keep')}
|
||||
className='flex-1'
|
||||
variant='active'
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Keep Templates'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleTemplateAction('delete')}
|
||||
variant='primary'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90 flex-1'
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete Templates'}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-3'>
|
||||
<p className='mb-2 font-[360] text-sm'>
|
||||
Enter the workspace name{' '}
|
||||
<span className='font-semibold'>{workspaceToDelete?.name}</span> to confirm.
|
||||
</p>
|
||||
<Input
|
||||
value={deleteConfirmationName}
|
||||
onChange={(e) => setDeleteConfirmationName(e.target.value)}
|
||||
placeholder={workspaceToDelete?.name}
|
||||
className='h-9 rounded-[8px]'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
{!showTemplateChoice && (
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='h-[32px] px-[12px]'
|
||||
variant='active'
|
||||
onClick={() => {
|
||||
resetDeleteState()
|
||||
setIsDeleteDialogOpen(false)
|
||||
@@ -712,11 +699,12 @@ export function WorkspaceSelector({
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleDeleteClick()
|
||||
}}
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
disabled={
|
||||
isDeleting ||
|
||||
deleteConfirmationName !== workspaceToDelete?.name ||
|
||||
@@ -741,34 +729,30 @@ export function WorkspaceSelector({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Leave workspace?</ModalTitle>
|
||||
<ModalDescription>
|
||||
<ModalContent className='w-[400px]'>
|
||||
<ModalHeader>Leave Workspace</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-tertiary)]'>
|
||||
Leaving this workspace will remove your access to all associated workflows, logs, and
|
||||
knowledge bases.{' '}
|
||||
<span className='text-[var(--text-error)] dark:text-[var(--text-error)]'>
|
||||
This action cannot be undone.
|
||||
</span>
|
||||
</ModalDescription>
|
||||
</ModalHeader>
|
||||
|
||||
<div className='py-2'>
|
||||
<p className='mb-2 font-[360] text-sm'>
|
||||
Enter the workspace name <strong>{workspaceToLeave?.name}</strong> to confirm.
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</p>
|
||||
<Input
|
||||
value={leaveConfirmationName}
|
||||
onChange={(e) => setLeaveConfirmationName(e.target.value)}
|
||||
placeholder={workspaceToLeave?.name}
|
||||
className='h-9'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-3'>
|
||||
<p className='mb-2 font-[360] text-sm'>
|
||||
Enter the workspace name <strong>{workspaceToLeave?.name}</strong> to confirm.
|
||||
</p>
|
||||
<Input
|
||||
value={leaveConfirmationName}
|
||||
onChange={(e) => setLeaveConfirmationName(e.target.value)}
|
||||
placeholder={workspaceToLeave?.name}
|
||||
className='h-9'
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='h-[32px] px-[12px]'
|
||||
variant='active'
|
||||
onClick={() => {
|
||||
setIsLeaveDialogOpen(false)
|
||||
setLeaveConfirmationName('')
|
||||
@@ -778,6 +762,7 @@ export function WorkspaceSelector({
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={() => {
|
||||
if (workspaceToLeave) {
|
||||
confirmLeaveWorkspace(workspaceToLeave)
|
||||
@@ -786,7 +771,7 @@ export function WorkspaceSelector({
|
||||
setWorkspaceToLeave(null)
|
||||
}
|
||||
}}
|
||||
className='h-[32px] bg-[var(--text-error)] px-[12px] text-[var(--white)] hover:bg-[var(--text-error)] hover:text-[var(--white)] dark:bg-[var(--text-error)] dark:text-[var(--white)] hover:dark:bg-[var(--text-error)] dark:hover:text-[var(--white)]'
|
||||
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
|
||||
disabled={isLeaving || leaveConfirmationName !== workspaceToLeave?.name}
|
||||
>
|
||||
{isLeaving ? 'Leaving...' : 'Leave'}
|
||||
|
||||
Reference in New Issue
Block a user