diff --git a/frontend-web/src/open-canvas/api/ai-writing-sample.ts b/frontend-web/src/open-canvas/api/ai-writing-sample.ts new file mode 100644 index 00000000..d438fa82 --- /dev/null +++ b/frontend-web/src/open-canvas/api/ai-writing-sample.ts @@ -0,0 +1,23 @@ +import { getBackendBaseURL } from '@/core/config'; +import { apiFetch } from '@/core/api'; + +export interface AIWritingSampleExtractResult { + filename: string; + text: string; + char_count: number; + truncated: boolean; +} + +export async function extractAIWritingSample(file: File): Promise { + const form = new FormData(); + form.append('file', file); + const resp = await apiFetch(`${getBackendBaseURL()}/api/ai-writing/sample/extract`, { + method: 'POST', + body: form, + }); + if (!resp.ok) { + const text = await resp.text().catch(() => ''); + throw new Error(text || `样文解析失败:${resp.status}`); + } + return resp.json(); +} 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 9b95b7ff..2b456993 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 } from 'react'; -import { PenLine, SlidersHorizontal } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { FileText, Loader2, PenLine, SlidersHorizontal, Upload } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; @@ -17,6 +17,7 @@ import { useStudioNotebooks, useNotebookSources } from '@/core/studio/hooks'; import type { AIWritingRequest } from '../../types/ai-writing'; import { useArticleTypes } from '../../hooks/useArticleTypes'; import { useWritingForm } from '../../contexts/WritingFormContext'; +import { extractAIWritingSample } from '../../api/ai-writing-sample'; interface AIWritingFormProps { onSubmit: (req: AIWritingRequest) => void; @@ -41,7 +42,9 @@ const INPUT_CLASS = const DEFAULT_KEYWORD_COUNT = 1; const DEFAULT_AUDIENCE = '普通读者'; type MaterialSource = 'general' | 'knowledge_base' | 'notebook'; +type WritingModeType = 'normal' | 'imitate'; const MAX_INTENT_LENGTH = 500; +const MAX_SAMPLE_TEXT_LENGTH = 120000; const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; desc: string }> = [ { value: 'general', label: '通用检索', desc: '用素材收集专家配置的技能检索(默认:知识库检索)' }, @@ -49,6 +52,17 @@ const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; des { value: 'notebook', label: '我的空间', desc: '从空间中检索素材' }, ]; +const WRITING_MODE_OPTIONS: Array<{ value: WritingModeType; label: string }> = [ + { value: 'normal', label: '普通写作' }, + { value: 'imitate', label: '样文仿写' }, +]; + +const IMITATE_STRENGTH_OPTIONS: Array<{ value: 'light' | 'medium' | 'strong'; label: string }> = [ + { value: 'light', label: '轻度' }, + { value: 'medium', label: '中度' }, + { value: 'strong', label: '高度' }, +]; + const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222] dark:text-[#AACEFF]'; const FIELD_TRIGGER_CLASS = 'h-12 w-full rounded-xl border-[#e8dedd] bg-white text-sm shadow-sm'; @@ -61,6 +75,11 @@ export function AIWritingForm({ }: AIWritingFormProps) { const { intent, setIntent, + writingModeType, setWritingModeType, + sampleText, setSampleText, + sampleFilename, setSampleFilename, + imitateStrength, setImitateStrength, + preserveSampleStructure, setPreserveSampleStructure, showOutline, setShowOutline, outlineText, setOutlineText, articleType, setArticleType, @@ -77,6 +96,10 @@ export function AIWritingForm({ modelName, setModelName, } = useWritingForm(); + const [sampleUploading, setSampleUploading] = useState(false); + const [sampleMessage, setSampleMessage] = useState(''); + const [sampleError, setSampleError] = useState(''); + const { models, isLoading: modelsLoading } = useModels(); const { notebooks, isLoading: notebooksLoading } = useStudioNotebooks(!studioToken); const { sources, isLoading: sourcesLoading } = useNotebookSources(selectedNotebookId, studioToken); @@ -154,13 +177,41 @@ export function AIWritingForm({ ); } + async function handleSampleFileChange(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + e.target.value = ''; + if (!file) return; + setSampleUploading(true); + setSampleError(''); + setSampleMessage(''); + try { + const result = await extractAIWritingSample(file); + setSampleFilename(result.filename); + setSampleText(result.text); + setSampleMessage(result.truncated ? '已提取样文文本,内容较长已截断。' : '已提取样文文本。'); + } catch (err) { + setSampleError(err instanceof Error ? err.message : '样文解析失败'); + } finally { + setSampleUploading(false); + } + } + function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!intent.trim()) return; + if (writingModeType === 'imitate' && !sampleText.trim()) { + setSampleError('请上传样文或粘贴样文文本'); + return; + } const wordCount = Math.max(100, parseInt(wordCountInput, 10) || 800); onSubmit({ userIntent: intent.trim(), userOutline: outlineText.trim() || undefined, + writingModeType, + sampleText: writingModeType === 'imitate' ? sampleText.trim() : undefined, + sampleFilename: writingModeType === 'imitate' ? sampleFilename || '粘贴文本' : undefined, + imitateStrength, + preserveSampleStructure, articleType, targetAudience: hideAdvancedFields ? DEFAULT_AUDIENCE @@ -190,6 +241,106 @@ export function AIWritingForm({ +
+ +
+ {WRITING_MODE_OPTIONS.map((opt) => ( + + ))} +
+
+ + {writingModeType === 'imitate' && ( +
+
+ + +
+ {sampleFilename && ( +
+ + {sampleFilename} +
+ )} +