diff --git a/crawler_platform/app/web/frontend/public/locales/en/common.json b/crawler_platform/app/web/frontend/public/locales/en/common.json
new file mode 100644
index 0000000..91a7d4e
--- /dev/null
+++ b/crawler_platform/app/web/frontend/public/locales/en/common.json
@@ -0,0 +1,33 @@
+{
+ "app": {
+ "title": "Ontology Builder",
+ "subtitle": "AI-powered ontology construction"
+ },
+ "nav": {
+ "dashboard": "Dashboard",
+ "onboard": "Upload Ontology",
+ "sources": "Sources",
+ "crawl": "Crawl",
+ "review": "Review",
+ "toggleSidebar": "Toggle sidebar"
+ },
+ "dashboard": {
+ "title": "Ontology Builder",
+ "subtitle": "Build and manage domain ontologies with AI-powered extraction",
+ "newProject": "New Project",
+ "projects": "Projects",
+ "projectCount": "{{count}} total",
+ "loadFailed": "Failed to load projects",
+ "empty": {
+ "title": "No projects yet",
+ "hint": "Create your first project to start building an ontology"
+ }
+ },
+ "common": {
+ "retry": "Retry",
+ "cancel": "Cancel",
+ "next": "Next",
+ "back": "Back",
+ "complete": "Complete"
+ }
+}
diff --git a/crawler_platform/app/web/frontend/public/locales/ko/common.json b/crawler_platform/app/web/frontend/public/locales/ko/common.json
new file mode 100644
index 0000000..274c0a1
--- /dev/null
+++ b/crawler_platform/app/web/frontend/public/locales/ko/common.json
@@ -0,0 +1,33 @@
+{
+ "app": {
+ "title": "온톨로지 빌더",
+ "subtitle": "AI 기반 온톨로지 구축 플랫폼"
+ },
+ "nav": {
+ "dashboard": "대시보드",
+ "onboard": "온톨로지 업로드",
+ "sources": "참고 소스",
+ "crawl": "크롤 진행",
+ "review": "결과 검토",
+ "toggleSidebar": "사이드바 토글"
+ },
+ "dashboard": {
+ "title": "온톨로지 빌더",
+ "subtitle": "도메인 온톨로지를 AI 추출로 구축하고 관리합니다",
+ "newProject": "새 프로젝트",
+ "projects": "프로젝트 목록",
+ "projectCount": "{{count}}개",
+ "loadFailed": "프로젝트를 불러오지 못했습니다",
+ "empty": {
+ "title": "아직 프로젝트가 없습니다",
+ "hint": "첫 프로젝트를 만들어 온톨로지 구축을 시작하세요"
+ }
+ },
+ "common": {
+ "retry": "다시 시도",
+ "cancel": "취소",
+ "next": "다음",
+ "back": "이전",
+ "complete": "완료"
+ }
+}
diff --git a/crawler_platform/app/web/frontend/src/App.tsx b/crawler_platform/app/web/frontend/src/App.tsx
index baad009..c3878ee 100644
--- a/crawler_platform/app/web/frontend/src/App.tsx
+++ b/crawler_platform/app/web/frontend/src/App.tsx
@@ -1,24 +1,22 @@
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";
+import AppShell from "@/components/layout/AppShell";
+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 (
-
-
+
+ }>
} />
} />
} />
} />
} />
-
-
+
+
);
}
diff --git a/crawler_platform/app/web/frontend/src/components/layout/AppShell.tsx b/crawler_platform/app/web/frontend/src/components/layout/AppShell.tsx
new file mode 100644
index 0000000..c00768b
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/components/layout/AppShell.tsx
@@ -0,0 +1,86 @@
+import { NavLink, Outlet } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { useSelector, useDispatch } from "react-redux";
+import { LayoutDashboard, UploadCloud, Settings2, Activity, ListChecks, Menu } from "lucide-react";
+import { RootState } from "@/stores";
+import { toggleSidebar } from "@/stores/slices/uiSlice";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+
+interface NavItem {
+ to: string;
+ labelKey: string;
+ icon: React.ComponentType<{ className?: string }>;
+}
+
+const navItems: NavItem[] = [
+ { to: "/", labelKey: "nav.dashboard", icon: LayoutDashboard },
+ { to: "/onboard", labelKey: "nav.onboard", icon: UploadCloud },
+ { to: "/sources/demo-project", labelKey: "nav.sources", icon: Settings2 },
+ { to: "/crawl/demo-project", labelKey: "nav.crawl", icon: Activity },
+ { to: "/review/demo-project", labelKey: "nav.review", icon: ListChecks },
+];
+
+export default function AppShell() {
+ const { t } = useTranslation();
+ const sidebarOpen = useSelector((s: RootState) => s.ui.sidebarOpen);
+ const dispatch = useDispatch();
+
+ return (
+
+
+
+
+
+
+ {t("app.subtitle", "AI-powered ontology construction")}
+
+
+
+
+
+
+
+ );
+}
diff --git a/crawler_platform/app/web/frontend/src/components/ui/button.tsx b/crawler_platform/app/web/frontend/src/components/ui/button.tsx
new file mode 100644
index 0000000..169c28a
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/components/ui/button.tsx
@@ -0,0 +1,51 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {}
+
+export const Button = React.forwardRef(
+ ({ className, variant, size, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { buttonVariants };
diff --git a/crawler_platform/app/web/frontend/src/components/ui/card.tsx b/crawler_platform/app/web/frontend/src/components/ui/card.tsx
new file mode 100644
index 0000000..0e01393
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/components/ui/card.tsx
@@ -0,0 +1,76 @@
+import * as React from "react";
+import { cn } from "@/lib/utils";
+
+export const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+export const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
+
+export const CardTitle = React.forwardRef<
+ HTMLHeadingElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
+
+export const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+export const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+export const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
diff --git a/crawler_platform/app/web/frontend/src/components/ui/skeleton.tsx b/crawler_platform/app/web/frontend/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..09a0298
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils";
+
+export function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
diff --git a/crawler_platform/app/web/frontend/src/hooks/queryKeys.ts b/crawler_platform/app/web/frontend/src/hooks/queryKeys.ts
new file mode 100644
index 0000000..886a97f
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/hooks/queryKeys.ts
@@ -0,0 +1,8 @@
+export const queryKeys = {
+ projects: {
+ all: ["projects"] as const,
+ list: () => [...queryKeys.projects.all, "list"] as const,
+ detail: (name: string) =>
+ [...queryKeys.projects.all, "detail", name] as const,
+ },
+};
diff --git a/crawler_platform/app/web/frontend/src/hooks/useProjects.ts b/crawler_platform/app/web/frontend/src/hooks/useProjects.ts
new file mode 100644
index 0000000..f2a294c
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/hooks/useProjects.ts
@@ -0,0 +1,33 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import {
+ CreateProjectRequest,
+ projectsApi,
+ ProjectSummary,
+ ProjectDetail,
+} from "@/lib/api/projects";
+import { queryKeys } from "./queryKeys";
+
+export function useProjects() {
+ return useQuery({
+ queryKey: queryKeys.projects.list(),
+ queryFn: () => projectsApi.list(),
+ });
+}
+
+export function useProject(name: string | undefined) {
+ return useQuery({
+ queryKey: queryKeys.projects.detail(name ?? ""),
+ queryFn: () => projectsApi.detail(name!),
+ enabled: Boolean(name),
+ });
+}
+
+export function useCreateProject() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (body: CreateProjectRequest) => projectsApi.create(body),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.projects.all });
+ },
+ });
+}
diff --git a/crawler_platform/app/web/frontend/src/api.js b/crawler_platform/app/web/frontend/src/legacy/api.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/api.js
rename to crawler_platform/app/web/frontend/src/legacy/api.js
diff --git a/crawler_platform/app/web/frontend/src/graph.js b/crawler_platform/app/web/frontend/src/legacy/graph.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/graph.js
rename to crawler_platform/app/web/frontend/src/legacy/graph.js
diff --git a/crawler_platform/app/web/frontend/src/i18n.js b/crawler_platform/app/web/frontend/src/legacy/i18n.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/i18n.js
rename to crawler_platform/app/web/frontend/src/legacy/i18n.js
diff --git a/crawler_platform/app/web/frontend/src/inspector.js b/crawler_platform/app/web/frontend/src/legacy/inspector.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/inspector.js
rename to crawler_platform/app/web/frontend/src/legacy/inspector.js
diff --git a/crawler_platform/app/web/frontend/src/main.js b/crawler_platform/app/web/frontend/src/legacy/main.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/main.js
rename to crawler_platform/app/web/frontend/src/legacy/main.js
diff --git a/crawler_platform/app/web/frontend/src/pipeline.js b/crawler_platform/app/web/frontend/src/legacy/pipeline.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/pipeline.js
rename to crawler_platform/app/web/frontend/src/legacy/pipeline.js
diff --git a/crawler_platform/app/web/frontend/src/search.js b/crawler_platform/app/web/frontend/src/legacy/search.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/search.js
rename to crawler_platform/app/web/frontend/src/legacy/search.js
diff --git a/crawler_platform/app/web/frontend/src/shell.js b/crawler_platform/app/web/frontend/src/legacy/shell.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/shell.js
rename to crawler_platform/app/web/frontend/src/legacy/shell.js
diff --git a/crawler_platform/app/web/frontend/src/sidebar.js b/crawler_platform/app/web/frontend/src/legacy/sidebar.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/sidebar.js
rename to crawler_platform/app/web/frontend/src/legacy/sidebar.js
diff --git a/crawler_platform/app/web/frontend/src/state.js b/crawler_platform/app/web/frontend/src/legacy/state.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/state.js
rename to crawler_platform/app/web/frontend/src/legacy/state.js
diff --git a/crawler_platform/app/web/frontend/src/styles/components.css b/crawler_platform/app/web/frontend/src/legacy/styles/components.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/components.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/components.css
diff --git a/crawler_platform/app/web/frontend/src/styles/graph.css b/crawler_platform/app/web/frontend/src/legacy/styles/graph.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/graph.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/graph.css
diff --git a/crawler_platform/app/web/frontend/src/styles/index.css b/crawler_platform/app/web/frontend/src/legacy/styles/index.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/index.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/index.css
diff --git a/crawler_platform/app/web/frontend/src/styles/inspector.css b/crawler_platform/app/web/frontend/src/legacy/styles/inspector.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/inspector.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/inspector.css
diff --git a/crawler_platform/app/web/frontend/src/styles/pipeline.css b/crawler_platform/app/web/frontend/src/legacy/styles/pipeline.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/pipeline.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/pipeline.css
diff --git a/crawler_platform/app/web/frontend/src/styles/search.css b/crawler_platform/app/web/frontend/src/legacy/styles/search.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/search.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/search.css
diff --git a/crawler_platform/app/web/frontend/src/styles/shell.css b/crawler_platform/app/web/frontend/src/legacy/styles/shell.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/shell.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/shell.css
diff --git a/crawler_platform/app/web/frontend/src/styles/sidebar.css b/crawler_platform/app/web/frontend/src/legacy/styles/sidebar.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/sidebar.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/sidebar.css
diff --git a/crawler_platform/app/web/frontend/src/styles/tokens.css b/crawler_platform/app/web/frontend/src/legacy/styles/tokens.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/tokens.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/tokens.css
diff --git a/crawler_platform/app/web/frontend/src/styles/workbench.css b/crawler_platform/app/web/frontend/src/legacy/styles/workbench.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/workbench.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/workbench.css
diff --git a/crawler_platform/app/web/frontend/src/styles/workspace.css b/crawler_platform/app/web/frontend/src/legacy/styles/workspace.css
similarity index 100%
rename from crawler_platform/app/web/frontend/src/styles/workspace.css
rename to crawler_platform/app/web/frontend/src/legacy/styles/workspace.css
diff --git a/crawler_platform/app/web/frontend/src/utils.js b/crawler_platform/app/web/frontend/src/legacy/utils.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/utils.js
rename to crawler_platform/app/web/frontend/src/legacy/utils.js
diff --git a/crawler_platform/app/web/frontend/src/workbench.js b/crawler_platform/app/web/frontend/src/legacy/workbench.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/workbench.js
rename to crawler_platform/app/web/frontend/src/legacy/workbench.js
diff --git a/crawler_platform/app/web/frontend/src/workspace.js b/crawler_platform/app/web/frontend/src/legacy/workspace.js
similarity index 100%
rename from crawler_platform/app/web/frontend/src/workspace.js
rename to crawler_platform/app/web/frontend/src/legacy/workspace.js
diff --git a/crawler_platform/app/web/frontend/src/lib/api/client.ts b/crawler_platform/app/web/frontend/src/lib/api/client.ts
new file mode 100644
index 0000000..ef5ed57
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/lib/api/client.ts
@@ -0,0 +1,89 @@
+import { z } from "zod";
+
+export class ApiError extends Error {
+ constructor(
+ public status: number,
+ public statusText: string,
+ public body: unknown,
+ public url: string,
+ ) {
+ super(`${status} ${statusText} — ${url}`);
+ this.name = "ApiError";
+ }
+}
+
+interface RequestOptions extends Omit {
+ body?: unknown;
+ query?: Record;
+}
+
+function buildUrl(path: string, query?: RequestOptions["query"]): string {
+ if (!query) return path;
+ const params = new URLSearchParams();
+ for (const [k, v] of Object.entries(query)) {
+ if (v === undefined || v === null) continue;
+ params.append(k, String(v));
+ }
+ const qs = params.toString();
+ return qs ? `${path}?${qs}` : path;
+}
+
+async function request(
+ path: string,
+ schema: z.ZodType,
+ options: RequestOptions = {},
+): Promise {
+ const { body, query, headers, ...rest } = options;
+ const url = buildUrl(path, query);
+
+ const init: RequestInit = {
+ ...rest,
+ headers: {
+ Accept: "application/json",
+ ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
+ ...headers,
+ },
+ body: body !== undefined ? JSON.stringify(body) : undefined,
+ };
+
+ const response = await fetch(url, init);
+ const contentType = response.headers.get("content-type") ?? "";
+ const raw =
+ contentType.includes("application/json") && response.status !== 204
+ ? await response.json()
+ : await response.text();
+
+ if (!response.ok) {
+ throw new ApiError(response.status, response.statusText, raw, url);
+ }
+
+ const parsed = schema.safeParse(raw);
+ if (!parsed.success) {
+ throw new ApiError(
+ response.status,
+ "Response schema mismatch",
+ { issues: parsed.error.issues, raw },
+ url,
+ );
+ }
+ return parsed.data;
+}
+
+export const apiClient = {
+ get: (path: string, schema: z.ZodType, opts?: RequestOptions) =>
+ request(path, schema, { ...opts, method: "GET" }),
+ post: (
+ path: string,
+ schema: z.ZodType,
+ body?: unknown,
+ opts?: RequestOptions,
+ ) => request(path, schema, { ...opts, method: "POST", body }),
+ put: (
+ path: string,
+ schema: z.ZodType,
+ body?: unknown,
+ opts?: RequestOptions,
+ ) => request(path, schema, { ...opts, method: "PUT", body }),
+ delete: (path: string, schema: z.ZodType, opts?: RequestOptions) =>
+ request(path, schema, { ...opts, method: "DELETE" }),
+};
diff --git a/crawler_platform/app/web/frontend/src/lib/api/index.ts b/crawler_platform/app/web/frontend/src/lib/api/index.ts
new file mode 100644
index 0000000..52f48ea
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/lib/api/index.ts
@@ -0,0 +1,2 @@
+export { apiClient, ApiError } from "./client";
+export * from "./projects";
diff --git a/crawler_platform/app/web/frontend/src/lib/api/projects.ts b/crawler_platform/app/web/frontend/src/lib/api/projects.ts
new file mode 100644
index 0000000..3a56bdb
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/lib/api/projects.ts
@@ -0,0 +1,54 @@
+import { z } from "zod";
+import { apiClient } from "./client";
+
+export const projectSummarySchema = z.object({
+ id: z.union([z.string(), z.number()]).transform(String),
+ name: z.string(),
+ domain: z.string(),
+ created_at: z.string(),
+ updated_at: z.string(),
+});
+
+export const projectListSchema = z.array(projectSummarySchema);
+
+export const projectSourceSchema = z.object({
+ id: z.union([z.string(), z.number()]).transform(String),
+ name: z.string(),
+ type: z.string(),
+ trust_level: z.union([z.string(), z.number()]).nullable().optional(),
+ respect_robots_txt: z.boolean().nullable().optional(),
+ rate_limit_per_minute: z.number().nullable().optional(),
+});
+
+export const projectDetailSchema = z.object({
+ id: z.union([z.string(), z.number()]).transform(String),
+ name: z.string(),
+ domain: z.string(),
+ config: z.unknown().optional(),
+ sources: z.array(projectSourceSchema).default([]),
+});
+
+export const createProjectResponseSchema = z.object({
+ id: z.union([z.string(), z.number()]).transform(String),
+ name: z.string(),
+ domain: z.string(),
+});
+
+export type ProjectSummary = z.infer;
+export type ProjectDetail = z.infer;
+export type ProjectSource = z.infer;
+
+export interface CreateProjectRequest {
+ config_path: string;
+}
+
+export const projectsApi = {
+ list: () => apiClient.get("/projects", projectListSchema),
+ detail: (projectName: string) =>
+ apiClient.get(
+ `/projects/${encodeURIComponent(projectName)}`,
+ projectDetailSchema,
+ ),
+ create: (body: CreateProjectRequest) =>
+ apiClient.post("/projects", createProjectResponseSchema, body),
+};
diff --git a/crawler_platform/app/web/frontend/src/lib/utils.ts b/crawler_platform/app/web/frontend/src/lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/crawler_platform/app/web/frontend/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/crawler_platform/app/web/frontend/src/pages/DashboardPage.tsx b/crawler_platform/app/web/frontend/src/pages/DashboardPage.tsx
index cc0139d..384a0f0 100644
--- a/crawler_platform/app/web/frontend/src/pages/DashboardPage.tsx
+++ b/crawler_platform/app/web/frontend/src/pages/DashboardPage.tsx
@@ -1,58 +1,138 @@
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
-import { Plus, BarChart3, Settings } from "lucide-react";
+import { Plus, FolderOpen, AlertCircle, Clock } from "lucide-react";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useProjects } from "@/hooks/useProjects";
+
+function formatRelative(iso: string): string {
+ const d = new Date(iso);
+ if (Number.isNaN(d.getTime())) return iso;
+ return d.toLocaleString();
+}
export default function DashboardPage() {
const navigate = useNavigate();
const { t } = useTranslation();
+ const { data: projects, isLoading, isError, error, refetch } = useProjects();
return (
-
-
-
-
-
- {t("Ontology Builder")}
-
-
- {t("Build and manage domain ontologies with AI-powered extraction")}
-
-
-
-
-
-
-
-
-
{t("Quick Start")}
-
- {t("Get started by uploading your domain ontology")}
-
-
-
-
-
-
{t("Phase 5 GraphRAG")}
-
- {t("Intelligent entity resolution and deduplication")}
-
-
-
-
-
-
{t("Phase 7 LLM")}
-
- {t("AI-powered extraction and validation")}
-
-
+
+
+
+
+ {t("dashboard.title", "Ontology Builder")}
+
+
+ {t(
+ "dashboard.subtitle",
+ "Build and manage domain ontologies with AI-powered extraction",
+ )}
+
+
+
+
+
+
+ {t("dashboard.projects", "Projects")}
+
+ {projects && (
+
+ {t("dashboard.projectCount", "{{count}} total", {
+ count: projects.length,
+ })}
+
+ )}
+
+
+ {isLoading && (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+ {isError && (
+
+
+
+
+ {t("dashboard.loadFailed", "Failed to load projects")}
+
+ {(error as Error).message}
+
+
+
+
+
+ )}
+
+ {projects && projects.length === 0 && (
+
+
+
+
+
+ {t("dashboard.empty.title", "No projects yet")}
+
+
+ {t(
+ "dashboard.empty.hint",
+ "Create your first project to start building an ontology",
+ )}
+
+
+
+
+
+ )}
+
+ {projects && projects.length > 0 && (
+
+ {projects.map((p) => (
+ navigate(`/sources/${p.name}`)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ navigate(`/sources/${p.name}`);
+ }
+ }}
+ className="cursor-pointer transition-colors hover:bg-accent/40"
+ >
+
+ {p.name}
+ {p.domain}
+
+
+
+ {formatRelative(p.updated_at)}
+
+
+ ))}
+
+ )}
+
);
}
diff --git a/crawler_platform/app/web/frontend/src/stores/slices/crawlSlice.ts b/crawler_platform/app/web/frontend/src/stores/slices/crawlSlice.ts
index 02e894c..19900a3 100644
--- a/crawler_platform/app/web/frontend/src/stores/slices/crawlSlice.ts
+++ b/crawler_platform/app/web/frontend/src/stores/slices/crawlSlice.ts
@@ -1,128 +1,40 @@
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 type CrawlStepKey = "fetching" | "extracting" | "merging" | "validating";
+
+export interface CrawlUiState {
+ activeJobId: string | null;
+ expandedStep: CrawlStepKey | null;
+ showLogs: boolean;
+ autoScrollLogs: boolean;
}
-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 initialState: CrawlUiState = {
+ activeJobId: null,
+ expandedStep: null,
+ showLogs: true,
+ autoScrollLogs: true,
};
const crawlSlice = createSlice({
name: "crawl",
initialState,
reducers: {
- startCrawl: (state, action: PayloadAction
) => {
- 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;
- });
+ setActiveJob: (state, action: PayloadAction) => {
+ state.activeJobId = action.payload;
},
- 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 };
+ expandStep: (state, action: PayloadAction) => {
+ state.expandedStep = action.payload;
},
- updateStats: (state, action: PayloadAction>) => {
- Object.assign(state.stats, action.payload);
+ toggleLogs: (state) => {
+ state.showLogs = !state.showLogs;
},
- addLog: (
- state,
- action: PayloadAction<{
- level: "info" | "warning" | "error";
- message: string;
- }>
- ) => {
- state.logs.push({
- timestamp: new Date().toISOString(),
- ...action.payload,
- });
- },
- setCrawlError: (state, action: PayloadAction) => {
- state.error = action.payload;
- state.isRunning = false;
- },
- completeCrawl: (state) => {
- state.isRunning = false;
- },
- resetCrawl: (state) => {
- Object.assign(state, initialState);
+ setAutoScroll: (state, action: PayloadAction) => {
+ state.autoScrollLogs = action.payload;
},
},
});
-export const {
- startCrawl,
- updateStep,
- updateStats,
- addLog,
- setCrawlError,
- completeCrawl,
- resetCrawl,
-} = crawlSlice.actions;
-
+export const { setActiveJob, expandStep, toggleLogs, setAutoScroll } =
+ crawlSlice.actions;
export default crawlSlice.reducer;
diff --git a/crawler_platform/app/web/frontend/src/stores/slices/ontologySlice.ts b/crawler_platform/app/web/frontend/src/stores/slices/ontologySlice.ts
index d19339c..8ef23d2 100644
--- a/crawler_platform/app/web/frontend/src/stores/slices/ontologySlice.ts
+++ b/crawler_platform/app/web/frontend/src/stores/slices/ontologySlice.ts
@@ -1,87 +1,50 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
-export interface OntologyEntity {
- id: string;
- label: string;
- type: string;
- description?: string;
-}
+export type OntologyFormat = "yaml" | "json" | "owl";
-export interface OntologyRelation {
- id: string;
- source: string;
- target: string;
- predicate: string;
-}
-
-export interface OntologyState {
- id: string | null;
+export interface OntologyDraft {
name: string;
domain: string;
- format: "yaml" | "json" | "owl" | null;
- entities: OntologyEntity[];
- relations: OntologyRelation[];
- isLoaded: boolean;
- isSaving: boolean;
- error: string | null;
+ format: OntologyFormat | null;
+ fileName: string | null;
+ rawText: string;
}
-const initialState: OntologyState = {
- id: null,
+export interface OntologyDraftState {
+ draft: OntologyDraft;
+ isDirty: boolean;
+}
+
+const emptyDraft: OntologyDraft = {
name: "",
domain: "",
format: null,
- entities: [],
- relations: [],
- isLoaded: false,
- isSaving: false,
- error: null,
+ fileName: null,
+ rawText: "",
+};
+
+const initialState: OntologyDraftState = {
+ draft: emptyDraft,
+ isDirty: false,
};
const ontologySlice = createSlice({
name: "ontology",
initialState,
reducers: {
- setOntology: (state, action: PayloadAction>) => {
- Object.assign(state, action.payload);
+ updateDraft: (state, action: PayloadAction>) => {
+ state.draft = { ...state.draft, ...action.payload };
+ state.isDirty = true;
},
- addEntity: (state, action: PayloadAction) => {
- state.entities.push(action.payload);
+ resetDraft: (state) => {
+ state.draft = emptyDraft;
+ state.isDirty = false;
},
- removeEntity: (state, action: PayloadAction) => {
- state.entities = state.entities.filter((e) => e.id !== action.payload);
- },
- addRelation: (state, action: PayloadAction) => {
- state.relations.push(action.payload);
- },
- removeRelation: (state, action: PayloadAction) => {
- state.relations = state.relations.filter((r) => r.id !== action.payload);
- },
- setLoading: (state, action: PayloadAction) => {
- state.isLoaded = action.payload;
- },
- setSaving: (state, action: PayloadAction) => {
- state.isSaving = action.payload;
- },
- setError: (state, action: PayloadAction) => {
- state.error = action.payload;
- },
- resetOntology: (state) => {
- Object.assign(state, initialState);
+ markClean: (state) => {
+ state.isDirty = false;
},
},
});
-export const {
- setOntology,
- addEntity,
- removeEntity,
- addRelation,
- removeRelation,
- setLoading,
- setSaving,
- setError,
- resetOntology,
-} = ontologySlice.actions;
-
+export const { updateDraft, resetDraft, markClean } = ontologySlice.actions;
export default ontologySlice.reducer;
diff --git a/crawler_platform/app/web/frontend/tailwind.config.js b/crawler_platform/app/web/frontend/tailwind.config.js
index 83b8a92..36ffc4d 100644
--- a/crawler_platform/app/web/frontend/tailwind.config.js
+++ b/crawler_platform/app/web/frontend/tailwind.config.js
@@ -2,7 +2,7 @@
export default {
content: [
"./index.html",
- "./src/**/*.{js,ts,jsx,tsx}",
+ "./src/**/*.{ts,tsx}",
],
theme: {
extend: {