feat: 智能体精选排序与新对话智能体选择器

- 后端新增 featured_order 字段及精选排序 API(/agents/selector、/agents/{id}/featured、/agents/featured/order),移除 user_agent_pin 表
- 前端新对话页 (ChatPage) 与输入框 (InputBox) 接入 agent-selector,选中后跳转 AgentChatPage 并通过 sessionStorage 透传首条消息
- 新增管理员智能体管理弹窗 (agent-manage-dialog) 支持精选添加/移除/上下移动
This commit is contained in:
吉鑫升 committed 2026-05-21 14:39:12 +08:00
1 parent a8a545690f
commit f1d58983c2
21 files changed
+1300 -100

No files matched your search

+37 -10
View File
@@ -3,11 +3,10 @@
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";
import { AgentWelcome } from "@/components/workspace/agent-welcome";
import { ArtifactTrigger } from "@/components/workspace/artifacts";
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
import { ExportTrigger } from "@/components/workspace/export-trigger";
@@ -56,6 +55,11 @@ export default function AgentChatPage() {
}
}, [isNewThread, threadId]);
// Auto-submit a message that the user typed on ChatPage right before
// selecting this agent. ChatPage stashed it in sessionStorage so we don't
// depend on router state (the next/navigation shim doesn't carry it).
const autoSubmitDoneRef = useRef(false);
const { showNotification } = useNotification();
const [thread, sendMessage] = useThreadStream({
threadId: isNewThread ? undefined : threadId,
@@ -100,6 +104,29 @@ export default function AgentChatPage() {
[sendMessage, threadId, agent_id],
);
useEffect(() => {
if (autoSubmitDoneRef.current) return;
if (!isNewThread || hasStartedConversation) return;
let raw: string | null = null;
try {
raw = sessionStorage.getItem("pendingAgentMessage");
} catch {
return;
}
if (!raw) return;
try {
const parsed = JSON.parse(raw) as { agentId?: string; text?: string };
if (parsed.agentId !== agent_id || !parsed.text) {
return;
}
autoSubmitDoneRef.current = true;
sessionStorage.removeItem("pendingAgentMessage");
handleSubmit({ text: parsed.text, files: [] });
} catch {
sessionStorage.removeItem("pendingAgentMessage");
}
}, [agent_id, hasStartedConversation, handleSubmit, isNewThread]);
const handleClarificationSubmit = useCallback(
(answer: string) => {
setHasStartedConversation(true);
@@ -116,7 +143,12 @@ export default function AgentChatPage() {
? MESSAGE_LIST_DEFAULT_PADDING_BOTTOM +
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM
: undefined;
const isLandingState = isNewThread && !hasStartedConversation;
// AgentChatPage skips the centered welcome/landing state entirely — when the
// user reaches this page they've already committed to an agent (either by
// selecting one on the homepage or by visiting the agent's URL), so we drop
// them straight into the conversation layout: messages area on top, input at
// the bottom. The agent badge in the header still identifies the agent.
const isLandingState = false;
return (
<ThreadContext.Provider value={{ thread }}>
@@ -198,9 +230,9 @@ export default function AgentChatPage() {
<InputBox
className={cn("bg-background/5 w-full -translate-y-4")}
isNewThread={isLandingState}
isNewThread={isNewThread}
threadId={threadId}
autoFocus={isLandingState}
autoFocus={isNewThread}
status={
thread.error
? "error"
@@ -209,11 +241,6 @@ export default function AgentChatPage() {
: "ready"
}
context={settings.context}
extraHeader={
isLandingState && (
<AgentWelcome agent={agent} agentName={agent?.name ?? agent_id} />
)
}
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
onContextChange={(context) => setSettings("context", context)}
onFollowupsVisibilityChange={setShowFollowups}
+40
View File
@@ -1,5 +1,6 @@
"use client";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
@@ -34,6 +35,7 @@ import { cn } from "@/lib/utils";
export default function ChatPage() {
const routeBase = useRouteBase();
const router = useRouter();
const { t } = useI18n();
const [showFollowups, setShowFollowups] = useState(false);
const [hasStartedConversation, setHasStartedConversation] = useState(false);
@@ -44,6 +46,7 @@ export default function ChatPage() {
const { tokenUsageEnabled } = useModels();
const { data: promptPrefixSettings } = usePromptPrefixSettings();
const [promptPrefixOn, setPromptPrefixOn] = useState(false);
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const { selectedArtifact } = useArtifacts();
useSpecificChatMode();
@@ -55,6 +58,7 @@ export default function ChatPage() {
if (isNewThread) {
setHasStartedConversation(false);
setPromptPrefixOn(false);
setSelectedAgentId(null);
}
}, [isNewThread, threadId]);
@@ -105,6 +109,32 @@ export default function ChatPage() {
const handleSubmit = useCallback(
(message: PromptInputMessage) => {
// If the user picked an agent before sending the opening turn, hand off
// to AgentChatPage so the conversation runs against that agent. We stash
// the pending text in sessionStorage because the next/navigation shim
// doesn't carry router state across hash-route pushes. File attachments
// are bound to the current thread upload directory and would not match
// the new thread, so we skip the handoff when files are present and let
// the message run on Lead Agent instead.
if (
selectedAgentId &&
isNewThread &&
!hasStartedConversation &&
message.text &&
(!message.files || message.files.length === 0)
) {
try {
sessionStorage.setItem(
"pendingAgentMessage",
JSON.stringify({ agentId: selectedAgentId, text: message.text }),
);
} catch {
// sessionStorage may be unavailable in private mode; fall through.
}
router.push(`/page/workspace/agents/${selectedAgentId}/chats/new`);
return;
}
// Only the opening turn of a brand-new thread may carry the prefix.
const shouldApplyPrefix =
isNewThread &&
@@ -140,6 +170,8 @@ export default function ChatPage() {
hasStartedConversation,
promptPrefixOn,
promptPrefixSettings,
selectedAgentId,
router,
settings.context.writing_mode,
writingArtifactPath,
],
@@ -258,6 +290,14 @@ export default function ChatPage() {
setSettings("context", context)
}
promptPrefix={promptPrefixProp}
agentSelector={
isLandingState
? {
selectedAgentId,
onAgentChange: setSelectedAgentId,
}
: undefined
}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}