fix: polish compact workspace controls
This commit is contained in:
1 parent
eb61b332a2
commit
210cb5117f
13 files changed
+503
-261
No files matched your search
@@ -138,6 +138,7 @@ export function AgentDetailPanel({
|
|||||||
disabled,
|
disabled,
|
||||||
defaultCollapsed,
|
defaultCollapsed,
|
||||||
showRecentChats,
|
showRecentChats,
|
||||||
|
compactAppearanceActions,
|
||||||
}: {
|
}: {
|
||||||
agent: Agent | null | undefined;
|
agent: Agent | null | undefined;
|
||||||
agentName: string;
|
agentName: string;
|
||||||
@@ -147,6 +148,8 @@ export function AgentDetailPanel({
|
|||||||
defaultCollapsed?: boolean;
|
defaultCollapsed?: boolean;
|
||||||
/** 在面板底部展示「有关此智能体的最近对话」(仅智能体问答页开启,任务工作区不展示)。 */
|
/** 在面板底部展示「有关此智能体的最近对话」(仅智能体问答页开启,任务工作区不展示)。 */
|
||||||
showRecentChats?: boolean;
|
showRecentChats?: boolean;
|
||||||
|
/** 简洁模式下挂在右上角收起/展开按钮左侧的外观控件。 */
|
||||||
|
compactAppearanceActions?: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false);
|
const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false);
|
||||||
@@ -830,6 +833,7 @@ export function AgentDetailPanel({
|
|||||||
<span className="text-sm font-semibold">智能体信息</span>
|
<span className="text-sm font-semibold">智能体信息</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{headerActions}
|
{headerActions}
|
||||||
|
{compactAppearanceActions}
|
||||||
{collapseButton}
|
{collapseButton}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -841,17 +845,20 @@ export function AgentDetailPanel({
|
|||||||
<>
|
<>
|
||||||
{/* ── Desktop (lg+): inline aside, collapsible to a rail ───────────── */}
|
{/* ── Desktop (lg+): inline aside, collapsible to a rail ───────────── */}
|
||||||
{collapsed ? (
|
{collapsed ? (
|
||||||
<div className="hidden shrink-0 flex-col items-center border-l pt-3 lg:flex">
|
<div className="hidden shrink-0 items-start border-l px-2 pt-3 lg:flex">
|
||||||
<Tooltip content="智能体信息">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
{compactAppearanceActions}
|
||||||
variant="ghost"
|
<Tooltip content="智能体信息">
|
||||||
size="icon-sm"
|
<Button
|
||||||
onClick={() => setCollapsed(false)}
|
variant="ghost"
|
||||||
aria-label="展开智能体信息"
|
size="icon-sm"
|
||||||
>
|
onClick={() => setCollapsed(false)}
|
||||||
<PanelRightOpenIcon className="size-4" />
|
aria-label="展开智能体信息"
|
||||||
</Button>
|
>
|
||||||
</Tooltip>
|
<PanelRightOpenIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<aside className="bg-card/40 hidden h-full w-[360px] shrink-0 flex-col border-l lg:flex">
|
<aside className="bg-card/40 hidden h-full w-[360px] shrink-0 flex-col border-l lg:flex">
|
||||||
|
|||||||
@@ -136,9 +136,11 @@ const LAYOUT_OPTIONS = [
|
|||||||
export function LayoutModeSwitcher({
|
export function LayoutModeSwitcher({
|
||||||
tone,
|
tone,
|
||||||
fallback = "compact",
|
fallback = "compact",
|
||||||
|
navigateTo = "/page/workspace/chats/new",
|
||||||
}: {
|
}: {
|
||||||
tone: AppearanceControlTone;
|
tone: AppearanceControlTone;
|
||||||
fallback?: "full" | "compact";
|
fallback?: "full" | "compact";
|
||||||
|
navigateTo?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const [mode, setLayoutMode] = useLayoutMode();
|
const [mode, setLayoutMode] = useLayoutMode();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -170,7 +172,9 @@ export function LayoutModeSwitcher({
|
|||||||
key={option.id}
|
key={option.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLayoutMode(option.id);
|
setLayoutMode(option.id);
|
||||||
navigate("/page/workspace/chats/new");
|
if (navigateTo) {
|
||||||
|
navigate(navigateTo);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useMemo } from "react";
|
import { type ReactNode, useMemo } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
@@ -33,8 +33,11 @@ export function WorkspaceContainer({
|
|||||||
export function WorkspaceHeader({
|
export function WorkspaceHeader({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
rightActions,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"header">) {
|
}: React.ComponentProps<"header"> & {
|
||||||
|
rightActions?: ReactNode;
|
||||||
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const segments = useMemo(() => {
|
const segments = useMemo(() => {
|
||||||
@@ -90,6 +93,9 @@ export function WorkspaceHeader({
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
{rightActions ? (
|
||||||
|
<div className="flex items-center gap-2 px-4">{rightActions}</div>
|
||||||
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useStudioNotebooks, useNotebookSources } from '@/core/studio/hooks';
|
|||||||
import type { AIWritingRequest } from '../../types/ai-writing';
|
import type { AIWritingRequest } from '../../types/ai-writing';
|
||||||
import { useArticleTypes } from '../../hooks/useArticleTypes';
|
import { useArticleTypes } from '../../hooks/useArticleTypes';
|
||||||
import { useWritingForm } from '../../contexts/WritingFormContext';
|
import { useWritingForm } from '../../contexts/WritingFormContext';
|
||||||
|
import { ResearchTemplatesPanel } from './ResearchTemplatesPanel';
|
||||||
|
|
||||||
interface AIWritingFormProps {
|
interface AIWritingFormProps {
|
||||||
onSubmit: (req: AIWritingRequest) => void;
|
onSubmit: (req: AIWritingRequest) => void;
|
||||||
@@ -436,6 +437,8 @@ export function AIWritingForm({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ResearchTemplatesPanel />
|
||||||
|
|
||||||
{!hideAdvancedFields && (
|
{!hideAdvancedFields && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-start gap-2 rounded-xl border border-[#e8dedd] bg-[#f9f5f5] px-3 py-3">
|
<div className="flex items-start gap-2 rounded-xl border border-[#e8dedd] bg-[#f9f5f5] px-3 py-3">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { ChevronsDown, CloudUpload, PanelRightClose, Pause, Play, Plus, RotateCcw, Settings2, Trash2 } from 'lucide-react';
|
import { ChevronsDown, CloudUpload, PanelRightClose, Pause, Play, Plus, RotateCcw, Settings2, Trash2 } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -73,11 +73,13 @@ export function AIWritingPanel({
|
|||||||
initialNotebookId,
|
initialNotebookId,
|
||||||
initialDocumentIds,
|
initialDocumentIds,
|
||||||
studioToken,
|
studioToken,
|
||||||
|
headerActionsBeforeCollapse,
|
||||||
}: {
|
}: {
|
||||||
hideAdvancedFields?: boolean;
|
hideAdvancedFields?: boolean;
|
||||||
initialNotebookId?: number;
|
initialNotebookId?: number;
|
||||||
initialDocumentIds?: number[];
|
initialDocumentIds?: number[];
|
||||||
studioToken?: string;
|
studioToken?: string;
|
||||||
|
headerActionsBeforeCollapse?: ReactNode;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
@@ -429,6 +431,7 @@ export function AIWritingPanel({
|
|||||||
onRename={rename}
|
onRename={rename}
|
||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
/>
|
/>
|
||||||
|
{headerActionsBeforeCollapse}
|
||||||
{/* 收起写作配置:仅在 idle 配置阶段、表单展开时出现(与 AIWritingPage 的整块收起联动) */}
|
{/* 收起写作配置:仅在 idle 配置阶段、表单展开时出现(与 AIWritingPage 的整块收起联动) */}
|
||||||
{writingForm && isIdle && !isHistoryMode && writingForm.formExpanded && (
|
{writingForm && isIdle && !isHistoryMode && writingForm.formExpanded && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BarChart3,
|
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Eye,
|
Eye,
|
||||||
FileText,
|
FileText,
|
||||||
Globe2,
|
|
||||||
History,
|
History,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Loader2,
|
Loader2,
|
||||||
Newspaper,
|
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
Pencil,
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
|
||||||
Settings2,
|
|
||||||
SlidersHorizontal,
|
|
||||||
Sparkles,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
TrendingUp,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -38,64 +28,11 @@ import { createSearchSpace } from "@/core/studio/api";
|
|||||||
import { useStudioNotebooks } from "@/core/studio/hooks";
|
import { useStudioNotebooks } from "@/core/studio/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { useResearchTemplates } from "../../hooks/useResearchTemplates";
|
|
||||||
import { useAIWritingSessions } from "../../hooks/useAIWritingSessions";
|
import { useAIWritingSessions } from "../../hooks/useAIWritingSessions";
|
||||||
import type { ResearchTemplate } from "../../api/ai-writing-research-templates";
|
|
||||||
import type { AIWritingSessionSummary } from "../../api/ai-writing-sessions";
|
import type { AIWritingSessionSummary } from "../../api/ai-writing-sessions";
|
||||||
import { useWritingForm } from "../../contexts/WritingFormContext";
|
|
||||||
import { useAIWriting } from "../../contexts/AIWritingContext";
|
|
||||||
import { useAIWritingHistory } from "../../contexts/AIWritingHistoryContext";
|
import { useAIWritingHistory } from "../../contexts/AIWritingHistoryContext";
|
||||||
import { formatRelative } from "./AIWritingHistoryDropdown";
|
import { formatRelative } from "./AIWritingHistoryDropdown";
|
||||||
|
|
||||||
type TemplateVisual = {
|
|
||||||
Icon: LucideIcon;
|
|
||||||
iconClass: string;
|
|
||||||
accentClass: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TEMPLATE_VISUALS: TemplateVisual[] = [
|
|
||||||
{
|
|
||||||
Icon: Globe2,
|
|
||||||
iconClass: "bg-blue-50 text-blue-600",
|
|
||||||
accentClass: "bg-blue-500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: Newspaper,
|
|
||||||
iconClass: "bg-amber-50 text-amber-600",
|
|
||||||
accentClass: "bg-amber-500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: Sparkles,
|
|
||||||
iconClass: "bg-violet-50 text-violet-600",
|
|
||||||
accentClass: "bg-violet-500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: TrendingUp,
|
|
||||||
iconClass: "bg-rose-50 text-rose-600",
|
|
||||||
accentClass: "bg-rose-500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: BookOpen,
|
|
||||||
iconClass: "bg-emerald-50 text-emerald-600",
|
|
||||||
accentClass: "bg-emerald-500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: BarChart3,
|
|
||||||
iconClass: "bg-cyan-50 text-cyan-600",
|
|
||||||
accentClass: "bg-cyan-500",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function getTemplateVisual(tpl: ResearchTemplate, index: number): TemplateVisual {
|
|
||||||
const text = `${tpl.name} ${tpl.description ?? ""} ${tpl.article_type_key ?? ""}`;
|
|
||||||
if (/网摘|网络|热点|摘/.test(text)) return TEMPLATE_VISUALS[0];
|
|
||||||
if (/周报|周刊|日报|月报/.test(text)) return TEMPLATE_VISUALS[1];
|
|
||||||
if (/要讯|资讯|快讯|简报/.test(text)) return TEMPLATE_VISUALS[2];
|
|
||||||
if (/行业|分析|市场|竞争/.test(text)) return TEMPLATE_VISUALS[3];
|
|
||||||
if (/政策|解读|法规/.test(text)) return TEMPLATE_VISUALS[4];
|
|
||||||
return TEMPLATE_VISUALS[index % TEMPLATE_VISUALS.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 历史记录状态药丸样式(对齐多智能体会商的历史记录观感)。
|
// 历史记录状态药丸样式(对齐多智能体会商的历史记录观感)。
|
||||||
// 注:`in_progress` 不再转圈显示「进行中」——刷新=重置后这些会话已无任何活进程,
|
// 注:`in_progress` 不再转圈显示「进行中」——刷新=重置后这些会话已无任何活进程,
|
||||||
// 转圈会误导用户以为还在跑;统一显示为中性、不动的「未完成」。真正还在跑的是 `background`
|
// 转圈会误导用户以为还在跑;统一显示为中性、不动的「未完成」。真正还在跑的是 `background`
|
||||||
@@ -237,25 +174,11 @@ export function ResearchSidebar({
|
|||||||
/** 收起侧栏(由页面承载收起态)。 */
|
/** 收起侧栏(由页面承载收起态)。 */
|
||||||
onCollapse?: () => void;
|
onCollapse?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { templates, refresh } = useResearchTemplates();
|
|
||||||
const { startWriting } = useAIWriting();
|
|
||||||
const {
|
const {
|
||||||
notebooks,
|
notebooks,
|
||||||
isLoading: notebooksLoading,
|
isLoading: notebooksLoading,
|
||||||
isError: notebooksError,
|
isError: notebooksError,
|
||||||
} = useStudioNotebooks();
|
} = useStudioNotebooks();
|
||||||
const {
|
|
||||||
setIntent,
|
|
||||||
setArticleType,
|
|
||||||
setOutlineText,
|
|
||||||
setShowOutline,
|
|
||||||
setMaterialSource,
|
|
||||||
setWordCountInput,
|
|
||||||
setAudience,
|
|
||||||
setStrictMode,
|
|
||||||
setFormExpanded,
|
|
||||||
} = useWritingForm();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// 「历史记录」列表(左下角,样式参考多智能体会商的历史记录):自带一份会话列表,
|
// 「历史记录」列表(左下角,样式参考多智能体会商的历史记录):自带一份会话列表,
|
||||||
@@ -283,26 +206,11 @@ export function ResearchSidebar({
|
|||||||
window.dispatchEvent(new CustomEvent("ai-writing:show-progress", { detail: session }));
|
window.dispatchEvent(new CustomEvent("ai-writing:show-progress", { detail: session }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const [templateSearch, setTemplateSearch] = useState("");
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
const [createError, setCreateError] = useState("");
|
const [createError, setCreateError] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
void refresh();
|
|
||||||
}, [refresh]);
|
|
||||||
|
|
||||||
const filteredTemplates = useMemo(() => {
|
|
||||||
const keyword = templateSearch.trim().toLowerCase();
|
|
||||||
if (!keyword) return templates;
|
|
||||||
return templates.filter((tpl) =>
|
|
||||||
[tpl.name, tpl.description, tpl.article_type_key]
|
|
||||||
.filter(Boolean)
|
|
||||||
.some((value) => String(value).toLowerCase().includes(keyword)),
|
|
||||||
);
|
|
||||||
}, [templates, templateSearch]);
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
const trimmed = name.trim();
|
const trimmed = name.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
@@ -323,39 +231,6 @@ export function ResearchSidebar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTemplateClick = (tpl: ResearchTemplate) => {
|
|
||||||
const intent = tpl.topic?.trim() ?? "";
|
|
||||||
const articleType = tpl.article_type_key || "analysis";
|
|
||||||
const outline = tpl.outline?.trim() || "";
|
|
||||||
const audience = tpl.audience?.trim() || "普通读者";
|
|
||||||
const wordCount = tpl.word_count > 0 ? tpl.word_count : 800;
|
|
||||||
const writingMode = tpl.strict_mode ? "strict" : "loose";
|
|
||||||
|
|
||||||
if (intent) setIntent(intent);
|
|
||||||
if (tpl.article_type_key) setArticleType(tpl.article_type_key);
|
|
||||||
if (outline) {
|
|
||||||
setOutlineText(outline);
|
|
||||||
setShowOutline(true);
|
|
||||||
}
|
|
||||||
setMaterialSource(tpl.material_source);
|
|
||||||
setWordCountInput(String(wordCount));
|
|
||||||
if (tpl.audience?.trim()) setAudience(audience);
|
|
||||||
setStrictMode(tpl.strict_mode);
|
|
||||||
setFormExpanded(true);
|
|
||||||
|
|
||||||
if (!intent) return;
|
|
||||||
void startWriting({
|
|
||||||
userIntent: intent,
|
|
||||||
userOutline: outline || undefined,
|
|
||||||
articleType,
|
|
||||||
targetAudience: audience,
|
|
||||||
wordCountTarget: wordCount,
|
|
||||||
keywordCount: 1,
|
|
||||||
writingMode,
|
|
||||||
materialSource: tpl.material_source,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="ai-research-sidebar flex h-full w-[320px] shrink-0 flex-col overflow-hidden border-r border-slate-100 bg-white text-slate-700 shadow-[1px_0_18px_rgba(15,23,42,0.03)]">
|
<aside className="ai-research-sidebar flex h-full w-[320px] shrink-0 flex-col overflow-hidden border-r border-slate-100 bg-white text-slate-700 shadow-[1px_0_18px_rgba(15,23,42,0.03)]">
|
||||||
{onCollapse && (
|
{onCollapse && (
|
||||||
@@ -373,91 +248,7 @@ export function ResearchSidebar({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden px-3 py-4">
|
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden px-3 py-4">
|
||||||
<section className="mb-5">
|
<section className="flex flex-col">
|
||||||
<div className="mb-3 flex items-center justify-between gap-3 px-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => navigate("/page/canvas/research-templates")}
|
|
||||||
title="配置研究模板"
|
|
||||||
aria-label="配置研究模板"
|
|
||||||
className="group flex items-center gap-2 rounded-lg text-[16px] font-bold text-slate-500 transition-colors hover:text-slate-800"
|
|
||||||
>
|
|
||||||
<SlidersHorizontal className="size-4 text-slate-400" />
|
|
||||||
<span>研究模板</span>
|
|
||||||
<Settings2 className="size-3.5 text-slate-300 opacity-0 transition-opacity group-hover:opacity-100" />
|
|
||||||
</button>
|
|
||||||
<div className="relative w-[150px]">
|
|
||||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-3.5 -translate-y-1/2 text-slate-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={templateSearch}
|
|
||||||
onChange={(event) => setTemplateSearch(event.target.value)}
|
|
||||||
placeholder="搜索写作模板..."
|
|
||||||
className="h-8 w-full rounded-lg border border-slate-200 bg-slate-50/80 pl-8 pr-3 text-[12px] text-slate-600 outline-none transition focus:border-blue-200 focus:bg-white focus:ring-2 focus:ring-blue-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{templates.length === 0 ? (
|
|
||||||
<p className="rounded-xl border border-dashed border-slate-200 px-3 py-5 text-center text-xs text-slate-400">
|
|
||||||
暂无研究模板,点击标题配置
|
|
||||||
</p>
|
|
||||||
) : filteredTemplates.length === 0 ? (
|
|
||||||
<p className="rounded-xl border border-dashed border-slate-200 px-3 py-5 text-center text-xs text-slate-400">
|
|
||||||
暂无匹配模板
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{filteredTemplates.map((tpl, index) => {
|
|
||||||
const visual = getTemplateVisual(tpl, index);
|
|
||||||
const Icon = visual.Icon;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={tpl.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleTemplateClick(tpl)}
|
|
||||||
title={tpl.description ?? tpl.name}
|
|
||||||
className="group relative flex w-full items-start gap-3 overflow-hidden rounded-[5px] bg-white px-4 py-4 text-left shadow-[0_8px_24px_rgba(15,23,42,0.04)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_30px_rgba(30,64,175,0.08)] active:translate-y-0"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"flex size-12 shrink-0 items-center justify-center rounded-xl transition-colors",
|
|
||||||
visual.iconClass,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="size-6 stroke-[2.2]" />
|
|
||||||
</span>
|
|
||||||
<span className="min-w-0 flex-1 pt-0.5">
|
|
||||||
<span className="block truncate text-[16px] font-bold leading-5 text-slate-900 transition-colors group-hover:text-blue-700">
|
|
||||||
{tpl.name}
|
|
||||||
</span>
|
|
||||||
{tpl.description && (
|
|
||||||
<span
|
|
||||||
className="mt-1.5 overflow-hidden text-[13px] leading-6 text-slate-500"
|
|
||||||
style={{
|
|
||||||
display: "-webkit-box",
|
|
||||||
WebkitBoxOrient: "vertical",
|
|
||||||
WebkitLineClamp: 2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tpl.description}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"absolute bottom-4 left-0 top-4 w-1 rounded-r-full opacity-0 transition-opacity group-hover:opacity-100",
|
|
||||||
visual.accentClass,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex flex-col border-t border-slate-100 pt-5">
|
|
||||||
<div className="mb-3 flex items-center justify-between px-2">
|
<div className="mb-3 flex items-center justify-between px-2">
|
||||||
<h2 className="text-[15px] font-bold tracking-wide text-slate-400">
|
<h2 className="text-[15px] font-bold tracking-wide text-slate-400">
|
||||||
笔记本
|
笔记本
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BarChart3,
|
||||||
|
BookOpen,
|
||||||
|
Globe2,
|
||||||
|
Newspaper,
|
||||||
|
Search,
|
||||||
|
Settings2,
|
||||||
|
SlidersHorizontal,
|
||||||
|
Sparkles,
|
||||||
|
TrendingUp,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import type { ResearchTemplate } from "../../api/ai-writing-research-templates";
|
||||||
|
import { useAIWriting } from "../../contexts/AIWritingContext";
|
||||||
|
import { useWritingForm } from "../../contexts/WritingFormContext";
|
||||||
|
import { useResearchTemplates } from "../../hooks/useResearchTemplates";
|
||||||
|
|
||||||
|
type TemplateVisual = {
|
||||||
|
Icon: LucideIcon;
|
||||||
|
iconClass: string;
|
||||||
|
accentClass: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEMPLATE_VISUALS: TemplateVisual[] = [
|
||||||
|
{
|
||||||
|
Icon: Globe2,
|
||||||
|
iconClass: "bg-blue-50 text-blue-600",
|
||||||
|
accentClass: "bg-blue-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: Newspaper,
|
||||||
|
iconClass: "bg-amber-50 text-amber-600",
|
||||||
|
accentClass: "bg-amber-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: Sparkles,
|
||||||
|
iconClass: "bg-violet-50 text-violet-600",
|
||||||
|
accentClass: "bg-violet-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: TrendingUp,
|
||||||
|
iconClass: "bg-rose-50 text-rose-600",
|
||||||
|
accentClass: "bg-rose-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: BookOpen,
|
||||||
|
iconClass: "bg-emerald-50 text-emerald-600",
|
||||||
|
accentClass: "bg-emerald-500",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Icon: BarChart3,
|
||||||
|
iconClass: "bg-cyan-50 text-cyan-600",
|
||||||
|
accentClass: "bg-cyan-500",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function getTemplateVisual(
|
||||||
|
tpl: ResearchTemplate,
|
||||||
|
index: number,
|
||||||
|
): TemplateVisual {
|
||||||
|
const text = `${tpl.name} ${tpl.description ?? ""} ${tpl.article_type_key ?? ""}`;
|
||||||
|
if (/网摘|网络|热点|摘/.test(text)) return TEMPLATE_VISUALS[0];
|
||||||
|
if (/周报|周刊|日报|月报/.test(text)) return TEMPLATE_VISUALS[1];
|
||||||
|
if (/要讯|资讯|快讯|简报/.test(text)) return TEMPLATE_VISUALS[2];
|
||||||
|
if (/行业|分析|市场|竞争/.test(text)) return TEMPLATE_VISUALS[3];
|
||||||
|
if (/政策|解读|法规/.test(text)) return TEMPLATE_VISUALS[4];
|
||||||
|
return TEMPLATE_VISUALS[index % TEMPLATE_VISUALS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResearchTemplatesPanel({
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { templates, refresh } = useResearchTemplates();
|
||||||
|
const { startWriting } = useAIWriting();
|
||||||
|
const {
|
||||||
|
setIntent,
|
||||||
|
setArticleType,
|
||||||
|
setOutlineText,
|
||||||
|
setShowOutline,
|
||||||
|
setMaterialSource,
|
||||||
|
setWordCountInput,
|
||||||
|
setAudience,
|
||||||
|
setStrictMode,
|
||||||
|
setFormExpanded,
|
||||||
|
} = useWritingForm();
|
||||||
|
const [templateSearch, setTemplateSearch] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
|
const filteredTemplates = useMemo(() => {
|
||||||
|
const keyword = templateSearch.trim().toLowerCase();
|
||||||
|
if (!keyword) return templates;
|
||||||
|
return templates.filter((tpl) =>
|
||||||
|
[tpl.name, tpl.description, tpl.article_type_key]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some((value) => String(value).toLowerCase().includes(keyword)),
|
||||||
|
);
|
||||||
|
}, [templates, templateSearch]);
|
||||||
|
|
||||||
|
const handleTemplateClick = (tpl: ResearchTemplate) => {
|
||||||
|
const intent = tpl.topic?.trim() ?? "";
|
||||||
|
const articleType = tpl.article_type_key || "analysis";
|
||||||
|
const outline = tpl.outline?.trim() || "";
|
||||||
|
const audience = tpl.audience?.trim() || "普通读者";
|
||||||
|
const wordCount = tpl.word_count > 0 ? tpl.word_count : 800;
|
||||||
|
const writingMode = tpl.strict_mode ? "strict" : "loose";
|
||||||
|
|
||||||
|
if (intent) setIntent(intent);
|
||||||
|
if (tpl.article_type_key) setArticleType(tpl.article_type_key);
|
||||||
|
if (outline) {
|
||||||
|
setOutlineText(outline);
|
||||||
|
setShowOutline(true);
|
||||||
|
}
|
||||||
|
setMaterialSource(tpl.material_source);
|
||||||
|
setWordCountInput(String(wordCount));
|
||||||
|
if (tpl.audience?.trim()) setAudience(audience);
|
||||||
|
setStrictMode(tpl.strict_mode);
|
||||||
|
setFormExpanded(true);
|
||||||
|
|
||||||
|
if (!intent) return;
|
||||||
|
void startWriting({
|
||||||
|
userIntent: intent,
|
||||||
|
userOutline: outline || undefined,
|
||||||
|
articleType,
|
||||||
|
targetAudience: audience,
|
||||||
|
wordCountTarget: wordCount,
|
||||||
|
keywordCount: 1,
|
||||||
|
writingMode,
|
||||||
|
materialSource: tpl.material_source,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={cn(
|
||||||
|
"rounded-2xl border border-[#ece5e4] bg-card p-5 shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mb-4 flex items-center justify-between gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate("/page/canvas/research-templates")}
|
||||||
|
title="配置研究模板"
|
||||||
|
aria-label="配置研究模板"
|
||||||
|
className="group flex items-center gap-2 rounded-lg text-[16px] font-bold text-slate-600 transition-colors hover:text-slate-900 dark:text-white"
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="size-4 text-slate-400 dark:text-white/70" />
|
||||||
|
<span>研究模板</span>
|
||||||
|
<Settings2 className="size-3.5 text-slate-300 opacity-0 transition-opacity group-hover:opacity-100 dark:text-white/60" />
|
||||||
|
</button>
|
||||||
|
<div className="relative min-w-0 flex-1 sm:max-w-[180px]">
|
||||||
|
<Search className="pointer-events-none absolute left-3 top-1/2 size-3.5 -translate-y-1/2 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={templateSearch}
|
||||||
|
onChange={(event) => setTemplateSearch(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") event.preventDefault();
|
||||||
|
}}
|
||||||
|
placeholder="搜索写作模板..."
|
||||||
|
className="h-8 w-full rounded-lg border border-slate-200 bg-slate-50/80 pl-8 pr-3 text-[12px] text-slate-600 outline-none transition focus:border-blue-200 focus:bg-white focus:ring-2 focus:ring-blue-100 dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{templates.length === 0 ? (
|
||||||
|
<p className="rounded-xl border border-dashed border-slate-200 px-3 py-5 text-center text-xs text-slate-400">
|
||||||
|
暂无研究模板,点击标题配置
|
||||||
|
</p>
|
||||||
|
) : filteredTemplates.length === 0 ? (
|
||||||
|
<p className="rounded-xl border border-dashed border-slate-200 px-3 py-5 text-center text-xs text-slate-400">
|
||||||
|
暂无匹配模板
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{filteredTemplates.map((tpl, index) => {
|
||||||
|
const visual = getTemplateVisual(tpl, index);
|
||||||
|
const Icon = visual.Icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tpl.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleTemplateClick(tpl)}
|
||||||
|
title={tpl.description ?? tpl.name}
|
||||||
|
className="group relative flex w-full items-start gap-3 overflow-hidden rounded-[5px] bg-white px-4 py-4 text-left shadow-[0_8px_24px_rgba(15,23,42,0.04)] transition-all hover:-translate-y-0.5 hover:shadow-[0_14px_30px_rgba(30,64,175,0.08)] active:translate-y-0 dark:bg-[#032650]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex size-12 shrink-0 items-center justify-center rounded-xl transition-colors",
|
||||||
|
visual.iconClass,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="size-6 stroke-[2.2]" />
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-1 pt-0.5">
|
||||||
|
<span className="block truncate text-[16px] font-bold leading-5 text-slate-900 transition-colors group-hover:text-blue-700 dark:text-white">
|
||||||
|
{tpl.name}
|
||||||
|
</span>
|
||||||
|
{tpl.description && (
|
||||||
|
<span
|
||||||
|
className="mt-1.5 overflow-hidden text-[13px] leading-6 text-slate-500 dark:text-[#AACEFF]/75"
|
||||||
|
style={{
|
||||||
|
display: "-webkit-box",
|
||||||
|
WebkitBoxOrient: "vertical",
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tpl.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute bottom-4 left-0 top-4 w-1 rounded-r-full opacity-0 transition-opacity group-hover:opacity-100",
|
||||||
|
visual.accentClass,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
* -d '{"retention_days": 0}'
|
* -d '{"retention_days": 0}'
|
||||||
* ──────────────────────────────────────────────────────────────────────────
|
* ──────────────────────────────────────────────────────────────────────────
|
||||||
*/
|
*/
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
History,
|
History,
|
||||||
@@ -113,6 +113,10 @@ import StudioNotebookDetailPage from '@/pages/StudioNotebookDetailPage';
|
|||||||
import StudioNotebooksPage from '@/pages/StudioNotebooksPage';
|
import StudioNotebooksPage from '@/pages/StudioNotebooksPage';
|
||||||
import AgentChatPage from '@/pages/AgentChatPage';
|
import AgentChatPage from '@/pages/AgentChatPage';
|
||||||
import { ChatRuntime } from '@/pages/WorkspaceRoutes';
|
import { ChatRuntime } from '@/pages/WorkspaceRoutes';
|
||||||
|
import {
|
||||||
|
LayoutModeSwitcher,
|
||||||
|
ThemeSwitcher,
|
||||||
|
} from '@/components/workspace/appearance-controls';
|
||||||
import {
|
import {
|
||||||
AgentModuleSidebar,
|
AgentModuleSidebar,
|
||||||
useAgentModuleSidebarData,
|
useAgentModuleSidebarData,
|
||||||
@@ -137,10 +141,20 @@ const WRITING_ID = 'aw-writing';
|
|||||||
const DRAFT_ID = 'aw-draft';
|
const DRAFT_ID = 'aw-draft';
|
||||||
const AI_WRITING_AGENT_MODULE_KEY = 'ai-writing';
|
const AI_WRITING_AGENT_MODULE_KEY = 'ai-writing';
|
||||||
const AI_WRITING_AGENT_MODULE_TITLE = 'AI 写作智能体';
|
const AI_WRITING_AGENT_MODULE_TITLE = 'AI 写作智能体';
|
||||||
|
const AI_WRITING_LAST_AGENT_KEY = `agent-module:${AI_WRITING_AGENT_MODULE_KEY}:last-agent`;
|
||||||
|
|
||||||
// Match the original canvas page proportions (chat=35, artifact=65)
|
// Match the original canvas page proportions (chat=35, artifact=65)
|
||||||
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 65 };
|
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 65 };
|
||||||
|
|
||||||
|
function AIWritingCompactAppearanceActions() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ThemeSwitcher tone="content" />
|
||||||
|
<LayoutModeSwitcher tone="content" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function useAIWritingRouteBase() {
|
function useAIWritingRouteBase() {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
@@ -170,7 +184,15 @@ function useAIWritingAgentRouteState(base: string) {
|
|||||||
|
|
||||||
function AIWritingAgentDefaultRedirect() {
|
function AIWritingAgentDefaultRedirect() {
|
||||||
const { groups, isLoading } = useAgentModuleSidebarData(AI_WRITING_AGENT_MODULE_KEY);
|
const { groups, isLoading } = useAgentModuleSidebarData(AI_WRITING_AGENT_MODULE_KEY);
|
||||||
const firstAgent = groups.flatMap((group) => group.agents)[0];
|
const agents = groups.flatMap((group) => group.agents);
|
||||||
|
const lastAgentId = (() => {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem(AI_WRITING_LAST_AGENT_KEY);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const targetAgent = agents.find((agent) => agent.id === lastAgentId) ?? agents[0];
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -180,7 +202,7 @@ function AIWritingAgentDefaultRedirect() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!firstAgent) {
|
if (!targetAgent) {
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full items-center justify-center px-6 text-center text-sm text-muted-foreground">
|
<div className="flex size-full items-center justify-center px-6 text-center text-sm text-muted-foreground">
|
||||||
暂无可用智能体,请先在模块设置中配置或创建智能体。
|
暂无可用智能体,请先在模块设置中配置或创建智能体。
|
||||||
@@ -188,24 +210,44 @@ function AIWritingAgentDefaultRedirect() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Navigate to={`agents/${firstAgent.id}/chats/new`} replace />;
|
return <Navigate to={`agents/${targetAgent.id}/chats/new`} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AIWritingAgentChatRoute() {
|
function AIWritingAgentChatRoute({
|
||||||
|
compactAppearanceActions,
|
||||||
|
}: {
|
||||||
|
compactAppearanceActions?: ReactNode;
|
||||||
|
}) {
|
||||||
const { agent_id } = useParams<{ agent_id: string }>();
|
const { agent_id } = useParams<{ agent_id: string }>();
|
||||||
return (
|
return (
|
||||||
<ChatRuntime>
|
<ChatRuntime>
|
||||||
<AgentChatPage key={agent_id} />
|
<AgentChatPage
|
||||||
|
key={agent_id}
|
||||||
|
compactAppearanceActions={compactAppearanceActions}
|
||||||
|
/>
|
||||||
</ChatRuntime>
|
</ChatRuntime>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AIWritingAgentRoutes({ base }: { base: string }) {
|
function AIWritingAgentRoutes({
|
||||||
|
base,
|
||||||
|
compactAppearanceActions,
|
||||||
|
}: {
|
||||||
|
base: string;
|
||||||
|
compactAppearanceActions?: ReactNode;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<RouteBaseProvider value={base}>
|
<RouteBaseProvider value={base}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<AIWritingAgentDefaultRedirect />} />
|
<Route index element={<AIWritingAgentDefaultRedirect />} />
|
||||||
<Route path="agents/:agent_id/chats/:thread_id" element={<AIWritingAgentChatRoute />} />
|
<Route
|
||||||
|
path="agents/:agent_id/chats/:thread_id"
|
||||||
|
element={
|
||||||
|
<AIWritingAgentChatRoute
|
||||||
|
compactAppearanceActions={compactAppearanceActions}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</RouteBaseProvider>
|
</RouteBaseProvider>
|
||||||
);
|
);
|
||||||
@@ -220,11 +262,26 @@ interface NotebookProps {
|
|||||||
studioToken?: string;
|
studioToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WritingFormPanel({ hideAdvancedFields, initialNotebookId, initialDocumentIds, studioToken }: { hideAdvancedFields: boolean } & NotebookProps) {
|
function WritingFormPanel({
|
||||||
|
hideAdvancedFields,
|
||||||
|
initialNotebookId,
|
||||||
|
initialDocumentIds,
|
||||||
|
studioToken,
|
||||||
|
headerActionsBeforeCollapse,
|
||||||
|
}: {
|
||||||
|
hideAdvancedFields: boolean;
|
||||||
|
headerActionsBeforeCollapse?: ReactNode;
|
||||||
|
} & NotebookProps) {
|
||||||
return (
|
return (
|
||||||
// 与原布局保持相同的 flex-1 min-h-0 包裹,确保内部 h-full 正确解析
|
// 与原布局保持相同的 flex-1 min-h-0 包裹,确保内部 h-full 正确解析
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<AIWritingPanel hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
<AIWritingPanel
|
||||||
|
hideAdvancedFields={hideAdvancedFields}
|
||||||
|
initialNotebookId={initialNotebookId}
|
||||||
|
initialDocumentIds={initialDocumentIds}
|
||||||
|
studioToken={studioToken}
|
||||||
|
headerActionsBeforeCollapse={headerActionsBeforeCollapse}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -234,7 +291,16 @@ function WritingFormPanel({ hideAdvancedFields, initialNotebookId, initialDocume
|
|||||||
* - idle(初始问答阶段):左=「写作助手 Q&A」,右=「写作配置选项」表单;
|
* - idle(初始问答阶段):左=「写作助手 Q&A」,右=「写作配置选项」表单;
|
||||||
* - 开始写作后:回到原布局——左=写作配置/时间线面板,右=草稿预览。
|
* - 开始写作后:回到原布局——左=写作配置/时间线面板,右=草稿预览。
|
||||||
*/
|
*/
|
||||||
function AIWritingLayout({ hideAdvancedFields, initialNotebookId, initialDocumentIds, studioToken }: { hideAdvancedFields: boolean } & NotebookProps) {
|
function AIWritingLayout({
|
||||||
|
hideAdvancedFields,
|
||||||
|
initialNotebookId,
|
||||||
|
initialDocumentIds,
|
||||||
|
studioToken,
|
||||||
|
compactAppearanceActions,
|
||||||
|
}: {
|
||||||
|
hideAdvancedFields: boolean;
|
||||||
|
compactAppearanceActions?: ReactNode;
|
||||||
|
} & NotebookProps) {
|
||||||
const layoutRef = useRef<GroupImperativeHandle>(null);
|
const layoutRef = useRef<GroupImperativeHandle>(null);
|
||||||
const { state } = useAIWriting();
|
const { state } = useAIWriting();
|
||||||
const { isHistoryMode } = useAIWritingHistory();
|
const { isHistoryMode } = useAIWritingHistory();
|
||||||
@@ -262,19 +328,28 @@ function AIWritingLayout({ hideAdvancedFields, initialNotebookId, initialDocumen
|
|||||||
formExpanded ? 'w-[36%] min-w-[340px] max-w-[600px] border-l' : 'w-0 min-w-0',
|
formExpanded ? 'w-[36%] min-w-[340px] max-w-[600px] border-l' : 'w-0 min-w-0',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<WritingFormPanel hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
<WritingFormPanel
|
||||||
|
hideAdvancedFields={hideAdvancedFields}
|
||||||
|
initialNotebookId={initialNotebookId}
|
||||||
|
initialDocumentIds={initialDocumentIds}
|
||||||
|
studioToken={studioToken}
|
||||||
|
headerActionsBeforeCollapse={compactAppearanceActions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 收起态:右上角浮动展开图标按钮(与展开态头部的收起按钮位置呼应) */}
|
{/* 收起态:右上角浮动展开图标按钮(与展开态头部的收起按钮位置呼应) */}
|
||||||
{!formExpanded && (
|
{!formExpanded && (
|
||||||
<button
|
<div className="absolute right-3 top-2 z-10 flex items-center gap-1">
|
||||||
type="button"
|
{compactAppearanceActions}
|
||||||
onClick={() => setFormExpanded(true)}
|
<button
|
||||||
title="展开写作配置"
|
type="button"
|
||||||
className="absolute right-3 top-2 z-10 flex h-7 w-7 items-center justify-center rounded-md border bg-background text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground"
|
onClick={() => setFormExpanded(true)}
|
||||||
>
|
title="展开写作配置"
|
||||||
<PanelRightOpen className="size-4" />
|
className="flex h-7 w-7 items-center justify-center rounded-md border bg-background text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground"
|
||||||
</button>
|
>
|
||||||
|
<PanelRightOpen className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -322,7 +397,16 @@ function AIWritingLayout({ hideAdvancedFields, initialNotebookId, initialDocumen
|
|||||||
* 写作页主体(侧栏 + 内容区)。放在 Provider 内部,以便读 useAIWriting 状态:
|
* 写作页主体(侧栏 + 内容区)。放在 Provider 内部,以便读 useAIWriting 状态:
|
||||||
* 进入写作(status !== idle 或查看历史)后自动收起左侧研究模板栏,让写作区更宽。
|
* 进入写作(status !== idle 或查看历史)后自动收起左侧研究模板栏,让写作区更宽。
|
||||||
*/
|
*/
|
||||||
function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentIds, studioToken }: { hideAdvancedFields: boolean } & NotebookProps) {
|
function AIWritingBody({
|
||||||
|
hideAdvancedFields,
|
||||||
|
initialNotebookId,
|
||||||
|
initialDocumentIds,
|
||||||
|
studioToken,
|
||||||
|
compactChrome = false,
|
||||||
|
}: {
|
||||||
|
hideAdvancedFields: boolean;
|
||||||
|
compactChrome?: boolean;
|
||||||
|
} & NotebookProps) {
|
||||||
const { state } = useAIWriting();
|
const { state } = useAIWriting();
|
||||||
const { isHistoryMode } = useAIWritingHistory();
|
const { isHistoryMode } = useAIWritingHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -354,6 +438,9 @@ function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentI
|
|||||||
// 历史内容显示到右侧区域);写作进行中收起;嵌入态(studioToken)始终不展示。
|
// 历史内容显示到右侧区域);写作进行中收起;嵌入态(studioToken)始终不展示。
|
||||||
const showSidebar =
|
const showSidebar =
|
||||||
!studioToken && (state.status === 'idle' || isHistoryMode || agentsActive);
|
!studioToken && (state.status === 'idle' || isHistoryMode || agentsActive);
|
||||||
|
const compactAppearanceActions = compactChrome ? (
|
||||||
|
<AIWritingCompactAppearanceActions />
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 min-h-0 overflow-hidden flex">
|
<div className="flex-1 min-h-0 overflow-hidden flex">
|
||||||
@@ -459,7 +546,13 @@ function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentI
|
|||||||
查看笔记本 / 所有空间时在其上叠加内嵌页。 */}
|
查看笔记本 / 所有空间时在其上叠加内嵌页。 */}
|
||||||
<div className="relative flex-1 min-h-0 overflow-hidden">
|
<div className="relative flex-1 min-h-0 overflow-hidden">
|
||||||
<div className={cn('absolute inset-0', (overlayActive || agentsActive) && 'hidden')}>
|
<div className={cn('absolute inset-0', (overlayActive || agentsActive) && 'hidden')}>
|
||||||
<AIWritingLayout hideAdvancedFields={hideAdvancedFields} initialNotebookId={initialNotebookId} initialDocumentIds={initialDocumentIds} studioToken={studioToken} />
|
<AIWritingLayout
|
||||||
|
hideAdvancedFields={hideAdvancedFields}
|
||||||
|
initialNotebookId={initialNotebookId}
|
||||||
|
initialDocumentIds={initialDocumentIds}
|
||||||
|
studioToken={studioToken}
|
||||||
|
compactAppearanceActions={compactAppearanceActions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{overlayActive && !agentsActive && (
|
{overlayActive && !agentsActive && (
|
||||||
<div className="absolute inset-0 flex flex-col bg-background">
|
<div className="absolute inset-0 flex flex-col bg-background">
|
||||||
@@ -500,7 +593,10 @@ function AIWritingBody({ hideAdvancedFields, initialNotebookId, initialDocumentI
|
|||||||
)}
|
)}
|
||||||
{agentsActive && (
|
{agentsActive && (
|
||||||
<div className="absolute inset-0 bg-background">
|
<div className="absolute inset-0 bg-background">
|
||||||
<AIWritingAgentRoutes base={routeBase} />
|
<AIWritingAgentRoutes
|
||||||
|
base={routeBase}
|
||||||
|
compactAppearanceActions={compactAppearanceActions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -521,11 +617,13 @@ export default function AIWritingPage({
|
|||||||
initialNotebookId,
|
initialNotebookId,
|
||||||
initialDocumentIds,
|
initialDocumentIds,
|
||||||
studioToken,
|
studioToken,
|
||||||
|
compactChrome = false,
|
||||||
}: {
|
}: {
|
||||||
hideAdvancedFields?: boolean;
|
hideAdvancedFields?: boolean;
|
||||||
initialNotebookId?: number;
|
initialNotebookId?: number;
|
||||||
initialDocumentIds?: number[];
|
initialDocumentIds?: number[];
|
||||||
studioToken?: string;
|
studioToken?: string;
|
||||||
|
compactChrome?: boolean;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
return (
|
return (
|
||||||
<CanvasThreadProvider>
|
<CanvasThreadProvider>
|
||||||
@@ -545,6 +643,7 @@ export default function AIWritingPage({
|
|||||||
initialNotebookId={initialNotebookId}
|
initialNotebookId={initialNotebookId}
|
||||||
initialDocumentIds={initialDocumentIds}
|
initialDocumentIds={initialDocumentIds}
|
||||||
studioToken={studioToken}
|
studioToken={studioToken}
|
||||||
|
compactChrome={compactChrome}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { BotIcon, PlusSquare } from "lucide-react";
|
|||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useRouteBase } from "@/core/route-base";
|
import { useRouteBase } from "@/core/route-base";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||||
|
|
||||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -65,6 +65,7 @@ export default function AgentChatPage({
|
|||||||
taskBind,
|
taskBind,
|
||||||
autoStartWhenPurposeEmpty,
|
autoStartWhenPurposeEmpty,
|
||||||
showAgentList,
|
showAgentList,
|
||||||
|
compactAppearanceActions,
|
||||||
}: {
|
}: {
|
||||||
/** 当作为「通用问答右侧嵌入」渲染时由父组件传入的 agent id(替代路由参数)。
|
/** 当作为「通用问答右侧嵌入」渲染时由父组件传入的 agent id(替代路由参数)。
|
||||||
* 传入即视为嵌入态:路由仍停留在 /chats/new,仅右侧问答区换成该智能体的对话。 */
|
* 传入即视为嵌入态:路由仍停留在 /chats/new,仅右侧问答区换成该智能体的对话。 */
|
||||||
@@ -85,6 +86,8 @@ export default function AgentChatPage({
|
|||||||
/** 是否在最左侧展示「智能体列表」栏(顶部下拉切换 我的/广场,点击进入该智能体对话)。
|
/** 是否在最左侧展示「智能体列表」栏(顶部下拉切换 我的/广场,点击进入该智能体对话)。
|
||||||
* 仅独立的智能体问答整页开启;任务工作区 / 模块页 / 嵌入态不传(各有自己的左栏)。 */
|
* 仅独立的智能体问答整页开启;任务工作区 / 模块页 / 嵌入态不传(各有自己的左栏)。 */
|
||||||
showAgentList?: boolean;
|
showAgentList?: boolean;
|
||||||
|
/** 简洁模式下,挂到右侧智能体信息面板收起按钮左侧的外观控件。 */
|
||||||
|
compactAppearanceActions?: ReactNode;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const routeBase = useRouteBase();
|
const routeBase = useRouteBase();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -589,6 +592,7 @@ export default function AgentChatPage({
|
|||||||
onAskQuestion={(text) => handleSubmit({ text, files: [] })}
|
onAskQuestion={(text) => handleSubmit({ text, files: [] })}
|
||||||
defaultCollapsed={taskCtxOn}
|
defaultCollapsed={taskCtxOn}
|
||||||
showRecentChats={!taskCtxOn}
|
showRecentChats={!taskCtxOn}
|
||||||
|
compactAppearanceActions={compactAppearanceActions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { ImportConversationsDialog } from "@/components/workspace/thread-share-dialogs";
|
import { ImportConversationsDialog } from "@/components/workspace/thread-share-dialogs";
|
||||||
|
import {
|
||||||
|
LayoutModeSwitcher,
|
||||||
|
ThemeSwitcher,
|
||||||
|
} from "@/components/workspace/appearance-controls";
|
||||||
import {
|
import {
|
||||||
WorkspaceBody,
|
WorkspaceBody,
|
||||||
WorkspaceContainer,
|
WorkspaceContainer,
|
||||||
@@ -40,7 +44,11 @@ import { cn } from "@/lib/utils";
|
|||||||
|
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
export default function ChatsPage() {
|
export default function ChatsPage({
|
||||||
|
compactChrome = false,
|
||||||
|
}: {
|
||||||
|
compactChrome?: boolean;
|
||||||
|
}) {
|
||||||
const pathOfThread = usePathOfThread();
|
const pathOfThread = usePathOfThread();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { toggle: togglePin } = useToggleThreadPin();
|
const { toggle: togglePin } = useToggleThreadPin();
|
||||||
@@ -171,7 +179,16 @@ export default function ChatsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceContainer>
|
<WorkspaceContainer>
|
||||||
<WorkspaceHeader></WorkspaceHeader>
|
<WorkspaceHeader
|
||||||
|
rightActions={
|
||||||
|
compactChrome ? (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ThemeSwitcher tone="content" />
|
||||||
|
<LayoutModeSwitcher tone="content" />
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
<WorkspaceBody>
|
<WorkspaceBody>
|
||||||
<div className="flex size-full flex-col">
|
<div className="flex size-full flex-col">
|
||||||
<header className="flex shrink-0 flex-col items-center gap-3 pt-8">
|
<header className="flex shrink-0 flex-col items-center gap-3 pt-8">
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ import {
|
|||||||
WorkspaceContainer,
|
WorkspaceContainer,
|
||||||
WorkspaceHeader,
|
WorkspaceHeader,
|
||||||
} from "@/components/workspace/workspace-container";
|
} from "@/components/workspace/workspace-container";
|
||||||
|
import {
|
||||||
|
LayoutModeSwitcher,
|
||||||
|
ThemeSwitcher,
|
||||||
|
} from "@/components/workspace/appearance-controls";
|
||||||
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
||||||
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
||||||
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||||
@@ -144,7 +148,11 @@ const SCHEDULED_TASK_TEXTAREA_CLASS =
|
|||||||
const DESCRIPTION_TEXTAREA_ROWS = 4;
|
const DESCRIPTION_TEXTAREA_ROWS = 4;
|
||||||
const PROMPT_TEXTAREA_ROWS = 10;
|
const PROMPT_TEXTAREA_ROWS = 10;
|
||||||
|
|
||||||
export default function ScheduledTasksPage() {
|
export default function ScheduledTasksPage({
|
||||||
|
compactChrome = false,
|
||||||
|
}: {
|
||||||
|
compactChrome?: boolean;
|
||||||
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const darkToastOpts =
|
const darkToastOpts =
|
||||||
@@ -490,7 +498,16 @@ export default function ScheduledTasksPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceContainer>
|
<WorkspaceContainer>
|
||||||
<WorkspaceHeader />
|
<WorkspaceHeader
|
||||||
|
rightActions={
|
||||||
|
compactChrome ? (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ThemeSwitcher tone="content" />
|
||||||
|
<LayoutModeSwitcher tone="content" />
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
<WorkspaceBody>
|
<WorkspaceBody>
|
||||||
<div className="grid size-full min-h-0 grid-cols-1 grid-rows-[minmax(0,1fr)_minmax(0,1fr)] gap-4 p-4 xl:grid-cols-[420px_minmax(0,1fr)] xl:grid-rows-1">
|
<div className="grid size-full min-h-0 grid-cols-1 grid-rows-[minmax(0,1fr)_minmax(0,1fr)] gap-4 p-4 xl:grid-cols-[420px_minmax(0,1fr)] xl:grid-rows-1">
|
||||||
<Card className="flex min-h-0 min-w-0 flex-col">
|
<Card className="flex min-h-0 min-w-0 flex-col">
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import { getStoredAuth } from '@/core/auth';
|
|||||||
|
|
||||||
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
|
||||||
import { PromptInputProvider } from '@/components/ai-elements/prompt-input';
|
import { PromptInputProvider } from '@/components/ai-elements/prompt-input';
|
||||||
|
import {
|
||||||
|
LayoutModeSwitcher,
|
||||||
|
ThemeSwitcher,
|
||||||
|
} from '@/components/workspace/appearance-controls';
|
||||||
import { WorkspaceSidebar } from '@/components/workspace/workspace-sidebar';
|
import { WorkspaceSidebar } from '@/components/workspace/workspace-sidebar';
|
||||||
|
import { UserIdentityBadge } from '@/components/workspace/user-identity-badge';
|
||||||
import { CommandPalette } from '@/components/workspace/command-palette';
|
import { CommandPalette } from '@/components/workspace/command-palette';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import { SubtasksProvider } from '@/core/tasks/context';
|
import { SubtasksProvider } from '@/core/tasks/context';
|
||||||
@@ -72,6 +77,27 @@ function isInIframe(): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldShowCompactTopActions(pathname: string): boolean {
|
||||||
|
return (
|
||||||
|
pathname === '/page/workspace/agents' ||
|
||||||
|
pathname === '/page/workspace/skills' ||
|
||||||
|
pathname === '/page/workspace/memory' ||
|
||||||
|
pathname === '/page/workspace/knowledge' ||
|
||||||
|
pathname.startsWith('/page/workspace/knowledge/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompactWorkspaceTopActions() {
|
||||||
|
return (
|
||||||
|
<div className="z-[60] flex h-12 shrink-0 items-center justify-end gap-2 border-b border-border/70 bg-card/90 px-4 shadow-sm backdrop-blur-md">
|
||||||
|
<ThemeSwitcher tone="content" />
|
||||||
|
<LayoutModeSwitcher tone="content" />
|
||||||
|
<div className="bg-border/70 mx-1 h-6 w-px shrink-0" aria-hidden />
|
||||||
|
<UserIdentityBadge />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function WorkspaceLayout({
|
export function WorkspaceLayout({
|
||||||
children,
|
children,
|
||||||
pageSidebarCollapsed,
|
pageSidebarCollapsed,
|
||||||
@@ -98,7 +124,10 @@ export function WorkspaceLayout({
|
|||||||
}) {
|
}) {
|
||||||
// 全量模式下通用问答(/page/workspace/chats*)的历史侧栏默认展开;其余紧凑页
|
// 全量模式下通用问答(/page/workspace/chats*)的历史侧栏默认展开;其余紧凑页
|
||||||
// (如智能体对话,为聊天区更宽而默认收起)维持原默认。
|
// (如智能体对话,为聊天区更宽而默认收起)维持原默认。
|
||||||
const isGeneralQa = useLocation().pathname.startsWith("/page/workspace/chats");
|
const pathname = useLocation().pathname;
|
||||||
|
const isGeneralQa = pathname.startsWith('/page/workspace/chats');
|
||||||
|
const showCompactTopActions =
|
||||||
|
headerHidden && shouldShowCompactTopActions(pathname);
|
||||||
|
|
||||||
const pageSidebarWidth = pageSidebarHidden
|
const pageSidebarWidth = pageSidebarHidden
|
||||||
? '0rem'
|
? '0rem'
|
||||||
@@ -136,7 +165,14 @@ export function WorkspaceLayout({
|
|||||||
<SidebarInset
|
<SidebarInset
|
||||||
className={cn('min-w-0', insetOverflowHidden && 'overflow-hidden')}
|
className={cn('min-w-0', insetOverflowHidden && 'overflow-hidden')}
|
||||||
>
|
>
|
||||||
{children}
|
{showCompactTopActions ? (
|
||||||
|
<>
|
||||||
|
<CompactWorkspaceTopActions />
|
||||||
|
<div className="min-h-0 flex-1">{children}</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<Toaster position="top-center" />
|
<Toaster position="top-center" />
|
||||||
@@ -222,7 +258,7 @@ export default function WorkspaceRoutes({
|
|||||||
path="chats"
|
path="chats"
|
||||||
element={
|
element={
|
||||||
<WorkspaceLayout {...layoutProps}>
|
<WorkspaceLayout {...layoutProps}>
|
||||||
<ChatsPage />
|
<ChatsPage compactChrome={headerHidden} />
|
||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -338,7 +374,10 @@ export default function WorkspaceRoutes({
|
|||||||
path="ai-writing/*"
|
path="ai-writing/*"
|
||||||
element={
|
element={
|
||||||
<WorkspaceLayout {...layoutProps}>
|
<WorkspaceLayout {...layoutProps}>
|
||||||
<AIWritingPage hideAdvancedFields />
|
<AIWritingPage
|
||||||
|
hideAdvancedFields
|
||||||
|
compactChrome={headerHidden}
|
||||||
|
/>
|
||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -371,7 +410,7 @@ export default function WorkspaceRoutes({
|
|||||||
element={
|
element={
|
||||||
<WorkspaceLayout {...layoutProps}>
|
<WorkspaceLayout {...layoutProps}>
|
||||||
<ChatRuntime>
|
<ChatRuntime>
|
||||||
<RoundtablePlanningPage />
|
<RoundtablePlanningPage compactChrome={headerHidden} />
|
||||||
</ChatRuntime>
|
</ChatRuntime>
|
||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
}
|
}
|
||||||
@@ -433,7 +472,7 @@ export default function WorkspaceRoutes({
|
|||||||
path="scheduled/tasks"
|
path="scheduled/tasks"
|
||||||
element={
|
element={
|
||||||
<WorkspaceLayout {...layoutProps}>
|
<WorkspaceLayout {...layoutProps}>
|
||||||
<ScheduledTasksPage />
|
<ScheduledTasksPage compactChrome={headerHidden} />
|
||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ import {
|
|||||||
|
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { useArtifacts } from "@/components/workspace/artifacts";
|
import { useArtifacts } from "@/components/workspace/artifacts";
|
||||||
|
import {
|
||||||
|
LayoutModeSwitcher,
|
||||||
|
ThemeSwitcher,
|
||||||
|
} from "@/components/workspace/appearance-controls";
|
||||||
import { useSidebarSafe } from "@/components/ui/sidebar";
|
import { useSidebarSafe } from "@/components/ui/sidebar";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -282,7 +286,11 @@ function readEmbedHashQueryPrefill(): RoundtablePrefill | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function RoundtablePlanningPage() {
|
export default function RoundtablePlanningPage({
|
||||||
|
compactChrome = false,
|
||||||
|
}: {
|
||||||
|
compactChrome?: boolean;
|
||||||
|
}) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
// 全工作区跨页的后台作业完成通知器(Provider 挂在 PageLayout 常驻层)。
|
// 全工作区跨页的后台作业完成通知器(Provider 挂在 PageLayout 常驻层)。
|
||||||
const { watchJob } = useRoundtableJobNotifier();
|
const { watchJob } = useRoundtableJobNotifier();
|
||||||
@@ -2075,6 +2083,12 @@ export default function RoundtablePlanningPage() {
|
|||||||
>
|
>
|
||||||
<Settings className="w-4 h-4" />
|
<Settings className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
{compactChrome && (
|
||||||
|
<>
|
||||||
|
<ThemeSwitcher tone="content" />
|
||||||
|
<LayoutModeSwitcher tone="content" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user