mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
feat(site): replace Agent chat textarea with Lexical editor (#22449)
## Summary Replaces the plain `<TextareaAutosize>` in the Agent chat input (`AgentChatInput`) with a Lexical-based editor component, matching the pattern used in [coder/blink](https://github.com/coder/blink). ## What changed ### New component: `ChatMessageInput` `site/src/components/ChatMessageInput/ChatMessageInput.tsx` A Lexical-powered text input that behaves as a plain-text editor with: - **Enter** submits, **Shift+Enter** inserts newline - Rich-text formatting disabled (Cmd+B/I/U blocked) - Paste sanitization (strips formatting, inserts plain text) - Undo/redo via HistoryPlugin - Imperative ref API: `insertText()`, `clear()`, `focus()`, `getValue()` ### Updated components - **`AgentChatInput.tsx`** — Swapped `<TextareaAutosize>` for `<ChatMessageInput>`. Moved from controlled `value`/`onChange` to ref-based pattern with `initialValue`/`onContentChange`. - **`AgentDetail.tsx`** — Updated to use `useRef` for input value tracking and `editorInitialValue` state for editor resets (edit/cancel flows). - **`AgentsPage.tsx`** — Updated to use `useRef` + `initialValue` pattern. - **`AgentChatInput.stories.tsx`** — Updated prop names. ### Why Lexical? This lays the groundwork for features that a native `<textarea>` can't support: - Ghost text / inline autocomplete suggestions - @-mentions and slash commands - Programmatic text insertion (e.g. from speech-to-text) - Custom inline decorators (chips, pills, badges) - Syntax-highlighted code blocks No adornments are added in this PR — it's a drop-in replacement that matches existing behavior. --------- Co-authored-by: Coder <coder@coder.com>
This commit is contained in:
@@ -46,6 +46,8 @@
|
||||
"@fontsource/ibm-plex-mono": "5.2.7",
|
||||
"@fontsource/jetbrains-mono": "5.2.8",
|
||||
"@fontsource/source-code-pro": "5.2.7",
|
||||
"@lexical/react": "0.41.0",
|
||||
"@lexical/utils": "0.41.0",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@mui/material": "5.18.0",
|
||||
"@mui/system": "5.18.0",
|
||||
@@ -90,6 +92,7 @@
|
||||
"front-matter": "4.0.2",
|
||||
"humanize-duration": "3.33.1",
|
||||
"jszip": "3.10.1",
|
||||
"lexical": "0.41.0",
|
||||
"lodash": "4.17.21",
|
||||
"lucide-react": "0.555.0",
|
||||
"monaco-editor": "0.55.1",
|
||||
|
||||
Generated
+329
@@ -52,6 +52,12 @@ importers:
|
||||
'@fontsource/source-code-pro':
|
||||
specifier: 5.2.7
|
||||
version: 5.2.7
|
||||
'@lexical/react':
|
||||
specifier: 0.41.0
|
||||
version: 0.41.0(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(yjs@13.6.29)
|
||||
'@lexical/utils':
|
||||
specifier: 0.41.0
|
||||
version: 0.41.0
|
||||
'@monaco-editor/react':
|
||||
specifier: 4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.2(react@19.2.2))(react@19.2.2)
|
||||
@@ -184,6 +190,9 @@ importers:
|
||||
jszip:
|
||||
specifier: 3.10.1
|
||||
version: 3.10.1
|
||||
lexical:
|
||||
specifier: 0.41.0
|
||||
version: 0.41.0
|
||||
lodash:
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
@@ -1037,15 +1046,33 @@ packages:
|
||||
'@floating-ui/core@1.7.3':
|
||||
resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==, tarball: https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz}
|
||||
|
||||
'@floating-ui/core@1.7.4':
|
||||
resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==, tarball: https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz}
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==, tarball: https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz}
|
||||
|
||||
'@floating-ui/dom@1.7.5':
|
||||
resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==, tarball: https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz}
|
||||
|
||||
'@floating-ui/react-dom@2.1.6':
|
||||
resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==, tarball: https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/react-dom@2.1.7':
|
||||
resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==, tarball: https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/react@0.27.18':
|
||||
resolution: {integrity: sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==, tarball: https://registry.npmjs.org/@floating-ui/react/-/react-0.27.18.tgz}
|
||||
peerDependencies:
|
||||
react: '>=17.0.0'
|
||||
react-dom: '>=17.0.0'
|
||||
|
||||
'@floating-ui/utils@0.2.10':
|
||||
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==, tarball: https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz}
|
||||
|
||||
@@ -1233,6 +1260,80 @@ packages:
|
||||
'@leeoniya/ufuzzy@1.0.10':
|
||||
resolution: {integrity: sha512-OR1yiyN8cKBn5UiHjKHUl0LcrTQt4vZPUpIf96qIIZVLxgd4xyASuRvTZ3tjbWvuyQAMgvKsq61Nwu131YyHnA==, tarball: https://registry.npmjs.org/@leeoniya/ufuzzy/-/ufuzzy-1.0.10.tgz}
|
||||
|
||||
'@lexical/clipboard@0.41.0':
|
||||
resolution: {integrity: sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA==, tarball: https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.41.0.tgz}
|
||||
|
||||
'@lexical/code@0.41.0':
|
||||
resolution: {integrity: sha512-0hoNi1KC9/N3SBOGcOcFqnT0OpwmcRRAhfxTKMGqfCtCvAMzULVwZ8RWc9/NV9bKYESgBTW5D9xkDANP2mspHg==, tarball: https://registry.npmjs.org/@lexical/code/-/code-0.41.0.tgz}
|
||||
|
||||
'@lexical/devtools-core@0.41.0':
|
||||
resolution: {integrity: sha512-FzJtluBhBc8bKS11TUZe72KoZN/hnzIyiiM0SPJAsPwGpoXuM01jqpXQGybWf/1bWB+bmmhOae7O4Nywi/Csuw==, tarball: https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.41.0.tgz}
|
||||
peerDependencies:
|
||||
react: '>=17.x'
|
||||
react-dom: '>=17.x'
|
||||
|
||||
'@lexical/dragon@0.41.0':
|
||||
resolution: {integrity: sha512-gBEqkk8Q6ZPruvDaRcOdF1EK9suCVBODzOCcR+EnoJTaTjfDkCM7pkPAm4w90Wa1wCZEtFHvCfas+jU9MDSumg==, tarball: https://registry.npmjs.org/@lexical/dragon/-/dragon-0.41.0.tgz}
|
||||
|
||||
'@lexical/extension@0.41.0':
|
||||
resolution: {integrity: sha512-sF4SPiP72yXvIGchmmIZ7Yg2XZTxNLOpFEIIzdqG7X/1fa1Ham9P/T7VbrblWpF6Ei5LJtK9JgNVB0hb4l3o1g==, tarball: https://registry.npmjs.org/@lexical/extension/-/extension-0.41.0.tgz}
|
||||
|
||||
'@lexical/hashtag@0.41.0':
|
||||
resolution: {integrity: sha512-tFWM74RW4KU0E/sj2aowfWl26vmLUTp331CgVESnhQKcZBfT40KJYd57HEqBDTfQKn4MUhylQCCA0hbpw6EeFQ==, tarball: https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.41.0.tgz}
|
||||
|
||||
'@lexical/history@0.41.0':
|
||||
resolution: {integrity: sha512-kGoVWsiOn62+RMjRolRa+NXZl8jFwxav6GNDiHH8yzivtoaH8n1SwUfLJELXCzeqzs81HySqD4q30VLJVTGoDg==, tarball: https://registry.npmjs.org/@lexical/history/-/history-0.41.0.tgz}
|
||||
|
||||
'@lexical/html@0.41.0':
|
||||
resolution: {integrity: sha512-3RyZy+H/IDKz2D66rNN/NqYx87xVFrngfEbyu1OWtbY963RUFnopiVHCQvsge/8kT04QSZ7U/DzjVFqeNS6clg==, tarball: https://registry.npmjs.org/@lexical/html/-/html-0.41.0.tgz}
|
||||
|
||||
'@lexical/link@0.41.0':
|
||||
resolution: {integrity: sha512-Rjtx5cGWAkKcnacncbVsZ1TqRnUB2Wm4eEVKpaAEG41+kHgqghzM2P+UGT15yROroxJu8KvAC9ISiYFiU4XE1w==, tarball: https://registry.npmjs.org/@lexical/link/-/link-0.41.0.tgz}
|
||||
|
||||
'@lexical/list@0.41.0':
|
||||
resolution: {integrity: sha512-RXvB+xcbzVoQLGRDOBRCacztG7V+bI95tdoTwl8pz5xvgPtAaRnkZWMDP+yMNzMJZsqEChdtpxbf0NgtMkun6g==, tarball: https://registry.npmjs.org/@lexical/list/-/list-0.41.0.tgz}
|
||||
|
||||
'@lexical/mark@0.41.0':
|
||||
resolution: {integrity: sha512-UO5WVs9uJAYIKHSlYh4Z1gHrBBchTOi21UCYBIZ7eAs4suK84hPzD+3/LAX5CB7ZltL6ke5Sly3FOwNXv/wfpA==, tarball: https://registry.npmjs.org/@lexical/mark/-/mark-0.41.0.tgz}
|
||||
|
||||
'@lexical/markdown@0.41.0':
|
||||
resolution: {integrity: sha512-bzI73JMXpjGFhqUWNV6KqfjWcgAWzwFT+J3RHtbCF5rysC8HLldBYojOgAAtPfXqfxyv2mDzsY7SoJ75s9uHZA==, tarball: https://registry.npmjs.org/@lexical/markdown/-/markdown-0.41.0.tgz}
|
||||
|
||||
'@lexical/offset@0.41.0':
|
||||
resolution: {integrity: sha512-2RHBXZqC8gm3X9C0AyRb0M8w7zJu5dKiasrif+jSKzsxPjAUeF1m95OtIOsWs1XLNUgASOSUqGovDZxKJslZfA==, tarball: https://registry.npmjs.org/@lexical/offset/-/offset-0.41.0.tgz}
|
||||
|
||||
'@lexical/overflow@0.41.0':
|
||||
resolution: {integrity: sha512-Iy6ZiJip8X14EBYt1zKPOrXyQ4eG9JLBEoPoSVBTiSbVd+lYicdUvaOThT0k0/qeVTN9nqTaEltBjm56IrVKCQ==, tarball: https://registry.npmjs.org/@lexical/overflow/-/overflow-0.41.0.tgz}
|
||||
|
||||
'@lexical/plain-text@0.41.0':
|
||||
resolution: {integrity: sha512-HIsGgmFUYRUNNyvckun33UQfU7LRzDlxymHUq67+Bxd5bXqdZOrStEKJXuDX+LuLh/GXZbaWNbDLqwLBObfbQg==, tarball: https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.41.0.tgz}
|
||||
|
||||
'@lexical/react@0.41.0':
|
||||
resolution: {integrity: sha512-7+GUdZUm6sofWm+zdsWAs6cFBwKNsvsHezZTrf6k8jrZxL461ZQmbz/16b4DvjCGL9r5P1fR7md9/LCmk8TiCg==, tarball: https://registry.npmjs.org/@lexical/react/-/react-0.41.0.tgz}
|
||||
peerDependencies:
|
||||
react: '>=17.x'
|
||||
react-dom: '>=17.x'
|
||||
|
||||
'@lexical/rich-text@0.41.0':
|
||||
resolution: {integrity: sha512-yUcr7ZaaVTZNi8bow4CK1M8jy2qyyls1Vr+5dVjwBclVShOL/F/nFyzBOSb6RtXXRbd3Ahuk9fEleppX/RNIdw==, tarball: https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.41.0.tgz}
|
||||
|
||||
'@lexical/selection@0.41.0':
|
||||
resolution: {integrity: sha512-1s7/kNyRzcv5uaTwsUL28NpiisqTf5xZ1zNukLsCN1xY+TWbv9RE9OxIv+748wMm4pxNczQe/UbIBODkbeknLw==, tarball: https://registry.npmjs.org/@lexical/selection/-/selection-0.41.0.tgz}
|
||||
|
||||
'@lexical/table@0.41.0':
|
||||
resolution: {integrity: sha512-d3SPThBAr+oZ8O74TXU0iXM3rLbrAVC7/HcOnSAq7/AhWQW8yMutT51JQGN+0fMLP9kqoWSAojNtkdvzXfU/+A==, tarball: https://registry.npmjs.org/@lexical/table/-/table-0.41.0.tgz}
|
||||
|
||||
'@lexical/text@0.41.0':
|
||||
resolution: {integrity: sha512-gGA+Anc7ck110EXo4KVKtq6Ui3M7Vz3OpGJ4QE6zJHWW8nV5h273koUGSutAMeoZgRVb6t01Izh3ORoFt/j1CA==, tarball: https://registry.npmjs.org/@lexical/text/-/text-0.41.0.tgz}
|
||||
|
||||
'@lexical/utils@0.41.0':
|
||||
resolution: {integrity: sha512-Wlsokr5NQCq83D+7kxZ9qs5yQ3dU3Qaf2M+uXxLRoPoDaXqW8xTWZq1+ZFoEzsHzx06QoPa4Vu/40BZR91uQPg==, tarball: https://registry.npmjs.org/@lexical/utils/-/utils-0.41.0.tgz}
|
||||
|
||||
'@lexical/yjs@0.41.0':
|
||||
resolution: {integrity: sha512-PaKTxSbVC4fpqUjQ7vUL9RkNF1PjL8TFl5jRe03PqoPYpE33buf3VXX6+cOUEfv9+uknSqLCPHoBS/4jN3a97w==, tarball: https://registry.npmjs.org/@lexical/yjs/-/yjs-0.41.0.tgz}
|
||||
peerDependencies:
|
||||
yjs: '>=13.5.22'
|
||||
|
||||
'@mdx-js/react@3.1.1':
|
||||
resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==, tarball: https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz}
|
||||
peerDependencies:
|
||||
@@ -1507,6 +1608,9 @@ packages:
|
||||
'@popperjs/core@2.11.8':
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==, tarball: https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz}
|
||||
|
||||
'@preact/signals-core@1.13.0':
|
||||
resolution: {integrity: sha512-slT6XeTCAbdql61GVLlGU4x7XHI7kCZV5Um5uhE4zLX4ApgiiXc0UYFvVOKq06xcovzp7p+61l68oPi563ARKg==, tarball: https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.13.0.tgz}
|
||||
|
||||
'@protobufjs/aspromise@1.1.2':
|
||||
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==, tarball: https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz}
|
||||
|
||||
@@ -4266,6 +4370,9 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz}
|
||||
|
||||
isomorphic.js@0.2.5:
|
||||
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==, tarball: https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz}
|
||||
|
||||
istanbul-lib-coverage@3.2.2:
|
||||
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==, tarball: https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4568,6 +4675,14 @@ packages:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, tarball: https://registry.npmjs.org/levn/-/levn-0.4.1.tgz}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
lexical@0.41.0:
|
||||
resolution: {integrity: sha512-pNIm5+n+hVnJHB9gYPDYsIO5Y59dNaDU9rJmPPsfqQhP2ojKFnUoPbcRnrI9FJLXB14sSumcY8LUw7Sq70TZqA==, tarball: https://registry.npmjs.org/lexical/-/lexical-0.41.0.tgz}
|
||||
|
||||
lib0@0.2.117:
|
||||
resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==, tarball: https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==, tarball: https://registry.npmjs.org/lie/-/lie-3.3.0.tgz}
|
||||
|
||||
@@ -5329,6 +5444,11 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^19.2.2
|
||||
|
||||
react-error-boundary@6.1.1:
|
||||
resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==, tarball: https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.1.tgz}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
|
||||
react-fast-compare@2.0.4:
|
||||
resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==, tarball: https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz}
|
||||
|
||||
@@ -5891,6 +6011,9 @@ packages:
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==, tarball: https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz}
|
||||
|
||||
tabbable@6.4.0:
|
||||
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==, tarball: https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz}
|
||||
|
||||
tailwind-merge@2.6.0:
|
||||
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==, tarball: https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz}
|
||||
|
||||
@@ -6499,6 +6622,10 @@ packages:
|
||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==, tarball: https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yjs@13.6.29:
|
||||
resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==, tarball: https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz}
|
||||
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
|
||||
|
||||
yn@3.1.1:
|
||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==, tarball: https://registry.npmjs.org/yn/-/yn-3.1.1.tgz}
|
||||
engines: {node: '>=6'}
|
||||
@@ -7066,17 +7193,40 @@ snapshots:
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/core@1.7.4':
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.7.4':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.3
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/dom@1.7.5':
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.7.4
|
||||
'@floating-ui/utils': 0.2.10
|
||||
|
||||
'@floating-ui/react-dom@2.1.6(react-dom@19.2.2(react@19.2.2))(react@19.2.2)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.4
|
||||
react: 19.2.2
|
||||
react-dom: 19.2.2(react@19.2.2)
|
||||
|
||||
'@floating-ui/react-dom@2.1.7(react-dom@19.2.2(react@19.2.2))(react@19.2.2)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.5
|
||||
react: 19.2.2
|
||||
react-dom: 19.2.2(react@19.2.2)
|
||||
|
||||
'@floating-ui/react@0.27.18(react-dom@19.2.2(react@19.2.2))(react@19.2.2)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.1.7(react-dom@19.2.2(react@19.2.2))(react@19.2.2)
|
||||
'@floating-ui/utils': 0.2.10
|
||||
react: 19.2.2
|
||||
react-dom: 19.2.2(react@19.2.2)
|
||||
tabbable: 6.4.0
|
||||
|
||||
'@floating-ui/utils@0.2.10': {}
|
||||
|
||||
'@fontsource-variable/geist@5.2.8': {}
|
||||
@@ -7390,6 +7540,165 @@ snapshots:
|
||||
|
||||
'@leeoniya/ufuzzy@1.0.10': {}
|
||||
|
||||
'@lexical/clipboard@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/html': 0.41.0
|
||||
'@lexical/list': 0.41.0
|
||||
'@lexical/selection': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/code@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
prismjs: 1.30.0
|
||||
|
||||
'@lexical/devtools-core@0.41.0(react-dom@19.2.2(react@19.2.2))(react@19.2.2)':
|
||||
dependencies:
|
||||
'@lexical/html': 0.41.0
|
||||
'@lexical/link': 0.41.0
|
||||
'@lexical/mark': 0.41.0
|
||||
'@lexical/table': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
react: 19.2.2
|
||||
react-dom: 19.2.2(react@19.2.2)
|
||||
|
||||
'@lexical/dragon@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/extension': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/extension@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/utils': 0.41.0
|
||||
'@preact/signals-core': 1.13.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/hashtag@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/text': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/history@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/extension': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/html@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/selection': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/link@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/extension': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/list@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/extension': 0.41.0
|
||||
'@lexical/selection': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/mark@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/markdown@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/code': 0.41.0
|
||||
'@lexical/link': 0.41.0
|
||||
'@lexical/list': 0.41.0
|
||||
'@lexical/rich-text': 0.41.0
|
||||
'@lexical/text': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/offset@0.41.0':
|
||||
dependencies:
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/overflow@0.41.0':
|
||||
dependencies:
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/plain-text@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/clipboard': 0.41.0
|
||||
'@lexical/dragon': 0.41.0
|
||||
'@lexical/selection': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/react@0.41.0(react-dom@19.2.2(react@19.2.2))(react@19.2.2)(yjs@13.6.29)':
|
||||
dependencies:
|
||||
'@floating-ui/react': 0.27.18(react-dom@19.2.2(react@19.2.2))(react@19.2.2)
|
||||
'@lexical/devtools-core': 0.41.0(react-dom@19.2.2(react@19.2.2))(react@19.2.2)
|
||||
'@lexical/dragon': 0.41.0
|
||||
'@lexical/extension': 0.41.0
|
||||
'@lexical/hashtag': 0.41.0
|
||||
'@lexical/history': 0.41.0
|
||||
'@lexical/link': 0.41.0
|
||||
'@lexical/list': 0.41.0
|
||||
'@lexical/mark': 0.41.0
|
||||
'@lexical/markdown': 0.41.0
|
||||
'@lexical/overflow': 0.41.0
|
||||
'@lexical/plain-text': 0.41.0
|
||||
'@lexical/rich-text': 0.41.0
|
||||
'@lexical/table': 0.41.0
|
||||
'@lexical/text': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
'@lexical/yjs': 0.41.0(yjs@13.6.29)
|
||||
lexical: 0.41.0
|
||||
react: 19.2.2
|
||||
react-dom: 19.2.2(react@19.2.2)
|
||||
react-error-boundary: 6.1.1(react@19.2.2)
|
||||
transitivePeerDependencies:
|
||||
- yjs
|
||||
|
||||
'@lexical/rich-text@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/clipboard': 0.41.0
|
||||
'@lexical/dragon': 0.41.0
|
||||
'@lexical/selection': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/selection@0.41.0':
|
||||
dependencies:
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/table@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/clipboard': 0.41.0
|
||||
'@lexical/extension': 0.41.0
|
||||
'@lexical/utils': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/text@0.41.0':
|
||||
dependencies:
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/utils@0.41.0':
|
||||
dependencies:
|
||||
'@lexical/selection': 0.41.0
|
||||
lexical: 0.41.0
|
||||
|
||||
'@lexical/yjs@0.41.0(yjs@13.6.29)':
|
||||
dependencies:
|
||||
'@lexical/offset': 0.41.0
|
||||
'@lexical/selection': 0.41.0
|
||||
lexical: 0.41.0
|
||||
yjs: 13.6.29
|
||||
|
||||
'@mdx-js/react@3.1.1(@types/react@19.2.7)(react@19.2.2)':
|
||||
dependencies:
|
||||
'@types/mdx': 2.0.13
|
||||
@@ -7645,6 +7954,8 @@ snapshots:
|
||||
|
||||
'@popperjs/core@2.11.8': {}
|
||||
|
||||
'@preact/signals-core@1.13.0': {}
|
||||
|
||||
'@protobufjs/aspromise@1.1.2': {}
|
||||
|
||||
'@protobufjs/base64@1.1.2': {}
|
||||
@@ -10555,6 +10866,8 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
isomorphic.js@0.2.5: {}
|
||||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
|
||||
istanbul-lib-instrument@5.2.1:
|
||||
@@ -11128,6 +11441,12 @@ snapshots:
|
||||
type-check: 0.4.0
|
||||
optional: true
|
||||
|
||||
lexical@0.41.0: {}
|
||||
|
||||
lib0@0.2.117:
|
||||
dependencies:
|
||||
isomorphic.js: 0.2.5
|
||||
|
||||
lie@3.3.0:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
@@ -12094,6 +12413,10 @@ snapshots:
|
||||
react: 19.2.2
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-error-boundary@6.1.1(react@19.2.2):
|
||||
dependencies:
|
||||
react: 19.2.2
|
||||
|
||||
react-fast-compare@2.0.4: {}
|
||||
|
||||
react-inspector@6.0.2(react@19.2.2):
|
||||
@@ -12762,6 +13085,8 @@ snapshots:
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
tabbable@6.4.0: {}
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
|
||||
tailwind-merge@3.4.1: {}
|
||||
@@ -13330,6 +13655,10 @@ snapshots:
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
yjs@13.6.29:
|
||||
dependencies:
|
||||
lib0: 0.2.117
|
||||
|
||||
yn@3.1.1:
|
||||
optional: true
|
||||
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
||||
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
||||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
||||
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
||||
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
|
||||
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
||||
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
||||
import { mergeRegister } from "@lexical/utils";
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$createTextNode,
|
||||
$getRoot,
|
||||
$getSelection,
|
||||
$insertNodes,
|
||||
$isRangeSelection,
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
FORMAT_ELEMENT_COMMAND,
|
||||
FORMAT_TEXT_COMMAND,
|
||||
KEY_ENTER_COMMAND,
|
||||
type LexicalEditor,
|
||||
PASTE_COMMAND,
|
||||
type ParagraphNode,
|
||||
} from "lexical";
|
||||
import {
|
||||
type FC,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
// Blocks Cmd+B/I/U and element formatting shortcuts so the editor
|
||||
// stays plain-text only.
|
||||
const DisableFormattingPlugin: FC = memo(function DisableFormattingPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerCommand(
|
||||
FORMAT_TEXT_COMMAND,
|
||||
() => true,
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
),
|
||||
editor.registerCommand(
|
||||
FORMAT_ELEMENT_COMMAND,
|
||||
() => true,
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
),
|
||||
);
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
// Intercepts paste events and inserts clipboard content as plain text,
|
||||
// stripping any rich-text formatting.
|
||||
const PasteSanitizationPlugin: FC = memo(function PasteSanitizationPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
PASTE_COMMAND,
|
||||
(event: ClipboardEvent | null) => {
|
||||
if (!event) return false;
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) return false;
|
||||
|
||||
const text = clipboardData.getData("text/plain");
|
||||
if (!text) return false;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
selection.insertText(text);
|
||||
} else {
|
||||
const root = $getRoot();
|
||||
const lastChild = root.getLastChild();
|
||||
if (lastChild) {
|
||||
if (lastChild.getType() === "paragraph") {
|
||||
const paragraph = lastChild as ParagraphNode;
|
||||
const textNode = $createTextNode(text);
|
||||
paragraph.append(textNode);
|
||||
textNode.selectEnd();
|
||||
} else {
|
||||
const textNode = $createTextNode(text);
|
||||
lastChild.insertAfter(textNode);
|
||||
textNode.selectEnd();
|
||||
}
|
||||
} else {
|
||||
const paragraph = $createParagraphNode();
|
||||
const textNode = $createTextNode(text);
|
||||
paragraph.append(textNode);
|
||||
root.append(paragraph);
|
||||
textNode.selectEnd();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
);
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
// Handles Enter key behavior: plain Enter submits via the onEnter
|
||||
// callback, Shift+Enter inserts a newline.
|
||||
const EnterKeyPlugin: FC<{ onEnter?: () => void }> = memo(
|
||||
function EnterKeyPlugin({ onEnter }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
KEY_ENTER_COMMAND,
|
||||
(event: KeyboardEvent | null) => {
|
||||
if (event?.shiftKey) {
|
||||
return false;
|
||||
}
|
||||
if (onEnter) {
|
||||
event?.preventDefault();
|
||||
onEnter();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
);
|
||||
}, [editor, onEnter]);
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
// Fires the onChange callback with the editor's plain-text content
|
||||
// on every update.
|
||||
const ContentChangePlugin: FC<{
|
||||
onChange?: (content: string) => void;
|
||||
}> = memo(function ContentChangePlugin({ onChange }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!onChange) return;
|
||||
|
||||
return editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
const root = $getRoot();
|
||||
const content = root.getTextContent();
|
||||
onChange(content);
|
||||
});
|
||||
});
|
||||
}, [editor, onChange]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
// Seeds the editor with an initial value on first mount.
|
||||
const ValueSyncPlugin: FC<{ initialValue?: string }> = memo(
|
||||
function ValueSyncPlugin({ initialValue }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current && initialValue !== undefined) {
|
||||
hasInitialized.current = true;
|
||||
|
||||
if (initialValue === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
root.clear();
|
||||
const paragraph = $createParagraphNode();
|
||||
const textNode = $createTextNode(initialValue);
|
||||
paragraph.append(textNode);
|
||||
root.append(paragraph);
|
||||
});
|
||||
}
|
||||
}, [editor, initialValue]);
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
// Exposes the LexicalEditor instance to the parent via a callback
|
||||
// so it can be stored in a ref for imperative access.
|
||||
const InsertTextPlugin: FC<{
|
||||
onEditorReady: (editor: LexicalEditor) => void;
|
||||
}> = memo(function InsertTextPlugin({ onEditorReady }) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
onEditorReady(editor);
|
||||
}, [editor, onEditorReady]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export interface ChatMessageInputRef {
|
||||
insertText: (text: string) => void;
|
||||
clear: () => void;
|
||||
focus: () => void;
|
||||
getValue: () => string;
|
||||
}
|
||||
|
||||
interface ChatMessageInputProps
|
||||
extends Omit<React.ComponentProps<"div">, "onChange" | "role" | "ref"> {
|
||||
placeholder?: string;
|
||||
initialValue?: string;
|
||||
onChange?: (content: string) => void;
|
||||
rows?: number;
|
||||
onEnter?: () => void;
|
||||
disabled?: boolean;
|
||||
autoFocus?: boolean;
|
||||
"aria-label"?: string;
|
||||
}
|
||||
|
||||
const ChatMessageInput = memo(
|
||||
({
|
||||
className,
|
||||
placeholder,
|
||||
initialValue,
|
||||
onChange,
|
||||
rows,
|
||||
onEnter,
|
||||
disabled,
|
||||
autoFocus,
|
||||
"aria-label": ariaLabel,
|
||||
ref,
|
||||
...props
|
||||
}: ChatMessageInputProps & { ref?: React.Ref<ChatMessageInputRef> }) => {
|
||||
const initialConfig = useMemo(
|
||||
() => ({
|
||||
namespace: "ChatMessageInput",
|
||||
theme: {
|
||||
paragraph: "m-0",
|
||||
},
|
||||
onError: (error: Error) => console.error("Lexical error:", error),
|
||||
nodes: [],
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
minHeight: rows ? `${rows * 1.5}rem` : undefined,
|
||||
}),
|
||||
[rows],
|
||||
);
|
||||
|
||||
const editorRef = useRef<LexicalEditor | null>(null);
|
||||
|
||||
const handleEditorReady = useCallback((editor: LexicalEditor) => {
|
||||
editorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
const handleContentChange = useCallback(
|
||||
(content: string) => {
|
||||
onChange?.(content);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
insertText: (text: string) => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
const textNode = $createTextNode(text);
|
||||
$insertNodes([textNode]);
|
||||
textNode.selectEnd();
|
||||
} else {
|
||||
const root = $getRoot();
|
||||
const lastChild = root.getLastChild();
|
||||
if (lastChild) {
|
||||
if (lastChild.getType() === "paragraph") {
|
||||
const paragraph = lastChild as ParagraphNode;
|
||||
const textNode = $createTextNode(text);
|
||||
paragraph.append(textNode);
|
||||
textNode.selectEnd();
|
||||
} else {
|
||||
const textNode = $createTextNode(text);
|
||||
lastChild.insertAfter(textNode);
|
||||
textNode.selectEnd();
|
||||
}
|
||||
} else {
|
||||
const paragraph = $createParagraphNode();
|
||||
const textNode = $createTextNode(text);
|
||||
paragraph.append(textNode);
|
||||
root.append(paragraph);
|
||||
textNode.selectEnd();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
clear: () => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
root.clear();
|
||||
const paragraph = $createParagraphNode();
|
||||
root.append(paragraph);
|
||||
paragraph.select();
|
||||
});
|
||||
},
|
||||
focus: () => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
editor.focus(() => {
|
||||
editor.update(() => {
|
||||
const root = $getRoot();
|
||||
const last = root.getLastChild();
|
||||
if (!last) {
|
||||
const paragraph = $createParagraphNode();
|
||||
root.append(paragraph);
|
||||
paragraph.select();
|
||||
return;
|
||||
}
|
||||
last.selectEnd();
|
||||
});
|
||||
});
|
||||
},
|
||||
getValue: () => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return "";
|
||||
let content = "";
|
||||
editor.getEditorState().read(() => {
|
||||
content = $getRoot().getTextContent();
|
||||
});
|
||||
return content;
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={initialConfig} key={initialValue}>
|
||||
<div
|
||||
className={cn(
|
||||
"grid w-full rounded-md bg-transparent text-base placeholder:text-content-secondary focus-visible:outline-none whitespace-pre-wrap break-words [&>*]:col-start-1 [&>*]:row-start-1",
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
className="outline-none w-full whitespace-pre-wrap [&_p]:leading-normal [&_p:first-child]:mt-0 [&_p:last-child]:mb-0"
|
||||
data-testid="chat-message-input"
|
||||
style={{ minHeight: "inherit" }}
|
||||
aria-label={ariaLabel}
|
||||
aria-disabled={disabled}
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
<div className="pointer-events-none text-content-secondary [&_p]:leading-normal">
|
||||
{placeholder}
|
||||
</div>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<HistoryPlugin />
|
||||
<DisableFormattingPlugin />
|
||||
<PasteSanitizationPlugin />
|
||||
<EnterKeyPlugin onEnter={disabled ? undefined : onEnter} />
|
||||
<ContentChangePlugin onChange={handleContentChange} />
|
||||
<ValueSyncPlugin initialValue={initialValue} />
|
||||
<InsertTextPlugin onEditorReady={handleEditorReady} />
|
||||
{autoFocus && <AutoFocusPlugin />}
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChatMessageInput.displayName = "ChatMessageInput";
|
||||
|
||||
export { ChatMessageInput };
|
||||
@@ -16,9 +16,9 @@ const meta: Meta<typeof AgentChatInput> = {
|
||||
component: AgentChatInput,
|
||||
args: {
|
||||
onSend: fn(),
|
||||
onChange: fn(),
|
||||
onContentChange: fn(),
|
||||
onModelChange: fn(),
|
||||
value: "",
|
||||
initialValue: "",
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
selectedModel: defaultModelOptions[0].id,
|
||||
@@ -47,12 +47,24 @@ export const DisablesSendUntilInput: Story = {
|
||||
export const SendsAndClearsInput: Story = {
|
||||
args: {
|
||||
onSend: fn(),
|
||||
value: "Run focused tests",
|
||||
initialValue: "Run focused tests",
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Send" }));
|
||||
// Wait for the Lexical editor to initialize and render the
|
||||
// initial value text into the DOM before interacting.
|
||||
const editor = canvas.getByTestId("chat-message-input");
|
||||
await waitFor(() => {
|
||||
expect(editor.textContent).toBe("Run focused tests");
|
||||
});
|
||||
|
||||
const sendButton = canvas.getByRole("button", { name: "Send" });
|
||||
await waitFor(() => {
|
||||
expect(sendButton).toBeEnabled();
|
||||
});
|
||||
|
||||
await userEvent.click(sendButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onSend).toHaveBeenCalledWith("Run focused tests");
|
||||
@@ -63,7 +75,7 @@ export const SendsAndClearsInput: Story = {
|
||||
export const DisabledInput: Story = {
|
||||
args: {
|
||||
isDisabled: true,
|
||||
value: "Should not send",
|
||||
initialValue: "Should not send",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -75,7 +87,7 @@ export const NoModelOptions: Story = {
|
||||
args: {
|
||||
isDisabled: false,
|
||||
hasModelOptions: false,
|
||||
value: "Model required",
|
||||
initialValue: "Model required",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -87,7 +99,7 @@ export const LoadingSpinner: Story = {
|
||||
args: {
|
||||
isDisabled: true,
|
||||
isLoading: true,
|
||||
value: "Sending...",
|
||||
initialValue: "Sending...",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -103,7 +115,7 @@ export const LoadingDisablesSend: Story = {
|
||||
args: {
|
||||
isDisabled: false,
|
||||
isLoading: true,
|
||||
value: "Another message",
|
||||
initialValue: "Another message",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
type ModelSelectorOption,
|
||||
} from "components/ai-elements";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
ChatMessageInput,
|
||||
type ChatMessageInputRef,
|
||||
} from "components/ChatMessageInput/ChatMessageInput";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -16,12 +20,13 @@ import {
|
||||
Square,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { memo, type ReactNode, useRef } from "react";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { memo, type ReactNode, useCallback, useRef, useState } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
import { formatProviderLabel } from "./modelOptions";
|
||||
import { QueuedMessagesList } from "./QueuedMessagesList";
|
||||
|
||||
export type { ChatMessageInputRef } from "components/ChatMessageInput/ChatMessageInput";
|
||||
|
||||
export interface AgentContextUsage {
|
||||
readonly usedTokens?: number;
|
||||
readonly contextLimitTokens?: number;
|
||||
@@ -39,9 +44,12 @@ interface AgentChatInputProps {
|
||||
placeholder?: string;
|
||||
isDisabled: boolean;
|
||||
isLoading: boolean;
|
||||
// Controlled input value. The parent owns the text state.
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
// Ref for the Lexical editor, exposed for imperative access.
|
||||
inputRef?: React.Ref<ChatMessageInputRef>;
|
||||
// Initial text to seed the editor with.
|
||||
initialValue?: string;
|
||||
// Called on every text change inside the editor.
|
||||
onContentChange?: (content: string) => void;
|
||||
// Model selector.
|
||||
selectedModel: string;
|
||||
onModelChange: (value: string) => void;
|
||||
@@ -208,8 +216,9 @@ export const AgentChatInput = memo<AgentChatInputProps>(
|
||||
placeholder = "Type a message...",
|
||||
isDisabled,
|
||||
isLoading,
|
||||
value,
|
||||
onChange,
|
||||
inputRef,
|
||||
initialValue,
|
||||
onContentChange,
|
||||
selectedModel,
|
||||
onModelChange,
|
||||
modelOptions,
|
||||
@@ -232,18 +241,71 @@ export const AgentChatInput = memo<AgentChatInputProps>(
|
||||
contextUsage,
|
||||
sticky = false,
|
||||
}) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const internalRef = useRef<ChatMessageInputRef>(null);
|
||||
|
||||
const canSend =
|
||||
!isDisabled && !isLoading && hasModelOptions && !!value.trim();
|
||||
// Merge the external inputRef with our internal ref so both
|
||||
// point to the same ChatMessageInputRef instance.
|
||||
const setRef = useCallback(
|
||||
(instance: ChatMessageInputRef | null) => {
|
||||
(
|
||||
internalRef as React.MutableRefObject<ChatMessageInputRef | null>
|
||||
).current = instance;
|
||||
if (typeof inputRef === "function") {
|
||||
inputRef(instance);
|
||||
} else if (inputRef && typeof inputRef === "object") {
|
||||
(
|
||||
inputRef as React.MutableRefObject<ChatMessageInputRef | null>
|
||||
).current = instance;
|
||||
}
|
||||
},
|
||||
[inputRef],
|
||||
);
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!canSend) {
|
||||
// Track whether the editor has content so we can gate the
|
||||
// send button without a controlled value prop.
|
||||
const [hasContent, setHasContent] = useState(() => !!initialValue?.trim());
|
||||
|
||||
const handleContentChange = useCallback(
|
||||
(content: string) => {
|
||||
setHasContent(!!content.trim());
|
||||
onContentChange?.(content);
|
||||
},
|
||||
[onContentChange],
|
||||
);
|
||||
|
||||
const canSend = !isDisabled && !isLoading && hasModelOptions && hasContent;
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const text = internalRef.current?.getValue()?.trim() ?? "";
|
||||
|
||||
// If the input is empty and there are queued messages,
|
||||
// promote the first one instead of submitting.
|
||||
if (
|
||||
!text &&
|
||||
!isDisabled &&
|
||||
!isLoading &&
|
||||
queuedMessages.length > 0 &&
|
||||
onPromoteQueuedMessage
|
||||
) {
|
||||
void onPromoteQueuedMessage(queuedMessages[0].id);
|
||||
return;
|
||||
}
|
||||
onSend(value.trim());
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
if (!text || isDisabled || isLoading || !hasModelOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSend(text);
|
||||
internalRef.current?.clear();
|
||||
internalRef.current?.focus();
|
||||
}, [
|
||||
isDisabled,
|
||||
isLoading,
|
||||
hasModelOptions,
|
||||
onSend,
|
||||
queuedMessages,
|
||||
onPromoteQueuedMessage,
|
||||
]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
@@ -257,23 +319,6 @@ export const AgentChatInput = memo<AgentChatInputProps>(
|
||||
e.preventDefault();
|
||||
onInterrupt();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
// If the input is empty and there are queued messages,
|
||||
// promote the first one instead of submitting.
|
||||
if (
|
||||
!value.trim() &&
|
||||
!isDisabled &&
|
||||
!isLoading &&
|
||||
queuedMessages.length > 0 &&
|
||||
onPromoteQueuedMessage
|
||||
) {
|
||||
void onPromoteQueuedMessage(queuedMessages[0].id);
|
||||
return;
|
||||
}
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -302,7 +347,10 @@ export const AgentChatInput = memo<AgentChatInputProps>(
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
<div className="rounded-2xl border border-border-default/80 bg-surface-secondary/45 p-1 shadow-sm has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-content-link/40">
|
||||
<div
|
||||
className="rounded-2xl border border-border-default/80 bg-surface-secondary/45 p-1 shadow-sm has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-content-link/40"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{editingQueuedMessageID !== null && (
|
||||
<div className="flex items-center justify-between border-b border-border-default/70 bg-surface-primary/25 px-3 py-1.5">
|
||||
<span className="text-sm text-content-secondary">
|
||||
@@ -340,16 +388,17 @@ export const AgentChatInput = memo<AgentChatInputProps>(
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<TextareaAutosize
|
||||
ref={textareaRef}
|
||||
<ChatMessageInput
|
||||
ref={setRef}
|
||||
aria-label="Chat message"
|
||||
className="min-h-[120px] w-full resize-none border-none bg-transparent px-3 py-2 font-sans text-[15px] leading-6 text-content-primary outline-none placeholder:text-content-secondary disabled:cursor-not-allowed disabled:opacity-70"
|
||||
className="min-h-[120px] w-full resize-none bg-transparent px-3 py-2 font-sans text-[15px] leading-6 text-content-primary placeholder:text-content-secondary disabled:cursor-not-allowed disabled:opacity-70"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
initialValue={initialValue}
|
||||
onChange={handleContentChange}
|
||||
onEnter={handleSubmit}
|
||||
disabled={isDisabled}
|
||||
minRows={4}
|
||||
rows={4}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2 px-2.5 pb-1.5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
|
||||
@@ -29,7 +29,7 @@ import { useNavigate, useOutletContext, useParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "utils/cn";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { AgentChatInput } from "./AgentChatInput";
|
||||
import { AgentChatInput, type ChatMessageInputRef } from "./AgentChatInput";
|
||||
import {
|
||||
selectChatStatus,
|
||||
selectHasStreamState,
|
||||
@@ -204,8 +204,9 @@ interface AgentDetailInputProps {
|
||||
modelCatalogStatusMessage: string | null;
|
||||
// Controlled input value and editing state, owned by the
|
||||
// conversation component.
|
||||
inputValue: string;
|
||||
onInputChange: (value: string) => void;
|
||||
inputRef?: React.Ref<ChatMessageInputRef>;
|
||||
initialValue?: string;
|
||||
onContentChange?: (content: string) => void;
|
||||
editingQueuedMessageID: number | null;
|
||||
onStartQueueEdit: (id: number, text: string) => void;
|
||||
onCancelQueueEdit: () => void;
|
||||
@@ -230,8 +231,9 @@ const AgentDetailInput: FC<AgentDetailInputProps> = ({
|
||||
modelSelectorPlaceholder,
|
||||
inputStatusText,
|
||||
modelCatalogStatusMessage,
|
||||
inputValue,
|
||||
onInputChange,
|
||||
inputRef,
|
||||
initialValue,
|
||||
onContentChange,
|
||||
editingQueuedMessageID,
|
||||
onStartQueueEdit,
|
||||
onCancelQueueEdit,
|
||||
@@ -264,8 +266,9 @@ const AgentDetailInput: FC<AgentDetailInputProps> = ({
|
||||
return (
|
||||
<AgentChatInput
|
||||
onSend={onSend}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
inputRef={inputRef}
|
||||
initialValue={initialValue}
|
||||
onContentChange={onContentChange}
|
||||
queuedMessages={queuedMessages}
|
||||
onDeleteQueuedMessage={onDeleteQueuedMessage}
|
||||
onPromoteQueuedMessage={onPromoteQueuedMessage}
|
||||
@@ -335,7 +338,9 @@ const AgentDetailConversation: FC<AgentDetailConversationProps> = ({
|
||||
modelCatalogStatusMessage,
|
||||
savingMessageId,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const inputValueRef = useRef("");
|
||||
const chatInputRef = useRef<ChatMessageInputRef>(null);
|
||||
const [editorInitialValue, setEditorInitialValue] = useState("");
|
||||
|
||||
// -- History editing state --
|
||||
const [editingMessageId, setEditingMessageId] = useState<number | null>(null);
|
||||
@@ -346,16 +351,18 @@ const AgentDetailConversation: FC<AgentDetailConversationProps> = ({
|
||||
const handleEditUserMessage = useCallback(
|
||||
(messageId: number, text: string) => {
|
||||
setDraftBeforeHistoryEdit((prev) =>
|
||||
editingMessageId !== null ? prev : inputValue,
|
||||
editingMessageId !== null ? prev : inputValueRef.current,
|
||||
);
|
||||
setEditingMessageId(messageId);
|
||||
setInputValue(text);
|
||||
setEditorInitialValue(text);
|
||||
inputValueRef.current = text;
|
||||
},
|
||||
[editingMessageId, inputValue],
|
||||
[editingMessageId],
|
||||
);
|
||||
|
||||
const handleCancelHistoryEdit = useCallback(() => {
|
||||
setInputValue(draftBeforeHistoryEdit ?? "");
|
||||
setEditorInitialValue(draftBeforeHistoryEdit ?? "");
|
||||
inputValueRef.current = draftBeforeHistoryEdit ?? "";
|
||||
setEditingMessageId(null);
|
||||
setDraftBeforeHistoryEdit(null);
|
||||
}, [draftBeforeHistoryEdit]);
|
||||
@@ -371,16 +378,18 @@ const AgentDetailConversation: FC<AgentDetailConversationProps> = ({
|
||||
const handleStartQueueEdit = useCallback(
|
||||
(id: number, text: string) => {
|
||||
setDraftBeforeQueueEdit((prev) =>
|
||||
editingQueuedMessageID === null ? inputValue : prev,
|
||||
editingQueuedMessageID === null ? inputValueRef.current : prev,
|
||||
);
|
||||
setEditingQueuedMessageID(id);
|
||||
setInputValue(text);
|
||||
setEditorInitialValue(text);
|
||||
inputValueRef.current = text;
|
||||
},
|
||||
[editingQueuedMessageID, inputValue],
|
||||
[editingQueuedMessageID],
|
||||
);
|
||||
|
||||
const handleCancelQueueEdit = useCallback(() => {
|
||||
setInputValue(draftBeforeQueueEdit ?? "");
|
||||
setEditorInitialValue(draftBeforeQueueEdit ?? "");
|
||||
inputValueRef.current = draftBeforeQueueEdit ?? "";
|
||||
setEditingQueuedMessageID(null);
|
||||
setDraftBeforeQueueEdit(null);
|
||||
}, [draftBeforeQueueEdit]);
|
||||
@@ -394,7 +403,8 @@ const AgentDetailConversation: FC<AgentDetailConversationProps> = ({
|
||||
const queueEditID = editingQueuedMessageID;
|
||||
|
||||
// Clear input and editing state optimistically.
|
||||
setInputValue("");
|
||||
setEditorInitialValue("");
|
||||
inputValueRef.current = "";
|
||||
if (editingMessageId !== null) {
|
||||
setEditingMessageId(null);
|
||||
setDraftBeforeHistoryEdit(null);
|
||||
@@ -412,7 +422,8 @@ const AgentDetailConversation: FC<AgentDetailConversationProps> = ({
|
||||
})
|
||||
.catch(() => {
|
||||
// Restore input so the user can retry.
|
||||
setInputValue(message);
|
||||
setEditorInitialValue(message);
|
||||
inputValueRef.current = message;
|
||||
});
|
||||
},
|
||||
[editingMessageId, editingQueuedMessageID, onDeleteQueuedMessage, onSend],
|
||||
@@ -445,8 +456,11 @@ const AgentDetailConversation: FC<AgentDetailConversationProps> = ({
|
||||
modelSelectorPlaceholder={modelSelectorPlaceholder}
|
||||
inputStatusText={inputStatusText}
|
||||
modelCatalogStatusMessage={modelCatalogStatusMessage}
|
||||
inputValue={inputValue}
|
||||
onInputChange={setInputValue}
|
||||
inputRef={chatInputRef}
|
||||
initialValue={editorInitialValue}
|
||||
onContentChange={(content) => {
|
||||
inputValueRef.current = content;
|
||||
}}
|
||||
editingQueuedMessageID={editingQueuedMessageID}
|
||||
onStartQueueEdit={handleStartQueueEdit}
|
||||
onCancelQueueEdit={handleCancelQueueEdit}
|
||||
@@ -896,8 +910,7 @@ const AgentDetail: FC = () => {
|
||||
</div>
|
||||
<AgentChatInput
|
||||
onSend={() => {}}
|
||||
value=""
|
||||
onChange={() => {}}
|
||||
initialValue=""
|
||||
isDisabled={isInputDisabled}
|
||||
isLoading={false}
|
||||
selectedModel={selectedModel}
|
||||
|
||||
@@ -511,12 +511,13 @@ export const AgentsEmptyState: FC<AgentsEmptyStateProps> = ({
|
||||
isConfigureAgentsDialogOpen,
|
||||
onConfigureAgentsDialogOpenChange,
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = useState(() => {
|
||||
const [initialInputValue] = useState(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return "";
|
||||
}
|
||||
return localStorage.getItem(emptyInputStorageKey) ?? "";
|
||||
});
|
||||
const inputValueRef = useRef(initialInputValue);
|
||||
const initialSystemPrompt = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return "";
|
||||
@@ -659,10 +660,10 @@ export const AgentsEmptyState: FC<AgentsEmptyStateProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = useCallback((value: string) => {
|
||||
setInputValue(value);
|
||||
const handleContentChange = useCallback((content: string) => {
|
||||
inputValueRef.current = content;
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(emptyInputStorageKey, value);
|
||||
localStorage.setItem(emptyInputStorageKey, content);
|
||||
}
|
||||
}, []);
|
||||
const handleModelChange = useCallback((value: string) => {
|
||||
@@ -720,8 +721,8 @@ export const AgentsEmptyState: FC<AgentsEmptyStateProps> = ({
|
||||
placeholder="Ask Coder to build, fix bugs, or explore your project..."
|
||||
isDisabled={isCreating}
|
||||
isLoading={isCreating}
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
initialValue={initialInputValue}
|
||||
onContentChange={handleContentChange}
|
||||
selectedModel={selectedModel}
|
||||
onModelChange={handleModelChange}
|
||||
modelOptions={modelOptions}
|
||||
|
||||
Reference in New Issue
Block a user