Chore: remove .pnpm_store, .claude, and .idea from repository

This commit is contained in:
tao.chen
2026-07-30 20:08:33 +08:00
parent c74b2abb48
commit 7d684a6736
28 changed files with 0 additions and 11016 deletions
-8
View File
@@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
Generated
-1
View File
@@ -1 +0,0 @@
.env
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_20" default="true" project-jdk-name="20" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/model-develop-refactored.iml" filepath="$PROJECT_DIR$/.idea/model-develop-refactored.iml" />
</modules>
</component>
</project>
Binary file not shown.
@@ -1,4 +0,0 @@
.react-router
build
node_modules
README.md
@@ -1,7 +0,0 @@
.DS_Store
.env
/node_modules/
# React Router
/.react-router/
/build/
@@ -1,22 +0,0 @@
# Frontend
React Router SPA,已将原前端按功能拆分到 `app/`
```text
app/components/ 通用图标组件
app/features/platform/ 主框架、脚本工作区和 Jupyter 编辑
app/features/schedules/ DAG 调度页面
app/features/admin/ 工作台与系统管理
app/routes/ React Router 路由入口
app/services/ API 请求、DTO 和演示上下文
app/styles/ 分功能样式
```
本地开发:
```bash
pnpm install
pnpm dev
```
生产构建由根目录 `nginx/Dockerfile` 完成,构建结果复制到 Nginx 静态目录。
@@ -1,11 +0,0 @@
:root {
font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
color: #1f354b;
background: #f3f6f9;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-width: 1024px; min-height: 100%; }
button, input, select, textarea { font: inherit; }
@@ -1,183 +0,0 @@
type IconName =
| "brand"
| "home"
| "script"
| "schedule"
| "experiment"
| "database"
| "settings"
| "chevron"
| "search"
| "plus"
| "upload"
| "folder"
| "notebook"
| "python"
| "refresh"
| "close"
| "check"
| "info"
| "workspace"
| "menu"
| "external"
| "release"
| "play";
type IconProps = {
name: IconName;
size?: number;
strokeWidth?: number;
};
export default function Icon({
name,
size = 18,
strokeWidth = 1.8,
}: IconProps) {
const common = {
width: size,
height: size,
viewBox: "0 0 24 24",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
"aria-hidden": true,
};
if (name === "brand") {
return (
<svg {...common} viewBox="0 0 32 32">
<path
d="M16 3.5 27 9.8v12.4L16 28.5 5 22.2V9.8L16 3.5Z"
fill="currentColor"
opacity=".2"
/>
<path
d="M16 7.7 23.4 12v8L16 24.3 8.6 20v-8L16 7.7Z"
fill="currentColor"
/>
<circle cx="16" cy="16" r="3.3" fill="white" />
</svg>
);
}
const paths: Record<Exclude<IconName, "brand">, React.ReactNode> = {
home: (
<>
<path d="m3.5 11 8.5-7 8.5 7" />
<path d="M5.5 10v10h13V10M9.5 20v-6h5v6" />
</>
),
script: (
<>
<rect x="5" y="3.5" width="14" height="17" rx="2" />
<path d="M8.5 8h7M8.5 12h7M8.5 16h4.5" />
</>
),
schedule: (
<>
<circle cx="12" cy="12" r="8.5" />
<path d="M12 7.5V12l3.2 2" />
</>
),
experiment: (
<>
<path d="M9 3.5h6M10 3.5v5l-5.2 9a2 2 0 0 0 1.7 3h11a2 2 0 0 0 1.7-3L14 8.5v-5" />
<path d="M7.3 15h9.4" />
</>
),
database: (
<>
<ellipse cx="12" cy="5.5" rx="7.5" ry="3" />
<path d="M4.5 5.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6" />
<path d="M4.5 11.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6" />
</>
),
settings: (
<>
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1a1.7 1.7 0 0 0 1.9.3A1.7 1.7 0 0 0 10 3v-.2h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z" />
</>
),
chevron: <path d="m9 7 5 5-5 5" />,
search: (
<>
<circle cx="10.5" cy="10.5" r="6.5" />
<path d="m15.5 15.5 4 4" />
</>
),
plus: <path d="M12 5v14M5 12h14" />,
upload: (
<>
<path d="M12 16V4m0 0L7.5 8.5M12 4l4.5 4.5" />
<path d="M4 15.5V20h16v-4.5" />
</>
),
folder: (
<path d="M3.5 6.5h6l2 2H20.5v10.5a1.5 1.5 0 0 1-1.5 1.5H5a1.5 1.5 0 0 1-1.5-1.5V6.5Z" />
),
notebook: (
<>
<rect x="5" y="3.5" width="14" height="17" rx="2" />
<path d="M8.5 3.5v17M11.5 8h4M11.5 12h4M11.5 16h2.5" />
</>
),
python: (
<>
<path d="M8 4.5h5.5a2.5 2.5 0 0 1 2.5 2.5v3H8a3 3 0 0 0-3 3v2" />
<path d="M16 19.5h-5.5A2.5 2.5 0 0 1 8 17v-3h8a3 3 0 0 0 3-3V9" />
<circle cx="11" cy="7" r=".8" fill="currentColor" stroke="none" />
<circle cx="13" cy="17" r=".8" fill="currentColor" stroke="none" />
</>
),
refresh: (
<>
<path d="M19 8V4l-1.7 1.7A8 8 0 1 0 20 12" />
<path d="M19 4h-4" />
</>
),
close: <path d="m6 6 12 12M18 6 6 18" />,
check: <path d="m5 12.5 4 4L19 7" />,
info: (
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 11v5M12 8h.01" />
</>
),
workspace: (
<>
<rect x="3.5" y="5" width="17" height="14" rx="2" />
<path d="M8 5V3.5h8V5M8 11h8M12 8v6" />
</>
),
menu: (
<>
<path d="M4 6h16M4 12h16M4 18h16" />
</>
),
external: (
<>
<path d="M14 5h5v5M19 5l-8 8" />
<path d="M18 13v6H5V6h6" />
</>
),
release: (
<>
<path d="M12 3.5v11M7.5 8 12 3.5 16.5 8" />
<path d="M5 12.5v7h14v-7" />
</>
),
play: <path d="m8 5 11 7-11 7V5Z" />,
};
return (
<svg
{...common}
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
>
{paths[name]}
</svg>
);
}
@@ -1,256 +0,0 @@
import { FormEvent, useEffect, useState } from "react";
import {
ApiRequestError,
createEmployee,
deleteEmployee,
demoContext,
listEmployees,
updateEmployee,
type Employee,
} from "../../services/api";
import Icon from "../../components/Icon";
import "../../styles/admin.css";
import "../../styles/dashboard.css";
type Notice = {
tone: "success" | "error" | "info";
message: string;
};
export function DashboardPage({
scriptCount,
online,
onNavigate,
}: {
scriptCount: number;
online: boolean;
onNavigate: (page: "scripts" | "schedules" | "system") => void;
}) {
return (
<section className="dashboard-page">
<div className="dashboard-hero">
<div>
<span>MODEL DEVELOPMENT PLATFORM</span>
<h2>{demoContext.userName}</h2>
<p> {demoContext.workspaceName}</p>
</div>
<span className="dashboard-hero__badge">{online ? "服务正常" : "服务连接中"}</span>
</div>
<div className="dashboard-metrics">
<article><Icon name="script" /><span><b>{scriptCount}</b><small></small></span></article>
<article><Icon name="workspace" /><span><b>2</b><small>Workspace</small></span></article>
<article><Icon name="settings" /><span><b>4</b><small></small></span></article>
<article><Icon name="check" /><span><b>{online ? "正常" : "检查中"}</b><small></small></span></article>
</div>
<div className="dashboard-actions">
<button type="button" onClick={() => onNavigate("scripts")}><Icon name="script" /></button>
<button type="button" onClick={() => onNavigate("schedules")}><Icon name="schedule" /></button>
<button type="button" onClick={() => onNavigate("system")}><Icon name="settings" /></button>
</div>
<div className="dashboard-grid">
<section className="dashboard-panel dashboard-panel--trend">
<header><div><span></span><h3> 7 </h3></div><b> 92.6%</b></header>
<div className="trend-chart">
{[38, 55, 44, 73, 61, 86, 78].map((value, index) => (
<div className="trend-column" key={index}>
<span className="trend-column__value">{Math.round(value / 7)}</span>
<i style={{ height: `${value}%` }} />
<small>{["周一", "周二", "周三", "周四", "周五", "周六", "今天"][index]}</small>
</div>
))}
</div>
<footer><span><i className="legend-dot is-success" /> 75</span><span><i className="legend-dot is-failed" /> 6</span></footer>
</section>
<section className="dashboard-panel dashboard-panel--donut">
<header><div><span></span><h3></h3></div></header>
<div className="donut-layout">
<div className="donut-chart"><span><b>{scriptCount}</b><small></small></span></div>
<div className="donut-legend">
<span><i className="legend-dot is-notebook" /><b>Notebook</b><small>{Math.max(1, Math.round(scriptCount * .67))} · 67%</small></span>
<span><i className="legend-dot is-python" /><b>Python</b><small>{Math.max(0, scriptCount - Math.round(scriptCount * .67))} · 33%</small></span>
<span><i className="legend-dot is-version" /><b></b><small>3 </small></span>
</div>
</div>
</section>
<section className="dashboard-panel dashboard-panel--activity">
<header><div><span>ACTIVITY</span><h3></h3></div><button type="button"></button></header>
<div className="activity-table">
<div className="activity-table__head"><span></span><span></span><span></span><span></span></div>
{[
["数据探索.ipynb 发布稳定版本 v3.0", "张三", "成功", "16:42"],
["每日模型训练流程完成调度运行", "Scheduler", "成功", "15:25"],
["批量预测.py 更新工作副本", "王五", "已同步", "14:18"],
["风险验证流程完成 DAG 校验", "李四", "成功", "11:06"],
].map((row) => (
<div className="activity-row" key={row[0]}>
<span><i className="activity-icon"><Icon name="check" size={13} /></i>{row[0]}</span>
<span>{row[1]}</span><span><b>{row[2]}</b></span><span>{row[3]}</span>
</div>
))}
</div>
</section>
</div>
</section>
);
}
const EMPTY_FORM = {
username: "",
display_name: "",
email: "",
role_code: "developer" as "admin" | "developer",
status: "active" as "active" | "disabled" | "locked",
};
export function SystemAdminPage({
onNotify,
onConnectionChange,
}: {
onNotify: (notice: Notice) => void;
onConnectionChange: (online: boolean) => void;
}) {
const [employees, setEmployees] = useState<Employee[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [editing, setEditing] = useState<Employee | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [form, setForm] = useState(EMPTY_FORM);
const canManage = demoContext.roleCode === "admin";
const load = async (): Promise<void> => {
setLoading(true);
try {
setEmployees(await listEmployees());
onConnectionChange(true);
} catch (error) {
onConnectionChange(false);
onNotify({
tone: "error",
message: error instanceof Error ? error.message : "员工列表加载失败",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
void load();
}, []);
const openCreate = (): void => {
setEditing(null);
setForm(EMPTY_FORM);
setDialogOpen(true);
};
const openEdit = (employee: Employee): void => {
setEditing(employee);
setForm({
username: employee.username,
display_name: employee.display_name,
email: employee.email ?? "",
role_code: employee.role_code,
status: employee.status,
});
setDialogOpen(true);
};
const submit = async (event: FormEvent): Promise<void> => {
event.preventDefault();
if (!form.display_name.trim() || (!editing && !form.username.trim())) return;
setSaving(true);
try {
if (editing) {
const updated = await updateEmployee(editing.user_id, {
display_name: form.display_name.trim(),
email: form.email.trim() || null,
role_code: form.role_code,
status: form.status,
});
setEmployees((current) => current.map(
(item) => item.user_id === updated.user_id ? updated : item,
));
} else {
const created = await createEmployee({
username: form.username.trim(),
display_name: form.display_name.trim(),
email: form.email.trim() || null,
role_code: form.role_code,
});
setEmployees((current) => [...current, created]);
}
setDialogOpen(false);
onNotify({ tone: "success", message: editing ? "员工信息已更新" : "员工已添加" });
} catch (error) {
onNotify({
tone: "error",
message: error instanceof ApiRequestError ? error.message : "保存员工失败",
});
} finally {
setSaving(false);
}
};
const remove = async (employee: Employee): Promise<void> => {
if (!window.confirm(`确定从当前 Workspace 删除员工“${employee.display_name}”吗?`)) return;
try {
await deleteEmployee(employee.user_id);
setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id));
onNotify({ tone: "success", message: "员工已删除" });
} catch (error) {
onNotify({
tone: "error",
message: error instanceof Error ? error.message : "删除员工失败",
});
}
};
return (
<section className="admin-page">
<header className="admin-page__header">
<div><span></span><h2></h2><p>{demoContext.workspaceName} · {employees.length} </p></div>
<button className="primary-button" type="button" disabled={!canManage} onClick={openCreate}>
<Icon name="plus" size={15} />
</button>
</header>
{!canManage && <div className="admin-readonly"></div>}
<div className="employee-table">
<div className="employee-table__head"><span></span><span></span><span></span><span></span><span></span></div>
{loading ? <p className="admin-empty"></p> : employees.map((employee) => {
const isProtectedAdmin = employee.role_code === "admin";
return (
<div className="employee-row" key={employee.user_id}>
<span className="employee-name"><b className="avatar">{employee.display_name.slice(0, 1)}</b><span><strong>{employee.display_name}</strong><small>{employee.email ?? "未设置邮箱"}</small></span></span>
<code>{employee.username}</code>
<span className={`role-pill is-${employee.role_code}`}>{employee.role_name}</span>
<span className={`status-pill is-${employee.status}`}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span>
<span className="employee-actions">
<button type="button" disabled={!canManage} onClick={() => openEdit(employee)}></button>
<button type="button" className="is-danger" disabled={!canManage || isProtectedAdmin} title={isProtectedAdmin ? "管理员账号不能删除" : "删除"} onClick={() => void remove(employee)}></button>
</span>
</div>
);
})}
</div>
{dialogOpen && (
<div className="modal-backdrop">
<section className="modal modal--compact" role="dialog" aria-modal="true">
<div className="modal__header"><div><span className="modal__eyebrow">EMPLOYEE</span><h2>{editing ? "编辑员工" : "添加员工"}</h2></div><button className="icon-button" type="button" onClick={() => setDialogOpen(false)}><Icon name="close" /></button></div>
<form onSubmit={(event) => void submit(event)}>
<label className="form-field"><span></span><input autoFocus value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} /></label>
<label className="form-field"><span></span><input disabled={Boolean(editing)} value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} /></label>
<label className="form-field"><span></span><input type="email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} /></label>
<label className="form-field"><span></span><select value={form.role_code} onChange={(event) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}><option value="developer"></option><option value="admin"></option></select></label>
{editing && <label className="form-field"><span></span><select value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}><option value="active"></option><option value="disabled"></option><option value="locked"></option></select></label>}
<div className="modal__footer"><button className="secondary-button" type="button" onClick={() => setDialogOpen(false)}></button><button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button></div>
</form>
</section>
</div>
)}
</section>
);
}
@@ -1,75 +0,0 @@
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
import type { Route } from "./+types/root";
import "./app.css";
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App() {
return <Outlet />;
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "页面加载失败";
let details = "系统发生了未预期的错误。";
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "请求失败";
details =
error.status === 404
? "没有找到请求的页面。"
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
);
}
@@ -1,3 +0,0 @@
import { type RouteConfig, route } from "@react-router/dev/routes";
export default [route("*", "routes/platform.tsx")] satisfies RouteConfig;
@@ -1,13 +0,0 @@
import type { Route } from "./+types/platform";
import ModelPlatformApp from "../features/platform/ModelPlatformApp";
export function meta({}: Route.MetaArgs) {
return [
{ title: "模型实验开发平台" },
{ name: "description", content: "模型实验、脚本版本与 DAG 调度平台" },
];
}
export default function PlatformRoute() {
return <ModelPlatformApp />;
}
@@ -1,853 +0,0 @@
export type DemoUser = {
userId: string;
userName: string;
username: string;
roleCode: "admin" | "developer";
roleName: string;
};
export type DemoWorkspace = {
workspaceId: string;
workspaceName: string;
};
export const demoUsers: DemoUser[] = [
{ userId: "0000000000RF6FG1SDBXG59S13", userName: "张三", username: "admin-zhang", roleCode: "admin", roleName: "管理员" },
{ userId: "0000000000H2QYCGPCWQM1JSGS", userName: "李四", username: "admin-li", roleCode: "admin", roleName: "管理员" },
{ userId: "0000000000RWG40ESZPGJT629J", userName: "王五", username: "dev-wang", roleCode: "developer", roleName: "开发人员" },
{ userId: "00000000004CQV7WASJA6N6FW4", userName: "赵六", username: "dev-zhao", roleCode: "developer", roleName: "开发人员" },
];
export const demoWorkspaces: DemoWorkspace[] = [
{ workspaceId: "00000000000BM630VT9ARVFZPC", workspaceName: "模型开发 Workspace" },
{ workspaceId: "0000000000AE0NC0V5T424KK86", workspaceName: "风险验证 Workspace" },
];
function readStoredContext(): Partial<{
userId: string;
workspaceId: string;
}> {
if (typeof window === "undefined") return {};
try {
return JSON.parse(
window.localStorage.getItem("model-platform-demo-context") ?? "{}",
) as Partial<{ userId: string; workspaceId: string }>;
} catch {
return {};
}
}
const storedContext = readStoredContext();
const initialUser = demoUsers.find((item) => item.userId === storedContext.userId)
?? demoUsers[0];
const initialWorkspace = demoWorkspaces.find(
(item) => item.workspaceId === storedContext.workspaceId,
) ?? demoWorkspaces[0];
export const demoContext = {
...initialUser,
...initialWorkspace,
};
export function setDemoContext(input: {
user?: DemoUser;
workspace?: DemoWorkspace;
}): void {
if (input.user) Object.assign(demoContext, input.user);
if (input.workspace) Object.assign(demoContext, input.workspace);
if (typeof window !== "undefined") {
window.localStorage.setItem("model-platform-demo-context", JSON.stringify({
userId: demoContext.userId,
workspaceId: demoContext.workspaceId,
}));
}
}
export type ScriptType = "python" | "notebook";
export type Visibility = "private" | "workspace" | "public";
export type Employee = {
user_id: string;
username: string;
display_name: string;
email: string | null;
status: "active" | "disabled" | "locked";
role_code: "admin" | "developer";
role_name: string;
created_at: string;
};
export type ScriptItem = {
script_id: string;
workspace_id: string;
current_object_id: string;
owner_user_id: string;
script_name: string;
script_type: ScriptType;
visibility: Visibility;
status: string;
relative_path: string;
content_hash: string;
size_bytes: number;
created_at: string;
updated_at: string;
};
export type WorkspaceDirectory = {
path: string;
name: string;
parent_path: string;
};
type ApiEnvelope<T> = {
request_id: string;
data: T;
meta: Record<string, unknown>;
};
type ApiErrorEnvelope = {
detail?: string | {
code?: string;
message?: string;
};
error?: {
code?: string;
message?: string;
details?: {
editor_name?: string;
lease_expires_at?: string;
};
};
};
export class ApiRequestError extends Error {
readonly status: number;
readonly code?: string;
constructor(message: string, status: number, code?: string) {
super(message);
this.name = "ApiRequestError";
this.status = status;
this.code = code;
}
}
async function apiRequest<T>(
path: string,
init: RequestInit = {},
): Promise<T> {
const response = await fetch(path, {
...init,
credentials: "same-origin",
headers: {
"X-User-ID": demoContext.userId,
"X-Workspace-ID": demoContext.workspaceId,
"X-Request-ID": crypto.randomUUID().replaceAll("-", ""),
...(init.body ? { "Content-Type": "application/json" } : {}),
...init.headers,
},
});
const payload = (await response.json().catch(() => ({}))) as
| ApiEnvelope<T>
| ApiErrorEnvelope;
if (!response.ok) {
const error = payload as ApiErrorEnvelope;
const detailMessage = typeof error.detail === "string"
? error.detail
: error.detail?.message;
const editor = error.error?.details?.editor_name;
throw new ApiRequestError(
(editor ? `${error.error?.message ?? "文件正在编辑"}${editor}` : undefined)
?? error.error?.message
?? detailMessage
?? `请求失败(HTTP ${response.status}`,
response.status,
typeof error.detail === "object"
? error.detail?.code
: error.error?.code,
);
}
return (payload as ApiEnvelope<T>).data;
}
export async function listScripts(): Promise<ScriptItem[]> {
return apiRequest<ScriptItem[]>("/api/v1/scripts");
}
function initialContent(scriptType: ScriptType): string {
if (scriptType === "python") {
return [
'"""模型实验开发平台构建脚本。"""',
"",
"",
"def main() -> None:",
' print("Hello, Model Platform!")',
"",
"",
'if __name__ == "__main__":',
" main()",
"",
].join("\n");
}
return JSON.stringify(
{
cells: [
{
cell_type: "markdown",
metadata: {},
source: ["# 新建模型实验\\n", "在这里开始数据探索与模型构建。"],
},
{
cell_type: "code",
execution_count: null,
metadata: {},
outputs: [],
source: ["print('Hello, Model Platform!')\\n"],
},
],
metadata: {
kernelspec: {
display_name: "Python 3",
language: "python",
name: "python3",
},
language_info: {
name: "python",
version: "3.12",
},
},
nbformat: 4,
nbformat_minor: 5,
},
null,
2,
);
}
export async function createScript(input: {
name: string;
scriptType: ScriptType;
visibility: Visibility;
parentPath?: string | null;
}): Promise<ScriptItem> {
return apiRequest<ScriptItem>("/api/v1/scripts", {
method: "POST",
body: JSON.stringify({
script_name: input.name.trim(),
script_type: input.scriptType,
visibility: input.visibility,
content: initialContent(input.scriptType),
parent_path: input.parentPath,
}),
});
}
export async function uploadScript(
file: File,
parentPath = "",
visibility: Visibility = "workspace",
): Promise<ScriptItem> {
const parameters = new URLSearchParams({
file_name: file.name,
parent_path: parentPath,
visibility,
});
return apiRequest<ScriptItem>(
`/api/v1/scripts/upload?${parameters.toString()}`,
{
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: file,
},
);
}
export async function deleteScript(
scriptId: string,
): Promise<{ script_id: string; status: string; versions_preserved: boolean }> {
return apiRequest(`/api/v1/scripts/${scriptId}`, { method: "DELETE" });
}
export async function listWorkspaceDirectories(): Promise<
WorkspaceDirectory[]
> {
const data = await apiRequest<{ directories: WorkspaceDirectory[] }>(
"/api/v1/workspace-tree",
);
return data.directories;
}
export async function createWorkspaceDirectory(
directoryName: string,
parentPath = "",
): Promise<WorkspaceDirectory> {
return apiRequest<WorkspaceDirectory>("/api/v1/workspace-directories", {
method: "POST",
body: JSON.stringify({
directory_name: directoryName,
parent_path: parentPath,
}),
});
}
export async function deleteWorkspaceDirectory(
path: string,
): Promise<{
path: string;
status: string;
deleted_scripts: number;
versions_preserved: boolean;
}> {
const parameters = new URLSearchParams({ path });
return apiRequest(`/api/v1/workspace-directories?${parameters.toString()}`, {
method: "DELETE",
});
}
export type FileLockSession = {
edit_session_id: string;
workspace_id: string;
storage_object_id: string;
user_id: string;
session_status: "active" | "closed" | "expired";
lease_seconds: number;
heartbeat_interval_seconds: number;
expires_at: string;
runtime_id: string;
jupyter_session_id: string;
jupyter_url?: string;
relative_path?: string;
lock_token?: string;
};
export type ActiveEditSession = FileLockSession & {
script_id: string;
script_name: string;
lock_token: string;
ticket_expires_at?: string;
};
export type JupyterAccessTicket = {
edit_session_id: string;
jupyter_url: string;
expires_at: string;
};
export type StableVersion = {
versions_id: string;
workspace_id: string;
script_id: string;
source_object_id: string;
artifact_object_id: string;
version_no: number;
version_label: string;
source_path: string;
artifact_path: string;
content_hash: string;
file_size_bytes: number;
visibility: Visibility;
release_note: string | null;
created_by: string;
created_at: string;
};
export async function acquireFileLock(
script: ScriptItem,
): Promise<ActiveEditSession> {
const session = await apiRequest<FileLockSession>(
`/api/v1/files/${script.current_object_id}/lock`,
{ method: "POST" },
);
if (!session.lock_token) {
throw new Error("加锁成功响应缺少 lock_token");
}
return {
...session,
script_id: script.script_id,
script_name: script.script_name,
lock_token: session.lock_token,
};
}
export async function heartbeatFileLock(
session: ActiveEditSession,
): Promise<FileLockSession> {
return apiRequest<FileLockSession>(
`/api/v1/file-locks/${session.edit_session_id}/heartbeat`,
{
method: "POST",
body: JSON.stringify({ lock_token: session.lock_token }),
},
);
}
export async function releaseFileLock(
session: ActiveEditSession,
): Promise<FileLockSession> {
return apiRequest<FileLockSession>(
`/api/v1/file-locks/${session.edit_session_id}`,
{
method: "DELETE",
body: JSON.stringify({ lock_token: session.lock_token }),
},
);
}
export function releaseFileLockOnUnload(session: ActiveEditSession): void {
void fetch(`/api/v1/file-locks/${session.edit_session_id}`, {
method: "DELETE",
credentials: "same-origin",
keepalive: true,
headers: {
"Content-Type": "application/json",
"X-User-ID": demoContext.userId,
"X-Workspace-ID": demoContext.workspaceId,
"X-Request-ID": crypto.randomUUID().replaceAll("-", ""),
},
body: JSON.stringify({ lock_token: session.lock_token }),
});
}
export async function createJupyterAccessTicket(
session: ActiveEditSession,
): Promise<JupyterAccessTicket> {
return apiRequest<JupyterAccessTicket>("/api/v1/jupyter/access-tickets", {
method: "POST",
body: JSON.stringify({
edit_session_id: session.edit_session_id,
lock_token: session.lock_token,
}),
});
}
export async function listScriptVersions(
scriptId: string,
): Promise<StableVersion[]> {
return apiRequest<StableVersion[]>(`/api/v1/scripts/${scriptId}/versions`);
}
export async function publishScriptVersion(input: {
script: ScriptItem;
releaseNote: string;
visibility: Visibility;
}): Promise<StableVersion> {
return apiRequest<StableVersion>(
`/api/v1/scripts/${input.script.script_id}/versions`,
{
method: "POST",
body: JSON.stringify({
source_object_id: input.script.current_object_id,
release_note: input.releaseNote.trim() || null,
visibility: input.visibility,
}),
},
);
}
export type ScheduleArtifact = {
versions_id: string;
version_label: string;
script_id: string;
script_name: string;
script_type: ScriptType;
content_hash: string;
file_size_bytes: number;
visibility: Visibility;
created_by: string;
created_at: string;
};
export type ScheduleNode = {
node_id: string;
schedule_id: string;
node_key: string;
node_name: string;
versions_id: string;
timeout_seconds: number;
retry_count: number;
retry_interval_sec: number;
position_x: number;
position_y: number;
arguments_json: Record<string, unknown>;
env_refs_json: Record<string, string>;
created_at: string;
updated_at: string;
version: {
versions_id: string;
version_label: string;
script_id: string;
script_name: string;
script_type: ScriptType;
content_hash: string;
created_at: string;
};
};
export type ScheduleEdge = {
edge_id: string;
schedule_id: string;
source_node_id: string;
target_node_id: string;
condition_expr: string | null;
created_at: string;
};
export type DagValidation = {
valid: boolean;
node_count: number;
edge_count: number;
root_node_ids: string[];
leaf_node_ids: string[];
topological_order: string[];
errors: Array<{
code: string;
message: string;
edge_id?: string;
node_ids?: string[];
}>;
};
export type Schedule = {
schedule_id: string;
workspace_id: string;
schedule_name: string;
description: string | null;
trigger_type: "manual" | "cron" | "api";
cron_expression: string | null;
timezone: string;
enabled: boolean;
workflow_version: number;
max_concurrency: number;
failure_policy: "stop" | "continue";
last_run_at: string | null;
next_run_at: string | null;
created_by: string;
updated_by: string;
created_at: string;
updated_at: string;
node_count: number;
edge_count: number;
nodes: ScheduleNode[];
edges: ScheduleEdge[];
dag_validation: DagValidation;
};
export type CronPreview = {
cron_expression: string;
timezone: string;
base_time: string;
occurrences: Array<{
local_time: string;
utc_time: string;
}>;
};
export type ScheduleRunStatus =
| "queued"
| "running"
| "succeeded"
| "failed"
| "cancelled"
| "timed_out";
export type ScheduleNodeRunStatus =
| ScheduleRunStatus
| "skipped";
export type ScheduleRunSummary = {
run_id: string;
schedule_id: string;
workspace_id: string;
workflow_version: number;
trigger_type: "manual" | "cron" | "api" | "retry";
run_status: ScheduleRunStatus;
state_version: number;
queued_at: string;
started_at: string | null;
finished_at: string | null;
duration_ms: number | null;
error_code: string | null;
error_message: string | null;
logs_object_id: string | null;
result_object_id: string | null;
};
export type ScheduleNodeRun = {
node_run_id: string;
run_id: string;
node_id: string;
versions_id: string;
attempt_no: number;
node_status: ScheduleNodeRunStatus;
state_version: number;
started_at: string | null;
finished_at: string | null;
duration_ms: number | null;
exit_code: number | null;
message: string | null;
logs_object_id: string | null;
result_object_id: string | null;
};
export type ScheduleRunDetail = ScheduleRunSummary & {
node_runs: ScheduleNodeRun[];
};
export async function listSchedules(): Promise<Schedule[]> {
return apiRequest<Schedule[]>("/api/v1/schedules");
}
export async function getSchedule(scheduleId: string): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}`);
}
export async function createSchedule(input: {
schedule_name: string;
description?: string | null;
trigger_type?: "manual" | "cron" | "api";
cron_expression?: string | null;
timezone?: string;
enabled?: boolean;
max_concurrency?: number;
failure_policy?: "stop" | "continue";
}): Promise<Schedule> {
return apiRequest<Schedule>("/api/v1/schedules", {
method: "POST",
body: JSON.stringify(input),
});
}
export async function updateSchedule(
scheduleId: string,
input: {
workflow_version: number;
schedule_name?: string;
description?: string | null;
trigger_type?: "manual" | "cron" | "api";
cron_expression?: string | null;
timezone?: string;
enabled?: boolean;
max_concurrency?: number;
failure_policy?: "stop" | "continue";
},
): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export async function deleteSchedule(
scheduleId: string,
workflowVersion: number,
): Promise<{ schedule_id: string; deleted: boolean; workflow_version: number }> {
return apiRequest(`/api/v1/schedules/${scheduleId}`, {
method: "DELETE",
body: JSON.stringify({ workflow_version: workflowVersion }),
});
}
export async function listScheduleArtifacts(): Promise<ScheduleArtifact[]> {
return apiRequest<ScheduleArtifact[]>("/api/v1/schedule-artifacts");
}
export async function hideScheduleArtifact(
versionsId: string,
): Promise<{
versions_id: string;
deleted: boolean;
artifact_preserved: boolean;
}> {
return apiRequest(`/api/v1/versions/${versionsId}`, {
method: "DELETE",
});
}
export async function listEmployees(): Promise<Employee[]> {
return apiRequest<Employee[]>("/api/v1/admin/employees");
}
export async function createEmployee(input: {
username: string;
display_name: string;
email?: string | null;
role_code: "admin" | "developer";
}): Promise<Employee> {
return apiRequest<Employee>("/api/v1/admin/employees", {
method: "POST",
body: JSON.stringify(input),
});
}
export async function updateEmployee(
userId: string,
input: {
display_name?: string;
email?: string | null;
role_code?: "admin" | "developer";
status?: "active" | "disabled" | "locked";
},
): Promise<Employee> {
return apiRequest<Employee>(`/api/v1/admin/employees/${userId}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}
export async function deleteEmployee(
userId: string,
): Promise<{ user_id: string; deleted: boolean }> {
return apiRequest(`/api/v1/admin/employees/${userId}`, {
method: "DELETE",
});
}
export async function createScheduleNode(
scheduleId: string,
input: {
workflow_version: number;
node_key: string;
node_name: string;
versions_id: string;
timeout_seconds?: number;
retry_count?: number;
retry_interval_sec?: number;
position_x?: number;
position_y?: number;
arguments_json?: Record<string, unknown>;
env_refs_json?: Record<string, string>;
},
): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}/nodes`, {
method: "POST",
body: JSON.stringify(input),
});
}
export async function updateScheduleNode(
scheduleId: string,
nodeId: string,
input: {
workflow_version: number;
node_name?: string;
versions_id?: string;
timeout_seconds?: number;
retry_count?: number;
retry_interval_sec?: number;
position_x?: number;
position_y?: number;
arguments_json?: Record<string, unknown>;
env_refs_json?: Record<string, string>;
},
): Promise<Schedule> {
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/nodes/${nodeId}`,
{
method: "PUT",
body: JSON.stringify(input),
},
);
}
export async function deleteScheduleNode(
scheduleId: string,
nodeId: string,
workflowVersion: number,
): Promise<Schedule> {
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/nodes/${nodeId}`,
{
method: "DELETE",
body: JSON.stringify({ workflow_version: workflowVersion }),
},
);
}
export async function createScheduleEdge(
scheduleId: string,
input: {
workflow_version: number;
source_node_id: string;
target_node_id: string;
condition_expr?: string | null;
},
): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}/edges`, {
method: "POST",
body: JSON.stringify(input),
});
}
export async function deleteScheduleEdge(
scheduleId: string,
edgeId: string,
workflowVersion: number,
): Promise<Schedule> {
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/edges/${edgeId}`,
{
method: "DELETE",
body: JSON.stringify({ workflow_version: workflowVersion }),
},
);
}
export async function validateSchedule(
scheduleId: string,
): Promise<DagValidation & {
schedule_id: string;
workflow_version: number;
}> {
return apiRequest(`/api/v1/schedules/${scheduleId}/validate`, {
method: "POST",
});
}
export async function previewCron(input: {
cron_expression: string;
timezone: string;
count?: number;
base_time?: string;
}): Promise<CronPreview> {
return apiRequest<CronPreview>("/api/v1/cron/preview", {
method: "POST",
body: JSON.stringify(input),
});
}
export async function runScheduleNow(
scheduleId: string,
): Promise<ScheduleRunDetail> {
return apiRequest<ScheduleRunDetail>(
`/api/v1/schedules/${scheduleId}/run`,
{
method: "POST",
headers: {
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ reason: "manual_run" }),
},
);
}
export async function listScheduleRuns(input: {
scheduleId?: string;
status?: ScheduleRunStatus;
limit?: number;
} = {}): Promise<ScheduleRunSummary[]> {
const query = new URLSearchParams();
if (input.scheduleId) query.set("schedule_id", input.scheduleId);
if (input.status) query.set("status", input.status);
query.set("limit", String(input.limit ?? 20));
return apiRequest<ScheduleRunSummary[]>(
`/api/v1/schedule-runs?${query.toString()}`,
);
}
export async function getScheduleRun(
runId: string,
): Promise<ScheduleRunDetail> {
return apiRequest<ScheduleRunDetail>(`/api/v1/schedule-runs/${runId}`);
}
@@ -1 +0,0 @@
.dashboard-page,.admin-page{height:100%;padding:24px;overflow:auto;background:#f3f6f9}.dashboard-hero{display:flex;align-items:center;justify-content:space-between;padding:34px;border-radius:12px;color:#fff;background:linear-gradient(125deg,#0b3d69,#1978d4);box-shadow:0 12px 30px rgb(19 80 137/18%)}.dashboard-hero span{font-size:10px;letter-spacing:.12em;opacity:.82}.dashboard-hero h2{margin:8px 0 5px;font-size:25px}.dashboard-hero p{margin:0;font-size:12px;opacity:.88}.dashboard-hero__badge{padding:8px 12px;border-radius:20px;background:rgb(255 255 255/16%)}.dashboard-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:18px}.dashboard-metrics article{display:flex;align-items:center;gap:14px;padding:20px;border:1px solid #e0e8ef;border-radius:9px;background:#fff}.dashboard-metrics svg{color:#1978d4}.dashboard-metrics span{display:flex;flex-direction:column}.dashboard-metrics b{color:#20364c;font-size:21px}.dashboard-metrics small{color:#8796a6}.dashboard-actions{display:flex;gap:12px;margin-top:18px}.dashboard-actions button{display:flex;align-items:center;gap:8px;padding:13px 18px;border:1px solid #d8e4ef;border-radius:7px;color:#346587;background:#fff;cursor:pointer}.admin-page__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:15px;padding:20px 22px;border:1px solid #dee7ef;border-radius:9px;background:#fff}.admin-page__header span{color:#1978d4;font-size:10px;font-weight:800}.admin-page__header h2{margin:4px 0;color:#20364c}.admin-page__header p{margin:0;color:#8a99a8;font-size:11px}.admin-readonly{margin-bottom:12px;padding:10px 13px;border:1px solid #f1d79c;border-radius:6px;color:#8a6416;background:#fff8e8;font-size:11px}.employee-table{overflow:hidden;border:1px solid #dfe7ee;border-radius:9px;background:#fff}.employee-table__head,.employee-row{display:grid;grid-template-columns:1.5fr 1fr .7fr .65fr .8fr;align-items:center;gap:12px;padding:12px 17px}.employee-table__head{color:#748598;background:#f5f8fb;font-size:10px;font-weight:700}.employee-row{min-height:64px;border-top:1px solid #edf1f5;color:#44576a;font-size:11px}.employee-name{display:flex;align-items:center;gap:10px}.employee-name .avatar{display:grid;width:32px;height:32px;place-items:center}.employee-name>span{display:flex;min-width:0;flex-direction:column}.employee-name strong{color:#23384e}.employee-name small{margin-top:3px;color:#93a0ae}.employee-row code{color:#60758b}.role-pill,.status-pill{width:max-content;padding:4px 8px;border-radius:12px}.role-pill{color:#1d6ab3;background:#eaf4ff}.role-pill.is-admin{color:#87580f;background:#fff2d6}.status-pill{color:#138657;background:#e8f8f1}.status-pill.is-disabled,.status-pill.is-locked{color:#a64b4b;background:#ffeded}.employee-actions{display:flex;gap:6px}.employee-actions button{padding:5px 9px;border:1px solid #d9e3ec;border-radius:4px;color:#4c6c88;background:#fff;cursor:pointer}.employee-actions button.is-danger{color:#c14a4a}.employee-actions button:disabled{cursor:not-allowed;opacity:.45}.admin-empty{padding:25px;text-align:center;color:#8796a6}
@@ -1,185 +0,0 @@
.dashboard-grid {
display: grid;
grid-template-columns: minmax(0, 1.6fr) minmax(310px, .9fr);
gap: 14px;
margin-top: 18px;
padding-bottom: 25px;
}
.dashboard-panel {
border: 1px solid #dfe7ee;
border-radius: 10px;
background: #fff;
box-shadow: 0 5px 16px rgb(33 65 97 / 5%);
}
.dashboard-panel > header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 17px 20px;
border-bottom: 1px solid #edf1f5;
}
.dashboard-panel header span {
color: #7890a6;
font-size: 9px;
font-weight: 800;
letter-spacing: .09em;
}
.dashboard-panel h3 {
margin: 4px 0 0;
color: #263b50;
font-size: 14px;
}
.dashboard-panel--trend > header > b {
padding: 5px 9px;
border-radius: 13px;
color: #16855a;
background: #e8f8f1;
font-size: 10px;
}
.trend-chart {
display: flex;
height: 190px;
align-items: flex-end;
gap: 18px;
padding: 26px 28px 17px;
background: repeating-linear-gradient(to bottom, #fff 0, #fff 44px, #eef3f7 45px);
}
.trend-column {
display: flex;
height: 100%;
flex: 1;
align-items: center;
justify-content: flex-end;
flex-direction: column;
gap: 5px;
}
.trend-column i {
display: block;
width: min(36px, 72%);
min-height: 12px;
border-radius: 5px 5px 2px 2px;
background: linear-gradient(#49a0eb, #207bd1);
box-shadow: 0 4px 9px rgb(32 123 209 / 18%);
}
.trend-column__value, .trend-column small {
color: #7890a3;
font-size: 9px;
}
.dashboard-panel--trend footer {
display: flex;
justify-content: center;
gap: 22px;
padding: 10px;
color: #708397;
font-size: 9px;
}
.dashboard-panel footer span {
display: flex;
align-items: center;
gap: 5px;
}
.legend-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
}
.legend-dot.is-success { background: #27b87c; }
.legend-dot.is-failed { background: #ed7777; }
.legend-dot.is-notebook { background: #318de0; }
.legend-dot.is-python { background: #72c9b0; }
.legend-dot.is-version { background: #f3b75c; }
.donut-layout {
display: flex;
min-height: 226px;
align-items: center;
justify-content: center;
gap: 28px;
padding: 20px;
}
.donut-chart {
position: relative;
display: grid;
width: 128px;
height: 128px;
flex: 0 0 auto;
place-items: center;
border-radius: 50%;
background: conic-gradient(#318de0 0 67%, #72c9b0 67% 86%, #f3b75c 86% 100%);
}
.donut-chart::after {
position: absolute;
width: 78px;
height: 78px;
border-radius: 50%;
background: #fff;
content: "";
}
.donut-chart span {
z-index: 1;
display: flex;
align-items: center;
flex-direction: column;
}
.donut-chart b { color: #273d53; font-size: 20px; }
.donut-chart small { color: #8a9aaa; font-size: 8px; }
.donut-legend { display: flex; flex-direction: column; gap: 15px; }
.donut-legend > span {
display: grid;
grid-template-columns: 8px 1fr;
align-items: center;
column-gap: 7px;
}
.donut-legend b { color: #40566c; font-size: 10px; }
.donut-legend small { grid-column: 2; color: #8a9baa; font-size: 8px; }
.dashboard-panel--activity { grid-column: 1 / -1; }
.dashboard-panel--activity > header > button {
border: 0;
color: #257bcb;
background: transparent;
font-size: 10px;
}
.activity-table__head, .activity-row {
display: grid;
grid-template-columns: 2fr .7fr .55fr .5fr;
align-items: center;
gap: 15px;
padding: 11px 20px;
}
.activity-table__head {
color: #8595a5;
background: #f7f9fb;
font-size: 9px;
font-weight: 700;
}
.activity-row {
border-top: 1px solid #edf1f5;
color: #607488;
font-size: 10px;
}
.activity-row > span:first-child {
display: flex;
align-items: center;
gap: 9px;
color: #344c63;
}
.activity-icon {
display: grid;
width: 25px;
height: 25px;
place-items: center;
border-radius: 6px;
color: #277fcc;
background: #eaf4ff;
}
.activity-row b {
padding: 3px 7px;
border-radius: 10px;
color: #16855a;
background: #e9f8f2;
font-size: 9px;
}
@media (max-width: 1200px) {
.dashboard-grid { grid-template-columns: 1fr; }
.dashboard-panel--activity { grid-column: auto; }
}
@@ -1,30 +0,0 @@
{
"name": "frontend",
"private": true,
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
"dependencies": {
"@react-router/node": "^8",
"@react-router/serve": "^8",
"isbot": "^5.1.36",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router": "^8"
},
"devDependencies": {
"@react-router/dev": "^8",
"@tailwindcss/vite": "^4.2.2",
"@types/node": "^22",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3",
"vite": "^8.0.3"
},
"packageManager": "pnpm@10.15.1"
}
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@@ -1,7 +0,0 @@
import type { Config } from "@react-router/dev/config";
export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: false,
} satisfies Config;
@@ -1,26 +0,0 @@
{
"include": [
"**/*",
"**/.server/**/*",
"**/.client/**/*",
".react-router/types/**/*"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
"paths": {
"~/*": ["./app/*"]
},
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true
}
}
@@ -1,21 +0,0 @@
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [reactRouter()],
server: {
host: "0.0.0.0",
port: 5173,
proxy: {
"/api": {
target: process.env.VITE_GATEWAY_URL || "http://127.0.0.1:8081",
changeOrigin: true,
},
"/jupyter": {
target: process.env.VITE_GATEWAY_URL || "http://127.0.0.1:8081",
changeOrigin: true,
ws: true,
},
},
},
});