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:
@@ -3,10 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,17 +1,44 @@
|
||||
{
|
||||
"name": "crawler-platform-ui",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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 react from "@vitejs/plugin-react";
|
||||
|
||||
const BACKEND = "http://127.0.0.1:8000";
|
||||
const PROXY_PREFIXES = [
|
||||
"/projects",
|
||||
"/ontology",
|
||||
"/ontologies",
|
||||
"/extractors",
|
||||
"/crawl",
|
||||
"/crawl-site",
|
||||
@@ -13,9 +15,12 @@ const PROXY_PREFIXES = [
|
||||
"/claims",
|
||||
"/entities",
|
||||
"/health",
|
||||
"/api/v1/graph",
|
||||
"/api/v1/llm",
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
root: ".",
|
||||
base: "/static/",
|
||||
build: {
|
||||
|
||||
Reference in New Issue
Block a user