fix(web): use tiptap mention for skill input

This commit is contained in:
JOJO 2026-05-30 23:31:13 +08:00
parent 72c2b2af2a
commit 39328cf6a9
9 changed files with 959 additions and 197 deletions

646
package-lock.json generated
View File

@ -8,6 +8,11 @@
"name": "ai-agent-frontend",
"version": "4.1.0",
"dependencies": {
"@tiptap/extension-mention": "^3.23.6",
"@tiptap/extension-placeholder": "^3.23.6",
"@tiptap/starter-kit": "^3.23.6",
"@tiptap/suggestion": "^3.23.6",
"@tiptap/vue-3": "^3.23.6",
"@types/html2canvas": "^0.5.35",
"diff": "^8.0.3",
"dompurify": "^3.2.7",
@ -567,6 +572,31 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@ -1310,6 +1340,469 @@
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@tiptap/core": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.23.6.tgz",
"integrity": "sha512-MRB3pHz4Oxqmcawh0cQ5iOGdY5xtNYp/1CoK7hdTLzw5K0C6/gTC2VvanB1R4INaB6EpBkxG/GiWkVirDRnuXw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-blockquote": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.23.6.tgz",
"integrity": "sha512-2RmnqNqTltZ2k1F7IfjoDNs935Uq4rRDR7d98mqkg3OlDktcQIyBpv0t9dTay6H5bkQeZUuS8ogK2S1E8Edjug==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-bold": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.23.6.tgz",
"integrity": "sha512-1LMhjnytdbbhWHSoOwnLxZAOQZWPkKyXVCNmaIk0Mhi4tLPUXptG4qKS5sVYTCveE5H6IBPFrbgBFi5dMI6krA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-bubble-menu": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.23.6.tgz",
"integrity": "sha512-Mwkyp9LkDHFbqmWRIkp63FinRxFu3ajC4qSb9t4mnHsb4kAdbNLLsGtbFg+le0SWk4CxGwAOwM7SzeJ+6UGqCA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-bullet-list": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.23.6.tgz",
"integrity": "sha512-RMRgfXZykr/13X8UBOwvpgysVOo9KchwqMoEbvqQSj4YFfU56iIn59C8sbxiQ1sKfeltUf0wH4fPc0I4iwKqAA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.6"
}
},
"node_modules/@tiptap/extension-code": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.23.6.tgz",
"integrity": "sha512-KG8KXFYyLrtYvT7AZ1WGV61ofx8pDe5g9pH658MERxqQGii+Pyfc6xkz04l7XeBts/7+571UQp/0O7i/z560TA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-code-block": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.23.6.tgz",
"integrity": "sha512-4kccgcn5yHThxrzsIhJny3EwfEZYIk+BjUCL4uIuzOyWvExtGhZ6JMHVCZeMhI8D1/bX1LNkkAKN5DXPzH4lXQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-document": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.23.6.tgz",
"integrity": "sha512-XDAIgG9KcKumFM9KJWUEUhXPbFIhhl47bfy5GknareWTRKke85rcoj/oxKKO9ihLZr8JfpbXjqnS4SCm5yhYPw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-dropcursor": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.23.6.tgz",
"integrity": "sha512-+XWEoRKf3lXxi7Le1aOM2xU1XHwxICGpXjT3m4QaYqUgIpsq8gQEuso6kVg8DnTD7biKQs6+oIQ0o2b/gTW9WA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "3.23.6"
}
},
"node_modules/@tiptap/extension-floating-menu": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.23.6.tgz",
"integrity": "sha512-2kjuDcEq69lEcECl75xqY5MyzUSh2zcC5aLrpwP1WwhJz5bxsIFHiaps5AP6h9R4A+ZBj5b2haay2Y1wDUU3VA==",
"license": "MIT",
"optional": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@floating-ui/dom": "^1.0.0",
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-gapcursor": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.23.6.tgz",
"integrity": "sha512-wbKmxXsszxWacEkrHucRpSQbiKjz4fmOebD6OVyL9AcrmlbxNk8vcM3iyh/8cVeRy09XY+morM165t/u7/z4IQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "3.23.6"
}
},
"node_modules/@tiptap/extension-hard-break": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.23.6.tgz",
"integrity": "sha512-KeUm+tkUfIVSX9QM9XOIhaay0Fn36sLKUo5NVYjN3uJaxFvaZXZmTlxdO85OTdgF2P5sqh9LomrIgliaFRGk4w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-heading": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.23.6.tgz",
"integrity": "sha512-A/0jPhxnUh9THSZymlu0OGPZe1wdFdwHAXnRCmqvYUCwJjrG7LCC/ahzmcj1tcNzI9hgHyuYPSfev8RXYrNu/w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-horizontal-rule": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.23.6.tgz",
"integrity": "sha512-hEUlz4H+I64r+TH6LCuNCRgO7JTHncXGmx9+WbU69EOfY8O0ZurcgeJc8HeiAKL+r9YuC1e5YHfFxgCaaC0jlg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-italic": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.23.6.tgz",
"integrity": "sha512-wol5KdwCPAvpiYhH9PLlvO8ZnJHwZtIboVevrfOGgBcKlXRA3dedR4OAMXHnUtkkzu9KtliLg1+TYzEx4JZG9Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-link": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.23.6.tgz",
"integrity": "sha512-KNZz7z7P2/qbQsx5bPAbSPjrKDg1VHsedGlLHJCr8U2VRD5VgmDLkMpkouP1CsDg15qgyUKv/nDib5KgPpLNWA==",
"license": "MIT",
"dependencies": {
"linkifyjs": "^4.3.3"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-list": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.23.6.tgz",
"integrity": "sha512-z6vj9+Qht2sjdQkyyHcUpsC/yCIZqTrQiyHDhs/HGKrfvoANyAZGpqdNeKf1wSyjIso+27tQuIH5NDfk8ygyNw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/extension-list-item": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.23.6.tgz",
"integrity": "sha512-3zzyhdkUWcHVpXuvy6KiIwjh29rbH6gEDEqPQqHLrl1XGnO9pnShC7pSHctlCDjmcx3O4n9cd4QMtVBlUerbiA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.6"
}
},
"node_modules/@tiptap/extension-list-keymap": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.23.6.tgz",
"integrity": "sha512-x8bPcLViGzg/RAmQM/XtmfqIwQ/Pv9Q8mkd+OgfUiTqjeJqKwVQmiqbLFNa7zw81+H61M+HDU+qGAaQ3vRIMjw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.6"
}
},
"node_modules/@tiptap/extension-mention": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-3.23.6.tgz",
"integrity": "sha512-rSjeAAtuMwMA1lj4nbxz3rbmM06yPFUc8TFzhrEpmA4/l5XNWOk/PQef6uiGN+Isv2Z2PrIhr8XrR7Me8OSCiA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6",
"@tiptap/suggestion": "3.23.6"
}
},
"node_modules/@tiptap/extension-ordered-list": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.23.6.tgz",
"integrity": "sha512-1m/wWB/ZtXcmG2vNdiUkCqsOgqv5vBjCv/mVaHhF9OvV+zQS8YDjoWE7zEuT/GgELdT77Xq8lHrn4nCDudB3/A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extension-list": "3.23.6"
}
},
"node_modules/@tiptap/extension-paragraph": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.23.6.tgz",
"integrity": "sha512-+7m58LUSncodjrIyXks4RZ3tLNYrvgT77wRR4l3HnM5OABY3GDsDTqi7c1t1yI29NVOSk/DUacqy6UwYAj1DGg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-placeholder": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.23.6.tgz",
"integrity": "sha512-8I6b2aevF74aLgymKMxbDxSLxWA2y+2dh0zZDeI8sRZ2m6WHHes+Kyuuwkq1HIPcR+ZLpbec74cmf6lcL/yvqQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/extensions": "3.23.6"
}
},
"node_modules/@tiptap/extension-strike": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.23.6.tgz",
"integrity": "sha512-oF7FEZ37f15aCe5kPgzGDYf/m+hr7VdQ/Ko/Hds/UM9pX7AG1fdtmRrl6wqkRqDM/incZaC/AQR2/Dpo2VCNGQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-text": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.23.6.tgz",
"integrity": "sha512-ipoC2TkIAIOTiF5ByiGgvQB1DqDyfP90wrUB3mohBcgvp7lQnwHszCDGv8dNnmcUek8uXV/uoLu2VXeVQlxjPA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extension-underline": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.23.6.tgz",
"integrity": "sha512-P55wGIZGYTVH92Fq0cgI4/O9AhLCaJC3hhxg15RSERP5/YegM9eJHDK/GQ1EE/DvYA+xpYGOV6agKwAUqfA/Iw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6"
}
},
"node_modules/@tiptap/extensions": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.23.6.tgz",
"integrity": "sha512-X09/Db1teB+ifXzDGVVFmOeQRx7wTAayE9/280spxpsHkHZvJ5bHRvWIzUzviMIjbBz+NPDIKYPK7gMfh9iaig==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/pm": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.23.6.tgz",
"integrity": "sha512-in5CaMaWlJcH2A1q6GJKFtrodE8WLS3M9tIi/f89jPmIVHJShpodC0KZDNyJkrVBQomYk0DEh86Utm6ASXzQww==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-commands": "^1.6.2",
"prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2",
"prosemirror-history": "^1.4.1",
"prosemirror-keymap": "^1.2.2",
"prosemirror-model": "^1.24.1",
"prosemirror-schema-list": "^1.5.0",
"prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.4",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.38.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tiptap/starter-kit": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.23.6.tgz",
"integrity": "sha512-gykwtGWrnWCmtql1hid3opac/KV8zQvOAnu3bTqIqcHrn1FusbUwKmNzavSbfGvcktHM3hFjb35W48JyVLyu/A==",
"license": "MIT",
"dependencies": {
"@tiptap/core": "^3.23.6",
"@tiptap/extension-blockquote": "^3.23.6",
"@tiptap/extension-bold": "^3.23.6",
"@tiptap/extension-bullet-list": "^3.23.6",
"@tiptap/extension-code": "^3.23.6",
"@tiptap/extension-code-block": "^3.23.6",
"@tiptap/extension-document": "^3.23.6",
"@tiptap/extension-dropcursor": "^3.23.6",
"@tiptap/extension-gapcursor": "^3.23.6",
"@tiptap/extension-hard-break": "^3.23.6",
"@tiptap/extension-heading": "^3.23.6",
"@tiptap/extension-horizontal-rule": "^3.23.6",
"@tiptap/extension-italic": "^3.23.6",
"@tiptap/extension-link": "^3.23.6",
"@tiptap/extension-list": "^3.23.6",
"@tiptap/extension-list-item": "^3.23.6",
"@tiptap/extension-list-keymap": "^3.23.6",
"@tiptap/extension-ordered-list": "^3.23.6",
"@tiptap/extension-paragraph": "^3.23.6",
"@tiptap/extension-strike": "^3.23.6",
"@tiptap/extension-text": "^3.23.6",
"@tiptap/extension-underline": "^3.23.6",
"@tiptap/extensions": "^3.23.6",
"@tiptap/pm": "^3.23.6"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@tiptap/suggestion": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-3.23.6.tgz",
"integrity": "sha512-YAoI2jctPClcyUhIcpxb1QlrUFG2a1Xsv1gS4tIfgh5KoOuEfGfCoeCq89TKgz/rHeP+ktRhzg1E2E4EY68HEA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6"
}
},
"node_modules/@tiptap/vue-3": {
"version": "3.23.6",
"resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-3.23.6.tgz",
"integrity": "sha512-MQ9uX1PGKqOrDpdq5O72zJIME7BA8AatcEDUmiJvEC8MH7cF2hpKBVt9TeBE+zTr1jrEuGG8wz76gaJsiykwhQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"optionalDependencies": {
"@tiptap/extension-bubble-menu": "^3.23.6",
"@tiptap/extension-floating-menu": "^3.23.6"
},
"peerDependencies": {
"@floating-ui/dom": "^1.0.0",
"@tiptap/core": "3.23.6",
"@tiptap/pm": "3.23.6",
"vue": "^3.0.0"
}
},
"node_modules/@types/debug": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
@ -3366,6 +3859,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/linkifyjs": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -4369,6 +4868,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/orderedmap": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
"license": "MIT"
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@ -4631,6 +5136,135 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/prosemirror-changeset": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
"integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
"license": "MIT",
"dependencies": {
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-commands": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.10.2"
}
},
"node_modules/prosemirror-dropcursor": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0",
"prosemirror-view": "^1.1.0"
}
},
"node_modules/prosemirror-gapcursor": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.0.0",
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-view": "^1.0.0"
}
},
"node_modules/prosemirror-history": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.31.0",
"rope-sequence": "^1.3.0"
}
},
"node_modules/prosemirror-keymap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^2.2.0"
}
},
"node_modules/prosemirror-model": {
"version": "1.25.7",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.7.tgz",
"integrity": "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==",
"license": "MIT",
"dependencies": {
"orderedmap": "^2.0.0"
}
},
"node_modules/prosemirror-schema-list": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.7.3"
}
},
"node_modules/prosemirror-state": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.27.0"
}
},
"node_modules/prosemirror-tables": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.2.3",
"prosemirror-model": "^1.25.4",
"prosemirror-state": "^1.4.4",
"prosemirror-transform": "^1.10.5",
"prosemirror-view": "^1.41.4"
}
},
"node_modules/prosemirror-transform": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
"version": "1.41.8",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz",
"integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -4887,6 +5521,12 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rope-sequence": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
"license": "MIT"
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -5556,6 +6196,12 @@
"typescript": "*"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",

View File

@ -14,6 +14,11 @@
"cli:typecheck": "npm --prefix cli run typecheck"
},
"dependencies": {
"@tiptap/extension-mention": "^3.23.6",
"@tiptap/extension-placeholder": "^3.23.6",
"@tiptap/starter-kit": "^3.23.6",
"@tiptap/suggestion": "^3.23.6",
"@tiptap/vue-3": "^3.23.6",
"@types/html2canvas": "^0.5.35",
"diff": "^8.0.3",
"dompurify": "^3.2.7",

View File

@ -324,10 +324,14 @@ def get_input_draft(terminal: WebTerminal, workspace: UserWorkspace, username: s
payload = _read_input_draft_payload(path)
content = payload.get("content")
updated_at = payload.get("updated_at")
skill_refs = payload.get("skill_refs")
editor_json = payload.get("editor_json")
return jsonify({
"success": True,
"data": {
"content": content if isinstance(content, str) else "",
"skill_refs": skill_refs if isinstance(skill_refs, list) else [],
"editor_json": editor_json if isinstance(editor_json, dict) else None,
"updated_at": str(updated_at or ""),
"scope": scope,
}
@ -350,6 +354,24 @@ def upsert_input_draft(terminal: WebTerminal, workspace: UserWorkspace, username
content = str(content)
if len(content) > 40000:
return jsonify({"success": False, "error": "输入草稿过长(最大 40000 字符)"}), 400
raw_skill_refs = body.get("skill_refs") if isinstance(body, dict) else []
skill_refs = []
if isinstance(raw_skill_refs, list):
for item in raw_skill_refs[:100]:
if not isinstance(item, dict):
continue
name = str(item.get("name") or "")[:200]
description = str(item.get("description") or "")[:1000]
path_value = str(item.get("path") or "")[:4000]
if name and path_value:
skill_refs.append({
"name": name,
"description": description,
"path": path_value,
})
editor_json = body.get("editor_json") if isinstance(body, dict) else None
if editor_json is not None and not isinstance(editor_json, dict):
editor_json = None
path, scope = _resolve_input_draft_path(workspace, username)
if content == "":
@ -370,6 +392,8 @@ def upsert_input_draft(terminal: WebTerminal, workspace: UserWorkspace, username
payload = {
"content": content,
"skill_refs": skill_refs,
"editor_json": editor_json,
"updated_at": datetime.now().isoformat(),
"scope": scope,
}

View File

@ -661,7 +661,19 @@ export const messageMethods = {
return false;
}
const text = ((usePresetText ? presetText : this.inputMessage) || '').trim();
let text = ((usePresetText ? presetText : this.inputMessage) || '').trim();
let preparedSkillRefs = [];
if (!usePresetText) {
const composerRef = typeof this.getInputComposerRef === 'function' ? this.getInputComposerRef() : null;
const prepared =
composerRef && typeof composerRef.prepareMessageForSend === 'function'
? composerRef.prepareMessageForSend(this.inputMessage)
: null;
if (prepared && typeof prepared.message === 'string') {
text = prepared.message.trim();
preparedSkillRefs = Array.isArray(prepared.skillRefs) ? prepared.skillRefs : [];
}
}
const images = usePresetText
? []
: Array.isArray(this.selectedImages)
@ -725,7 +737,11 @@ export const messageMethods = {
}
const message = text;
const skillRefs = usePresetText ? [] : extractSkillRefsFromMessage(message);
const skillRefs = usePresetText
? []
: preparedSkillRefs.length
? preparedSkillRefs
: extractSkillRefsFromMessage(message);
const wasBlank = this.isConversationBlank();
if (wasBlank) {

View File

@ -413,7 +413,19 @@ export const uiMethods = {
this.composerDraftSaveTimer = null;
}
const payloadText = JSON.stringify({ content });
const composerRef = typeof this.getInputComposerRef === 'function' ? this.getInputComposerRef() : null;
const composerMeta =
composerRef && typeof composerRef.getComposerDraftMeta === 'function'
? composerRef.getComposerDraftMeta()
: {};
const payloadText = JSON.stringify({
content,
editor_json:
composerMeta?.editor_json && typeof composerMeta.editor_json === 'object'
? composerMeta.editor_json
: null,
skill_refs: Array.isArray(composerMeta?.skill_refs) ? composerMeta.skill_refs : []
});
if (
useBeacon &&
typeof navigator !== 'undefined' &&
@ -472,10 +484,20 @@ export const uiMethods = {
return;
}
const content = this.normalizeComposerDraftContent(payload?.data?.content || '');
const skillRefs = Array.isArray(payload?.data?.skill_refs) ? payload.data.skill_refs : [];
const editorJson =
payload?.data?.editor_json && typeof payload.data.editor_json === 'object'
? payload.data.editor_json
: null;
this.composerDraftLastSyncedContent = content;
this.composerDraftDirty = false;
this.inputSetMessage(content);
this.$nextTick(() => {
const composerRef =
typeof this.getInputComposerRef === 'function' ? this.getInputComposerRef() : null;
if (composerRef && typeof composerRef.restoreComposerDraftMeta === 'function') {
composerRef.restoreComposerDraftMeta({ skill_refs: skillRefs, editor_json: editorJson });
}
if (typeof this.autoResizeInput === 'function') {
this.autoResizeInput();
}

View File

@ -137,38 +137,13 @@
+
</button>
<div class="stadium-input-rich">
<div
v-if="hasSkillMarkdownLink"
ref="richInput"
class="stadium-input stadium-input-editor"
contenteditable="true"
role="textbox"
aria-multiline="true"
:data-placeholder="props.inputMessage ? '' : '输入消息... (Ctrl+Enter 发送)'"
@input="onRichInput"
@focus="$emit('input-focus')"
@blur="onInputBlur"
@keydown="onRichKeydown"
></div>
<textarea
v-else
:key="composerInputKey"
ref="stadiumInput"
class="stadium-input"
rows="1"
:value="inputMessage"
:disabled="!isConnected || inputLocked"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
placeholder="输入消息... (Ctrl+Enter 发送)"
@input="onInput"
@scroll="onTextareaScroll"
@focus="$emit('input-focus')"
@blur="onInputBlur"
@keydown="onKeydown"
@keydown.enter.ctrl.prevent="$emit('send-or-stop')"
></textarea>
<EditorContent
:editor="editor"
class="stadium-input stadium-input-tiptap"
:class="{ 'is-disabled': !isConnected || inputLocked }"
@focusin="$emit('input-focus')"
@focusout="onInputBlur"
/>
</div>
<button
type="button"
@ -336,6 +311,11 @@ import {
nextTick,
computed
} from 'vue';
import { EditorContent, useEditor, type JSONContent } from '@tiptap/vue-3';
import StarterKit from '@tiptap/starter-kit';
import Mention from '@tiptap/extension-mention';
import Placeholder from '@tiptap/extension-placeholder';
import { TextSelection } from 'prosemirror-state';
import QuickMenu from '@/components/input/QuickMenu.vue';
import { useInputStore } from '@/stores/input';
import { usePersonalizationStore } from '@/stores/personalization';
@ -445,11 +425,11 @@ const inputAreaRoot = ref<HTMLElement | null>(null);
const stadiumShellOuter = ref<HTMLElement | null>(null);
const compactInputShell = ref<HTMLElement | null>(null);
const stadiumInput = ref<HTMLTextAreaElement | null>(null);
const richInput = ref<HTMLElement | null>(null);
const fileUploadInput = ref<HTMLInputElement | null>(null);
const baselineComposerVisualHeight = ref<number>(0);
type SkillItem = { name: string; description: string; path: string };
const availableSkills = ref<SkillItem[]>([]);
const selectedSkillRefs = ref<SkillItem[]>([]);
const skillsLoaded = ref(false);
const skillsLoading = ref(false);
const skillSlashOpen = ref(false);
@ -457,6 +437,7 @@ const skillSlashQuery = ref('');
const skillSlashActiveIndex = ref(0);
const skillSlashList = ref<HTMLElement | null>(null);
let composerResizeObserver: ResizeObserver | null = null;
let syncingEditorFromProps = false;
const formatImageName = (path: string): string => {
if (!path) return '';
@ -488,101 +469,22 @@ const runtimeQueuedMessagesForRender = computed(() => {
}));
});
const escapeHtml = (value: string): string =>
String(value || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const SKILL_LINK_RE = /\[\$([^\]\n]+)\]\(([^)\n]*\/\.agents\/skills\/[^)\n]+\/SKILL\.md)\)/g;
const hasSkillMarkdownLink = computed(() => {
SKILL_LINK_RE.lastIndex = 0;
return SKILL_LINK_RE.test(props.inputMessage || '');
});
const parseSkillSegments = (sourceValue = '') => {
const source = String(sourceValue || '');
const segments: Array<{ type: 'text' | 'skill'; text: string; raw?: string }> = [];
SKILL_LINK_RE.lastIndex = 0;
let cursor = 0;
let match: RegExpExecArray | null;
while ((match = SKILL_LINK_RE.exec(source))) {
if (match.index > cursor) {
segments.push({ type: 'text', text: source.slice(cursor, match.index) });
}
segments.push({ type: 'skill', text: match[1] || '', raw: match[0] || '' });
cursor = match.index + match[0].length;
}
if (cursor < source.length) {
segments.push({ type: 'text', text: source.slice(cursor) });
}
return segments;
};
const renderRichEditor = () => {
const el = richInput.value;
if (!el) return;
const rawValue = props.inputMessage || '';
el.innerHTML = '';
const segments = parseSkillSegments(rawValue);
segments.forEach((segment) => {
if (segment.type === 'skill') {
const span = document.createElement('span');
span.className = 'skill-md-link';
span.contentEditable = 'false';
span.dataset.skillRaw = segment.raw || '';
span.textContent = segment.text || '';
el.appendChild(span);
return;
}
el.appendChild(document.createTextNode(segment.text || ''));
});
if (!segments.length) {
el.appendChild(document.createTextNode(''));
}
};
const rawFromRichEditor = () => {
const el = richInput.value;
if (!el) return props.inputMessage || '';
const readNode = (node: Node): string => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent || '';
}
if (node instanceof HTMLBRElement) {
return '\n';
}
if (node instanceof HTMLElement) {
if (node.dataset.skillRaw) {
return node.dataset.skillRaw;
}
const childText = Array.from(node.childNodes).map(readNode).join('');
if (node.tagName === 'DIV' || node.tagName === 'P') {
return `${childText}\n`;
}
return childText;
}
return '';
};
return Array.from(el.childNodes).map(readNode).join('').replace(/\n$/, '');
};
const findSlashToken = () => {
const textarea = stadiumInput.value;
const cursor = textarea?.selectionStart ?? (props.inputMessage || '').length;
const before = (props.inputMessage || '').slice(0, cursor);
const match = /(^|\s)\/([A-Za-z0-9_-]*)$/.exec(before);
if (!match) {
return null;
}
const slashStart = before.length - (match[2] || '').length - 1;
const instance = editor.value;
if (!instance) return null;
const { state } = instance;
const { $from } = state.selection;
const lineStart = $from.start();
const beforeInLine = $from.parent.textBetween(0, $from.parentOffset, '\n', '\n');
const match = /(^|[ \n])\/([A-Za-z0-9_-]*)$/.exec(beforeInLine);
if (!match) return null;
const query = match[2] || '';
const start = $from.pos - query.length - 1;
const end = $from.pos;
return {
start: slashStart,
end: cursor,
query: match[2] || ''
start,
end,
query
};
};
@ -661,33 +563,37 @@ const refreshSkillSlashState = () => {
const selectSkillSlashItem = (index = skillSlashActiveIndex.value) => {
const token = findSlashToken();
const skill = filteredSkills.value[index];
if (!token || !skill) {
const instance = editor.value;
if (!token || !skill || !instance) {
return;
}
const current = props.inputMessage || '';
const link = `[$${skill.name}](${skill.path})`;
const nextValue = `${current.slice(0, token.start)}${link}${current.slice(token.end)}`;
const nextCursor = token.start + link.length;
emit('update:input-message', nextValue);
emit('input-change');
selectedSkillRefs.value = [
...selectedSkillRefs.value.filter((item) => item.path !== skill.path),
skill
];
const { state, view, schema } = instance;
const mentionType = schema.nodes.mention;
if (!mentionType) {
return;
}
const mentionNode = mentionType.create({
id: skill.path,
label: skill.name,
path: skill.path,
description: skill.description || '',
mentionSuggestionChar: ''
});
let tr = state.tr.delete(token.start, token.end);
tr = tr.insert(token.start, mentionNode);
const afterMention = token.start + mentionNode.nodeSize;
tr = tr.insertText(' ', afterMention);
tr = tr.setSelection(TextSelection.near(tr.doc.resolve(afterMention + 1)));
view.dispatch(tr);
view.focus();
skillSlashOpen.value = false;
skillSlashQuery.value = '';
skillSlashActiveIndex.value = 0;
nextTick(() => {
const textarea = stadiumInput.value;
if (textarea) {
textarea.focus();
textarea.setSelectionRange(nextCursor, nextCursor);
} else if (richInput.value) {
renderRichEditor();
richInput.value.focus();
const range = document.createRange();
range.selectNodeContents(richInput.value);
range.collapse(false);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
adjustTextareaSize();
});
};
@ -732,34 +638,12 @@ const handleSlashMenuKeydown = (event: KeyboardEvent): boolean => {
return false;
};
const onKeydown = (event: KeyboardEvent) => {
const onKeydown = (event: KeyboardEvent): boolean => {
if (handleSlashMenuKeydown(event)) {
return;
return true;
}
requestAnimationFrame(refreshSkillSlashState);
};
const onRichKeydown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault();
emit('send-or-stop');
return;
}
if (handleSlashMenuKeydown(event)) {
return;
}
requestAnimationFrame(() => {
emit('update:input-message', rawFromRichEditor());
emit('input-change');
});
};
const onRichInput = () => {
emit('update:input-message', rawFromRichEditor());
emit('input-change');
nextTick(() => {
adjustTextareaSize();
});
return false;
};
const onInputBlur = () => {
@ -769,6 +653,114 @@ const onInputBlur = () => {
}, 120);
};
const textToTiptapContent = (text = ''): JSONContent => {
const lines = String(text || '').split('\n');
return {
type: 'doc',
content: (lines.length ? lines : ['']).map((line) => ({
type: 'paragraph',
content: line ? [{ type: 'text', text: line }] : undefined
}))
};
};
const getEditorPlainText = () => {
const instance = editor.value;
if (!instance) return props.inputMessage || '';
return instance.getText({ blockSeparator: '\n' } as any);
};
const editorJsonToMessage = (json?: JSONContent | null) => {
const skillRefs: Array<{ name: string; path: string }> = [];
const readInline = (node?: JSONContent): string => {
if (!node) return '';
if (node.type === 'text') return node.text || '';
if (node.type === 'mention') {
const attrs = node.attrs || {};
const name = String(attrs.label || attrs.id || '').trim();
const path = String(attrs.path || attrs.id || '').trim();
if (name && path && !skillRefs.some((item) => item.path === path)) {
skillRefs.push({ name, path });
}
return name && path ? `[$${name}](${path})` : name;
}
if (node.type === 'hardBreak') return '\n';
return Array.isArray(node.content) ? node.content.map(readInline).join('') : '';
};
const blocks = Array.isArray(json?.content) ? json.content : [];
const message = blocks.map((block) => readInline(block)).join('\n').trim();
return { message, skillRefs };
};
const editor = useEditor({
content: textToTiptapContent(props.inputMessage || ''),
editable: props.isConnected && !props.inputLocked,
extensions: [
StarterKit.configure({
codeBlock: false,
heading: false,
blockquote: false,
bulletList: false,
orderedList: false,
listItem: false,
horizontalRule: false
}),
Placeholder.configure({
placeholder: '输入消息... (Ctrl+Enter 发送)'
}),
Mention.configure({
HTMLAttributes: {
class: 'skill-md-link'
},
deleteTriggerWithBackspace: true,
renderText({ node }) {
return String(node.attrs.label || node.attrs.id || '');
},
renderHTML({ node, HTMLAttributes }) {
const attrs = HTMLAttributes || {};
return [
'span',
{
...attrs,
class: `${attrs.class || ''} skill-md-link`.trim(),
'data-skill-path': node.attrs.path || node.attrs.id || ''
},
String(node.attrs.label || node.attrs.id || '')
];
},
suggestion: {
char: '\u0000'
} as any
})
],
editorProps: {
attributes: {
class: 'stadium-input-editor',
'data-placeholder': '输入消息... (Ctrl+Enter 发送)'
},
handleKeyDown(_view, event) {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault();
emit('send-or-stop');
return true;
}
return onKeydown(event);
}
},
onUpdate() {
if (syncingEditorFromProps) return;
emit('update:input-message', getEditorPlainText());
emit('input-change');
nextTick(() => {
adjustTextareaSize();
refreshSkillSlashState();
});
},
onFocus() {
emit('input-focus');
}
});
const RUNTIME_QUEUE_ANIM_DURATION = 300;
const RUNTIME_QUEUE_ANIM_EASING = 'cubic-bezier(0.25, 0.8, 0.25, 1)';
const runtimeQueueList = ref<HTMLElement | null>(null);
@ -1095,13 +1087,11 @@ const emitComposerHeight = () => {
};
const adjustTextareaSize = () => {
const textarea = stadiumInput.value;
const rich = richInput.value;
if (!textarea && !rich) {
const root = inputAreaRoot.value?.querySelector('.stadium-input-editor') as HTMLElement | null;
if (!root) {
return;
}
const target = textarea || rich;
if (!target) return;
const target = root;
target.style.height = 'auto';
const computedStyle = window.getComputedStyle(target);
const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20;
@ -1136,6 +1126,25 @@ const onFileChange = (event: Event) => {
}
};
const prepareMessageForSend = () => editorJsonToMessage(editor.value?.getJSON() as JSONContent);
const getComposerDraftMeta = () => ({
editor_json: editor.value?.getJSON() || null,
skill_refs: editorJsonToMessage(editor.value?.getJSON() as JSONContent).skillRefs
});
const restoreComposerDraftMeta = (meta?: { skill_refs?: SkillItem[]; editor_json?: JSONContent } | null) => {
const instance = editor.value;
if (!instance) return;
if (meta?.editor_json && typeof meta.editor_json === 'object') {
syncingEditorFromProps = true;
instance.commands.setContent(meta.editor_json);
syncingEditorFromProps = false;
emit('update:input-message', getEditorPlainText());
nextTick(adjustTextareaSize);
}
};
const triggerQuickUpload = () => {
if (!props.isConnected || props.uploading) {
return;
@ -1221,17 +1230,22 @@ defineExpose({
stadiumShellOuter,
compactInputShell,
stadiumInput,
richInput,
fileUploadInput
fileUploadInput,
prepareMessageForSend,
getComposerDraftMeta,
restoreComposerDraftMeta
});
watch(
() => props.inputMessage,
async () => {
await nextTick();
if (hasSkillMarkdownLink.value && richInput.value && document.activeElement !== richInput.value) {
renderRichEditor();
const instance = editor.value;
if (instance && !syncingEditorFromProps && getEditorPlainText() !== (props.inputMessage || '')) {
syncingEditorFromProps = true;
instance.commands.setContent(textToTiptapContent(props.inputMessage || ''));
syncingEditorFromProps = false;
}
await nextTick();
adjustTextareaSize();
refreshSkillSlashState();
}
@ -1240,14 +1254,22 @@ watch(
watch(
() => props.isConnected,
async (connected) => {
if (!connected && stadiumInput.value) {
stadiumInput.value.blur();
editor.value?.setEditable(!!connected && !props.inputLocked);
if (!connected) {
editor.value?.commands.blur();
}
await nextTick();
adjustTextareaSize();
}
);
watch(
() => props.inputLocked,
(locked) => {
editor.value?.setEditable(props.isConnected && !locked);
}
);
watch(
() => props.currentConversationId,
async () => {
@ -1282,9 +1304,6 @@ watch(goalBannerCollapsed, async () => {
});
onMounted(() => {
if (hasSkillMarkdownLink.value) {
renderRichEditor();
}
adjustTextareaSize();
nextTick(() => {
emitComposerHeight();
@ -1306,6 +1325,7 @@ onMounted(() => {
});
onBeforeUnmount(() => {
editor.value?.destroy();
runtimeQueueItemRefs.forEach((el) => {
if (el) {
stopRuntimeQueueAnimation(el);

View File

@ -546,6 +546,8 @@
.user-message .message-text .user-skill-link {
color: #2563eb;
font-weight: 600;
display: inline-block;
margin: 0 0.25em;
}
:root[data-theme='dark'] .user-message .message-text .user-skill-link,

View File

@ -244,7 +244,8 @@ body[data-theme='dark'] {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
}
.stadium-input-editor .skill-md-link {
.stadium-input-editor .skill-md-link,
.stadium-input-highlight .skill-md-link {
color: #60a5fa;
}
}
@ -539,16 +540,17 @@ body[data-theme='dark'] {
.stadium-input-editor .skill-md-link {
color: #2563eb;
font-weight: 600;
display: inline-block;
border-radius: 6px;
font-weight: inherit;
border-radius: 4px;
padding: 0 1px;
user-select: all;
white-space: nowrap;
}
.stadium-input-editor:empty::before {
.stadium-input-editor p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: var(--claude-text-tertiary);
float: left;
height: 0;
pointer-events: none;
}
@ -607,8 +609,33 @@ body[data-theme='dark'] {
z-index: 1;
}
.stadium-input-tiptap {
display: block;
}
.stadium-input-tiptap.is-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.stadium-input-editor {
cursor: text;
min-height: 20px;
outline: none;
overflow-y: auto;
scrollbar-width: none;
white-space: pre-wrap;
word-break: break-all;
overflow-wrap: normal;
}
.stadium-input-editor p {
margin: 0;
}
.stadium-input-editor::-webkit-scrollbar {
width: 0;
height: 0;
}
.stadium-input:disabled {