Spaces:
Sleeping
Sleeping
Commit ·
9081dbe
0
Parent(s):
init
Browse files- .gitignore +12 -0
- Dockerfile +40 -0
- README.md +45 -0
- app.py +141 -0
- frontend/.gitignore +24 -0
- frontend/README.md +73 -0
- frontend/eslint.config.js +23 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +41 -0
- frontend/public/vite.svg +1 -0
- frontend/src/App.css +42 -0
- frontend/src/App.tsx +139 -0
- frontend/src/CustomNode.tsx +41 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/index.css +11 -0
- frontend/src/main.tsx +10 -0
- frontend/tsconfig.app.json +28 -0
- frontend/tsconfig.json +7 -0
- frontend/tsconfig.node.json +26 -0
- frontend/vite.config.ts +16 -0
- requirements.txt +3 -0
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
.env
|
| 5 |
+
.venv
|
| 6 |
+
venv/
|
| 7 |
+
env/
|
| 8 |
+
data/
|
| 9 |
+
frontend/node_modules/
|
| 10 |
+
frontend/dist/
|
| 11 |
+
frontend/.env
|
| 12 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stage 1: Build Frontend
|
| 2 |
+
FROM node:20-slim AS frontend-builder
|
| 3 |
+
WORKDIR /app/frontend
|
| 4 |
+
COPY frontend/package*.json ./
|
| 5 |
+
RUN npm install
|
| 6 |
+
COPY frontend/ ./
|
| 7 |
+
RUN npm run build
|
| 8 |
+
|
| 9 |
+
# Stage 2: Backend Setup
|
| 10 |
+
FROM python:3.9-slim
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
|
| 13 |
+
# Install system dependencies if needed
|
| 14 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 15 |
+
git \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
+
|
| 18 |
+
# Copy backend requirements
|
| 19 |
+
COPY requirements.txt .
|
| 20 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 21 |
+
|
| 22 |
+
# Copy backend code
|
| 23 |
+
COPY app.py .
|
| 24 |
+
COPY data ./data
|
| 25 |
+
|
| 26 |
+
# Copy built frontend from Stage 1
|
| 27 |
+
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
|
| 28 |
+
|
| 29 |
+
# Set permissions for data directory (crucial for HF Spaces)
|
| 30 |
+
RUN chmod -R 777 /app/data
|
| 31 |
+
|
| 32 |
+
# Environment variables
|
| 33 |
+
ENV PYTHONUNBUFFERED=1
|
| 34 |
+
ENV PORT=7860
|
| 35 |
+
|
| 36 |
+
# Expose port
|
| 37 |
+
EXPOSE 7860
|
| 38 |
+
|
| 39 |
+
# Run command
|
| 40 |
+
CMD ["python", "app.py"]
|
README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Skill Tree Tracker
|
| 2 |
+
|
| 3 |
+
A personal growth visualization tool that helps you track your skills, learning progress, and future roadmap. Built with React Flow and Python Flask, deployable to Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **Visual Skill Tree**: Interactive node-based graph to map out your skills.
|
| 8 |
+
- **Progress Tracking**: Mark skills as Pending, In Progress, or Completed.
|
| 9 |
+
- **Data Persistence**: Saves data locally and syncs to Hugging Face Dataset (optional).
|
| 10 |
+
- **Modern UI**: Clean interface built with Tailwind CSS.
|
| 11 |
+
|
| 12 |
+
## Local Development
|
| 13 |
+
|
| 14 |
+
### Prerequisites
|
| 15 |
+
|
| 16 |
+
- Node.js 20+
|
| 17 |
+
- Python 3.9+
|
| 18 |
+
|
| 19 |
+
### Setup
|
| 20 |
+
|
| 21 |
+
1. **Backend**:
|
| 22 |
+
```bash
|
| 23 |
+
pip install -r requirements.txt
|
| 24 |
+
python app.py
|
| 25 |
+
```
|
| 26 |
+
The backend runs on http://localhost:7860.
|
| 27 |
+
|
| 28 |
+
2. **Frontend**:
|
| 29 |
+
```bash
|
| 30 |
+
cd frontend
|
| 31 |
+
npm install
|
| 32 |
+
npm run dev
|
| 33 |
+
```
|
| 34 |
+
The frontend runs on http://localhost:5173.
|
| 35 |
+
|
| 36 |
+
### Deployment to Hugging Face Spaces
|
| 37 |
+
|
| 38 |
+
1. Create a new Space on Hugging Face (Docker SDK).
|
| 39 |
+
2. Upload the contents of this repository.
|
| 40 |
+
3. Set `HF_TOKEN` in Space secrets if you want data sync capabilities.
|
| 41 |
+
4. Set `HF_DB_REPO` to your dataset repository ID (e.g., `username/skill-tree-data`).
|
| 42 |
+
|
| 43 |
+
## Data Sync
|
| 44 |
+
|
| 45 |
+
The application automatically syncs `skills.json` to a configured Hugging Face Dataset. This ensures your progress is saved even if the Space restarts.
|
app.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import threading
|
| 4 |
+
import time
|
| 5 |
+
import shutil
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from flask import Flask, jsonify, request, send_from_directory
|
| 8 |
+
try:
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
load_dotenv(override=True)
|
| 11 |
+
except ImportError:
|
| 12 |
+
pass
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from huggingface_hub import HfApi, snapshot_download
|
| 16 |
+
except ImportError:
|
| 17 |
+
HfApi = None
|
| 18 |
+
|
| 19 |
+
app = Flask(__name__, static_folder='frontend/dist')
|
| 20 |
+
|
| 21 |
+
# Configuration
|
| 22 |
+
DATA_DIR = Path("data")
|
| 23 |
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
| 24 |
+
SKILLS_FILE = DATA_DIR / "skills.json"
|
| 25 |
+
REPO_ID = os.getenv("HF_DB_REPO", "duqing2026/skill-tree-data") # Default repo, user should configure
|
| 26 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 27 |
+
|
| 28 |
+
# Default initial data
|
| 29 |
+
DEFAULT_SKILLS = {
|
| 30 |
+
"nodes": [
|
| 31 |
+
{"id": "1", "type": "input", "data": {"label": "开始", "status": "completed"}, "position": {"x": 250, "y": 0}},
|
| 32 |
+
{"id": "2", "data": {"label": "学习 Python", "status": "in_progress"}, "position": {"x": 100, "y": 100}},
|
| 33 |
+
{"id": "3", "data": {"label": "学习 React", "status": "pending"}, "position": {"x": 400, "y": 100}},
|
| 34 |
+
],
|
| 35 |
+
"edges": [
|
| 36 |
+
{"id": "e1-2", "source": "1", "target": "2"},
|
| 37 |
+
{"id": "e1-3", "source": "1", "target": "3"},
|
| 38 |
+
]
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
class SyncManager:
|
| 42 |
+
def __init__(self, repo_id, token, data_dir):
|
| 43 |
+
self.repo_id = repo_id
|
| 44 |
+
self.token = token
|
| 45 |
+
self.data_dir = data_dir
|
| 46 |
+
self.api = HfApi(token=token) if HfApi and token else None
|
| 47 |
+
self.is_pushing = False
|
| 48 |
+
|
| 49 |
+
def pull(self):
|
| 50 |
+
if not self.api:
|
| 51 |
+
print("Sync: Skipping pull (no API/Token)")
|
| 52 |
+
return
|
| 53 |
+
print("Sync: Pulling data...")
|
| 54 |
+
try:
|
| 55 |
+
temp_dir = self.data_dir.parent / "temp_data_sync"
|
| 56 |
+
if temp_dir.exists(): shutil.rmtree(temp_dir)
|
| 57 |
+
temp_dir.mkdir()
|
| 58 |
+
|
| 59 |
+
snapshot_download(
|
| 60 |
+
repo_id=self.repo_id,
|
| 61 |
+
repo_type="dataset",
|
| 62 |
+
local_dir=temp_dir,
|
| 63 |
+
token=self.token,
|
| 64 |
+
allow_patterns=["skills.json"]
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
# Simple overwrite strategy for now, as this is a personal tool
|
| 68 |
+
# In a real multi-user scenario, we'd need smart merging
|
| 69 |
+
src = temp_dir / "skills.json"
|
| 70 |
+
dst = self.data_dir / "skills.json"
|
| 71 |
+
if src.exists():
|
| 72 |
+
shutil.copy2(src, dst)
|
| 73 |
+
print("Sync: Data pulled successfully.")
|
| 74 |
+
|
| 75 |
+
shutil.rmtree(temp_dir)
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"Sync: Pull failed: {e}")
|
| 78 |
+
|
| 79 |
+
def push(self):
|
| 80 |
+
if not self.api: return
|
| 81 |
+
if self.is_pushing: return
|
| 82 |
+
|
| 83 |
+
print("Sync: Pushing data...")
|
| 84 |
+
self.is_pushing = True
|
| 85 |
+
try:
|
| 86 |
+
self.api.upload_file(
|
| 87 |
+
path_or_fileobj=self.data_dir / "skills.json",
|
| 88 |
+
path_in_repo="skills.json",
|
| 89 |
+
repo_id=self.repo_id,
|
| 90 |
+
repo_type="dataset",
|
| 91 |
+
commit_message=f"Update skills.json {int(time.time())}"
|
| 92 |
+
)
|
| 93 |
+
print("Sync: Push successful.")
|
| 94 |
+
except Exception as e:
|
| 95 |
+
print(f"Sync: Push failed: {e}")
|
| 96 |
+
finally:
|
| 97 |
+
self.is_pushing = False
|
| 98 |
+
|
| 99 |
+
sync_manager = SyncManager(REPO_ID, HF_TOKEN, DATA_DIR)
|
| 100 |
+
|
| 101 |
+
# Initialize data
|
| 102 |
+
if not SKILLS_FILE.exists():
|
| 103 |
+
# Try to pull first
|
| 104 |
+
sync_manager.pull()
|
| 105 |
+
# If still not exists, use default
|
| 106 |
+
if not SKILLS_FILE.exists():
|
| 107 |
+
with open(SKILLS_FILE, "w") as f:
|
| 108 |
+
json.dump(DEFAULT_SKILLS, f, indent=2)
|
| 109 |
+
|
| 110 |
+
@app.route("/")
|
| 111 |
+
def serve_index():
|
| 112 |
+
if os.path.exists(os.path.join(app.static_folder, "index.html")):
|
| 113 |
+
return send_from_directory(app.static_folder, "index.html")
|
| 114 |
+
return "Frontend not built. Please run `npm run build` in frontend directory."
|
| 115 |
+
|
| 116 |
+
@app.route("/<path:path>")
|
| 117 |
+
def serve_static(path):
|
| 118 |
+
if os.path.exists(os.path.join(app.static_folder, path)):
|
| 119 |
+
return send_from_directory(app.static_folder, path)
|
| 120 |
+
return serve_index()
|
| 121 |
+
|
| 122 |
+
@app.route("/api/skills", methods=["GET"])
|
| 123 |
+
def get_skills():
|
| 124 |
+
if SKILLS_FILE.exists():
|
| 125 |
+
with open(SKILLS_FILE, "r") as f:
|
| 126 |
+
return jsonify(json.load(f))
|
| 127 |
+
return jsonify(DEFAULT_SKILLS)
|
| 128 |
+
|
| 129 |
+
@app.route("/api/skills", methods=["POST"])
|
| 130 |
+
def save_skills():
|
| 131 |
+
data = request.json
|
| 132 |
+
with open(SKILLS_FILE, "w") as f:
|
| 133 |
+
json.dump(data, f, indent=2)
|
| 134 |
+
|
| 135 |
+
# Trigger sync in background
|
| 136 |
+
threading.Thread(target=sync_manager.push).start()
|
| 137 |
+
|
| 138 |
+
return jsonify({"status": "success"})
|
| 139 |
+
|
| 140 |
+
if __name__ == "__main__":
|
| 141 |
+
app.run(host="0.0.0.0", port=7861)
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
frontend/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + TypeScript + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
| 9 |
+
|
| 10 |
+
## React Compiler
|
| 11 |
+
|
| 12 |
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
| 13 |
+
|
| 14 |
+
## Expanding the ESLint configuration
|
| 15 |
+
|
| 16 |
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
| 17 |
+
|
| 18 |
+
```js
|
| 19 |
+
export default defineConfig([
|
| 20 |
+
globalIgnores(['dist']),
|
| 21 |
+
{
|
| 22 |
+
files: ['**/*.{ts,tsx}'],
|
| 23 |
+
extends: [
|
| 24 |
+
// Other configs...
|
| 25 |
+
|
| 26 |
+
// Remove tseslint.configs.recommended and replace with this
|
| 27 |
+
tseslint.configs.recommendedTypeChecked,
|
| 28 |
+
// Alternatively, use this for stricter rules
|
| 29 |
+
tseslint.configs.strictTypeChecked,
|
| 30 |
+
// Optionally, add this for stylistic rules
|
| 31 |
+
tseslint.configs.stylisticTypeChecked,
|
| 32 |
+
|
| 33 |
+
// Other configs...
|
| 34 |
+
],
|
| 35 |
+
languageOptions: {
|
| 36 |
+
parserOptions: {
|
| 37 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 38 |
+
tsconfigRootDir: import.meta.dirname,
|
| 39 |
+
},
|
| 40 |
+
// other options...
|
| 41 |
+
},
|
| 42 |
+
},
|
| 43 |
+
])
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
| 47 |
+
|
| 48 |
+
```js
|
| 49 |
+
// eslint.config.js
|
| 50 |
+
import reactX from 'eslint-plugin-react-x'
|
| 51 |
+
import reactDom from 'eslint-plugin-react-dom'
|
| 52 |
+
|
| 53 |
+
export default defineConfig([
|
| 54 |
+
globalIgnores(['dist']),
|
| 55 |
+
{
|
| 56 |
+
files: ['**/*.{ts,tsx}'],
|
| 57 |
+
extends: [
|
| 58 |
+
// Other configs...
|
| 59 |
+
// Enable lint rules for React
|
| 60 |
+
reactX.configs['recommended-typescript'],
|
| 61 |
+
// Enable lint rules for React DOM
|
| 62 |
+
reactDom.configs.recommended,
|
| 63 |
+
],
|
| 64 |
+
languageOptions: {
|
| 65 |
+
parserOptions: {
|
| 66 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 67 |
+
tsconfigRootDir: import.meta.dirname,
|
| 68 |
+
},
|
| 69 |
+
// other options...
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
])
|
| 73 |
+
```
|
frontend/eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 7 |
+
|
| 8 |
+
export default defineConfig([
|
| 9 |
+
globalIgnores(['dist']),
|
| 10 |
+
{
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs.flat.recommended,
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
ecmaVersion: 2020,
|
| 20 |
+
globals: globals.browser,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
])
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>frontend</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@tailwindcss/vite": "^4.1.18",
|
| 14 |
+
"@xyflow/react": "^12.10.0",
|
| 15 |
+
"clsx": "^2.1.1",
|
| 16 |
+
"lucide-react": "^0.563.0",
|
| 17 |
+
"react": "^19.2.0",
|
| 18 |
+
"react-dom": "^19.2.0",
|
| 19 |
+
"tailwind-merge": "^3.4.0"
|
| 20 |
+
},
|
| 21 |
+
"devDependencies": {
|
| 22 |
+
"@eslint/js": "^9.39.1",
|
| 23 |
+
"@types/node": "^24.10.1",
|
| 24 |
+
"@types/react": "^19.2.5",
|
| 25 |
+
"@types/react-dom": "^19.2.3",
|
| 26 |
+
"@vitejs/plugin-react": "^5.1.1",
|
| 27 |
+
"autoprefixer": "^10.4.23",
|
| 28 |
+
"eslint": "^9.39.1",
|
| 29 |
+
"eslint-plugin-react-hooks": "^7.0.1",
|
| 30 |
+
"eslint-plugin-react-refresh": "^0.4.24",
|
| 31 |
+
"globals": "^16.5.0",
|
| 32 |
+
"postcss": "^8.5.6",
|
| 33 |
+
"tailwindcss": "^4.1.18",
|
| 34 |
+
"typescript": "~5.9.3",
|
| 35 |
+
"typescript-eslint": "^8.46.4",
|
| 36 |
+
"vite": "npm:rolldown-vite@7.2.5"
|
| 37 |
+
},
|
| 38 |
+
"overrides": {
|
| 39 |
+
"vite": "npm:rolldown-vite@7.2.5"
|
| 40 |
+
}
|
| 41 |
+
}
|
frontend/public/vite.svg
ADDED
|
|
frontend/src/App.css
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#root {
|
| 2 |
+
max-width: 1280px;
|
| 3 |
+
margin: 0 auto;
|
| 4 |
+
padding: 2rem;
|
| 5 |
+
text-align: center;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.logo {
|
| 9 |
+
height: 6em;
|
| 10 |
+
padding: 1.5em;
|
| 11 |
+
will-change: filter;
|
| 12 |
+
transition: filter 300ms;
|
| 13 |
+
}
|
| 14 |
+
.logo:hover {
|
| 15 |
+
filter: drop-shadow(0 0 2em #646cffaa);
|
| 16 |
+
}
|
| 17 |
+
.logo.react:hover {
|
| 18 |
+
filter: drop-shadow(0 0 2em #61dafbaa);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
@keyframes logo-spin {
|
| 22 |
+
from {
|
| 23 |
+
transform: rotate(0deg);
|
| 24 |
+
}
|
| 25 |
+
to {
|
| 26 |
+
transform: rotate(360deg);
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
@media (prefers-reduced-motion: no-preference) {
|
| 31 |
+
a:nth-of-type(2) .logo {
|
| 32 |
+
animation: logo-spin infinite 20s linear;
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.card {
|
| 37 |
+
padding: 2em;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.read-the-docs {
|
| 41 |
+
color: #888;
|
| 42 |
+
}
|
frontend/src/App.tsx
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useEffect, useState } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
ReactFlow,
|
| 4 |
+
MiniMap,
|
| 5 |
+
Controls,
|
| 6 |
+
Background,
|
| 7 |
+
useNodesState,
|
| 8 |
+
useEdgesState,
|
| 9 |
+
addEdge,
|
| 10 |
+
Panel,
|
| 11 |
+
type Connection,
|
| 12 |
+
type Node,
|
| 13 |
+
} from '@xyflow/react';
|
| 14 |
+
import '@xyflow/react/dist/style.css';
|
| 15 |
+
import { CustomNode } from './CustomNode';
|
| 16 |
+
import { Save, Plus } from 'lucide-react';
|
| 17 |
+
|
| 18 |
+
const nodeTypes = {
|
| 19 |
+
custom: CustomNode,
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
const INITIAL_NODES: Node[] = [
|
| 23 |
+
{ id: '1', type: 'custom', data: { label: '开始', status: 'completed' }, position: { x: 250, y: 5 } },
|
| 24 |
+
];
|
| 25 |
+
|
| 26 |
+
export default function App() {
|
| 27 |
+
const [nodes, setNodes, onNodesChange] = useNodesState(INITIAL_NODES);
|
| 28 |
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
| 29 |
+
const [loading, setLoading] = useState(true);
|
| 30 |
+
|
| 31 |
+
// Fetch initial data
|
| 32 |
+
useEffect(() => {
|
| 33 |
+
fetch('/api/skills')
|
| 34 |
+
.then(res => res.json())
|
| 35 |
+
.then(data => {
|
| 36 |
+
if (data.nodes && data.nodes.length > 0) {
|
| 37 |
+
// Map standard types to custom if needed
|
| 38 |
+
const mappedNodes = data.nodes.map((n: Node) => ({
|
| 39 |
+
...n,
|
| 40 |
+
type: 'custom', // Force custom type
|
| 41 |
+
}));
|
| 42 |
+
setNodes(mappedNodes);
|
| 43 |
+
setEdges(data.edges || []);
|
| 44 |
+
}
|
| 45 |
+
})
|
| 46 |
+
.catch(err => console.error("Failed to load skills", err))
|
| 47 |
+
.finally(() => setLoading(false));
|
| 48 |
+
}, [setNodes, setEdges]);
|
| 49 |
+
|
| 50 |
+
const onConnect = useCallback(
|
| 51 |
+
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
|
| 52 |
+
[setEdges],
|
| 53 |
+
);
|
| 54 |
+
|
| 55 |
+
const onSave = useCallback(() => {
|
| 56 |
+
fetch('/api/skills', {
|
| 57 |
+
method: 'POST',
|
| 58 |
+
headers: { 'Content-Type': 'application/json' },
|
| 59 |
+
body: JSON.stringify({ nodes, edges }),
|
| 60 |
+
}).then(() => alert('保存成功!'));
|
| 61 |
+
}, [nodes, edges]);
|
| 62 |
+
|
| 63 |
+
const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
|
| 64 |
+
// Simple toggle status logic for demo
|
| 65 |
+
setNodes((nds) =>
|
| 66 |
+
nds.map((n) => {
|
| 67 |
+
if (n.id === node.id) {
|
| 68 |
+
const statuses = ['pending', 'in_progress', 'completed'];
|
| 69 |
+
const currentIdx = statuses.indexOf((n.data.status as string) || 'pending');
|
| 70 |
+
const nextStatus = statuses[(currentIdx + 1) % 3];
|
| 71 |
+
return {
|
| 72 |
+
...n,
|
| 73 |
+
data: { ...n.data, status: nextStatus },
|
| 74 |
+
};
|
| 75 |
+
}
|
| 76 |
+
return n;
|
| 77 |
+
})
|
| 78 |
+
);
|
| 79 |
+
}, [setNodes]);
|
| 80 |
+
|
| 81 |
+
const onAddNode = useCallback(() => {
|
| 82 |
+
const id = (Math.max(...nodes.map(n => parseInt(n.id) || 0), 0) + 1).toString();
|
| 83 |
+
const newNode: Node = {
|
| 84 |
+
id,
|
| 85 |
+
type: 'custom',
|
| 86 |
+
position: { x: Math.random() * 400 + 100, y: Math.random() * 400 + 100 },
|
| 87 |
+
data: { label: `技能 ${id}`, status: 'pending' },
|
| 88 |
+
};
|
| 89 |
+
setNodes((nds) => nds.concat(newNode));
|
| 90 |
+
}, [nodes, setNodes]);
|
| 91 |
+
|
| 92 |
+
if (loading) return <div className="flex items-center justify-center h-screen" style={{ height: '100vh' }}>加载中...</div>;
|
| 93 |
+
|
| 94 |
+
return (
|
| 95 |
+
<div className="w-full h-screen bg-gray-50" style={{ height: '100vh', width: '100vw' }}>
|
| 96 |
+
<ReactFlow
|
| 97 |
+
nodes={nodes}
|
| 98 |
+
edges={edges}
|
| 99 |
+
onNodesChange={onNodesChange}
|
| 100 |
+
onEdgesChange={onEdgesChange}
|
| 101 |
+
onConnect={onConnect}
|
| 102 |
+
onNodeClick={onNodeClick}
|
| 103 |
+
nodeTypes={nodeTypes}
|
| 104 |
+
fitView
|
| 105 |
+
>
|
| 106 |
+
<Controls />
|
| 107 |
+
<MiniMap />
|
| 108 |
+
<Background gap={12} size={1} />
|
| 109 |
+
<Panel position="top-right" className="flex gap-2">
|
| 110 |
+
<button
|
| 111 |
+
onClick={onAddNode}
|
| 112 |
+
className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-50 text-gray-700 font-medium cursor-pointer"
|
| 113 |
+
>
|
| 114 |
+
<Plus className="w-4 h-4" />
|
| 115 |
+
添加技能
|
| 116 |
+
</button>
|
| 117 |
+
<button
|
| 118 |
+
onClick={onSave}
|
| 119 |
+
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg shadow-sm hover:bg-blue-700 font-medium cursor-pointer"
|
| 120 |
+
>
|
| 121 |
+
<Save className="w-4 h-4" />
|
| 122 |
+
保存进度
|
| 123 |
+
</button>
|
| 124 |
+
</Panel>
|
| 125 |
+
<Panel position="top-left" className="bg-white/80 backdrop-blur p-4 rounded-lg border border-gray-200 shadow-sm max-w-sm">
|
| 126 |
+
<h1 className="text-xl font-bold text-gray-900 mb-1">技能树追踪器</h1>
|
| 127 |
+
<p className="text-sm text-gray-500">
|
| 128 |
+
点击节点切换状态,拖拽手柄连接节点。
|
| 129 |
+
</p>
|
| 130 |
+
<div className="flex gap-3 mt-3 text-xs">
|
| 131 |
+
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-gray-400"></div>待办</div>
|
| 132 |
+
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-blue-400"></div>进行中</div>
|
| 133 |
+
<div className="flex items-center gap-1"><div className="w-2 h-2 rounded-full bg-green-400"></div>已完成</div>
|
| 134 |
+
</div>
|
| 135 |
+
</Panel>
|
| 136 |
+
</ReactFlow>
|
| 137 |
+
</div>
|
| 138 |
+
);
|
| 139 |
+
}
|
frontend/src/CustomNode.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Handle, Position, type NodeProps, type Node } from '@xyflow/react';
|
| 2 |
+
import { clsx } from 'clsx';
|
| 3 |
+
import { CheckCircle2, Circle, Clock } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
type NodeData = {
|
| 6 |
+
label: string;
|
| 7 |
+
status: 'pending' | 'in_progress' | 'completed';
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
// We need to extend NodeProps to include our specific data type
|
| 11 |
+
export function CustomNode({ data }: NodeProps<Node<NodeData>>) {
|
| 12 |
+
const statusColors = {
|
| 13 |
+
pending: 'bg-gray-100 border-gray-300 text-gray-500',
|
| 14 |
+
in_progress: 'bg-blue-50 border-blue-400 text-blue-700',
|
| 15 |
+
completed: 'bg-green-50 border-green-400 text-green-700',
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
const status = (data.status as keyof typeof statusColors) || 'pending';
|
| 19 |
+
|
| 20 |
+
const StatusIcon = {
|
| 21 |
+
pending: Circle,
|
| 22 |
+
in_progress: Clock,
|
| 23 |
+
completed: CheckCircle2,
|
| 24 |
+
}[status];
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<div className={clsx(
|
| 28 |
+
"px-4 py-2 rounded-lg border-2 shadow-sm min-w-[150px] transition-all bg-white",
|
| 29 |
+
statusColors[status]
|
| 30 |
+
)}>
|
| 31 |
+
<Handle type="target" position={Position.Top} className="w-3 h-3 !bg-gray-400" />
|
| 32 |
+
|
| 33 |
+
<div className="flex items-center gap-2">
|
| 34 |
+
<StatusIcon className="w-4 h-4" />
|
| 35 |
+
<span className="font-medium text-sm">{data.label}</span>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<Handle type="source" position={Position.Bottom} className="w-3 h-3 !bg-gray-400" />
|
| 39 |
+
</div>
|
| 40 |
+
);
|
| 41 |
+
}
|
frontend/src/assets/react.svg
ADDED
|
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
@theme {
|
| 4 |
+
--font-sans: "Inter", system-ui, sans-serif;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
html, body, #root {
|
| 8 |
+
height: 100%;
|
| 9 |
+
margin: 0;
|
| 10 |
+
font-family: var(--font-sans);
|
| 11 |
+
}
|
frontend/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.tsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
frontend/tsconfig.app.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "ES2022",
|
| 5 |
+
"useDefineForClassFields": true,
|
| 6 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 7 |
+
"module": "ESNext",
|
| 8 |
+
"types": ["vite/client"],
|
| 9 |
+
"skipLibCheck": true,
|
| 10 |
+
|
| 11 |
+
/* Bundler mode */
|
| 12 |
+
"moduleResolution": "bundler",
|
| 13 |
+
"allowImportingTsExtensions": true,
|
| 14 |
+
"verbatimModuleSyntax": true,
|
| 15 |
+
"moduleDetection": "force",
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
|
| 19 |
+
/* Linting */
|
| 20 |
+
"strict": true,
|
| 21 |
+
"noUnusedLocals": true,
|
| 22 |
+
"noUnusedParameters": true,
|
| 23 |
+
"erasableSyntaxOnly": true,
|
| 24 |
+
"noFallthroughCasesInSwitch": true,
|
| 25 |
+
"noUncheckedSideEffectImports": true
|
| 26 |
+
},
|
| 27 |
+
"include": ["src"]
|
| 28 |
+
}
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
frontend/tsconfig.node.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
| 4 |
+
"target": "ES2023",
|
| 5 |
+
"lib": ["ES2023"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"types": ["node"],
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"erasableSyntaxOnly": true,
|
| 22 |
+
"noFallthroughCasesInSwitch": true,
|
| 23 |
+
"noUncheckedSideEffectImports": true
|
| 24 |
+
},
|
| 25 |
+
"include": ["vite.config.ts"]
|
| 26 |
+
}
|
frontend/vite.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import tailwindcss from '@tailwindcss/vite'
|
| 4 |
+
|
| 5 |
+
// https://vite.dev/config/
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
plugins: [react(), tailwindcss()],
|
| 8 |
+
server: {
|
| 9 |
+
proxy: {
|
| 10 |
+
'/api': {
|
| 11 |
+
target: 'http://127.0.0.1:7861',
|
| 12 |
+
changeOrigin: true,
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
})
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
python-dotenv
|
| 3 |
+
huggingface_hub
|