本次同步 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/(内嵌独立仓库,远程亦未收录其源码)
239 lines
7.3 KiB
TypeScript
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>
|
|
);
|
|
}
|