Files
deepdata/deep-flow/web/src/app/chat/components/research-block.tsx
T
沃巴蒂 ff22ed3a8b feat(deepdata): 引入修改密码、文件预览组件与清洗算子增强,deepflow 改名为 deep-flow
本次同步 09-12 至 09-28 的代码变更,主要内容:

新增功能
- 修改密码:后端新增 ChangePasswordRequest DTO 与UserController/UserService
  对应接口,前端新增 ChangePasswordDialog 并在 Header 接入入口
- 文件预览组件重构:file-preview 目录拆分出 VideoPreview(支持 blobUrl 直传)、
  ImagePreview、AudioPreview,index.tsx 统一调度各类型预览器
- 新增 my_ops 算子模块:markitdown_parser、mineru_pdf_parser、text_quality_evaluator
- 新增 tools/minio-file-server MinIO 文件服务

功能增强
- 数据清洗:cleaning_task_service/scheduler/routes 重构,
  data_juicer_executor 与 wrapper 增强,OperatorTable/LogsTable/TaskDetail 同步调整
- 数据标注:annotation sync.py、project.py 接口与schema 更新
- 数据采集、算子市场、首页与菜单路由相关页面调整
- DataJuicer 算子接入(useOperatorOperations、dj_ops_schema)

结构调整
- deepflow/ 更名为 deep-flow/(419 个文件重命名)
- 新增 helm 部署编排、GitHub Actions 之外的自建镜像构建脚本

未纳入本次提交
- scripts-backup/、runtime/python-executor-backup/、components_backup/(备份目录)
- *.backup*、__pycache__ 产物
- zh_core_web_sm-3.8.0-py3-none-any.whl(46MB 构建产物)
- DataX/(内嵌独立仓库,远程亦未收录其源码)
2026-10-03 22:44:56 +08:00

239 lines
7.3 KiB
TypeScript

// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
import { Check, Copy, Headphones, Pencil, Undo2, X, Download } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react";
import { ScrollContainer } from "~/components/deep-flow/scroll-container";
import { Tooltip } from "~/components/deep-flow/tooltip";
import { Button } from "~/components/ui/button";
import { Card } from "~/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { useReplay } from "~/core/replay";
import { closeResearch, listenToPodcast, useStore } from "~/core/store";
import { cn } from "~/lib/utils";
import { ResearchActivitiesBlock } from "./research-activities-block";
import { ResearchReportBlock } from "./research-report-block";
export function ResearchBlock({
className,
researchId = null,
}: {
className?: string;
researchId: string | null;
}) {
const t = useTranslations("chat.research");
const reportId = useStore((state) =>
researchId ? state.researchReportIds.get(researchId) : undefined,
);
const [activeTab, setActiveTab] = useState("activities");
const hasReport = useStore((state) =>
researchId ? state.researchReportIds.has(researchId) : false,
);
const reportStreaming = useStore((state) =>
reportId ? (state.messages.get(reportId)?.isStreaming ?? false) : false,
);
const { isReplay } = useReplay();
useEffect(() => {
if (hasReport) {
setActiveTab("report");
}
}, [hasReport]);
const handleGeneratePodcast = useCallback(async () => {
if (!researchId) {
return;
}
await listenToPodcast(researchId);
}, [researchId]);
const [editing, setEditing] = useState(false);
const [copied, setCopied] = useState(false);
const handleCopy = useCallback(() => {
if (!reportId) {
return;
}
const report = useStore.getState().messages.get(reportId);
if (!report) {
return;
}
void navigator.clipboard.writeText(report.content);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1000);
}, [reportId]);
// Download report as markdown
const handleDownload = useCallback(() => {
if (!reportId) {
return;
}
const report = useStore.getState().messages.get(reportId);
if (!report) {
return;
}
const now = new Date();
const pad = (n: number) => n.toString().padStart(2, '0');
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
const filename = `research-report-${timestamp}.md`;
const blob = new Blob([report.content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
try {
if (a.parentNode) {
a.parentNode.removeChild(a);
}
} finally {
URL.revokeObjectURL(url);
}
}, 0);
}, [reportId]);
const handleEdit = useCallback(() => {
setEditing((editing) => !editing);
}, []);
// When the research id changes, set the active tab to activities
useEffect(() => {
if (!hasReport) {
setActiveTab("activities");
}
}, [hasReport, researchId]);
return (
<div className={cn("h-full w-full", className)}>
<Card className={cn("relative h-full w-full pt-4", className)}>
<div className="absolute right-4 flex h-9 items-center justify-center">
{hasReport && !reportStreaming && (
<>
<Tooltip title={t("generatePodcast")}>
<Button
className="text-gray-400"
size="icon"
variant="ghost"
disabled={isReplay}
onClick={handleGeneratePodcast}
>
<Headphones />
</Button>
</Tooltip>
<Tooltip title={t("edit")}>
<Button
className="text-gray-400"
size="icon"
variant="ghost"
disabled={isReplay}
onClick={handleEdit}
>
{editing ? <Undo2 /> : <Pencil />}
</Button>
</Tooltip>
<Tooltip title={t("copy")}>
<Button
className="text-gray-400"
size="icon"
variant="ghost"
onClick={handleCopy}
>
{copied ? <Check /> : <Copy />}
</Button>
</Tooltip>
<Tooltip title={t("downloadReport")}>
<Button
className="text-gray-400"
size="icon"
variant="ghost"
onClick={handleDownload}
>
<Download />
</Button>
</Tooltip>
</>
)}
<Tooltip title={t("close")}>
<Button
className="text-gray-400"
size="sm"
variant="ghost"
onClick={() => {
closeResearch();
}}
>
<X />
</Button>
</Tooltip>
</div>
<Tabs
className="flex h-full w-full flex-col"
value={activeTab}
onValueChange={(value) => setActiveTab(value)}
>
<div className="flex w-full justify-center">
<TabsList className="">
<TabsTrigger
className="px-8"
value="report"
disabled={!hasReport}
>
{t("report")}
</TabsTrigger>
<TabsTrigger className="px-8" value="activities">
{t("activities")}
</TabsTrigger>
</TabsList>
</div>
<TabsContent
className="h-full min-h-0 flex-grow px-8"
value="report"
forceMount
hidden={activeTab !== "report"}
>
<ScrollContainer
className="px-5pb-20 h-full"
scrollShadowColor="var(--card)"
autoScrollToBottom={!hasReport || reportStreaming}
>
{reportId && researchId && (
<ResearchReportBlock
className="mt-4"
researchId={researchId}
messageId={reportId}
editing={editing}
/>
)}
</ScrollContainer>
</TabsContent>
<TabsContent
className="h-full min-h-0 flex-grow px-8"
value="activities"
forceMount
hidden={activeTab !== "activities"}
>
<ScrollContainer
className="h-full"
scrollShadowColor="var(--card)"
autoScrollToBottom={!hasReport || reportStreaming}
>
{researchId && (
<ResearchActivitiesBlock
className="mt-4"
researchId={researchId}
/>
)}
</ScrollContainer>
</TabsContent>
</Tabs>
</Card>
</div>
);
}