diff --git a/frontend-web/src/core/page-layout/sidebar-menu.ts b/frontend-web/src/core/page-layout/sidebar-menu.ts
index c343a91b..ca1c76ff 100644
--- a/frontend-web/src/core/page-layout/sidebar-menu.ts
+++ b/frontend-web/src/core/page-layout/sidebar-menu.ts
@@ -249,13 +249,6 @@ export const sidebarMenuItems: MenuItem[] = [
path: "/page/strategy/admin/positions",
adminOnly: true,
},
- {
- id: "mgmt-user-leaderboard",
- label: "用户排行榜",
- icon: Trophy,
- path: "/page/strategy/admin/user-leaderboard",
- adminOnly: true,
- },
{
id: "mgmt-admin-scheduled-tasks",
label: "定时任务管理",
diff --git a/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx b/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx
index b1207024..e620b0e2 100644
--- a/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx
+++ b/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx
@@ -28,7 +28,7 @@ import { uuid } from '@/core/utils/uuid';
import { cn } from '@/lib/utils';
import { useArticleTypes } from '../../hooks/useArticleTypes';
-import { useWritingForm } from '../../contexts/WritingFormContext';
+import { useWritingFormOptional } from '../../contexts/WritingFormContext';
import { WritingWelcome } from './WritingWelcome';
/**
@@ -42,13 +42,25 @@ import { WritingWelcome } from './WritingWelcome';
*
* 这条线程只负责「帮用户把表单聊清」,与真正写作的 ai_writing graph 完全解耦。
*/
-export function WritingSetupChat() {
+export function WritingSetupChat({
+ onApprove: onApproveProp,
+ title = '写作助手',
+ hint = '描述你想写什么,我来帮你填好右侧配置',
+}: {
+ /** 审批卡「应用」回调;缺省回填写作表单(需 WritingFormProvider)。传入则脱离表单上下文,
+ * 用于复用到「新建研究模板」等场景,把建议字段写进调用方的表单。 */
+ onApprove?: (payload: SetupWritingApprovalPayload) => void;
+ title?: string;
+ hint?: string;
+} = {}) {
const threadId = useMemo(() => uuid(), []);
const [threadPrepared, setThreadPrepared] = useState(false);
const [chatModel, setChatModel] = useState('');
const { models, isLoading: modelsLoading } = useModels();
const { articleTypes } = useArticleTypes();
- const { applyProposal } = useWritingForm();
+ // 缺省回填写作表单;onApprove 传入时脱离 WritingFormProvider(如模板创建页)。
+ const formCtx = useWritingFormOptional();
+ const applyProposal = onApproveProp ?? formCtx?.applyProposal;
// 首个模型可用时默认选中
useEffect(() => {
@@ -98,8 +110,8 @@ export function WritingSetupChat() {
const handleApprove = useCallback(
async (payload: SetupWritingApprovalPayload) => {
- applyProposal(payload);
- toast.success('已应用到左侧表单,可继续编辑或开始写作');
+ applyProposal?.(payload);
+ toast.success('已应用到表单,可继续编辑');
},
[applyProposal],
);
@@ -119,8 +131,8 @@ export function WritingSetupChat() {
{!showWelcome && (
写作助手
- 描述你想写什么,我来帮你填好右侧配置
+ {title}
+ {hint}
这是一个专业的自动化写作系统。你只需在下方对话框中
diff --git a/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx b/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx
new file mode 100644
index 00000000..adcdf976
--- /dev/null
+++ b/frontend-web/src/open-canvas/pages/ResearchTemplateEditorPage.tsx
@@ -0,0 +1,300 @@
+import { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { ArrowLeft, Loader2 } from 'lucide-react';
+
+import { Button } from '@/components/ui/button';
+import type { SetupWritingApprovalPayload } from '@/components/workspace/messages';
+
+import { useResearchTemplates } from '../hooks/useResearchTemplates';
+import { useArticleTypes } from '../hooks/useArticleTypes';
+import { WritingSetupChat } from '../components/ai-writing/WritingSetupChat';
+import type {
+ CreateResearchTemplateRequest,
+ ResearchTemplate,
+} from '../api/ai-writing-research-templates';
+
+const LIST_PATH = '/page/canvas/research-templates';
+
+const INPUT_CLASS =
+ 'flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ' +
+ 'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50';
+const TEXTAREA_CLASS =
+ 'flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ' +
+ 'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring resize-none';
+const LABEL_CLASS = 'mb-1 block text-xs font-medium text-muted-foreground';
+
+interface FormState {
+ name: string;
+ description: string;
+ article_type_key: string;
+ outline: string;
+ material_source: 'general' | 'notebook';
+ word_count: string;
+ audience: string;
+ strict_mode: boolean;
+ topic: string;
+ sort_order: string;
+}
+
+const EMPTY_FORM: FormState = {
+ name: '',
+ description: '',
+ article_type_key: '',
+ outline: '',
+ material_source: 'general',
+ word_count: '800',
+ audience: '',
+ strict_mode: false,
+ topic: '',
+ sort_order: '0',
+};
+
+function toForm(t: ResearchTemplate): FormState {
+ return {
+ name: t.name,
+ description: t.description ?? '',
+ article_type_key: t.article_type_key ?? '',
+ outline: t.outline ?? '',
+ material_source: t.material_source,
+ word_count: String(t.word_count),
+ audience: t.audience ?? '',
+ strict_mode: t.strict_mode,
+ topic: t.topic ?? '',
+ sort_order: String(t.sort_order),
+ };
+}
+
+function toPayload(form: FormState): CreateResearchTemplateRequest {
+ return {
+ name: form.name.trim(),
+ description: form.description.trim() || null,
+ article_type_key: form.article_type_key || null,
+ outline: form.outline.trim() || null,
+ material_source: form.material_source,
+ word_count: Math.max(100, parseInt(form.word_count, 10) || 800),
+ audience: form.audience.trim() || null,
+ strict_mode: form.strict_mode,
+ topic: form.topic.trim() || null,
+ sort_order: parseInt(form.sort_order, 10) || 0,
+ };
+}
+
+export default function ResearchTemplateEditorPage() {
+ const navigate = useNavigate();
+ const { id } = useParams<{ id: string }>();
+ const editing = !!id;
+
+ const { templates, refresh, create, update } = useResearchTemplates();
+ const { articleTypes, refresh: refreshTypes } = useArticleTypes();
+
+ const [form, setForm] = useState
+ 左侧用问答描述需求,自动填好右侧字段;也可手动编辑。
+
+ {editing ? '编辑研究模板' : '新建研究模板'}
+
+