智能体对话选 Agent 直达、澄清问题批量提交、LLM 错误中文化等
- 前端:新会话选 Agent 后直接跳转并自动发送,智能体页输入框宽度与首页对齐 - 前端:澄清卡片支持多选+多问题合并提交,消除单题逐一发送的割裂感 - 前端:定时任务弹窗 Agent 下拉支持长描述截断,避免撑破布局 - 后端:ask_clarification 支持同回合多次调用以提多个独立问题 - 后端:LLM 错误处理与定时任务失败提示统一改为中文 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c74be2b789
commit
ba2bc46042
7 files changed
+327
-73
No files matched your search
@@ -3,7 +3,7 @@
|
||||
import { BotIcon, PlusSquare } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useRouteBase } from "@/core/route-base";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -36,13 +36,30 @@ export default function AgentChatPage() {
|
||||
const routeBase = useRouteBase();
|
||||
const { t } = useI18n();
|
||||
const [showFollowups, setShowFollowups] = useState(false);
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const { agent_id } = useParams<{
|
||||
agent_id: string;
|
||||
}>();
|
||||
|
||||
// If ChatPage stashed an opening message for this agent, skip the landing
|
||||
// screen on first render so the user doesn't see a flash of the welcome UI
|
||||
// before the auto-sent message takes over.
|
||||
const hasPendingForThisAgent = () => {
|
||||
if (typeof window === "undefined" || !agent_id) return false;
|
||||
try {
|
||||
const raw = sessionStorage.getItem("pendingAgentMessage");
|
||||
if (!raw) return false;
|
||||
const parsed = JSON.parse(raw);
|
||||
return parsed?.agentId === agent_id && !!parsed?.text;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(
|
||||
hasPendingForThisAgent,
|
||||
);
|
||||
|
||||
const { agent } = useAgent(agent_id);
|
||||
|
||||
const { threadId, setThreadId, isNewThread, setIsNewThread } =
|
||||
@@ -52,9 +69,10 @@ export default function AgentChatPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewThread) {
|
||||
setHasStartedConversation(false);
|
||||
setHasStartedConversation(hasPendingForThisAgent());
|
||||
}
|
||||
}, [isNewThread, threadId]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isNewThread, threadId, agent_id]);
|
||||
|
||||
const { showNotification } = useNotification();
|
||||
const [thread, sendMessage] = useThreadStream({
|
||||
@@ -100,6 +118,26 @@ export default function AgentChatPage() {
|
||||
[sendMessage, threadId, agent_id],
|
||||
);
|
||||
|
||||
// ChatPage stashes the opening message in sessionStorage when the user picks
|
||||
// an agent on the landing screen, then redirects here. Replay it once so the
|
||||
// user doesn't have to re-type the question on the agent chat page.
|
||||
const pendingMessageHandled = useRef(false);
|
||||
useEffect(() => {
|
||||
if (pendingMessageHandled.current) return;
|
||||
if (!agent_id || !isNewThread || hasStartedConversation) return;
|
||||
let pending: { agentId?: string; text?: string } | null = null;
|
||||
try {
|
||||
const raw = sessionStorage.getItem("pendingAgentMessage");
|
||||
if (raw) pending = JSON.parse(raw);
|
||||
} catch {
|
||||
pending = null;
|
||||
}
|
||||
if (!pending || pending.agentId !== agent_id || !pending.text) return;
|
||||
pendingMessageHandled.current = true;
|
||||
sessionStorage.removeItem("pendingAgentMessage");
|
||||
handleSubmit({ text: pending.text, files: [] });
|
||||
}, [agent_id, isNewThread, hasStartedConversation, handleSubmit]);
|
||||
|
||||
const handleClarificationSubmit = useCallback(
|
||||
(answer: string) => {
|
||||
setHasStartedConversation(true);
|
||||
@@ -180,8 +218,8 @@ export default function AgentChatPage() {
|
||||
"relative w-full",
|
||||
isLandingState && "-translate-y-[calc(50vh-96px)]",
|
||||
isLandingState
|
||||
? "max-w-(--container-width-sm)"
|
||||
: "max-w-(--container-width-md)",
|
||||
? "max-w-(--container-width-md)"
|
||||
: "max-w-(--container-width-lg)",
|
||||
)}
|
||||
>
|
||||
<div className="absolute -top-4 right-0 left-0 z-0">
|
||||
|
||||
@@ -758,11 +758,11 @@ export default function ScheduledTasksPage() {
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={creating || !!editing} onOpenChange={(open) => !open && closeDialog()}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogContent className="max-w-2xl overflow-hidden">
|
||||
<DialogHeader className="min-w-0">
|
||||
<DialogTitle>{creating ? "新建定时任务" : "编辑定时任务"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex min-w-0 flex-col gap-3">
|
||||
<Input
|
||||
value={form.name}
|
||||
onChange={(event) =>
|
||||
@@ -798,15 +798,20 @@ export default function ScheduledTasksPage() {
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full min-w-0 max-w-full overflow-hidden [&>span]:min-w-0 [&>span]:flex-1 [&>span]:truncate">
|
||||
<SelectValue placeholder="选择执行智能体" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent
|
||||
position="popper"
|
||||
className="w-[var(--radix-select-trigger-width)] max-w-[var(--radix-select-trigger-width)]"
|
||||
>
|
||||
<SelectItem value="__default__">默认助手(不指定)</SelectItem>
|
||||
{agents.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
{agent.name}
|
||||
{agent.description ? ` — ${agent.description}` : ""}
|
||||
<span className="block truncate">
|
||||
{agent.name}
|
||||
{agent.description ? ` — ${agent.description}` : ""}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
Reference in new issue
Block a user