From 6dc85cd8702c861f0637421c22f32feebf7270cf Mon Sep 17 00:00:00 2001 From: Zhaoqi Li Date: Fri, 15 Aug 2025 13:59:17 -0700 Subject: [PATCH] initial implementation of knowledgebase questions and answers --- .../[kbId]/questions/[questionId]/route.ts | 237 ++++++++ .../knowledge-bases/[kbId]/questions/route.ts | 157 +++++ .../[id]/knowledge-bases/[kbId]/route.ts | 181 ++++++ .../[id]/knowledge-bases/route.ts | 117 ++++ .../[orgId]/knowledge-base/page.tsx | 23 + .../organizations/KnowledgeBaseContent.tsx | 537 ++++++++++++++++++ components/organizations/index.ts | 1 + layouts/sidebar-layout/sidebar-layout.tsx | 8 +- 8 files changed, 1260 insertions(+), 1 deletion(-) create mode 100644 app/api/organizations/[id]/knowledge-bases/[kbId]/questions/[questionId]/route.ts create mode 100644 app/api/organizations/[id]/knowledge-bases/[kbId]/questions/route.ts create mode 100644 app/api/organizations/[id]/knowledge-bases/[kbId]/route.ts create mode 100644 app/api/organizations/[id]/knowledge-bases/route.ts create mode 100644 app/organizations/[orgId]/knowledge-base/page.tsx create mode 100644 components/organizations/KnowledgeBaseContent.tsx diff --git a/app/api/organizations/[id]/knowledge-bases/[kbId]/questions/[questionId]/route.ts b/app/api/organizations/[id]/knowledge-bases/[kbId]/questions/[questionId]/route.ts new file mode 100644 index 00000000..4dd07794 --- /dev/null +++ b/app/api/organizations/[id]/knowledge-bases/[kbId]/questions/[questionId]/route.ts @@ -0,0 +1,237 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { organizationService } from '@/lib/organization-service'; + +// Get specific question +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string; kbId: string; questionId: string }> } +) { + try { + const { id, kbId, questionId } = await params; + const organizationId = id; + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user is a member of this organization + const isMember = await organizationService.isUserOrganizationMember( + currentUser.id, + organizationId + ); + + if (!isMember) { + return NextResponse.json( + { error: 'You do not have access to this organization' }, + { status: 403 } + ); + } + + const question = await db.knowledgeBaseQuestion.findFirst({ + where: { + id: questionId, + knowledgeBase: { + id: kbId, + organizationId, + }, + }, + include: { + answer: true, + knowledgeBase: true, + }, + }); + + if (!question) { + return NextResponse.json( + { error: 'Question not found' }, + { status: 404 } + ); + } + + return NextResponse.json(question); + } catch (error) { + console.error('Error fetching question:', error); + return NextResponse.json( + { error: 'Failed to fetch question' }, + { status: 500 } + ); + } +} + +// Update question +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string; kbId: string; questionId: string }> } +) { + try { + const { id, kbId, questionId } = await params; + const organizationId = id; + const { text, topic, tags, answer } = await request.json(); + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user has permissions + const userRole = await organizationService.getUserOrganizationRole( + currentUser.id, + organizationId + ); + + if (!userRole || !['admin', 'owner', 'member'].includes(userRole)) { + return NextResponse.json( + { error: 'You do not have permission to update questions' }, + { status: 403 } + ); + } + + // Verify question belongs to the knowledge base and organization + const existingQuestion = await db.knowledgeBaseQuestion.findFirst({ + where: { + id: questionId, + knowledgeBase: { + id: kbId, + organizationId, + }, + }, + include: { + answer: true, + }, + }); + + if (!existingQuestion) { + return NextResponse.json( + { error: 'Question not found' }, + { status: 404 } + ); + } + + // Update question and answer in a transaction + const result = await db.$transaction(async (tx) => { + // Update question + const updatedQuestion = await tx.knowledgeBaseQuestion.update({ + where: { id: questionId }, + data: { + text, + topic, + tags: tags || [], + }, + }); + + // Handle answer update/creation/deletion + if (answer) { + if (existingQuestion.answer) { + // Update existing answer + await tx.knowledgeBaseAnswer.update({ + where: { questionId }, + data: { text: answer }, + }); + } else { + // Create new answer + await tx.knowledgeBaseAnswer.create({ + data: { + text: answer, + questionId, + }, + }); + } + } else if (existingQuestion.answer) { + // Delete existing answer if no answer provided + await tx.knowledgeBaseAnswer.delete({ + where: { questionId }, + }); + } + + return updatedQuestion; + }); + + // Fetch updated question with answer + const questionWithAnswer = await db.knowledgeBaseQuestion.findUnique({ + where: { id: questionId }, + include: { answer: true }, + }); + + return NextResponse.json(questionWithAnswer); + } catch (error) { + console.error('Error updating question:', error); + return NextResponse.json( + { error: 'Failed to update question' }, + { status: 500 } + ); + } +} + +// Delete question +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string; kbId: string; questionId: string }> } +) { + try { + const { id, kbId, questionId } = await params; + const organizationId = id; + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user has permissions + const userRole = await organizationService.getUserOrganizationRole( + currentUser.id, + organizationId + ); + + if (!userRole || !['admin', 'owner', 'member'].includes(userRole)) { + return NextResponse.json( + { error: 'You do not have permission to delete questions' }, + { status: 403 } + ); + } + + // Verify question belongs to the knowledge base and organization + const existingQuestion = await db.knowledgeBaseQuestion.findFirst({ + where: { + id: questionId, + knowledgeBase: { + id: kbId, + organizationId, + }, + }, + }); + + if (!existingQuestion) { + return NextResponse.json( + { error: 'Question not found' }, + { status: 404 } + ); + } + + // Delete question (cascade will handle answer deletion) + await db.knowledgeBaseQuestion.delete({ + where: { id: questionId }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error deleting question:', error); + return NextResponse.json( + { error: 'Failed to delete question' }, + { status: 500 } + ); + } +} diff --git a/app/api/organizations/[id]/knowledge-bases/[kbId]/questions/route.ts b/app/api/organizations/[id]/knowledge-bases/[kbId]/questions/route.ts new file mode 100644 index 00000000..5ed675d6 --- /dev/null +++ b/app/api/organizations/[id]/knowledge-bases/[kbId]/questions/route.ts @@ -0,0 +1,157 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { organizationService } from '@/lib/organization-service'; + +// Get all questions for a knowledge base +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string; kbId: string }> } +) { + try { + const { id, kbId } = await params; + const organizationId = id; + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user is a member of this organization + const isMember = await organizationService.isUserOrganizationMember( + currentUser.id, + organizationId + ); + + if (!isMember) { + return NextResponse.json( + { error: 'You do not have access to this organization' }, + { status: 403 } + ); + } + + // Verify knowledge base belongs to organization + const knowledgeBase = await db.knowledgeBase.findUnique({ + where: { + id: kbId, + organizationId, + }, + }); + + if (!knowledgeBase) { + return NextResponse.json( + { error: 'Knowledge base not found' }, + { status: 404 } + ); + } + + const questions = await db.knowledgeBaseQuestion.findMany({ + where: { + knowledgeBaseId: kbId, + }, + include: { + answer: true, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + return NextResponse.json(questions); + } catch (error) { + console.error('Error fetching questions:', error); + return NextResponse.json( + { error: 'Failed to fetch questions' }, + { status: 500 } + ); + } +} + +// Create a new question +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string; kbId: string }> } +) { + try { + const { id, kbId } = await params; + const organizationId = id; + const { text, topic, tags, answer } = await request.json(); + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user has permissions + const userRole = await organizationService.getUserOrganizationRole( + currentUser.id, + organizationId + ); + + if (!userRole || !['admin', 'owner', 'member'].includes(userRole)) { + return NextResponse.json( + { error: 'You do not have permission to create questions' }, + { status: 403 } + ); + } + + // Verify knowledge base belongs to organization + const knowledgeBase = await db.knowledgeBase.findUnique({ + where: { + id: kbId, + organizationId, + }, + }); + + if (!knowledgeBase) { + return NextResponse.json( + { error: 'Knowledge base not found' }, + { status: 404 } + ); + } + + // Create question and answer in a transaction + const result = await db.$transaction(async (tx) => { + const question = await tx.knowledgeBaseQuestion.create({ + data: { + text, + topic, + tags: tags || [], + knowledgeBaseId: kbId, + }, + }); + + let answerRecord = null; + if (answer) { + answerRecord = await tx.knowledgeBaseAnswer.create({ + data: { + text: answer, + questionId: question.id, + }, + }); + } + + return { question, answer: answerRecord }; + }); + + const questionWithAnswer = await db.knowledgeBaseQuestion.findUnique({ + where: { id: result.question.id }, + include: { answer: true }, + }); + + return NextResponse.json(questionWithAnswer); + } catch (error) { + console.error('Error creating question:', error); + return NextResponse.json( + { error: 'Failed to create question' }, + { status: 500 } + ); + } +} diff --git a/app/api/organizations/[id]/knowledge-bases/[kbId]/route.ts b/app/api/organizations/[id]/knowledge-bases/[kbId]/route.ts new file mode 100644 index 00000000..038deeeb --- /dev/null +++ b/app/api/organizations/[id]/knowledge-bases/[kbId]/route.ts @@ -0,0 +1,181 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { organizationService } from '@/lib/organization-service'; + +// Get specific knowledge base +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string; kbId: string }> } +) { + try { + const { id, kbId } = await params; + const organizationId = id; + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user is a member of this organization + const isMember = await organizationService.isUserOrganizationMember( + currentUser.id, + organizationId + ); + + if (!isMember) { + return NextResponse.json( + { error: 'You do not have access to this organization' }, + { status: 403 } + ); + } + + const knowledgeBase = await db.knowledgeBase.findUnique({ + where: { + id: kbId, + organizationId, + }, + include: { + questions: { + include: { + answer: true, + }, + orderBy: { + createdAt: 'desc', + }, + }, + _count: { + select: { + questions: true, + }, + }, + }, + }); + + if (!knowledgeBase) { + return NextResponse.json( + { error: 'Knowledge base not found' }, + { status: 404 } + ); + } + + return NextResponse.json(knowledgeBase); + } catch (error) { + console.error('Error fetching knowledge base:', error); + return NextResponse.json( + { error: 'Failed to fetch knowledge base' }, + { status: 500 } + ); + } +} + +// Update knowledge base +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string; kbId: string }> } +) { + try { + const { id, kbId } = await params; + const organizationId = id; + const { name, description } = await request.json(); + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user has admin/owner permissions + const userRole = await organizationService.getUserOrganizationRole( + currentUser.id, + organizationId + ); + + if (!userRole || !['admin', 'owner'].includes(userRole)) { + return NextResponse.json( + { error: 'You do not have permission to update knowledge bases' }, + { status: 403 } + ); + } + + const knowledgeBase = await db.knowledgeBase.update({ + where: { + id: kbId, + organizationId, + }, + data: { + name, + description, + }, + include: { + _count: { + select: { + questions: true, + }, + }, + }, + }); + + return NextResponse.json(knowledgeBase); + } catch (error) { + console.error('Error updating knowledge base:', error); + return NextResponse.json( + { error: 'Failed to update knowledge base' }, + { status: 500 } + ); + } +} + +// Delete knowledge base +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string; kbId: string }> } +) { + try { + const { id, kbId } = await params; + const organizationId = id; + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user has admin/owner permissions + const userRole = await organizationService.getUserOrganizationRole( + currentUser.id, + organizationId + ); + + if (!userRole || !['admin', 'owner'].includes(userRole)) { + return NextResponse.json( + { error: 'You do not have permission to delete knowledge bases' }, + { status: 403 } + ); + } + + await db.knowledgeBase.delete({ + where: { + id: kbId, + organizationId, + }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error deleting knowledge base:', error); + return NextResponse.json( + { error: 'Failed to delete knowledge base' }, + { status: 500 } + ); + } +} diff --git a/app/api/organizations/[id]/knowledge-bases/route.ts b/app/api/organizations/[id]/knowledge-bases/route.ts new file mode 100644 index 00000000..3164b8c1 --- /dev/null +++ b/app/api/organizations/[id]/knowledge-bases/route.ts @@ -0,0 +1,117 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { organizationService } from '@/lib/organization-service'; + +// Get all knowledge bases for an organization +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const organizationId = id; + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user is a member of this organization + const isMember = await organizationService.isUserOrganizationMember( + currentUser.id, + organizationId + ); + + if (!isMember) { + return NextResponse.json( + { error: 'You do not have access to this organization' }, + { status: 403 } + ); + } + + const knowledgeBases = await db.knowledgeBase.findMany({ + where: { + organizationId, + }, + include: { + _count: { + select: { + questions: true, + }, + }, + }, + orderBy: { + createdAt: 'desc', + }, + }); + + return NextResponse.json(knowledgeBases); + } catch (error) { + console.error('Error fetching knowledge bases:', error); + return NextResponse.json( + { error: 'Failed to fetch knowledge bases' }, + { status: 500 } + ); + } +} + +// Create a new knowledge base +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const organizationId = id; + const { name, description } = await request.json(); + + const currentUser = await organizationService.getCurrentUser(); + + if (!currentUser) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Check if user has admin/owner permissions + const userRole = await organizationService.getUserOrganizationRole( + currentUser.id, + organizationId + ); + + if (!userRole || !['admin', 'owner', 'member'].includes(userRole)) { + return NextResponse.json( + { error: 'You do not have permission to create knowledge bases' }, + { status: 403 } + ); + } + + const knowledgeBase = await db.knowledgeBase.create({ + data: { + name, + description, + organizationId, + }, + include: { + _count: { + select: { + questions: true, + }, + }, + }, + }); + + return NextResponse.json(knowledgeBase); + } catch (error) { + console.error('Error creating knowledge base:', error); + return NextResponse.json( + { error: 'Failed to create knowledge base' }, + { status: 500 } + ); + } +} diff --git a/app/organizations/[orgId]/knowledge-base/page.tsx b/app/organizations/[orgId]/knowledge-base/page.tsx new file mode 100644 index 00000000..f4137f4a --- /dev/null +++ b/app/organizations/[orgId]/knowledge-base/page.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { Suspense } from 'react'; +import { PageSkeleton } from '@/components/projects/PageSkeleton'; +import { KnowledgeBaseContent } from '@/components/organizations/KnowledgeBaseContent'; + +interface KnowledgeBasePageProps { + params: Promise<{ + orgId: string; + }>; +} + +export default function KnowledgeBasePage({ params }: KnowledgeBasePageProps) { + return ( + }> + + + ); +} + +function KnowledgeBasePageContent({ params }: KnowledgeBasePageProps) { + return ; +} diff --git a/components/organizations/KnowledgeBaseContent.tsx b/components/organizations/KnowledgeBaseContent.tsx new file mode 100644 index 00000000..90c9569b --- /dev/null +++ b/components/organizations/KnowledgeBaseContent.tsx @@ -0,0 +1,537 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useParams } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { useToast } from '@/components/ui/use-toast'; +import { + BookOpen, + Plus, + Search, + Edit, + Trash2, + MessageSquare, + Tag, + MoreHorizontal, + FileText, + ChevronRight +} from 'lucide-react'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; + +interface KnowledgeBase { + id: string; + name: string; + description?: string; + createdAt: string; + updatedAt: string; + _count: { + questions: number; + }; +} + +interface KnowledgeBaseQuestion { + id: string; + text: string; + topic?: string; + tags: string[]; + createdAt: string; + updatedAt: string; + answer?: { + id: string; + text: string; + createdAt: string; + updatedAt: string; + }; +} + +interface KnowledgeBaseContentProps { + params: Promise<{ + orgId: string; + }>; +} + +export function KnowledgeBaseContent({ params }: KnowledgeBaseContentProps) { + const { orgId } = useParams() as { orgId: string }; + const { toast } = useToast(); + + const [knowledgeBases, setKnowledgeBases] = useState([]); + const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState(null); + const [questions, setQuestions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + + // Dialog states + const [isCreateKBOpen, setIsCreateKBOpen] = useState(false); + const [isCreateQuestionOpen, setIsCreateQuestionOpen] = useState(false); + const [editingQuestion, setEditingQuestion] = useState(null); + + // Form states + const [kbForm, setKbForm] = useState({ name: '', description: '' }); + const [questionForm, setQuestionForm] = useState({ + text: '', + topic: '', + tags: '', + answer: '' + }); + + // Fetch knowledge bases + const fetchKnowledgeBases = async () => { + try { + setIsLoading(true); + const response = await fetch(`/api/organizations/${orgId}/knowledge-bases`); + if (response.ok) { + const data = await response.json(); + setKnowledgeBases(data); + if (data.length > 0 && !selectedKnowledgeBase) { + setSelectedKnowledgeBase(data[0]); + } + } + } catch (error) { + toast({ + title: "Error", + description: "Failed to load knowledge bases", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + // Fetch questions for selected knowledge base + const fetchQuestions = async (kbId: string) => { + try { + const response = await fetch(`/api/organizations/${orgId}/knowledge-bases/${kbId}/questions`); + if (response.ok) { + const data = await response.json(); + setQuestions(data); + } + } catch (error) { + toast({ + title: "Error", + description: "Failed to load questions", + variant: "destructive", + }); + } + }; + + useEffect(() => { + fetchKnowledgeBases(); + }, [orgId]); + + useEffect(() => { + if (selectedKnowledgeBase) { + fetchQuestions(selectedKnowledgeBase.id); + } + }, [selectedKnowledgeBase]); + + // Create knowledge base + const handleCreateKB = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const response = await fetch(`/api/organizations/${orgId}/knowledge-bases`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(kbForm), + }); + + if (response.ok) { + toast({ + title: "Success", + description: "Knowledge base created successfully", + }); + setKbForm({ name: '', description: '' }); + setIsCreateKBOpen(false); + fetchKnowledgeBases(); + } + } catch (error) { + toast({ + title: "Error", + description: "Failed to create knowledge base", + variant: "destructive", + }); + } + }; + + // Create/Update question + const handleSaveQuestion = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedKnowledgeBase) return; + + try { + const url = editingQuestion + ? `/api/organizations/${orgId}/knowledge-bases/${selectedKnowledgeBase.id}/questions/${editingQuestion.id}` + : `/api/organizations/${orgId}/knowledge-bases/${selectedKnowledgeBase.id}/questions`; + + const method = editingQuestion ? 'PUT' : 'POST'; + + const response = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...questionForm, + tags: questionForm.tags.split(',').map(t => t.trim()).filter(t => t), + }), + }); + + if (response.ok) { + toast({ + title: "Success", + description: editingQuestion ? "Question updated successfully" : "Question created successfully", + }); + setQuestionForm({ text: '', topic: '', tags: '', answer: '' }); + setIsCreateQuestionOpen(false); + setEditingQuestion(null); + fetchQuestions(selectedKnowledgeBase.id); + } + } catch (error) { + toast({ + title: "Error", + description: "Failed to save question", + variant: "destructive", + }); + } + }; + + // Filter questions based on search + const filteredQuestions = questions.filter(q => + q.text.toLowerCase().includes(searchQuery.toLowerCase()) || + q.topic?.toLowerCase().includes(searchQuery.toLowerCase()) || + q.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) + ); + + if (isLoading) { + return ( +
+
+
+
+
+ {[1, 2, 3].map((i) => ( +
+ ))} +
+
+
+ ); + } + + return ( +
+
+
+

+ + Knowledge Base +

+

+ Manage pre-built questions and answers for common RFP responses +

+
+ + + + + + + Create Knowledge Base + + Create a new knowledge base to organize your questions and answers. + + +
+
+ + setKbForm({ ...kbForm, name: e.target.value })} + placeholder="e.g., Technical Questions, Compliance, Pricing" + required + /> +
+
+ +