refactor: 不使用硬编码,而全部使用i18n
This commit is contained in:
parent
3d5006af48
commit
9758ae8a3a
|
|
@ -94,7 +94,7 @@ export default function ChatPage() {
|
||||||
const currentSlogan = motivationSlogans[
|
const currentSlogan = motivationSlogans[
|
||||||
sloganIndex % motivationSlogans.length
|
sloganIndex % motivationSlogans.length
|
||||||
] ?? {
|
] ?? {
|
||||||
text: "来,一起学习工作吧",
|
text: t.chatPage.defaultSlogan,
|
||||||
color: "#333333",
|
color: "#333333",
|
||||||
};
|
};
|
||||||
const tickerCharacterList = useMemo(() => {
|
const tickerCharacterList = useMemo(() => {
|
||||||
|
|
@ -133,7 +133,7 @@ export default function ChatPage() {
|
||||||
if (!safeThreadId) {
|
if (!safeThreadId) {
|
||||||
if (!warnedMissingThreadIdRef.current) {
|
if (!warnedMissingThreadIdRef.current) {
|
||||||
warnedMissingThreadIdRef.current = true;
|
warnedMissingThreadIdRef.current = true;
|
||||||
toast.error("缺少 thread_id,无法创建会话");
|
toast.error(t.chatPage.missingThreadIdForCreate);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -156,9 +156,15 @@ export default function ChatPage() {
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
initializedThreadRef.current = null;
|
initializedThreadRef.current = null;
|
||||||
toast.error("会话创建失败,请稍后重试");
|
toast.error(t.chatPage.createSessionFailed);
|
||||||
});
|
});
|
||||||
}, [apiClient, isNewThread, safeThreadId]);
|
}, [
|
||||||
|
apiClient,
|
||||||
|
isNewThread,
|
||||||
|
safeThreadId,
|
||||||
|
t.chatPage.createSessionFailed,
|
||||||
|
t.chatPage.missingThreadIdForCreate,
|
||||||
|
]);
|
||||||
|
|
||||||
// 监听宿主页 selectedSkill 消息
|
// 监听宿主页 selectedSkill 消息
|
||||||
const {
|
const {
|
||||||
|
|
@ -183,7 +189,7 @@ export default function ChatPage() {
|
||||||
},
|
},
|
||||||
onFinish: (state) => {
|
onFinish: (state) => {
|
||||||
if (document.hidden || !document.hasFocus()) {
|
if (document.hidden || !document.hasFocus()) {
|
||||||
let body = "Conversation finished";
|
let body = t.chatPage.conversationFinished;
|
||||||
const lastMessage = state.messages.at(-1);
|
const lastMessage = state.messages.at(-1);
|
||||||
if (lastMessage) {
|
if (lastMessage) {
|
||||||
const textContent = textOfMessage(lastMessage);
|
const textContent = textOfMessage(lastMessage);
|
||||||
|
|
@ -235,12 +241,13 @@ export default function ChatPage() {
|
||||||
? thread.values.title
|
? thread.values.title
|
||||||
: t.pages.untitled;
|
: t.pages.untitled;
|
||||||
if (thread.isThreadLoading) {
|
if (thread.isThreadLoading) {
|
||||||
document.title = `Loading... - ${t.pages.appName}`;
|
document.title = `${t.common.loading} - ${t.pages.appName}`;
|
||||||
} else {
|
} else {
|
||||||
document.title = `${pageTitle} - ${t.pages.appName}`;
|
document.title = `${pageTitle} - ${t.pages.appName}`;
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isNewThread,
|
isNewThread,
|
||||||
|
t.common.loading,
|
||||||
t.pages.newChat,
|
t.pages.newChat,
|
||||||
t.pages.untitled,
|
t.pages.untitled,
|
||||||
t.pages.appName,
|
t.pages.appName,
|
||||||
|
|
@ -283,7 +290,7 @@ export default function ChatPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isNewThread && !safeThreadId) {
|
if (isNewThread && !safeThreadId) {
|
||||||
toast.error("缺少 thread_id,无法发送消息");
|
toast.error(t.chatPage.missingThreadIdForSend);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setHasSubmitted(true);
|
setHasSubmitted(true);
|
||||||
|
|
@ -299,6 +306,7 @@ export default function ChatPage() {
|
||||||
safeThreadId,
|
safeThreadId,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
showWelcomeStyle,
|
showWelcomeStyle,
|
||||||
|
t.chatPage.missingThreadIdForSend,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
|
|
@ -407,7 +415,7 @@ export default function ChatPage() {
|
||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
{artifacts?.length > 0 && !artifactsOpen && (
|
{artifacts?.length > 0 && !artifactsOpen && (
|
||||||
<Tooltip content="点击可查看生成的文件结果">
|
<Tooltip content={t.chatPage.viewArtifactsTooltip}>
|
||||||
<Button
|
<Button
|
||||||
data-testid="artifacts-open-button"
|
data-testid="artifacts-open-button"
|
||||||
className="text-[#150033] hover:text-[#150033]/80"
|
className="text-[#150033] hover:text-[#150033]/80"
|
||||||
|
|
@ -487,8 +495,8 @@ export default function ChatPage() {
|
||||||
{thread.values.artifacts?.length === 0 ? (
|
{thread.values.artifacts?.length === 0 ? (
|
||||||
<ConversationEmptyState
|
<ConversationEmptyState
|
||||||
icon={<FilesIcon />}
|
icon={<FilesIcon />}
|
||||||
title="No artifact selected"
|
title={t.chatPage.noArtifactSelectedTitle}
|
||||||
description="Select an artifact to view its details"
|
description={t.chatPage.noArtifactSelectedDescription}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center">
|
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center">
|
||||||
|
|
@ -594,10 +602,10 @@ export default function ChatPage() {
|
||||||
<DevDialog open={showExitDialog} onOpenChange={setShowExitDialog}>
|
<DevDialog open={showExitDialog} onOpenChange={setShowExitDialog}>
|
||||||
<DevDialogContent>
|
<DevDialogContent>
|
||||||
<DevDialogHeader>
|
<DevDialogHeader>
|
||||||
<DevDialogTitle>提示</DevDialogTitle>
|
<DevDialogTitle>{t.chatPage.exitDialogTitle}</DevDialogTitle>
|
||||||
</DevDialogHeader>
|
</DevDialogHeader>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
历史记录每七天自动删除,现在将返回欢迎页,是否继续?
|
{t.chatPage.exitDialogDescription}
|
||||||
</p>
|
</p>
|
||||||
<DevDialogFooter>
|
<DevDialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -605,7 +613,7 @@ export default function ChatPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setShowExitDialog(false)}
|
onClick={() => setShowExitDialog(false)}
|
||||||
>
|
>
|
||||||
取消
|
{t.common.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="w-full bg-[#f9f8fa] hover:bg-[#8E47F0] hover:text-white"
|
className="w-full bg-[#f9f8fa] hover:bg-[#8E47F0] hover:text-white"
|
||||||
|
|
@ -631,7 +639,7 @@ export default function ChatPage() {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
确定
|
{t.chatPage.exitDialogConfirm}
|
||||||
</Button>
|
</Button>
|
||||||
</DevDialogFooter>
|
</DevDialogFooter>
|
||||||
</DevDialogContent>
|
</DevDialogContent>
|
||||||
|
|
@ -647,11 +655,11 @@ export default function ChatPage() {
|
||||||
<DevDialogContent>
|
<DevDialogContent>
|
||||||
<DevDialogHeader>
|
<DevDialogHeader>
|
||||||
<DevDialogTitle>
|
<DevDialogTitle>
|
||||||
⚠️ {selectedSkillError?.title ?? "技能加载失败"}
|
⚠️ {selectedSkillError?.title ?? t.chatPage.selectedSkillLoadFailed}
|
||||||
</DevDialogTitle>
|
</DevDialogTitle>
|
||||||
</DevDialogHeader>
|
</DevDialogHeader>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
{selectedSkillError?.message ?? "发生了未知错误,请稍后重试。"}
|
{selectedSkillError?.message ?? t.chatPage.unknownErrorRetry}
|
||||||
</p>
|
</p>
|
||||||
<DevDialogFooter singleColumn>
|
<DevDialogFooter singleColumn>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -659,7 +667,7 @@ export default function ChatPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={clearSelectedSkillError}
|
onClick={clearSelectedSkillError}
|
||||||
>
|
>
|
||||||
关闭
|
{t.common.close}
|
||||||
</Button>
|
</Button>
|
||||||
</DevDialogFooter>
|
</DevDialogFooter>
|
||||||
</DevDialogContent>
|
</DevDialogContent>
|
||||||
|
|
|
||||||
|
|
@ -1153,19 +1153,19 @@ export const PromptInputSubmit = ({
|
||||||
|
|
||||||
let Icon = <ArrowUpIcon className="size-4" />;
|
let Icon = <ArrowUpIcon className="size-4" />;
|
||||||
|
|
||||||
let text: string = "发送";
|
let text: string = t.inputBox.submit;
|
||||||
|
|
||||||
if (status === "submitted") {
|
if (status === "submitted") {
|
||||||
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
Icon = <Loader2Icon className="size-4 animate-spin" />;
|
||||||
text = "生成中...";
|
text = t.inputBox.submitting;
|
||||||
} else if (status === "streaming") {
|
} else if (status === "streaming") {
|
||||||
Icon = <SquareIcon className="size-4" />;
|
Icon = <SquareIcon className="size-4" />;
|
||||||
text = "停止";
|
text = t.inputBox.stop;
|
||||||
} else if (status === "error") {
|
} else if (status === "error") {
|
||||||
// 没有报错状态,先用error状态代替
|
// 没有报错状态,先用error状态代替
|
||||||
Icon = <XIcon className="size-4" />;
|
Icon = <XIcon className="size-4" />;
|
||||||
// MARK: 这里后端没有返回错误信息,先写死一个文本
|
// MARK: 这里后端没有返回错误信息,先写死一个文本
|
||||||
text = "发送";
|
text = t.inputBox.submit;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -210,8 +210,18 @@ export function ArtifactFileDetail({
|
||||||
artifactUrl,
|
artifactUrl,
|
||||||
fileName,
|
fileName,
|
||||||
kind: artifactPreviewKind,
|
kind: artifactPreviewKind,
|
||||||
|
pdfPreviewMessage: t.artifactPreview.pdfPreviewFailed,
|
||||||
|
unsupportedTypeMessage: t.artifactPreview.unsupportedType,
|
||||||
|
openInNewTabLabel: t.artifactPreview.openInNewTab,
|
||||||
});
|
});
|
||||||
}, [artifactUrl, fileName, artifactPreviewKind]);
|
}, [
|
||||||
|
artifactUrl,
|
||||||
|
fileName,
|
||||||
|
artifactPreviewKind,
|
||||||
|
t.artifactPreview.openInNewTab,
|
||||||
|
t.artifactPreview.pdfPreviewFailed,
|
||||||
|
t.artifactPreview.unsupportedType,
|
||||||
|
]);
|
||||||
// Native PDF iframe rendering is intentionally disabled; PDFs are rendered via pdf.js.
|
// Native PDF iframe rendering is intentionally disabled; PDFs are rendered via pdf.js.
|
||||||
const artifactViewerSrc = useMemo(() => {
|
const artifactViewerSrc = useMemo(() => {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -954,6 +964,7 @@ function ArtifactPdfPreview({
|
||||||
artifactUrl: string;
|
artifactUrl: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [pageCount, setPageCount] = useState(0);
|
const [pageCount, setPageCount] = useState(0);
|
||||||
|
|
@ -1033,7 +1044,7 @@ function ArtifactPdfPreview({
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to render pdf preview:", err);
|
console.error("Failed to render pdf preview:", err);
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
setError("无法预览该 PDF 文件,请下载后查看。");
|
setError(t.artifactPreview.pdfPreviewFailed);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
|
|
@ -1047,7 +1058,7 @@ function ArtifactPdfPreview({
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
};
|
};
|
||||||
}, [artifactUrl]);
|
}, [artifactUrl, t.artifactPreview.pdfPreviewFailed]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1061,7 +1072,7 @@ function ArtifactPdfPreview({
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
在新标签页打开
|
{t.artifactPreview.openInNewTab}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1071,7 +1082,9 @@ function ArtifactPdfPreview({
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative overflow-auto bg-[#f8f9fb] p-4", className)}>
|
<div className={cn("relative overflow-auto bg-[#f8f9fb] p-4", className)}>
|
||||||
<div className="mb-3 text-center text-xs text-[#667085]">
|
<div className="mb-3 text-center text-xs text-[#667085]">
|
||||||
{pageCount > 0 ? `${fileName} · ${pageCount} page(s)` : fileName}
|
{pageCount > 0
|
||||||
|
? t.artifactPreview.pageCountLabel(fileName, pageCount)
|
||||||
|
: fileName}
|
||||||
</div>
|
</div>
|
||||||
<div ref={containerRef} />
|
<div ref={containerRef} />
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
|
@ -1094,6 +1107,7 @@ function ArtifactOfficePreview({
|
||||||
artifactUrl: string;
|
artifactUrl: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [sheetNames, setSheetNames] = useState<string[]>([]);
|
const [sheetNames, setSheetNames] = useState<string[]>([]);
|
||||||
|
|
@ -1138,7 +1152,7 @@ function ArtifactOfficePreview({
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to render docx preview:", err);
|
console.error("Failed to render docx preview:", err);
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
setError("无法预览该 DOCX 文件。");
|
setError(t.artifactPreview.docxPreviewFailed);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
|
|
@ -1151,7 +1165,7 @@ function ArtifactOfficePreview({
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
};
|
};
|
||||||
}, [artifactUrl, canRenderDocx]);
|
}, [artifactUrl, canRenderDocx, t.artifactPreview.docxPreviewFailed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
|
|
@ -1186,7 +1200,7 @@ function ArtifactOfficePreview({
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to render xlsx preview:", err);
|
console.error("Failed to render xlsx preview:", err);
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
setError("无法预览该 Excel 文件。");
|
setError(t.artifactPreview.excelPreviewFailed);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
|
|
@ -1199,7 +1213,7 @@ function ArtifactOfficePreview({
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
};
|
};
|
||||||
}, [artifactUrl, canRenderXlsx]);
|
}, [artifactUrl, canRenderXlsx, t.artifactPreview.excelPreviewFailed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canRenderXlsx || !activeSheet || !workbookRef.current) {
|
if (!canRenderXlsx || !activeSheet || !workbookRef.current) {
|
||||||
|
|
@ -1213,9 +1227,9 @@ function ArtifactOfficePreview({
|
||||||
setXlsxRows(rows);
|
setXlsxRows(rows);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to switch xlsx sheet:", err);
|
console.error("Failed to switch xlsx sheet:", err);
|
||||||
setError("切换工作表失败。");
|
setError(t.artifactPreview.switchSheetFailed);
|
||||||
}
|
}
|
||||||
}, [activeSheet, canRenderXlsx]);
|
}, [activeSheet, canRenderXlsx, t.artifactPreview.switchSheetFailed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canRenderXlsx || !xlsxGridContainerRef.current) {
|
if (!canRenderXlsx || !xlsxGridContainerRef.current) {
|
||||||
|
|
@ -1247,7 +1261,7 @@ function ArtifactOfficePreview({
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to render RevoGrid preview:", err);
|
console.error("Failed to render RevoGrid preview:", err);
|
||||||
if (!disposed) {
|
if (!disposed) {
|
||||||
setError("无法渲染 Excel 网格预览。");
|
setError(t.artifactPreview.excelGridPreviewFailed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1257,14 +1271,19 @@ function ArtifactOfficePreview({
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
};
|
};
|
||||||
}, [canRenderXlsx, xlsxColumns, xlsxRows]);
|
}, [
|
||||||
|
canRenderXlsx,
|
||||||
|
xlsxColumns,
|
||||||
|
xlsxRows,
|
||||||
|
t.artifactPreview.excelGridPreviewFailed,
|
||||||
|
]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canRenderPptx) {
|
if (!canRenderPptx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError("请下载ppt文件以获得最佳效果");
|
setError(t.artifactPreview.pptxDownloadHint);
|
||||||
}, [canRenderPptx]);
|
}, [canRenderPptx, t.artifactPreview.pptxDownloadHint]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("relative h-full overflow-hidden bg-white", className)}>
|
<div className={cn("relative h-full overflow-hidden bg-white", className)}>
|
||||||
|
|
@ -1328,6 +1347,7 @@ function ArtifactPreviewFallback({
|
||||||
fileName: string;
|
fileName: string;
|
||||||
artifactUrl: string;
|
artifactUrl: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-20 grid place-content-center bg-white p-6 text-center">
|
<div className="absolute inset-0 z-20 grid place-content-center bg-white p-6 text-center">
|
||||||
<p className="text-foreground mb-2 text-sm font-medium">{fileName}</p>
|
<p className="text-foreground mb-2 text-sm font-medium">{fileName}</p>
|
||||||
|
|
@ -1338,7 +1358,7 @@ function ArtifactPreviewFallback({
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
点击下载
|
{t.artifactPreview.clickToDownload}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -1459,13 +1479,22 @@ function buildArtifactViewerSrcDoc({
|
||||||
artifactUrl,
|
artifactUrl,
|
||||||
fileName,
|
fileName,
|
||||||
kind,
|
kind,
|
||||||
|
pdfPreviewMessage,
|
||||||
|
unsupportedTypeMessage,
|
||||||
|
openInNewTabLabel,
|
||||||
}: {
|
}: {
|
||||||
artifactUrl: string;
|
artifactUrl: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
kind: ArtifactPreviewKind;
|
kind: ArtifactPreviewKind;
|
||||||
|
pdfPreviewMessage: string;
|
||||||
|
unsupportedTypeMessage: string;
|
||||||
|
openInNewTabLabel: string;
|
||||||
}) {
|
}) {
|
||||||
const safeUrl = escapeHtml(artifactUrl);
|
const safeUrl = escapeHtml(artifactUrl);
|
||||||
const safeName = escapeHtml(fileName);
|
const safeName = escapeHtml(fileName);
|
||||||
|
const safePdfPreviewMessage = escapeHtml(pdfPreviewMessage);
|
||||||
|
const safeUnsupportedTypeMessage = escapeHtml(unsupportedTypeMessage);
|
||||||
|
const safeOpenInNewTabLabel = escapeHtml(openInNewTabLabel);
|
||||||
|
|
||||||
const content = (() => {
|
const content = (() => {
|
||||||
if (kind === "image") {
|
if (kind === "image") {
|
||||||
|
|
@ -1480,8 +1509,8 @@ function buildArtifactViewerSrcDoc({
|
||||||
if (kind === "pdf") {
|
if (kind === "pdf") {
|
||||||
return `<div class="fallback">
|
return `<div class="fallback">
|
||||||
<p class="title">${safeName}</p>
|
<p class="title">${safeName}</p>
|
||||||
<p class="desc">PDF preview is temporarily disabled. Please download the file to view it.</p>
|
<p class="desc">${safePdfPreviewMessage}</p>
|
||||||
<a class="link" href="${safeUrl}" target="_blank" rel="noopener noreferrer">Open in new tab</a>
|
<a class="link" href="${safeUrl}" target="_blank" rel="noopener noreferrer">${safeOpenInNewTabLabel}</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (kind === "html") {
|
if (kind === "html") {
|
||||||
|
|
@ -1489,8 +1518,8 @@ function buildArtifactViewerSrcDoc({
|
||||||
}
|
}
|
||||||
return `<div class="fallback">
|
return `<div class="fallback">
|
||||||
<p class="title">${safeName}</p>
|
<p class="title">${safeName}</p>
|
||||||
<p class="desc">This file type is not previewable in the custom viewer.</p>
|
<p class="desc">${safeUnsupportedTypeMessage}</p>
|
||||||
<a class="link" href="${safeUrl}" target="_blank" rel="noopener noreferrer">Open in new tab</a>
|
<a class="link" href="${safeUrl}" target="_blank" rel="noopener noreferrer">${safeOpenInNewTabLabel}</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
@ -1622,6 +1651,7 @@ export const ArtifactZoomSelector = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: ArtifactZoomSelectorProps) => {
|
}: ArtifactZoomSelectorProps) => {
|
||||||
|
const { t } = useI18n();
|
||||||
const handleZoomIn = () => {
|
const handleZoomIn = () => {
|
||||||
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
const currentIndex = ZOOM_LEVELS.indexOf(value);
|
||||||
const nextValue = ZOOM_LEVELS[currentIndex + 1];
|
const nextValue = ZOOM_LEVELS[currentIndex + 1];
|
||||||
|
|
@ -1660,7 +1690,7 @@ export const ArtifactZoomSelector = ({
|
||||||
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||||
"dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-foreground",
|
"dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
aria-label="放大"
|
aria-label={t.artifactPreview.zoomIn}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -1700,7 +1730,7 @@ export const ArtifactZoomSelector = ({
|
||||||
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
||||||
"dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-foreground",
|
"dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-foreground",
|
||||||
)}
|
)}
|
||||||
aria-label="缩小"
|
aria-label={t.artifactPreview.zoomOut}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ export function ArtifactFileList({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
引用
|
{t.common.reference}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export const ArtifactTrigger = () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Tooltip content="Show artifacts of this conversation">
|
<Tooltip content={t.artifactPreview.showArtifactsTooltip}>
|
||||||
<Button
|
<Button
|
||||||
className="text-muted-foreground hover:text-foreground"
|
className="text-muted-foreground hover:text-foreground"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
ResizablePanelGroup,
|
ResizablePanelGroup,
|
||||||
} from "@/components/ui/resizable";
|
} from "@/components/ui/resizable";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -27,6 +28,7 @@ const ChatBox: React.FC<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
threadId: string | undefined;
|
threadId: string | undefined;
|
||||||
}> = ({ children, threadId }) => {
|
}> = ({ children, threadId }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
const { thread } = useThread();
|
const { thread } = useThread();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const threadIdRef = useRef(threadId);
|
const threadIdRef = useRef(threadId);
|
||||||
|
|
@ -152,13 +154,13 @@ const ChatBox: React.FC<{
|
||||||
{thread.values.artifacts?.length === 0 ? (
|
{thread.values.artifacts?.length === 0 ? (
|
||||||
<ConversationEmptyState
|
<ConversationEmptyState
|
||||||
icon={<FilesIcon />}
|
icon={<FilesIcon />}
|
||||||
title="No artifact selected"
|
title={t.chatPage.noArtifactSelectedTitle}
|
||||||
description="Select an artifact to view its details"
|
description={t.chatPage.noArtifactSelectedDescription}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
|
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
|
||||||
<header className="shrink-0">
|
<header className="shrink-0">
|
||||||
<h2 className="text-lg font-medium">Artifacts</h2>
|
<h2 className="text-lg font-medium">{t.common.artifacts}</h2>
|
||||||
</header>
|
</header>
|
||||||
<main className="min-h-0 grow">
|
<main className="min-h-0 grow">
|
||||||
<ArtifactFileList
|
<ArtifactFileList
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,6 @@ import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.
|
||||||
|
|
||||||
const MAX_REFERENCES_PER_MESSAGE = 10;
|
const MAX_REFERENCES_PER_MESSAGE = 10;
|
||||||
|
|
||||||
const REFERENCE_SOURCE_LABELS = {
|
|
||||||
artifact: "生成文件",
|
|
||||||
upload: "上传附件",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type MentionCandidate = {
|
type MentionCandidate = {
|
||||||
key: string;
|
key: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
|
@ -200,6 +195,13 @@ export function InputBox({
|
||||||
onStop?: () => void;
|
onStop?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const referenceSourceLabels = useMemo(
|
||||||
|
() => ({
|
||||||
|
artifact: t.inputBox.referenceSourceArtifact,
|
||||||
|
upload: t.inputBox.referenceSourceUpload,
|
||||||
|
}),
|
||||||
|
[t],
|
||||||
|
);
|
||||||
const { thread } = useThread();
|
const { thread } = useThread();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const iframeSkill = useIframeSkill({ threadId: threadIdFromProps });
|
const iframeSkill = useIframeSkill({ threadId: threadIdFromProps });
|
||||||
|
|
@ -280,7 +282,7 @@ export function InputBox({
|
||||||
pathTail: getPathTail(path),
|
pathTail: getPathTail(path),
|
||||||
ref_source: "artifact" as const,
|
ref_source: "artifact" as const,
|
||||||
ref_kind: "mention" as const,
|
ref_kind: "mention" as const,
|
||||||
typeLabel: REFERENCE_SOURCE_LABELS.artifact,
|
typeLabel: referenceSourceLabels.artifact,
|
||||||
isImage: isImageFilename(filename),
|
isImage: isImageFilename(filename),
|
||||||
previewUrl: threadId
|
previewUrl: threadId
|
||||||
? urlOfArtifact({
|
? urlOfArtifact({
|
||||||
|
|
@ -299,7 +301,7 @@ export function InputBox({
|
||||||
pathTail: getPathTail(file.virtual_path),
|
pathTail: getPathTail(file.virtual_path),
|
||||||
ref_source: "upload" as const,
|
ref_source: "upload" as const,
|
||||||
ref_kind: "mention" as const,
|
ref_kind: "mention" as const,
|
||||||
typeLabel: REFERENCE_SOURCE_LABELS.upload,
|
typeLabel: referenceSourceLabels.upload,
|
||||||
isImage: isImageFilename(file.filename),
|
isImage: isImageFilename(file.filename),
|
||||||
previewUrl: file.artifact_url,
|
previewUrl: file.artifact_url,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
@ -309,7 +311,13 @@ export function InputBox({
|
||||||
deduped.set(candidate.key, candidate);
|
deduped.set(candidate.key, candidate);
|
||||||
});
|
});
|
||||||
return [...deduped.values()];
|
return [...deduped.values()];
|
||||||
}, [thread.values.artifacts, uploadedFilesData?.files, threadId]);
|
}, [
|
||||||
|
referenceSourceLabels.artifact,
|
||||||
|
referenceSourceLabels.upload,
|
||||||
|
thread.values.artifacts,
|
||||||
|
uploadedFilesData?.files,
|
||||||
|
threadId,
|
||||||
|
]);
|
||||||
|
|
||||||
const filteredMentionCandidates = useMemo(() => {
|
const filteredMentionCandidates = useMemo(() => {
|
||||||
const query = mentionQuery.trim().toLowerCase();
|
const query = mentionQuery.trim().toLowerCase();
|
||||||
|
|
@ -383,12 +391,12 @@ export function InputBox({
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
if (prev.length >= MAX_REFERENCES_PER_MESSAGE) {
|
if (prev.length >= MAX_REFERENCES_PER_MESSAGE) {
|
||||||
toast.error("单条消息最多引用 10 个文件");
|
toast.error(t.inputBox.maxReferencesReached);
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
return prev.concat(reference);
|
return prev.concat(reference);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [t.inputBox.maxReferencesReached]);
|
||||||
|
|
||||||
const selectMentionCandidate = useCallback(
|
const selectMentionCandidate = useCallback(
|
||||||
(candidate: MentionCandidate) => {
|
(candidate: MentionCandidate) => {
|
||||||
|
|
@ -687,7 +695,7 @@ export function InputBox({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuLabel className="p-0 text-[14px] text-[#333333]">
|
<DropdownMenuLabel className="p-0 text-[14px] text-[#333333]">
|
||||||
添加引用
|
{t.inputBox.addReference}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" />
|
<DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" />
|
||||||
<DropdownMenuGroup className="flex pt-[20px] px-0 max-h-[480px] flex-col gap-[10px] overflow-y-auto">
|
<DropdownMenuGroup className="flex pt-[20px] px-0 max-h-[480px] flex-col gap-[10px] overflow-y-auto">
|
||||||
|
|
@ -842,7 +850,7 @@ export function InputBox({
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{followupsLoading ? (
|
{followupsLoading ? (
|
||||||
<div className="text-muted-foreground bg-background/80 rounded-full border px-4 py-2 text-xs backdrop-blur-sm">
|
<div className="text-muted-foreground bg-background/80 rounded-full border px-4 py-2 text-xs backdrop-blur-sm">
|
||||||
加载中...
|
{t.inputBox.followupLoading}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Suggestions className="min-h-16 w-fit items-start">
|
<Suggestions className="min-h-16 w-fit items-start">
|
||||||
|
|
@ -872,19 +880,21 @@ export function InputBox({
|
||||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>提示</DialogTitle>
|
<DialogTitle>{t.inputBox.followupConfirmTitle}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
请确认要如何处理当前的追加建议内容?
|
{t.inputBox.followupConfirmDescription}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setConfirmOpen(false)}>
|
<Button variant="outline" onClick={() => setConfirmOpen(false)}>
|
||||||
取消
|
{t.common.cancel}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" onClick={confirmAppendAndSend}>
|
<Button variant="secondary" onClick={confirmAppendAndSend}>
|
||||||
追加内容
|
{t.inputBox.followupConfirmAppend}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={confirmReplaceAndSend}>
|
||||||
|
{t.inputBox.followupConfirmReplace}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={confirmReplaceAndSend}>替换发送</Button>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
@ -1165,6 +1175,11 @@ function AttachmentPreviewBar({
|
||||||
threadId: string;
|
threadId: string;
|
||||||
onRemoveReference: (reference: PromptInputReference) => void;
|
onRemoveReference: (reference: PromptInputReference) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const referenceSourceLabels = {
|
||||||
|
artifact: t.inputBox.referenceSourceArtifact,
|
||||||
|
upload: t.inputBox.referenceSourceUpload,
|
||||||
|
} as const;
|
||||||
const attachments = usePromptInputAttachments();
|
const attachments = usePromptInputAttachments();
|
||||||
const hasReferences = references.length > 0;
|
const hasReferences = references.length > 0;
|
||||||
const hasAttachmentFiles = attachments.files.length > 0;
|
const hasAttachmentFiles = attachments.files.length > 0;
|
||||||
|
|
@ -1214,7 +1229,7 @@ function AttachmentPreviewBar({
|
||||||
}}
|
}}
|
||||||
data-testid="reference-chip"
|
data-testid="reference-chip"
|
||||||
onRemove={() => onRemoveReference(reference)}
|
onRemove={() => onRemoveReference(reference)}
|
||||||
title={`${REFERENCE_SOURCE_LABELS[reference.ref_source]}${reference.path ? ` · ${getPathTail(reference.path)}` : ""}`}
|
title={`${referenceSourceLabels[reference.ref_source]}${reference.path ? ` · ${getPathTail(reference.path)}` : ""}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -387,11 +387,15 @@ function RichFileCard({
|
||||||
clear_target: true,
|
clear_target: true,
|
||||||
});
|
});
|
||||||
setMaterializeMessage(
|
setMaterializeMessage(
|
||||||
`已创建 ${result.created_files} 个文件 / ${result.created_directories} 个目录`,
|
t.messageListItem.materializeSuccess(
|
||||||
|
result.created_files,
|
||||||
|
result.created_directories,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "解析失败";
|
const message =
|
||||||
setMaterializeMessage(`失败: ${message}`);
|
error instanceof Error ? error.message : t.messageListItem.parseFailed;
|
||||||
|
setMaterializeMessage(t.messageListItem.materializeFailed(message));
|
||||||
} finally {
|
} finally {
|
||||||
setIsMaterializing(false);
|
setIsMaterializing(false);
|
||||||
}
|
}
|
||||||
|
|
@ -430,7 +434,7 @@ function RichFileCard({
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
引用
|
{t.common.reference}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|
@ -470,7 +474,9 @@ function RichFileCard({
|
||||||
}}
|
}}
|
||||||
disabled={isMaterializing}
|
disabled={isMaterializing}
|
||||||
>
|
>
|
||||||
{isMaterializing ? "解析中..." : "一键导入为 Skill 目录"}
|
{isMaterializing
|
||||||
|
? t.messageListItem.materializing
|
||||||
|
: t.messageListItem.importAsSkillDir}
|
||||||
</Button>
|
</Button>
|
||||||
{materializeMessage && (
|
{materializeMessage && (
|
||||||
<span className="text-muted-foreground text-[10px] leading-tight">
|
<span className="text-muted-foreground text-[10px] leading-tight">
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ export function MessageList({
|
||||||
"z-20 rounded-full border bg-white/90 shadow-sm backdrop-blur-sm",
|
"z-20 rounded-full border bg-white/90 shadow-sm backdrop-blur-sm",
|
||||||
scrollButtonClassName,
|
scrollButtonClassName,
|
||||||
)}
|
)}
|
||||||
title="滚动到底部"
|
title={t.chats.scrollToBottom}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Conversation>
|
</Conversation>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export function WorkspaceHeader({ className }: { className?: string }) {
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ? (
|
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ? (
|
||||||
<Link href="/" className="text-primary ml-2 font-serif">
|
<Link href="/" className="text-primary ml-2 font-serif">
|
||||||
XClaw侧边栏
|
{t.workspaceHeader.sidebarTitle}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-primary ml-2 cursor-default font-serif">
|
<div className="text-primary ml-2 cursor-default font-serif">
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export const enUS: Translations = {
|
||||||
exportAsJSON: "Export as JSON",
|
exportAsJSON: "Export as JSON",
|
||||||
exportSuccess: "Conversation exported",
|
exportSuccess: "Conversation exported",
|
||||||
removeAttachment: "Remove attachment",
|
removeAttachment: "Remove attachment",
|
||||||
|
reference: "Reference",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Welcome
|
// Welcome
|
||||||
|
|
@ -115,6 +116,13 @@ export const enUS: Translations = {
|
||||||
"You already have text in the input. Choose how to send it.",
|
"You already have text in the input. Choose how to send it.",
|
||||||
followupConfirmAppend: "Append & send",
|
followupConfirmAppend: "Append & send",
|
||||||
followupConfirmReplace: "Replace & send",
|
followupConfirmReplace: "Replace & send",
|
||||||
|
submit: "Send",
|
||||||
|
submitting: "Generating...",
|
||||||
|
stop: "Stop",
|
||||||
|
addReference: "Add reference",
|
||||||
|
referenceSourceArtifact: "Generated file",
|
||||||
|
referenceSourceUpload: "Uploaded attachment",
|
||||||
|
maxReferencesReached: "You can reference up to 10 files per message",
|
||||||
suggestions: [
|
suggestions: [
|
||||||
{
|
{
|
||||||
suggestion: "Paper Writing",
|
suggestion: "Paper Writing",
|
||||||
|
|
@ -248,6 +256,87 @@ export const enUS: Translations = {
|
||||||
// Chats
|
// Chats
|
||||||
chats: {
|
chats: {
|
||||||
searchChats: "Search chats",
|
searchChats: "Search chats",
|
||||||
|
scrollToBottom: "Scroll to bottom",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Workspace Chat Page
|
||||||
|
chatPage: {
|
||||||
|
defaultSlogan: "Let's study and work together",
|
||||||
|
missingThreadIdForCreate: "Missing thread_id, cannot create session",
|
||||||
|
createSessionFailed: "Failed to create session, please try again later",
|
||||||
|
conversationFinished: "Conversation finished",
|
||||||
|
missingThreadIdForSend: "Missing thread_id, cannot send message",
|
||||||
|
viewArtifactsTooltip: "Click to view generated artifacts",
|
||||||
|
noArtifactSelectedTitle: "No artifact selected",
|
||||||
|
noArtifactSelectedDescription: "Select an artifact to view its details",
|
||||||
|
exitDialogTitle: "Notice",
|
||||||
|
exitDialogDescription:
|
||||||
|
"Chat history is automatically deleted every seven days. You will return to the welcome page now. Continue?",
|
||||||
|
exitDialogConfirm: "Confirm",
|
||||||
|
selectedSkillLoadFailed: "Failed to load skill",
|
||||||
|
unknownErrorRetry: "An unknown error occurred. Please try again later.",
|
||||||
|
},
|
||||||
|
|
||||||
|
messageListItem: {
|
||||||
|
materializing: "Parsing...",
|
||||||
|
importAsSkillDir: "Import as Skill directory",
|
||||||
|
materializeSuccess: (files: number, directories: number) =>
|
||||||
|
`Created ${files} file(s) / ${directories} director${directories === 1 ? "y" : "ies"}`,
|
||||||
|
parseFailed: "Parse failed",
|
||||||
|
materializeFailed: (message: string) => `Failed: ${message}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
artifactPreview: {
|
||||||
|
pdfPreviewFailed: "Unable to preview this PDF file. Please download it.",
|
||||||
|
unsupportedType: "This file type is not previewable in the custom viewer.",
|
||||||
|
docxPreviewFailed: "Unable to preview this DOCX file.",
|
||||||
|
excelPreviewFailed: "Unable to preview this Excel file.",
|
||||||
|
switchSheetFailed: "Failed to switch worksheet.",
|
||||||
|
excelGridPreviewFailed: "Unable to render Excel grid preview.",
|
||||||
|
pptxDownloadHint: "Please download the PPT file for the best experience.",
|
||||||
|
openInNewTab: "Open in new tab",
|
||||||
|
clickToDownload: "Click to download",
|
||||||
|
pageCountLabel: (fileName: string, pageCount: number) =>
|
||||||
|
`${fileName} · ${pageCount} page(s)`,
|
||||||
|
zoomIn: "Zoom in",
|
||||||
|
zoomOut: "Zoom out",
|
||||||
|
showArtifactsTooltip: "Show artifacts of this conversation",
|
||||||
|
},
|
||||||
|
|
||||||
|
workspaceHeader: {
|
||||||
|
sidebarTitle: "XClaw Sidebar",
|
||||||
|
},
|
||||||
|
|
||||||
|
models: {
|
||||||
|
updating: "System is updating, please wait...",
|
||||||
|
apiUnavailable:
|
||||||
|
"Model API is unavailable. Please check backend routes or service status.",
|
||||||
|
},
|
||||||
|
|
||||||
|
threads: {
|
||||||
|
streamError: "Something went wrong.",
|
||||||
|
invalidThreadId: "Invalid thread id 'new'. Please refresh and retry.",
|
||||||
|
staleReferencesRemoved:
|
||||||
|
"Some referenced files were invalid and were removed automatically.",
|
||||||
|
uploadFailed: "Failed to upload files.",
|
||||||
|
uploadPrepareFailed: (count: number) =>
|
||||||
|
`Failed to prepare ${count} attachment(s) for upload. Please retry.`,
|
||||||
|
threadNotReadyForUpload: "Thread is not ready for file upload.",
|
||||||
|
},
|
||||||
|
|
||||||
|
skills: {
|
||||||
|
loadFailed: "Failed to load skill",
|
||||||
|
missingThreadId: "Missing thread_id, cannot initialize skill",
|
||||||
|
invalidSkillId: "Invalid skill_id",
|
||||||
|
loading: (title: string) => `Loading skill "${title}"...`,
|
||||||
|
loadFailedWithTitle: (title: string) => `Failed to load skill "${title}"`,
|
||||||
|
loadSuccessWithTitle: (title: string) =>
|
||||||
|
`Skill "${title}" loaded successfully`,
|
||||||
|
loadErrorWithTitle: (title: string) => `Error loading skill "${title}"`,
|
||||||
|
unknownError: "Unknown error",
|
||||||
|
networkRequestFailed: "Network request failed",
|
||||||
|
createdFiles: (count: number) => `Created ${count} file(s)`,
|
||||||
|
invalidSkillIdArray: "Invalid skill_id array",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Page titles (document title)
|
// Page titles (document title)
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ export interface Translations {
|
||||||
exportAsJSON: string;
|
exportAsJSON: string;
|
||||||
exportSuccess: string;
|
exportSuccess: string;
|
||||||
removeAttachment: string;
|
removeAttachment: string;
|
||||||
|
reference: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Welcome
|
// Welcome
|
||||||
|
|
@ -99,6 +100,13 @@ export interface Translations {
|
||||||
followupConfirmDescription: string;
|
followupConfirmDescription: string;
|
||||||
followupConfirmAppend: string;
|
followupConfirmAppend: string;
|
||||||
followupConfirmReplace: string;
|
followupConfirmReplace: string;
|
||||||
|
submit: string;
|
||||||
|
submitting: string;
|
||||||
|
stop: string;
|
||||||
|
addReference: string;
|
||||||
|
referenceSourceArtifact: string;
|
||||||
|
referenceSourceUpload: string;
|
||||||
|
maxReferencesReached: string;
|
||||||
suggestions: {
|
suggestions: {
|
||||||
suggestion: string;
|
suggestion: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
|
@ -179,6 +187,80 @@ export interface Translations {
|
||||||
// Chats
|
// Chats
|
||||||
chats: {
|
chats: {
|
||||||
searchChats: string;
|
searchChats: string;
|
||||||
|
scrollToBottom: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Workspace Chat Page
|
||||||
|
chatPage: {
|
||||||
|
defaultSlogan: string;
|
||||||
|
missingThreadIdForCreate: string;
|
||||||
|
createSessionFailed: string;
|
||||||
|
conversationFinished: string;
|
||||||
|
missingThreadIdForSend: string;
|
||||||
|
viewArtifactsTooltip: string;
|
||||||
|
noArtifactSelectedTitle: string;
|
||||||
|
noArtifactSelectedDescription: string;
|
||||||
|
exitDialogTitle: string;
|
||||||
|
exitDialogDescription: string;
|
||||||
|
exitDialogConfirm: string;
|
||||||
|
selectedSkillLoadFailed: string;
|
||||||
|
unknownErrorRetry: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
messageListItem: {
|
||||||
|
materializing: string;
|
||||||
|
importAsSkillDir: string;
|
||||||
|
materializeSuccess: (files: number, directories: number) => string;
|
||||||
|
parseFailed: string;
|
||||||
|
materializeFailed: (message: string) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
artifactPreview: {
|
||||||
|
pdfPreviewFailed: string;
|
||||||
|
unsupportedType: string;
|
||||||
|
docxPreviewFailed: string;
|
||||||
|
excelPreviewFailed: string;
|
||||||
|
switchSheetFailed: string;
|
||||||
|
excelGridPreviewFailed: string;
|
||||||
|
pptxDownloadHint: string;
|
||||||
|
openInNewTab: string;
|
||||||
|
clickToDownload: string;
|
||||||
|
pageCountLabel: (fileName: string, pageCount: number) => string;
|
||||||
|
zoomIn: string;
|
||||||
|
zoomOut: string;
|
||||||
|
showArtifactsTooltip: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
workspaceHeader: {
|
||||||
|
sidebarTitle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
models: {
|
||||||
|
updating: string;
|
||||||
|
apiUnavailable: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
threads: {
|
||||||
|
streamError: string;
|
||||||
|
invalidThreadId: string;
|
||||||
|
staleReferencesRemoved: string;
|
||||||
|
uploadFailed: string;
|
||||||
|
uploadPrepareFailed: (count: number) => string;
|
||||||
|
threadNotReadyForUpload: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
skills: {
|
||||||
|
loadFailed: string;
|
||||||
|
missingThreadId: string;
|
||||||
|
invalidSkillId: string;
|
||||||
|
loading: (title: string) => string;
|
||||||
|
loadFailedWithTitle: (title: string) => string;
|
||||||
|
loadSuccessWithTitle: (title: string) => string;
|
||||||
|
loadErrorWithTitle: (title: string) => string;
|
||||||
|
unknownError: string;
|
||||||
|
networkRequestFailed: string;
|
||||||
|
createdFiles: (count: number) => string;
|
||||||
|
invalidSkillIdArray: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Page titles (document title)
|
// Page titles (document title)
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export const zhCN: Translations = {
|
||||||
exportAsJSON: "导出为 JSON",
|
exportAsJSON: "导出为 JSON",
|
||||||
exportSuccess: "对话已导出",
|
exportSuccess: "对话已导出",
|
||||||
removeAttachment: "移除附件",
|
removeAttachment: "移除附件",
|
||||||
|
reference: "引用",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Welcome
|
// Welcome
|
||||||
|
|
@ -112,6 +113,13 @@ export const zhCN: Translations = {
|
||||||
followupConfirmDescription: "当前输入框已有内容,选择发送方式。",
|
followupConfirmDescription: "当前输入框已有内容,选择发送方式。",
|
||||||
followupConfirmAppend: "追加并发送",
|
followupConfirmAppend: "追加并发送",
|
||||||
followupConfirmReplace: "替换并发送",
|
followupConfirmReplace: "替换并发送",
|
||||||
|
submit: "发送",
|
||||||
|
submitting: "生成中...",
|
||||||
|
stop: "停止",
|
||||||
|
addReference: "添加引用",
|
||||||
|
referenceSourceArtifact: "生成文件",
|
||||||
|
referenceSourceUpload: "上传附件",
|
||||||
|
maxReferencesReached: "单条消息最多引用 10 个文件",
|
||||||
suggestions: [
|
suggestions: [
|
||||||
{
|
{
|
||||||
suggestion: "自媒体文案",
|
suggestion: "自媒体文案",
|
||||||
|
|
@ -237,6 +245,83 @@ export const zhCN: Translations = {
|
||||||
// Chats
|
// Chats
|
||||||
chats: {
|
chats: {
|
||||||
searchChats: "搜索对话",
|
searchChats: "搜索对话",
|
||||||
|
scrollToBottom: "滚动到底部",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Workspace Chat Page
|
||||||
|
chatPage: {
|
||||||
|
defaultSlogan: "来,一起学习工作吧",
|
||||||
|
missingThreadIdForCreate: "缺少 thread_id,无法创建会话",
|
||||||
|
createSessionFailed: "会话创建失败,请稍后重试",
|
||||||
|
conversationFinished: "对话已完成",
|
||||||
|
missingThreadIdForSend: "缺少 thread_id,无法发送消息",
|
||||||
|
viewArtifactsTooltip: "点击可查看生成的文件结果",
|
||||||
|
noArtifactSelectedTitle: "未选择生成文件",
|
||||||
|
noArtifactSelectedDescription: "请选择一个生成文件以查看详情",
|
||||||
|
exitDialogTitle: "提示",
|
||||||
|
exitDialogDescription: "历史记录每七天自动删除,现在将返回欢迎页,是否继续?",
|
||||||
|
exitDialogConfirm: "确定",
|
||||||
|
selectedSkillLoadFailed: "技能加载失败",
|
||||||
|
unknownErrorRetry: "发生了未知错误,请稍后重试。",
|
||||||
|
},
|
||||||
|
|
||||||
|
messageListItem: {
|
||||||
|
materializing: "解析中...",
|
||||||
|
importAsSkillDir: "一键导入为 Skill 目录",
|
||||||
|
materializeSuccess: (files: number, directories: number) =>
|
||||||
|
`已创建 ${files} 个文件 / ${directories} 个目录`,
|
||||||
|
parseFailed: "解析失败",
|
||||||
|
materializeFailed: (message: string) => `失败: ${message}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
artifactPreview: {
|
||||||
|
pdfPreviewFailed: "无法预览该 PDF 文件,请下载后查看。",
|
||||||
|
unsupportedType: "该文件类型暂不支持在自定义预览器中查看。",
|
||||||
|
docxPreviewFailed: "无法预览该 DOCX 文件。",
|
||||||
|
excelPreviewFailed: "无法预览该 Excel 文件。",
|
||||||
|
switchSheetFailed: "切换工作表失败。",
|
||||||
|
excelGridPreviewFailed: "无法渲染 Excel 网格预览。",
|
||||||
|
pptxDownloadHint: "请下载 ppt 文件以获得最佳效果",
|
||||||
|
openInNewTab: "在新标签页打开",
|
||||||
|
clickToDownload: "点击下载",
|
||||||
|
pageCountLabel: (fileName: string, pageCount: number) =>
|
||||||
|
`${fileName} · 共 ${pageCount} 页`,
|
||||||
|
zoomIn: "放大",
|
||||||
|
zoomOut: "缩小",
|
||||||
|
showArtifactsTooltip: "查看当前对话的生成文件",
|
||||||
|
},
|
||||||
|
|
||||||
|
workspaceHeader: {
|
||||||
|
sidebarTitle: "XClaw侧边栏",
|
||||||
|
},
|
||||||
|
|
||||||
|
models: {
|
||||||
|
updating: "系统正在更新,请稍候……",
|
||||||
|
apiUnavailable: "模型接口不可用,请检查后端路由或服务状态。",
|
||||||
|
},
|
||||||
|
|
||||||
|
threads: {
|
||||||
|
streamError: "出现了某些错误。",
|
||||||
|
invalidThreadId: "线程 ID 无效(new),请刷新后重试。",
|
||||||
|
staleReferencesRemoved: "部分引用文件已失效,已自动移除并继续发送。",
|
||||||
|
uploadFailed: "文件上传失败。",
|
||||||
|
uploadPrepareFailed: (count: number) =>
|
||||||
|
`准备上传附件失败(${count} 个),请重试。`,
|
||||||
|
threadNotReadyForUpload: "当前线程尚未就绪,无法上传文件。",
|
||||||
|
},
|
||||||
|
|
||||||
|
skills: {
|
||||||
|
loadFailed: "技能加载失败",
|
||||||
|
missingThreadId: "缺少 thread_id,无法初始化技能",
|
||||||
|
invalidSkillId: "无效的 skill_id",
|
||||||
|
loading: (title: string) => `正在加载技能「${title}」...`,
|
||||||
|
loadFailedWithTitle: (title: string) => `技能「${title}」加载失败`,
|
||||||
|
loadSuccessWithTitle: (title: string) => `技能「${title}」加载成功`,
|
||||||
|
loadErrorWithTitle: (title: string) => `技能「${title}」加载出错`,
|
||||||
|
unknownError: "未知错误",
|
||||||
|
networkRequestFailed: "网络请求失败",
|
||||||
|
createdFiles: (count: number) => `已创建 ${count} 个文件`,
|
||||||
|
invalidSkillIdArray: "非法 skill_id 数组",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Page titles (document title)
|
// Page titles (document title)
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@ import { useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { useI18n } from "../i18n/hooks";
|
||||||
|
|
||||||
import { loadModels } from "./api";
|
import { loadModels } from "./api";
|
||||||
import type { Model } from "./types";
|
import type { Model } from "./types";
|
||||||
|
|
||||||
const MODELS_UPDATING_TOAST_ID = "models-server-updating";
|
const MODELS_UPDATING_TOAST_ID = "models-server-updating";
|
||||||
|
|
||||||
export function useModels({ enabled = true }: { enabled?: boolean } = {}) {
|
export function useModels({ enabled = true }: { enabled?: boolean } = {}) {
|
||||||
|
const { t } = useI18n();
|
||||||
const { data, isLoading, error, failureReason } = useQuery<Model[], Error>({
|
const { data, isLoading, error, failureReason } = useQuery<Model[], Error>({
|
||||||
queryKey: ["models"],
|
queryKey: ["models"],
|
||||||
queryFn: () => loadModels(),
|
queryFn: () => loadModels(),
|
||||||
|
|
@ -31,7 +34,7 @@ export function useModels({ enabled = true }: { enabled?: boolean } = {}) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
toast.loading("系统正在更新,请稍候……", {
|
toast.loading(t.models.updating, {
|
||||||
id: MODELS_UPDATING_TOAST_ID,
|
id: MODELS_UPDATING_TOAST_ID,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
@ -42,9 +45,9 @@ export function useModels({ enabled = true }: { enabled?: boolean } = {}) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error?.message.includes("HTTP error: 4")) {
|
if (error?.message.includes("HTTP error: 4")) {
|
||||||
toast.error("模型接口不可用,请检查后端路由或服务状态。");
|
toast.error(t.models.apiUnavailable);
|
||||||
}
|
}
|
||||||
}, [error]);
|
}, [error, t.models.apiUnavailable]);
|
||||||
|
|
||||||
return { models: data ?? [], isLoading, error };
|
return { models: data ?? [], isLoading, error };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ export type LegacyThreadStreamOptions = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const STREAM_ERROR_FALLBACK_MESSAGE = "Request failed.";
|
const STREAM_ERROR_FALLBACK_MESSAGE = "Request failed.";
|
||||||
const STREAM_ERROR_TOAST_MESSAGE = "出现了某些错误。";
|
|
||||||
const STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS = 2000;
|
const STREAM_ERROR_TOAST_DEDUPE_WINDOW_MS = 2000;
|
||||||
const STREAM_CANCEL_PATTERNS = [
|
const STREAM_CANCEL_PATTERNS = [
|
||||||
/\bcancellederror\b/i,
|
/\bcancellederror\b/i,
|
||||||
|
|
@ -276,8 +275,8 @@ export function useThreadStream({
|
||||||
lastErrorToastRef.current = { message, timestamp: now };
|
lastErrorToastRef.current = { message, timestamp: now };
|
||||||
console.error("[useThreadStream] conversation stream error:", error);
|
console.error("[useThreadStream] conversation stream error:", error);
|
||||||
console.error("[useThreadStream] parsed error message:", message);
|
console.error("[useThreadStream] parsed error message:", message);
|
||||||
toast.error(STREAM_ERROR_TOAST_MESSAGE);
|
toast.error(t.threads.streamError);
|
||||||
}, []);
|
}, [t.threads.streamError]);
|
||||||
|
|
||||||
const handleStreamStart = useCallback(
|
const handleStreamStart = useCallback(
|
||||||
(_threadId: string) => {
|
(_threadId: string) => {
|
||||||
|
|
@ -407,7 +406,7 @@ export function useThreadStream({
|
||||||
normalizeThreadId(threadIdRef.current) ??
|
normalizeThreadId(threadIdRef.current) ??
|
||||||
undefined;
|
undefined;
|
||||||
if (resolvedThreadId === "new") {
|
if (resolvedThreadId === "new") {
|
||||||
toast.error("Invalid thread id 'new'. Please refresh and retry.");
|
toast.error(t.threads.invalidThreadId);
|
||||||
sendInFlightRef.current = false;
|
sendInFlightRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -503,12 +502,12 @@ export function useThreadStream({
|
||||||
|
|
||||||
if (failedConversions > 0) {
|
if (failedConversions > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to prepare ${failedConversions} attachment(s) for upload. Please retry.`,
|
t.threads.uploadPrepareFailed(failedConversions),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resolvedThreadId) {
|
if (!resolvedThreadId) {
|
||||||
throw new Error("Thread is not ready for file upload.");
|
throw new Error(t.threads.threadNotReadyForUpload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
|
|
@ -545,9 +544,7 @@ export function useThreadStream({
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to upload files:", error);
|
console.error("Failed to upload files:", error);
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error
|
error instanceof Error ? error.message : t.threads.uploadFailed;
|
||||||
? error.message
|
|
||||||
: "Failed to upload files.";
|
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
setOptimisticMessages([]);
|
setOptimisticMessages([]);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -563,7 +560,7 @@ export function useThreadStream({
|
||||||
normalizedReferences,
|
normalizedReferences,
|
||||||
);
|
);
|
||||||
if (staleCount > 0) {
|
if (staleCount > 0) {
|
||||||
toast.error("部分引用文件已失效,已自动移除并继续发送。");
|
toast.error(t.threads.staleReferencesRemoved);
|
||||||
}
|
}
|
||||||
|
|
||||||
await thread.submit(
|
await thread.submit(
|
||||||
|
|
@ -621,6 +618,11 @@ export function useThreadStream({
|
||||||
thread,
|
thread,
|
||||||
_handleOnStart,
|
_handleOnStart,
|
||||||
t.uploads.uploadingFiles,
|
t.uploads.uploadingFiles,
|
||||||
|
t.threads.invalidThreadId,
|
||||||
|
t.threads.uploadPrepareFailed,
|
||||||
|
t.threads.threadNotReadyForUpload,
|
||||||
|
t.threads.uploadFailed,
|
||||||
|
t.threads.staleReferencesRemoved,
|
||||||
context,
|
context,
|
||||||
queryClient,
|
queryClient,
|
||||||
apiClient,
|
apiClient,
|
||||||
|
|
@ -659,12 +661,13 @@ export function useSubmitThread({
|
||||||
uploadTarget?: UploadTarget;
|
uploadTarget?: UploadTarget;
|
||||||
afterSubmit?: () => void;
|
afterSubmit?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const apiClient = getAPIClient();
|
const apiClient = getAPIClient();
|
||||||
const callback = useCallback(
|
const callback = useCallback(
|
||||||
async (message: PromptInputMessage) => {
|
async (message: PromptInputMessage) => {
|
||||||
if (threadId === "new") {
|
if (threadId === "new") {
|
||||||
toast.error("Invalid thread id 'new'. Please refresh and retry.");
|
toast.error(t.threads.invalidThreadId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const text = message.text.trim();
|
const text = message.text.trim();
|
||||||
|
|
@ -730,7 +733,7 @@ export function useSubmitThread({
|
||||||
normalizedReferences,
|
normalizedReferences,
|
||||||
);
|
);
|
||||||
if (staleCount > 0) {
|
if (staleCount > 0) {
|
||||||
toast.error("部分引用文件已失效,已自动移除并继续发送。");
|
toast.error(t.threads.staleReferencesRemoved);
|
||||||
}
|
}
|
||||||
|
|
||||||
await thread.submit(
|
await thread.submit(
|
||||||
|
|
@ -769,6 +772,8 @@ export function useSubmitThread({
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
thread,
|
thread,
|
||||||
|
t.threads.invalidThreadId,
|
||||||
|
t.threads.staleReferencesRemoved,
|
||||||
createNewSession,
|
createNewSession,
|
||||||
threadId,
|
threadId,
|
||||||
threadContext,
|
threadContext,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
type SelectedSkillPayloadItem,
|
type SelectedSkillPayloadItem,
|
||||||
sendToParent,
|
sendToParent,
|
||||||
} from "@/core/iframe-messages";
|
} from "@/core/iframe-messages";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
||||||
|
|
||||||
// Skill 数据类型
|
// Skill 数据类型
|
||||||
|
|
@ -79,6 +80,7 @@ interface UseIframeSkillOptions {
|
||||||
export function useIframeSkill(
|
export function useIframeSkill(
|
||||||
options?: UseIframeSkillOptions,
|
options?: UseIframeSkillOptions,
|
||||||
): UseIframeSkillReturn {
|
): UseIframeSkillReturn {
|
||||||
|
const { t } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const threadIdFromQuery = searchParams.get("thread_id");
|
const threadIdFromQuery = searchParams.get("thread_id");
|
||||||
|
|
@ -251,8 +253,8 @@ export function useIframeSkill(
|
||||||
title: string;
|
title: string;
|
||||||
}) => {
|
}) => {
|
||||||
if (!threadId) {
|
if (!threadId) {
|
||||||
toast.error("技能加载失败", {
|
toast.error(t.skills.loadFailed, {
|
||||||
description: "缺少 thread_id,无法初始化技能",
|
description: t.skills.missingThreadId,
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -266,8 +268,8 @@ export function useIframeSkill(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (content_ids.length === 0) {
|
if (content_ids.length === 0) {
|
||||||
toast.error("技能加载失败", {
|
toast.error(t.skills.loadFailed, {
|
||||||
description: "无效的 skill_id",
|
description: t.skills.invalidSkillId,
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +280,7 @@ export function useIframeSkill(
|
||||||
const languageType = languageTypeRaw ? Number(languageTypeRaw) : 0;
|
const languageType = languageTypeRaw ? Number(languageTypeRaw) : 0;
|
||||||
|
|
||||||
setIsBootstrapping(true);
|
setIsBootstrapping(true);
|
||||||
toast.loading(`正在加载技能「${title}」...`, {
|
toast.loading(t.skills.loading(title), {
|
||||||
id: "suggest-skill-bootstrap",
|
id: "suggest-skill-bootstrap",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -298,8 +300,8 @@ export function useIframeSkill(
|
||||||
String(item.id).trim(),
|
String(item.id).trim(),
|
||||||
);
|
);
|
||||||
removeFailedSkills(failedIds);
|
removeFailedSkills(failedIds);
|
||||||
toast.error(`技能「${title}」加载失败`, {
|
toast.error(t.skills.loadFailedWithTitle(title), {
|
||||||
description: result.message || "未知错误",
|
description: result.message || t.skills.unknownError,
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -312,9 +314,8 @@ export function useIframeSkill(
|
||||||
setSelectedSkill(normalizedSkills[0] ?? null);
|
setSelectedSkill(normalizedSkills[0] ?? null);
|
||||||
setSelectedSkills(normalizedSkills);
|
setSelectedSkills(normalizedSkills);
|
||||||
|
|
||||||
toast.success(`技能「${title}」加载成功`, {
|
toast.success(t.skills.loadSuccessWithTitle(title), {
|
||||||
description:
|
description: result.message || t.skills.createdFiles(result.created_files),
|
||||||
result.message || `已创建 ${result.created_files} 个文件`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -322,8 +323,9 @@ export function useIframeSkill(
|
||||||
const failedIds = selectedSkills.map((item) => String(item.id).trim());
|
const failedIds = selectedSkills.map((item) => String(item.id).trim());
|
||||||
removeFailedSkills(failedIds);
|
removeFailedSkills(failedIds);
|
||||||
toast.dismiss("suggest-skill-bootstrap");
|
toast.dismiss("suggest-skill-bootstrap");
|
||||||
const message = error instanceof Error ? error.message : "网络请求失败";
|
const message =
|
||||||
toast.error(`技能「${title}」加载失败`, {
|
error instanceof Error ? error.message : t.skills.networkRequestFailed;
|
||||||
|
toast.error(t.skills.loadFailedWithTitle(title), {
|
||||||
description: message,
|
description: message,
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -331,7 +333,7 @@ export function useIframeSkill(
|
||||||
setIsBootstrapping(false);
|
setIsBootstrapping(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[removeFailedSkills, searchParams, sendSelectSkill, threadId],
|
[removeFailedSkills, searchParams, sendSelectSkill, t.skills, threadId],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 打开 skill 选择对话框
|
// 打开 skill 选择对话框
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
isSelectedSkillsMessage,
|
isSelectedSkillsMessage,
|
||||||
type SelectedSkillPayloadItem,
|
type SelectedSkillPayloadItem,
|
||||||
} from "@/core/iframe-messages";
|
} from "@/core/iframe-messages";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
import { bootstrapRemoteSkill } from "@/core/skills/api";
|
||||||
|
|
||||||
/** 技能基础数据 */
|
/** 技能基础数据 */
|
||||||
|
|
@ -46,6 +47,7 @@ interface UseSelectedSkillListenerReturn {
|
||||||
export function useSelectedSkillListener({
|
export function useSelectedSkillListener({
|
||||||
threadId,
|
threadId,
|
||||||
}: UseSelectedSkillListenerOptions): UseSelectedSkillListenerReturn {
|
}: UseSelectedSkillListenerOptions): UseSelectedSkillListenerReturn {
|
||||||
|
const { t } = useI18n();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [selectedSkill, setSelectedSkill] = useState<SkillData | null>(null);
|
const [selectedSkill, setSelectedSkill] = useState<SkillData | null>(null);
|
||||||
const [skillError, setSkillError] = useState<SkillError | null>(null);
|
const [skillError, setSkillError] = useState<SkillError | null>(null);
|
||||||
|
|
@ -67,8 +69,8 @@ export function useSelectedSkillListener({
|
||||||
if (contentIds.length === 0) {
|
if (contentIds.length === 0) {
|
||||||
console.warn("[useSelectedSkillListener] 忽略非法 skill ids", skills);
|
console.warn("[useSelectedSkillListener] 忽略非法 skill ids", skills);
|
||||||
setSkillError({
|
setSkillError({
|
||||||
title: `技能「${title}」加载失败`,
|
title: t.skills.loadFailedWithTitle(title),
|
||||||
message: "非法 skill_id 数组",
|
message: t.skills.invalidSkillIdArray,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +89,7 @@ export function useSelectedSkillListener({
|
||||||
`[useSelectedSkillListener] 开始初始化技能: ${title} (${contentIds.join(",")})`,
|
`[useSelectedSkillListener] 开始初始化技能: ${title} (${contentIds.join(",")})`,
|
||||||
);
|
);
|
||||||
setIsBootstrapping(true);
|
setIsBootstrapping(true);
|
||||||
toast.loading(`正在加载技能「${title}」...`, { id: "skill-bootstrap" });
|
toast.loading(t.skills.loading(title), { id: "skill-bootstrap" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await bootstrapRemoteSkill({
|
const result = await bootstrapRemoteSkill({
|
||||||
|
|
@ -102,26 +104,26 @@ export function useSelectedSkillListener({
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
skillBootstrappedKeyRef.current = initKey;
|
skillBootstrappedKeyRef.current = initKey;
|
||||||
toast.success(`技能「${title}」加载成功`, {
|
toast.success(t.skills.loadSuccessWithTitle(title), {
|
||||||
description:
|
description: result.message || t.skills.createdFiles(result.created_files),
|
||||||
result.message || `已创建 ${result.created_files} 个文件`,
|
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setSkillError({
|
setSkillError({
|
||||||
title: `技能「${title}」加载失败`,
|
title: t.skills.loadFailedWithTitle(title),
|
||||||
message: result.message || "未知错误",
|
message: result.message || t.skills.unknownError,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.dismiss("skill-bootstrap");
|
toast.dismiss("skill-bootstrap");
|
||||||
const message = err instanceof Error ? err.message : "网络请求失败";
|
const message =
|
||||||
setSkillError({ title: `技能「${title}」加载出错`, message });
|
err instanceof Error ? err.message : t.skills.networkRequestFailed;
|
||||||
|
setSkillError({ title: t.skills.loadErrorWithTitle(title), message });
|
||||||
} finally {
|
} finally {
|
||||||
setIsBootstrapping(false);
|
setIsBootstrapping(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[threadId, searchParams],
|
[threadId, searchParams, t.skills],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1. URL 初始化集成
|
// 1. URL 初始化集成
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue