0.1.0
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { createSupabaseRouteClient } from "@/lib/supabase/server";
|
||||
import { blocksToPlainText, normalizeBlocksFromContent } from "@/lib/documents/plain-text";
|
||||
|
||||
interface SavePayload {
|
||||
documentId: string;
|
||||
@@ -17,10 +18,17 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const { documentId, content }: SavePayload = await request.json();
|
||||
const blocks = normalizeBlocksFromContent(content);
|
||||
const rawText = blocksToPlainText(blocks);
|
||||
|
||||
const updatePayload: Record<string, unknown> = { content, index_status: "pending" };
|
||||
if (rawText) {
|
||||
updatePayload.raw_text = rawText;
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("documents")
|
||||
.update({ content })
|
||||
.update(updatePayload)
|
||||
.eq("id", documentId)
|
||||
.eq("user_id", session.user.id);
|
||||
|
||||
@@ -28,5 +36,19 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||
}
|
||||
|
||||
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL;
|
||||
if (backendUrl && rawText) {
|
||||
void fetch(`${backendUrl}/api/v1/lightrag/index`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
body: JSON.stringify({ document_id: documentId }),
|
||||
}).catch((err) => {
|
||||
console.warn("[lightrag-index] trigger failed", err);
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -90,3 +90,26 @@ export async function POST(request: Request) {
|
||||
|
||||
return NextResponse.json({ asset: data as MediaAsset });
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
const supabase = await createSupabaseRouteClient();
|
||||
const {
|
||||
data: { session },
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { assetId } = (await request.json().catch(() => ({}))) as { assetId?: string };
|
||||
if (!assetId) {
|
||||
return NextResponse.json({ error: "缺少 assetId" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { error } = await supabase.from("media_assets").delete().eq("id", assetId).limit(1);
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export async function POST(request: Request) {
|
||||
if (!assetId) {
|
||||
return NextResponse.json({ error: "缺少 assetId" }, { status: 400 });
|
||||
}
|
||||
console.log("media ocr api called", assetId, Boolean(session?.access_token));
|
||||
|
||||
const { error } = await supabase
|
||||
.from("media_assets")
|
||||
@@ -28,10 +29,13 @@ export async function POST(request: Request) {
|
||||
|
||||
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL;
|
||||
if (backendUrl) {
|
||||
console.log("media ocr session token prefix", session.access_token?.slice(0, 8));
|
||||
void fetch(`${backendUrl}/api/v1/tasks/media-ocr`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
"X-Supabase-Access-Token": session.access_token,
|
||||
},
|
||||
body: JSON.stringify({ asset_id: assetId }),
|
||||
}).catch((err) => {
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSessionContext } from "@supabase/auth-helpers-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSessionContext, useSupabaseClient } from "@supabase/auth-helpers-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import type { Database } from "@/types/supabase";
|
||||
|
||||
type BackgroundTaskRow = Database["public"]["Tables"]["background_tasks"]["Row"];
|
||||
|
||||
interface TaskResponse {
|
||||
task_id: string;
|
||||
status: string;
|
||||
progress: number;
|
||||
message?: string | null;
|
||||
task_type?: string | null;
|
||||
created_at?: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -18,12 +24,62 @@ interface Props {
|
||||
|
||||
export function DocumentTaskPanel({ documentId }: Props) {
|
||||
const { session } = useSessionContext();
|
||||
const [task, setTask] = useState<TaskResponse | null>(null);
|
||||
const supabase = useSupabaseClient<Database>();
|
||||
const [activeTask, setActiveTask] = useState<TaskResponse | null>(null);
|
||||
const [recentTasks, setRecentTasks] = useState<TaskResponse[]>([]);
|
||||
const [pending, setPending] = useState(false);
|
||||
const [fileUrl, setFileUrl] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const backendUrl = useMemo(() => process.env.NEXT_PUBLIC_BACKEND_URL, []);
|
||||
|
||||
const toTaskResponse = useCallback((record: BackgroundTaskRow | null): TaskResponse | null => {
|
||||
if (!record) return null;
|
||||
return {
|
||||
task_id: String(record.id ?? ""),
|
||||
status: record.status ?? "pending",
|
||||
progress: record.progress ?? 0,
|
||||
message: record.message,
|
||||
task_type: record.task_type ?? "ocr",
|
||||
created_at: record.created_at,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const upsertTask = useCallback(
|
||||
(record: BackgroundTaskRow | null) => {
|
||||
const parsed = toTaskResponse(record);
|
||||
if (!parsed) return;
|
||||
setRecentTasks((prev) => {
|
||||
const next = prev.filter((item) => item.task_id !== parsed.task_id);
|
||||
next.unshift(parsed);
|
||||
return next.slice(0, 5);
|
||||
});
|
||||
setActiveTask((current) => {
|
||||
if (current?.task_id === parsed.task_id) {
|
||||
return parsed;
|
||||
}
|
||||
return current;
|
||||
});
|
||||
},
|
||||
[toTaskResponse],
|
||||
);
|
||||
|
||||
const fetchRecentTasks = useCallback(async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("background_tasks")
|
||||
.select("*")
|
||||
.eq("document_id", documentId)
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(5);
|
||||
if (error) {
|
||||
console.warn("加载任务失败", error);
|
||||
return;
|
||||
}
|
||||
const next = (data ?? []).map((row) => toTaskResponse(row)).filter((row): row is TaskResponse => Boolean(row));
|
||||
setRecentTasks(next);
|
||||
}, [documentId, supabase, toTaskResponse]);
|
||||
|
||||
const triggerTask = async () => {
|
||||
if (!backendUrl || !session?.access_token) return;
|
||||
if (!backendUrl || !session?.access_token || !fileUrl) return;
|
||||
setPending(true);
|
||||
try {
|
||||
const response = await fetch(`${backendUrl}/api/v1/tasks/ocr`, {
|
||||
@@ -34,12 +90,23 @@ export function DocumentTaskPanel({ documentId }: Props) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
document_id: documentId,
|
||||
file_url: "https://example.com/sample.pdf",
|
||||
file_url: fileUrl,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
setTask(data);
|
||||
const nextTask: TaskResponse = {
|
||||
task_id: data.task_id,
|
||||
status: data.status,
|
||||
progress: data.progress,
|
||||
message: data.message,
|
||||
task_type: "ocr",
|
||||
};
|
||||
setActiveTask(nextTask);
|
||||
setRecentTasks((prev) => [nextTask, ...prev.filter((item) => item.task_id !== nextTask.task_id)].slice(0, 5));
|
||||
setErrorMessage(null);
|
||||
} else {
|
||||
setErrorMessage(data?.detail ?? data?.error ?? "触发 OCR 失败");
|
||||
}
|
||||
} finally {
|
||||
setPending(false);
|
||||
@@ -47,40 +114,76 @@ export function DocumentTaskPanel({ documentId }: Props) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!backendUrl || !session?.access_token || !task?.task_id) {
|
||||
return;
|
||||
}
|
||||
const timer = setInterval(async () => {
|
||||
const response = await fetch(`${backendUrl}/api/v1/tasks/${task.task_id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
void fetchRecentTasks();
|
||||
}, [fetchRecentTasks]);
|
||||
|
||||
useEffect(() => {
|
||||
const channel = supabase
|
||||
.channel(`bg-task-doc-${documentId}`)
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{ event: "*", schema: "public", table: "background_tasks", filter: `document_id=eq.${documentId}` },
|
||||
(payload) => {
|
||||
upsertTask((payload.new as BackgroundTaskRow) ?? null);
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
const data = (await response.json()) as TaskResponse;
|
||||
setTask(data);
|
||||
if (data.status === "completed") {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 2000);
|
||||
return () => clearInterval(timer);
|
||||
}, [backendUrl, session?.access_token, task?.task_id]);
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
void supabase.removeChannel(channel);
|
||||
};
|
||||
}, [documentId, supabase, upsertTask]);
|
||||
|
||||
const taskTypeLabel = (taskType?: string | null) => {
|
||||
if (taskType === "index") return "LightRAG 索引";
|
||||
if (taskType === "ocr") return "OCR 解析";
|
||||
return taskType ?? "任务";
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mt-4 bg-white shadow-sm">
|
||||
<CardContent className="flex items-center justify-between py-3 text-sm text-gray-600">
|
||||
<CardContent className="flex flex-col gap-4 py-3 text-sm text-gray-600">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">后端 OCR 流程</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
状态:{task ? task.status : "未开始"} · 进度:{task ? `${task.progress}%` : "0%"}
|
||||
<div className="font-medium text-gray-900">最近任务</div>
|
||||
<div className="mt-2 space-y-2 text-xs text-gray-500">
|
||||
{recentTasks.length === 0 ? (
|
||||
<div>暂无任务记录</div>
|
||||
) : (
|
||||
recentTasks.map((task) => (
|
||||
<div
|
||||
key={task.task_id}
|
||||
className="rounded border border-gray-100 bg-gray-50 px-3 py-2 text-gray-700 dark:bg-gray-900/30"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold text-gray-900">{taskTypeLabel(task.task_type)}</span>
|
||||
<span>{task.progress}%</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">状态:{task.status}</div>
|
||||
{task.message && <div className="text-xs text-gray-500">提示:{task.message}</div>}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{task?.message && <div className="text-xs text-gray-500">提示:{task.message}</div>}
|
||||
</div>
|
||||
<Button onClick={triggerTask} disabled={pending} variant="outline">
|
||||
{pending ? "触发中..." : "触发 OCR"}
|
||||
</Button>
|
||||
<div className="flex items-center justify-between gap-6 text-sm text-gray-600">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="font-medium text-gray-900">手动触发 OCR</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
状态:{activeTask ? activeTask.status : "未开始"} · 进度:{activeTask ? `${activeTask.progress}%` : "0%"}
|
||||
</div>
|
||||
{activeTask?.message && <div className="text-xs text-gray-500">提示:{activeTask.message}</div>}
|
||||
<Input
|
||||
placeholder="输入 Supabase Storage 签名 URL"
|
||||
value={fileUrl}
|
||||
onChange={(event) => setFileUrl(event.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
{errorMessage && <div className="text-xs text-red-500">{errorMessage}</div>}
|
||||
</div>
|
||||
<Button onClick={triggerTask} disabled={pending || !fileUrl} variant="outline">
|
||||
{pending ? "触发中..." : "触发 OCR"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import "@blocknote/core/style.css";
|
||||
import "@blocknote/react/style.css";
|
||||
import "@blocknote/mantine/style.css";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { BlockNoteView } from "@blocknote/mantine";
|
||||
import {
|
||||
SideMenuController,
|
||||
@@ -212,7 +212,9 @@ export function BlockNoteEditor({
|
||||
const [tocEntries, setTocEntries] = useState<TocEntry[]>([]);
|
||||
const [fullScreenTableId, setFullScreenTableId] = useState<string | null>(null);
|
||||
const isFullScreenTableOpen = fullScreenTableId !== null;
|
||||
|
||||
const mediaAssetIdsRef = useRef<Set<string>>(new Set());
|
||||
const mediaAssetIdsInitializedRef = useRef(false);
|
||||
|
||||
const openReferencePalette = useSearchPaletteStore((state) => state.openReference);
|
||||
const registerEditorBridge = useEditorBridgeStore((state) => state.registerBridge);
|
||||
|
||||
@@ -290,6 +292,28 @@ export function BlockNoteEditor({
|
||||
const blocks = editor.topLevelBlocks;
|
||||
debouncedSave(blocks as Json);
|
||||
const typedBlocks = blocks as Block<CustomBlockSchema>[];
|
||||
const currentAssetIds = collectMediaAssetIds(typedBlocks);
|
||||
if (mediaAssetIdsInitializedRef.current) {
|
||||
const removedAssetIds: string[] = [];
|
||||
mediaAssetIdsRef.current.forEach((id) => {
|
||||
if (!currentAssetIds.has(id)) {
|
||||
removedAssetIds.push(id);
|
||||
}
|
||||
});
|
||||
if (removedAssetIds.length > 0) {
|
||||
void Promise.allSettled(
|
||||
removedAssetIds.map((assetId) =>
|
||||
fetch("/api/media/assets", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ assetId }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
mediaAssetIdsRef.current = currentAssetIds;
|
||||
mediaAssetIdsInitializedRef.current = true;
|
||||
setTocEntries(buildHeadingToc(typedBlocks));
|
||||
syncProgressMeters(editor);
|
||||
const stats = computeDocumentStats(typedBlocks);
|
||||
@@ -399,6 +423,25 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
};
|
||||
};
|
||||
|
||||
const collectMediaAssetIds = (blocks: Block<CustomBlockSchema>[]): Set<string> => {
|
||||
const ids = new Set<string>();
|
||||
const traverse = (targetBlocks: Block<CustomBlockSchema>[]) => {
|
||||
targetBlocks.forEach((block) => {
|
||||
if (block.type === "media") {
|
||||
const assetId = block.props.assetId as string | undefined;
|
||||
if (assetId) {
|
||||
ids.add(assetId);
|
||||
}
|
||||
}
|
||||
if (block.children && block.children.length > 0) {
|
||||
traverse(block.children as Block<CustomBlockSchema>[]);
|
||||
}
|
||||
});
|
||||
};
|
||||
traverse(blocks);
|
||||
return ids;
|
||||
};
|
||||
|
||||
const insertMediaAssetBlock = useCallback(
|
||||
(asset: MediaAsset) => {
|
||||
if (!editor) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import type { CustomBlockSchema } from "../schema";
|
||||
import { supabaseBrowser } from "@/lib/supabase/client";
|
||||
|
||||
type MediaAlign = "left" | "center" | "right";
|
||||
|
||||
@@ -260,6 +261,47 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 监听 media_assets 变化,实时同步 OCR 状态
|
||||
const assetId = block.props.assetId as string | undefined;
|
||||
if (!assetId) return undefined;
|
||||
|
||||
let active = true;
|
||||
|
||||
const syncStatus = async () => {
|
||||
const { data } = await supabaseBrowser
|
||||
.from("media_assets")
|
||||
.select("ocr_status")
|
||||
.eq("id", assetId)
|
||||
.maybeSingle();
|
||||
if (!active || !data?.ocr_status) return;
|
||||
if (data.ocr_status !== block.props.ocrStatus) {
|
||||
editor.updateBlock(block, { props: { ocrStatus: data.ocr_status } });
|
||||
}
|
||||
};
|
||||
|
||||
void syncStatus();
|
||||
|
||||
const channel = supabaseBrowser
|
||||
.channel(`media-asset-${assetId}`)
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{ event: "*", schema: "public", table: "media_assets", filter: `id=eq.${assetId}` },
|
||||
(payload) => {
|
||||
const nextStatus = (payload.new as { ocr_status?: string } | null)?.ocr_status;
|
||||
if (nextStatus && nextStatus !== block.props.ocrStatus) {
|
||||
editor.updateBlock(block, { props: { ocrStatus: nextStatus } });
|
||||
}
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
void supabaseBrowser.removeChannel(channel);
|
||||
};
|
||||
}, [block, editor]);
|
||||
|
||||
if (!fileUrl) {
|
||||
return (
|
||||
<div className="wolai-media wolai-media--empty">
|
||||
@@ -460,7 +502,9 @@ const figure = (
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="wolai-media__hint">{block.props.ocrStatus === "processing" ? "OCR 处理中..." : ""}</div>
|
||||
<div className="wolai-media__hint">
|
||||
{block.props.ocrStatus === "processing" ? "OCR 处理中..." : block.props.ocrStatus === "completed" ? "OCR 已完成" : ""}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -73,6 +73,16 @@ const CustomDragHandleMenu = ({ block, currentDocumentId }: CustomDragProps) =>
|
||||
void removePageReference();
|
||||
return;
|
||||
}
|
||||
if (block.type === "media") {
|
||||
const assetId = block.props.assetId as string | undefined;
|
||||
if (assetId) {
|
||||
void fetch("/api/media/assets", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ assetId }),
|
||||
}).catch((error) => console.warn("删除媒体资源失败", error));
|
||||
}
|
||||
}
|
||||
if (block.type === "onlineTable") {
|
||||
const tableId = block.props.tableId as string | undefined;
|
||||
if (tableId) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Json } from "@/types/supabase";
|
||||
|
||||
type BlockNode = {
|
||||
id?: string;
|
||||
type?: string;
|
||||
content?: unknown;
|
||||
children?: unknown;
|
||||
props?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const toBlockArray = (value: unknown): BlockNode[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter((item): item is BlockNode => typeof item === "object" && item !== null);
|
||||
}
|
||||
if (value && typeof value === "object" && Array.isArray((value as Record<string, unknown>).blocks)) {
|
||||
return (value as { blocks: Json[] }).blocks.filter(
|
||||
(item): item is BlockNode => typeof item === "object" && item !== null,
|
||||
);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const extractInlineText = (value: unknown): string => {
|
||||
if (!Array.isArray(value)) {
|
||||
return "";
|
||||
}
|
||||
return value
|
||||
.map((node) => {
|
||||
if (!node || typeof node !== "object") {
|
||||
return "";
|
||||
}
|
||||
const candidate = node as { text?: unknown; content?: unknown; children?: unknown };
|
||||
if (typeof candidate.text === "string") {
|
||||
return candidate.text;
|
||||
}
|
||||
if (Array.isArray(candidate.children)) {
|
||||
return extractInlineText(candidate.children);
|
||||
}
|
||||
if (Array.isArray(candidate.content)) {
|
||||
return extractInlineText(candidate.content);
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join(" ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
};
|
||||
|
||||
const flattenBlocks = (blocks: BlockNode[], lines: string[]) => {
|
||||
blocks.forEach((block) => {
|
||||
const inlineText = extractInlineText(block.content);
|
||||
if (inlineText) {
|
||||
lines.push(inlineText);
|
||||
}
|
||||
if (Array.isArray(block.children)) {
|
||||
const nextChildren = block.children.filter(
|
||||
(item): item is BlockNode => typeof item === "object" && item !== null,
|
||||
);
|
||||
if (nextChildren.length > 0) {
|
||||
flattenBlocks(nextChildren, lines);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const normalizeBlocksFromContent = (content: unknown): BlockNode[] => {
|
||||
return toBlockArray(content);
|
||||
};
|
||||
|
||||
export const blocksToPlainText = (blocks: BlockNode[]): string => {
|
||||
const lines: string[] = [];
|
||||
flattenBlocks(blocks, lines);
|
||||
return lines.join("\n").trim();
|
||||
};
|
||||
@@ -9,6 +9,49 @@ export type Json =
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
background_tasks: {
|
||||
Row: {
|
||||
id: string;
|
||||
user_id: string;
|
||||
document_id: string | null;
|
||||
task_type: string;
|
||||
status: string;
|
||||
progress: number;
|
||||
message: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
Insert: {
|
||||
id?: string;
|
||||
user_id: string;
|
||||
document_id?: string | null;
|
||||
task_type?: string;
|
||||
status?: string;
|
||||
progress?: number;
|
||||
message?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
Update: {
|
||||
id?: string;
|
||||
user_id?: string;
|
||||
document_id?: string | null;
|
||||
task_type?: string;
|
||||
status?: string;
|
||||
progress?: number;
|
||||
message?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "background_tasks_document_id_fkey";
|
||||
columns: ["document_id"];
|
||||
referencedRelation: "documents";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
];
|
||||
};
|
||||
documents: {
|
||||
Row: {
|
||||
access_scope: "private" | "shared" | "public";
|
||||
|
||||
Reference in New Issue
Block a user