Files
ai-write/src/components/WriterPanel.vue
empty f9f0785106 feat: 完善范式润色功能和相关组件
- 新增范式选择模态框 (ParadigmSelectorModal)
- 新增差异标注面板 (DiffAnnotationPanel)
- 新增精确差异处理工具 (preciseDiff.js)
- 更新各面板组件支持范式润色功能
- 更新范式配置文件 (paradigms.js)
- 更新依赖配置

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 03:02:21 +08:00

279 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<aside class="w-[400px] h-screen flex flex-col border-r border-slate-700 bg-slate-800 shrink-0">
<!-- 头部 -->
<header class="p-4 border-b border-slate-700 flex items-center justify-between">
<h1 class="font-bold text-lg text-white flex items-center gap-2">
<span class="text-2xl"></span> AI 写作工坊
</h1>
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
</header>
<!-- 内容区 -->
<div class="flex-1 overflow-y-auto p-4 space-y-6 min-h-0">
<!-- 写作任务 -->
<section>
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-medium text-slate-400">1. 写作任务 (User Input)</label>
<div class="flex bg-slate-900 rounded p-0.5 border border-slate-700">
<button
@click="inputType = 'text'"
:class="['text-xs px-2 py-0.5 rounded transition', inputType === 'text' ? 'bg-slate-700 text-white' : 'text-slate-500 hover:text-slate-300']"
>自由文本</button>
<button
@click="inputType = 'outline'"
:class="['text-xs px-2 py-0.5 rounded transition', inputType === 'outline' ? 'bg-slate-700 text-white' : 'text-slate-500 hover:text-slate-300']"
>大纲模式</button>
</div>
</div>
<div v-if="inputType === 'text'">
<textarea
v-model="inputTask"
class="w-full h-32 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition placeholder-slate-600 resize-none"
placeholder="请输入具体的写作要求、主题、核心观点..."
></textarea>
<div class="text-right mt-1">
<span class="text-xs text-slate-500">{{ inputTask.length }} </span>
</div>
</div>
<div v-else class="space-y-2">
<input v-model="outlinePoints.topic" placeholder="核心主题" class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs outline-none focus:border-blue-500">
<input v-model="outlinePoints.audience" placeholder="目标受众" class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs outline-none focus:border-blue-500">
<textarea v-model="outlinePoints.keyPoints" placeholder="关键观点(每行一个)" class="w-full h-20 bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs outline-none focus:border-blue-500 resize-none"></textarea>
</div>
</section>
<!-- 参考案例 -->
<section>
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-medium text-slate-400">2. 参考案例 (Style Ref)</label>
<button @click="showRefInput = !showRefInput" class="text-xs text-blue-400 hover:text-blue-300">
{{ showRefInput ? '取消' : '+ 添加案例' }}
</button>
</div>
<div v-if="showRefInput" class="mb-3 p-3 bg-slate-900 rounded-lg border border-blue-500/30">
<input v-model="newRefTitle" placeholder="案例标题" class="w-full mb-2 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs outline-none">
<textarea v-model="newRefContent" placeholder="粘贴优秀的参考文本..." class="w-full h-24 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs outline-none resize-none mb-2"></textarea>
<button @click="addReference" class="w-full bg-blue-600 hover:bg-blue-500 text-xs py-1.5 rounded text-white">确认添加</button>
</div>
<div class="space-y-2">
<div v-for="(ref, index) in references" :key="index" class="group flex flex-col bg-slate-700/50 p-2 rounded border border-slate-700 hover:border-slate-600 transition">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-2 overflow-hidden">
<span class="text-lg">📄</span>
<span class="text-xs font-medium text-slate-200 truncate">{{ ref.title }}</span>
</div>
<button @click="removeReference(index)" class="text-slate-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2">×</button>
</div>
<div class="pl-7">
<p class="text-[10px] text-slate-500 truncate mb-1.5">{{ ref.content.substring(0, 30) }}...</p>
<!-- 风格分析结果 -->
<div v-if="ref.isAnalyzing" class="flex items-center gap-1 text-[10px] text-indigo-400 animate-pulse">
<span class="w-1 h-1 rounded-full bg-indigo-400"></span>
正在分析风格特征...
</div>
<div v-else-if="ref.styleTags && ref.styleTags.length > 0" class="flex flex-wrap gap-1">
<span
v-for="tag in ref.styleTags"
:key="tag"
class="text-[10px] px-1.5 py-0.5 rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
</section>
<!-- 专家指令范式预设时显示 -->
<section v-if="activeParadigm">
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-medium text-amber-400 flex items-center gap-1">
专家指令 (Expert Guidelines)
</label>
<button
@click="clearParadigm"
class="text-[10px] text-slate-500 hover:text-red-400 transition"
>
清除范式
</button>
</div>
<div class="bg-amber-950/20 border border-amber-500/30 rounded-lg p-3 space-y-2">
<div class="flex items-center gap-2 mb-2">
<span class="text-amber-400">{{ activeParadigm.icon }}</span>
<span class="text-xs font-medium text-amber-300">已加载{{ activeParadigm.name }}专家标准</span>
</div>
<div class="space-y-1.5">
<div
v-for="(guideline, idx) in expertGuidelines"
:key="idx"
class="text-[11px] text-slate-400 flex items-start gap-2"
>
<span class="text-amber-500/70 shrink-0">{{ idx + 1 }}.</span>
<div>
<span class="text-amber-300/80 font-medium">{{ guideline.title }}</span>
<span class="text-slate-500">{{ guideline.description }}</span>
</div>
</div>
</div>
</div>
</section>
<!-- 输出规范 -->
<section>
<label class="block text-sm font-medium text-slate-400 mb-2">3. 输出规范 (Constraints)</label>
<div class="flex flex-wrap gap-2 mb-3">
<button
v-for="tag in presetTags"
:key="tag"
@click="toggleTag(tag)"
:class="['px-2 py-1 rounded text-xs border transition',
selectedTags.includes(tag)
? 'bg-blue-600/20 border-blue-500 text-blue-300'
: 'bg-slate-900 border-slate-700 text-slate-500 hover:border-slate-500']"
>
{{ tag }}
</button>
</div>
<input
v-model="customConstraint"
type="text"
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs focus:border-blue-500 outline-none"
placeholder="补充其他要求"
>
<!-- 深度模式开关 -->
<div class="mt-4 flex items-center justify-between bg-slate-900/50 p-3 rounded border border-indigo-500/30">
<div class="flex flex-col">
<span class="text-sm font-bold text-indigo-300 flex items-center gap-1">
🧠 深度模式 (Deep Mode)
</span>
<span class="text-[10px] text-slate-500">模拟人类初稿-反思-润色的思维链</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" v-model="isDeepMode" class="sr-only peer">
<div class="w-9 h-5 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
</section>
</div>
<!-- 底部操作区 -->
<footer class="p-4 bg-slate-800 border-t border-slate-700 space-y-3">
<div class="flex items-center justify-between">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showPromptDebug" class="hidden">
<div class="w-8 h-4 bg-slate-900 rounded-full border border-slate-600 relative transition-colors" :class="{'bg-blue-900 border-blue-500': showPromptDebug}">
<div class="w-2 h-2 bg-slate-400 rounded-full absolute top-1 left-1 transition-transform" :class="{'translate-x-4 bg-blue-400': showPromptDebug}"></div>
</div>
<span class="text-xs text-slate-500 select-none">预览构建的 Prompt</span>
</label>
<span class="text-xs text-slate-600">deepseek</span>
</div>
<!-- API配置 -->
<div class="space-y-2">
<input
v-model="apiUrl"
type="text"
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs focus:border-blue-500 outline-none"
placeholder="API 地址"
>
<input
v-model="apiKey"
type="password"
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs focus:border-blue-500 outline-none"
placeholder="API Key"
>
</div>
<button
@click="generateContent"
:disabled="!canGenerate"
class="w-full py-3 rounded-lg font-bold text-white shadow-lg shadow-blue-900/20 flex items-center justify-center gap-2 transition-all transform active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
:class="isGenerating ? 'bg-slate-700' : 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500'"
>
<span v-if="isGenerating" class="animate-spin text-lg"></span>
{{ isGenerating ? '正在思考与撰写...' : '开始生成文稿' }}
</button>
</footer>
</aside>
</template>
<script setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useAppStore } from '../stores/app'
const appStore = useAppStore()
const {
inputTask,
references,
showRefInput,
newRefTitle,
newRefContent,
selectedTags,
customConstraint,
activeParadigm,
expertGuidelines,
isGenerating,
showPromptDebug,
apiUrl,
apiKey,
isDeepMode,
inputType,
outlinePoints
} = storeToRefs(appStore)
const { switchPage, clearParadigm } = appStore
const presetTags = ['Markdown格式', '总分总结构', '数据支撑', '语气幽默', '严禁被动语态', '引用权威来源']
// 添加参考案例
const addReference = () => {
if (!newRefTitle.value || !newRefContent.value) return
appStore.addReferenceFromAnalysis(newRefTitle.value, newRefContent.value)
newRefTitle.value = ''
newRefContent.value = ''
showRefInput.value = false
}
// 删除参考案例
const removeReference = (index) => {
references.value.splice(index, 1)
}
// 切换标签
const toggleTag = (tag) => {
if (selectedTags.value.includes(tag)) {
selectedTags.value = selectedTags.value.filter(t => t !== tag)
} else {
selectedTags.value.push(tag)
}
}
// 生成内容
const generateContent = async () => {
try {
await appStore.generateContentAction()
} catch (error) {
alert(error.message)
}
}
const canGenerate = computed(() => {
if (isGenerating.value) return false
if (inputType.value === 'text') {
return !!inputTask.value?.trim()
} else {
return !!outlinePoints.value?.topic?.trim()
}
})
</script>