Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
'use client'

import Link from 'next/link'
import { useParams } from 'next/navigation'
import { useCallback, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Badge, DocumentAttachment, Tooltip } from '@/components/emcn'
import { BaseTagsModal } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import { DeleteKnowledgeBaseModal } from '../delete-knowledge-base-modal/delete-knowledge-base-modal'
import { EditKnowledgeBaseModal } from '../edit-knowledge-base-modal/edit-knowledge-base-modal'
import { KnowledgeBaseContextMenu } from '../knowledge-base-context-menu/knowledge-base-context-menu'

interface BaseCardProps {
id?: string
Expand All @@ -11,6 +17,8 @@ interface BaseCardProps {
description: string
createdAt?: string
updatedAt?: string
onUpdate?: (id: string, name: string, description: string) => Promise<void>
onDelete?: (id: string) => Promise<void>
}

/**
Expand Down Expand Up @@ -109,9 +117,32 @@ export function BaseCardSkeletonGrid({ count = 8 }: { count?: number }) {
/**
* Knowledge base card component displaying overview information
*/
export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCardProps) {
export function BaseCard({
id,
title,
docCount,
description,
updatedAt,
onUpdate,
onDelete,
}: BaseCardProps) {
const params = useParams()
const router = useRouter()
const workspaceId = params?.workspaceId as string
const userPermissions = useUserPermissionsContext()

const {
isOpen: isContextMenuOpen,
position: contextMenuPosition,
menuRef,
handleContextMenu,
closeMenu: closeContextMenu,
} = useContextMenu()

const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)

const searchParams = new URLSearchParams({
kbName: title,
Expand All @@ -120,41 +151,154 @@ export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCa

const shortId = id ? `kb-${id.slice(0, 8)}` : ''

return (
<Link href={href} prefetch={true} className='h-full'>
<div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-3)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-4)] dark:bg-[var(--surface-4)] dark:hover:bg-[var(--surface-5)]'>
<div className='flex items-center justify-between gap-[8px]'>
<h3 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
{title}
</h3>
{shortId && <Badge className='flex-shrink-0 rounded-[4px] text-[12px]'>{shortId}</Badge>}
</div>
const handleClick = useCallback(
(e: React.MouseEvent) => {
if (isContextMenuOpen) {
e.preventDefault()
return
}
router.push(href)
},
[isContextMenuOpen, router, href]
)

const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
router.push(href)
}
},
[router, href]
)

const handleOpenInNewTab = useCallback(() => {
window.open(href, '_blank')
}, [href])

const handleViewTags = useCallback(() => {
setIsTagsModalOpen(true)
}, [])

const handleEdit = useCallback(() => {
setIsEditModalOpen(true)
}, [])

const handleDelete = useCallback(() => {
setIsDeleteModalOpen(true)
}, [])

const handleConfirmDelete = useCallback(async () => {
if (!id || !onDelete) return
setIsDeleting(true)
try {
await onDelete(id)
setIsDeleteModalOpen(false)
} finally {
setIsDeleting(false)
}
}, [id, onDelete])

<div className='flex flex-1 flex-col gap-[8px]'>
<div className='flex items-center justify-between'>
<span className='flex items-center gap-[6px] text-[12px] text-[var(--text-tertiary)]'>
<DocumentAttachment className='h-[12px] w-[12px]' />
{docCount} {docCount === 1 ? 'doc' : 'docs'}
</span>
{updatedAt && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='text-[12px] text-[var(--text-tertiary)]'>
last updated: {formatRelativeTime(updatedAt)}
</span>
</Tooltip.Trigger>
<Tooltip.Content>{formatAbsoluteDate(updatedAt)}</Tooltip.Content>
</Tooltip.Root>
const handleSave = useCallback(
async (knowledgeBaseId: string, name: string, newDescription: string) => {
if (!onUpdate) return
await onUpdate(knowledgeBaseId, name, newDescription)
},
[onUpdate]
)

return (
<>
<div
role='button'
tabIndex={0}
className='h-full cursor-pointer'
onClick={handleClick}
onKeyDown={handleKeyDown}
onContextMenu={handleContextMenu}
>
<div className='group flex h-full flex-col gap-[12px] rounded-[4px] bg-[var(--surface-3)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-4)] dark:bg-[var(--surface-4)] dark:hover:bg-[var(--surface-5)]'>
<div className='flex items-center justify-between gap-[8px]'>
<h3 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
{title}
</h3>
{shortId && (
<Badge className='flex-shrink-0 rounded-[4px] text-[12px]'>{shortId}</Badge>
)}
</div>

<div className='h-0 w-full border-[var(--divider)] border-t' />
<div className='flex flex-1 flex-col gap-[8px]'>
<div className='flex items-center justify-between'>
<span className='flex items-center gap-[6px] text-[12px] text-[var(--text-tertiary)]'>
<DocumentAttachment className='h-[12px] w-[12px]' />
{docCount} {docCount === 1 ? 'doc' : 'docs'}
</span>
{updatedAt && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='text-[12px] text-[var(--text-tertiary)]'>
last updated: {formatRelativeTime(updatedAt)}
</span>
</Tooltip.Trigger>
<Tooltip.Content>{formatAbsoluteDate(updatedAt)}</Tooltip.Content>
</Tooltip.Root>
)}
</div>

<div className='h-0 w-full border-[var(--divider)] border-t' />

<p className='line-clamp-2 h-[36px] text-[12px] text-[var(--text-tertiary)] leading-[18px]'>
{description}
</p>
<p className='line-clamp-2 h-[36px] text-[12px] text-[var(--text-tertiary)] leading-[18px]'>
{description}
</p>
</div>
</div>
</div>
</Link>

<KnowledgeBaseContextMenu
isOpen={isContextMenuOpen}
position={contextMenuPosition}
menuRef={menuRef}
onClose={closeContextMenu}
onOpenInNewTab={handleOpenInNewTab}
onViewTags={handleViewTags}
onEdit={handleEdit}
onDelete={handleDelete}
showOpenInNewTab={true}
showViewTags={!!id}
showEdit={!!onUpdate}
showDelete={!!onDelete}
disableEdit={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit}
/>

{id && onUpdate && (
<EditKnowledgeBaseModal
open={isEditModalOpen}
onOpenChange={setIsEditModalOpen}
knowledgeBaseId={id}
initialName={title}
initialDescription={description === 'No description provided' ? '' : description}
onSave={handleSave}
/>
)}

{id && onDelete && (
<DeleteKnowledgeBaseModal
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
onConfirm={handleConfirmDelete}
isDeleting={isDeleting}
knowledgeBaseName={title}
/>
)}

{id && (
<BaseTagsModal
open={isTagsModalOpen}
onOpenChange={setIsTagsModalOpen}
knowledgeBaseId={id}
/>
)}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client'

import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@/components/emcn'

interface DeleteKnowledgeBaseModalProps {
/**
* Whether the modal is open
*/
isOpen: boolean
/**
* Callback when modal should close
*/
onClose: () => void
/**
* Callback when delete is confirmed
*/
onConfirm: () => void
/**
* Whether the delete operation is in progress
*/
isDeleting: boolean
/**
* Name of the knowledge base being deleted
*/
knowledgeBaseName?: string
}

/**
* Delete confirmation modal for knowledge base items.
* Displays a warning message and confirmation buttons.
*/
export function DeleteKnowledgeBaseModal({
isOpen,
onClose,
onConfirm,
isDeleting,
knowledgeBaseName,
}: DeleteKnowledgeBaseModalProps) {
return (
<Modal open={isOpen} onOpenChange={onClose}>
<ModalContent className='w-[400px]'>
<ModalHeader>Delete Knowledge Base</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-secondary)]'>
{knowledgeBaseName ? (
<>
Are you sure you want to delete{' '}
<span className='font-medium text-[var(--text-primary)]'>{knowledgeBaseName}</span>?
This will permanently remove all associated documents, chunks, and embeddings.
</>
) : (
'Are you sure you want to delete this knowledge base? This will permanently remove all associated documents, chunks, and embeddings.'
)}{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
</ModalBody>
<ModalFooter>
<Button variant='active' onClick={onClose} disabled={isDeleting}>
Cancel
</Button>
<Button variant='destructive' onClick={onConfirm} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
Loading