버그수정

This commit is contained in:
lasta
2026-05-14 23:37:21 +09:00
parent 39097d0240
commit a537bba74f
17 changed files with 3833 additions and 1233 deletions

View File

@@ -1,14 +1,32 @@
import { NavLink, Outlet } from "react-router-dom";
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, Network, ListChecks, Menu } from "lucide-react";
import {
LayoutDashboard,
UploadCloud,
Settings2,
Activity,
Brain,
Network,
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";
import { useProjects } from "@/hooks/useProjects";
interface NavItem {
to: string;
to?: string;
projectPath?: string;
labelKey: string;
icon: React.ComponentType<{ className?: string }>;
}
@@ -16,17 +34,71 @@ interface NavItem {
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: "/research/demo-project", labelKey: "nav.research", icon: Brain },
{ to: "/editor/demo-project", labelKey: "nav.editor", icon: Network },
{ to: "/review/demo-project", labelKey: "nav.review", icon: ListChecks },
{ projectPath: "sources", labelKey: "nav.sources", icon: Settings2 },
{ projectPath: "crawl", labelKey: "nav.crawl", icon: Activity },
{ projectPath: "research", labelKey: "nav.research", icon: Brain },
{ projectPath: "editor", labelKey: "nav.editor", icon: Network },
{ projectPath: "review", labelKey: "nav.review", icon: ListChecks },
];
const projectRoutePatterns = [
"/sources/:projectId",
"/crawl/:projectId",
"/research/:projectId",
"/editor/:projectId",
"/review/: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">
@@ -51,25 +123,62 @@ export default function AppShell() {
<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, labelKey, icon: Icon }) => (
<NavLink
key={to}
to={to}
end={to === "/"}
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)}</span>}
</NavLink>
))}
{navItems.map(({ to, projectPath, labelKey, 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)}</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)}</span>}
</NavLink>
);
})}
</nav>
</aside>

View File

@@ -15,6 +15,7 @@ export const projectSourceSchema = z.object({
id: z.union([z.string(), z.number()]).transform(String),
name: z.string(),
type: z.string(),
base_url: z.string().nullable().optional(),
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(),