[crawler_platform 삭제]

This commit is contained in:
LASTA_DEV01\lasta
2026-05-20 13:21:08 +09:00
parent be717a8f9a
commit fc69dfd063
173 changed files with 307 additions and 1982 deletions

View File

@@ -0,0 +1,216 @@
import { useEffect } from "react";
import {
matchPath,
NavLink,
Outlet,
useLocation,
useNavigate,
} from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import {
LayoutDashboard,
UploadCloud,
Settings2,
Activity,
Brain,
Code2,
GitBranch,
Network,
ListChecks,
Menu,
SearchCheck,
ShieldCheck,
ClipboardCheck,
Layers3,
} from "lucide-react";
import { RootState } from "@/stores";
import { toggleSidebar } from "@/stores/slices/uiSlice";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { useProjects } from "@/hooks/useProjects";
interface NavItem {
to?: string;
projectPath?: string;
labelKey: string;
label: string;
icon: React.ComponentType<{ className?: string }>;
}
const navItems: NavItem[] = [
{ to: "/", labelKey: "nav.dashboard", label: "Dashboard", icon: LayoutDashboard },
{ to: "/onboard", labelKey: "nav.onboard", label: "New Project", icon: UploadCloud },
{ projectPath: "sources", labelKey: "nav.sources", label: "Source Explorer", icon: Settings2 },
{ projectPath: "crawl", labelKey: "nav.crawl", label: "Seed Crawl", icon: Activity },
{ projectPath: "pipeline", labelKey: "nav.pipeline", label: "Build Pipeline", icon: Layers3 },
{ projectPath: "analysis", labelKey: "nav.analysis", label: "Page Analysis", icon: SearchCheck },
{ projectPath: "schema", labelKey: "nav.schema", label: "Schema Designer", icon: ShieldCheck },
{ projectPath: "editor", labelKey: "nav.editor", label: "Entity Manager", icon: Network },
{ projectPath: "review", labelKey: "nav.review", label: "Claim Review", icon: ListChecks },
{ projectPath: "quality", labelKey: "nav.quality", label: "Quality Inspector", icon: ClipboardCheck },
{ projectPath: "graph", labelKey: "nav.graph", label: "Graph View", icon: GitBranch },
{ projectPath: "export", labelKey: "nav.export", label: "Export / API", icon: Code2 },
{ projectPath: "research", labelKey: "nav.research", label: "Graph Research", icon: Brain },
];
const projectRoutePatterns = [
"/sources/:projectId",
"/crawl/:projectId",
"/pipeline/:projectId",
"/analysis/:projectId",
"/schema/:projectId",
"/research/:projectId",
"/editor/:projectId",
"/review/:projectId",
"/quality/:projectId",
"/graph/:projectId",
"/export/:projectId",
];
function projectIdFromPathname(pathname: string): string | undefined {
for (const pattern of projectRoutePatterns) {
const match = matchPath({ path: pattern, end: false }, pathname);
if (match?.params.projectId) return match.params.projectId;
}
return undefined;
}
function workspaceSectionFromPathname(pathname: string): string | undefined {
for (const pattern of projectRoutePatterns) {
const match = matchPath({ path: pattern, end: false }, pathname);
if (match?.params.projectId) return pattern.split("/")[1];
}
return undefined;
}
export default function AppShell() {
const { t } = useTranslation();
const sidebarOpen = useSelector((s: RootState) => s.ui.sidebarOpen);
const dispatch = useDispatch();
const location = useLocation();
const navigate = useNavigate();
const routeProjectId = projectIdFromPathname(location.pathname);
const workspaceSection = workspaceSectionFromPathname(location.pathname);
const { data: projects } = useProjects();
const fallbackProjectId = projects?.[0]?.name;
const routeProjectExists =
!routeProjectId ||
!projects ||
projects.some((project) => project.name === routeProjectId);
const currentProjectId = routeProjectExists
? routeProjectId ?? fallbackProjectId
: fallbackProjectId;
useEffect(() => {
if (routeProjectId && projects && !routeProjectExists) {
const nextPath =
workspaceSection && fallbackProjectId
? `/${workspaceSection}/${encodeURIComponent(fallbackProjectId)}`
: "/";
navigate(nextPath, { replace: true });
}
}, [
fallbackProjectId,
navigate,
projects,
routeProjectExists,
routeProjectId,
workspaceSection,
]);
return (
<div className="flex min-h-screen bg-background text-foreground">
<aside
className={cn(
"border-r bg-card transition-all duration-200 ease-out",
sidebarOpen ? "w-60" : "w-16",
)}
>
<div className="flex h-14 items-center justify-between px-4 border-b">
{sidebarOpen && (
<span className="text-sm font-semibold">
{t("app.title", "Ontology Builder")}
</span>
)}
<Button
variant="ghost"
size="icon"
onClick={() => dispatch(toggleSidebar())}
aria-label={t("nav.toggleSidebar", "Toggle sidebar")}
>
<Menu className="h-4 w-4" />
</Button>
</div>
{sidebarOpen && (
<div className="border-b px-4 py-3">
<p className="text-xs text-muted-foreground">
{t("nav.currentProject", "Current project")}
</p>
<p className="mt-1 truncate text-sm font-medium">
{currentProjectId ??
t("nav.noProjectSelected", "Select a project")}
</p>
</div>
)}
<nav className="flex flex-col gap-1 p-2">
{navItems.map(({ to, projectPath, labelKey, label, icon: Icon }) => {
const href =
to ??
(currentProjectId
? `/${projectPath}/${encodeURIComponent(currentProjectId)}`
: undefined);
if (!href) {
return (
<div
key={labelKey}
className="flex cursor-not-allowed items-center gap-3 rounded-md px-3 py-2 text-sm text-muted-foreground/50"
title={t(
"nav.selectProjectFirst",
"Select a project from the dashboard first",
)}
>
<Icon className="h-4 w-4 flex-shrink-0" />
{sidebarOpen && <span>{t(labelKey, label)}</span>}
</div>
);
}
return (
<NavLink
key={labelKey}
to={href}
end={href === "/"}
className={({ isActive }) =>
cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
isActive
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)
}
>
<Icon className="h-4 w-4 flex-shrink-0" />
{sidebarOpen && <span>{t(labelKey, label)}</span>}
</NavLink>
);
})}
</nav>
</aside>
<div className="flex flex-1 flex-col">
<header className="flex h-14 items-center justify-between border-b bg-card px-6">
<h1 className="text-sm font-medium text-muted-foreground">
{t("app.subtitle", "AI-powered ontology construction")}
</h1>
</header>
<main className="flex-1 overflow-auto">
<Outlet />
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary:
"border-transparent bg-secondary text-secondary-foreground",
destructive:
"border-transparent bg-destructive text-destructive-foreground",
outline: "text-foreground",
success:
"border-transparent bg-green-100 text-green-800",
warning:
"border-transparent bg-yellow-100 text-yellow-800",
},
},
defaultVariants: { variant: "default" },
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLSpanElement>,
VariantProps<typeof badgeVariants> {}
export function Badge({ className, variant, ...props }: BadgeProps) {
return (
<span className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { badgeVariants };

View File

@@ -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<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
);
},
);
Button.displayName = "Button";
export { buttonVariants };

View File

@@ -0,0 +1,76 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
export const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
));
CardHeader.displayName = "CardHeader";
export const CardTitle = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
CardTitle.displayName = "CardTitle";
export const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
CardDescription.displayName = "CardDescription";
export const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
));
CardContent.displayName = "CardContent";
export const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
));
CardFooter.displayName = "CardFooter";

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type = "text", ...props }, ref) => {
return (
<input
type={type}
ref={ref}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
);
},
);
Input.displayName = "Input";

View File

@@ -0,0 +1,17 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export const Label = React.forwardRef<
HTMLLabelElement,
React.LabelHTMLAttributes<HTMLLabelElement>
>(({ className, ...props }, ref) => (
<label
ref={ref}
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className,
)}
{...props}
/>
));
Label.displayName = "Label";

View File

@@ -0,0 +1,37 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
value?: number;
max?: number;
indeterminate?: boolean;
}
export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
({ className, value = 0, max = 100, indeterminate, ...props }, ref) => {
const percent = Math.min(100, Math.max(0, (value / max) * 100));
return (
<div
ref={ref}
role="progressbar"
aria-valuemin={0}
aria-valuemax={max}
aria-valuenow={indeterminate ? undefined : value}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
{...props}
>
<div
className={cn(
"h-full bg-primary transition-all",
indeterminate && "w-1/3 animate-pulse",
)}
style={indeterminate ? undefined : { width: `${percent}%` }}
/>
</div>
);
},
);
Progress.displayName = "Progress";

View File

@@ -0,0 +1,22 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export type SelectProps = React.SelectHTMLAttributes<HTMLSelectElement>;
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, children, ...props }, ref) => {
return (
<select
ref={ref}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
{children}
</select>
);
},
);
Select.displayName = "Select";

View File

@@ -0,0 +1,13 @@
import { cn } from "@/lib/utils";
export function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
);
}

View File

@@ -0,0 +1,98 @@
import * as React from "react";
import { cn } from "@/lib/utils";
interface TabsContextValue {
value: string;
onChange: (value: string) => void;
}
const TabsContext = React.createContext<TabsContextValue | null>(null);
function useTabs() {
const ctx = React.useContext(TabsContext);
if (!ctx) throw new Error("Tabs primitives must be used within <Tabs>");
return ctx;
}
interface TabsProps {
value: string;
onValueChange: (value: string) => void;
className?: string;
children: React.ReactNode;
}
export function Tabs({ value, onValueChange, className, children }: TabsProps) {
return (
<TabsContext.Provider value={{ value, onChange: onValueChange }}>
<div className={cn("flex flex-col gap-3", className)}>{children}</div>
</TabsContext.Provider>
);
}
export function TabsList({
className,
children,
}: {
className?: string;
children: React.ReactNode;
}) {
return (
<div
role="tablist"
className={cn(
"inline-flex h-10 items-center justify-start gap-1 rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
>
{children}
</div>
);
}
export function TabsTrigger({
value,
children,
className,
}: {
value: string;
children: React.ReactNode;
className?: string;
}) {
const ctx = useTabs();
const active = ctx.value === value;
return (
<button
role="tab"
type="button"
aria-selected={active}
onClick={() => ctx.onChange(value)}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
active
? "bg-background text-foreground shadow-sm"
: "hover:text-foreground",
className,
)}
>
{children}
</button>
);
}
export function TabsContent({
value,
children,
className,
}: {
value: string;
children: React.ReactNode;
className?: string;
}) {
const ctx = useTabs();
if (ctx.value !== value) return null;
return (
<div role="tabpanel" className={cn("focus-visible:outline-none", className)}>
{children}
</div>
);
}

View File

@@ -0,0 +1,20 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
ref={ref}
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
);
},
);
Textarea.displayName = "Textarea";