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:
1 parent
a8a545690f
commit
f1d58983c2
21 files changed
+1300
-100
No files matched your search
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user