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