diff --git a/frontend-web/package.json b/frontend-web/package.json index 1613e744..3b00ed53 100644 --- a/frontend-web/package.json +++ b/frontend-web/package.json @@ -57,6 +57,7 @@ "cmdk": "^1.1.1", "codemirror": "^6.0.2", "date-fns": "^4.1.0", + "dayjs": "^1.11.21", "docx": "^9.5.1", "dotenv": "^17.2.3", "embla-carousel-react": "^8.6.0", diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx index 7f9db889..197b2e94 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { PenLine } from 'lucide-react'; +import { BookOpen, PenLine, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; @@ -11,6 +11,7 @@ import { SelectValue, } from '@/components/ui/select'; import { useModels } from '@/core/models/hooks'; +import { useStudioNotebooks, useNotebookSources } from '@/core/studio/hooks'; import type { AIWritingRequest } from '../../types/ai-writing'; import { useArticleTypes } from '../../hooks/useArticleTypes'; @@ -46,7 +47,13 @@ export function AIWritingForm({ const [wordCountInput, setWordCountInput] = useState('800'); const [strictMode, setStrictMode] = useState(false); const [modelName, setModelName] = useState(''); + // 笔记来源:选中的 search_space_id 和多个 document_id + const [selectedNotebookId, setSelectedNotebookId] = useState(null); + const [selectedDocumentIds, setSelectedDocumentIds] = useState([]); + const { models, isLoading: modelsLoading } = useModels(); + const { notebooks, isLoading: notebooksLoading } = useStudioNotebooks(); + const { sources, isLoading: sourcesLoading } = useNotebookSources(selectedNotebookId); const { articleTypes, loading: typesLoading, refresh: refreshTypes } = useArticleTypes(); @@ -98,6 +105,22 @@ export function AIWritingForm({ } } + function handleNotebookChange(val: string) { + setSelectedNotebookId(val ? Number(val) : null); + setSelectedDocumentIds([]); + } + + function clearNoteSelection() { + setSelectedNotebookId(null); + setSelectedDocumentIds([]); + } + + function toggleDocument(docId: number) { + setSelectedDocumentIds((prev) => + prev.includes(docId) ? prev.filter((id) => id !== docId) : [...prev, docId], + ); + } + function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!intent.trim()) return; @@ -112,6 +135,9 @@ export function AIWritingForm({ keywordCount: DEFAULT_KEYWORD_COUNT, writingMode: hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose', modelName: selectedModel?.name || modelName || undefined, + noteSearchSpaceId: selectedNotebookId ?? undefined, + // 有勾选文档则传 document_id 列表,否则传空数组(搜整个笔记本) + noteSourceIds: selectedNotebookId != null ? selectedDocumentIds : undefined, }); } @@ -206,6 +232,82 @@ export function AIWritingForm({ + {/* 笔记来源(可选) */} +
+
+
+ + 笔记来源(可选) +
+ {selectedNotebookId !== null && ( + + )} +
+ + + + {selectedNotebookId !== null && ( +
+

+ {sourcesLoading + ? '文档加载中…' + : sources.filter((s) => s.document?.id != null).length === 0 + ? '该笔记本暂无文档' + : `选择文档(可多选;不选则搜索整个笔记本${selectedDocumentIds.length > 0 ? `,已选 ${selectedDocumentIds.length} 篇` : ''})`} +

+ {!sourcesLoading && ( +
+ {sources + .filter((s) => s.document?.id != null) + .map((s) => { + const docId = s.document!.id; + const checked = selectedDocumentIds.includes(docId); + return ( + + ); + })} +
+ )} +
+ )} +
+