## 核心功能 - 范式检查和复检支持自动分批处理(15句/批) - 智能粒度过滤:根据选中句子数量自动调整检查粒度 * 1-5句:仅句子级检查(术语、语气、表达) * 6-20句:句子级+段落级检查(逻辑结构、递进关系) * 21+句:全文级检查(章节完整性、篇幅占比等) ## 新增文件 - src/stores/paradigm.js:自定义范式管理 store * inferScope:智能推断检查项粒度 * ensureGuidelinesHasScope:为旧范式补充 scope 字段 - src/utils/requirementParser.js:需求文档解析工具 * buildRequirementParserPrompt:生成解析 prompt * validateParadigm:范式验证(兼容字符串/对象格式) - src/components/RequirementParserPanel.vue:需求解析面板 * 支持粘贴文本或选择文件 * AI 自动生成范式配置(specializedPrompt + expertGuidelines) * 可视化编辑检查项(title/description/scope) ## 主要改进 - ArticleRewritePanel.vue: * 范式检查支持粒度过滤和批量处理 * 复检功能支持粒度过滤和批量处理 * 集成 paradigm store 自动补充 scope 字段 * 添加批次进度显示 - RequirementParserPanel.vue: * 修复对象格式 expertGuidelines 显示问题 * 支持编辑 title/description/scope 三个字段 * 添加粒度下拉选择器(句子级/段落级/全文级) ## 技术细节 - 向后兼容:支持字符串和对象两种 expertGuidelines 格式 - 自动转换:旧范式字符串格式自动转换为对象格式并推断 scope - 错误处理:JSON 解析失败时提供降级方案 - 控制台日志:[复检] 前缀标识复检相关日志 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
432 lines
16 KiB
Vue
432 lines
16 KiB
Vue
<template>
|
||
<div class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||
<div class="w-full max-w-4xl bg-slate-800 rounded-lg shadow-2xl border border-slate-700 max-h-[90vh] flex flex-col">
|
||
<!-- 头部 -->
|
||
<header class="p-4 border-b border-slate-700 flex items-center justify-between shrink-0">
|
||
<h2 class="font-bold text-lg text-white flex items-center gap-2">
|
||
<span class="text-2xl">🎯</span> 需求文档解析
|
||
</h2>
|
||
<button
|
||
@click="$emit('close')"
|
||
class="text-slate-400 hover:text-white transition"
|
||
>
|
||
<span class="text-xl">×</span>
|
||
</button>
|
||
</header>
|
||
|
||
<!-- 主体内容 -->
|
||
<div class="flex-1 overflow-y-auto p-6">
|
||
<!-- 步骤1:输入需求文档 -->
|
||
<section v-if="step === 1" class="space-y-4">
|
||
<div class="flex items-center gap-2 text-sm text-slate-400">
|
||
<span class="bg-indigo-600 text-white w-6 h-6 rounded-full flex items-center justify-center text-xs">1</span>
|
||
<span>输入需求文档</span>
|
||
</div>
|
||
|
||
<div class="space-y-3">
|
||
<!-- 文档来源选择 -->
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-sm text-slate-400">来源:</span>
|
||
<div class="flex bg-slate-900 rounded p-1 border border-slate-700">
|
||
<button
|
||
@click="sourceType = 'text'"
|
||
:class="['text-xs px-3 py-1 rounded transition',
|
||
sourceType === 'text' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white']"
|
||
>
|
||
粘贴文本
|
||
</button>
|
||
<button
|
||
@click="sourceType = 'file'"
|
||
:class="['text-xs px-3 py-1 rounded transition',
|
||
sourceType === 'file' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white']"
|
||
>
|
||
选择文件
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文本输入区 -->
|
||
<div v-if="sourceType === 'text'">
|
||
<textarea
|
||
v-model="requirementText"
|
||
placeholder="请粘贴需求文档内容,例如会议要求、文稿规范等..."
|
||
class="w-full h-64 p-4 bg-slate-900 border border-slate-700 rounded text-sm text-slate-200 placeholder-slate-600 focus:outline-none focus:border-indigo-500 resize-none"
|
||
></textarea>
|
||
<div class="flex items-center justify-between mt-2 text-xs text-slate-500">
|
||
<span>{{ requirementText.length }} 字符</span>
|
||
<span v-if="requirementText.length > 10000" class="text-amber-500">
|
||
⚠️ 文档过长可能影响解析质量,建议控制在10000字符以内
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文件选择区 -->
|
||
<div v-if="sourceType === 'file'" class="space-y-3">
|
||
<input
|
||
type="file"
|
||
ref="fileInput"
|
||
@change="handleFileSelect"
|
||
accept=".txt,.md"
|
||
class="hidden"
|
||
/>
|
||
<button
|
||
@click="$refs.fileInput.click()"
|
||
class="w-full p-6 border-2 border-dashed border-slate-700 rounded-lg hover:border-indigo-500 transition text-slate-400 hover:text-indigo-400 flex flex-col items-center gap-2"
|
||
>
|
||
<span class="text-3xl">📄</span>
|
||
<span class="text-sm">点击选择文件(支持 .txt, .md)</span>
|
||
<span v-if="selectedFileName" class="text-xs text-indigo-400 mt-2">
|
||
已选择:{{ selectedFileName }}
|
||
</span>
|
||
</button>
|
||
<div v-if="requirementText" class="bg-slate-900 p-4 rounded border border-slate-700 max-h-48 overflow-y-auto">
|
||
<pre class="text-xs text-slate-400 whitespace-pre-wrap">{{ requirementText.slice(0, 500) }}{{ requirementText.length > 500 ? '...' : '' }}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="flex justify-end gap-2 pt-4">
|
||
<button
|
||
@click="$emit('close')"
|
||
class="px-4 py-2 text-sm rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
@click="startParsing"
|
||
:disabled="!requirementText || isParsing"
|
||
class="px-4 py-2 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-500 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||
>
|
||
<span v-if="isParsing" class="animate-spin">↻</span>
|
||
<span>{{ isParsing ? '解析中...' : '开始解析' }}</span>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 步骤2:解析进度 -->
|
||
<section v-if="step === 2" class="space-y-4">
|
||
<div class="flex items-center gap-2 text-sm text-slate-400 mb-4">
|
||
<span class="bg-indigo-600 text-white w-6 h-6 rounded-full flex items-center justify-center text-xs animate-pulse">2</span>
|
||
<span>AI 解析中...</span>
|
||
</div>
|
||
|
||
<div class="bg-slate-900 p-6 rounded-lg border border-slate-700 space-y-3">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-8 h-8 border-4 border-indigo-600 border-t-transparent rounded-full animate-spin"></div>
|
||
<span class="text-slate-300">{{ parsingProgress || '正在分析需求文档...' }}</span>
|
||
</div>
|
||
<div class="text-xs text-slate-500 leading-relaxed">
|
||
AI 正在提取文档中的核心要求,生成范式配置。这可能需要 10-30 秒...
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 步骤3:编辑确认 -->
|
||
<section v-if="step === 3" class="space-y-4">
|
||
<div class="flex items-center gap-2 text-sm text-slate-400 mb-4">
|
||
<span class="bg-green-600 text-white w-6 h-6 rounded-full flex items-center justify-center text-xs">3</span>
|
||
<span>编辑确认</span>
|
||
</div>
|
||
|
||
<div v-if="parsedParadigm" class="space-y-4">
|
||
<!-- 范式名称 -->
|
||
<div>
|
||
<label class="text-sm text-slate-400 block mb-2">范式名称</label>
|
||
<input
|
||
v-model="parsedParadigm.metadata.name"
|
||
placeholder="如:2025年组织生活会对照检查材料"
|
||
class="w-full p-2 bg-slate-900 border border-slate-700 rounded text-sm text-white focus:outline-none focus:border-indigo-500"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 范式描述 -->
|
||
<div>
|
||
<label class="text-sm text-slate-400 block mb-2">描述</label>
|
||
<input
|
||
v-model="parsedParadigm.metadata.description"
|
||
placeholder="简要描述该范式的用途..."
|
||
class="w-full p-2 bg-slate-900 border border-slate-700 rounded text-sm text-white focus:outline-none focus:border-indigo-500"
|
||
/>
|
||
</div>
|
||
|
||
<!-- System Prompt -->
|
||
<div>
|
||
<label class="text-sm text-slate-400 block mb-2 flex items-center justify-between">
|
||
<span>System Prompt(系统提示词)</span>
|
||
<span class="text-xs text-slate-600">{{ parsedParadigm.specializedPrompt.length }} 字符</span>
|
||
</label>
|
||
<textarea
|
||
v-model="parsedParadigm.specializedPrompt"
|
||
class="w-full h-48 p-3 bg-slate-900 border border-slate-700 rounded text-xs text-slate-200 font-mono focus:outline-none focus:border-indigo-500 resize-none"
|
||
></textarea>
|
||
</div>
|
||
|
||
<!-- Expert Guidelines -->
|
||
<div>
|
||
<label class="text-sm text-slate-400 block mb-2 flex items-center justify-between">
|
||
<span>专家检查指令</span>
|
||
<button
|
||
@click="addGuideline"
|
||
class="text-xs px-2 py-1 rounded bg-green-600 text-white hover:bg-green-500 transition"
|
||
>
|
||
+ 添加
|
||
</button>
|
||
</label>
|
||
<div class="space-y-3">
|
||
<div
|
||
v-for="(guideline, index) in parsedParadigm.expertGuidelines"
|
||
:key="index"
|
||
class="bg-slate-900 border border-slate-700 rounded p-3 space-y-2"
|
||
>
|
||
<div class="flex items-start gap-2">
|
||
<span class="text-xs text-slate-600 mt-2 shrink-0">{{ index + 1 }}.</span>
|
||
<div class="flex-1 space-y-2">
|
||
<!-- 标题 -->
|
||
<input
|
||
v-model="guideline.title"
|
||
placeholder="检查项标题"
|
||
class="w-full p-2 bg-slate-800 border border-slate-600 rounded text-xs text-white focus:outline-none focus:border-indigo-500"
|
||
/>
|
||
<!-- 描述 -->
|
||
<textarea
|
||
v-model="guideline.description"
|
||
placeholder="检查项详细描述"
|
||
rows="2"
|
||
class="w-full p-2 bg-slate-800 border border-slate-600 rounded text-xs text-white focus:outline-none focus:border-indigo-500 resize-none"
|
||
></textarea>
|
||
<!-- Scope 选择 -->
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs text-slate-500">粒度:</span>
|
||
<select
|
||
v-model="guideline.scope"
|
||
class="text-xs px-2 py-1 bg-slate-800 border border-slate-600 rounded text-white focus:outline-none focus:border-indigo-500"
|
||
>
|
||
<option value="sentence">句子级</option>
|
||
<option value="paragraph">段落级</option>
|
||
<option value="document">全文级</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button
|
||
@click="removeGuideline(index)"
|
||
class="text-xs px-2 py-2 rounded bg-red-600/20 text-red-400 hover:bg-red-600/30 transition shrink-0"
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 关键要求(只读预览) -->
|
||
<div v-if="parsedParadigm.metadata.keyRequirements">
|
||
<label class="text-sm text-slate-400 block mb-2">核心要求关键词</label>
|
||
<div class="flex flex-wrap gap-2">
|
||
<span
|
||
v-for="(keyword, index) in parsedParadigm.metadata.keyRequirements"
|
||
:key="index"
|
||
class="px-2 py-1 bg-indigo-900/50 text-indigo-300 rounded text-xs"
|
||
>
|
||
{{ keyword }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="flex justify-end gap-2 pt-4">
|
||
<button
|
||
@click="step = 1"
|
||
class="px-4 py-2 text-sm rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||
>
|
||
重新解析
|
||
</button>
|
||
<button
|
||
@click="saveParadigm"
|
||
class="px-4 py-2 text-sm rounded bg-green-600 text-white hover:bg-green-500 transition flex items-center gap-2"
|
||
>
|
||
<span>✓</span>
|
||
<span>保存并使用</span>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 解析失败 -->
|
||
<section v-if="step === 'error'" class="space-y-4">
|
||
<div class="bg-red-900/20 border border-red-700 rounded-lg p-6 text-center">
|
||
<span class="text-4xl block mb-3">⚠️</span>
|
||
<p class="text-red-400 mb-2">解析失败</p>
|
||
<p class="text-xs text-slate-500">{{ errorMessage }}</p>
|
||
<button
|
||
@click="step = 1"
|
||
class="mt-4 px-4 py-2 text-sm rounded bg-slate-700 text-white hover:bg-slate-600 transition"
|
||
>
|
||
返回重试
|
||
</button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { useParadigmStore } from '../stores/paradigm'
|
||
import DeepSeekAPI from '../api/deepseek'
|
||
import { config } from '../utils/config'
|
||
import {
|
||
buildRequirementParserPrompt,
|
||
parseParadigmConfig,
|
||
buildParadigmObject,
|
||
validateParadigm
|
||
} from '../utils/requirementParser'
|
||
|
||
const emit = defineEmits(['close', 'paradigm-created'])
|
||
|
||
const paradigmStore = useParadigmStore()
|
||
|
||
// 状态
|
||
const step = ref(1) // 1: 输入, 2: 解析中, 3: 编辑确认, 'error': 失败
|
||
const sourceType = ref('text') // 'text' | 'file'
|
||
const requirementText = ref('')
|
||
const selectedFileName = ref('')
|
||
const isParsing = ref(false)
|
||
const parsingProgress = ref('')
|
||
const parsedParadigm = ref(null)
|
||
const errorMessage = ref('')
|
||
|
||
// 文件输入引用
|
||
const fileInput = ref(null)
|
||
|
||
/**
|
||
* 处理文件选择
|
||
*/
|
||
async function handleFileSelect(event) {
|
||
const file = event.target.files[0]
|
||
if (!file) return
|
||
|
||
selectedFileName.value = file.name
|
||
|
||
try {
|
||
const text = await file.text()
|
||
requirementText.value = text
|
||
} catch (error) {
|
||
console.error('读取文件失败:', error)
|
||
alert('读取文件失败,请重试')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 开始解析
|
||
*/
|
||
async function startParsing() {
|
||
if (!requirementText.value.trim()) {
|
||
alert('请先输入需求文档内容')
|
||
return
|
||
}
|
||
|
||
isParsing.value = true
|
||
step.value = 2
|
||
parsingProgress.value = '正在生成解析 Prompt...'
|
||
|
||
try {
|
||
// 构建 Prompt
|
||
const prompt = buildRequirementParserPrompt(requirementText.value)
|
||
|
||
parsingProgress.value = '正在调用 AI 分析...'
|
||
|
||
// 调用 DeepSeek API - 传递配置
|
||
const api = new DeepSeekAPI({
|
||
url: config.apiUrl,
|
||
key: config.apiKey
|
||
})
|
||
let responseText = ''
|
||
|
||
await api.generateContent(
|
||
prompt,
|
||
(chunk) => {
|
||
responseText += chunk
|
||
parsingProgress.value = `已接收 ${responseText.length} 字符...`
|
||
},
|
||
{ temperature: 0.7 } // 使用较高温度以获得更有创造性的输出
|
||
)
|
||
|
||
parsingProgress.value = '正在解析 AI 返回结果...'
|
||
|
||
// 调试:输出响应
|
||
console.log('收到的完整响应长度:', responseText.length);
|
||
console.log('响应内容:', responseText);
|
||
|
||
// 解析返回的配置
|
||
const parsedConfig = parseParadigmConfig(responseText)
|
||
if (!parsedConfig) {
|
||
throw new Error(`AI 返回的配置格式不正确。响应长度:${responseText.length} 字符。请检查浏览器控制台查看完整输出。`)
|
||
}
|
||
|
||
// 构建完整的范式对象
|
||
const paradigm = buildParadigmObject(parsedConfig)
|
||
|
||
// 验证
|
||
const validation = validateParadigm(paradigm)
|
||
if (!validation.valid) {
|
||
console.warn('范式验证警告:', validation.errors)
|
||
// 仍然允许用户编辑,只是给出提示
|
||
}
|
||
|
||
parsedParadigm.value = paradigm
|
||
step.value = 3
|
||
} catch (error) {
|
||
console.error('解析失败:', error)
|
||
errorMessage.value = error.message || '未知错误'
|
||
step.value = 'error'
|
||
} finally {
|
||
isParsing.value = false
|
||
parsingProgress.value = ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加新的检查指令
|
||
*/
|
||
function addGuideline() {
|
||
if (!parsedParadigm.value) return
|
||
parsedParadigm.value.expertGuidelines.push({
|
||
title: '新的检查项',
|
||
description: '请填写检查项的详细描述...',
|
||
scope: 'sentence'
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 删除检查指令
|
||
*/
|
||
function removeGuideline(index) {
|
||
if (!parsedParadigm.value) return
|
||
parsedParadigm.value.expertGuidelines.splice(index, 1)
|
||
}
|
||
|
||
/**
|
||
* 保存范式
|
||
*/
|
||
function saveParadigm() {
|
||
if (!parsedParadigm.value) return
|
||
|
||
// 再次验证
|
||
const validation = validateParadigm(parsedParadigm.value)
|
||
if (!validation.valid) {
|
||
if (!confirm(`范式存在以下问题:\n${validation.errors.join('\n')}\n\n是否仍然保存?`)) {
|
||
return
|
||
}
|
||
}
|
||
|
||
// 保存到 store
|
||
paradigmStore.addCustomParadigm(parsedParadigm.value)
|
||
|
||
// 通知父组件
|
||
emit('paradigm-created', parsedParadigm.value)
|
||
emit('close')
|
||
}
|
||
</script>
|