fix(studio): hide sidebar group until token resolves; remove VITE_STUDIO_ENABLED
- workspace-nav-studio: return null while isLoading so the "工作室" group no longer briefly mounts and then unmounts when getTokenByEmail returns "用户不存在" (404). Skeleton branch became dead code, removed. - studio/hooks: switch all three token queries from retry: 1 to false — account-not-found is a permanent error; retry just doubled isError latency. - env: drop VITE_STUDIO_ENABLED everywhere (env.ts + 7 env files + workspace-nav-chat-list). Sidebar availability is now decided at runtime by token success instead of a build-time flag. - env: add VITE_AIPI_PRESENT_URL + VITE_AIPI_KNOWES_URL for the document upload popup's A-system jump + B-system knowledge iframe; reuse the existing VITE_AIPI_KNOWLEDGE_URL for the Y-system iframe. - document-upload-popup: replace 8 hardcoded URLs (6 present, 1 knowes, 1 knowlibrary) with the corresponding env vars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bd0eceda61
commit
4416348c51
13 files changed
+287
-80
No files matched your search
@@ -24,15 +24,16 @@ VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
|
|||||||
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
|
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
|
||||||
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
|
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
|
||||||
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
|
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
|
||||||
VITE_AIPI_BACKENP_URL=http://47.88.25.99:8000
|
VITE_AIPI_BACKENP_URL=http://localhost:8003
|
||||||
# 工作室/笔记本后端接口(侧边栏“工作室”列表、笔记本详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
|
# 工作室/笔记本后端接口(侧边栏“工作室”列表、笔记本详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
|
||||||
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
||||||
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
|
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||||
VITE_NOTEBOOK_API_BASE_URL=http://47.88.25.99:8000
|
VITE_AIPI_FRONTEND_URL=http://localhost:3000
|
||||||
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
|
|
||||||
VITE_STUDIO_ENABLED=true
|
|
||||||
VITE_AIPI_FRONTEND_URL=http://47.88.25.99:3000
|
|
||||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
||||||
|
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
|
||||||
|
VITE_AIPI_PRESENT_URL=https://present.a.uat.4.cn/present/domain
|
||||||
|
# B 系统知识库 iframe 根地址
|
||||||
|
VITE_AIPI_KNOWES_URL=https://ch1.b.uat.4.cn/knowes
|
||||||
|
|
||||||
# MaxKey 鉴权(getTokenByOnlineTicket)
|
# MaxKey 鉴权(getTokenByOnlineTicket)
|
||||||
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
|
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
|
|||||||
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
||||||
VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
|
VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
||||||
|
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
|
||||||
|
VITE_AIPI_PRESENT_URL=http://present.a.uat.4.cn/present/domain
|
||||||
|
# B 系统知识库 iframe 根地址
|
||||||
|
VITE_AIPI_KNOWES_URL=http://ch1.b.uat.4.cn/knowes
|
||||||
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
|
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
|
||||||
VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn
|
VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn
|
||||||
VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn
|
VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn
|
||||||
|
|
||||||
# VITE_NOTE_API_BASE_URL=http://127.0.0.1:8000
|
# VITE_NOTE_API_BASE_URL=http://127.0.0.1:8000
|
||||||
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
|
|
||||||
VITE_STUDIO_ENABLED=true
|
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
# VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
|
# VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
|
||||||
# VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
|
# VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||||
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
|
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||||
# VITE_STUDIO_ENABLED=true # 仅控制“工作室”入口显示,列表内容仍依赖 notebook 接口数据
|
|
||||||
|
|
||||||
# --- 生产打包(见 .env.production)---
|
# --- 生产打包(见 .env.production)---
|
||||||
# 部署前修改 .env.production 中的地址,再执行 pnpm build
|
# 部署前修改 .env.production 中的地址,再执行 pnpm build
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ VITE_AIPI_BACKENP_URL=http://47.88.25.99:8000
|
|||||||
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
||||||
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
|
# VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||||
VITE_NOTEBOOK_API_BASE_URL=http://47.88.25.99:8000
|
VITE_NOTEBOOK_API_BASE_URL=http://47.88.25.99:8000
|
||||||
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
|
|
||||||
VITE_STUDIO_ENABLED=true
|
|
||||||
VITE_AIPI_FRONTEND_URL=http://47.88.25.99:3000
|
VITE_AIPI_FRONTEND_URL=http://47.88.25.99:3000
|
||||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
||||||
|
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
|
||||||
|
VITE_AIPI_PRESENT_URL=http://present.a.uat.4.cn/present/domain
|
||||||
|
# B 系统知识库 iframe 根地址
|
||||||
|
VITE_AIPI_KNOWES_URL=http://ch1.b.uat.4.cn/knowes
|
||||||
|
|
||||||
# MaxKey 鉴权(getTokenByOnlineTicket)
|
# MaxKey 鉴权(getTokenByOnlineTicket)
|
||||||
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
|
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
|
||||||
|
|||||||
@@ -24,17 +24,16 @@ VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
|
|||||||
# 我的笔记前端
|
# 我的笔记前端
|
||||||
VITE_AIPI_FRONTEND_URL= xxx
|
VITE_AIPI_FRONTEND_URL= xxx
|
||||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
||||||
|
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
|
||||||
|
VITE_AIPI_PRESENT_URL=https://present.a.uat.4.cn/present/domain
|
||||||
|
# B 系统知识库 iframe 根地址
|
||||||
|
VITE_AIPI_KNOWES_URL=https://ch1.b.uat.4.cn/knowes
|
||||||
# MaxKey 鉴权(getTokenByOnlineTicket)
|
# MaxKey 鉴权(getTokenByOnlineTicket)
|
||||||
VITE_AUTH_BASE_URL=https://auth.y.uat.4.cn
|
VITE_AUTH_BASE_URL=https://auth.y.uat.4.cn
|
||||||
VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn
|
VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn
|
||||||
VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn
|
VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn
|
||||||
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
||||||
VITE_AIPI_FRONTEND_URL=https://knowledgetoolweb.b.uat.4.cn
|
VITE_AIPI_FRONTEND_URL=https://knowledgetoolweb.b.uat.4.cn
|
||||||
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
|
|
||||||
VITE_STUDIO_ENABLED=true
|
|
||||||
|
|
||||||
# 可选:Canvas 独立 LangGraph 地址
|
# 可选:Canvas 独立 LangGraph 地址
|
||||||
# VITE_CANVAS_LANGGRAPH_BASE_URL=
|
# VITE_CANVAS_LANGGRAPH_BASE_URL=
|
||||||
|
|
||||||
# 我的笔记是否显示,侧边栏
|
|
||||||
VITE_STUDIO_ENABLED=false
|
|
||||||
@@ -5,4 +5,3 @@
|
|||||||
# VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
# VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
||||||
# VITE_AIPI_BACKENP_URL=https://your-notebook-backend-host
|
# VITE_AIPI_BACKENP_URL=https://your-notebook-backend-host
|
||||||
# VITE_AIPI_FRONTEND_URL=https://your-notebook-frontend-host
|
# VITE_AIPI_FRONTEND_URL=https://your-notebook-frontend-host
|
||||||
# VITE_STUDIO_ENABLED=true
|
|
||||||
@@ -22,14 +22,16 @@ VITE_AIPI_MOPECHAT_URL=https://ch1.b.sit.4.cn/likun_b1_mope
|
|||||||
# 工作室/笔记本后端接口(侧边栏“工作室”列表、笔记本详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
|
# 工作室/笔记本后端接口(侧边栏“工作室”列表、笔记本详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
|
||||||
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.sit.4.cn
|
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.sit.4.cn
|
||||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.sit.4.cn/consumer
|
VITE_AIPI_CONSUMER_URL=https://ch1.b.sit.4.cn/consumer
|
||||||
|
# A 系统跳转地址(人物/组织/事件等列表项打开链接)
|
||||||
|
VITE_AIPI_PRESENT_URL=http://present.a.sit.4.cn/present/domain
|
||||||
|
# B 系统知识库 iframe 根地址
|
||||||
|
VITE_AIPI_KNOWES_URL=https://ch1.b.sit.4.cn/knowes
|
||||||
# MaxKey 鉴权(getTokenByOnlineTicket)
|
# MaxKey 鉴权(getTokenByOnlineTicket)
|
||||||
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
|
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
|
||||||
VITE_AIPI_PGLINKTO_URL=https://pg.b.sit.4.cn
|
VITE_AIPI_PGLINKTO_URL=https://pg.b.sit.4.cn
|
||||||
VITE_AIPI_XKLINKTO_URL=https://kz.b.sit.4.cn
|
VITE_AIPI_XKLINKTO_URL=https://kz.b.sit.4.cn
|
||||||
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
||||||
VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.sit.4.cn
|
VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.sit.4.cn
|
||||||
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
|
|
||||||
VITE_STUDIO_ENABLED=true
|
|
||||||
|
|
||||||
# 可选:Canvas 独立 LangGraph 地址
|
# 可选:Canvas 独立 LangGraph 地址
|
||||||
# VITE_CANVAS_LANGGRAPH_BASE_URL=
|
# VITE_CANVAS_LANGGRAPH_BASE_URL=
|
||||||
@@ -50,7 +50,7 @@ function getYUserId(): string {
|
|||||||
const obj = JSON.parse(raw) as { yUserId?: string };
|
const obj = JSON.parse(raw) as { yUserId?: string };
|
||||||
if (obj?.yUserId) return obj.yUserId;
|
if (obj?.yUserId) return obj.yUserId;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch { }
|
||||||
return "1157761008018653184";
|
return "1157761008018653184";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +72,23 @@ interface DocumentUploadDialogContextType {
|
|||||||
mode?: "search",
|
mode?: "search",
|
||||||
data?: { query: string; mode: string }
|
data?: { query: string; mode: string }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
// 上传成功回调,返回上传的来源信息
|
||||||
|
onUploadComplete?: (sources: Array<{ id?: number; title: string; task_id?: string; status: string }>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 任务轮询类型 ─────────────────────────────────────────────
|
||||||
|
interface TaskSource {
|
||||||
|
task_id: string;
|
||||||
|
title: string;
|
||||||
|
status: 'processing' | 'success' | 'error';
|
||||||
|
source_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UploadResponse {
|
||||||
|
status: string;
|
||||||
|
uploaded: number;
|
||||||
|
skipped: number;
|
||||||
|
sources: TaskSource[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface listDataItem {
|
interface listDataItem {
|
||||||
@@ -83,6 +100,10 @@ interface listDataItem {
|
|||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
// 上传文件轮询相关字段
|
||||||
|
task_id?: string;
|
||||||
|
processing_status?: 'processing' | 'success' | 'error';
|
||||||
|
is_uploaded_file?: boolean;
|
||||||
country?: string;
|
country?: string;
|
||||||
dateOfBirth?: string;
|
dateOfBirth?: string;
|
||||||
occupationData?: string;
|
occupationData?: string;
|
||||||
@@ -188,9 +209,10 @@ export const DocumentUploadDialogProvider: FC<{
|
|||||||
mode?: "search",
|
mode?: "search",
|
||||||
data?: { query: string; mode: string }
|
data?: { query: string; mode: string }
|
||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
searchSpace: { id: number | string; [key: string]: unknown };
|
searchSpace: { id: number | string;[key: string]: unknown };
|
||||||
onUploadSuccess?: () => void;
|
onUploadSuccess?: () => void;
|
||||||
}> = ({ children, onClose, searchSpace, onUploadSuccess }) => {
|
onUploadComplete?: (sources: Array<{ id?: number; title: string; task_id?: string; status: string }>) => void;
|
||||||
|
}> = ({ children, onClose, searchSpace, onUploadSuccess, onUploadComplete }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [initialSearchText, setInitialSearchText] = useState("");
|
const [initialSearchText, setInitialSearchText] = useState("");
|
||||||
const isClosingRef = useRef(false);
|
const isClosingRef = useRef(false);
|
||||||
@@ -236,7 +258,7 @@ export const DocumentUploadDialogProvider: FC<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentUploadDialogContext.Provider value={{ openDialog, closeDialog }}>
|
<DocumentUploadDialogContext.Provider value={{ openDialog, closeDialog, onUploadComplete }}>
|
||||||
{children}
|
{children}
|
||||||
<DocumentUploadPopupContent
|
<DocumentUploadPopupContent
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@@ -257,7 +279,7 @@ const UPLOAD_ACCEPT =
|
|||||||
const DocumentUploadPopupContent: FC<{
|
const DocumentUploadPopupContent: FC<{
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
searchSpace: { id: number | string; [key: string]: unknown };
|
searchSpace: { id: number | string;[key: string]: unknown };
|
||||||
onUploadSuccess?: () => void;
|
onUploadSuccess?: () => void;
|
||||||
closeDialog: (
|
closeDialog: (
|
||||||
mode?: "search",
|
mode?: "search",
|
||||||
@@ -265,12 +287,15 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
initialSearchText?: string;
|
initialSearchText?: string;
|
||||||
}> = ({ isOpen, onOpenChange, searchSpace, onUploadSuccess, initialSearchText }) => {
|
}> = ({ isOpen, onOpenChange, searchSpace, onUploadSuccess, initialSearchText }) => {
|
||||||
|
const { onUploadComplete } = useContext(DocumentUploadDialogContext) || {};
|
||||||
const searchSpaceId = String(searchSpace?.id);
|
const searchSpaceId = String(searchSpace?.id);
|
||||||
const backendUrl =
|
|
||||||
typeof window !== "undefined"
|
// 从环境变量获取配置
|
||||||
? (import.meta.env.VITE_BACKEND_BASE_URL || "http://localhost:8001")
|
const backendUrl = import.meta.env.VITE_AIPI_BACKENP_URL || "http://localhost:8001";
|
||||||
: "http://localhost:8001";
|
const consumerUrl = import.meta.env.VITE_AIPI_CONSUMER_URL || "http://ch1.b.uat.4.cn/consumer";
|
||||||
const consumerUrl = "http://ch1.b.uat.4.cn/consumer";
|
const presentUrl = import.meta.env.VITE_AIPI_PRESENT_URL || "http://present.a.uat.4.cn/present/domain";
|
||||||
|
const knowesUrl = import.meta.env.VITE_AIPI_KNOWES_URL || "http://ch1.b.uat.4.cn/knowes";
|
||||||
|
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL || "http://chlib.b.uat.4.cn/knowlibrary";
|
||||||
|
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
@@ -321,8 +346,19 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success("上传成功");
|
const data = await res.json() as UploadResponse;
|
||||||
handleSuccess();
|
// 调用上传完成回调,传递来源信息
|
||||||
|
if (data.sources && data.sources.length > 0) {
|
||||||
|
onUploadComplete?.(data.sources.map(s => ({
|
||||||
|
id: undefined, // 后端返回的数据中暂时没有 id
|
||||||
|
title: s.title,
|
||||||
|
task_id: s.task_id,
|
||||||
|
status: s.status,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
toast.success(`上传成功 ${data.uploaded || files.length} 个文件`);
|
||||||
|
// 只关闭上传弹窗
|
||||||
|
onOpenChange(false);
|
||||||
} else {
|
} else {
|
||||||
toast.error("上传失败,请重试");
|
toast.error("上传失败,请重试");
|
||||||
}
|
}
|
||||||
@@ -414,7 +450,7 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
item.title = item.name || "";
|
item.title = item.name || "";
|
||||||
item.content = item.abstractz || "";
|
item.content = item.abstractz || "";
|
||||||
const redirect = urlSafeBtoa(`/gene/#/human/basic?mid=${item.id}`);
|
const redirect = urlSafeBtoa(`/gene/#/human/basic?mid=${item.id}`);
|
||||||
item.url = `http://present.a.uat.4.cn/present/domain?inredirect=${redirect}`;
|
item.url = `${presentUrl}?inredirect=${redirect}`;
|
||||||
});
|
});
|
||||||
setListData(result);
|
setListData(result);
|
||||||
}
|
}
|
||||||
@@ -437,7 +473,7 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
item.title = item.name || "";
|
item.title = item.name || "";
|
||||||
item.content = item.abstractz || "";
|
item.content = item.abstractz || "";
|
||||||
const redirect = urlSafeBtoa(`/gene/#/organization/basicAttr?mid=${item.mongoId}`);
|
const redirect = urlSafeBtoa(`/gene/#/organization/basicAttr?mid=${item.mongoId}`);
|
||||||
item.url = `http://present.a.uat.4.cn/present/domain?inredirect=${redirect}`;
|
item.url = `${presentUrl}?inredirect=${redirect}`;
|
||||||
});
|
});
|
||||||
setListData(result);
|
setListData(result);
|
||||||
}
|
}
|
||||||
@@ -461,7 +497,7 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
item.title = item.community_name || "";
|
item.title = item.community_name || "";
|
||||||
item.content = item.community_description || "";
|
item.content = item.community_description || "";
|
||||||
const redirect = urlSafeBtoa(`/gene/#/commune/basicCommune?cid=${item.id}`);
|
const redirect = urlSafeBtoa(`/gene/#/commune/basicCommune?cid=${item.id}`);
|
||||||
item.url = `http://present.a.uat.4.cn/present/domain?inredirect=${redirect}`;
|
item.url = `${presentUrl}?inredirect=${redirect}`;
|
||||||
});
|
});
|
||||||
setListData(result);
|
setListData(result);
|
||||||
}
|
}
|
||||||
@@ -483,7 +519,7 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
item.title = item.issue_name || "";
|
item.title = item.issue_name || "";
|
||||||
item.content = item.issue_type || "";
|
item.content = item.issue_type || "";
|
||||||
const redirect = encodeURIComponent(`/gene/#/issue/zzyyIssue?mid=${item.id}&event_type=issue`);
|
const redirect = encodeURIComponent(`/gene/#/issue/zzyyIssue?mid=${item.id}&event_type=issue`);
|
||||||
item.url = `http://present.a.uat.4.cn/present/domain?inredirect=${redirect}`;
|
item.url = `${presentUrl}?inredirect=${redirect}`;
|
||||||
});
|
});
|
||||||
setListData(result);
|
setListData(result);
|
||||||
}
|
}
|
||||||
@@ -504,7 +540,7 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
item.checked = false;
|
item.checked = false;
|
||||||
const redirect = urlSafeBtoa(`/gene/#/event/basicEvent?event_type=hot&mid=${item.id}`);
|
const redirect = urlSafeBtoa(`/gene/#/event/basicEvent?event_type=hot&mid=${item.id}`);
|
||||||
item.url = `http://present.a.uat.4.cn/present/domain?inredirect=${redirect}`;
|
item.url = `${presentUrl}?inredirect=${redirect}`;
|
||||||
});
|
});
|
||||||
setListData(result);
|
setListData(result);
|
||||||
}
|
}
|
||||||
@@ -526,7 +562,7 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
results.forEach((item) => {
|
results.forEach((item) => {
|
||||||
item.checked = false;
|
item.checked = false;
|
||||||
const redirect = urlSafeBtoa(`/gene/#/event/basicEvent?event_type=hot&mid=${item.id}`);
|
const redirect = urlSafeBtoa(`/gene/#/event/basicEvent?event_type=hot&mid=${item.id}`);
|
||||||
item.url = `http://present.a.uat.4.cn/present/domain?inredirect=${redirect}`;
|
item.url = `${presentUrl}?inredirect=${redirect}`;
|
||||||
});
|
});
|
||||||
result = [...result, ...results];
|
result = [...result, ...results];
|
||||||
}
|
}
|
||||||
@@ -614,9 +650,8 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
onClick={() => void searchActiveChange(key)}
|
onClick={() => void searchActiveChange(key)}
|
||||||
className={`relative pb-3 px-6 pt-4 text-sm font-medium transition-colors ${
|
className={`relative pb-3 px-6 pt-4 text-sm font-medium transition-colors ${activeTab === key ? "text-gray-900" : "text-gray-400 hover:text-gray-600"
|
||||||
activeTab === key ? "text-gray-900" : "text-gray-400 hover:text-gray-600"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{activeTab === key && (
|
{activeTab === key && (
|
||||||
@@ -632,7 +667,7 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
{activeTab === "search" && !isSearching && (
|
{activeTab === "search" && !isSearching && (
|
||||||
<iframe
|
<iframe
|
||||||
className="w-full h-[calc(100vh-200px)]"
|
className="w-full h-[calc(100vh-200px)]"
|
||||||
src={`http://ch1.b.uat.4.cn/knowes/fromB1?yUserId=${yUserId}&processType=searchMode&bookChatId=${searchSpaceId}&inputValue=${searchValue}`}
|
src={`${knowesUrl}/fromB1?yUserId=${yUserId}&processType=searchMode&bookChatId=${searchSpaceId}&inputValue=${searchValue}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -640,7 +675,7 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
{activeTab === "Y" && !isSearching && (
|
{activeTab === "Y" && !isSearching && (
|
||||||
<iframe
|
<iframe
|
||||||
className="w-full h-[calc(100vh-200px)]"
|
className="w-full h-[calc(100vh-200px)]"
|
||||||
src={`http://chlib.b.uat.4.cn/knowlibrary/#/sousou?chatType=new-chat&bookChatId=${searchSpaceId}&inputValue=${searchText}`}
|
src={`${knowledgeUrl}/#/sousou?chatType=new-chat&bookChatId=${searchSpaceId}&inputValue=${searchText}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -686,6 +721,23 @@ const DocumentUploadPopupContent: FC<{
|
|||||||
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
<div className="bg-white p-[10px] grid grid-cols-5 gap-[10px]">
|
||||||
{listData.map((item, index) => (
|
{listData.map((item, index) => (
|
||||||
<div key={index} className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
|
<div key={index} className="relative flex items-center shadow-[0px_0px_2px_1px_#ededed] mb-[10px] p-[10px] rounded-[5px]">
|
||||||
|
{/* 上传文件显示 loading 状态 */}
|
||||||
|
{item.is_uploaded_file && item.processing_status === 'processing' && (
|
||||||
|
<div className="absolute inset-0 bg-white/80 flex items-center justify-center z-10">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* 上传文件完成状态标记 */}
|
||||||
|
{item.is_uploaded_file && item.processing_status === 'error' && (
|
||||||
|
<div className="absolute top-1 left-1">
|
||||||
|
<span className="text-xs text-red-500">处理失败</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{item.is_uploaded_file && item.processing_status === 'success' && (
|
||||||
|
<div className="absolute top-1 left-1">
|
||||||
|
<span className="text-xs text-green-500">✓ 已完成</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="w-[50px] mr-[10px]">
|
<div className="w-[50px] mr-[10px]">
|
||||||
<img src={item.imagePath || ""} className="w-[50px] h-[50px]" alt="" />
|
<img src={item.imagePath || ""} className="w-[50px] h-[50px]" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ import { BotIcon, BrainIcon, ListChecks, MessageSquarePlus, MessagesSquare, PenL
|
|||||||
import { SidebarCollapseButton } from "./workspace-header";
|
import { SidebarCollapseButton } from "./workspace-header";
|
||||||
import { WorkspaceNavStudio } from "./workspace-nav-studio";
|
import { WorkspaceNavStudio } from "./workspace-nav-studio";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { env } from "@/env";
|
|
||||||
|
|
||||||
const studioEnabled = env.STUDIO_ENABLED?.toLowerCase() === "true";
|
|
||||||
|
|
||||||
// 普通导航项:默认中性色,激活态与「新对话」一致——主色浅底 + 主色文字/图标
|
// 普通导航项:默认中性色,激活态与「新对话」一致——主色浅底 + 主色文字/图标
|
||||||
const NAV_ITEM_CLASS =
|
const NAV_ITEM_CLASS =
|
||||||
@@ -168,7 +165,7 @@ export function WorkspaceNavChatList({
|
|||||||
)}
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
{!showCollapseTrigger && studioEnabled && <WorkspaceNavStudio />}
|
{!showCollapseTrigger && <WorkspaceNavStudio />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,8 @@ export function WorkspaceNavStudio() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isError || (!isLoading && notebooks.length === 0)) {
|
// 加载中、token 失败、空列表都不渲染整段,避免在 token 未确认前先闪一段工作室 UI 再消失。
|
||||||
|
if (isLoading || isError || notebooks.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,32 +103,26 @@ export function WorkspaceNavStudio() {
|
|||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|
||||||
{/* 笔记本列表(最多展示 SIDEBAR_PREVIEW_COUNT 条) */}
|
{/* 笔记本列表(最多展示 SIDEBAR_PREVIEW_COUNT 条) */}
|
||||||
{isLoading
|
{preview.map((nb) => (
|
||||||
? Array.from({ length: 3 }).map((_, i) => (
|
<SidebarMenuItem key={nb.id}>
|
||||||
<SidebarMenuItem key={i}>
|
<SidebarMenuButton
|
||||||
<div className="mx-3 my-1 h-5 animate-pulse rounded-md bg-sidebar-accent/40" />
|
asChild
|
||||||
</SidebarMenuItem>
|
isActive={pathname === `/page/workspace/studio/notebooks/${nb.id}`}
|
||||||
))
|
className={cn(NAV_ITEM_CLASS)}
|
||||||
: preview.map((nb) => (
|
>
|
||||||
<SidebarMenuItem key={nb.id}>
|
<Link
|
||||||
<SidebarMenuButton
|
to={`/page/workspace/studio/notebooks/${nb.id}`}
|
||||||
asChild
|
title={nb.description || nb.name}
|
||||||
isActive={pathname === `/page/workspace/studio/notebooks/${nb.id}`}
|
>
|
||||||
className={cn(NAV_ITEM_CLASS)}
|
<BookOpen className="shrink-0 size-4" />
|
||||||
>
|
<span className="truncate">{nb.name}</span>
|
||||||
<Link
|
</Link>
|
||||||
to={`/page/workspace/studio/notebooks/${nb.id}`}
|
</SidebarMenuButton>
|
||||||
title={nb.description || nb.name}
|
</SidebarMenuItem>
|
||||||
>
|
))}
|
||||||
<BookOpen className="shrink-0 size-4" />
|
|
||||||
<span className="truncate">{nb.name}</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* 所有笔记本 */}
|
{/* 所有笔记本 */}
|
||||||
{!isLoading && notebooks.length > 0 && (
|
{notebooks.length > 0 && (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export function useStudioNotebooks(enabled = true): {
|
|||||||
queryFn: () => fetchStudioToken(email),
|
queryFn: () => fetchStudioToken(email),
|
||||||
enabled: enabled && Boolean(email),
|
enabled: enabled && Boolean(email),
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
retry: 1,
|
// 账号不存在(404/422)是永久错误,重试无意义,立即让 isError 落地
|
||||||
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notebooksQuery = useQuery({
|
const notebooksQuery = useQuery({
|
||||||
@@ -71,7 +72,7 @@ export function useNotebookChat(
|
|||||||
queryFn: () => fetchStudioToken(email),
|
queryFn: () => fetchStudioToken(email),
|
||||||
enabled: Boolean(email),
|
enabled: Boolean(email),
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
retry: 1,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const thread = useStream<AgentThreadState>({
|
const thread = useStream<AgentThreadState>({
|
||||||
@@ -137,7 +138,7 @@ export function useNotebookSources(
|
|||||||
queryFn: () => fetchStudioToken(email),
|
queryFn: () => fetchStudioToken(email),
|
||||||
enabled: !overrideToken && Boolean(email),
|
enabled: !overrideToken && Boolean(email),
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
retry: 1,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedToken = overrideToken ?? tokenQuery.data;
|
const resolvedToken = overrideToken ?? tokenQuery.data;
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ function readEnv(
|
|||||||
| "VITE_AIPI_BACKENP_URL"
|
| "VITE_AIPI_BACKENP_URL"
|
||||||
| "VITE_AIPI_FRONTEND_URL"
|
| "VITE_AIPI_FRONTEND_URL"
|
||||||
| "VITE_STATIC_WEBSITE_ONLY"
|
| "VITE_STATIC_WEBSITE_ONLY"
|
||||||
| "VITE_GITHUB_OAUTH_TOKEN"
|
| "VITE_GITHUB_OAUTH_TOKEN",
|
||||||
| "VITE_STUDIO_ENABLED",
|
|
||||||
): EnvValue {
|
): EnvValue {
|
||||||
const value = import.meta.env[key];
|
const value = import.meta.env[key];
|
||||||
return typeof value === "string" && value.trim() ? value : undefined;
|
return typeof value === "string" && value.trim() ? value : undefined;
|
||||||
@@ -26,5 +25,4 @@ export const env = {
|
|||||||
NEXT_PUBLIC_NOTEBOOK_API_BASE_URL: readEnv("VITE_AIPI_BACKENP_URL"),
|
NEXT_PUBLIC_NOTEBOOK_API_BASE_URL: readEnv("VITE_AIPI_BACKENP_URL"),
|
||||||
NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readEnv("VITE_STATIC_WEBSITE_ONLY"),
|
NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readEnv("VITE_STATIC_WEBSITE_ONLY"),
|
||||||
GITHUB_OAUTH_TOKEN: readEnv("VITE_GITHUB_OAUTH_TOKEN"),
|
GITHUB_OAUTH_TOKEN: readEnv("VITE_GITHUB_OAUTH_TOKEN"),
|
||||||
STUDIO_ENABLED: readEnv("VITE_STUDIO_ENABLED"),
|
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
@@ -63,10 +63,12 @@ function SourceCard({
|
|||||||
item,
|
item,
|
||||||
selected,
|
selected,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
item: SourceItem;
|
item: SourceItem;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onToggle: (id: number) => void;
|
onToggle: (id: number) => void;
|
||||||
|
onDelete: (id: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const title = item.title ?? item.document?.title ?? "未命名";
|
const title = item.title ?? item.document?.title ?? "未命名";
|
||||||
const isProcessing = item.status === "processing";
|
const isProcessing = item.status === "processing";
|
||||||
@@ -81,6 +83,18 @@ function SourceCard({
|
|||||||
: "bg-muted ring-border/50 hover:bg-muted/70"
|
: "bg-muted ring-border/50 hover:bg-muted/70"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{/* 删除按钮 - 右上角始终显示,悬停时高亮 */}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDelete(item.id);
|
||||||
|
}}
|
||||||
|
className="absolute top-1.5 right-1.5 z-20 rounded-md bg-background/80 backdrop-blur p-1.5 text-muted-foreground opacity-60 shadow-sm transition-all hover:bg-destructive hover:text-white group-hover:opacity-100"
|
||||||
|
title="删除来源"
|
||||||
|
>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="flex size-10 items-center justify-center rounded-xl bg-background">
|
<div className="flex size-10 items-center justify-center rounded-xl bg-background">
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||||
@@ -89,7 +103,7 @@ function SourceCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="truncate text-xs font-medium text-foreground">{title}</p>
|
<p className="truncate text-xs font-medium text-foreground">{title}</p>
|
||||||
<div className="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 hidden w-max max-w-[200px] -translate-x-1/2 rounded-lg bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md ring-1 ring-border group-hover:block">
|
<div className="pointer-events-none absolute top-full left-1/2 z-10 mt-2 hidden w-max max-w-[200px] -translate-x-1/2 rounded-lg bg-popover px-3 py-2 text-xs text-popover-foreground shadow-md ring-1 ring-border group-hover:block">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,6 +129,7 @@ function SourcesDialog({
|
|||||||
isLoadingSources,
|
isLoadingSources,
|
||||||
selectedSourceIds,
|
selectedSourceIds,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -123,6 +138,7 @@ function SourcesDialog({
|
|||||||
isLoadingSources: boolean;
|
isLoadingSources: boolean;
|
||||||
selectedSourceIds: number[];
|
selectedSourceIds: number[];
|
||||||
onToggle: (id: number) => void;
|
onToggle: (id: number) => void;
|
||||||
|
onDelete: (id: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const { openDialog: openUploadPopup } = useDocumentUploadDialog();
|
const { openDialog: openUploadPopup } = useDocumentUploadDialog();
|
||||||
|
|
||||||
@@ -170,6 +186,7 @@ function SourcesDialog({
|
|||||||
item={src}
|
item={src}
|
||||||
selected={selectedSourceIds.includes(src.id)}
|
selected={selectedSourceIds.includes(src.id)}
|
||||||
onToggle={onToggle}
|
onToggle={onToggle}
|
||||||
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -201,6 +218,9 @@ export default function StudioNotebookDetailPage() {
|
|||||||
const THREADS_LIMIT = 5;
|
const THREADS_LIMIT = 5;
|
||||||
const [citationDoc, setCitationDoc] = useState<DocumentWithChunks | null>(null);
|
const [citationDoc, setCitationDoc] = useState<DocumentWithChunks | null>(null);
|
||||||
const [citationLoading, setCitationLoading] = useState(false);
|
const [citationLoading, setCitationLoading] = useState(false);
|
||||||
|
// 轮询任务管理:task_id -> 来源信息
|
||||||
|
const [pendingSources, setPendingSources] = useState<Map<string, { id?: number; title: string }>>(new Map());
|
||||||
|
const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedModel && models.length > 0) {
|
if (!selectedModel && models.length > 0) {
|
||||||
@@ -239,6 +259,29 @@ export default function StudioNotebookDetailPage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteSource = async (sourceId: number) => {
|
||||||
|
if (!notebook) return;
|
||||||
|
try {
|
||||||
|
const backendUrl = import.meta.env.VITE_AIPI_BACKENP_URL || "http://localhost:8001";
|
||||||
|
const response = await fetch(`${backendUrl}/api/v1/source/${sourceId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Authorization: `Bearer ${studioToken ?? ""}` },
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
// 刷新来源列表
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "sources"] });
|
||||||
|
// 刷新 notebooks 数据以更新 source_count
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "notebooks", studioToken] });
|
||||||
|
// 从选中列表中移除
|
||||||
|
setSelectedSourceIds((prev) => prev.filter((id) => id !== sourceId));
|
||||||
|
} else {
|
||||||
|
console.error("删除来源失败");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("删除来源失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSend = async (e: React.SyntheticEvent) => {
|
const handleSend = async (e: React.SyntheticEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const text = input.trim();
|
const text = input.trim();
|
||||||
@@ -254,6 +297,117 @@ export default function StudioNotebookDetailPage() {
|
|||||||
void refetchThreads();
|
void refetchThreads();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 上传完成回调
|
||||||
|
const handleUploadComplete = (uploadedSources: Array<{ id?: number; title: string; task_id?: string; status: string }>) => {
|
||||||
|
if (!notebook) return;
|
||||||
|
// 将正在处理的来源添加到轮询队列
|
||||||
|
const processing = uploadedSources.filter(s => s.status === 'processing' && s.task_id);
|
||||||
|
if (processing.length > 0) {
|
||||||
|
setPendingSources((prev) => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
processing.forEach(s => {
|
||||||
|
if (s.task_id) {
|
||||||
|
newMap.set(s.task_id, { id: s.id, title: s.title });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 刷新来源列表
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "sources", notebook.id] });
|
||||||
|
// 刷新 notebooks 数据以更新 source_count
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "notebooks", studioToken] });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 轮询单个来源任务状态
|
||||||
|
const pollSourceStatus = useCallback(async (taskId: string) => {
|
||||||
|
if (!notebook) return;
|
||||||
|
try {
|
||||||
|
const backendUrl = import.meta.env.VITE_AIPI_BACKENP_URL || "http://localhost:8001";
|
||||||
|
const token = getStoredAuth()?.access_token ?? "";
|
||||||
|
const response = await fetch(`${backendUrl}/api/v1/task/${taskId}/status`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json() as { status: 'processing' | 'success' | 'failed' | 'error' };
|
||||||
|
const taskStatus = data.status === 'failed' ? 'error' : data.status;
|
||||||
|
if (taskStatus !== 'processing') {
|
||||||
|
// 任务完成,从轮询队列移除并刷新来源列表
|
||||||
|
setPendingSources((prev) => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
const source = newMap.get(taskId);
|
||||||
|
newMap.delete(taskId);
|
||||||
|
if (source) {
|
||||||
|
if (taskStatus === 'success') {
|
||||||
|
// toast.success(`来源 "${source.title}" 处理完成`);
|
||||||
|
} else {
|
||||||
|
// toast.error(`来源 "${source.title}" 处理失败`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
// 刷新来源列表
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "sources"] });
|
||||||
|
// 刷新 notebooks 数据以更新 source_count
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "notebooks", studioToken] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`轮询任务 ${taskId} 状态失败:`, error);
|
||||||
|
}
|
||||||
|
}, [notebook?.id, queryClient]);
|
||||||
|
|
||||||
|
// 控制轮询 - 只在对话框打开时轮询
|
||||||
|
useEffect(() => {
|
||||||
|
// 对话框关闭时,停止轮询(但保留 pendingSources 数据)
|
||||||
|
if (!sourcesOpen) {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话框打开时,刷新来源列表获取最新状态
|
||||||
|
if (sourcesOpen && notebook) {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "sources", notebook.id] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话框打开且有待处理任务时,启动轮询
|
||||||
|
if (pendingSources.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pollingIntervalRef.current) return;
|
||||||
|
|
||||||
|
// 立即检查一次
|
||||||
|
pendingSources.forEach((_, taskId) => {
|
||||||
|
void pollSourceStatus(taskId);
|
||||||
|
});
|
||||||
|
// 设置定时轮询
|
||||||
|
pollingIntervalRef.current = setInterval(() => {
|
||||||
|
setPendingSources((current) => {
|
||||||
|
if (current.size === 0) {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
current.forEach((_, taskId) => {
|
||||||
|
void pollSourceStatus(taskId);
|
||||||
|
});
|
||||||
|
return current;
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (pollingIntervalRef.current) {
|
||||||
|
clearInterval(pollingIntervalRef.current);
|
||||||
|
pollingIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [sourcesOpen, pendingSources, pollSourceStatus, notebook, queryClient]);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-6 bg-background px-8">
|
<div className="flex h-full flex-col items-center justify-center gap-6 bg-background px-8">
|
||||||
@@ -283,8 +437,10 @@ export default function StudioNotebookDetailPage() {
|
|||||||
<DocumentUploadDialogProvider
|
<DocumentUploadDialogProvider
|
||||||
searchSpace={{ id: notebook.id }}
|
searchSpace={{ id: notebook.id }}
|
||||||
onUploadSuccess={() => {
|
onUploadSuccess={() => {
|
||||||
void queryClient.invalidateQueries({ queryKey: ["studio", "sources", notebook.id] });
|
void queryClient.invalidateQueries({ queryKey: ["studio", "sources"] });
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "notebooks", studioToken] });
|
||||||
}}
|
}}
|
||||||
|
onUploadComplete={handleUploadComplete}
|
||||||
>
|
>
|
||||||
<ThreadContext.Provider value={{ thread }}>
|
<ThreadContext.Provider value={{ thread }}>
|
||||||
<ChatBox threadId={chatThreadId ?? ""}>
|
<ChatBox threadId={chatThreadId ?? ""}>
|
||||||
@@ -381,7 +537,10 @@ export default function StudioNotebookDetailPage() {
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSourcesOpen(true)}
|
onClick={() => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ["studio", "sources"] });
|
||||||
|
setSourcesOpen(true);
|
||||||
|
}}
|
||||||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<Plus className="size-5" />
|
<Plus className="size-5" />
|
||||||
@@ -579,6 +738,7 @@ export default function StudioNotebookDetailPage() {
|
|||||||
isLoadingSources={isLoadingSources}
|
isLoadingSources={isLoadingSources}
|
||||||
selectedSourceIds={selectedSourceIds}
|
selectedSourceIds={selectedSourceIds}
|
||||||
onToggle={handleToggleSource}
|
onToggle={handleToggleSource}
|
||||||
|
onDelete={handleDeleteSource}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ChatBox>
|
</ChatBox>
|
||||||
|
|||||||
Reference in new issue
Block a user