From 0e4caa5a663c9ac52a15d29c7a6f4695e571f3f4 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Tue, 16 Jun 2026 16:37:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai-writing):=20=E8=AF=BE=E9=A2=98=E7=A0=94?= =?UTF-8?q?=E7=A9=B6=E7=AC=94=E8=AE=B0=E6=9C=AC=E5=86=85=E5=B5=8C=E6=89=93?= =?UTF-8?q?=E5=BC=80=20+=20=E6=96=B0=E5=BB=BA/=E6=89=80=E6=9C=89=E7=A9=BA?= =?UTF-8?q?=E9=97=B4=E5=85=A5=E5=8F=A3=EF=BC=880616=20#5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 左栏点笔记本/所有空间 → 右侧内嵌打开(不跳路由),写作布局保持挂载(display:none,不丢 setup 线程),顶部「返回写作」 - ResearchSidebar 加「新建空间」(弹窗 createSearchSpace) 与「所有空间」入口 - StudioNotebookDetailPage 加可选 notebookId prop;StudioNotebooksPage 加可选 onOpenNotebook(内嵌时卡片走回调而非 Link),列表内点空间内嵌打开详情 - 内嵌笔记本详情用 ChatRuntime 包裹(修复 useArtifacts must be used within an ArtifactsProvider) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/ai-writing/ResearchSidebar.tsx | 169 ++++++++++++++---- .../src/open-canvas/pages/AIWritingPage.tsx | 69 ++++++- .../src/pages/StudioNotebookDetailPage.tsx | 11 +- .../src/pages/StudioNotebooksPage.tsx | 41 ++++- 4 files changed, 243 insertions(+), 47 deletions(-) diff --git a/frontend-web/src/open-canvas/components/ai-writing/ResearchSidebar.tsx b/frontend-web/src/open-canvas/components/ai-writing/ResearchSidebar.tsx index 1d6a19cc..c7d0aeec 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/ResearchSidebar.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/ResearchSidebar.tsx @@ -1,8 +1,18 @@ "use client"; -import { BookOpen, FileText } from "lucide-react"; -import { useEffect } from "react"; +import { BookOpen, FileText, LayoutGrid, Loader2, Plus } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { getStoredAuth } from "@/core/auth"; +import { createSearchSpace } from "@/core/studio/api"; import { useStudioNotebooks } from "@/core/studio/hooks"; import { cn } from "@/lib/utils"; @@ -16,38 +26,64 @@ import { useWritingForm } from "../../contexts/WritingFormContext"; * - 下半「笔记本」= 我的空间(``useStudioNotebooks``);点击设为当前课题研究的素材来源 * (materialSource='notebook' + 选中该本)。 */ -export function ResearchSidebar() { +export function ResearchSidebar({ + viewingNotebookId, + onOpenNotebook, + onOpenAllSpaces, +}: { + /** 当前在右侧查看的笔记本 id(高亮用)。 */ + viewingNotebookId?: number | null; + /** 点击笔记本 → 在右侧内嵌打开该笔记本页(替换右侧内容)。 */ + onOpenNotebook: (notebookId: number) => void; + /** 点击「所有空间」→ 在右侧内嵌打开空间列表页。 */ + onOpenAllSpaces: () => void; +}) { const { articleTypes, refresh } = useArticleTypes(); - const { notebooks } = useStudioNotebooks(); const { - articleType, - setArticleType, - setWordCountInput, - setFormExpanded, - materialSource, - setMaterialSource, - selectedNotebookId, - setSelectedNotebookId, - setSelectedDocumentIds, - } = useWritingForm(); + notebooks, + isLoading: notebooksLoading, + isError: notebooksError, + } = useStudioNotebooks(); + const { articleType, setArticleType, setWordCountInput, setFormExpanded } = + useWritingForm(); + const queryClient = useQueryClient(); + + // 新建空间弹窗(与 workspace-nav-studio 同款)。 + const [createOpen, setCreateOpen] = useState(false); + const [name, setName] = useState(""); + const [creating, setCreating] = useState(false); + const [createError, setCreateError] = useState(""); useEffect(() => { void refresh(); }, [refresh]); + const handleCreate = async () => { + const trimmed = name.trim(); + if (!trimmed) return; + setCreating(true); + setCreateError(""); + try { + const email = getStoredAuth()?.email ?? ""; + const token = queryClient.getQueryData(["studio", "token", email]); + if (!token) throw new Error("未登录或 token 已过期,请刷新页面"); + await createSearchSpace(trimmed, token); + setCreateOpen(false); + setName(""); + await queryClient.invalidateQueries({ queryKey: ["studio", "notebooks"] }); + } catch (e) { + setCreateError(e instanceof Error ? e.message : "创建失败"); + } finally { + setCreating(false); + } + }; + 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 ( ); } diff --git a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx index 4835ac97..8df94935 100644 --- a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx +++ b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx @@ -81,8 +81,8 @@ * -d '{"retention_days": 0}' * ────────────────────────────────────────────────────────────────────────── */ -import { useRef } from 'react'; -import { PanelRightOpen } from 'lucide-react'; +import { useRef, useState } from 'react'; +import { ArrowLeft, PanelRightOpen } from 'lucide-react'; import { TooltipProvider } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; import type { GroupImperativeHandle } from 'react-resizable-panels'; @@ -94,6 +94,12 @@ 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 StudioNotebookDetailPage from '@/pages/StudioNotebookDetailPage'; +import StudioNotebooksPage from '@/pages/StudioNotebooksPage'; +import { ChatRuntime } from '@/pages/WorkspaceRoutes'; + +/** 右侧内嵌视图:笔记本详情 / 所有空间列表 / null(写作) */ +type RightView = { kind: 'notebook'; id: number } | { kind: 'allSpaces' } | null; import { AIWritingProvider, useAIWriting } from '../contexts/AIWritingContext'; import { AIWritingHistoryProvider, useAIWritingHistory } from '../contexts/AIWritingHistoryContext'; import { WritingFormProvider, useWritingForm } from '../contexts/WritingFormContext'; @@ -222,6 +228,12 @@ export default function AIWritingPage({ initialDocumentIds?: number[]; studioToken?: string; } = {}) { + // 需求 0616 #5:左栏点「笔记本 / 所有空间」→ 在右侧内嵌打开对应页,替换右侧内容 + // (写作布局不卸载,仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程)。 + const [rightView, setRightView] = useState(null); + const overlayActive = rightView != null; + const viewingNotebookId = rightView?.kind === 'notebook' ? rightView.id : null; + return ( @@ -237,9 +249,56 @@ export default function AIWritingPage({
{/* 需求 0616 #5:课题研究左栏(研究模板 + 笔记本)。studio 嵌入态(studioToken)隐藏。 */} - {!studioToken && } -
- + {!studioToken && ( + setRightView({ kind: 'notebook', id })} + onOpenAllSpaces={() => setRightView({ kind: 'allSpaces' })} + /> + )} + {/* 右侧:写作布局常驻(仅 display:none 隐藏,避免 WritingSetupChat 重挂丢线程), + 查看笔记本 / 所有空间时在其上叠加内嵌页。 */} +
+
+ +
+ {overlayActive && ( +
+
+ + {rightView.kind === 'notebook' && ( + + )} +
+
+ {rightView.kind === 'notebook' ? ( + // 笔记本详情页内部用 ChatBox(useArtifacts) → 需 ChatRuntime 提供 Provider。 + + + + ) : ( +
+ setRightView({ kind: 'notebook', id })} + /> +
+ )} +
+
+ )}
diff --git a/frontend-web/src/pages/StudioNotebookDetailPage.tsx b/frontend-web/src/pages/StudioNotebookDetailPage.tsx index ed841f52..b0747e85 100644 --- a/frontend-web/src/pages/StudioNotebookDetailPage.tsx +++ b/frontend-web/src/pages/StudioNotebookDetailPage.tsx @@ -209,8 +209,15 @@ function SourcesDialog({ } // ── 主页面 ────────────────────────────────────────────── -export default function StudioNotebookDetailPage() { - const { notebookId } = useParams<{ notebookId: string }>(); +export default function StudioNotebookDetailPage({ + notebookId: notebookIdProp, +}: { + /** 内嵌使用(如课题研究右侧)时直接传入;缺省则从路由参数取。 */ + notebookId?: number; +} = {}) { + const params = useParams<{ notebookId: string }>(); + const notebookId = + notebookIdProp != null ? String(notebookIdProp) : params.notebookId; const { notebooks, isLoading, token: studioToken } = useStudioNotebooks(); // token 用于跳转 callback const { models } = useModels(); const queryClient = useQueryClient(); diff --git a/frontend-web/src/pages/StudioNotebooksPage.tsx b/frontend-web/src/pages/StudioNotebooksPage.tsx index a8952b76..e090a6ae 100644 --- a/frontend-web/src/pages/StudioNotebooksPage.tsx +++ b/frontend-web/src/pages/StudioNotebooksPage.tsx @@ -39,17 +39,16 @@ function NotebookCard({ nb, onRename, onDelete, + onOpen, }: { nb: StudioNotebook; onRename: (nb: StudioNotebook) => void; onDelete: (nb: StudioNotebook) => void; + /** 传入则点击卡片回调(内嵌打开),不再路由跳转。 */ + onOpen?: (id: number) => void; }) { - return ( -
- + const inner = ( + <>
{nb.avatar_url ? ( @@ -67,7 +66,27 @@ function NotebookCard({ {nb.source_count} 个来源
- + + ); + + return ( +
+ {onOpen ? ( + + ) : ( + + {inner} + + )} {/* three-dot menu — absolute so it doesn't shift layout */}
@@ -99,7 +118,12 @@ function NotebookCard({ ); } -export default function StudioNotebooksPage() { +export default function StudioNotebooksPage({ + onOpenNotebook, +}: { + /** 内嵌使用(课题研究右侧)时传入:点击空间卡片回调,不跳路由。 */ + onOpenNotebook?: (id: number) => void; +} = {}) { const { notebooks, isLoading, isError } = useStudioNotebooks(); const queryClient = useQueryClient(); @@ -204,6 +228,7 @@ export default function StudioNotebooksPage() { nb={nb} onRename={(nb) => { setRenameTarget(nb); setRenameName(nb.name); setRenameError(""); }} onDelete={(nb) => { setDeleteTarget(nb); setDeleteError(""); }} + onOpen={onOpenNotebook} /> ))}