feat: 完善范式润色功能和相关组件
- 新增范式选择模态框 (ParadigmSelectorModal) - 新增差异标注面板 (DiffAnnotationPanel) - 新增精确差异处理工具 (preciseDiff.js) - 更新各面板组件支持范式润色功能 - 更新范式配置文件 (paradigms.js) - 更新依赖配置 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
53
docs/找问题Prompt.md
Normal file
53
docs/找问题Prompt.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# AI检查Prompt - 党委副书记2025年组织生活会个人对照检查发言材料
|
||||
|
||||
你是一个具有较高政治敏感性和党内写作经验的AI助手,要求根据下面的写作指导思想,**为一篇党委副书记的个人对照检查发言材料进行检查和改进**。你需要**逐一分析**文章中的每一条问题,并根据以下**标准检查是否符合要求**。
|
||||
|
||||
---
|
||||
|
||||
## **写作要点**:
|
||||
1. **每个问题必须基于以下“带头”领域进行深刻剖析:**
|
||||
- 强化政治忠诚、提高政治能力
|
||||
- 增强党性修养,坚定理想信念
|
||||
- 敬畏人民、敬畏组织、敬畏法纪
|
||||
- 干事创业、担当作为
|
||||
- 坚决扛起管党治党责任
|
||||
2. **结构要求:**
|
||||
- **问题表述**要清晰、具象
|
||||
- **分析根源**要深刻,不仅局限于“行为”或“表现”,更要分析思想层面的问题
|
||||
- 每个问题要配有**具体例证**和**后果分析**,确保不空泛
|
||||
|
||||
---
|
||||
|
||||
## **具体检查步骤**:
|
||||
### 1. **检查每一条“问题表述”的清晰度**:
|
||||
- **问题表述是否具体**:检查是否有过于抽象的描述(如“政治意识不够强”);需要将问题具象化为行为或思想上的具体表现。
|
||||
- **例子**:是否有实际例子来支撑问题(如“某次重要决策中,未能及时向党中央报告”等)。
|
||||
|
||||
### 2. **深度分析“思想根源”**:
|
||||
- **是否分析了问题的思想来源**:检查文章是否仅仅停留在行为问题的表述上,而没有深入剖析背后的思想根源。
|
||||
- **建议**:每个问题应附带对“思想波动”、“认识偏差”的分析,**使用党内常见表述**如“学用脱节”,“上接天线下接地气”等。
|
||||
- **例子**:是否有分析到思想层面的“误区”,如“表面上说得好,但实际工作中没有贯彻党的基本方针政策”。
|
||||
|
||||
### 3. **分析“行为习惯”**:
|
||||
- **是否提到具体的“行为习惯”**:对于每个问题,检查是否有行为上的具体表现,如“开会时未能充分发挥批评与自我批评”等。
|
||||
- **要点**:强调行为上的不规范或松懈,如何影响了工作效果或党性修养。
|
||||
|
||||
### 4. **典型案例的使用**:
|
||||
- **是否引入了具体的典型案例**:文章是否包含了至少1-2个实际案例,**并以此为基础展开自我反思**。
|
||||
- **案例要求**:检查是否有“通过案例揭示出个人在党性、作风或责任心上的差距”。
|
||||
|
||||
### 5. **改进措施的具体性与可操作性**:
|
||||
- **是否提出了针对性的改进措施**:检查文章是否在每个问题后都提出了明确的整改措施,而不仅是泛泛之谈。
|
||||
- **是否列举了明确的整改路径**:例如,改进政治意识的方法,落实党纪党规的方法。
|
||||
|
||||
---
|
||||
|
||||
## **最终输出要求:**
|
||||
- **检查与改进建议**:对每个问题逐条进行检查,并指出其中的不足之处。
|
||||
- **确保检查后每个问题都有明确的“整改措施”**,并且这些措施必须具有可操作性。
|
||||
- **避免空话套话**:批评必须具体、切实,避免只做表面文章。
|
||||
- **增加建设性反馈**:除了指出问题,还需要为改进提供具有指导意义的反馈。
|
||||
|
||||
---
|
||||
|
||||
开始检查文章。
|
||||
207
package-lock.json
generated
207
package-lock.json
generated
@@ -13,6 +13,9 @@
|
||||
"@tiptap/starter-kit": "^3.15.3",
|
||||
"@tiptap/vue-3": "^3.15.3",
|
||||
"axios": "^1.6.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"docx": "^9.5.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"marked": "^9.1.0",
|
||||
"pinia": "^2.1.0",
|
||||
"sql.js": "^1.13.0",
|
||||
@@ -1322,6 +1325,15 @@
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
||||
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
|
||||
@@ -1490,6 +1502,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
@@ -1511,6 +1529,47 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/docx": {
|
||||
"version": "9.5.1",
|
||||
"resolved": "https://registry.npmjs.org/docx/-/docx-9.5.1.tgz",
|
||||
"integrity": "sha512-ABDI7JEirFD2+bHhOBlsGZxaG1UgZb2M/QMKhLSDGgVNhxDesTCDcP+qoDnDGjZ4EOXTRfUjUgwHVuZ6VSTfWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^24.0.1",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/docx/node_modules/nanoid": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
|
||||
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -1652,6 +1711,12 @@
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-saver": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
@@ -1788,6 +1853,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -1800,6 +1875,45 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
@@ -1901,6 +2015,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -1925,6 +2045,12 @@
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1981,6 +2107,12 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prosemirror-changeset": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz",
|
||||
@@ -2194,6 +2326,21 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.55.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
|
||||
@@ -2245,6 +2392,27 @@
|
||||
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
|
||||
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -2260,12 +2428,33 @@
|
||||
"integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
@@ -2380,6 +2569,24 @@
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"@tiptap/starter-kit": "^3.15.3",
|
||||
"@tiptap/vue-3": "^3.15.3",
|
||||
"axios": "^1.6.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"docx": "^9.5.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"marked": "^9.1.0",
|
||||
"pinia": "^2.1.0",
|
||||
"sql.js": "^1.13.0",
|
||||
|
||||
@@ -2,17 +2,9 @@
|
||||
<aside class="w-[400px] flex flex-col border-r border-slate-700 bg-slate-800 shrink-0 h-screen">
|
||||
<!-- 头部 -->
|
||||
<header class="p-4 border-b border-slate-700 flex items-center justify-between shrink-0">
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="font-bold text-lg text-white flex items-center gap-2">
|
||||
<span class="text-2xl">✍️</span> 写作范式分析
|
||||
<span class="text-2xl">🎯</span> 写作范式分析
|
||||
</h1>
|
||||
<button
|
||||
@click="switchPage('writer')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
返回写作
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
|
||||
</header>
|
||||
|
||||
@@ -219,159 +211,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 写作约束(高级) -->
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-2">写作约束(每行一条)</label>
|
||||
<textarea
|
||||
v-model="editForm.constraintsInput"
|
||||
rows="4"
|
||||
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition resize-none font-mono"
|
||||
placeholder="如: 开篇必须明确阐述问题 结尾需总结核心要点"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 维度配置(新架构) -->
|
||||
<!-- ⭐ 核心字段:完整 Prompt -->
|
||||
<div class="border-t border-slate-700 pt-4 mt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-sm text-slate-400 font-medium">📐 维度配置(高级)</label>
|
||||
<button
|
||||
@click="toggleDimensionEditor"
|
||||
class="text-xs text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
{{ showDimensionEditor ? '收起' : '展开' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showDimensionEditor" class="space-y-3">
|
||||
<!-- 维度集选择 -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500 mb-1">选择维度集模板</label>
|
||||
<select
|
||||
v-model="editForm.dimensionSetId"
|
||||
@change="onDimensionSetChange"
|
||||
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
||||
>
|
||||
<option :value="null">不使用维度集</option>
|
||||
<option v-for="ds in dimensionSetOptions" :key="ds.id" :value="ds.id">
|
||||
{{ ds.name }} - {{ ds.description }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 维度列表编辑 -->
|
||||
<div v-if="editForm.dimensions.length > 0">
|
||||
<label class="block text-xs text-slate-500 mb-2">维度列表(可编辑)</label>
|
||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
||||
<div
|
||||
v-for="(dim, index) in editForm.dimensions"
|
||||
:key="dim.id || index"
|
||||
class="bg-slate-900/50 rounded p-2 border border-slate-700"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="dim.name"
|
||||
class="flex-1 bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs"
|
||||
placeholder="维度名称"
|
||||
/>
|
||||
<button
|
||||
@click="removeDimension(index)"
|
||||
class="text-red-400 hover:text-red-300 text-xs px-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="dim.focus"
|
||||
class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-400"
|
||||
placeholder="关注重点(如:理想信念、宗旨意识)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="addDimension"
|
||||
class="mt-2 text-xs text-green-400 hover:text-green-300 flex items-center gap-1"
|
||||
>
|
||||
<span>+</span> 添加维度
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 快速添加维度 -->
|
||||
<div v-else class="text-center py-4 text-slate-500 text-xs">
|
||||
<p>选择一个维度集模板,或手动添加维度</p>
|
||||
<button
|
||||
@click="addDimension"
|
||||
class="mt-2 text-green-400 hover:text-green-300"
|
||||
>
|
||||
+ 添加自定义维度
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 素材库配置(新增) -->
|
||||
<div class="border-t border-slate-700 pt-4 mt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-sm text-slate-400 font-medium">📚 素材库配置</label>
|
||||
<button
|
||||
@click="toggleReferenceEditor"
|
||||
class="text-xs text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
{{ showReferenceEditor ? '收起' : '展开' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showReferenceEditor" class="space-y-3">
|
||||
<!-- 自动匹配开关 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-slate-500">自动匹配相关素材</span>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="editForm.autoMatchRefs" class="sr-only peer">
|
||||
<div class="w-9 h-5 bg-slate-600 peer-focus:ring-2 peer-focus:ring-blue-500 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-blue-600"></div>
|
||||
<label class="block text-sm text-slate-400 mb-2 flex items-center gap-2">
|
||||
<span>✨</span> 完整 Prompt(核心字段)
|
||||
<span class="text-xs text-slate-500 font-normal">粘贴您的完整提示词</span>
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="editForm.specializedPrompt"
|
||||
rows="12"
|
||||
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition resize-y font-mono leading-relaxed"
|
||||
placeholder="在此粘贴您的完整 Prompt...
|
||||
|
||||
<!-- 素材类型筛选 -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500 mb-2">手动选择素材(按类型)</label>
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
<button
|
||||
v-for="type in referenceTypes"
|
||||
:key="type.id"
|
||||
@click="filterReferencesByType(type.id)"
|
||||
:class="['text-xs px-2 py-1 rounded transition',
|
||||
selectedRefType === type.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-700 text-slate-300 hover:bg-slate-600']"
|
||||
>
|
||||
{{ type.icon }} {{ type.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
示例:
|
||||
# Role
|
||||
你是一个具有较高政治敏感性的AI助手...
|
||||
|
||||
<!-- 素材列表 -->
|
||||
<div class="max-h-40 overflow-y-auto space-y-1">
|
||||
<div
|
||||
v-for="ref in filteredReferences"
|
||||
:key="ref.id"
|
||||
@click="toggleReference(ref.id)"
|
||||
:class="['p-2 rounded cursor-pointer border transition text-xs',
|
||||
editForm.selectedRefs.includes(ref.id)
|
||||
? 'bg-blue-900/30 border-blue-500'
|
||||
: 'bg-slate-900/50 border-slate-700 hover:border-slate-500']"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-white">{{ ref.title }}</span>
|
||||
<span v-if="editForm.selectedRefs.includes(ref.id)" class="text-green-400">✓</span>
|
||||
</div>
|
||||
<div class="text-slate-500 text-[10px] mt-1">{{ ref.excerptCount }} 条可引用内容</div>
|
||||
</div>
|
||||
</div>
|
||||
# 任务目标
|
||||
你需要逐一分析文章中的每一条问题...
|
||||
|
||||
<!-- 已选素材摘要 -->
|
||||
<div v-if="editForm.selectedRefs.length > 0" class="text-xs text-slate-400">
|
||||
已选择 {{ editForm.selectedRefs.length }} 个素材
|
||||
</div>
|
||||
</div>
|
||||
# 检查步骤
|
||||
1. 检查问题表述的清晰度
|
||||
2. 深度分析思想根源
|
||||
..."
|
||||
></textarea>
|
||||
<p class="text-xs text-slate-500 mt-2">
|
||||
💡 提示:这是 AI 调用时的核心指令。完整的 Prompt 应包含角色、目标、步骤和输出格式。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -401,17 +267,14 @@ import { storeToRefs } from 'pinia'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useDatabaseStore } from '../stores/database.js'
|
||||
import DeepSeekAPI from '../api/deepseek.js'
|
||||
import { getParadigmList, getParadigmDimensions } from '../config/paradigms.js'
|
||||
import { getDimensionSetList, getDimensionSetById } from '../config/dimensionSets.js'
|
||||
import { getLogicParadigmList } from '../config/logicParadigms.js'
|
||||
import { REFERENCE_TYPES } from '../config/references.js'
|
||||
import { getParadigmList } from '../config/paradigms.js'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { analysisText, isAnalyzing } = storeToRefs(appStore)
|
||||
|
||||
// 数据库 Store
|
||||
const dbStore = useDatabaseStore()
|
||||
const { references: dbReferences, isInitialized: dbInitialized } = storeToRefs(dbStore)
|
||||
const { isInitialized: dbInitialized } = storeToRefs(dbStore)
|
||||
|
||||
// 选中的范式
|
||||
const selectedParadigm = ref(null)
|
||||
@@ -434,54 +297,10 @@ const editForm = reactive({
|
||||
description: '',
|
||||
tagsInput: '',
|
||||
tagClass: 'bg-blue-900/30 text-blue-300',
|
||||
constraintsInput: '',
|
||||
// 新架构:维度配置
|
||||
dimensionSetId: null,
|
||||
dimensions: [],
|
||||
// 素材库配置
|
||||
autoMatchRefs: true,
|
||||
selectedRefs: []
|
||||
// ⭐ 核心字段:完整 Prompt
|
||||
specializedPrompt: ''
|
||||
})
|
||||
|
||||
// 维度编辑器状态
|
||||
const showDimensionEditor = ref(false)
|
||||
|
||||
// 素材编辑器状态
|
||||
const showReferenceEditor = ref(false)
|
||||
const selectedRefType = ref('all')
|
||||
|
||||
// 使用数据库中的素材数据
|
||||
const allReferences = computed(() => {
|
||||
return dbReferences.value.map(ref => ({
|
||||
id: ref.id,
|
||||
type: ref.type,
|
||||
title: ref.title,
|
||||
source: ref.source,
|
||||
tags: ref.tags,
|
||||
excerptCount: ref.excerpts?.length || 0
|
||||
}))
|
||||
})
|
||||
|
||||
const filteredReferences = computed(() => {
|
||||
if (selectedRefType.value === 'all') {
|
||||
return allReferences.value
|
||||
}
|
||||
return allReferences.value.filter(ref => ref.type === selectedRefType.value)
|
||||
})
|
||||
|
||||
// 素材类型选项
|
||||
const referenceTypes = [
|
||||
{ id: 'all', name: '全部', icon: '📚' },
|
||||
{ id: REFERENCE_TYPES.POLICY, name: '政策文件', icon: '📄' },
|
||||
{ id: REFERENCE_TYPES.SPEECH, name: '领导讲话', icon: '🎙️' },
|
||||
{ id: REFERENCE_TYPES.CASE, name: '典型案例', icon: '📌' },
|
||||
{ id: REFERENCE_TYPES.QUOTE, name: '金句警句', icon: '✨' },
|
||||
{ id: REFERENCE_TYPES.REGULATION, name: '党规党纪', icon: '📖' }
|
||||
]
|
||||
|
||||
// 维度集选项
|
||||
const dimensionSetOptions = getDimensionSetList()
|
||||
|
||||
// 图标选项
|
||||
const iconOptions = ['📝', '💻', '📊', '🚀', '📚', '🏛️', '🔥', '🏢', '💡', '🎯', '📋', '✨']
|
||||
|
||||
@@ -495,6 +314,7 @@ const colorOptions = [
|
||||
{ label: '青色', class: 'bg-cyan-900/30 text-cyan-300' }
|
||||
]
|
||||
|
||||
|
||||
// 初始化范式列表
|
||||
const initParadigms = () => {
|
||||
// 先加载默认范式
|
||||
@@ -538,28 +358,13 @@ const openEditModal = (paradigm) => {
|
||||
editForm.description = paradigm.description || ''
|
||||
editForm.tagsInput = (paradigm.tags || []).join(', ')
|
||||
editForm.tagClass = paradigm.tagClass || 'bg-blue-900/30 text-blue-300'
|
||||
editForm.constraintsInput = (paradigm.systemConstraints || []).join('\n')
|
||||
// ⭐ 核心字段:加载完整 Prompt
|
||||
editForm.specializedPrompt = paradigm.specializedPrompt || ''
|
||||
|
||||
// 加载维度配置
|
||||
editForm.dimensionSetId = paradigm.dimensionSetId || null
|
||||
if (paradigm.dimensionSetId) {
|
||||
const dimSet = getDimensionSetById(paradigm.dimensionSetId)
|
||||
editForm.dimensions = dimSet ? JSON.parse(JSON.stringify(dimSet.dimensions)) : []
|
||||
} else if (paradigm.customDimensions) {
|
||||
editForm.dimensions = JSON.parse(JSON.stringify(paradigm.customDimensions))
|
||||
} else {
|
||||
editForm.dimensions = []
|
||||
}
|
||||
|
||||
// 加载素材配置
|
||||
editForm.autoMatchRefs = paradigm.autoMatchRefs !== false // 默认为true
|
||||
editForm.selectedRefs = paradigm.selectedRefs ? [...paradigm.selectedRefs] : []
|
||||
|
||||
showDimensionEditor.value = false
|
||||
showReferenceEditor.value = false
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
|
||||
// 关闭弹窗
|
||||
const closeEditModal = () => {
|
||||
showEditModal.value = false
|
||||
@@ -573,84 +378,15 @@ const resetEditForm = () => {
|
||||
editForm.description = ''
|
||||
editForm.tagsInput = ''
|
||||
editForm.tagClass = 'bg-blue-900/30 text-blue-300'
|
||||
editForm.constraintsInput = ''
|
||||
editForm.dimensionSetId = null
|
||||
editForm.dimensions = []
|
||||
// 素材配置重置
|
||||
editForm.autoMatchRefs = true
|
||||
editForm.selectedRefs = []
|
||||
showDimensionEditor.value = false
|
||||
showReferenceEditor.value = false
|
||||
// ⭐ 核心字段重置
|
||||
editForm.specializedPrompt = ''
|
||||
}
|
||||
|
||||
// 切换维度编辑器显示
|
||||
const toggleDimensionEditor = () => {
|
||||
showDimensionEditor.value = !showDimensionEditor.value
|
||||
}
|
||||
|
||||
// 维度集变更时更新维度列表
|
||||
const onDimensionSetChange = () => {
|
||||
if (editForm.dimensionSetId) {
|
||||
const dimSet = getDimensionSetById(editForm.dimensionSetId)
|
||||
if (dimSet) {
|
||||
// 深拷贝维度列表,允许用户编辑
|
||||
editForm.dimensions = JSON.parse(JSON.stringify(dimSet.dimensions))
|
||||
}
|
||||
} else {
|
||||
editForm.dimensions = []
|
||||
}
|
||||
}
|
||||
|
||||
// 添加自定义维度
|
||||
const addDimension = () => {
|
||||
editForm.dimensions.push({
|
||||
id: `custom-dim-${Date.now()}`,
|
||||
name: '',
|
||||
focus: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除维度
|
||||
const removeDimension = (index) => {
|
||||
editForm.dimensions.splice(index, 1)
|
||||
}
|
||||
|
||||
// 切换素材编辑器显示
|
||||
const toggleReferenceEditor = () => {
|
||||
showReferenceEditor.value = !showReferenceEditor.value
|
||||
}
|
||||
|
||||
// 按类型筛选素材(通过修改selectedRefType触发computed重新计算)
|
||||
const filterReferencesByType = (typeId) => {
|
||||
selectedRefType.value = typeId
|
||||
}
|
||||
|
||||
// 切换素材选中状态
|
||||
const toggleReference = (refId) => {
|
||||
const index = editForm.selectedRefs.indexOf(refId)
|
||||
if (index === -1) {
|
||||
editForm.selectedRefs.push(refId)
|
||||
} else {
|
||||
editForm.selectedRefs.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存范式
|
||||
const saveParadigm = () => {
|
||||
const tags = editForm.tagsInput.split(',').map(t => t.trim()).filter(t => t)
|
||||
const constraints = editForm.constraintsInput.split('\n').map(c => c.trim()).filter(c => c)
|
||||
|
||||
// 处理维度配置
|
||||
const dimensionConfig = {
|
||||
dimensionSetId: editForm.dimensionSetId,
|
||||
customDimensions: editForm.dimensions.length > 0 ? editForm.dimensions : null
|
||||
}
|
||||
|
||||
// 处理素材配置
|
||||
const referenceConfig = {
|
||||
autoMatchRefs: editForm.autoMatchRefs,
|
||||
selectedRefs: editForm.selectedRefs
|
||||
}
|
||||
|
||||
if (isAddMode.value) {
|
||||
// 新增自定义范式
|
||||
@@ -658,20 +394,12 @@ const saveParadigm = () => {
|
||||
id: `custom-${Date.now()}`,
|
||||
icon: editForm.icon,
|
||||
name: editForm.name,
|
||||
description: editForm.description,
|
||||
description: editForm.description || editForm.specializedPrompt.substring(0, 100) + '...',
|
||||
tags,
|
||||
tagClass: editForm.tagClass,
|
||||
systemConstraints: constraints,
|
||||
isCustom: true,
|
||||
// 新架构:维度配置
|
||||
...dimensionConfig,
|
||||
// 素材配置
|
||||
...referenceConfig,
|
||||
logicParadigms: {
|
||||
problemSection: 'progressive-problem',
|
||||
analysisSection: 'deep-attribution',
|
||||
remediationSection: 'remediation'
|
||||
}
|
||||
// ⭐ 核心字段:完整 Prompt
|
||||
specializedPrompt: editForm.specializedPrompt
|
||||
}
|
||||
|
||||
paradigms.value.push(newParadigm)
|
||||
@@ -692,11 +420,8 @@ const saveParadigm = () => {
|
||||
description: editForm.description,
|
||||
tags,
|
||||
tagClass: editForm.tagClass,
|
||||
systemConstraints: constraints,
|
||||
// 新架构:维度配置
|
||||
...dimensionConfig,
|
||||
// 素材配置
|
||||
...referenceConfig
|
||||
// ⭐ 核心字段:完整 Prompt
|
||||
specializedPrompt: editForm.specializedPrompt
|
||||
}
|
||||
|
||||
paradigms.value[index] = updatedParadigm
|
||||
@@ -721,11 +446,7 @@ const saveParadigm = () => {
|
||||
description: editForm.description,
|
||||
tags,
|
||||
tagClass: editForm.tagClass,
|
||||
systemConstraints: constraints,
|
||||
// 新架构:维度配置
|
||||
...dimensionConfig,
|
||||
// 素材配置
|
||||
...referenceConfig
|
||||
specializedPrompt: editForm.specializedPrompt
|
||||
}
|
||||
localStorage.setItem('paradigmCustomizations', JSON.stringify(customizations))
|
||||
}
|
||||
@@ -735,6 +456,7 @@ const saveParadigm = () => {
|
||||
closeEditModal()
|
||||
}
|
||||
|
||||
|
||||
// 删除自定义范式
|
||||
const deleteParadigm = (paradigm) => {
|
||||
if (!paradigm.isCustom) return
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col bg-slate-900">
|
||||
<div class="h-screen w-full flex flex-col bg-slate-900">
|
||||
<!-- 头部 -->
|
||||
<header class="p-4 border-b border-slate-700 bg-slate-800 flex items-center justify-between shrink-0">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -43,16 +43,39 @@
|
||||
</div>
|
||||
|
||||
<!-- 范式规则展开区 -->
|
||||
<div v-if="showParadigmRules && hasParadigmRules" class="px-4 py-3 bg-indigo-950/50 border-b border-indigo-800/30 max-h-48 overflow-y-auto shrink-0">
|
||||
<div v-if="showParadigmRules && hasParadigmRules" class="px-4 py-3 bg-indigo-950/50 border-b border-indigo-800/30 max-h-56 overflow-y-auto shrink-0">
|
||||
<!-- 操作栏 -->
|
||||
<div class="flex items-center justify-between mb-2 pb-2 border-b border-indigo-800/30">
|
||||
<span class="text-xs text-indigo-300">已启用 {{ enabledRulesCount }}/{{ paradigmRulesCount }} 条规则</span>
|
||||
<button
|
||||
@click="toggleAllRules"
|
||||
class="text-xs px-2 py-0.5 rounded bg-indigo-700/50 text-indigo-200 hover:bg-indigo-600/50 transition"
|
||||
>
|
||||
{{ enabledRulesCount === paradigmRulesCount ? '取消全选' : '全选' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 规则列表 -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div v-for="(rule, idx) in getFilteredRules(null)" :key="idx"
|
||||
:class="['text-xs px-2 py-1 rounded flex items-start gap-1',
|
||||
rule.scope === checkMode ? 'bg-indigo-800/50 text-indigo-100' : 'bg-slate-800/50 text-slate-400'
|
||||
<div v-for="rule in allRulesWithIdx" :key="rule.idx"
|
||||
@click="toggleRule(rule.idx)"
|
||||
:class="['text-xs px-2 py-1.5 rounded flex items-start gap-2 cursor-pointer transition border',
|
||||
enabledRuleIdxs.has(rule.idx)
|
||||
? (rule.scope === checkMode ? 'bg-indigo-800/50 text-indigo-100 border-indigo-600/50' : 'bg-slate-800/50 text-slate-300 border-slate-600/50')
|
||||
: 'bg-slate-900/50 text-slate-500 border-slate-700/30 opacity-60'
|
||||
]">
|
||||
<span :class="['shrink-0 px-1 rounded text-[10px]',
|
||||
<!-- 勾选框 -->
|
||||
<div :class="['w-4 h-4 rounded border flex items-center justify-center shrink-0 mt-0.5 transition',
|
||||
enabledRuleIdxs.has(rule.idx) ? 'bg-indigo-600 border-indigo-500' : 'bg-slate-800 border-slate-600'
|
||||
]">
|
||||
<span v-if="enabledRuleIdxs.has(rule.idx)" class="text-white text-[10px]">✓</span>
|
||||
</div>
|
||||
<!-- 规则标签和文本 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<span :class="['shrink-0 px-1 rounded text-[10px] mr-1',
|
||||
rule.scope === 'paragraph' ? 'bg-amber-600/50 text-amber-200' : 'bg-blue-600/50 text-blue-200'
|
||||
]">{{ rule.scope === 'paragraph' ? '段' : '文' }}</span>
|
||||
<span>{{ rule.text }}</span>
|
||||
<span class="break-words">{{ rule.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +88,26 @@
|
||||
<h2 class="text-sm font-medium text-amber-400 flex items-center gap-2">
|
||||
📋 要求原文
|
||||
</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-slate-500">{{ leftParagraphs.length }} 段</span>
|
||||
<!-- 保存按钮(仅当内容来自文稿库时显示) -->
|
||||
<button
|
||||
v-if="leftSourceType === 'document' && leftSourceDocId"
|
||||
@click="saveLeftContent"
|
||||
:disabled="isLeftSaving || !hasLeftContentChanged"
|
||||
class="text-xs px-2 py-1 rounded flex items-center gap-1 transition"
|
||||
:class="hasLeftContentChanged
|
||||
? 'bg-green-600 text-white hover:bg-green-500'
|
||||
: 'bg-slate-700 text-slate-400 cursor-not-allowed'"
|
||||
>
|
||||
<span v-if="isLeftSaving" class="animate-spin">↻</span>
|
||||
<span v-else>💾</span>
|
||||
{{ isLeftSaving ? '保存中...' : (hasLeftContentChanged ? '保存版本' : '已保存') }}
|
||||
</button>
|
||||
<span v-if="leftSourceType === 'document' && leftSourceDocTitle" class="text-xs text-amber-400">
|
||||
📄 {{ leftSourceDocTitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 来源选择器 -->
|
||||
<div class="px-3 py-2 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
|
||||
@@ -77,11 +119,10 @@
|
||||
leftSourceType === 'paste' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>粘贴</button>
|
||||
<button
|
||||
v-if="activeParadigm?.defaultReference"
|
||||
@click="loadParadigmReference"
|
||||
@click="showParadigmSelector = true"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
leftSourceType === 'paradigm' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>范式范文</button>
|
||||
>范式库</button>
|
||||
<button
|
||||
@click="showMaterialSelector = true"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
@@ -373,6 +414,13 @@
|
||||
@select="handleMaterialSelect"
|
||||
/>
|
||||
|
||||
<!-- 范式选择弹窗 -->
|
||||
<ParadigmSelectorModal
|
||||
:visible="showParadigmSelector"
|
||||
@close="showParadigmSelector = false"
|
||||
@select="handleParadigmSelect"
|
||||
/>
|
||||
|
||||
<!-- 文稿选择弹窗(左侧) -->
|
||||
<DocumentSelectorModal
|
||||
:visible="showLeftDocSelector"
|
||||
@@ -712,6 +760,7 @@ import { getLogicParadigmById, buildLogicPrompt } from '../config/logicParadigms
|
||||
import { computeDiff, applySelectedChanges as applyDiffChanges, getDiffStats, splitIntoSentencesWithPosition } from '../utils/textDiff'
|
||||
import MaterialSelectorModal from './MaterialSelectorModal.vue'
|
||||
import DocumentSelectorModal from './DocumentSelectorModal.vue'
|
||||
import ParadigmSelectorModal from './ParadigmSelectorModal.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const dbStore = useDatabaseStore()
|
||||
@@ -723,8 +772,13 @@ const rightContent = ref('')
|
||||
|
||||
// 左侧来源相关
|
||||
const leftSourceType = ref('paste') // 'paste' | 'paradigm' | 'material' | 'document'
|
||||
const showParadigmSelector = ref(false)
|
||||
const showMaterialSelector = ref(false)
|
||||
const showLeftDocSelector = ref(false)
|
||||
const leftSourceDocId = ref(null) // 来源文稿ID
|
||||
const leftSourceDocTitle = ref('') // 来源文稿标题
|
||||
const leftOriginalContent = ref('') // 来源文稿原始内容(用于差异比对)
|
||||
const isLeftSaving = ref(false) // 左侧保存中状态
|
||||
|
||||
// 右侧来源相关
|
||||
const rightSourceType = ref('paste') // 'paste' | 'document'
|
||||
@@ -773,6 +827,7 @@ const rewrittenSentences = ref([]) // 重写后句子列表
|
||||
// 范式相关
|
||||
const showParadigmRules = ref(false)
|
||||
const checkMode = ref('paragraph') // 'paragraph' | 'document'
|
||||
const enabledRuleIdxs = ref(new Set()) // 已启用的规则索引集合
|
||||
|
||||
const hasParadigmRules = computed(() => {
|
||||
const p = activeParadigm.value
|
||||
@@ -783,46 +838,88 @@ const activeParadigmName = computed(() => {
|
||||
return activeParadigm.value?.name || ''
|
||||
})
|
||||
|
||||
// 提取范式规则列表(根据 scope 过滤)
|
||||
// 提取范式规则列表(根据 scope 过滤,添加索引用于勾选)
|
||||
const getFilteredRules = (scope) => {
|
||||
const paradigm = activeParadigm.value
|
||||
if (!paradigm) return []
|
||||
|
||||
const rules = []
|
||||
let idx = 0
|
||||
|
||||
if (paradigm.expertGuidelines) {
|
||||
paradigm.expertGuidelines
|
||||
.filter(g => !scope || g.scope === scope || !g.scope) // 无 scope 的规则默认都包含
|
||||
.forEach(g => {
|
||||
rules.push({ text: `【${g.title}】${g.description}`, scope: g.scope || 'both' })
|
||||
rules.push({
|
||||
idx: idx++,
|
||||
text: `【${g.title}】${g.description}`,
|
||||
scope: g.scope || 'both'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// systemConstraints 默认为全文级
|
||||
if (paradigm.systemConstraints && (scope === 'document' || !scope)) {
|
||||
paradigm.systemConstraints.forEach(c => {
|
||||
rules.push({ text: c, scope: 'document' })
|
||||
rules.push({ idx: idx++, text: c, scope: 'document' })
|
||||
})
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
// 获取所有规则(带索引)
|
||||
const allRulesWithIdx = computed(() => getFilteredRules(null))
|
||||
|
||||
// 初始化规则勾选状态(默认全部勾选)
|
||||
const initializeRuleSelection = () => {
|
||||
const rules = allRulesWithIdx.value
|
||||
enabledRuleIdxs.value = new Set(rules.map(r => r.idx))
|
||||
}
|
||||
|
||||
// 切换单个规则的勾选状态
|
||||
const toggleRule = (idx) => {
|
||||
if (enabledRuleIdxs.value.has(idx)) {
|
||||
enabledRuleIdxs.value.delete(idx)
|
||||
} else {
|
||||
enabledRuleIdxs.value.add(idx)
|
||||
}
|
||||
enabledRuleIdxs.value = new Set(enabledRuleIdxs.value) // 触发响应式更新
|
||||
}
|
||||
|
||||
// 全选/取消全选规则
|
||||
const toggleAllRules = () => {
|
||||
const allRules = allRulesWithIdx.value
|
||||
if (enabledRuleIdxs.value.size === allRules.length) {
|
||||
enabledRuleIdxs.value = new Set()
|
||||
} else {
|
||||
enabledRuleIdxs.value = new Set(allRules.map(r => r.idx))
|
||||
}
|
||||
}
|
||||
|
||||
// 监听范式变化,重新初始化勾选状态
|
||||
watch(() => activeParadigm.value, () => {
|
||||
initializeRuleSelection()
|
||||
}, { immediate: true })
|
||||
|
||||
// 当前模式下的规则列表
|
||||
const paradigmRulesList = computed(() => getFilteredRules(null).map(r => r.text))
|
||||
const paragraphRules = computed(() => getFilteredRules('paragraph'))
|
||||
const documentRules = computed(() => getFilteredRules('document'))
|
||||
|
||||
const paradigmRulesCount = computed(() => paradigmRulesList.value.length)
|
||||
const currentScopeRulesCount = computed(() =>
|
||||
checkMode.value === 'paragraph' ? paragraphRules.value.length : documentRules.value.length
|
||||
)
|
||||
const enabledRulesCount = computed(() => enabledRuleIdxs.value.size)
|
||||
const currentScopeRulesCount = computed(() => {
|
||||
const rules = checkMode.value === 'paragraph' ? paragraphRules.value : documentRules.value
|
||||
return rules.filter(r => enabledRuleIdxs.value.has(r.idx)).length
|
||||
})
|
||||
|
||||
// 获取范式规则文本(根据当前检查模式过滤)
|
||||
// 获取范式规则文本(根据当前检查模式过滤,只包含勾选的规则)
|
||||
const getParadigmRulesText = () => {
|
||||
const rules = checkMode.value === 'paragraph' ? paragraphRules.value : documentRules.value
|
||||
if (rules.length === 0) return ''
|
||||
return rules.map(r => r.text).join('\n')
|
||||
const enabledRules = rules.filter(r => enabledRuleIdxs.value.has(r.idx))
|
||||
if (enabledRules.length === 0) return ''
|
||||
return enabledRules.map(r => r.text).join('\n')
|
||||
}
|
||||
|
||||
// 解析段落
|
||||
@@ -851,6 +948,12 @@ const hasContentChanged = computed(() => {
|
||||
return rightContent.value !== rightOriginalContent.value
|
||||
})
|
||||
|
||||
// 左侧内容是否已修改(与原始内容对比)
|
||||
const hasLeftContentChanged = computed(() => {
|
||||
if (leftSourceType.value !== 'document' || !leftSourceDocId.value) return false
|
||||
return leftContent.value !== leftOriginalContent.value
|
||||
})
|
||||
|
||||
// 选择段落(多选切换)
|
||||
const selectLeftParagraph = (idx) => {
|
||||
const i = selectedLeftIdxs.value.indexOf(idx)
|
||||
@@ -897,12 +1000,21 @@ const goBack = () => {
|
||||
appStore.switchPage('writer')
|
||||
}
|
||||
|
||||
// 加载范式默认参考范文
|
||||
const loadParadigmReference = () => {
|
||||
if (activeParadigm.value?.defaultReference) {
|
||||
leftContent.value = activeParadigm.value.defaultReference.content
|
||||
// 加载范式范文或规则
|
||||
const handleParadigmSelect = (p) => {
|
||||
appStore.activeParadigm = p
|
||||
leftSourceType.value = 'paradigm'
|
||||
|
||||
// 填充内容:优先使用系统约束作为要求原文,如果没有则用范文内容
|
||||
if (p.systemConstraints && p.systemConstraints.length > 0) {
|
||||
leftContent.value = p.systemConstraints.join('\n\n')
|
||||
} else if (p.defaultReference?.content) {
|
||||
leftContent.value = p.defaultReference.content
|
||||
} else {
|
||||
leftContent.value = p.description || ''
|
||||
}
|
||||
|
||||
showParadigmSelector.value = false
|
||||
}
|
||||
|
||||
// 处理素材选择
|
||||
@@ -918,6 +1030,9 @@ const handleMaterialSelect = (material) => {
|
||||
const handleLeftDocSelect = (doc) => {
|
||||
leftContent.value = doc.content || ''
|
||||
leftSourceType.value = 'document'
|
||||
leftSourceDocId.value = doc.id // 保存文稿ID
|
||||
leftSourceDocTitle.value = doc.title // 保存文稿标题
|
||||
leftOriginalContent.value = doc.content || '' // 保存原始内容用于差异比对
|
||||
showLeftDocSelector.value = false
|
||||
}
|
||||
|
||||
@@ -974,6 +1089,49 @@ const saveRightContent = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存左侧内容到文稿
|
||||
const saveLeftContent = async () => {
|
||||
if (leftSourceType.value !== 'document' || !leftSourceDocId.value) {
|
||||
alert('当前内容不是来自文稿库,无需保存')
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasLeftContentChanged.value) {
|
||||
alert('内容未修改,无需保存')
|
||||
return
|
||||
}
|
||||
|
||||
isLeftSaving.value = true
|
||||
|
||||
try {
|
||||
// 计算差异用于生成版本说明
|
||||
const changes = computeDiff(leftOriginalContent.value, leftContent.value)
|
||||
const stats = getDiffStats(changes)
|
||||
|
||||
// 生成简短的变更说明
|
||||
let changeNote = `修改了 ${stats.modified} 处`
|
||||
if (stats.added > 0) changeNote += `,新增了 ${stats.added} 处`
|
||||
if (stats.removed > 0) changeNote += `,删除了 ${stats.removed} 处`
|
||||
|
||||
// 保存新版本
|
||||
const versionNumber = saveDocumentVersion(leftSourceDocId.value, leftContent.value, changeNote)
|
||||
|
||||
// 同时更新文稿主内容
|
||||
updateDocument(leftSourceDocId.value, { content: leftContent.value })
|
||||
|
||||
// 更新原始内容为当前内容(标记为已保存)
|
||||
leftOriginalContent.value = leftContent.value
|
||||
|
||||
console.log(`左侧文稿保存成功,版本号: ${versionNumber},${changeNote}`)
|
||||
alert(`保存成功!新版本: v${versionNumber}\n${changeNote}`)
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
alert('保存失败,请重试')
|
||||
} finally {
|
||||
isLeftSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换段落类型下拉菜单
|
||||
const toggleTypeDropdown = (idx) => {
|
||||
showTypeDropdown.value = showTypeDropdown.value === idx ? null : idx
|
||||
|
||||
439
src/components/DiffAnnotationPanel.vue
Normal file
439
src/components/DiffAnnotationPanel.vue
Normal file
@@ -0,0 +1,439 @@
|
||||
<template>
|
||||
<div class="h-screen w-full flex flex-col bg-slate-950">
|
||||
<!-- 顶部工具栏 -->
|
||||
<header class="h-14 border-b border-slate-800 flex items-center justify-between px-6 bg-slate-950 shrink-0">
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="text-lg font-bold text-white flex items-center gap-2">
|
||||
📊 文稿差异标注
|
||||
</h1>
|
||||
<button
|
||||
@click="goBack"
|
||||
class="text-xs px-3 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
← 返回写作
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- 图例说明 -->
|
||||
<div class="text-xs text-slate-500 flex items-center gap-3">
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-amber-500/50"></span> 修改</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-green-500/50"></span> 新增</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-red-500/50"></span> 删除</span>
|
||||
</div>
|
||||
<!-- 差异统计 -->
|
||||
<div v-if="diffStats.total > 0" class="text-xs text-slate-400 flex items-center gap-3 border-l border-slate-700 pl-4">
|
||||
<span class="text-amber-400">{{ diffStats.modified }} 处修改</span>
|
||||
<span class="text-green-400">{{ diffStats.added }} 处新增</span>
|
||||
<span class="text-red-400">{{ diffStats.removed }} 处删除</span>
|
||||
</div>
|
||||
<!-- 导出按钮 -->
|
||||
<button
|
||||
@click="exportToWord"
|
||||
:disabled="!leftContent || !rightContent"
|
||||
class="text-sm px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition flex items-center gap-2"
|
||||
>
|
||||
📥 导出 Word
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区:左右对比 -->
|
||||
<div class="flex-1 flex w-full overflow-hidden">
|
||||
<!-- 左侧:原文 -->
|
||||
<div class="flex-1 flex flex-col border-r border-slate-700 min-w-0">
|
||||
<div class="p-3 bg-slate-800 border-b border-slate-700 flex items-center justify-between shrink-0">
|
||||
<h2 class="text-sm font-medium text-amber-400 flex items-center gap-2">
|
||||
📄 原文
|
||||
</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-slate-500">来源:</span>
|
||||
<div class="flex bg-slate-900 rounded p-0.5 border border-slate-700">
|
||||
<button
|
||||
@click="leftSourceType = 'paste'"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
leftSourceType === 'paste' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>粘贴</button>
|
||||
<button
|
||||
@click="showLeftDocSelector = true"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
leftSourceType === 'document' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>文稿库</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异高亮显示区 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 bg-slate-900/50 min-h-0" :style="{ minHeight: `calc(100% - ${inputHeight}px - 60px)` }">
|
||||
<div v-if="diffSegments.length > 0" class="text-sm leading-relaxed whitespace-pre-wrap">
|
||||
<template v-for="(segment, idx) in diffSegments" :key="'left-' + idx">
|
||||
<span
|
||||
v-if="segment.type === 'unchanged'"
|
||||
class="text-slate-300"
|
||||
>{{ segment.original }}</span>
|
||||
<span
|
||||
v-else-if="segment.type === 'modified'"
|
||||
class="bg-amber-500/30 text-amber-200 px-0.5 rounded"
|
||||
>{{ segment.original }}</span>
|
||||
<span
|
||||
v-else-if="segment.type === 'removed'"
|
||||
class="bg-red-500/30 text-red-200 px-0.5 rounded line-through"
|
||||
>{{ segment.original }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="h-full flex flex-col items-center justify-center text-slate-500">
|
||||
<span class="text-5xl block mb-3 opacity-30">📝</span>
|
||||
<p class="text-sm">在下方输入原文内容</p>
|
||||
<p class="text-xs text-slate-600 mt-1">或从文稿库选择</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可拖动分割线 -->
|
||||
<div
|
||||
class="h-2 bg-slate-700 cursor-ns-resize hover:bg-slate-600 flex items-center justify-center transition shrink-0"
|
||||
@mousedown="startResize"
|
||||
>
|
||||
<div class="w-12 h-1 bg-slate-500 rounded"></div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区 -->
|
||||
<div class="p-3 border-t border-slate-700 bg-slate-800 shrink-0" :style="{ height: inputHeight + 'px' }">
|
||||
<textarea
|
||||
v-model="leftContent"
|
||||
class="w-full h-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm text-slate-200 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none"
|
||||
placeholder="粘贴或输入原文内容..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:修改文 -->
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<div class="p-3 bg-slate-800 border-b border-slate-700 flex items-center justify-between shrink-0">
|
||||
<h2 class="text-sm font-medium text-blue-400 flex items-center gap-2">
|
||||
✏️ 修改文
|
||||
</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-slate-500">来源:</span>
|
||||
<div class="flex bg-slate-900 rounded p-0.5 border border-slate-700">
|
||||
<button
|
||||
@click="rightSourceType = 'paste'"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
rightSourceType === 'paste' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>粘贴</button>
|
||||
<button
|
||||
@click="showRightDocSelector = true"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
rightSourceType === 'document' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>文稿库</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异高亮显示区 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 bg-slate-900/50 min-h-0" :style="{ minHeight: `calc(100% - ${inputHeight}px - 60px)` }">
|
||||
<div v-if="diffSegments.length > 0" class="text-sm leading-relaxed whitespace-pre-wrap">
|
||||
<template v-for="(segment, idx) in diffSegments" :key="'right-' + idx">
|
||||
<span
|
||||
v-if="segment.type === 'unchanged'"
|
||||
class="text-slate-300"
|
||||
>{{ segment.rewritten }}</span>
|
||||
<span
|
||||
v-else-if="segment.type === 'modified'"
|
||||
class="bg-amber-500/30 text-amber-200 px-0.5 rounded"
|
||||
>{{ segment.rewritten }}</span>
|
||||
<span
|
||||
v-else-if="segment.type === 'added'"
|
||||
class="bg-green-500/30 text-green-200 px-0.5 rounded"
|
||||
>{{ segment.rewritten }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="h-full flex flex-col items-center justify-center text-slate-500">
|
||||
<span class="text-5xl block mb-3 opacity-30">✏️</span>
|
||||
<p class="text-sm">在下方输入修改后的内容</p>
|
||||
<p class="text-xs text-slate-600 mt-1">或从文稿库选择</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可拖动分割线 -->
|
||||
<div
|
||||
class="h-2 bg-slate-700 cursor-ns-resize hover:bg-slate-600 flex items-center justify-center transition shrink-0"
|
||||
@mousedown="startResize"
|
||||
>
|
||||
<div class="w-12 h-1 bg-slate-500 rounded"></div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区 -->
|
||||
<div class="p-3 border-t border-slate-700 bg-slate-800 shrink-0" :style="{ height: inputHeight + 'px' }">
|
||||
<textarea
|
||||
v-model="rightContent"
|
||||
class="w-full h-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm text-slate-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none resize-none"
|
||||
placeholder="粘贴或输入修改后的内容..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文稿选择弹窗 - 左侧 -->
|
||||
<DocumentSelectorModal
|
||||
:visible="showLeftDocSelector"
|
||||
@close="showLeftDocSelector = false"
|
||||
@select="handleLeftDocSelect"
|
||||
/>
|
||||
|
||||
<!-- 文稿选择弹窗 - 右侧 -->
|
||||
<DocumentSelectorModal
|
||||
:visible="showRightDocSelector"
|
||||
@close="showRightDocSelector = false"
|
||||
@select="handleRightDocSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { computePreciseDiff, getPreciseDiffStats } from '../utils/preciseDiff.js'
|
||||
import { Document, Packer, Paragraph, TextRun } from 'docx'
|
||||
import { saveAs } from 'file-saver'
|
||||
import DocumentSelectorModal from './DocumentSelectorModal.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 内容
|
||||
const leftContent = ref('')
|
||||
const rightContent = ref('')
|
||||
|
||||
// 来源类型
|
||||
const leftSourceType = ref('paste')
|
||||
const rightSourceType = ref('paste')
|
||||
|
||||
// 文稿选择器
|
||||
const showLeftDocSelector = ref(false)
|
||||
const showRightDocSelector = ref(false)
|
||||
|
||||
// 差异计算
|
||||
const diffSegments = ref([])
|
||||
|
||||
// 可拖动分割线状态
|
||||
const inputHeight = ref(160)
|
||||
const isResizing = ref(false)
|
||||
const startY = ref(0)
|
||||
const startHeight = ref(0)
|
||||
|
||||
// 监听内容变化,实时计算差异(使用精确算法)
|
||||
watch([leftContent, rightContent], () => {
|
||||
if (leftContent.value && rightContent.value) {
|
||||
diffSegments.value = computePreciseDiff(leftContent.value, rightContent.value)
|
||||
} else {
|
||||
diffSegments.value = []
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 差异统计
|
||||
const diffStats = computed(() => {
|
||||
return getPreciseDiffStats(diffSegments.value)
|
||||
})
|
||||
|
||||
// 处理左侧文稿选择
|
||||
const handleLeftDocSelect = (doc) => {
|
||||
leftContent.value = doc.content || ''
|
||||
leftSourceType.value = 'document'
|
||||
showLeftDocSelector.value = false
|
||||
}
|
||||
|
||||
// 处理右侧文稿选择
|
||||
const handleRightDocSelect = (doc) => {
|
||||
rightContent.value = doc.content || ''
|
||||
rightSourceType.value = 'document'
|
||||
showRightDocSelector.value = false
|
||||
}
|
||||
|
||||
// 分割线拖动处理
|
||||
const startResize = (e) => {
|
||||
isResizing.value = true
|
||||
startY.value = e.clientY
|
||||
startHeight.value = inputHeight.value
|
||||
document.addEventListener('mousemove', doResize)
|
||||
document.addEventListener('mouseup', stopResize)
|
||||
document.body.style.cursor = 'ns-resize'
|
||||
document.body.style.userSelect = 'none'
|
||||
}
|
||||
|
||||
const doResize = (e) => {
|
||||
if (!isResizing.value) return
|
||||
const delta = startY.value - e.clientY
|
||||
const newHeight = Math.max(80, Math.min(400, startHeight.value + delta))
|
||||
inputHeight.value = newHeight
|
||||
}
|
||||
|
||||
const stopResize = () => {
|
||||
isResizing.value = false
|
||||
document.removeEventListener('mousemove', doResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
|
||||
// 清理事件监听
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', doResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
})
|
||||
|
||||
// 返回写作页面
|
||||
const goBack = () => {
|
||||
appStore.setCurrentPage('writer')
|
||||
}
|
||||
|
||||
// 导出 Word 文档
|
||||
const exportToWord = async () => {
|
||||
if (!leftContent.value || !rightContent.value) {
|
||||
alert('请先填写原文和修改文内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建文档段落
|
||||
const children = []
|
||||
|
||||
// 标题
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: '文稿差异对比报告',
|
||||
bold: true,
|
||||
size: 32
|
||||
})
|
||||
],
|
||||
spacing: { after: 400 }
|
||||
})
|
||||
)
|
||||
|
||||
// 统计信息
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: `差异统计:修改 ${diffStats.value.modified} 处,新增 ${diffStats.value.added} 处,删除 ${diffStats.value.removed} 处`,
|
||||
size: 20,
|
||||
color: '666666'
|
||||
})
|
||||
],
|
||||
spacing: { after: 400 }
|
||||
})
|
||||
)
|
||||
|
||||
// 分隔线
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: '─'.repeat(50) })],
|
||||
spacing: { after: 200 }
|
||||
})
|
||||
)
|
||||
|
||||
// 原文标题
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: '【原文】',
|
||||
bold: true,
|
||||
size: 24
|
||||
})
|
||||
],
|
||||
spacing: { after: 200 }
|
||||
})
|
||||
)
|
||||
|
||||
// 原文内容(带高亮)
|
||||
const leftRuns = []
|
||||
for (const segment of diffSegments.value) {
|
||||
if (segment.type === 'unchanged' && segment.original) {
|
||||
leftRuns.push(new TextRun({ text: segment.original, size: 22 }))
|
||||
} else if (segment.type === 'modified' && segment.original) {
|
||||
leftRuns.push(new TextRun({
|
||||
text: segment.original,
|
||||
size: 22,
|
||||
color: 'D97706', // 橙色
|
||||
highlight: 'yellow'
|
||||
}))
|
||||
} else if (segment.type === 'removed' && segment.original) {
|
||||
leftRuns.push(new TextRun({
|
||||
text: segment.original,
|
||||
size: 22,
|
||||
color: 'DC2626', // 红色
|
||||
strike: true,
|
||||
highlight: 'red'
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (leftRuns.length > 0) {
|
||||
children.push(new Paragraph({ children: leftRuns, spacing: { after: 400 } }))
|
||||
}
|
||||
|
||||
// 分隔线
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: '─'.repeat(50) })],
|
||||
spacing: { after: 200 }
|
||||
})
|
||||
)
|
||||
|
||||
// 修改文标题
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: '【修改文】',
|
||||
bold: true,
|
||||
size: 24
|
||||
})
|
||||
],
|
||||
spacing: { after: 200 }
|
||||
})
|
||||
)
|
||||
|
||||
// 修改文内容(带高亮)
|
||||
const rightRuns = []
|
||||
for (const segment of diffSegments.value) {
|
||||
if (segment.type === 'unchanged' && segment.rewritten) {
|
||||
rightRuns.push(new TextRun({ text: segment.rewritten, size: 22 }))
|
||||
} else if (segment.type === 'modified' && segment.rewritten) {
|
||||
rightRuns.push(new TextRun({
|
||||
text: segment.rewritten,
|
||||
size: 22,
|
||||
color: 'D97706', // 橙色
|
||||
highlight: 'yellow'
|
||||
}))
|
||||
} else if (segment.type === 'added' && segment.rewritten) {
|
||||
rightRuns.push(new TextRun({
|
||||
text: segment.rewritten,
|
||||
size: 22,
|
||||
color: '16A34A', // 绿色
|
||||
highlight: 'green'
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (rightRuns.length > 0) {
|
||||
children.push(new Paragraph({ children: rightRuns, spacing: { after: 400 } }))
|
||||
}
|
||||
|
||||
// 创建文档
|
||||
const doc = new Document({
|
||||
sections: [{
|
||||
children: children
|
||||
}]
|
||||
})
|
||||
|
||||
// 生成并下载
|
||||
const blob = await Packer.toBlob(doc)
|
||||
const timestamp = new Date().toISOString().slice(0, 10)
|
||||
saveAs(blob, `文稿差异对比_${timestamp}.docx`)
|
||||
|
||||
alert('导出成功!')
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
alert('导出失败,请重试')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,17 +2,9 @@
|
||||
<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">
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="font-bold text-lg text-white flex items-center gap-2">
|
||||
<span class="text-2xl">📂</span> 文稿管理
|
||||
</h1>
|
||||
<button
|
||||
@click="switchPage('writer')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
返回写作
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
|
||||
</header>
|
||||
|
||||
@@ -197,6 +189,11 @@ const selectDocument = (doc) => {
|
||||
// 通知父组件
|
||||
const selectedDoc = selectedDocId.value ? doc : null
|
||||
emit('document-selected', selectedDoc)
|
||||
|
||||
// 同步更新 store 以便右侧编辑区同步显示
|
||||
if (selectedDoc) {
|
||||
appStore.currentDocument = selectedDoc
|
||||
}
|
||||
}
|
||||
|
||||
// 切换版本面板
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<main class="flex-1 flex flex-col bg-slate-950 relative h-screen min-h-0">
|
||||
<!-- Prompt调试面板 -->
|
||||
<div v-if="showPromptDebug && currentPage === 'writer'" class="absolute inset-0 bg-slate-950/95 z-20 p-8 overflow-auto backdrop-blur-sm">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-bold text-yellow-500">🔧 构建的 Prompt 结构预览</h3>
|
||||
<button @click="showPromptDebug = false" class="text-sm text-slate-400 hover:text-white">关闭预览</button>
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="flex-1 overflow-y-auto p-8 md:p-12 min-h-0">
|
||||
<div class="max-w-3xl mx-auto pb-8">
|
||||
<div class="max-w-5xl mx-auto pb-8">
|
||||
<!-- 写作页面内容 -->
|
||||
<div v-if="currentPage === 'writer'">
|
||||
<div v-if="!generatedContent && !isGenerating" class="h-full flex flex-col items-center justify-center text-slate-700 mt-20">
|
||||
|
||||
@@ -2,17 +2,9 @@
|
||||
<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">
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="font-bold text-lg text-white flex items-center gap-2">
|
||||
<span class="text-2xl">📚</span> 素材库
|
||||
</h1>
|
||||
<button
|
||||
@click="switchPage('writer')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
返回写作
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
|
||||
</header>
|
||||
|
||||
|
||||
168
src/components/ParadigmSelectorModal.vue
Normal file
168
src/components/ParadigmSelectorModal.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div v-if="visible" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="bg-slate-800 rounded-lg w-[600px] max-h-[80vh] overflow-hidden border border-slate-600 flex flex-col shadow-2xl">
|
||||
<!-- 头部 -->
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between shrink-0">
|
||||
<h3 class="text-lg font-bold text-white flex items-center gap-2">
|
||||
<span>📚</span> 选择写作范式
|
||||
</h3>
|
||||
<button @click="$emit('close')" class="text-slate-400 hover:text-white transition">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索/过滤 -->
|
||||
<div class="px-4 py-3 border-b border-slate-700 bg-slate-800/50 shrink-0">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="搜索范式名称或描述..."
|
||||
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm text-slate-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 范式列表 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-3 min-h-0 bg-slate-900/20">
|
||||
<div v-if="filteredParadigms.length === 0" class="text-center text-slate-500 py-12">
|
||||
<span class="text-4xl block mb-2">🔍</span>
|
||||
<p class="text-sm">未找到相关范式</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="paradigm in filteredParadigms"
|
||||
:key="paradigm.id"
|
||||
@click="selectParadigm(paradigm)"
|
||||
:class="['p-4 rounded-xl border cursor-pointer transition-all duration-300 group',
|
||||
selectedId === paradigm.id
|
||||
? 'bg-blue-600/10 border-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.1)]'
|
||||
: 'bg-slate-800 border-slate-700 hover:border-slate-500 hover:bg-slate-750'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-start gap-4">
|
||||
<!-- 图标区 -->
|
||||
<div :class="['w-12 h-12 rounded-xl flex items-center justify-center text-2xl shrink-0 transition-transform group-hover:scale-110',
|
||||
selectedId === paradigm.id ? 'bg-blue-600' : 'bg-slate-700']">
|
||||
{{ paradigm.icon }}
|
||||
</div>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<h4 class="font-bold text-white text-base truncate">{{ paradigm.name }}</h4>
|
||||
<div class="flex gap-1">
|
||||
<span v-if="paradigm.isNew" class="text-[10px] px-1.5 py-0.5 rounded bg-orange-500 text-white font-bold animate-pulse">NEW</span>
|
||||
<span v-if="paradigm.isCustom" class="text-[10px] px-1.5 py-0.5 rounded bg-purple-500 text-white font-bold">自定义</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 line-clamp-2 leading-relaxed mb-3">{{ paradigm.description }}</p>
|
||||
|
||||
<!-- 标签列表 -->
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<span
|
||||
v-for="tag in paradigm.tags"
|
||||
:key="tag"
|
||||
:class="['text-[10px] px-2 py-0.5 rounded-full border border-slate-700/50',
|
||||
selectedId === paradigm.id ? 'bg-blue-900/40 text-blue-200' : 'bg-slate-700 text-slate-300']"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 勾选指示 -->
|
||||
<div class="w-6 h-6 rounded-full border-2 flex items-center justify-center shrink-0 mt-1 transition-colors"
|
||||
:class="selectedId === paradigm.id ? 'border-blue-500 bg-blue-600' : 'border-slate-600'">
|
||||
<span v-if="selectedId === paradigm.id" class="text-white text-xs">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="p-4 border-t border-slate-700 bg-slate-800 flex gap-3 shrink-0">
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="flex-1 py-2.5 rounded-lg bg-slate-700 text-slate-300 hover:bg-slate-600 transition font-medium"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="confirmSelect"
|
||||
:disabled="!selectedId"
|
||||
class="flex-1 py-2.5 rounded-lg bg-blue-600 text-white hover:bg-blue-500 transition font-medium disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-900/20"
|
||||
>
|
||||
确认作为对比要求
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { getParadigmList } from '../config/paradigms.js'
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'select'])
|
||||
|
||||
// 状态
|
||||
const paradigms = ref([])
|
||||
const selectedId = ref(null)
|
||||
const searchQuery = ref('')
|
||||
|
||||
// 加载范式列表(集成默认与自定义)
|
||||
const loadParadigms = () => {
|
||||
const defaultList = getParadigmList()
|
||||
|
||||
// 从本地存储加载自定义范式
|
||||
const savedCustomParadigms = localStorage.getItem('customParadigms')
|
||||
const customList = savedCustomParadigms ? JSON.parse(savedCustomParadigms) : []
|
||||
|
||||
// 合并并处理
|
||||
paradigms.value = [...defaultList, ...customList]
|
||||
}
|
||||
|
||||
// 筛选后的列表
|
||||
const filteredParadigms = computed(() => {
|
||||
if (!searchQuery.value.trim()) return paradigms.value
|
||||
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return paradigms.value.filter(p =>
|
||||
p.name.toLowerCase().includes(query) ||
|
||||
p.description.toLowerCase().includes(query) ||
|
||||
(p.tags && p.tags.some(tag => tag.toLowerCase().includes(query)))
|
||||
)
|
||||
})
|
||||
|
||||
// 选择
|
||||
const selectParadigm = (p) => {
|
||||
selectedId.value = p.id
|
||||
}
|
||||
|
||||
// 确认
|
||||
const confirmSelect = () => {
|
||||
const p = paradigms.value.find(item => item.id === selectedId.value)
|
||||
if (p) {
|
||||
emit('select', p)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听显示状态
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
searchQuery.value = ''
|
||||
selectedId.value = null
|
||||
loadParadigms()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -2,17 +2,9 @@
|
||||
<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">
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="font-bold text-lg text-white flex items-center gap-2">
|
||||
<span class="text-2xl">⚙️</span> 设置
|
||||
<span class="text-2xl">⚙️</span> 设置中心
|
||||
</h1>
|
||||
<button
|
||||
@click="switchPage('writer')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
返回写作
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -2,41 +2,9 @@
|
||||
<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">
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="font-bold text-lg text-white flex items-center gap-2">
|
||||
<span class="text-2xl">✍️</span> AI 写作工坊
|
||||
</h1>
|
||||
<button
|
||||
@click="switchPage('analysis')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
写作范式
|
||||
</button>
|
||||
<button
|
||||
@click="switchPage('documents')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
📂 文稿
|
||||
</button>
|
||||
<button
|
||||
@click="switchPage('materials')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
📚 素材
|
||||
</button>
|
||||
<button
|
||||
@click="switchPage('compare')"
|
||||
class="text-xs px-2 py-1 rounded bg-amber-900/50 text-amber-300 border border-amber-700/50 hover:bg-amber-800/50 transition"
|
||||
>
|
||||
🔍 对照检查
|
||||
</button>
|
||||
<button
|
||||
@click="switchPage('settings')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -675,6 +675,126 @@ export const PARADIGMS = {
|
||||
],
|
||||
|
||||
recommendedTags: ['Markdown格式', '数据支撑', '总分总结构']
|
||||
},
|
||||
|
||||
// 党委副书记2025年组织生活会
|
||||
'deputy-secretary-2025': {
|
||||
id: 'deputy-secretary-2025',
|
||||
name: '2025党委副书记对照检查',
|
||||
icon: '🎖️',
|
||||
description: '适用于党委副书记2025年组织生活会个人对照检查,强调政治敏感性与深度剖析',
|
||||
tags: ['五个带头', '思想根源', '行为习惯', '典型案例'],
|
||||
tagClass: 'bg-indigo-900/30 text-indigo-300',
|
||||
|
||||
// ===== 新架构:组合模式配置 =====
|
||||
logicParadigms: {
|
||||
problemSection: 'progressive-problem',
|
||||
analysisSection: 'deep-attribution',
|
||||
remediationSection: 'remediation'
|
||||
},
|
||||
dimensionSetId: 'five-leads',
|
||||
customDimensions: null,
|
||||
|
||||
expertGuidelines: [
|
||||
{
|
||||
title: '问题表述清晰具象',
|
||||
description: '严禁抽象、空泛表述。必须将问题具象化为行为或思想上的具体表现,并配备实际例子支撑。',
|
||||
checkKey: 'clear_problem',
|
||||
validationRule: '检查是否包含具体的岗位、职责或事件描述,避免"政治意识不够强"等空洞词汇'
|
||||
},
|
||||
{
|
||||
title: '思想根源深度剖析',
|
||||
description: '必须深入剖析问题背后的思想来源、波动及认识偏差,自然嵌入"学用脱节"、"上接天线、下接地气"等专业表述。',
|
||||
checkKey: 'deep_roots',
|
||||
validationRule: '检查是否包含"认识偏差"、"思想波动"等关键词,以及分析深度'
|
||||
},
|
||||
{
|
||||
title: '行为习惯精准刻画',
|
||||
description: '每个问题应包含具体的行为习惯表现(如开会发挥作用不足等),分析其对工作效果和党性修养的影响。',
|
||||
checkKey: 'behavior_habit',
|
||||
validationRule: '检查是否提到具体的日常行为或习惯'
|
||||
},
|
||||
{
|
||||
title: '典型案例反思',
|
||||
description: '必须包含至少1-2个典型案例,以此为基础展开反思,揭示个人在党性、作风或责任心上的差距。',
|
||||
checkKey: 'case_reflection',
|
||||
validationRule: '检查是否包含"典型案例"或具体的案例参考'
|
||||
},
|
||||
{
|
||||
title: '整改措施针对性',
|
||||
description: '针对每个问题提出明确的整改路径(如改进方法、落实路径),确保措施具有可操作性,而非泛泛之谈。',
|
||||
checkKey: 'remediation_path',
|
||||
validationRule: '检查整改措施是否包含具体的时间、频率或量化指标'
|
||||
}
|
||||
],
|
||||
|
||||
systemConstraints: [
|
||||
'每个问题必须基于指定的五个“带头”领域进行深刻剖析',
|
||||
'结构严格遵循:问题表述(具象) + 分析根源(思想层面) + 具体例证/后果分析',
|
||||
'整改措施必须具有极强的可操作性,避免空话套话',
|
||||
'必须引入至少1-2个典型案例展开反思'
|
||||
],
|
||||
|
||||
// 完整的 System Prompt(保留原始 Prompt 的全部语义)
|
||||
specializedPrompt: `# Role
|
||||
你是一个具有较高政治敏感性和党内写作经验的AI助手,专门为党委副书记的个人对照检查发言材料进行**深度检查和改进**。
|
||||
|
||||
# 任务目标
|
||||
你需要**逐一分析**文章中的每一条问题,并根据以下标准检查是否符合要求。
|
||||
|
||||
---
|
||||
|
||||
# 写作要点
|
||||
1. **每个问题必须基于以下"带头"领域进行深刻剖析:**
|
||||
- 强化政治忠诚、提高政治能力
|
||||
- 增强党性修养,坚定理想信念
|
||||
- 敬畏人民、敬畏组织、敬畏法纪
|
||||
- 干事创业、担当作为
|
||||
- 坚决扛起管党治党责任
|
||||
2. **结构要求:**
|
||||
- **问题表述**要清晰、具象
|
||||
- **分析根源**要深刻,不仅局限于"行为"或"表现",更要分析思想层面的问题
|
||||
- 每个问题要配有**具体例证**和**后果分析**,确保不空泛
|
||||
|
||||
---
|
||||
|
||||
# 具体检查步骤
|
||||
|
||||
## 步骤1:检查每一条"问题表述"的清晰度
|
||||
- **问题表述是否具体**:检查是否有过于抽象的描述(如"政治意识不够强");需要将问题具象化为行为或思想上的具体表现。
|
||||
- **例子**:是否有实际例子来支撑问题(如"某次重要决策中,未能及时向党中央报告"等)。
|
||||
|
||||
## 步骤2:深度分析"思想根源"
|
||||
- **是否分析了问题的思想来源**:检查文章是否仅仅停留在行为问题的表述上,而没有深入剖析背后的思想根源。
|
||||
- **建议**:每个问题应附带对"思想波动"、"认识偏差"的分析,**使用党内常见表述**如"学用脱节","上接天线下接地气"等。
|
||||
- **例子**:是否有分析到思想层面的"误区",如"表面上说得好,但实际工作中没有贯彻党的基本方针政策"。
|
||||
|
||||
## 步骤3:分析"行为习惯"
|
||||
- **是否提到具体的"行为习惯"**:对于每个问题,检查是否有行为上的具体表现,如"开会时未能充分发挥批评与自我批评"等。
|
||||
- **要点**:强调行为上的不规范或松懈,如何影响了工作效果或党性修养。
|
||||
|
||||
## 步骤4:典型案例的使用
|
||||
- **是否引入了具体的典型案例**:文章是否包含了至少1-2个实际案例,**并以此为基础展开自我反思**。
|
||||
- **案例要求**:检查是否有"通过案例揭示出个人在党性、作风或责任心上的差距"。
|
||||
|
||||
## 步骤5:改进措施的具体性与可操作性
|
||||
- **是否提出了针对性的改进措施**:检查文章是否在每个问题后都提出了明确的整改措施,而不仅是泛泛之谈。
|
||||
- **是否列举了明确的整改路径**:例如,改进政治意识的方法,落实党纪党规的方法。
|
||||
|
||||
---
|
||||
|
||||
# 最终输出要求
|
||||
1. **检查与改进建议**:对每个问题逐条进行检查,并指出其中的不足之处。
|
||||
2. **确保检查后每个问题都有明确的"整改措施"**,并且这些措施必须具有可操作性。
|
||||
3. **避免空话套话**:批评必须具体、切实,避免只做表面文章。
|
||||
4. **增加建设性反馈**:除了指出问题,还需要为改进提供具有指导意义的反馈。
|
||||
|
||||
---
|
||||
|
||||
# 开始检查
|
||||
现在,请根据上述标准,逐一分析并检查下面的文章内容。`,
|
||||
|
||||
recommendedTags: ['深度归因', '案例驱动', '语气诚恳', '政治校准']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
114
src/utils/preciseDiff.js
Normal file
114
src/utils/preciseDiff.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 精确文本差异计算工具
|
||||
* 使用 diff-match-patch 算法实现字符级别的精确对比
|
||||
*/
|
||||
|
||||
import DiffMatchPatch from 'diff-match-patch'
|
||||
|
||||
const dmp = new DiffMatchPatch()
|
||||
|
||||
/**
|
||||
* 计算两段文本的精确差异(字符级别)
|
||||
* @param {string} original - 原文
|
||||
* @param {string} rewritten - 修改后的文本
|
||||
* @returns {Array<{type: 'unchanged'|'modified'|'added'|'removed', original: string, rewritten: string}>}
|
||||
*/
|
||||
export const computePreciseDiff = (original, rewritten) => {
|
||||
if (!original && !rewritten) return []
|
||||
if (!original) {
|
||||
return [{ type: 'added', original: '', rewritten: rewritten }]
|
||||
}
|
||||
if (!rewritten) {
|
||||
return [{ type: 'removed', original: original, rewritten: '' }]
|
||||
}
|
||||
|
||||
// 使用 diff-match-patch 计算差异
|
||||
const diffs = dmp.diff_main(original, rewritten)
|
||||
dmp.diff_cleanupSemantic(diffs) // 语义清理,让差异更可读
|
||||
|
||||
const result = []
|
||||
|
||||
for (const [op, text] of diffs) {
|
||||
if (op === 0) {
|
||||
// 相同部分
|
||||
result.push({
|
||||
type: 'unchanged',
|
||||
original: text,
|
||||
rewritten: text
|
||||
})
|
||||
} else if (op === -1) {
|
||||
// 删除
|
||||
result.push({
|
||||
type: 'removed',
|
||||
original: text,
|
||||
rewritten: ''
|
||||
})
|
||||
} else if (op === 1) {
|
||||
// 新增
|
||||
result.push({
|
||||
type: 'added',
|
||||
original: '',
|
||||
rewritten: text
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 合并相邻的删除和新增为修改
|
||||
return mergeAdjacentChanges(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并相邻的删除和新增为修改
|
||||
* @param {Array} diffs - 差异数组
|
||||
* @returns {Array} - 合并后的差异数组
|
||||
*/
|
||||
const mergeAdjacentChanges = (diffs) => {
|
||||
const merged = []
|
||||
let i = 0
|
||||
|
||||
while (i < diffs.length) {
|
||||
const current = diffs[i]
|
||||
const next = diffs[i + 1]
|
||||
|
||||
// 如果当前是删除,下一个是新增,合并为修改
|
||||
if (current.type === 'removed' && next?.type === 'added') {
|
||||
merged.push({
|
||||
type: 'modified',
|
||||
original: current.original,
|
||||
rewritten: next.rewritten
|
||||
})
|
||||
i += 2
|
||||
}
|
||||
// 如果当前是新增,下一个是删除,也合并为修改
|
||||
else if (current.type === 'added' && next?.type === 'removed') {
|
||||
merged.push({
|
||||
type: 'modified',
|
||||
original: next.original,
|
||||
rewritten: current.rewritten
|
||||
})
|
||||
i += 2
|
||||
}
|
||||
else {
|
||||
merged.push(current)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取精确差异统计
|
||||
* @param {Array} diffSegments - 差异片段
|
||||
* @returns {{total: number, modified: number, added: number, removed: number, unchanged: number}}
|
||||
*/
|
||||
export const getPreciseDiffStats = (diffSegments) => {
|
||||
const nonUnchanged = diffSegments.filter(s => s.type !== 'unchanged')
|
||||
return {
|
||||
total: nonUnchanged.length,
|
||||
modified: diffSegments.filter(s => s.type === 'modified').length,
|
||||
added: diffSegments.filter(s => s.type === 'added').length,
|
||||
removed: diffSegments.filter(s => s.type === 'removed').length,
|
||||
unchanged: diffSegments.filter(s => s.type === 'unchanged').length
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user