chore: update ji branch work
This commit is contained in:
1 parent
6c30e649e3
commit
cbd07c9d1b
9 files changed
+188
-124
No files matched your search
@@ -222,6 +222,7 @@ export function AgentManageDialog({
|
||||
key={agent.id}
|
||||
value={`${agent.name} ${agent.description ?? ""}`}
|
||||
onSelect={() => void add(agent)}
|
||||
className="flex min-w-0 items-center overflow-hidden"
|
||||
>
|
||||
<BotIcon className="text-muted-foreground mr-2 size-4 shrink-0" />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
|
||||
@@ -120,7 +120,7 @@ export function AgentSelector({
|
||||
value={`${agent.name} ${agent.description ?? ""}`}
|
||||
onSelect={() => handleSelect(agent)}
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
"flex min-w-0 items-center gap-2 overflow-hidden",
|
||||
agent.id === selectedAgentId && "bg-accent",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useAgents } from "@/core/agents/hooks";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import {
|
||||
useCreateCustomPrompt,
|
||||
useCustomPrompts,
|
||||
@@ -42,16 +43,6 @@ import {
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** Sentinel select value for "no agent association" (global prompt). */
|
||||
const NO_AGENT = "__global__";
|
||||
|
||||
/**
|
||||
* Bar above the chat input showing the user's saved prompts as centered,
|
||||
* clickable chips. When there are too many to fit, they become a horizontal
|
||||
* carousel (arrows appear). A gear pinned to the far right opens the manage
|
||||
* dialog. Prompts are bound to the account (server-backed), and this coexists
|
||||
* with the admin-configured global prompt prefix.
|
||||
*/
|
||||
export function CustomPromptBar({
|
||||
onInsert,
|
||||
className,
|
||||
@@ -59,19 +50,13 @@ export function CustomPromptBar({
|
||||
}: {
|
||||
onInsert: (content: string) => void;
|
||||
className?: string;
|
||||
/**
|
||||
* The agent currently selected for this chat (or null on the general
|
||||
* new-chat page). Agent-associated prompts only show when their ``agent_id``
|
||||
* matches; un-associated (global) prompts always show.
|
||||
*/
|
||||
currentAgentId?: string | null;
|
||||
}) {
|
||||
const allPrompts = useCustomPrompts();
|
||||
const prompts = allPrompts.filter(
|
||||
(p) => !p.agent_id || p.agent_id === currentAgentId,
|
||||
(prompt) => !prompt.agent_id || prompt.agent_id === currentAgentId,
|
||||
);
|
||||
const [manageOpen, setManageOpen] = useState(false);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [canLeft, setCanLeft] = useState(false);
|
||||
const [canRight, setCanRight] = useState(false);
|
||||
@@ -87,7 +72,7 @@ export function CustomPromptBar({
|
||||
updateArrows();
|
||||
window.addEventListener("resize", updateArrows);
|
||||
return () => window.removeEventListener("resize", updateArrows);
|
||||
}, [updateArrows, prompts]);
|
||||
}, [prompts, updateArrows]);
|
||||
|
||||
const scrollByDir = (dir: 1 | -1) => {
|
||||
scrollRef.current?.scrollBy({ left: dir * 220, behavior: "smooth" });
|
||||
@@ -97,7 +82,7 @@ export function CustomPromptBar({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn("flex items-center gap-1", className)}>
|
||||
<div className={cn("flex min-w-0 items-center gap-1", className)}>
|
||||
{prompts.length > 0 && (
|
||||
<>
|
||||
{overflowing && (
|
||||
@@ -116,7 +101,7 @@ export function CustomPromptBar({
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={updateArrows}
|
||||
className="flex-1 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
className="min-w-0 flex-1 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
<div className="mx-auto flex w-max items-center gap-1.5 px-0.5 py-0.5">
|
||||
{prompts.map((prompt) => (
|
||||
@@ -150,7 +135,7 @@ export function CustomPromptBar({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{prompts.length === 0 && <div className="flex-1" />}
|
||||
{prompts.length === 0 && <div className="min-w-0 flex-1" />}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -183,34 +168,33 @@ function ManagePromptsDialog({
|
||||
const { agents } = useAgents();
|
||||
const preferences = useUserPreferences();
|
||||
const updatePreferences = useUpdateUserPreferences();
|
||||
const currentUserId = getStoredAuth()?.user_id ?? null;
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [agentId, setAgentId] = useState<string>(NO_AGENT);
|
||||
const [agentId, setAgentId] = useState<string | undefined>(undefined);
|
||||
|
||||
const saving = createPrompt.isPending || updatePrompt.isPending;
|
||||
|
||||
const agentNameById = (id?: string | null) =>
|
||||
id ? agents.find((a) => a.id === id)?.name ?? null : null;
|
||||
|
||||
// Only offer the agents the user has favorited (收藏到「我的」) for
|
||||
// association — the full catalog is huge, mostly irrelevant, and overflowed
|
||||
// the dropdown. The agent a prompt is *already* bound to is kept in the list
|
||||
// even if it is no longer a favorite, so editing never silently drops it.
|
||||
const selectableAgents = useMemo(() => {
|
||||
const favorites = agents.filter((a) => a.is_favorite);
|
||||
if (agentId !== NO_AGENT && !favorites.some((a) => a.id === agentId)) {
|
||||
const bound = agents.find((a) => a.id === agentId);
|
||||
if (bound) return [bound, ...favorites];
|
||||
const owned = currentUserId
|
||||
? agents.filter((agent) => agent.user_id === currentUserId)
|
||||
: [];
|
||||
if (agentId && !owned.some((agent) => agent.id === agentId)) {
|
||||
const bound = agents.find((agent) => agent.id === agentId);
|
||||
if (bound) return [bound, ...owned];
|
||||
}
|
||||
return favorites;
|
||||
}, [agents, agentId]);
|
||||
return owned;
|
||||
}, [agents, agentId, currentUserId]);
|
||||
|
||||
const agentNameById = (id?: string | null) =>
|
||||
id ? agents.find((agent) => agent.id === id)?.name ?? null : null;
|
||||
|
||||
const resetForm = () => {
|
||||
setEditingId(null);
|
||||
setTitle("");
|
||||
setContent("");
|
||||
setAgentId(NO_AGENT);
|
||||
setAgentId(undefined);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -219,19 +203,23 @@ function ManagePromptsDialog({
|
||||
toast.error("提示词内容不能为空");
|
||||
return;
|
||||
}
|
||||
const boundAgentId = agentId === NO_AGENT ? null : agentId;
|
||||
if (!agentId) {
|
||||
toast.error("请选择关联智能体");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingId) {
|
||||
await updatePrompt.mutateAsync({
|
||||
id: editingId,
|
||||
input: { title: trimmedTitle, content, agent_id: boundAgentId },
|
||||
input: { title: trimmedTitle, content, agent_id: agentId },
|
||||
});
|
||||
toast.success("提示词已更新");
|
||||
} else {
|
||||
await createPrompt.mutateAsync({
|
||||
title: trimmedTitle,
|
||||
content,
|
||||
agent_id: boundAgentId,
|
||||
agent_id: agentId,
|
||||
});
|
||||
toast.success("提示词已添加");
|
||||
}
|
||||
@@ -242,12 +230,12 @@ function ManagePromptsDialog({
|
||||
};
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
const target = prompts.find((p) => p.id === id);
|
||||
const target = prompts.find((prompt) => prompt.id === id);
|
||||
if (!target) return;
|
||||
setEditingId(id);
|
||||
setTitle(target.title);
|
||||
setContent(target.content);
|
||||
setAgentId(target.agent_id || NO_AGENT);
|
||||
setAgentId(target.agent_id || undefined);
|
||||
};
|
||||
|
||||
const handleToggleHideAdmin = async (next: boolean) => {
|
||||
@@ -262,6 +250,7 @@ function ManagePromptsDialog({
|
||||
try {
|
||||
await deletePrompt.mutateAsync(id);
|
||||
if (editingId === id) resetForm();
|
||||
toast.success("提示词已删除");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
@@ -279,7 +268,7 @@ function ManagePromptsDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>管理常用提示词</DialogTitle>
|
||||
<DialogDescription>
|
||||
保存常用的提示词,保存后会以小标签形式显示在输入框上方,点击即可填入。提示词与你的账号绑定,可跨设备使用。
|
||||
保存常用提示词后,会显示在输入框上方。提示词与账号绑定,可跨设备使用。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -287,41 +276,44 @@ function ManagePromptsDialog({
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">只用我自己的提示词</p>
|
||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
||||
开启后,输入框不再显示管理员配置的提示词(提示词前缀)开关,本账号仅使用自己保存的提示词。
|
||||
开启后,输入框不再显示管理员配置的提示词前缀开关,本账号仅使用自己保存的提示词。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.hide_admin_prompt_prefix}
|
||||
disabled={updatePreferences.isPending}
|
||||
onCheckedChange={(v) => void handleToggleHideAdmin(v)}
|
||||
onCheckedChange={(value) => void handleToggleHideAdmin(value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 rounded-md border p-3">
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
placeholder="名称(可选,例如:周报总结)"
|
||||
/>
|
||||
<Textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onChange={(event) => setContent(event.target.value)}
|
||||
placeholder="提示词内容"
|
||||
className="min-h-28"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-muted-foreground text-xs">关联智能体(可选)</p>
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<p className="text-muted-foreground text-xs">关联智能体</p>
|
||||
<Select value={agentId} onValueChange={setAgentId}>
|
||||
<SelectTrigger className="w-full min-w-0 max-w-full overflow-hidden [&>span]:min-w-0 [&>span]:flex-1 [&>span]:truncate [&>span]:text-left">
|
||||
<SelectValue placeholder="不关联(所有对话可见)" />
|
||||
<SelectValue placeholder="请选择已配置的智能体" />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
position="popper"
|
||||
className="max-h-[40vh] w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)]"
|
||||
className="max-h-[40vh] w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)] overflow-hidden"
|
||||
>
|
||||
<SelectItem value={NO_AGENT}>不关联(所有对话可见)</SelectItem>
|
||||
{selectableAgents.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id} className="pr-8">
|
||||
<SelectItem
|
||||
key={agent.id}
|
||||
value={agent.id}
|
||||
className="max-w-full pr-8"
|
||||
>
|
||||
<span className="block max-w-full truncate">
|
||||
{desensitize(agent.name)}
|
||||
</span>
|
||||
@@ -331,13 +323,18 @@ function ManagePromptsDialog({
|
||||
</Select>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{selectableAgents.length === 0
|
||||
? "仅可关联你收藏到「我的」的常用智能体;当前还没有收藏任何智能体。"
|
||||
: "关联后,仅在选中该智能体的对话里显示此提示词。这里只列出你收藏的常用智能体。"}
|
||||
? "当前账号还没有已配置的智能体。"
|
||||
: "关联后,仅在选中该智能体的对话里显示此提示词。"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
{editingId && (
|
||||
<Button variant="outline" size="sm" onClick={resetForm} disabled={saving}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={resetForm}
|
||||
disabled={saving}
|
||||
>
|
||||
取消编辑
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -48,6 +48,13 @@ export function useUserPreferences(): UserPreferences {
|
||||
return data ?? DEFAULT_PREFS;
|
||||
}
|
||||
|
||||
export function useUserPreferencesQuery() {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEY,
|
||||
queryFn: getUserPreferences,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateUserPreferences() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
|
||||
@@ -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">
|
||||
|
||||
+11
-3
@@ -243,6 +243,17 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
|
||||
# Get newly uploaded files from the current message's additional_kwargs.files
|
||||
new_files = self._files_from_kwargs(last_message, uploads_dir) or []
|
||||
|
||||
# Only inject when the *current* message actually carries new uploads.
|
||||
# Turns without a fresh upload must not re-inject previously uploaded
|
||||
# files: the original upload turn already injected (and persisted) their
|
||||
# <uploaded_files> block, and the files remain physically available for
|
||||
# read_file/grep/view_image. Re-injecting on every later message made the
|
||||
# agent re-reference old images/docs, and — since the frontend parses
|
||||
# files out of the <uploaded_files> content when additional_kwargs.files
|
||||
# is empty — re-rendered the old attachment on each new question.
|
||||
if not new_files:
|
||||
return None
|
||||
|
||||
# Collect historical files from the uploads directory (all except the new ones)
|
||||
new_filenames = {f["filename"] for f in new_files}
|
||||
historical_files: list[dict] = []
|
||||
@@ -270,9 +281,6 @@ class UploadsMiddleware(AgentMiddleware[UploadsMiddlewareState]):
|
||||
file["outline"] = outline
|
||||
file["outline_preview"] = preview
|
||||
|
||||
if not new_files and not historical_files:
|
||||
return None
|
||||
|
||||
logger.debug(f"New files: {[f['filename'] for f in new_files]}, historical: {[f['filename'] for f in historical_files]}")
|
||||
|
||||
# Create files message and prepend to the last human message content
|
||||
|
||||
+5
-5
@@ -1007,11 +1007,11 @@ class ScheduledTaskService:
|
||||
if negative_prompt:
|
||||
parts.append(f"限制条件(最高优先级,必须严格遵守):{negative_prompt}")
|
||||
gather_prompt = "\n".join(parts)
|
||||
# When the Markdown switch is also on, ask this same gather pass to save a
|
||||
# .md file (the length gate is enforced in code, not surfaced here) so the
|
||||
# task produces both deliverables in one agent run.
|
||||
if also_markdown:
|
||||
gather_prompt += _MARKDOWN_INSTRUCTION
|
||||
# When the Markdown switch is also on, this same gather pass must save a
|
||||
# .md file. We do NOT append _MARKDOWN_INSTRUCTION here — `_start_agent_run`
|
||||
# already appends it exactly once when `require_markdown` is present in the
|
||||
# execution context (kept below). Appending it here too made the prompt
|
||||
# carry the 【输出要求】 block twice when both HTML and Markdown were on.
|
||||
gather_task = {**task, "prompt": gather_prompt}
|
||||
gather_ctx = dict(task.get("execution_context") or {})
|
||||
# Strip the HTML-specific flags so the gather pass is a plain agent run.
|
||||
|
||||
Reference in new issue
Block a user