diff --git a/app/(main)/knowledge-base/page.tsx b/app/(main)/knowledge-base/page.tsx index 0d4ffe67..8f285654 100644 --- a/app/(main)/knowledge-base/page.tsx +++ b/app/(main)/knowledge-base/page.tsx @@ -1,18 +1,21 @@ "use client"; import { useState } from "react"; -import CollectionsList from "@/app/components/knowledge-base/CollectionsList"; -import CreateCollectionForm from "@/app/components/knowledge-base/CreateCollectionForm"; -import CollectionDetail from "@/app/components/knowledge-base/CollectionDetail"; -import DocumentPickerModal from "@/app/components/knowledge-base/DocumentPickerModal"; -import DeleteCollectionModal from "@/app/components/knowledge-base/DeleteCollectionModal"; -import DocumentPreviewModal from "@/app/components/knowledge-base/DocumentPreviewModal"; +import { + CollectionDetail, + CollectionsList, + CreateCollectionForm, + DeleteCollectionModal, + DocumentPickerModal, + DocumentPreviewModal, + EditCollectionModal, +} from "@/app/components/knowledge-base"; import { Modal, Loader } from "@/app/components/ui"; import { Sidebar, PageHeader } from "@/app/components"; import { BookOpenIcon } from "@/app/components/icons"; import { useApp } from "@/app/lib/context/AppContext"; import { useCollections } from "@/app/hooks/useCollections"; -import { Document } from "@/app/lib/types/document"; +import { Document, Collection } from "@/app/lib/types/document"; export default function KnowledgeBasePage() { const { sidebarCollapsed } = useApp(); @@ -22,12 +25,15 @@ export default function KnowledgeBasePage() { selectedCollection, isLoading, isLoadingDetail, + isLoadingDocuments, isCreating, setSelectedCollection, fetchCollectionDetails, createCollection, deleteCollection, + updateCollection, fetchAndPreviewDoc, + fetchDocuments, } = useCollections(); const [showCreateForm, setShowCreateForm] = useState(false); @@ -39,6 +45,9 @@ export default function KnowledgeBasePage() { const [showDocPreviewModal, setShowDocPreviewModal] = useState(false); const [previewDoc, setPreviewDoc] = useState(null); const [isPreviewLoading, setIsPreviewLoading] = useState(false); + const [collectionToEdit, setCollectionToEdit] = useState( + null, + ); const [collectionName, setCollectionName] = useState(""); const [collectionDescription, setCollectionDescription] = useState(""); @@ -65,6 +74,7 @@ export default function KnowledgeBasePage() { const handleCreateNew = () => { setShowCreateForm(true); setSelectedCollection(null); + fetchDocuments(); }; const handleCancelCreate = () => { @@ -81,12 +91,13 @@ export default function KnowledgeBasePage() { description: collectionDescription, documentIds: Array.from(selectedDocuments), }; + const ok = await createCollection(params); + if (!ok) return; setShowCreateForm(false); setShowDocumentPicker(false); setCollectionName(""); setCollectionDescription(""); setSelectedDocuments(new Set()); - await createCollection(params); }; const handleRequestDelete = (collectionId: string) => { @@ -94,6 +105,18 @@ export default function KnowledgeBasePage() { setShowConfirmDelete(true); }; + const handleRequestEdit = (collection: Collection) => { + setCollectionToEdit(collection); + }; + + const handleSaveEdit = async (patch: { + name?: string; + description?: string; + }) => { + if (!collectionToEdit) return false; + return updateCollection(collectionToEdit.id, patch); + }; + const handleConfirmDelete = async () => { if (!collectionToDelete) return; setShowConfirmDelete(false); @@ -142,6 +165,7 @@ export default function KnowledgeBasePage() { isLoading={isLoading} onSelect={handleSelectCollection} onRequestDelete={handleRequestDelete} + onRequestEdit={handleRequestEdit} onCreateNew={handleCreateNew} /> @@ -154,6 +178,7 @@ export default function KnowledgeBasePage() { setCollectionDescription={setCollectionDescription} selectedDocuments={selectedDocuments} availableDocuments={availableDocuments} + isLoadingDocuments={isLoadingDocuments} onToggleDocument={toggleDocumentSelection} onOpenDocumentPicker={() => setShowDocumentPicker(true)} isCreating={isCreating} @@ -207,6 +232,7 @@ export default function KnowledgeBasePage() { onClose={handleCancelCreate} maxWidth="max-w-2xl" maxHeight="max-h-[90vh]" + showClose={false} > @@ -228,6 +255,7 @@ export default function KnowledgeBasePage() { onClose={() => setSelectedCollection(null)} maxWidth="max-w-2xl" maxHeight="max-h-[90vh]" + showClose={false} > {selectedCollection && ( setSelectedCollection(null)} /> )} @@ -272,6 +301,13 @@ export default function KnowledgeBasePage() { isLoading={isPreviewLoading} onSelectDocument={handleSelectPreviewDoc} /> + + setCollectionToEdit(null)} + onSave={handleSaveEdit} + /> ); } diff --git a/app/api/collections/[collection_id]/route.ts b/app/api/collections/[collection_id]/route.ts index bee11a02..6b61d1a2 100644 --- a/app/api/collections/[collection_id]/route.ts +++ b/app/api/collections/[collection_id]/route.ts @@ -1,7 +1,6 @@ import { NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; -// GET /api/collections/[collection_id] - Get a specific collection export async function GET( request: Request, { params }: { params: Promise<{ collection_id: string }> }, @@ -26,7 +25,35 @@ export async function GET( } } -// DELETE /api/collection/[collection_id] - Delete a collection +export async function PATCH( + request: Request, + { params }: { params: Promise<{ collection_id: string }> }, +) { + const { collection_id } = await params; + try { + const body = await request.json(); + const { status, data } = await apiClient( + request, + `/api/v1/collections/${collection_id}`, + { + method: "PATCH", + body: JSON.stringify(body), + headers: { "Content-Type": "application/json" }, + }, + ); + return NextResponse.json(data, { status }); + } catch (error: unknown) { + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : String(error), + data: null, + }, + { status: 500 }, + ); + } +} + export async function DELETE( request: Request, { params }: { params: Promise<{ collection_id: string }> }, diff --git a/app/components/document/UploadDocumentModal.tsx b/app/components/document/UploadDocumentModal.tsx index fa736bb3..d9d41674 100644 --- a/app/components/document/UploadDocumentModal.tsx +++ b/app/components/document/UploadDocumentModal.tsx @@ -3,7 +3,7 @@ import { useRef } from "react"; import { Button, Modal } from "@/app/components/ui"; import { CloudUploadIcon } from "@/app/components/icons"; -import DocumentChip from "@/app/components/knowledge-base/DocumentChip"; +import { DocumentChip } from "@/app/components/knowledge-base"; import type { UploadPhase } from "@/app/lib/apiClient"; import { ACCEPTED_DOCUMENT_TYPES, diff --git a/app/components/knowledge-base/CollectionDetail.tsx b/app/components/knowledge-base/CollectionDetail.tsx index 85dd987d..ccff750c 100644 --- a/app/components/knowledge-base/CollectionDetail.tsx +++ b/app/components/knowledge-base/CollectionDetail.tsx @@ -5,6 +5,7 @@ import { Button } from "@/app/components/ui"; import { CheckLineIcon, ChevronDownIcon, + CloseIcon, CopyIcon, } from "@/app/components/icons"; import { formatDate } from "@/app/components/utils"; @@ -14,12 +15,14 @@ interface CollectionDetailProps { collection: Collection; onRequestDelete: (collectionId: string) => void; onPreviewDocument: (firstDocument: Document) => void; + onClose?: () => void; } export default function CollectionDetail({ collection, onRequestDelete, onPreviewDocument, + onClose, }: CollectionDetailProps) { const [showAllDocs, setShowAllDocs] = useState(false); const [copied, setCopied] = useState(false); @@ -42,8 +45,8 @@ export default function CollectionDetail({ return ( <>
-
-
+
+

{collection.name}

@@ -53,15 +56,27 @@ export default function CollectionDetail({

)}
- {!isOptimistic && ( - - )} +
+ {!isOptimistic && ( + + )} + {onClose && ( + + )} +
diff --git a/app/components/knowledge-base/CollectionsList.tsx b/app/components/knowledge-base/CollectionsList.tsx index a89cfb0a..010d75d9 100644 --- a/app/components/knowledge-base/CollectionsList.tsx +++ b/app/components/knowledge-base/CollectionsList.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/app/components/ui"; -import { BookOpenIcon, TrashIcon } from "@/app/components/icons"; +import { BookOpenIcon, EditIcon, TrashIcon } from "@/app/components/icons"; import { formatDate } from "@/app/components/utils"; import { Collection } from "@/app/lib/types/document"; import CollectionsListSkeleton from "./CollectionsListSkeleton"; @@ -12,6 +12,7 @@ interface CollectionsListProps { isLoading: boolean; onSelect: (collectionId: string) => void; onRequestDelete: (collectionId: string) => void; + onRequestEdit: (collection: Collection) => void; onCreateNew: () => void; } @@ -21,6 +22,7 @@ export default function CollectionsList({ isLoading, onSelect, onRequestDelete, + onRequestEdit, onCreateNew, }: CollectionsListProps) { return ( @@ -81,15 +83,27 @@ export default function CollectionsList({

{!isOptimistic && ( - { - e.stopPropagation(); - onRequestDelete(collection.id); - }} - className="p-1.5 rounded-md border border-status-error-border bg-bg-primary text-status-error-text hover:bg-status-error-bg transition-colors shrink-0 cursor-pointer" - title="Delete Knowledge Base" - > - + + { + e.stopPropagation(); + onRequestEdit(collection); + }} + className="p-1.5 rounded-md border border-border bg-bg-primary text-text-secondary hover:text-accent-primary hover:border-accent-primary hover:bg-accent-primary/10 transition-colors cursor-pointer" + title="Edit Knowledge Base" + > + + + { + e.stopPropagation(); + onRequestDelete(collection.id); + }} + className="p-1.5 rounded-md border border-status-error-border bg-bg-primary text-status-error-text hover:bg-status-error-bg transition-colors cursor-pointer" + title="Delete Knowledge Base" + > + + )}
diff --git a/app/components/knowledge-base/CreateCollectionForm.tsx b/app/components/knowledge-base/CreateCollectionForm.tsx index 532540af..6eba94ec 100644 --- a/app/components/knowledge-base/CreateCollectionForm.tsx +++ b/app/components/knowledge-base/CreateCollectionForm.tsx @@ -1,8 +1,12 @@ "use client"; import { Button, Field } from "@/app/components/ui"; -import { ChevronRightIcon } from "@/app/components/icons"; +import { ChevronRightIcon, CloseIcon } from "@/app/components/icons"; import { Document } from "@/app/lib/types/document"; +import { + COLLECTION_DESCRIPTION_MAX, + COLLECTION_NAME_MAX, +} from "@/app/lib/constants"; import DocumentChip from "./DocumentChip"; interface CreateCollectionFormProps { @@ -12,11 +16,13 @@ interface CreateCollectionFormProps { setCollectionDescription: (value: string) => void; selectedDocuments: Set; availableDocuments: Document[]; + isLoadingDocuments?: boolean; onToggleDocument: (documentId: string) => void; onOpenDocumentPicker: () => void; isCreating: boolean; onCancel: () => void; onCreate: () => void; + onClose?: () => void; } export default function CreateCollectionForm({ @@ -26,33 +32,53 @@ export default function CreateCollectionForm({ setCollectionDescription, selectedDocuments, availableDocuments, + isLoadingDocuments = false, onToggleDocument, onOpenDocumentPicker, isCreating, onCancel, onCreate, + onClose, }: CreateCollectionFormProps) { const isCreateDisabled = isCreating || !collectionName.trim() || selectedDocuments.size === 0; return (
+
+
+

+ Create Knowledge Base +

+

+ Group documents into a collection for RAG retrieval +

+
+ {onClose && ( + + )} +
+
-

- Create Knowledge Base -

-

- Group documents into a collection for RAG retrieval + +

+ {collectionName.length}/{COLLECTION_NAME_MAX}

- -
@@ -73,13 +103,21 @@ export default function CreateCollectionForm({ diff --git a/app/components/knowledge-base/EditCollectionModal.tsx b/app/components/knowledge-base/EditCollectionModal.tsx new file mode 100644 index 00000000..9a1b2695 --- /dev/null +++ b/app/components/knowledge-base/EditCollectionModal.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button, Modal, Field } from "@/app/components/ui"; +import { Collection } from "@/app/lib/types/document"; +import { + COLLECTION_DESCRIPTION_MAX, + COLLECTION_NAME_MAX, +} from "@/app/lib/constants"; + +interface EditCollectionModalProps { + open: boolean; + collection: Collection | null; + onClose: () => void; + onSave: (patch: { name?: string; description?: string }) => Promise; +} + +export default function EditCollectionModal({ + open, + collection, + onClose, + onSave, +}: EditCollectionModalProps) { + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + if (open && collection) { + setName(collection.name ?? ""); + setDescription(collection.description ?? ""); + } + }, [open, collection]); + + const handleSave = async () => { + if (!collection) return; + const trimmedName = name.trim(); + if (!trimmedName) return; + const patch: { name?: string; description?: string } = {}; + if (trimmedName !== (collection.name ?? "")) patch.name = trimmedName; + if (description !== (collection.description ?? "")) + patch.description = description; + if (Object.keys(patch).length === 0) { + onClose(); + return; + } + setIsSaving(true); + const ok = await onSave(patch); + setIsSaving(false); + if (ok) onClose(); + }; + + const isDisabled = isSaving || !name.trim(); + + return ( + +
+
+ +

+ {name.length}/{COLLECTION_NAME_MAX} +

+
+
+ +