chore: update ji branch work

This commit is contained in:
吉鑫升 committed 2026-06-04 17:46:11 +08:00
1 parent 6c30e649e3
commit cbd07c9d1b
9 files changed
+188 -124

No files matched your search

+8 -2
View File
@@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { usePromptPrefixSettings } from "@/core/system-settings/hooks";
import { useUserPreferences } from "@/core/user-preferences";
import { useUserPreferencesQuery } from "@/core/user-preferences";
import { ArtifactTrigger, useArtifacts } from "@/components/workspace/artifacts";
import {
ChatBox,
@@ -53,7 +53,9 @@ export default function ChatPage() {
const { data: promptPrefixSettings } = usePromptPrefixSettings();
// Personal opt-out: when set, hide the admin prompt-prefix switch entirely and
// never apply the prefix — the user uses only their own prompts.
const { hide_admin_prompt_prefix: hideAdminPrompt } = useUserPreferences();
const { data: userPreferences, isLoading: preferencesLoading } =
useUserPreferencesQuery();
const hideAdminPrompt = userPreferences?.hide_admin_prompt_prefix === true;
const [promptPrefixOn, setPromptPrefixOn] = useState(false);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const { agents: selectorAgents, isLoading: selectorLoading } =
@@ -163,6 +165,7 @@ export default function ChatPage() {
isNewThread &&
!hasStartedConversation &&
promptPrefixOn &&
!preferencesLoading &&
!hideAdminPrompt &&
promptPrefixSettings?.enabled === true;
setHasStartedConversation(true);
@@ -195,6 +198,7 @@ export default function ChatPage() {
promptPrefixOn,
promptPrefixSettings,
hideAdminPrompt,
preferencesLoading,
selectedAgentId,
router,
settings.context.writing_mode,
@@ -223,6 +227,7 @@ export default function ChatPage() {
const promptPrefixProp = useMemo(() => {
if (!promptPrefixSettings?.enabled) return undefined;
if (preferencesLoading) return undefined;
// Personal opt-out hides the admin prompt-prefix switch entirely.
if (hideAdminPrompt) return undefined;
// The prefix toggle is offered only on the opening turn of a new thread.
@@ -235,6 +240,7 @@ export default function ChatPage() {
}, [
promptPrefixSettings,
hideAdminPrompt,
preferencesLoading,
isNewThread,
hasStartedConversation,
promptPrefixOn,
@@ -257,6 +257,7 @@ function stripTimestampSuffix(title: string): string {
}
const BODY_TAB_VALUE = "__body__";
const HTML_TAB_VALUE = "__html__";
const RUN_LIST_COLLAPSED_WIDTH = 56;
const RUN_LIST_DEFAULT_WIDTH = 380;
const RUN_LIST_MIN_WIDTH = 280;
@@ -427,12 +428,8 @@ export default function PublicScheduledTaskPage() {
className="relative flex min-h-0 shrink-0 transition-[width] duration-200 ease-out"
style={{ width: visibleRunListWidth }}
>
<Card
className={cn(
"flex min-h-0 w-full flex-col overflow-hidden",
!runListExpanded && "pointer-events-none [&>*]:invisible",
)}
>
{runListExpanded ? (
<Card className="flex min-h-0 w-full flex-col overflow-hidden">
<CardHeader className="gap-1">
<div className="flex items-start justify-between gap-2">
<Tooltip>
@@ -447,9 +444,21 @@ export default function PublicScheduledTaskPage() {
</TooltipContent>
)}
</Tooltip>
<Badge variant={task?.enabled ? "default" : "secondary"} className="shrink-0">
{task?.enabled ? "启用" : "暂停"}
</Badge>
<div className="flex shrink-0 items-center gap-1">
<Badge variant={task?.enabled ? "default" : "secondary"}>
{task?.enabled ? "启用" : "暂停"}
</Badge>
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-primary size-7"
onClick={collapseRunList}
title="收起执行记录列表"
aria-label="收起执行记录列表"
>
<ChevronLeft className="size-4" />
</Button>
</div>
</div>
{task?.description && task.description.trim() && (
<p className="text-muted-foreground line-clamp-3 text-xs leading-relaxed break-all whitespace-pre-wrap">
@@ -552,15 +561,20 @@ export default function PublicScheduledTaskPage() {
</ScrollArea>
</CardContent>
</Card>
<button
type="button"
onClick={runListExpanded ? collapseRunList : expandRunList}
className="bg-background text-muted-foreground hover:bg-accent hover:text-primary focus-visible:ring-ring absolute top-3 left-1/2 z-30 flex size-9 -translate-x-1/2 items-center justify-center rounded-md border shadow-sm transition-colors focus:outline-none focus-visible:ring-2"
title={runListExpanded ? "收起执行记录列表" : "展开执行记录列表"}
aria-label={runListExpanded ? "收起执行记录列表" : "展开执行记录列表"}
>
{runListExpanded ? <ChevronLeft className="size-4" /> : <List className="size-4" />}
</button>
) : (
<Card className="flex min-h-0 w-full flex-col items-center overflow-hidden py-3">
<Button
size="icon"
variant="ghost"
className="text-muted-foreground hover:text-primary"
onClick={expandRunList}
title="展开执行记录列表"
aria-label="展开执行记录列表"
>
<List className="size-4" />
</Button>
</Card>
)}
<div
role="separator"
aria-orientation="vertical"
@@ -638,12 +652,14 @@ export default function PublicScheduledTaskPage() {
}
type ContentTab = {
/** Stable tab identifier — `__body__` for inline content, `file-<index>` for files. */
/** Stable tab identifier — `__html__` for the HTML preview, `__body__` for
* inline content, `file-<index>` for files. */
value: string;
/** Short label rendered in the TabsTrigger. */
label: string;
/** Distinguishes body vs MD attachment so the editor knows which PUT to call. */
kind: "body" | "file";
/** Distinguishes the HTML preview vs body vs MD attachment so the renderer /
* editor knows what to show and which PUT to call. */
kind: "body" | "file" | "html";
/** File index (when `kind === "file"`). */
fileIndex?: number;
/** Filename for downloads (defaults to the run title for body). */
@@ -684,17 +700,27 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
[run.result?.files],
);
// Tabs: every MD file first (priority per spec), then the inline body.
// Tabs: HTML preview first (when present), then every MD file, then the
// inline body. For an HTML run that also produced a Markdown file, this keeps
// the page preview AND surfaces the .md instead of hiding it. The generated
// HTML document itself is never listed as a downloadable "file" tab — it has
// its own preview.
const tabs: ContentTab[] = useMemo(() => {
const list: ContentTab[] = markdownFiles.map((file, idx) => ({
value: `file-${file.index}`,
// Show the actual filename without the `.md` suffix — the user explicitly
// asked for "no '文件' label, just the name without extension".
label: stripExtension(file.name) || `文件 ${idx + 1}`,
kind: "file",
fileIndex: file.index,
fileName: file.name,
}));
const list: ContentTab[] = [];
if (isHtmlRun) {
list.push({ value: HTML_TAB_VALUE, label: "HTML 预览", kind: "html" });
}
list.push(
...markdownFiles.map((file, idx) => ({
value: `file-${file.index}`,
// Show the actual filename without the `.md` suffix — the user explicitly
// asked for "no '文件' label, just the name without extension".
label: stripExtension(file.name) || `文件 ${idx + 1}`,
kind: "file" as const,
fileIndex: file.index,
fileName: file.name,
})),
);
list.push({
value: BODY_TAB_VALUE,
label: "正文",
@@ -702,7 +728,7 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
fileName: run.result?.title || "scheduled-task-result",
});
return list;
}, [markdownFiles, run.result?.title]);
}, [isHtmlRun, markdownFiles, run.result?.title]);
const [activeTab, setActiveTab] = useState<string>(tabs[0]?.value ?? BODY_TAB_VALUE);
@@ -843,13 +869,13 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
下载原文件
</a>
)}
{!isHtmlRun && (
{activeTabMeta?.kind !== "html" && (
<Button size="sm" variant="outline" onClick={handleDownloadWord}>
<FileText className="size-4" />
下载 Word
</Button>
)}
{!isHtmlRun &&
{activeTabMeta?.kind !== "html" &&
(isEditing ? (
<>
<Button
@@ -895,22 +921,7 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
</CardHeader>
<CardContent className="min-h-0 flex-1 overflow-hidden p-0">
{isHtmlRun ? (
<div className="h-full p-4">
{htmlContent ? (
<iframe
title="HTML 预览"
sandbox="allow-same-origin"
srcDoc={htmlContent}
className="h-full min-h-[60vh] w-full rounded-md border bg-white"
/>
) : (
<div className="text-muted-foreground rounded-md border p-3 text-sm">
HTML 页面尚未生成或正在生成中。
</div>
)}
</div>
) : tabs.length === 0 ? (
{tabs.length === 0 ? (
<div className="text-muted-foreground p-6 text-sm">无可用内容。</div>
) : (
<Tabs
@@ -940,7 +951,20 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
{run.error}
</div>
)}
{fileIsLoading ? (
{activeTabMeta?.kind === "html" ? (
htmlContent ? (
<iframe
title="HTML 预览"
sandbox="allow-same-origin"
srcDoc={htmlContent}
className="min-h-[60vh] w-full flex-1 rounded-md border bg-white"
/>
) : (
<div className="text-muted-foreground rounded-md border p-3 text-sm">
HTML 页面尚未生成或正在生成中。
</div>
)
) : fileIsLoading ? (
<div className="text-muted-foreground flex items-center gap-2 text-sm">
<Loader2 className="size-4 animate-spin" />
正在加载文件内容...
@@ -270,7 +270,16 @@ export default function ScheduledTaskRunDetailPage() {
}
};
const resultFiles = useMemo(() => result?.files ?? [], [result]);
// The generated HTML document is already shown in the dedicated "HTML 预览"
// tab, so it is excluded from the "结果文件" list — that list surfaces the
// Markdown (and any other non-HTML artifact) only.
const resultFiles = useMemo(
() =>
(result?.files ?? []).filter(
(f) => !(f.kind === "html" || (f.name ?? "").toLowerCase().endsWith(".html")),
),
[result],
);
const fileCount = resultFiles.length;
const resultMetadata = useMemo(() => {
if (!result) return "";
@@ -295,18 +304,28 @@ export default function ScheduledTaskRunDetailPage() {
}
setFilesLoading(true);
try {
const loaded = await Promise.all(
// Load each file independently. A single failing file (e.g. an archive
// that 404s) must NOT blank the whole panel — otherwise the tab shows
// "结果文件(2)" while the body says "没有生成文件". Render every file
// that loads and skip the ones that don't.
const settled = await Promise.allSettled(
resultFiles.map((f) => {
const index = result.files.indexOf(f);
return getScheduledTaskRunFileContent(run.id, index);
}),
);
if (!cancelled) {
const loaded = settled
.filter(
(r): r is PromiseFulfilledResult<ScheduledTaskResultFileContent> =>
r.status === "fulfilled",
)
.map((r) => r.value);
setFileContents(loaded);
}
} catch (error) {
if (!cancelled) {
toast.error(error instanceof Error ? error.message : "加载文件内容失败");
const failed = settled.length - loaded.length;
if (failed > 0 && loaded.length === 0) {
toast.error("结果文件已不可用(可能已被清理)");
}
}
} finally {
if (!cancelled) {
@@ -584,7 +603,9 @@ export default function ScheduledTaskRunDetailPage() {
)}
{!filesLoading && fileContents.length === 0 && (
<div className="text-muted-foreground rounded-md border p-3 text-sm">
本次执行没有生成文件。
{fileCount > 0
? `本次执行生成了 ${fileCount} 个文件,但当前无法读取(文件可能已被清理或移动)。可尝试「下载全部」。`
: "本次执行没有生成文件。"}
</div>
)}
<div className="flex flex-col gap-3">