improvement: modal UI (#2202)

* fix: trigger-save delete modal

* improvement: old modal styling
This commit is contained in:
Emir Karabeg
2025-12-04 17:10:59 -08:00
committed by GitHub
parent d22b5783be
commit 1642ed754b
14 changed files with 781 additions and 881 deletions
@@ -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>
</>
@@ -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 ? (
<>
@@ -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>
@@ -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'
@@ -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>
)
@@ -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>
@@ -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>
@@ -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>
@@ -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}
>
@@ -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>
@@ -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>
@@ -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'}