updating whole project
This commit is contained in:
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
.cache/
|
||||
.npm/
|
||||
.yarn/
|
||||
.pnp.cjs
|
||||
.pnp.loader.mjs
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Generated static assets
|
||||
*.out
|
||||
*.log
|
||||
|
||||
# React-specific
|
||||
*.map
|
||||
69
README.md
Normal file
69
README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# RAG Data Uploader
|
||||
|
||||
This is a minimal React front‑end for your Retrieval‑Augmented Generation (RAG) school project. It allows you to:
|
||||
|
||||
- Fetch and list available subjects from the back‑end.
|
||||
- Select a subject to view its details and list of lessons.
|
||||
- Select a lesson and upload custom text (with an optional title) associated with that subject/lesson.
|
||||
|
||||
The client uses the API endpoints documented in your OpenAPI file:
|
||||
|
||||
- `GET /api/subjects/` — list available subjects
|
||||
- `GET /api/lessons/?subject=<subject_id>` — list lessons for a subject
|
||||
- `POST /api/upload-text` — upload a text payload for a subject/lesson
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Node.js** (≥14) installed on your machine.
|
||||
2. The back‑end API running locally or remotely and accepting requests at a known base URL.
|
||||
|
||||
## Configuration
|
||||
|
||||
The front‑end expects an environment variable called `VITE_API_BASE_URL` to prefix API calls. If your API is served from `http://localhost:8000`, you would set:
|
||||
|
||||
```bash
|
||||
export VITE_API_BASE_URL=http://localhost:8000
|
||||
```
|
||||
|
||||
If you do not provide this environment variable the client assumes the API is served from the same origin as the front‑end (i.e. relative paths such as `/api/subjects/`).
|
||||
|
||||
To set local environment variables for Vite, you can create a `.env` file in the project root with the following content:
|
||||
|
||||
```
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
```
|
||||
|
||||
## Running the application
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
cd rag-data-frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Vite will start a server (default port `3000`) and automatically open the app in your browser. Any changes you make to the source files will hot reload during development.
|
||||
|
||||
3. Build for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
The optimized, minified output will be placed in the `dist/` directory. You can serve these static files from any HTTP server.
|
||||
|
||||
## Customizing
|
||||
|
||||
This project was kept deliberately simple so you can adapt it to your needs:
|
||||
|
||||
* **Styling**: The default CSS is minimal. You can swap it out for Tailwind, Bootstrap or your own styles.
|
||||
* **Fields**: The upload payload currently uses the lesson’s `chapter` property as the `lesson_number`. Adjust this logic in `src/App.jsx` if your API expects a different value.
|
||||
* **Error handling**: Basic errors are surfaced under the form. You can extend this to surface more detailed error messages or integrate a notification library.
|
||||
|
||||
Happy coding!
|
||||
17
index.html
Normal file
17
index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>RAG Data Uploader</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
Vite will inject your compiled React bundle here. The script tag
|
||||
points at our entry point in the src directory. When you run
|
||||
`npm run dev` Vite serves this page and hot‑reloads changes.
|
||||
-->
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
1547
package-lock.json
generated
Normal file
1547
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "rag-data-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "A minimal React front‑end for selecting subjects and lessons and uploading custom text to the RAG back end.",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"vite": "^4.4.0"
|
||||
}
|
||||
}
|
||||
633
src/App.jsx
Normal file
633
src/App.jsx
Normal file
@@ -0,0 +1,633 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import axios from "axios";
|
||||
|
||||
const API_BASE =
|
||||
(typeof import.meta !== "undefined" && import.meta.env?.VITE_API_BASE_URL) ||
|
||||
"https://apirag.tadrisino.org";
|
||||
|
||||
const GRADES = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
function normalizeAxiosError(err) {
|
||||
const out = { title: "Request failed", status: null, lines: [] };
|
||||
|
||||
if (!err) {
|
||||
out.lines.push("Unknown error.");
|
||||
return out;
|
||||
}
|
||||
|
||||
// Network/CORS/timeout
|
||||
if (!err.response) {
|
||||
out.title = "Network error";
|
||||
out.lines.push(
|
||||
err.message ||
|
||||
"No response received. Check internet, CORS, and server availability."
|
||||
);
|
||||
return out;
|
||||
}
|
||||
|
||||
const { status, statusText, data } = err.response;
|
||||
out.status = status;
|
||||
out.title = `HTTP ${status}${statusText ? ` ${statusText}` : ""}`;
|
||||
|
||||
if (data == null) {
|
||||
out.lines.push("Empty error response from server.");
|
||||
return out;
|
||||
}
|
||||
|
||||
if (typeof data === "string") {
|
||||
out.lines.push(data);
|
||||
return out;
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((x) => out.lines.push(String(x)));
|
||||
return out;
|
||||
}
|
||||
|
||||
if (typeof data === "object") {
|
||||
// Common keys
|
||||
if (typeof data.detail === "string") out.lines.push(data.detail);
|
||||
if (typeof data.message === "string") out.lines.push(data.message);
|
||||
if (typeof data.error === "string") out.lines.push(data.error);
|
||||
|
||||
// Field errors (DRF-like): {field: ["msg"], ...}
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (["detail", "message", "error"].includes(k)) continue;
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
v.forEach((msg) => out.lines.push(`${k}: ${String(msg)}`));
|
||||
} else if (v && typeof v === "object") {
|
||||
out.lines.push(`${k}: ${JSON.stringify(v)}`);
|
||||
} else if (v != null) {
|
||||
out.lines.push(`${k}: ${String(v)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (out.lines.length === 0) out.lines.push(JSON.stringify(data));
|
||||
return out;
|
||||
}
|
||||
|
||||
out.lines.push(String(data));
|
||||
return out;
|
||||
}
|
||||
|
||||
function StepChip({ active, done, label }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "8px 10px",
|
||||
borderRadius: 999,
|
||||
border: "1px solid #e6e8f0",
|
||||
background: active ? "#111827" : "#fff",
|
||||
color: active ? "#fff" : "#344054",
|
||||
fontSize: 12,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 99,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
border: "1px solid",
|
||||
borderColor: active ? "#111827" : "#d0d5dd",
|
||||
background: done ? "#22c55e" : active ? "#fff" : "#fff",
|
||||
color: done ? "#fff" : active ? "#111827" : "#667085",
|
||||
fontWeight: 800,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{done ? "✓" : "•"}
|
||||
</span>
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
// Step state
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
// Selections
|
||||
const [gradeId, setGradeId] = useState("");
|
||||
const [subjectId, setSubjectId] = useState("");
|
||||
const [lessonId, setLessonId] = useState("");
|
||||
|
||||
// Loaded data
|
||||
const [subjects, setSubjects] = useState([]);
|
||||
const [lessons, setLessons] = useState([]);
|
||||
|
||||
// Upload fields
|
||||
const [lessonNumber, setLessonNumber] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const [text, setText] = useState("");
|
||||
|
||||
// UI states
|
||||
const [loadingSubjects, setLoadingSubjects] = useState(false);
|
||||
const [loadingLessons, setLoadingLessons] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
// Messaging
|
||||
const [success, setSuccess] = useState("");
|
||||
const [errorBox, setErrorBox] = useState(null);
|
||||
|
||||
function clearMessages() {
|
||||
setSuccess("");
|
||||
setErrorBox(null);
|
||||
}
|
||||
|
||||
const selectedSubject = useMemo(
|
||||
() => subjects.find((s) => String(s.id) === String(subjectId)) || null,
|
||||
[subjects, subjectId]
|
||||
);
|
||||
|
||||
const selectedLesson = useMemo(
|
||||
() => lessons.find((l) => String(l.id) === String(lessonId)) || null,
|
||||
[lessons, lessonId]
|
||||
);
|
||||
|
||||
// Keep step consistent
|
||||
useEffect(() => {
|
||||
if (!gradeId) setStep(1);
|
||||
else if (!subjectId) setStep(2);
|
||||
else if (!lessonId) setStep(3);
|
||||
else setStep(4);
|
||||
}, [gradeId, subjectId, lessonId]);
|
||||
|
||||
function resetAfterGradeChange() {
|
||||
setSubjects([]);
|
||||
setLessons([]);
|
||||
setSubjectId("");
|
||||
setLessonId("");
|
||||
setLessonNumber("");
|
||||
setTitle("");
|
||||
setText("");
|
||||
}
|
||||
|
||||
function resetAfterSubjectChange() {
|
||||
setLessons([]);
|
||||
setLessonId("");
|
||||
setLessonNumber("");
|
||||
setTitle("");
|
||||
setText("");
|
||||
}
|
||||
|
||||
// Step 2: Load subjects (GET /subjects/?grade=)
|
||||
async function loadSubjects() {
|
||||
clearMessages();
|
||||
if (!gradeId) {
|
||||
setErrorBox({
|
||||
title: "Missing grade",
|
||||
status: null,
|
||||
lines: ["Select a grade first."],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingSubjects(true);
|
||||
try {
|
||||
const res = await axios.get(`${API_BASE}/api/subjects/`, {
|
||||
params: { grade: Number(gradeId) },
|
||||
});
|
||||
|
||||
if (!Array.isArray(res.data)) {
|
||||
setSubjects([]);
|
||||
setErrorBox({
|
||||
title: "Unexpected response",
|
||||
status: 200,
|
||||
lines: ["Subjects response is not a list (array)."],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSubjects(res.data);
|
||||
setSubjectId("");
|
||||
resetAfterSubjectChange();
|
||||
setStep(2);
|
||||
} catch (err) {
|
||||
setSubjects([]);
|
||||
setErrorBox(normalizeAxiosError(err));
|
||||
} finally {
|
||||
setLoadingSubjects(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Load lessons (GET /lessons/?grade=&subject=)
|
||||
async function loadLessons() {
|
||||
clearMessages();
|
||||
if (!gradeId || !subjectId) {
|
||||
setErrorBox({
|
||||
title: "Missing selection",
|
||||
status: null,
|
||||
lines: ["Select grade and subject first."],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingLessons(true);
|
||||
try {
|
||||
const res = await axios.get(`${API_BASE}/api/lessons/`, {
|
||||
params: {
|
||||
grade: Number(gradeId),
|
||||
subject: Number(subjectId),
|
||||
},
|
||||
});
|
||||
|
||||
if (!Array.isArray(res.data)) {
|
||||
setLessons([]);
|
||||
setErrorBox({
|
||||
title: "Unexpected response",
|
||||
status: 200,
|
||||
lines: ["Lessons response is not a list (array)."],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLessons(res.data);
|
||||
setLessonId("");
|
||||
setLessonNumber("");
|
||||
setTitle("");
|
||||
setText("");
|
||||
setStep(3);
|
||||
} catch (err) {
|
||||
setLessons([]);
|
||||
setErrorBox(normalizeAxiosError(err));
|
||||
} finally {
|
||||
setLoadingLessons(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-fill lesson_number if API provides something usable; user can override
|
||||
useEffect(() => {
|
||||
if (!selectedLesson) return;
|
||||
|
||||
const maybe =
|
||||
selectedLesson.lesson_number ??
|
||||
selectedLesson.lessonNumber ??
|
||||
selectedLesson.chapter ??
|
||||
selectedLesson.number ??
|
||||
"";
|
||||
|
||||
if (lessonNumber === "" && maybe !== "" && !Number.isNaN(Number(maybe))) {
|
||||
setLessonNumber(String(maybe));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedLesson]);
|
||||
|
||||
const payload = useMemo(
|
||||
() => ({
|
||||
lesson_number: Number(lessonNumber),
|
||||
title: title ?? "",
|
||||
text: text ?? "",
|
||||
subject_id: Number(subjectId),
|
||||
}),
|
||||
[lessonNumber, title, text, subjectId]
|
||||
);
|
||||
|
||||
const validationErrors = useMemo(() => {
|
||||
const errs = [];
|
||||
if (!gradeId) errs.push("Grade is required.");
|
||||
if (!subjectId) errs.push("Subject is required.");
|
||||
if (!lessonId) errs.push("Lesson is required.");
|
||||
if (lessonNumber === "" || Number.isNaN(Number(lessonNumber)))
|
||||
errs.push("Lesson number must be a valid number.");
|
||||
if (!text.trim()) errs.push("Text is required.");
|
||||
return errs;
|
||||
}, [gradeId, subjectId, lessonId, lessonNumber, text]);
|
||||
|
||||
async function uploadText() {
|
||||
clearMessages();
|
||||
|
||||
if (validationErrors.length) {
|
||||
setErrorBox({
|
||||
title: "Fix these before upload",
|
||||
status: null,
|
||||
lines: validationErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
const res = await axios.post(`${API_BASE}/api/upload-text`, payload, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
// You asked specifically: when upload was 201, clear textarea to avoid duplicates
|
||||
if (res?.status === 201) {
|
||||
setTitle("");
|
||||
setText("");
|
||||
} else {
|
||||
// Still a success (2xx), but not 201; keep behavior safe and consistent
|
||||
setTitle("");
|
||||
setText("");
|
||||
}
|
||||
|
||||
setSuccess("✅ Uploaded successfully.");
|
||||
} catch (err) {
|
||||
setErrorBox(normalizeAxiosError(err));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const done1 = !!gradeId;
|
||||
const done2 = !!subjectId;
|
||||
const done3 =
|
||||
!!lessonId && lessonNumber !== "" && !Number.isNaN(Number(lessonNumber));
|
||||
const done4 = false;
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "100vh", background: "#f6f7fb", padding: 22 }}>
|
||||
<style>{`
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; }
|
||||
.wrap { max-width: 980px; margin: 0 auto; }
|
||||
.card { background: #fff; border: 1px solid #e6e8f0; border-radius: 16px; box-shadow: 0 10px 30px rgba(10, 20, 60, 0.06); overflow: hidden; }
|
||||
.head { padding: 16px 16px 12px; border-bottom: 1px solid #eef0f6; display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; align-items: baseline; }
|
||||
h1 { margin: 0; font-size: 18px; }
|
||||
.sub { margin: 0; color: #667085; font-size: 13px; }
|
||||
.pill { border: 1px solid #e6e8f0; border-radius: 999px; padding: 6px 10px; font-size: 12px; color: #344054; background: #fff; }
|
||||
.steps { padding: 12px 16px; display: flex; gap: 8px; flex-wrap: wrap; border-bottom: 1px solid #eef0f6; background: #fcfdff; }
|
||||
.body { padding: 16px; display: grid; grid-template-columns: 1fr; gap: 14px; }
|
||||
@media (min-width: 900px) { .body { grid-template-columns: 1.05fr 0.95fr; } }
|
||||
.panel { border: 1px solid #eef0f6; border-radius: 14px; background: #fff; padding: 14px; }
|
||||
.panel h3 { margin: 0 0 10px; font-size: 14px; color: #111827; }
|
||||
label { display: block; font-size: 12px; color: #344054; margin: 10px 0 6px; }
|
||||
select, input, textarea {
|
||||
width: 100%;
|
||||
border: 1px solid #d9dce7;
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
outline: none;
|
||||
}
|
||||
textarea { resize: vertical; min-height: 150px; }
|
||||
select:disabled, input:disabled, textarea:disabled { opacity: 0.65; background: #f5f6fa; }
|
||||
.hint { margin-top: 6px; font-size: 12px; color: #667085; }
|
||||
.btnrow { display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap; margin-top: 12px; }
|
||||
.btn {
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #d0d5dd;
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
}
|
||||
.btn.secondary { background: #fff; color: #111827; }
|
||||
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
.msg { margin: 12px 16px 0; border-radius: 14px; padding: 12px; border: 1px solid; }
|
||||
.msg.success { background: #ecfdf3; border-color: #abefc6; color: #067647; }
|
||||
.msg.error { background: #fffbfa; border-color: #fecdca; color: #b42318; }
|
||||
.msg h4 { margin: 0 0 6px; font-size: 13px; }
|
||||
.msg ul { margin: 0; padding-left: 18px; }
|
||||
.kv { display: grid; grid-template-columns: 120px 1fr; gap: 8px 12px; font-size: 13px; color: #111827; }
|
||||
.muted { color: #667085; }
|
||||
`}</style>
|
||||
|
||||
<div className="wrap">
|
||||
<div className="card">
|
||||
<div className="head">
|
||||
<div>
|
||||
<h1>RAG Text Uploader</h1>
|
||||
<p className="sub">Step-by-step upload flow</p>
|
||||
</div>
|
||||
<div className="pill">Ready</div>
|
||||
</div>
|
||||
|
||||
<div className="steps">
|
||||
<StepChip active={step === 1} done={done1} label="1) Grade" />
|
||||
<StepChip active={step === 2} done={done2} label="2) Subject" />
|
||||
<StepChip active={step === 3} done={done3} label="3) Lesson + number" />
|
||||
<StepChip active={step === 4} done={done4} label="4) Upload" />
|
||||
</div>
|
||||
|
||||
{success ? (
|
||||
<div className="msg success">
|
||||
<h4>Success</h4>
|
||||
<div>{success}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{errorBox ? (
|
||||
<div className="msg error">
|
||||
<h4>
|
||||
{errorBox.title}
|
||||
{typeof errorBox.status === "number" ? ` (status ${errorBox.status})` : ""}
|
||||
</h4>
|
||||
<ul>
|
||||
{errorBox.lines.map((l, i) => (
|
||||
<li key={i}>{l}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="body">
|
||||
{/* LEFT: Steps */}
|
||||
<div className="panel">
|
||||
<h3>Steps</h3>
|
||||
|
||||
<label>Step 1: Choose Grade</label>
|
||||
<select
|
||||
value={gradeId}
|
||||
onChange={(e) => {
|
||||
clearMessages();
|
||||
setGradeId(e.target.value);
|
||||
setStep(1);
|
||||
resetAfterGradeChange();
|
||||
}}
|
||||
disabled={uploading}
|
||||
>
|
||||
<option value="">Select grade</option>
|
||||
{GRADES.map((g) => (
|
||||
<option key={g} value={g}>
|
||||
Grade {g}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="btnrow">
|
||||
<button
|
||||
className="btn"
|
||||
onClick={loadSubjects}
|
||||
disabled={!gradeId || loadingSubjects || uploading}
|
||||
>
|
||||
{loadingSubjects ? "Loading..." : "Step 2: Load Subjects"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label style={{ marginTop: 14 }}>Step 2: Choose Subject</label>
|
||||
<select
|
||||
value={subjectId}
|
||||
onChange={(e) => {
|
||||
clearMessages();
|
||||
setSubjectId(e.target.value);
|
||||
resetAfterSubjectChange();
|
||||
setStep(2);
|
||||
}}
|
||||
disabled={!subjects.length || loadingSubjects || uploading}
|
||||
>
|
||||
<option value="">
|
||||
{!gradeId
|
||||
? "Select grade first"
|
||||
: loadingSubjects
|
||||
? "Loading..."
|
||||
: subjects.length
|
||||
? "Select subject"
|
||||
: "No subjects (load first)"}
|
||||
</option>
|
||||
{subjects.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name ?? `Subject #${s.id}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="btnrow">
|
||||
<button
|
||||
className="btn"
|
||||
onClick={loadLessons}
|
||||
disabled={!gradeId || !subjectId || loadingLessons || uploading}
|
||||
>
|
||||
{loadingLessons ? "Loading..." : "Step 3: Load Lessons"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label style={{ marginTop: 14 }}>Step 3: Choose Lesson</label>
|
||||
<select
|
||||
value={lessonId}
|
||||
onChange={(e) => {
|
||||
clearMessages();
|
||||
setLessonId(e.target.value);
|
||||
setStep(3);
|
||||
}}
|
||||
disabled={!lessons.length || loadingLessons || uploading}
|
||||
>
|
||||
<option value="">
|
||||
{!subjectId
|
||||
? "Select subject first"
|
||||
: loadingLessons
|
||||
? "Loading..."
|
||||
: lessons.length
|
||||
? "Select lesson"
|
||||
: "No lessons (load first)"}
|
||||
</option>
|
||||
{lessons.map((l) => (
|
||||
<option key={l.id} value={l.id}>
|
||||
{l.title ?? `Lesson #${l.id}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<label>Lesson Number</label>
|
||||
<input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
value={lessonNumber}
|
||||
onChange={(e) => {
|
||||
clearMessages();
|
||||
setLessonNumber(e.target.value);
|
||||
}}
|
||||
disabled={!lessonId || uploading}
|
||||
placeholder="Example: 1"
|
||||
/>
|
||||
|
||||
<div className="btnrow" style={{ marginTop: 14 }}>
|
||||
<button
|
||||
className="btn secondary"
|
||||
onClick={() => {
|
||||
clearMessages();
|
||||
setTitle("");
|
||||
setText("");
|
||||
}}
|
||||
disabled={uploading}
|
||||
>
|
||||
Clear text
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn"
|
||||
onClick={uploadText}
|
||||
disabled={uploading || validationErrors.length > 0}
|
||||
title={validationErrors.length ? validationErrors.join(" | ") : "Upload"}
|
||||
>
|
||||
{uploading ? "Uploading..." : "Step 4: Upload"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{validationErrors.length > 0 ? (
|
||||
<div className="hint">
|
||||
<strong className="muted">Blocked:</strong>{" "}
|
||||
{validationErrors.join(" • ")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* RIGHT: Form */}
|
||||
<div className="panel">
|
||||
<h3>Upload Form</h3>
|
||||
|
||||
<div className="kv">
|
||||
<div className="muted">Grade</div>
|
||||
<div>{gradeId || "-"}</div>
|
||||
|
||||
<div className="muted">Subject</div>
|
||||
<div>
|
||||
{selectedSubject
|
||||
? `${selectedSubject.name} (ID: ${selectedSubject.id})`
|
||||
: "-"}
|
||||
</div>
|
||||
|
||||
<div className="muted">Lesson</div>
|
||||
<div>
|
||||
{selectedLesson
|
||||
? `${selectedLesson.title ?? "Lesson"} (ID: ${selectedLesson.id})`
|
||||
: "-"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label style={{ marginTop: 14 }}>Title (optional)</label>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => {
|
||||
clearMessages();
|
||||
setTitle(e.target.value);
|
||||
}}
|
||||
disabled={!lessonId || uploading}
|
||||
placeholder="Optional title"
|
||||
/>
|
||||
|
||||
<label>Text</label>
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={(e) => {
|
||||
clearMessages();
|
||||
setText(e.target.value);
|
||||
}}
|
||||
disabled={!lessonId || uploading}
|
||||
placeholder="Paste your content here..."
|
||||
/>
|
||||
|
||||
<div className="hint">
|
||||
After a successful upload, the text is cleared automatically to prevent double-submitting.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "0 16px 16px" }} className="hint">
|
||||
Tip: If you see an error, it will show the server details here clearly.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
src/index.css
Normal file
83
src/index.css
Normal file
@@ -0,0 +1,83 @@
|
||||
/* Basic styles for the RAG data uploader. */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
background-color: #f9f9f9;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
padding: 1rem;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
select,
|
||||
input[type='text'],
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
13
src/main.jsx
Normal file
13
src/main.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.jsx';
|
||||
import './index.css';
|
||||
|
||||
// Entrypoint for our React app.
|
||||
// This mounts the App component into the DOM element with id "root".
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
12
vite.config.js
Normal file
12
vite.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// Vite configuration for the RAG data uploader.
|
||||
// See https://vitejs.dev/config/ for more details.
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user