feat(qa-center): 问答中心左侧栏改造(0616 需求 2-5)
- 多智能体会商:Step1 落地左栏「业务链(public 前3) + 分析记录」;点链=勾选(autoChainResult),第一步澄清后点「进入研讨」才按链执行
- zzJC/zzXD:独立模块路由 /page/strategy/qa/{zzjc,zzxd},常驻左栏(按 agent 标签分组 + 仅本模块会话的聊天记录),点智能体模块内切换不跳页,默认落地配置类型下首个智能体
- zzJC/zzXD admin 设置:左栏齿轮→单选类型(TDesign)配置面板+智能体预览;后端 system_settings.agent_modules 持久化(GET 开放/PUT admin)+ test_agent_modules
- 课题研究:左栏「研究模板(文章类型) + 笔记本」;模板点击预填表单,笔记本点击设为素材来源(selectedNotebookId 上提 WritingFormContext)
- 通用问答:全量模式历史侧栏默认展开
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8794c51f88
commit
f39cffcb9a
18 files changed
+1113
-41
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { PenLine, SlidersHorizontal } from 'lucide-react';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -55,8 +55,6 @@ export function AIWritingForm({
|
||||
onSubmit,
|
||||
disabled,
|
||||
hideAdvancedFields = false,
|
||||
initialNotebookId,
|
||||
initialDocumentIds,
|
||||
studioToken,
|
||||
}: AIWritingFormProps) {
|
||||
const {
|
||||
@@ -68,13 +66,12 @@ export function AIWritingForm({
|
||||
wordCountInput, setWordCountInput,
|
||||
strictMode, setStrictMode,
|
||||
materialSource, setMaterialSource,
|
||||
// 笔记来源(search_space_id + document_id 列表)已上提到 WritingFormContext,
|
||||
// 以便左栏「笔记本」点击可直接设为素材来源(需求 0616 #5)。
|
||||
selectedNotebookId, setSelectedNotebookId,
|
||||
selectedDocumentIds, setSelectedDocumentIds,
|
||||
modelName, setModelName,
|
||||
} = useWritingForm();
|
||||
// 笔记来源:选中的 search_space_id 和多个 document_id
|
||||
const [selectedNotebookId, setSelectedNotebookId] = useState<number | null>(
|
||||
initialNotebookId ?? null,
|
||||
);
|
||||
const [selectedDocumentIds, setSelectedDocumentIds] = useState<number[]>(initialDocumentIds ?? []);
|
||||
|
||||
const { models, isLoading: modelsLoading } = useModels();
|
||||
const { notebooks, isLoading: notebooksLoading } = useStudioNotebooks(!studioToken);
|
||||
@@ -146,8 +143,10 @@ export function AIWritingForm({
|
||||
}
|
||||
|
||||
function toggleDocument(docId: number) {
|
||||
setSelectedDocumentIds((prev) =>
|
||||
prev.includes(docId) ? prev.filter((id) => id !== docId) : [...prev, docId],
|
||||
setSelectedDocumentIds(
|
||||
selectedDocumentIds.includes(docId)
|
||||
? selectedDocumentIds.filter((id) => id !== docId)
|
||||
: [...selectedDocumentIds, docId],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
import { BookOpen, FileText } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useStudioNotebooks } from "@/core/studio/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { useArticleTypes } from "../../hooks/useArticleTypes";
|
||||
import { useWritingForm } from "../../contexts/WritingFormContext";
|
||||
|
||||
/**
|
||||
* 课题研究左栏(需求 0616 #5)。
|
||||
* - 上半「研究模板」= 文章类型(``useArticleTypes``);点击预填表单(设置文章类型 +
|
||||
* 默认字数 + 展开写作配置),沿用现有 articleType 设置逻辑。
|
||||
* - 下半「笔记本」= 我的空间(``useStudioNotebooks``);点击设为当前课题研究的素材来源
|
||||
* (materialSource='notebook' + 选中该本)。
|
||||
*/
|
||||
export function ResearchSidebar() {
|
||||
const { articleTypes, refresh } = useArticleTypes();
|
||||
const { notebooks } = useStudioNotebooks();
|
||||
const {
|
||||
articleType,
|
||||
setArticleType,
|
||||
setWordCountInput,
|
||||
setFormExpanded,
|
||||
materialSource,
|
||||
setMaterialSource,
|
||||
selectedNotebookId,
|
||||
setSelectedNotebookId,
|
||||
setSelectedDocumentIds,
|
||||
} = useWritingForm();
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const handleTemplateClick = (key: string, defaultWordCount: number) => {
|
||||
setArticleType(key);
|
||||
if (defaultWordCount > 0) setWordCountInput(String(defaultWordCount));
|
||||
setFormExpanded(true);
|
||||
};
|
||||
|
||||
const handleNotebookClick = (notebookId: number) => {
|
||||
setMaterialSource("notebook");
|
||||
setSelectedNotebookId(notebookId);
|
||||
setSelectedDocumentIds([]);
|
||||
setFormExpanded(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="flex h-full w-64 shrink-0 flex-col overflow-hidden border-r border-border/70 bg-card/60 backdrop-blur-sm">
|
||||
{/* 上半:研究模板(文章类型) */}
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-2 py-3">
|
||||
<h2 className="text-muted-foreground/70 px-2 mb-1.5 text-[10px] font-semibold tracking-[0.12em] uppercase select-none">
|
||||
研究模板
|
||||
</h2>
|
||||
{articleTypes.length === 0 ? (
|
||||
<p className="px-2 py-1.5 text-xs text-muted-foreground/70">暂无研究模板</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
{articleTypes.map((t) => {
|
||||
const active = articleType === t.key;
|
||||
return (
|
||||
<button
|
||||
key={t.key}
|
||||
type="button"
|
||||
onClick={() => handleTemplateClick(t.key, t.default_word_count)}
|
||||
title={t.description ?? t.label}
|
||||
className={cn(
|
||||
"flex items-start gap-2 rounded-lg px-2.5 py-1.5 text-left transition-colors",
|
||||
active
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-sidebar-accent/60 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<FileText className="mt-0.5 size-3.5 shrink-0 text-muted-foreground/70" />
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-[13px] font-medium">{t.label}</span>
|
||||
{t.description && (
|
||||
<span className="text-muted-foreground/70 line-clamp-1 block text-[11px]">
|
||||
{t.description}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 下半:笔记本(我的空间) */}
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto border-t border-border/60 px-2 py-3">
|
||||
<h2 className="text-muted-foreground/70 px-2 mb-1.5 text-[10px] font-semibold tracking-[0.12em] uppercase select-none">
|
||||
笔记本
|
||||
</h2>
|
||||
{notebooks.length === 0 ? (
|
||||
<p className="px-2 py-1.5 text-xs text-muted-foreground/70">暂无笔记本</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{notebooks.map((nb) => {
|
||||
const active =
|
||||
materialSource === "notebook" && selectedNotebookId === nb.id;
|
||||
return (
|
||||
<button
|
||||
key={nb.id}
|
||||
type="button"
|
||||
onClick={() => handleNotebookClick(nb.id)}
|
||||
title={nb.description || nb.name}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-[13px] transition-colors",
|
||||
active
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-muted-foreground hover:bg-sidebar-accent/60 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<BookOpen className="size-3.5 shrink-0 text-muted-foreground/70" />
|
||||
<span className="truncate">{nb.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,12 @@ export interface WritingFormValue {
|
||||
setStrictMode: (v: boolean) => void;
|
||||
materialSource: 'general' | 'notebook';
|
||||
setMaterialSource: (v: 'general' | 'notebook') => void;
|
||||
/** 我的空间(笔记本)来源:选中的 search_space_id;上提到 Context 以便左栏「笔记本」点击直接设为素材来源。 */
|
||||
selectedNotebookId: number | null;
|
||||
setSelectedNotebookId: (v: number | null) => void;
|
||||
/** 选中笔记本下的文档 id 列表(缩小检索范围)。 */
|
||||
selectedDocumentIds: number[];
|
||||
setSelectedDocumentIds: (v: number[]) => void;
|
||||
modelName: string;
|
||||
setModelName: (v: string) => void;
|
||||
/** 把模型建议(setup_writing 审批 payload)回填到表单各字段。 */
|
||||
@@ -56,7 +62,17 @@ export function useWritingFormOptional(): WritingFormValue | null {
|
||||
return useContext(WritingFormContext);
|
||||
}
|
||||
|
||||
export function WritingFormProvider({ children, initialMaterialSource }: { children: ReactNode; initialMaterialSource?: 'general' | 'notebook' }) {
|
||||
export function WritingFormProvider({
|
||||
children,
|
||||
initialMaterialSource,
|
||||
initialNotebookId,
|
||||
initialDocumentIds,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
initialMaterialSource?: 'general' | 'notebook';
|
||||
initialNotebookId?: number | null;
|
||||
initialDocumentIds?: number[];
|
||||
}) {
|
||||
const [intent, setIntent] = useState('');
|
||||
const [formExpanded, setFormExpanded] = useState(false);
|
||||
const [showOutline, setShowOutline] = useState(true);
|
||||
@@ -66,6 +82,8 @@ export function WritingFormProvider({ children, initialMaterialSource }: { child
|
||||
const [audience, setAudience] = useState('普通读者');
|
||||
const [strictMode, setStrictMode] = useState(false);
|
||||
const [materialSource, setMaterialSource] = useState<'general' | 'notebook'>(initialMaterialSource ?? 'general');
|
||||
const [selectedNotebookId, setSelectedNotebookId] = useState<number | null>(initialNotebookId ?? null);
|
||||
const [selectedDocumentIds, setSelectedDocumentIds] = useState<number[]>(initialDocumentIds ?? []);
|
||||
const [modelName, setModelName] = useState('');
|
||||
|
||||
const applyProposal = useCallback((payload: SetupWritingApprovalPayload) => {
|
||||
@@ -100,10 +118,12 @@ export function WritingFormProvider({ children, initialMaterialSource }: { child
|
||||
audience, setAudience,
|
||||
strictMode, setStrictMode,
|
||||
materialSource, setMaterialSource,
|
||||
selectedNotebookId, setSelectedNotebookId,
|
||||
selectedDocumentIds, setSelectedDocumentIds,
|
||||
modelName, setModelName,
|
||||
applyProposal,
|
||||
}),
|
||||
[intent, formExpanded, showOutline, outlineText, articleType, wordCountInput, audience, strictMode, materialSource, modelName, applyProposal],
|
||||
[intent, formExpanded, showOutline, outlineText, articleType, wordCountInput, audience, strictMode, materialSource, selectedNotebookId, selectedDocumentIds, modelName, applyProposal],
|
||||
);
|
||||
|
||||
return <WritingFormContext.Provider value={value}>{children}</WritingFormContext.Provider>;
|
||||
|
||||
@@ -93,6 +93,7 @@ import { CanvasUIProvider } from '../contexts/CanvasUIContext';
|
||||
import { AIWritingPanel } from '../components/ai-writing/AIWritingPanel';
|
||||
import { AIWritingDraftPanel } from '../components/ai-writing/AIWritingDraftPanel';
|
||||
import { WritingSetupChat } from '../components/ai-writing/WritingSetupChat';
|
||||
import { ResearchSidebar } from '../components/ai-writing/ResearchSidebar';
|
||||
import { AIWritingProvider, useAIWriting } from '../contexts/AIWritingContext';
|
||||
import { AIWritingHistoryProvider, useAIWritingHistory } from '../contexts/AIWritingHistoryContext';
|
||||
import { WritingFormProvider, useWritingForm } from '../contexts/WritingFormContext';
|
||||
@@ -227,11 +228,19 @@ export default function AIWritingPage({
|
||||
<CanvasUIProvider>
|
||||
<AIWritingHistoryProvider>
|
||||
<AIWritingProvider>
|
||||
<WritingFormProvider initialMaterialSource={studioToken ?? initialNotebookId != null ? 'notebook' : undefined}>
|
||||
<WritingFormProvider
|
||||
initialMaterialSource={studioToken ?? initialNotebookId != null ? 'notebook' : undefined}
|
||||
initialNotebookId={initialNotebookId}
|
||||
initialDocumentIds={initialDocumentIds}
|
||||
>
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<div className="h-full flex flex-col overflow-hidden bg-background canvas-writing-bg">
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
||||
<div className="flex-1 min-h-0 overflow-hidden flex">
|
||||
{/* 需求 0616 #5:课题研究左栏(研究模板 + 笔记本)。studio 嵌入态(studioToken)隐藏。 */}
|
||||
{!studioToken && <ResearchSidebar />}
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
Reference in new issue
Block a user