- Connect your Nextcloud account to import PDFs directly when uploading.
+ Connect your Nextcloud to import PDFs directly from Upload page.
- {saved &&
+
Save
+
+ {status === 'testing' ? 'Testing...' : 'Test Connection'}
+
{username && Disconnect }
- Credentials are stored locally in your browser. Use an App Password, not your account password.
+ Use an App Password (Nextcloud → Settings → Security), not your account password.
)
diff --git a/frontend/src/pages/UploadPage.jsx b/frontend/src/pages/UploadPage.jsx
index c7cb63f..0f883c3 100644
--- a/frontend/src/pages/UploadPage.jsx
+++ b/frontend/src/pages/UploadPage.jsx
@@ -2,19 +2,134 @@ import { useState, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '../api/client'
+function NextcloudBrowser({ onFile }) {
+ const ncServer = localStorage.getItem('nc_server') || ''
+ const ncUser = localStorage.getItem('nc_username') || ''
+ const ncPass = localStorage.getItem('nc_password') || ''
+
+ const [path, setPath] = useState('/')
+ const [items, setItems] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [downloading, setDownloading] = useState(null)
+ const [error, setError] = useState('')
+
+ const browse = async (p = path) => {
+ setLoading(true); setError('')
+ try {
+ const res = await api.post('/nextcloud/files', { server: ncServer, username: ncUser, password: ncPass, path: p })
+ setItems(res.data.items)
+ setPath(p)
+ } catch (err) {
+ setError(err.response?.data?.detail || 'Failed to load files')
+ } finally { setLoading(false) }
+ }
+
+ const importFile = async (item) => {
+ setDownloading(item.path)
+ try {
+ const res = await api.post('/nextcloud/download',
+ { server: ncServer, username: ncUser, password: ncPass, path: item.path },
+ { responseType: 'blob' }
+ )
+ const file = new File([res.data], item.name, { type: 'application/pdf' })
+ onFile(file)
+ } catch (err) {
+ setError(err.response?.data?.detail || 'Download failed')
+ } finally { setDownloading(null) }
+ }
+
+ const goUp = () => {
+ const parts = path.replace(/\/$/, '').split('/')
+ parts.pop()
+ browse(parts.join('/') || '/')
+ }
+
+ if (!ncUser) {
+ return (
+
+
☁️
+ No Nextcloud account configured.{' '}
+
Go to Settings to add one.
+
+ )
+ }
+
+ return (
+
+ {error &&
{error}
}
+
+ ☁️ {ncServer}
+ →
+ {path}
+ {path !== '/' && (
+ ↑ Up
+ )}
+ {items === null && (
+ browse('/')} disabled={loading}>
+ {loading ? 'Loading...' : 'Browse'}
+
+ )}
+ {items !== null && (
+ browse(path)} disabled={loading}>
+ {loading ? '...' : '↻'}
+
+ )}
+
+
+ {items !== null && (
+
+ {items.length === 0 && (
+
+ No PDFs in this folder
+
+ )}
+ {items.map((item, i) => (
+
+ {item.type === 'dir' ? '📁' : '📄'}
+
+ {item.name}
+ {item.type === 'pdf' && (
+
+ {(item.size / 1024 / 1024).toFixed(1)} MB
+
+ )}
+
+ {item.type === 'dir' ? (
+ browse(item.path)}>Open
+ ) : (
+ importFile(item)}
+ disabled={downloading === item.path}>
+ {downloading === item.path ? 'Downloading...' : 'Import'}
+
+ )}
+
+ ))}
+
+ )}
+
+ )
+}
+
export default function UploadPage() {
const [file, setFile] = useState(null)
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const [error, setError] = useState('')
const [dragging, setDragging] = useState(false)
+ const [tab, setTab] = useState('local') // 'local' | 'nextcloud'
const fileRef = useRef()
const navigate = useNavigate()
+ const hasNextcloud = !!localStorage.getItem('nc_username')
+
const handleFile = (f) => {
if (f && f.type === 'application/pdf') {
- setFile(f)
- setError('')
+ setFile(f); setError(''); setTab('local')
} else {
setError('Please select a PDF file')
}
@@ -22,91 +137,91 @@ export default function UploadPage() {
const handleUpload = async () => {
if (!file) return
- setUploading(true)
- setError('')
-
+ setUploading(true); setError('')
const formData = new FormData()
formData.append('file', file)
-
try {
const res = await api.post('/documents/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
- onUploadProgress: (e) => {
- if (e.total) setProgress(Math.round((e.loaded / e.total) * 100))
- },
+ onUploadProgress: (e) => { if (e.total) setProgress(Math.round((e.loaded / e.total) * 100)) },
})
navigate(`/documents/${res.data.id}`)
} catch (err) {
setError(err.response?.data?.detail || 'Upload failed')
- } finally {
- setUploading(false)
- }
+ } finally { setUploading(false) }
}
return (
Upload PDF Document
-
- Upload a PDF file (up to 500MB) to generate interactive quizzes from its content.
+
+ Upload a PDF file (up to 500MB) to generate interactive quizzes.
+ {/* Tab selector */}
+
+ setTab('local')}>
+ 💻 Local File
+
+ setTab('nextcloud')}>
+ ☁️ Nextcloud{!hasNextcloud && ' (not set up)'}
+
+
+
{error &&
{error}
}
-
fileRef.current?.click()}
- onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
- onDragLeave={() => setDragging(false)}
- onDrop={(e) => {
- e.preventDefault()
- setDragging(false)
- handleFile(e.dataTransfer.files[0])
- }}
- >
- {file ? (
-
-
{file.name}
-
- {(file.size / 1024 / 1024).toFixed(1)} MB
-
+ {tab === 'local' && (
+ <>
+
fileRef.current?.click()}
+ onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
+ onDragLeave={() => setDragging(false)}
+ onDrop={(e) => { e.preventDefault(); setDragging(false); handleFile(e.dataTransfer.files[0]) }}
+ >
+ {file ? (
+
+
{file.name}
+
{(file.size / 1024 / 1024).toFixed(1)} MB
+
+ ) : (
+
+
PDF
+
Click or drag a PDF file here
+
Supports files up to 500MB
+
+ )}
- ) : (
-
-
PDF
-
Click or drag a PDF file here
-
- Supports files up to 500MB
-
-
- )}
-
+
handleFile(e.target.files[0])} />
+ >
+ )}
-
handleFile(e.target.files[0])}
- />
+ {tab === 'nextcloud' && (
+
+ )}
+
+ {/* Selected file from Nextcloud */}
+ {tab === 'nextcloud' && file && (
+
+ Selected: {file.name} ({(file.size / 1024 / 1024).toFixed(1)} MB)
+ setFile(null)} style={{ marginLeft: 12, background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer' }}>✕
+
+ )}
{uploading && (
-
-
+
)}
-
+
{uploading ? `Uploading ${progress}%...` : 'Upload & Process'}
- navigate('/')}>
- Cancel
-
+ navigate('/')}>Cancel