Skip to content
Open
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
15 changes: 14 additions & 1 deletion app/(dashboard)/ai-tutor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import NextLink from "next/link"
import { LoginGateModal } from "@/components/login-gate-modal"
import { CreditsExhaustedModal } from "@/components/credits-exhausted-modal"
import { MarkdownRenderer } from "@/components/markdown-renderer"
import { BookmarkButton } from "@/components/bookmarks/BookmarkButton"

interface ResourceLink {
title: string
Expand Down Expand Up @@ -1143,9 +1144,14 @@ function AITutorContent() {
>
<div className="mx-auto max-w-4xl space-y-4 md:space-y-6">
<div ref={messagesTopRef} />
{messages.map((message) => {
{messages.map((message, messageIndex) => {
const feedback = messageFeedback[message.id]
const hasSources = message.role === "assistant" && (message.sources?.length || 0) > 0

const precedingUserMessage = [...messages.slice(0, messageIndex)].reverse().find((m) => m.role === "user")
const userQuestion = precedingUserMessage?.content || "AI Tutor Doubt"
const currentSession = chatSessions.find((s) => s.id === activeSessionId)
const subjectTopic = currentSession?.title || "AI Tutor"

return (
<div
Expand Down Expand Up @@ -1274,6 +1280,13 @@ function AITutorContent() {
)}
</Button>

<BookmarkButton
question={userQuestion}
answer={message.content}
subject={subjectTopic}
tags={["AI Tutor"]}
/>

<Button
variant="ghost"
size="icon"
Expand Down
28 changes: 28 additions & 0 deletions app/(dashboard)/bookmarks/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Metadata } from "next"
import { BookmarkList } from "@/components/bookmarks/BookmarkList"
import { Bookmark } from "lucide-react"

export const metadata: Metadata = {
title: "Bookmarks",
description: "Revisit and organize your bookmarked AI Tutor responses.",
}

export default function BookmarksPage() {
return (
<div className="flex-1 space-y-6 p-4 md:p-8 max-w-7xl mx-auto">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10">
<Bookmark className="h-6 w-6 text-primary" />
</div>
<div>
<h1 className="text-2xl font-bold tracking-tight text-foreground md:text-3xl">Saved Bookmarks</h1>
<p className="text-sm text-muted-foreground md:text-base">
Your personal knowledge library of bookmarked AI responses.
</p>
</div>
</div>

<BookmarkList />
</div>
)
}
25 changes: 13 additions & 12 deletions app/api/ai/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

export const dynamic = "force-dynamic";

import { NextRequest, NextResponse } from "next/server";
Expand Down Expand Up @@ -97,19 +98,19 @@ export async function POST(req: NextRequest) {

const aiResponse = attachments.length
? await analyzeAttachmentsWithGemini({
message:
cleanMessage ||
"Please review the uploaded file and help me understand it.",
attachments,
webContext,
})
message:
cleanMessage ||
"Please review the uploaded file and help me understand it.",
attachments,
webContext,
})
: await generateAIResponse(
cleanMessage || "Please help me with the uploaded study material.",
{
mode,
webContext,
},
);
cleanMessage || "Please help me with the uploaded study material.",
{
mode,
webContext,
},
);

const finalReply = formatReplyWithSources(aiResponse, sources);

Expand Down
26 changes: 26 additions & 0 deletions app/api/bookmarks/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextResponse } from "next/server"
import { getUser } from "@/lib/auth-server"
import { deleteBookmark } from "@/lib/bookmarks-db"

export const dynamic = "force-dynamic"

export async function DELETE(
_req: Request,
context: { params: Promise<{ id: string }> }
) {
try {
const user = await getUser()
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}

const { id } = await context.params
await deleteBookmark(user.id, id)

return NextResponse.json({ success: true })
} catch (err) {
console.error("[api/bookmarks/[id]] DELETE Error:", err)
const msg = err instanceof Error ? err.message : "Failed to delete bookmark"
return NextResponse.json({ error: msg }, { status: 500 })
}
}
64 changes: 64 additions & 0 deletions app/api/bookmarks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { NextResponse, NextRequest } from "next/server"
import { getUser } from "@/lib/auth-server"
import { getBookmarks, createBookmark } from "@/lib/bookmarks-db"
import { z } from "zod"

export const dynamic = "force-dynamic"

const createBookmarkSchema = z.object({
question: z.string().min(1, "Question is required"),
answer: z.string().min(1, "Answer is required"),
subject: z.string().nullable().optional(),
tags: z.array(z.string()).default([]),
})

export async function GET(req: NextRequest) {
try {
const user = await getUser()
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}

const { searchParams } = new URL(req.url)
const search = searchParams.get("search") || undefined
const tag = searchParams.get("tag") || undefined

const bookmarks = await getBookmarks(user.id, { search, tag })
return NextResponse.json({ success: true, data: bookmarks })
} catch (err) {
console.error("[api/bookmarks] GET Error:", err)
const msg = err instanceof Error ? err.message : "Failed to load bookmarks"
return NextResponse.json({ error: msg }, { status: 500 })
}
}

export async function POST(req: Request) {
try {
const user = await getUser()
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}

const body = await req.json().catch(() => ({}))
const result = createBookmarkSchema.safeParse(body)

if (!result.success) {
const errorMsg = result.error.errors.map((e) => e.message).join(", ")
return NextResponse.json({ error: errorMsg }, { status: 400 })
}

const bookmark = await createBookmark(user.id, result.data)
return NextResponse.json({ success: true, data: bookmark }, { status: 201 })
} catch (err) {
console.error("[api/bookmarks] POST Error:", err)
const code = (err as { code?: string })?.code
if (code === "DUPLICATE_BOOKMARK") {
return NextResponse.json(
{ error: "This response is already bookmarked", code: "DUPLICATE_BOOKMARK" },
{ status: 409 }
)
}
const msg = err instanceof Error ? err.message : "Failed to save bookmark"
return NextResponse.json({ error: msg }, { status: 500 })
}
}
109 changes: 109 additions & 0 deletions components/bookmarks/BookmarkButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Bookmark, BookmarkCheck, Loader2 } from "lucide-react"
import { useToast } from "@/hooks/use-toast"
import { cn } from "@/lib/utils"

interface BookmarkButtonProps {
question: string
answer: string
subject?: string | null
tags?: string[]
className?: string
}

export function BookmarkButton({
question,
answer,
subject,
tags = [],
className,
}: BookmarkButtonProps) {
const [isBookmarked, setIsBookmarked] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const { toast } = useToast()

const handleBookmark = async (e: React.MouseEvent) => {
e.stopPropagation()
if (isSaving || isBookmarked) return

setIsSaving(true)
try {
const response = await fetch("/api/bookmarks", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
question,
answer,
subject: subject || "AI Tutor",
tags,
}),
})

const data = await response.json()

if (response.status === 401) {
toast({
title: "Sign in required",
description: "You must be signed in to bookmark AI Tutor responses.",
variant: "destructive",
})
return
}

if (response.status === 409 || data.code === "DUPLICATE_BOOKMARK") {
setIsBookmarked(true)
toast({
title: "Already bookmarked",
description: "This response is already saved in your bookmarks.",
})
return
}

if (!response.ok) {
throw new Error(data.error || "Failed to save bookmark")
}

setIsBookmarked(true)
toast({
title: "Bookmark saved",
description: "AI response has been bookmarked successfully.",
})
} catch (error) {
toast({
variant: "destructive",
title: "Error saving bookmark",
description: error instanceof Error ? error.message : "Something went wrong.",
})
} finally {
setIsSaving(false)
}
}

return (
<Button
variant="ghost"
size="icon"
className={cn(
"h-7 w-7 text-muted-foreground hover:text-foreground transition-colors duration-200",
isBookmarked && "text-primary hover:text-primary",
className
)}
onClick={handleBookmark}
disabled={isSaving}
title={isBookmarked ? "Bookmarked" : "Bookmark this answer"}
>
{isSaving ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : isBookmarked ? (
<BookmarkCheck className="h-3.5 w-3.5" />
) : (
<Bookmark className="h-3.5 w-3.5" />
)}
</Button>
)
}
Loading