feat: add flow board with database drafts
This commit is contained in:
parent
36f7eea13b
commit
d88c28356a
12
app/(chat)/flow-board/layout.tsx
Normal file
12
app/(chat)/flow-board/layout.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
export default function FlowBoardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<link href="/vendor/drawnix.css" rel="stylesheet" />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
19
app/(chat)/flow-board/page.tsx
Normal file
19
app/(chat)/flow-board/page.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import type { Metadata } from "next";
|
||||
import { FlowBoardClient } from "@/components/flow-board/flow-board-client";
|
||||
import { getSiteSettings } from "@/lib/db/queries";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "流程白板",
|
||||
};
|
||||
|
||||
export default async function FlowBoardPage() {
|
||||
const siteSettings = await getSiteSettings();
|
||||
|
||||
return (
|
||||
<FlowBoardClient
|
||||
siteLogo={siteSettings.siteLogo || null}
|
||||
siteLogoDark={siteSettings.siteLogoDark || null}
|
||||
siteName={siteSettings.siteName || "AI Chatbot"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
90
app/api/flow-board/draft/route.ts
Normal file
90
app/api/flow-board/draft/route.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import {
|
||||
getFlowBoardDraftByUserId,
|
||||
upsertFlowBoardDraftByUserId,
|
||||
} from "@/lib/db/queries";
|
||||
import type { FlowBoardSnapshotData } from "@/lib/db/schema";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeRecord(value: unknown): Record<string, unknown> | null {
|
||||
return isRecord(value) ? value : null;
|
||||
}
|
||||
|
||||
function normalizeFlowBoardSnapshot(value: unknown): FlowBoardSnapshotData {
|
||||
const raw = isRecord(value) ? value : {};
|
||||
const updatedAt =
|
||||
typeof raw.updatedAt === "string" && raw.updatedAt.trim()
|
||||
? raw.updatedAt
|
||||
: new Date().toISOString();
|
||||
|
||||
return {
|
||||
children: Array.isArray(raw.children)
|
||||
? raw.children.filter(isRecord)
|
||||
: [],
|
||||
viewport: normalizeRecord(raw.viewport),
|
||||
theme: normalizeRecord(raw.theme),
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "未登录" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const draft = await getFlowBoardDraftByUserId({
|
||||
userId: session.user.id,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
snapshot: draft?.snapshot ?? { children: [] },
|
||||
updatedAt: draft?.updatedAt?.toISOString() ?? null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "未登录" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = (await request.json().catch(() => ({}))) as {
|
||||
snapshot?: unknown;
|
||||
};
|
||||
const snapshot = normalizeFlowBoardSnapshot(body.snapshot ?? body);
|
||||
const savedDraft = await upsertFlowBoardDraftByUserId({
|
||||
userId: session.user.id,
|
||||
snapshot,
|
||||
});
|
||||
|
||||
if (!savedDraft) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "保存流程白板失败" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
snapshot: savedDraft.snapshot,
|
||||
updatedAt: savedDraft.updatedAt?.toISOString() ?? snapshot.updatedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -69,12 +69,13 @@ export function ChatShell({
|
||||
const pathname = usePathname();
|
||||
const isLovartHome = pathname === "/" && homeVariant === "lovart";
|
||||
const isAiCadWorkspace = pathname === "/ai-cad";
|
||||
const isFlowBoardWorkspace = pathname === "/flow-board";
|
||||
|
||||
if (isLovartHome) {
|
||||
return <div className="h-svh w-full overflow-y-auto">{children}</div>;
|
||||
}
|
||||
|
||||
if (isAiCadWorkspace) {
|
||||
if (isAiCadWorkspace || isFlowBoardWorkspace) {
|
||||
return <div className="h-svh w-full overflow-hidden">{children}</div>;
|
||||
}
|
||||
|
||||
|
||||
268
components/flow-board/drawnix-workspace.tsx
Normal file
268
components/flow-board/drawnix-workspace.tsx
Normal file
@ -0,0 +1,268 @@
|
||||
"use client";
|
||||
|
||||
import { Drawnix, type DrawnixProps } from "@drawnix/drawnix";
|
||||
import type { BoardChangeData } from "@plait-board/react-board";
|
||||
import type {
|
||||
PlaitElement,
|
||||
PlaitTheme,
|
||||
ThemeColorMode,
|
||||
Viewport,
|
||||
} from "@plait/core";
|
||||
import { ChevronLeft, Save } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type FlowBoardWorkspaceProps = {
|
||||
siteName: string;
|
||||
siteLogo: string | null;
|
||||
siteLogoDark: string | null;
|
||||
};
|
||||
|
||||
type FlowBoardSnapshot = {
|
||||
children: PlaitElement[];
|
||||
viewport?: Viewport;
|
||||
theme?: PlaitTheme;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
const FLOW_BOARD_DRAFT_API = "/api/flow-board/draft";
|
||||
|
||||
function normalizeSnapshot(value: unknown): FlowBoardSnapshot {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Partial<FlowBoardSnapshot>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
children: Array.isArray(raw.children) ? raw.children : [],
|
||||
viewport: raw.viewport,
|
||||
theme: raw.theme,
|
||||
updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function formatSavedAt(value: Date | null) {
|
||||
if (!value) return "数据库自动保存";
|
||||
return `已保存 ${value.toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}`;
|
||||
}
|
||||
|
||||
export function DrawnixWorkspace({
|
||||
siteName,
|
||||
siteLogo,
|
||||
siteLogoDark,
|
||||
}: FlowBoardWorkspaceProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [snapshot, setSnapshot] = useState<FlowBoardSnapshot>({
|
||||
children: [],
|
||||
});
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null);
|
||||
const snapshotRef = useRef<FlowBoardSnapshot>({ children: [] });
|
||||
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const displayLogo = useMemo(() => {
|
||||
if (resolvedTheme === "dark" && siteLogoDark) {
|
||||
return siteLogoDark;
|
||||
}
|
||||
return siteLogo || siteLogoDark;
|
||||
}, [resolvedTheme, siteLogo, siteLogoDark]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadDraft = async () => {
|
||||
try {
|
||||
const response = await fetch(FLOW_BOARD_DRAFT_API, {
|
||||
cache: "no-store",
|
||||
credentials: "include",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load flow board draft: ${response.status}`);
|
||||
}
|
||||
const result = (await response.json()) as {
|
||||
data?: { snapshot?: unknown; updatedAt?: string | null };
|
||||
};
|
||||
if (!cancelled) {
|
||||
const nextSnapshot = normalizeSnapshot(result.data?.snapshot);
|
||||
snapshotRef.current = nextSnapshot;
|
||||
setSnapshot(nextSnapshot);
|
||||
setLastSavedAt(
|
||||
result.data?.updatedAt
|
||||
? new Date(result.data.updatedAt)
|
||||
: nextSnapshot.updatedAt
|
||||
? new Date(nextSnapshot.updatedAt)
|
||||
: null
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[FlowBoard] Failed to load draft:", error);
|
||||
toast.error("流程白板草稿加载失败");
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsReady(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void loadDraft();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (saveTimerRef.current) {
|
||||
clearTimeout(saveTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scheduleSave = useCallback((nextSnapshot: FlowBoardSnapshot) => {
|
||||
if (saveTimerRef.current) {
|
||||
clearTimeout(saveTimerRef.current);
|
||||
}
|
||||
|
||||
saveTimerRef.current = setTimeout(async () => {
|
||||
try {
|
||||
const response = await fetch(FLOW_BOARD_DRAFT_API, {
|
||||
body: JSON.stringify({ snapshot: nextSnapshot }),
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "PUT",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save flow board draft: ${response.status}`);
|
||||
}
|
||||
const result = (await response.json()) as {
|
||||
data?: { updatedAt?: string | null };
|
||||
};
|
||||
setLastSavedAt(
|
||||
result.data?.updatedAt
|
||||
? new Date(result.data.updatedAt)
|
||||
: new Date(nextSnapshot.updatedAt || Date.now())
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("[FlowBoard] Failed to save draft:", error);
|
||||
toast.error("流程白板草稿保存到数据库失败");
|
||||
}
|
||||
}, 350);
|
||||
}, []);
|
||||
|
||||
const updateSnapshot = useCallback(
|
||||
(partialSnapshot: Partial<FlowBoardSnapshot>) => {
|
||||
const nextSnapshot = normalizeSnapshot({
|
||||
...snapshotRef.current,
|
||||
...partialSnapshot,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
snapshotRef.current = nextSnapshot;
|
||||
setSnapshot(nextSnapshot);
|
||||
scheduleSave(nextSnapshot);
|
||||
},
|
||||
[scheduleSave]
|
||||
);
|
||||
|
||||
const handleDrawnixChange = useCallback(
|
||||
(value: BoardChangeData) => {
|
||||
const shouldPersist = value.operations.some(
|
||||
(operation) => operation.type !== "set_selection"
|
||||
);
|
||||
if (!shouldPersist) return;
|
||||
|
||||
updateSnapshot({
|
||||
children: value.children,
|
||||
theme: value.theme,
|
||||
viewport: value.viewport,
|
||||
});
|
||||
},
|
||||
[updateSnapshot]
|
||||
);
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(children: PlaitElement[]) => {
|
||||
updateSnapshot({ children });
|
||||
},
|
||||
[updateSnapshot]
|
||||
);
|
||||
|
||||
const handleViewportChange = useCallback(
|
||||
(viewport: Viewport) => {
|
||||
updateSnapshot({ viewport });
|
||||
},
|
||||
[updateSnapshot]
|
||||
);
|
||||
|
||||
const handleThemeChange = useCallback(
|
||||
(themeColorMode: ThemeColorMode) => {
|
||||
updateSnapshot({ theme: { themeColorMode } });
|
||||
},
|
||||
[updateSnapshot]
|
||||
);
|
||||
|
||||
if (!isReady) {
|
||||
return (
|
||||
<div className="flex h-svh w-full items-center justify-center bg-[#f7f8fb] text-[#697386]">
|
||||
<div className="rounded-2xl border border-black/10 bg-white px-5 py-3 font-medium text-sm shadow-sm">
|
||||
正在恢复流程白板...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="relative h-svh w-full overflow-hidden bg-[#f7f8fb]">
|
||||
<div className="pointer-events-none absolute top-1 left-3 z-20 flex h-7 items-center gap-1.5">
|
||||
<button
|
||||
className="pointer-events-auto inline-flex h-7 items-center gap-1 rounded-full border border-black/10 bg-white px-2 font-medium text-[#4f566b] text-xs shadow-sm hover:bg-white"
|
||||
onClick={() => {
|
||||
window.location.href = "/";
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
返回
|
||||
</button>
|
||||
|
||||
<div className="pointer-events-auto flex h-7 items-center gap-2 rounded-full border border-black/10 bg-white px-2 shadow-sm">
|
||||
{displayLogo ? (
|
||||
<img
|
||||
alt={siteName}
|
||||
className="h-5 w-5 rounded-full object-cover"
|
||||
src={displayLogo}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-[#111827] font-semibold text-[10px] text-white">
|
||||
{siteName.slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="font-semibold text-[#111827] text-xs">
|
||||
流程白板
|
||||
</span>
|
||||
<span className="max-w-[140px] truncate text-[#7a8297] text-[10px]">
|
||||
{siteName}
|
||||
</span>
|
||||
<span className="mx-0.5 h-3 w-px bg-black/10" />
|
||||
<Save className="h-3 w-3 text-[#7a8297]" />
|
||||
<span className="text-[#7a8297] text-[10px]">
|
||||
{formatSavedAt(lastSavedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Drawnix
|
||||
className="h-full w-full"
|
||||
onChange={handleDrawnixChange as NonNullable<DrawnixProps["onChange"]>}
|
||||
onThemeChange={handleThemeChange}
|
||||
onValueChange={handleValueChange}
|
||||
onViewportChange={handleViewportChange}
|
||||
theme={snapshot.theme}
|
||||
tutorial={false}
|
||||
value={snapshot.children}
|
||||
viewport={snapshot.viewport}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
32
components/flow-board/flow-board-client.tsx
Normal file
32
components/flow-board/flow-board-client.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
type FlowBoardClientProps = {
|
||||
siteName: string;
|
||||
siteLogo: string | null;
|
||||
siteLogoDark: string | null;
|
||||
};
|
||||
|
||||
const DrawnixWorkspace = dynamic(
|
||||
() =>
|
||||
import("@/components/flow-board/drawnix-workspace").then(
|
||||
(mod) => mod.DrawnixWorkspace
|
||||
),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div className="flex h-svh w-full items-center justify-center bg-[#f7f8fb] text-[#697386]">
|
||||
<div className="flex items-center gap-3 rounded-2xl border border-black/10 bg-white px-5 py-3 shadow-sm">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="font-medium text-sm">正在加载流程白板...</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
export function FlowBoardClient(props: FlowBoardClientProps) {
|
||||
return <DrawnixWorkspace {...props} />;
|
||||
}
|
||||
@ -35,6 +35,7 @@ import {
|
||||
User,
|
||||
Users,
|
||||
Wand2,
|
||||
Workflow,
|
||||
X,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
@ -1595,6 +1596,19 @@ export function LovartHome({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">视频画布</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
|
||||
onClick={() => router.push("/flow-board")}
|
||||
title="流程白板"
|
||||
>
|
||||
<Workflow className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">流程白板</TooltipContent>
|
||||
</Tooltip>
|
||||
{siteSettingsData?.productPlanningEnabled !== false ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
Sparkles,
|
||||
Trophy,
|
||||
Wand2,
|
||||
Workflow,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@ -615,6 +616,18 @@ export function TopNavbar({
|
||||
<span className={navLabelClass}>{getShortLabel("视频画布")}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={navItemBaseClass}
|
||||
onClick={() => {
|
||||
window.location.href = "/flow-board";
|
||||
}}
|
||||
title="流程白板"
|
||||
type="button"
|
||||
>
|
||||
<Workflow className="h-5 w-5" />
|
||||
<span className={navLabelClass}>{getShortLabel("流程白板")}</span>
|
||||
</button>
|
||||
|
||||
{/* 无限画布 */}
|
||||
<button
|
||||
className={navItemBaseClass}
|
||||
|
||||
11
lib/db/migrations/0069_add_flow_board_drafts.sql
Normal file
11
lib/db/migrations/0069_add_flow_board_drafts.sql
Normal file
@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS "FlowBoardDraft" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"userId" uuid NOT NULL REFERENCES "User"("id") ON DELETE cascade,
|
||||
"snapshot" jsonb NOT NULL DEFAULT '{"children":[]}'::jsonb,
|
||||
"createdAt" timestamp NOT NULL DEFAULT now(),
|
||||
"updatedAt" timestamp NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "FlowBoardDraft_userId_unique" UNIQUE("userId")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "FlowBoardDraft_user_updated_idx"
|
||||
ON "FlowBoardDraft"("userId", "updatedAt");
|
||||
@ -386,6 +386,13 @@
|
||||
"when": 1775782800000,
|
||||
"tag": "0068_add_agent_psd_render_tasks",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 55,
|
||||
"version": "7",
|
||||
"when": 1775869200000,
|
||||
"tag": "0069_add_flow_board_drafts",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -46,6 +46,9 @@ import {
|
||||
vote,
|
||||
aiCadWorkspace,
|
||||
type AiCadWorkspace,
|
||||
flowBoardDraft,
|
||||
type FlowBoardDraft,
|
||||
type FlowBoardSnapshotData,
|
||||
aiProvider,
|
||||
type AiProvider,
|
||||
aiModel,
|
||||
@ -894,6 +897,59 @@ export async function deleteAiCadWorkspaceById(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFlowBoardDraftByUserId({
|
||||
userId,
|
||||
}: {
|
||||
userId: string;
|
||||
}): Promise<FlowBoardDraft | null> {
|
||||
try {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(flowBoardDraft)
|
||||
.where(eq(flowBoardDraft.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
return result ?? null;
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to get flow board draft"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertFlowBoardDraftByUserId(params: {
|
||||
userId: string;
|
||||
snapshot: FlowBoardSnapshotData;
|
||||
}): Promise<FlowBoardDraft | null> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const [result] = await db
|
||||
.insert(flowBoardDraft)
|
||||
.values({
|
||||
userId: params.userId,
|
||||
snapshot: params.snapshot,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: flowBoardDraft.userId,
|
||||
set: {
|
||||
snapshot: params.snapshot,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
|
||||
return result ?? null;
|
||||
} catch (_error) {
|
||||
throw new ChatSDKError(
|
||||
"bad_request:database",
|
||||
"Failed to save flow board draft"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertConversationWorkingState(params: {
|
||||
userId: string;
|
||||
chatId: string;
|
||||
|
||||
@ -953,6 +953,29 @@ export const aiCadWorkspace = pgTable("AiCadWorkspace", {
|
||||
|
||||
export type AiCadWorkspace = InferSelectModel<typeof aiCadWorkspace>;
|
||||
|
||||
export type FlowBoardSnapshotData = {
|
||||
children: Record<string, unknown>[];
|
||||
viewport?: Record<string, unknown> | null;
|
||||
theme?: Record<string, unknown> | null;
|
||||
updatedAt?: string | null;
|
||||
};
|
||||
|
||||
export const flowBoardDraft = pgTable("FlowBoardDraft", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
userId: uuid("userId")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" })
|
||||
.unique(),
|
||||
snapshot: jsonb("snapshot")
|
||||
.$type<FlowBoardSnapshotData>()
|
||||
.notNull()
|
||||
.default({ children: [] }),
|
||||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type FlowBoardDraft = InferSelectModel<typeof flowBoardDraft>;
|
||||
|
||||
// 每轮消息的目标资产/图片定位审计表
|
||||
export const messageTargetResolution = pgTable("MessageTargetResolution", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
|
||||
14
package.json
14
package.json
@ -32,12 +32,21 @@
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.35.3",
|
||||
"@drawnix/drawnix": "0.4.0-2",
|
||||
"@fal-ai/serverless-client": "^0.15.0",
|
||||
"@icons-pack/react-simple-icons": "^13.7.0",
|
||||
"@langchain/core": "^1.1.19",
|
||||
"@langchain/langgraph": "^1.1.3",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.200.0",
|
||||
"@plait-board/react-board": "0.4.0-2",
|
||||
"@plait-board/react-text": "0.4.0-2",
|
||||
"@plait/common": "^0.92.1",
|
||||
"@plait/core": "^0.92.1",
|
||||
"@plait/draw": "^0.92.1",
|
||||
"@plait/layouts": "^0.92.1",
|
||||
"@plait/mind": "^0.92.1",
|
||||
"@plait/text-plugins": "^0.92.1",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
@ -75,6 +84,7 @@
|
||||
"framer-motion": "^11.18.2",
|
||||
"geist": "^1.3.1",
|
||||
"immer": "^11.1.3",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.25",
|
||||
"lucide-react": "^0.446.0",
|
||||
@ -107,6 +117,10 @@
|
||||
"server-only": "^0.0.1",
|
||||
"sharp": "^0.34.5",
|
||||
"shiki": "^3.12.2",
|
||||
"slate": "^0.124.0",
|
||||
"slate-dom": "^0.124.0",
|
||||
"slate-history": "^0.115.0",
|
||||
"slate-react": "^0.124.0",
|
||||
"sonner": "^1.5.0",
|
||||
"streamdown": "^1.3.0",
|
||||
"swr": "^2.2.5",
|
||||
|
||||
750
pnpm-lock.yaml
generated
750
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4
public/vendor/drawnix.css
vendored
Normal file
4
public/vendor/drawnix.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user