Phase 0: React environment setup + project initialization
구현 사항: 1. React + TypeScript 마이그레이션 시작 - package.json: React, Redux Toolkit, TanStack Query, shadcn/ui dependencies 추가 - vite.config.js: React 플러그인 + Phase 5/7 API 프록시 추가 - tsconfig.json, tsconfig.node.json 추가 2. Redux 스토어 설정 - stores/ontologySlice.ts: 온톨로지 상태 관리 - stores/crawlSlice.ts: 크롤링 진행 상태 - stores/uiSlice.ts: UI 상태 3. 리액트 진입점 및 기본 컴포넌트 - src/main.tsx: React 앱 초기화 - src/App.tsx: 라우팅 설정 (대시보드, 온보딩, 소스, 크롤, 검증) - 5개 페이지 컴포넌트 (placeholder) 4. 스타일 및 설정 - tailwind.config.js: Tailwind 설정 - src/styles/globals.css: 글로벌 스타일 - postcss.config.js: PostCSS 설정 - src/i18n.ts: i18next 다국어 설정 - src/lib/queryClient.ts: React Query 설정 5. TypeScript 타입 정의 - src/stores/slices: 각 Redux slice의 TypeScript 타입 프로젝트 구조: crawler_platform/app/web/frontend/ ├── src/ │ ├── main.tsx (React 진입점) │ ├── App.tsx (라우팅) │ ├── pages/ (4개 페이지 컴포넌트) │ ├── stores/ (Redux 스토어 + slices) │ ├── lib/ (유틸리티) │ ├── styles/ (CSS) │ └── i18n.ts (다국어 설정) ├── index.html (업데이트: React root) ├── package.json (의존성 추가) ├── vite.config.js (React 플러그인) ├── tsconfig.json (TypeScript 설정) ├── tailwind.config.js (Tailwind 설정) └── postcss.config.js (PostCSS 설정) 다음 단계: - npm install로 의존성 설치 - Phase 1: 온톨로지 업로더 구현 - Phase 1: 참고 사이트 매니저 구현 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,9 @@
|
|||||||
"Bash(python -m pytest tests/core/graph/test_entity_resolver.py -v --tb=line)",
|
"Bash(python -m pytest tests/core/graph/test_entity_resolver.py -v --tb=line)",
|
||||||
"Bash(python -m pytest tests/api/test_phase5_app.py -v --tb=short)",
|
"Bash(python -m pytest tests/api/test_phase5_app.py -v --tb=short)",
|
||||||
"Bash(python -m pytest tests/core/graph/test_subgraph_retriever.py -v --tb=short)",
|
"Bash(python -m pytest tests/core/graph/test_subgraph_retriever.py -v --tb=short)",
|
||||||
"Bash(python -m pytest tests/core/graph/test_entity_resolver.py tests/core/graph/test_subgraph_retriever.py tests/core/graph/test_rdf_converter.py tests/api/test_phase5_app.py -v --tb=line)"
|
"Bash(python -m pytest tests/core/graph/test_entity_resolver.py tests/core/graph/test_subgraph_retriever.py tests/core/graph/test_rdf_converter.py tests/api/test_phase5_app.py -v --tb=line)",
|
||||||
|
"Bash(python -m pytest tests/integration/test_phase5_phase7_integration.py -v --tb=short)",
|
||||||
|
"Bash(python -m pytest tests/core/graph/test_entity_resolver.py tests/core/graph/test_subgraph_retriever.py tests/core/graph/test_rdf_converter.py tests/api/test_phase5_app.py tests/integration/test_phase5_phase7_integration.py -v --tb=no)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Ontology Crawler Platform</title>
|
<meta name="description" content="Ontology Construction Platform with Phase 5 GraphRAG and Phase 7 LLM" />
|
||||||
|
<title>Ontology Builder - AI-Powered Ontology Construction</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,17 +1,44 @@
|
|||||||
{
|
{
|
||||||
"name": "crawler-platform-ui",
|
"name": "crawler-platform-ui",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.0.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cytoscape": "^3.33.3"
|
"@hookform/resolvers": "^3.3.4",
|
||||||
|
"@reduxjs/toolkit": "^1.9.7",
|
||||||
|
"@tanstack/react-query": "^5.28.0",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"cytoscape": "^3.33.3",
|
||||||
|
"i18next": "^23.7.6",
|
||||||
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
|
"i18next-http-backend": "^2.4.2",
|
||||||
|
"lucide-react": "^0.292.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hook-form": "^7.48.0",
|
||||||
|
"react-i18next": "^13.4.0",
|
||||||
|
"react-redux": "^1.13.0",
|
||||||
|
"react-router-dom": "^6.20.0",
|
||||||
|
"reactflow": "^11.10.1",
|
||||||
|
"sonner": "^1.2.3",
|
||||||
|
"tailwind-merge": "^2.2.0",
|
||||||
|
"zod": "^3.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
crawler_platform/app/web/frontend/postcss.config.js
Normal file
6
crawler_platform/app/web/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
25
crawler_platform/app/web/frontend/src/App.tsx
Normal file
25
crawler_platform/app/web/frontend/src/App.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import OnboardingPage from "./pages/OnboardingPage";
|
||||||
|
import ConfigureSourcesPage from "./pages/ConfigureSourcesPage";
|
||||||
|
import CrawlPage from "./pages/CrawlPage";
|
||||||
|
import ReviewPage from "./pages/ReviewPage";
|
||||||
|
import DashboardPage from "./pages/DashboardPage";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={i18n.language === "ko" ? "font-sans" : "font-sans"}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<DashboardPage />} />
|
||||||
|
<Route path="/onboard" element={<OnboardingPage />} />
|
||||||
|
<Route path="/sources/:projectId" element={<ConfigureSourcesPage />} />
|
||||||
|
<Route path="/crawl/:projectId" element={<CrawlPage />} />
|
||||||
|
<Route path="/review/:projectId" element={<ReviewPage />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
25
crawler_platform/app/web/frontend/src/i18n.ts
Normal file
25
crawler_platform/app/web/frontend/src/i18n.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
import HttpBackend from "i18next-http-backend";
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(HttpBackend)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: "en",
|
||||||
|
ns: ["common", "pages"],
|
||||||
|
defaultNS: "common",
|
||||||
|
backend: {
|
||||||
|
loadPath: "/static/locales/{{lng}}/{{ns}}.json",
|
||||||
|
},
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false,
|
||||||
|
},
|
||||||
|
react: {
|
||||||
|
useSuspense: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
14
crawler_platform/app/web/frontend/src/lib/queryClient.ts
Normal file
14
crawler_platform/app/web/frontend/src/lib/queryClient.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { QueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime)
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
24
crawler_platform/app/web/frontend/src/main.tsx
Normal file
24
crawler_platform/app/web/frontend/src/main.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { Toaster } from "sonner";
|
||||||
|
import "./i18n";
|
||||||
|
import App from "./App";
|
||||||
|
import store from "./stores";
|
||||||
|
import { queryClient } from "./lib/queryClient";
|
||||||
|
import "./styles/globals.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<BrowserRouter basename="/static/">
|
||||||
|
<App />
|
||||||
|
<Toaster position="top-right" />
|
||||||
|
</BrowserRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</Provider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function ConfigureSourcesPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { projectId } = useParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-4">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
{t("Configure Reference Sites")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
{t("Step 2 of 4: Add 5-20 reference sites for knowledge extraction")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<p className="text-gray-500">{t("No sites added yet")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate("/onboard")}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition"
|
||||||
|
>
|
||||||
|
{t("Back")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/crawl/${projectId}`)}
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
{t("Next")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
crawler_platform/app/web/frontend/src/pages/CrawlPage.tsx
Normal file
40
crawler_platform/app/web/frontend/src/pages/CrawlPage.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function CrawlPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { projectId } = useParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-4">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
{t("Build Ontology")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
{t("Step 3 of 4: Auto-crawl and build ontology with Phase 5 + 7")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<p className="text-gray-500">{t("Crawl pipeline will appear here")}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/sources/${projectId}`)}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition"
|
||||||
|
>
|
||||||
|
{t("Back")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/review/${projectId}`)}
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
{t("Review Results")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Plus, BarChart3, Settings } from "lucide-react";
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-12">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900">
|
||||||
|
{t("Ontology Builder")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mt-2">
|
||||||
|
{t("Build and manage domain ontologies with AI-powered extraction")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate("/onboard")}
|
||||||
|
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5" />
|
||||||
|
{t("New Project")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<BarChart3 className="w-8 h-8 text-blue-600 mb-4" />
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{t("Quick Start")}</h3>
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
{t("Get started by uploading your domain ontology")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<Settings className="w-8 h-8 text-green-600 mb-4" />
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{t("Phase 5 GraphRAG")}</h3>
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
{t("Intelligent entity resolution and deduplication")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<Settings className="w-8 h-8 text-purple-600 mb-4" />
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{t("Phase 7 LLM")}</h3>
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
{t("AI-powered extraction and validation")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function OnboardingPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl p-8 max-w-2xl w-full">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-4">
|
||||||
|
{t("Upload Ontology")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
{t("Step 1 of 4: Upload your domain ontology (YAML, JSON, or OWL)")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="border-2 border-dashed border-blue-300 rounded-lg p-8 text-center bg-blue-50 mb-6">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
{t("Drag and drop your ontology file here, or click to browse")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition"
|
||||||
|
>
|
||||||
|
{t("Cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate("/sources/demo-project")}
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
{t("Next")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
crawler_platform/app/web/frontend/src/pages/ReviewPage.tsx
Normal file
51
crawler_platform/app/web/frontend/src/pages/ReviewPage.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export default function ReviewPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { projectId } = useParams();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 p-4">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
{t("Review & Validate")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
{t("Step 4 of 4: Review extracted entities and relations")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">
|
||||||
|
{t("Phase 5 Entity Merges")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500">{t("Review merge suggestions")}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h3 className="font-semibold text-lg mb-2">
|
||||||
|
{t("Phase 7 Extractions")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500">{t("Validate extracted claims")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/crawl/${projectId}`)}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition"
|
||||||
|
>
|
||||||
|
{t("Back")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition"
|
||||||
|
>
|
||||||
|
{t("Complete")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
crawler_platform/app/web/frontend/src/stores/index.ts
Normal file
17
crawler_platform/app/web/frontend/src/stores/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import ontologyReducer from "./slices/ontologySlice";
|
||||||
|
import crawlReducer from "./slices/crawlSlice";
|
||||||
|
import uiReducer from "./slices/uiSlice";
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
ontology: ontologyReducer,
|
||||||
|
crawl: crawlReducer,
|
||||||
|
ui: uiReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
export default store;
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
export interface CrawlStep {
|
||||||
|
name: string;
|
||||||
|
status: "pending" | "in_progress" | "completed" | "failed";
|
||||||
|
progress: number; // 0-100
|
||||||
|
message: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrawlState {
|
||||||
|
projectId: string | null;
|
||||||
|
isRunning: boolean;
|
||||||
|
steps: {
|
||||||
|
fetching: CrawlStep;
|
||||||
|
extracting: CrawlStep;
|
||||||
|
merging: CrawlStep;
|
||||||
|
validating: CrawlStep;
|
||||||
|
};
|
||||||
|
stats: {
|
||||||
|
pagesFound: number;
|
||||||
|
pagesCrawled: number;
|
||||||
|
entitiesExtracted: number;
|
||||||
|
entitiesMerged: number;
|
||||||
|
claimsValidated: number;
|
||||||
|
};
|
||||||
|
logs: Array<{
|
||||||
|
timestamp: string;
|
||||||
|
level: "info" | "warning" | "error";
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialCrawlStep: CrawlStep = {
|
||||||
|
name: "",
|
||||||
|
status: "pending",
|
||||||
|
progress: 0,
|
||||||
|
message: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: CrawlState = {
|
||||||
|
projectId: null,
|
||||||
|
isRunning: false,
|
||||||
|
steps: {
|
||||||
|
fetching: { ...initialCrawlStep, name: "Fetching pages" },
|
||||||
|
extracting: { ...initialCrawlStep, name: "Extracting entities" },
|
||||||
|
merging: { ...initialCrawlStep, name: "Merging duplicates" },
|
||||||
|
validating: { ...initialCrawlStep, name: "Validating results" },
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
pagesFound: 0,
|
||||||
|
pagesCrawled: 0,
|
||||||
|
entitiesExtracted: 0,
|
||||||
|
entitiesMerged: 0,
|
||||||
|
claimsValidated: 0,
|
||||||
|
},
|
||||||
|
logs: [],
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const crawlSlice = createSlice({
|
||||||
|
name: "crawl",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
startCrawl: (state, action: PayloadAction<string>) => {
|
||||||
|
state.projectId = action.payload;
|
||||||
|
state.isRunning = true;
|
||||||
|
state.error = null;
|
||||||
|
state.logs = [];
|
||||||
|
Object.values(state.steps).forEach((step) => {
|
||||||
|
step.status = "pending";
|
||||||
|
step.progress = 0;
|
||||||
|
step.error = undefined;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateStep: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
step: keyof typeof state.steps;
|
||||||
|
status: CrawlStep["status"];
|
||||||
|
progress: number;
|
||||||
|
message: string;
|
||||||
|
error?: string;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const { step, status, progress, message, error } = action.payload;
|
||||||
|
state.steps[step] = { ...state.steps[step], status, progress, message, error };
|
||||||
|
},
|
||||||
|
updateStats: (state, action: PayloadAction<Partial<typeof state.stats>>) => {
|
||||||
|
Object.assign(state.stats, action.payload);
|
||||||
|
},
|
||||||
|
addLog: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
level: "info" | "warning" | "error";
|
||||||
|
message: string;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
state.logs.push({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...action.payload,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCrawlError: (state, action: PayloadAction<string>) => {
|
||||||
|
state.error = action.payload;
|
||||||
|
state.isRunning = false;
|
||||||
|
},
|
||||||
|
completeCrawl: (state) => {
|
||||||
|
state.isRunning = false;
|
||||||
|
},
|
||||||
|
resetCrawl: (state) => {
|
||||||
|
Object.assign(state, initialState);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
startCrawl,
|
||||||
|
updateStep,
|
||||||
|
updateStats,
|
||||||
|
addLog,
|
||||||
|
setCrawlError,
|
||||||
|
completeCrawl,
|
||||||
|
resetCrawl,
|
||||||
|
} = crawlSlice.actions;
|
||||||
|
|
||||||
|
export default crawlSlice.reducer;
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
export interface OntologyEntity {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OntologyRelation {
|
||||||
|
id: string;
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
predicate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OntologyState {
|
||||||
|
id: string | null;
|
||||||
|
name: string;
|
||||||
|
domain: string;
|
||||||
|
format: "yaml" | "json" | "owl" | null;
|
||||||
|
entities: OntologyEntity[];
|
||||||
|
relations: OntologyRelation[];
|
||||||
|
isLoaded: boolean;
|
||||||
|
isSaving: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: OntologyState = {
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
domain: "",
|
||||||
|
format: null,
|
||||||
|
entities: [],
|
||||||
|
relations: [],
|
||||||
|
isLoaded: false,
|
||||||
|
isSaving: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ontologySlice = createSlice({
|
||||||
|
name: "ontology",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setOntology: (state, action: PayloadAction<Partial<OntologyState>>) => {
|
||||||
|
Object.assign(state, action.payload);
|
||||||
|
},
|
||||||
|
addEntity: (state, action: PayloadAction<OntologyEntity>) => {
|
||||||
|
state.entities.push(action.payload);
|
||||||
|
},
|
||||||
|
removeEntity: (state, action: PayloadAction<string>) => {
|
||||||
|
state.entities = state.entities.filter((e) => e.id !== action.payload);
|
||||||
|
},
|
||||||
|
addRelation: (state, action: PayloadAction<OntologyRelation>) => {
|
||||||
|
state.relations.push(action.payload);
|
||||||
|
},
|
||||||
|
removeRelation: (state, action: PayloadAction<string>) => {
|
||||||
|
state.relations = state.relations.filter((r) => r.id !== action.payload);
|
||||||
|
},
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isLoaded = action.payload;
|
||||||
|
},
|
||||||
|
setSaving: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isSaving = action.payload;
|
||||||
|
},
|
||||||
|
setError: (state, action: PayloadAction<string | null>) => {
|
||||||
|
state.error = action.payload;
|
||||||
|
},
|
||||||
|
resetOntology: (state) => {
|
||||||
|
Object.assign(state, initialState);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setOntology,
|
||||||
|
addEntity,
|
||||||
|
removeEntity,
|
||||||
|
addRelation,
|
||||||
|
removeRelation,
|
||||||
|
setLoading,
|
||||||
|
setSaving,
|
||||||
|
setError,
|
||||||
|
resetOntology,
|
||||||
|
} = ontologySlice.actions;
|
||||||
|
|
||||||
|
export default ontologySlice.reducer;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
export interface UIState {
|
||||||
|
sidebarOpen: boolean;
|
||||||
|
currentStep: number; // 0: dashboard, 1: onboard, 2: sources, 3: crawl, 4: review
|
||||||
|
selectedProjectId: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
notification: {
|
||||||
|
type: "success" | "error" | "info" | "warning" | null;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: UIState = {
|
||||||
|
sidebarOpen: true,
|
||||||
|
currentStep: 0,
|
||||||
|
selectedProjectId: null,
|
||||||
|
isLoading: false,
|
||||||
|
notification: {
|
||||||
|
type: null,
|
||||||
|
message: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const uiSlice = createSlice({
|
||||||
|
name: "ui",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
toggleSidebar: (state) => {
|
||||||
|
state.sidebarOpen = !state.sidebarOpen;
|
||||||
|
},
|
||||||
|
setCurrentStep: (state, action: PayloadAction<number>) => {
|
||||||
|
state.currentStep = action.payload;
|
||||||
|
},
|
||||||
|
setSelectedProject: (state, action: PayloadAction<string | null>) => {
|
||||||
|
state.selectedProjectId = action.payload;
|
||||||
|
},
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isLoading = action.payload;
|
||||||
|
},
|
||||||
|
showNotification: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
type: "success" | "error" | "info" | "warning";
|
||||||
|
message: string;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
state.notification = action.payload;
|
||||||
|
},
|
||||||
|
clearNotification: (state) => {
|
||||||
|
state.notification = { type: null, message: "" };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
toggleSidebar,
|
||||||
|
setCurrentStep,
|
||||||
|
setSelectedProject,
|
||||||
|
setLoading,
|
||||||
|
showNotification,
|
||||||
|
clearNotification,
|
||||||
|
} = uiSlice.actions;
|
||||||
|
|
||||||
|
export default uiSlice.reducer;
|
||||||
60
crawler_platform/app/web/frontend/src/styles/globals.css
Normal file
60
crawler_platform/app/web/frontend/src/styles/globals.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--muted: 221.2 63.6% 97%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 221.2 83.2% 53.3%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 221.2 83.2% 53.3%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--primary: 222.2 47.6% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96%;
|
||||||
|
--secondary-foreground: 222.2 47.6% 11.2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 91.2% 59.8%;
|
||||||
|
--accent-foreground: 222.2 47.6% 11.2%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.6% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
crawler_platform/app/web/frontend/tailwind.config.js
Normal file
44
crawler_platform/app/web/frontend/tailwind.config.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
40
crawler_platform/app/web/frontend/tsconfig.json
Normal file
40
crawler_platform/app/web/frontend/tsconfig.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Path mapping */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@components/*": ["src/components/*"],
|
||||||
|
"@pages/*": ["src/pages/*"],
|
||||||
|
"@hooks/*": ["src/hooks/*"],
|
||||||
|
"@stores/*": ["src/stores/*"],
|
||||||
|
"@types/*": ["src/types/*"],
|
||||||
|
"@utils/*": ["src/utils/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
crawler_platform/app/web/frontend/tsconfig.node.json
Normal file
10
crawler_platform/app/web/frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
const BACKEND = "http://127.0.0.1:8000";
|
const BACKEND = "http://127.0.0.1:8000";
|
||||||
const PROXY_PREFIXES = [
|
const PROXY_PREFIXES = [
|
||||||
"/projects",
|
"/projects",
|
||||||
"/ontology",
|
"/ontology",
|
||||||
|
"/ontologies",
|
||||||
"/extractors",
|
"/extractors",
|
||||||
"/crawl",
|
"/crawl",
|
||||||
"/crawl-site",
|
"/crawl-site",
|
||||||
@@ -13,9 +15,12 @@ const PROXY_PREFIXES = [
|
|||||||
"/claims",
|
"/claims",
|
||||||
"/entities",
|
"/entities",
|
||||||
"/health",
|
"/health",
|
||||||
|
"/api/v1/graph",
|
||||||
|
"/api/v1/llm",
|
||||||
];
|
];
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
root: ".",
|
root: ".",
|
||||||
base: "/static/",
|
base: "/static/",
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user