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
154 changes: 134 additions & 20 deletions app/(dashboard)/flashcards/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Label } from "@/components/ui/label"
import {
ChevronLeft,
Expand Down Expand Up @@ -114,6 +120,7 @@ export default function FlashcardsPage() {
const [historyLoading, setHistoryLoading] = useState(true)
const [currentSavedSetId, setCurrentSavedSetId] = useState<string | null>(null)
const [reviewSubmitting, setReviewSubmitting] = useState(false)
const [isExportingPdf, setIsExportingPdf] = useState(false)

const currentCard = cards[currentIndex]
const masteredCount = useMemo(() => cards.filter((c) => c.mastered).length, [cards])
Expand Down Expand Up @@ -362,8 +369,28 @@ export default function FlashcardsPage() {
setIsFlipped(false)
}

const canExport = cards.length >= 1 && cards[0].id !== "demo"

function exportFilename(extension: string) {
const cleanTopic = aiTopic ? aiTopic.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-") : "set"
return `edupilot-flashcards-${cleanTopic}-${Date.now()}.${extension}`
}

function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const link = document.createElement("a")

link.setAttribute("href", url)
link.setAttribute("download", filename)
link.style.visibility = "hidden"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}

const handleExportCSV = () => {
if (cards.length <= 1 || cards[0].id === "demo") return
if (!canExport) return

const headers = `"Front","Back"\n`
const csvContent = cards
Expand All @@ -377,20 +404,91 @@ export default function FlashcardsPage() {
const csvData = headers + csvContent

const blob = new Blob([csvData], { type: "text/csv;charset=utf-8;" })
const url = URL.createObjectURL(blob)
const link = document.createElement("a")

// Clean topic name for filename
const cleanTopic = aiTopic ? aiTopic.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-") : "set"
const filename = `edupilot-flashcards-${cleanTopic}-${Date.now()}.csv`

link.setAttribute("href", url)
link.setAttribute("download", filename)
link.style.visibility = "hidden"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
downloadBlob(blob, exportFilename("csv"))
}

const handleExportPDF = async () => {
if (!canExport || isExportingPdf) return

setIsExportingPdf(true)

try {
const html2pdf = (await import("html2pdf.js")).default

const container = document.createElement("div")
container.style.fontFamily = "Arial, Helvetica, sans-serif"
container.style.padding = "24px"
container.style.color = "#111111"
container.style.background = "#ffffff"

const title = document.createElement("h1")
title.textContent = aiTopic ? `Flashcards: ${aiTopic}` : "Flashcards"
title.style.fontSize = "20px"
title.style.marginBottom = "4px"
container.appendChild(title)

const subtitle = document.createElement("p")
subtitle.textContent = `${cards.length} card${cards.length === 1 ? "" : "s"} · Generated by EduPilot`
subtitle.style.fontSize = "11px"
subtitle.style.color = "#666666"
subtitle.style.marginBottom = "16px"
container.appendChild(subtitle)

cards.forEach((card, index) => {
const cardEl = document.createElement("div")
cardEl.style.border = "1px solid #d0d0d0"
cardEl.style.borderRadius = "8px"
cardEl.style.padding = "12px 16px"
cardEl.style.marginBottom = "10px"
cardEl.style.pageBreakInside = "avoid"
cardEl.style.breakInside = "avoid"

const label = document.createElement("div")
label.textContent = `Card ${index + 1}`
label.style.fontSize = "10px"
label.style.color = "#888888"
label.style.marginBottom = "6px"
cardEl.appendChild(label)

const front = document.createElement("div")
front.textContent = card.front
front.style.fontSize = "13px"
front.style.fontWeight = "600"
front.style.marginBottom = "6px"
front.style.whiteSpace = "pre-wrap"
cardEl.appendChild(front)

const divider = document.createElement("div")
divider.style.borderTop = "1px dashed #cccccc"
divider.style.margin = "6px 0"
cardEl.appendChild(divider)

const back = document.createElement("div")
back.textContent = card.back
back.style.fontSize = "12px"
back.style.color = "#333333"
back.style.whiteSpace = "pre-wrap"
cardEl.appendChild(back)

container.appendChild(cardEl)
})

await html2pdf()
.set({
margin: 10,
filename: exportFilename("pdf"),
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
// `pagebreak` isn't in the shipped type defs but is a supported html2pdf.js option.
...({ pagebreak: { mode: ["css", "legacy"] } } as object),
})
.from(container)
.save()
} catch (err) {
console.error("Failed to export flashcards as PDF", err)
} finally {
setIsExportingPdf(false)
}
}

const handleGenerate = async () => {
Expand Down Expand Up @@ -599,11 +697,27 @@ export default function FlashcardsPage() {
<RotateCcw className="h-4 w-4" />
Reset
</Button>
{cards.length > 1 && cards[0].id !== "demo" && (
<Button variant="outline" size="sm" className="gap-1.5" onClick={handleExportCSV}>
<Download className="h-4 w-4" />
Export CSV
</Button>
{canExport && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-1.5" disabled={isExportingPdf}>
{isExportingPdf ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
Export
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center">
<DropdownMenuItem onClick={() => void handleExportPDF()} disabled={isExportingPdf}>
Export as PDF
</DropdownMenuItem>
<DropdownMenuItem onClick={handleExportCSV}>
Export as CSV (Anki)
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>

Expand Down
6 changes: 4 additions & 2 deletions app/api/documents/upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server"
import { createServerClient } from "@supabase/ssr"
import pdfParse from "pdf-parse/lib/pdf-parse"
import { PDFParse } from "pdf-parse"
import { generateEmbedding } from "@/lib/ai"

export const maxDuration = 60
Expand Down Expand Up @@ -50,8 +50,10 @@ export async function POST(req: NextRequest) {
// 2. Parse PDF Text
const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
const pdfData = await pdfParse(buffer)
const parser = new PDFParse({ data: buffer })
const pdfData = await parser.getText()
const text = pdfData.text
await parser.destroy()

// 3. Create document record in DB
const { data: doc, error: docError } = await supabase
Expand Down
Loading
Loading