feat: 提问导航悬浮索引 + 知识库召回会话级开关 + 智能体技能白名单收紧
- 新增 QuestionNavigator:聊天/智能体页右侧悬浮提问索引,点击滚动定位到对应消息(message-list 为 human 消息生成锚点 id) - 知识库召回开关从单会话头部按钮迁移到侧边栏,改为会话级内存开关(刷新重置,不再写 localStorage),开启时所有对话默认携带 knowledge_rag_enabled - 智能体可配置技能收紧为「内置 + 自己的 + 已发布到广场的」(新增 core/skills/utils.ts 的 isAgentSkillCandidate,NewAgentPage 与 agent-detail-panel 共用) - 智能体详情面板在 artifact 预览展开/新文件生成时自动收起让位 - 启用 html-multipage 技能并更新后端 CLAUDE.md 文档 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
81f2a48c6d
commit
d9d5780ea7
16 files changed
+598
-49
No files matched your search
@@ -16,6 +16,7 @@ import { ExportTrigger } from "@/components/workspace/export-trigger";
|
||||
import { InputBox } from "@/components/workspace/input-box";
|
||||
import { MessageList } from "@/components/workspace/messages";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import { QuestionNavigator } from "@/components/workspace/question-navigator";
|
||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||
import { TodoList } from "@/components/workspace/todo-list";
|
||||
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
|
||||
@@ -182,8 +183,9 @@ export default function AgentChatPage() {
|
||||
<div className="flex size-full min-h-0 overflow-hidden">
|
||||
<div className="min-h-0 min-w-0 flex-1">
|
||||
<ChatBox threadId={threadId}>
|
||||
{/* Main conversation column */}
|
||||
<main className="flex size-full min-h-0 min-w-0 flex-col">
|
||||
{/* Main conversation column. `relative` hosts the floating
|
||||
question navigator overlay. */}
|
||||
<main className="relative flex size-full min-h-0 min-w-0 flex-col">
|
||||
{/* Header in normal flow, fixed height. */}
|
||||
<header className="bg-card/90 border-border/70 flex h-12 shrink-0 items-center gap-2 border-b px-4 backdrop-blur-md">
|
||||
<div className="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1">
|
||||
@@ -271,6 +273,12 @@ export default function AgentChatPage() {
|
||||
/* Conversation: messages scroll, input
|
||||
docked at the bottom of the main column only. */
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{/* Question index pinned to this conversation column; the
|
||||
bottom inset keeps it clear of the docked input box. */}
|
||||
<QuestionNavigator
|
||||
messages={thread.messages}
|
||||
className="top-14 right-0 bottom-40 left-0"
|
||||
/>
|
||||
<MessageList
|
||||
className="min-h-0 flex-1"
|
||||
threadId={threadId}
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from "@/components/workspace/chats";
|
||||
import { ExportTrigger } from "@/components/workspace/export-trigger";
|
||||
import { KnowledgeSaveTrigger } from "@/components/workspace/knowledge-save-trigger";
|
||||
import { KnowledgeRagToggle } from "@/components/workspace/knowledge-rag-toggle";
|
||||
import { InputBox } from "@/components/workspace/input-box";
|
||||
import {
|
||||
MessageList,
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM,
|
||||
} from "@/components/workspace/messages";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import { QuestionNavigator } from "@/components/workspace/question-navigator";
|
||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||
import { TodoList } from "@/components/workspace/todo-list";
|
||||
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
|
||||
@@ -35,6 +35,7 @@ import { useNotification } from "@/core/notification/hooks";
|
||||
import {
|
||||
getLastAgentId,
|
||||
saveLastAgentId,
|
||||
useKnowledgeRagEnabled,
|
||||
useThreadSettings,
|
||||
} from "@/core/settings";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
@@ -90,6 +91,18 @@ export default function ChatPage() {
|
||||
const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } =
|
||||
useThreadChat();
|
||||
const [settings, setSettings] = useThreadSettings(threadId);
|
||||
// Sidebar session toggle: while on, every message in every chat carries
|
||||
// knowledge_rag_enabled so the backend injects knowledge-base recall.
|
||||
// In-memory only — explicitly overrides any value persisted by the old
|
||||
// per-thread header toggle so stale localStorage can't re-enable it.
|
||||
const [knowledgeRagEnabled] = useKnowledgeRagEnabled();
|
||||
const streamContext = useMemo(
|
||||
() => ({
|
||||
...settings.context,
|
||||
knowledge_rag_enabled: knowledgeRagEnabled,
|
||||
}),
|
||||
[settings.context, knowledgeRagEnabled],
|
||||
);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { tokenUsageEnabled } = useModels();
|
||||
const { data: promptPrefixSettings } = usePromptPrefixSettings();
|
||||
@@ -136,7 +149,7 @@ export default function ChatPage() {
|
||||
|
||||
const [thread, sendMessage, isUploading] = useThreadStream({
|
||||
threadId: isNewThread ? undefined : threadId,
|
||||
context: settings.context,
|
||||
context: streamContext,
|
||||
isMock,
|
||||
onStart: (createdThreadId) => {
|
||||
setThreadId(createdThreadId);
|
||||
@@ -410,7 +423,6 @@ export default function ChatPage() {
|
||||
enabled={tokenUsageEnabled}
|
||||
messages={thread.messages}
|
||||
/>
|
||||
<KnowledgeRagToggle threadId={threadId} />
|
||||
<ExportTrigger threadId={threadId} />
|
||||
<KnowledgeSaveTrigger threadId={threadId} />
|
||||
<ArtifactTrigger />
|
||||
@@ -521,6 +533,19 @@ export default function ChatPage() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{/*
|
||||
ChatGPT-style question index. Anchored to the chat column (this
|
||||
relative wrapper), not the browser edge; insets keep it below the
|
||||
overlay header (h-12) and above the docked input box, and it is
|
||||
pointer-events-none outside its own cards so it never blocks the
|
||||
header buttons or input.
|
||||
*/}
|
||||
{!isLandingState && (
|
||||
<QuestionNavigator
|
||||
messages={thread.messages}
|
||||
className="top-16 right-0 bottom-48 left-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ChatBox>
|
||||
</ThreadContext.Provider>
|
||||
|
||||
@@ -47,9 +47,10 @@ import {
|
||||
createAgent,
|
||||
updateAgent,
|
||||
} from "@/core/agents/api";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import type { Skill } from "@/core/skills";
|
||||
import { isAgentSkillCandidate, type Skill } from "@/core/skills";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
import { uuid } from "@/core/utils/uuid";
|
||||
@@ -94,9 +95,19 @@ export default function NewAgentPage() {
|
||||
const { skills, isLoading: isSkillsLoading } = useSkills();
|
||||
const { models, isLoading: isModelsLoading } = useModels();
|
||||
|
||||
// 可配置的技能限定为「内置 + 自己的 + 已发布到广场的」,
|
||||
// 其他用户未发布的自定义技能不可选。
|
||||
const storedAuth = useMemo(() => getStoredAuth(), []);
|
||||
const enabledSkills = useMemo(
|
||||
() => skills.filter((skill) => skill.enabled),
|
||||
[skills],
|
||||
() =>
|
||||
skills.filter((skill) =>
|
||||
isAgentSkillCandidate(
|
||||
skill,
|
||||
storedAuth?.user_id ?? null,
|
||||
storedAuth?.system_role === "admin",
|
||||
),
|
||||
),
|
||||
[skills, storedAuth],
|
||||
);
|
||||
|
||||
const filteredSkills = useMemo(() => {
|
||||
|
||||
Reference in new issue
Block a user