mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-05-01 09:40:45 +08:00
feat: RAG Integration (#238)
* feat: add rag provider and retriever * feat: retriever tool * feat: add retriever tool to the researcher node * feat: add rag http apis * feat: new message input supports resource mentions * feat: new message input component support resource mentions * refactor: need_web_search to need_search * chore: RAG integration docs * chore: change example api host * fix: user message color in dark mode * fix: mentions style * feat: add local_search_tool to researcher prompt * chore: research prompt * fix: ragflow page size and reporter with * docs: ragflow integration and add acknowledgment projects * chore: format
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"check": "next lint && tsc --noEmit",
|
||||
"dev": "next dev --turbo",
|
||||
"dev": "dotenv -f ../.env -e true run next dev --turbo",
|
||||
"scan": "next dev & npx react-scan@latest localhost:3000",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||
"format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
|
||||
@@ -35,12 +35,16 @@
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tabs": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "^1.2.0",
|
||||
"@rc-component/mentions": "^1.2.0",
|
||||
"@t3-oss/env-nextjs": "^0.11.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tiptap/extension-document": "^2.12.0",
|
||||
"@tiptap/extension-mention": "^2.12.0",
|
||||
"@tiptap/extension-table": "^2.11.7",
|
||||
"@tiptap/extension-table-cell": "^2.11.7",
|
||||
"@tiptap/extension-table-header": "^2.11.7",
|
||||
"@tiptap/extension-table-row": "^2.11.7",
|
||||
"@tiptap/extension-text": "^2.12.0",
|
||||
"@tiptap/react": "^2.11.7",
|
||||
"@xyflow/react": "^12.6.0",
|
||||
"best-effort-json-parser": "^1.1.3",
|
||||
@@ -70,6 +74,7 @@
|
||||
"remark-math": "^6.0.0",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
|
||||
210
web/pnpm-lock.yaml
generated
210
web/pnpm-lock.yaml
generated
@@ -62,12 +62,21 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(@types/react-dom@19.1.1(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/mentions':
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@t3-oss/env-nextjs':
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.1(typescript@5.8.3)(zod@3.24.3)
|
||||
'@tailwindcss/typography':
|
||||
specifier: ^0.5.16
|
||||
version: 0.5.16(tailwindcss@4.1.4)
|
||||
'@tiptap/extension-document':
|
||||
specifier: ^2.12.0
|
||||
version: 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-mention':
|
||||
specifier: ^2.12.0
|
||||
version: 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(@tiptap/suggestion@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-table':
|
||||
specifier: ^2.11.7
|
||||
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
@@ -80,6 +89,9 @@ importers:
|
||||
'@tiptap/extension-table-row':
|
||||
specifier: ^2.11.7
|
||||
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-text':
|
||||
specifier: ^2.12.0
|
||||
version: 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/react':
|
||||
specifier: ^2.11.7
|
||||
version: 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -167,6 +179,9 @@ importers:
|
||||
tailwind-merge:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
tippy.js:
|
||||
specifier: ^6.3.7
|
||||
version: 6.3.7
|
||||
tiptap-markdown:
|
||||
specifier: ^0.8.10
|
||||
version: 0.8.10(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
@@ -1193,6 +1208,56 @@ packages:
|
||||
'@radix-ui/rect@1.1.1':
|
||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
||||
|
||||
'@rc-component/input@1.0.1':
|
||||
resolution: {integrity: sha512-omxsjWpB+RamzDDB0NzgV6qI7Ok/U6nrN2KLL/hLZJcI7sZZgLYAN+Xs1pN7OYBnUeyn25PizcntEE0nofHv8Q==}
|
||||
peerDependencies:
|
||||
react: '>=16.0.0'
|
||||
react-dom: '>=16.0.0'
|
||||
|
||||
'@rc-component/mentions@1.2.0':
|
||||
resolution: {integrity: sha512-dSr9mX5bQWDegeVLr+NoffjZO5paG/nzM5f+RVslpznfVqR5d3c+xan+f6ZqZWHJqJOfROqNGAkUb8pqqAV7wQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/menu@1.1.3':
|
||||
resolution: {integrity: sha512-NN/J0nJFwwDfQBycl9mordDTBdSai5Ie4nxaGkH2eHVa37KjyhpU98EtcVb/ss393I7SZTDCvoylS3MQOjgYkw==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/motion@1.1.4':
|
||||
resolution: {integrity: sha512-rz3+kqQ05xEgIAB9/UKQZKCg5CO/ivGNU78QWYKVfptmbjJKynZO4KXJ7pJD3oMxE9aW94LD/N3eppXWeysTjw==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/portal@2.0.0':
|
||||
resolution: {integrity: sha512-337ADhBfgH02S8OujUl33OT+8zVJ67eyuUq11j/dE71rXKYNihMsggW8R2VfI2aL3SciDp8gAFsmPVoPkxLUGw==}
|
||||
engines: {node: '>=12.x'}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
|
||||
'@rc-component/resize-observer@1.0.0':
|
||||
resolution: {integrity: sha512-inR8Ka87OOwtrDJzdVp2VuEVlc5nK20lHolvkwFUnXwV50p+nLhKny1NvNTCKvBmS/pi/rTn/1Hvsw10sRRnXA==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/textarea@1.0.0':
|
||||
resolution: {integrity: sha512-GuXakeRWZuWUnF2sqfC8RjtzfAh5UI89dPk6r5SgosyQGfQIueuN8LkWmFq5OKTOJIlc82MOjHiPBigKB9+KGw==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/trigger@3.4.0':
|
||||
resolution: {integrity: sha512-Vu+RS7bGAHHNtzP6EzrMwH+xiZl+SHQgR98oAUXtoQIy4+4lsSppwQPcl6Q7ORZuZevil1BSw4GHXNWD8BJOXw==}
|
||||
engines: {node: '>=8.x'}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
|
||||
'@rc-component/util@1.2.1':
|
||||
resolution: {integrity: sha512-AUVu6jO+lWjQnUOOECwu8iR0EdElQgWW5NBv5vP/Uf9dWbAX3udhMutRlkVXjuac2E40ghkFy+ve00mc/3Fymg==}
|
||||
peerDependencies:
|
||||
@@ -1399,8 +1464,8 @@ packages:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/extension-text-style': ^2.7.0
|
||||
|
||||
'@tiptap/extension-document@2.11.7':
|
||||
resolution: {integrity: sha512-95ouJXPjdAm9+VBRgFo4lhDoMcHovyl/awORDI8gyEn0Rdglt+ZRZYoySFzbVzer9h0cre+QdIwr9AIzFFbfdA==}
|
||||
'@tiptap/extension-document@2.12.0':
|
||||
resolution: {integrity: sha512-sA1Q+mxDIv0Y3qQTBkYGwknNbDcGFiJ/fyAFholXpqbrcRx3GavwR/o0chBdsJZlFht0x7AWGwUYWvIo7wYilA==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
@@ -1470,6 +1535,13 @@ packages:
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-mention@2.12.0':
|
||||
resolution: {integrity: sha512-+b/fqOU+pRWWAo0ZfyInkhkvV0Ub5RpNrYZ45v2nn5PjbXbxyxNQ51zT6cGk2F6Jmc6UBmlR8iqqNTIQY9ieEg==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
'@tiptap/pm': ^2.7.0
|
||||
'@tiptap/suggestion': ^2.7.0
|
||||
|
||||
'@tiptap/extension-ordered-list@2.11.7':
|
||||
resolution: {integrity: sha512-bLGCHDMB0vbJk7uu8bRg8vES3GsvxkX7Cgjgm/6xysHFbK98y0asDtNxkW1VvuRreNGz4tyB6vkcVCfrxl4jKw==}
|
||||
peerDependencies:
|
||||
@@ -1528,8 +1600,8 @@ packages:
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
'@tiptap/extension-text@2.11.7':
|
||||
resolution: {integrity: sha512-wObCn8qZkIFnXTLvBP+X8KgaEvTap/FJ/i4hBMfHBCKPGDx99KiJU6VIbDXG8d5ZcFZE0tOetK1pP5oI7qgMlQ==}
|
||||
'@tiptap/extension-text@2.12.0':
|
||||
resolution: {integrity: sha512-0ytN9V1tZYTXdiYDQg4FB2SQ56JAJC9r/65snefb9ztl+gZzDrIvih7CflHs1ic9PgyjexfMLeH+VzuMccNyZw==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.7.0
|
||||
|
||||
@@ -3520,6 +3592,24 @@ packages:
|
||||
peerDependencies:
|
||||
webpack: ^4.0.0 || ^5.0.0
|
||||
|
||||
rc-overflow@1.4.1:
|
||||
resolution: {integrity: sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
rc-resize-observer@1.4.3:
|
||||
resolution: {integrity: sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
rc-util@5.44.4:
|
||||
resolution: {integrity: sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
react-css-styled@1.1.9:
|
||||
resolution: {integrity: sha512-M7fJZ3IWFaIHcZEkoFOnkjdiUFmwd8d+gTh2bpqMOcnxy/0Gsykw4dsL4QBiKsxcGow6tETUa4NAUcmJF+/nfw==}
|
||||
|
||||
@@ -3639,6 +3729,9 @@ packages:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -5047,6 +5140,74 @@ snapshots:
|
||||
|
||||
'@radix-ui/rect@1.1.1': {}
|
||||
|
||||
'@rc-component/input@1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/mentions@1.2.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/input': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/menu': 1.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/textarea': 1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/trigger': 3.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/menu@1.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/motion': 1.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/trigger': 3.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
rc-overflow: 1.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/motion@1.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/portal@2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/resize-observer@1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/textarea@1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/input': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/resize-observer': 1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/trigger@3.4.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@rc-component/motion': 1.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/portal': 2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/resize-observer': 1.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@rc-component/util': 1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
classnames: 2.5.1
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
'@rc-component/util@1.2.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
@@ -5216,7 +5377,7 @@ snapshots:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
'@tiptap/extension-text-style': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
|
||||
'@tiptap/extension-document@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
'@tiptap/extension-document@2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
|
||||
@@ -5276,6 +5437,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
|
||||
'@tiptap/extension-mention@2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)(@tiptap/suggestion@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
'@tiptap/pm': 2.11.7
|
||||
'@tiptap/suggestion': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
|
||||
'@tiptap/extension-ordered-list@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
@@ -5323,7 +5490,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
|
||||
'@tiptap/extension-text@2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
'@tiptap/extension-text@2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))':
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.7(@tiptap/pm@2.11.7)
|
||||
|
||||
@@ -5376,7 +5543,7 @@ snapshots:
|
||||
'@tiptap/extension-bullet-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-code': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-code-block': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
'@tiptap/extension-document': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-document': 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-dropcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
'@tiptap/extension-gapcursor': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))(@tiptap/pm@2.11.7)
|
||||
'@tiptap/extension-hard-break': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
@@ -5388,7 +5555,7 @@ snapshots:
|
||||
'@tiptap/extension-ordered-list': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-paragraph': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-strike': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-text': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-text': 2.12.0(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/extension-text-style': 2.11.7(@tiptap/core@2.11.7(@tiptap/pm@2.11.7))
|
||||
'@tiptap/pm': 2.11.7
|
||||
|
||||
@@ -7816,6 +7983,31 @@ snapshots:
|
||||
schema-utils: 3.3.0
|
||||
webpack: 5.99.6
|
||||
|
||||
rc-overflow@1.4.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
classnames: 2.5.1
|
||||
rc-resize-observer: 1.4.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
rc-util: 5.44.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
rc-resize-observer@1.4.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
classnames: 2.5.1
|
||||
rc-util: 5.44.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
resize-observer-polyfill: 1.5.1
|
||||
|
||||
rc-util@5.44.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.0
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-is: 18.3.1
|
||||
|
||||
react-css-styled@1.1.9:
|
||||
dependencies:
|
||||
css-styled: 1.0.8
|
||||
@@ -8020,6 +8212,8 @@ snapshots:
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-pkg-maps@1.0.0: {}
|
||||
|
||||
@@ -23,19 +23,19 @@ event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " reason it's trending, and some key statistics (stars, forks, contributors, etc.).\",\n \"title\": \"Research Plan: Top Trending GitHub Repository Today"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": "\",\n \"steps\": [\n {\n \"need_web_search\": true,\n \"title\": \"Identify and Profile the Top Trending Repository\",\n \"description\": \"Identify the #1 trending repository on"}
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": "\",\n \"steps\": [\n {\n \"need_search\": true,\n \"title\": \"Identify and Profile the Top Trending Repository\",\n \"description\": \"Identify the #1 trending repository on"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " GitHub today. Collect the following information: repository name, repository owner/organization, a short description of the repository's purpose, the primary programming language used, and the reason GitHub marks it as trending (e.g., 'X new stars today"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": "'). Note: ensure to filter for 'today' to get the current trending repo.\",\n \"step_type\": \"research\"\n },\n {\n \"need_web_search\": true,\n \"title\": \"Gather Repository Statistics and Community Data\",\n \"description\": \"Collect"}
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": "'). Note: ensure to filter for 'today' to get the current trending repo.\",\n \"step_type\": \"research\"\n },\n {\n \"need_search\": true,\n \"title\": \"Gather Repository Statistics and Community Data\",\n \"description\": \"Collect"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " detailed statistics for the top trending repository. This includes the total number of stars, forks, open issues, closed issues, contributors, and recent commit activity. Also, gather data about the community's involvement, such as the number of active contributors in the last month, and any available information on significant discussions or contributions happening"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " within the project. Check for recent release notes or announcements.\",\n \"step_type\": \"research\"\n },\n {\n \"need_web_search\": true,\n \"title\": \"Determine Context and Significance\",\n \"description\": \"Research the broader context and significance of the trending"}
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " within the project. Check for recent release notes or announcements.\",\n \"step_type\": \"research\"\n },\n {\n \"need_search\": true,\n \"title\": \"Determine Context and Significance\",\n \"description\": \"Research the broader context and significance of the trending"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "LmC3xxJCFljoFXggnmvst", "agent": "planner", "id": "run-33af75e6-c1b5-4276-9749-7cfb7a967402", "role": "assistant", "content": " repository. Determine the repository's purpose or function. Investigate the project's background, the problem it solves, or the features it provides. Identify the industry, user base, or application area it serves. Search for recent news, articles, or blog posts mentioning the repository and its impact or potential. Identify its license"}
|
||||
|
||||
@@ -20,16 +20,16 @@ event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " culinary scene and document its traditional dishes. I will create comprehensive steps to gather the most important data and create a good final report.\",\n \"title\": \"Research"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " Plan: Nanjing's Culinary Scene and Traditional Dishes\",\n \"steps\": [\n {\n \"need_web_search\": true,\n "}
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " Plan: Nanjing's Culinary Scene and Traditional Dishes\",\n \"steps\": [\n {\n \"need_search\": true,\n "}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": "\"title\": \"Identify and Document Key Traditional Nanjing Dishes\",\n \"description\": \"Research and compile a comprehensive list of traditional Nanjing dishes, including their names (in both English and Chinese), detailed descriptions of ingredients and preparation methods, and historical origins"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": ". Identify dishes that are representative of Nanjing's culinary heritage and those that are less well-known but still significant. Document the specific cooking techniques that characterize Nanjing cuisine.\",\n \"step_type\": \"research\"\n },\n {\n \"need_web_search\": true,\n \"title\": \"Investigate the History and Cultural Significance of Nanjing Cuisine\",\n \"description\": \"Explore the historical influences that have shaped Nanjing's culinary traditions, including its role as a former capital city. Document the cultural significance of specific dishes and"}
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": ". Identify dishes that are representative of Nanjing's culinary heritage and those that are less well-known but still significant. Document the specific cooking techniques that characterize Nanjing cuisine.\",\n \"step_type\": \"research\"\n },\n {\n \"need_search\": true,\n \"title\": \"Investigate the History and Cultural Significance of Nanjing Cuisine\",\n \"description\": \"Explore the historical influences that have shaped Nanjing's culinary traditions, including its role as a former capital city. Document the cultural significance of specific dishes and"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " their connection to local customs, festivals, and celebrations. Research the evolution of Nanjing cuisine over time, identifying key periods of change and the factors that contributed to them.\",\n \"step_type\": \"research\"\n },\n {\n \"need_web_search\": true,\n \"title\":"}
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " their connection to local customs, festivals, and celebrations. Research the evolution of Nanjing cuisine over time, identifying key periods of change and the factors that contributed to them.\",\n \"step_type\": \"research\"\n },\n {\n \"need_search\": true,\n \"title\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "PDgExJb-Qsq2fNtO4B_sZ", "agent": "planner", "id": "run-f9561a11-723f-4d5f-917c-95f96601f87f", "role": "assistant", "content": " \"Analyze the Current State of Nanjing's Culinary Scene and Identify Key Restaurants\",\n \"description\": \"Investigate the current state of Nanjing's culinary scene, identifying key restaurants that specialize in traditional Nanjing cuisine. Gather information on their menus, pricing, and customer reviews. Document any trends or changes in the local food"}
|
||||
|
||||
@@ -83,7 +83,7 @@ event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "\": [\n {\n \""}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_web_search\":"}
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": " true,\n \""}
|
||||
@@ -134,7 +134,7 @@ event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": " {\n \""}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_web_search\":"}
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": " true,\n \"title"}
|
||||
@@ -194,7 +194,7 @@ event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "\"\n },\n {\n \""}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_web_search\":"}
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": "need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "5CG_qm7snTVKbpVCrWTon", "agent": "planner", "id": "run-3006007c-5c06-4500-ba23-3fab94c70ae7", "role": "assistant", "content": " true,\n \"title"}
|
||||
|
||||
@@ -140,7 +140,7 @@ event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": "research\"\n },\n {\n"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " \"need_web_search\":"}
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " \"need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " true,\n \"title"}
|
||||
@@ -200,7 +200,7 @@ event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": "research\"\n },\n {\n"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " \"need_web_search\":"}
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " \"need_search\":"}
|
||||
|
||||
event: message_chunk
|
||||
data: {"thread_id": "01uPkjxNhUsYZHQ1DrkhK", "agent": "planner", "id": "run-77b32288-ec82-4b8e-b815-d403687915bd", "role": "assistant", "content": " true,\n \"title"}
|
||||
|
||||
@@ -3,18 +3,15 @@
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { ArrowUp, X } from "lucide-react";
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useCallback, useRef } from "react";
|
||||
|
||||
import { Detective } from "~/components/deer-flow/icons/detective";
|
||||
import MessageInput, {
|
||||
type MessageInputRef,
|
||||
} from "~/components/deer-flow/message-input";
|
||||
import { Tooltip } from "~/components/deer-flow/tooltip";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import type { Option } from "~/core/messages";
|
||||
import type { Option, Resource } from "~/core/messages";
|
||||
import {
|
||||
setEnableBackgroundInvestigation,
|
||||
useSettingsStore,
|
||||
@@ -23,7 +20,6 @@ import { cn } from "~/lib/utils";
|
||||
|
||||
export function InputBox({
|
||||
className,
|
||||
size,
|
||||
responding,
|
||||
feedback,
|
||||
onSend,
|
||||
@@ -34,72 +30,52 @@ export function InputBox({
|
||||
size?: "large" | "normal";
|
||||
responding?: boolean;
|
||||
feedback?: { option: Option } | null;
|
||||
onSend?: (message: string, options?: { interruptFeedback?: string }) => void;
|
||||
onSend?: (
|
||||
message: string,
|
||||
options?: {
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
},
|
||||
) => void;
|
||||
onCancel?: () => void;
|
||||
onRemoveFeedback?: () => void;
|
||||
}) {
|
||||
const [message, setMessage] = useState("");
|
||||
const [imeStatus, setImeStatus] = useState<"active" | "inactive">("inactive");
|
||||
const [indent, setIndent] = useState(0);
|
||||
const backgroundInvestigation = useSettingsStore(
|
||||
(state) => state.general.enableBackgroundInvestigation,
|
||||
);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<MessageInputRef>(null);
|
||||
const feedbackRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (feedback) {
|
||||
setMessage("");
|
||||
|
||||
setTimeout(() => {
|
||||
if (feedbackRef.current) {
|
||||
setIndent(feedbackRef.current.offsetWidth);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
setTimeout(() => {
|
||||
textareaRef.current?.focus();
|
||||
}, 0);
|
||||
}, [feedback]);
|
||||
|
||||
const handleSendMessage = useCallback(() => {
|
||||
if (responding) {
|
||||
onCancel?.();
|
||||
} else {
|
||||
if (message.trim() === "") {
|
||||
return;
|
||||
}
|
||||
if (onSend) {
|
||||
onSend(message, {
|
||||
interruptFeedback: feedback?.option.value,
|
||||
});
|
||||
setMessage("");
|
||||
onRemoveFeedback?.();
|
||||
}
|
||||
}
|
||||
}, [responding, onCancel, message, onSend, feedback, onRemoveFeedback]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const handleSendMessage = useCallback(
|
||||
(message: string, resources: Array<Resource>) => {
|
||||
console.log(message, resources);
|
||||
if (responding) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
imeStatus === "inactive"
|
||||
) {
|
||||
event.preventDefault();
|
||||
handleSendMessage();
|
||||
onCancel?.();
|
||||
} else {
|
||||
if (message.trim() === "") {
|
||||
return;
|
||||
}
|
||||
if (onSend) {
|
||||
onSend(message, {
|
||||
interruptFeedback: feedback?.option.value,
|
||||
resources,
|
||||
});
|
||||
onRemoveFeedback?.();
|
||||
}
|
||||
}
|
||||
},
|
||||
[responding, imeStatus, handleSendMessage],
|
||||
[responding, onCancel, onSend, feedback, onRemoveFeedback],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("bg-card relative rounded-[24px] border", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-card relative flex h-full w-full flex-col rounded-[24px] border",
|
||||
className,
|
||||
)}
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="w-full">
|
||||
<AnimatePresence>
|
||||
{feedback && (
|
||||
@@ -122,25 +98,10 @@ export function InputBox({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className={cn(
|
||||
"m-0 w-full resize-none border-none px-4 py-3 text-lg",
|
||||
size === "large" ? "min-h-32" : "min-h-4",
|
||||
)}
|
||||
style={{ textIndent: feedback ? `${indent}px` : 0 }}
|
||||
placeholder={
|
||||
feedback
|
||||
? `Describe how you ${feedback.option.text.toLocaleLowerCase()}?`
|
||||
: "What can I do for you?"
|
||||
}
|
||||
value={message}
|
||||
onCompositionStart={() => setImeStatus("active")}
|
||||
onCompositionEnd={() => setImeStatus("inactive")}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(event) => {
|
||||
setMessage(event.target.value);
|
||||
}}
|
||||
<MessageInput
|
||||
className={cn("h-24 px-4 pt-3")}
|
||||
ref={inputRef}
|
||||
onEnter={handleSendMessage}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center px-4 py-2">
|
||||
@@ -181,7 +142,7 @@ export function InputBox({
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn("h-10 w-10 rounded-full")}
|
||||
onClick={handleSendMessage}
|
||||
onClick={() => inputRef.current?.submit()}
|
||||
>
|
||||
{responding ? (
|
||||
<div className="flex h-10 w-10 items-center justify-center">
|
||||
|
||||
@@ -174,7 +174,14 @@ function MessageListItem({
|
||||
>
|
||||
<MessageBubble message={message}>
|
||||
<div className="flex w-full flex-col">
|
||||
<Markdown>{message?.content}</Markdown>
|
||||
<Markdown
|
||||
className={cn(
|
||||
message.role === "user" &&
|
||||
"prose-invert not-dark:text-secondary dark:text-inherit",
|
||||
)}
|
||||
>
|
||||
{message?.content}
|
||||
</Markdown>
|
||||
</div>
|
||||
</MessageBubble>
|
||||
</div>
|
||||
@@ -214,9 +221,8 @@ function MessageBubble({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 shadow`,
|
||||
message.role === "user" &&
|
||||
"text-primary-foreground bg-brand rounded-ee-none",
|
||||
`group flex w-fit max-w-[85%] flex-col rounded-2xl px-4 py-3 text-nowrap shadow`,
|
||||
message.role === "user" && "bg-brand rounded-ee-none",
|
||||
message.role === "assistant" && "bg-card rounded-es-none",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from "~/components/ui/card";
|
||||
import { fastForwardReplay } from "~/core/api";
|
||||
import { useReplayMetadata } from "~/core/api/hooks";
|
||||
import type { Option } from "~/core/messages";
|
||||
import type { Option, Resource } from "~/core/messages";
|
||||
import { useReplay } from "~/core/replay";
|
||||
import { sendMessage, useMessageIds, useStore } from "~/core/store";
|
||||
import { env } from "~/env";
|
||||
@@ -36,7 +36,13 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const [feedback, setFeedback] = useState<{ option: Option } | null>(null);
|
||||
const handleSend = useCallback(
|
||||
async (message: string, options?: { interruptFeedback?: string }) => {
|
||||
async (
|
||||
message: string,
|
||||
options?: {
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
},
|
||||
) => {
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
try {
|
||||
@@ -45,6 +51,7 @@ export function MessagesBlock({ className }: { className?: string }) {
|
||||
{
|
||||
interruptFeedback:
|
||||
options?.interruptFeedback ?? feedback?.option.value,
|
||||
resources: options?.resources,
|
||||
},
|
||||
{
|
||||
abortSignal: abortController.signal,
|
||||
|
||||
@@ -276,8 +276,8 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
}
|
||||
|
||||
function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const code = useMemo<string>(() => {
|
||||
return (toolCall.args as { code: string }).code;
|
||||
const code = useMemo<string | undefined>(() => {
|
||||
return (toolCall.args as { code?: string }).code;
|
||||
}, [toolCall.args]);
|
||||
const { resolvedTheme } = useTheme();
|
||||
return (
|
||||
@@ -302,7 +302,7 @@ function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
{code.trim()}
|
||||
{code?.trim() ?? ""}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,10 +53,7 @@ export function ResearchReportBlock({
|
||||
// }, [isCompleted]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn("relative flex flex-col pt-4 pb-8", className)}
|
||||
>
|
||||
<div ref={contentRef} className={cn("w-full pt-4 pb-8", className)}>
|
||||
{!isReplay && isCompleted && editing ? (
|
||||
<ReportEditor
|
||||
content={message?.content}
|
||||
|
||||
@@ -37,7 +37,7 @@ export const Link = ({
|
||||
}, [credibleLinks, href, responding, checkLinkCredibility]);
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
|
||||
184
web/src/components/deer-flow/message-input.tsx
Normal file
184
web/src/components/deer-flow/message-input.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
"use client";
|
||||
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
import { Editor, Extension, type Content } from "@tiptap/react";
|
||||
import {
|
||||
EditorContent,
|
||||
type EditorInstance,
|
||||
EditorRoot,
|
||||
type JSONContent,
|
||||
StarterKit,
|
||||
Placeholder,
|
||||
} from "novel";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { cx } from "class-variance-authority";
|
||||
|
||||
import "~/styles/prosemirror.css";
|
||||
import { resourceSuggestion } from "./resource-suggestion";
|
||||
import React, { forwardRef, useMemo, useRef } from "react";
|
||||
import type { Resource } from "~/core/messages";
|
||||
import { useRAGProvider } from "~/core/api/hooks";
|
||||
|
||||
export interface MessageInputRef {
|
||||
focus: () => void;
|
||||
submit: () => void;
|
||||
}
|
||||
|
||||
export interface MessageInputProps {
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
onChange?: (markdown: string) => void;
|
||||
onEnter?: (message: string, resources: Array<Resource>) => void;
|
||||
}
|
||||
|
||||
function formatMessage(content: JSONContent) {
|
||||
if (content.content) {
|
||||
const output: {
|
||||
text: string;
|
||||
resources: Array<Resource>;
|
||||
} = {
|
||||
text: "",
|
||||
resources: [],
|
||||
};
|
||||
for (const node of content.content) {
|
||||
const { text, resources } = formatMessage(node);
|
||||
output.text += text;
|
||||
output.resources.push(...resources);
|
||||
}
|
||||
return output;
|
||||
} else {
|
||||
return formatItem(content);
|
||||
}
|
||||
}
|
||||
|
||||
function formatItem(item: JSONContent): {
|
||||
text: string;
|
||||
resources: Array<Resource>;
|
||||
} {
|
||||
if (item.type === "text") {
|
||||
return { text: item.text ?? "", resources: [] };
|
||||
}
|
||||
if (item.type === "mention") {
|
||||
return {
|
||||
text: `[${item.attrs?.label}](${item.attrs?.id})`,
|
||||
resources: [
|
||||
{ uri: item.attrs?.id ?? "", title: item.attrs?.label ?? "" },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { text: "", resources: [] };
|
||||
}
|
||||
|
||||
const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
({ className, onChange, onEnter }: MessageInputProps, ref) => {
|
||||
const editorRef = useRef<Editor>(null);
|
||||
const debouncedUpdates = useDebouncedCallback(
|
||||
async (editor: EditorInstance) => {
|
||||
if (onChange) {
|
||||
const markdown = editor.storage.markdown.getMarkdown();
|
||||
onChange(markdown);
|
||||
}
|
||||
},
|
||||
200,
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
editorRef.current?.view.focus();
|
||||
},
|
||||
submit: () => {
|
||||
if (onEnter) {
|
||||
const { text, resources } = formatMessage(
|
||||
editorRef.current?.getJSON() ?? [],
|
||||
);
|
||||
onEnter(text, resources);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const { provider, loading } = useRAGProvider();
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const extensions = [
|
||||
StarterKit,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
tightLists: true,
|
||||
tightListClass: "tight",
|
||||
bulletListMarker: "-",
|
||||
linkify: false,
|
||||
breaks: false,
|
||||
transformPastedText: false,
|
||||
transformCopiedText: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: provider
|
||||
? "What can I do for you? \nYou may refer to RAG resources by using @."
|
||||
: "What can I do for you?",
|
||||
emptyEditorClass: "placeholder",
|
||||
}),
|
||||
Extension.create({
|
||||
name: "keyboardHandler",
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Enter: () => {
|
||||
if (onEnter) {
|
||||
const { text, resources } = formatMessage(
|
||||
this.editor.getJSON() ?? [],
|
||||
);
|
||||
onEnter(text, resources);
|
||||
}
|
||||
return this.editor.commands.clearContent();
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
];
|
||||
if (provider) {
|
||||
extensions.push(
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: "mention",
|
||||
},
|
||||
suggestion: resourceSuggestion,
|
||||
}) as Extension,
|
||||
);
|
||||
}
|
||||
return extensions;
|
||||
}, [onEnter, provider]);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<EditorRoot>
|
||||
<EditorContent
|
||||
immediatelyRender={false}
|
||||
extensions={extensions}
|
||||
className="border-muted h-full w-full overflow-auto"
|
||||
editorProps={{
|
||||
attributes: {
|
||||
class:
|
||||
"prose prose-base dark:prose-invert prose-headings:font-title font-default focus:outline-none max-w-full",
|
||||
},
|
||||
}}
|
||||
onCreate={({ editor }) => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
onUpdate={({ editor }) => {
|
||||
debouncedUpdates(editor);
|
||||
}}
|
||||
></EditorContent>
|
||||
</EditorRoot>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default MessageInput;
|
||||
84
web/src/components/deer-flow/resource-mentions.tsx
Normal file
84
web/src/components/deer-flow/resource-mentions.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import type { Resource } from "~/core/messages";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
export interface ResourceMentionsProps {
|
||||
items: Array<Resource>;
|
||||
command: (item: { id: string; label: string }) => void;
|
||||
}
|
||||
|
||||
export const ResourceMentions = forwardRef<
|
||||
{ onKeyDown: (args: { event: KeyboardEvent }) => boolean },
|
||||
ResourceMentionsProps
|
||||
>((props, ref) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
const item = props.items[index];
|
||||
|
||||
if (item) {
|
||||
props.command({ id: item.uri, label: item.title });
|
||||
}
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex(
|
||||
(selectedIndex + props.items.length - 1) % props.items.length,
|
||||
);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||
};
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex);
|
||||
};
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === "ArrowUp") {
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
enterHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="bg-card border-var(--border) relative flex flex-col gap-1 overflow-auto rounded-md border p-2 shadow">
|
||||
{props.items.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<button
|
||||
className={cn(
|
||||
"focus-visible:ring-ring hover:bg-accent hover:text-accent-foreground inline-flex h-9 w-full items-center justify-start gap-2 rounded-md px-4 py-2 text-sm whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
selectedIndex === index &&
|
||||
"bg-secondary text-secondary-foreground",
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="items-center justify-center text-gray-500">
|
||||
No result
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
83
web/src/components/deer-flow/resource-suggestion.tsx
Normal file
83
web/src/components/deer-flow/resource-suggestion.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { MentionOptions } from "@tiptap/extension-mention";
|
||||
import { ReactRenderer } from "@tiptap/react";
|
||||
import {
|
||||
ResourceMentions,
|
||||
type ResourceMentionsProps,
|
||||
} from "./resource-mentions";
|
||||
import type { Instance, Props } from "tippy.js";
|
||||
import tippy from "tippy.js";
|
||||
import { resolveServiceURL } from "~/core/api/resolve-service-url";
|
||||
import type { Resource } from "~/core/messages";
|
||||
|
||||
export const resourceSuggestion: MentionOptions["suggestion"] = {
|
||||
items: ({ query }) => {
|
||||
return fetch(resolveServiceURL(`rag/resources?query=${query}`), {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
return res.resources as Array<Resource>;
|
||||
})
|
||||
.catch((err) => {
|
||||
return [];
|
||||
});
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let reactRenderer: ReactRenderer<
|
||||
{ onKeyDown: (args: { event: KeyboardEvent }) => boolean },
|
||||
ResourceMentionsProps
|
||||
>;
|
||||
let popup: Instance<Props>[] | null = null;
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactRenderer = new ReactRenderer(ResourceMentions, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
appendTo: () => document.body,
|
||||
content: reactRenderer.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "top-start",
|
||||
});
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
reactRenderer.updateProps(props);
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
popup?.[0]?.setProps({
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
});
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === "Escape") {
|
||||
popup?.[0]?.hide();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return reactRenderer.ref?.onKeyDown(props) ?? false;
|
||||
},
|
||||
|
||||
onExit() {
|
||||
popup?.[0]?.destroy();
|
||||
reactRenderer.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -66,17 +66,6 @@ const ReportEditor = ({ content, onMarkdownChange }: ReportEditorProps) => {
|
||||
|
||||
const debouncedUpdates = useDebouncedCallback(
|
||||
async (editor: EditorInstance) => {
|
||||
// const json = editor.getJSON();
|
||||
// // setCharsCount(editor.storage.characterCount.words());
|
||||
// window.localStorage.setItem(
|
||||
// "html-content",
|
||||
// highlightCodeblocks(editor.getHTML()),
|
||||
// );
|
||||
// window.localStorage.setItem("novel-content", JSON.stringify(json));
|
||||
// window.localStorage.setItem(
|
||||
// "markdown",
|
||||
// editor.storage.markdown.getMarkdown(),
|
||||
// );
|
||||
if (onMarkdownChange) {
|
||||
const markdown = editor.storage.markdown.getMarkdown();
|
||||
onMarkdownChange(markdown);
|
||||
@@ -86,12 +75,6 @@ const ReportEditor = ({ content, onMarkdownChange }: ReportEditorProps) => {
|
||||
500,
|
||||
);
|
||||
|
||||
// useEffect(() => {
|
||||
// const content = window.localStorage.getItem("novel-content");
|
||||
// if (content) setInitialContent(JSON.parse(content));
|
||||
// else setInitialContent(defaultEditorContent);
|
||||
// }, []);
|
||||
|
||||
if (!initialContent) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { env } from "~/env";
|
||||
|
||||
import type { MCPServerMetadata } from "../mcp";
|
||||
import type { Resource } from "../messages";
|
||||
import { extractReplayIdFromSearchParams } from "../replay/get-replay-id";
|
||||
import { fetchStream } from "../sse";
|
||||
import { sleep } from "../utils";
|
||||
@@ -15,6 +16,7 @@ export async function* chatStream(
|
||||
userMessage: string,
|
||||
params: {
|
||||
thread_id: string;
|
||||
resources?: Array<Resource>;
|
||||
auto_accepted_plan: boolean;
|
||||
max_plan_iterations: number;
|
||||
max_step_num: number;
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { env } from "~/env";
|
||||
|
||||
import { useReplay } from "../replay";
|
||||
|
||||
import { fetchReplayTitle } from "./chat";
|
||||
import { getRAGConfig } from "./rag";
|
||||
|
||||
export function useReplayMetadata() {
|
||||
const { isReplay } = useReplay();
|
||||
@@ -39,3 +42,26 @@ export function useReplayMetadata() {
|
||||
}, [isLoading, isReplay, title]);
|
||||
return { title, isLoading, hasError: error };
|
||||
}
|
||||
|
||||
export function useRAGProvider() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [provider, setProvider] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
getRAGConfig()
|
||||
.then(setProvider)
|
||||
.catch((e) => {
|
||||
setProvider(null);
|
||||
console.error("Failed to get RAG provider", e);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { provider, loading };
|
||||
}
|
||||
|
||||
24
web/src/core/api/rag.ts
Normal file
24
web/src/core/api/rag.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Resource } from "../messages";
|
||||
|
||||
import { resolveServiceURL } from "./resolve-service-url";
|
||||
|
||||
export function queryRAGResources(query: string) {
|
||||
return fetch(resolveServiceURL(`rag/resources?query=${query}`), {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
return res.resources as Array<Resource>;
|
||||
})
|
||||
.catch((err) => {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
export function getRAGConfig() {
|
||||
return fetch(resolveServiceURL(`rag/config`), {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => res.provider);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export interface Message {
|
||||
options?: Option[];
|
||||
finishReason?: "stop" | "interrupt" | "tool_calls";
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@@ -35,3 +36,8 @@ export interface ToolCallRuntime {
|
||||
argsChunks?: string[];
|
||||
result?: string;
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
uri: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { create } from "zustand";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
import { chatStream, generatePodcast } from "../api";
|
||||
import type { Message } from "../messages";
|
||||
import type { Message, Resource } from "../messages";
|
||||
import { mergeMessage } from "../messages";
|
||||
import { parseJSON } from "../utils";
|
||||
|
||||
@@ -78,8 +78,10 @@ export async function sendMessage(
|
||||
content?: string,
|
||||
{
|
||||
interruptFeedback,
|
||||
resources,
|
||||
}: {
|
||||
interruptFeedback?: string;
|
||||
resources?: Array<Resource>;
|
||||
} = {},
|
||||
options: { abortSignal?: AbortSignal } = {},
|
||||
) {
|
||||
@@ -90,6 +92,7 @@ export async function sendMessage(
|
||||
role: "user",
|
||||
content: content,
|
||||
contentChunks: [content],
|
||||
resources,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,6 +102,7 @@ export async function sendMessage(
|
||||
{
|
||||
thread_id: THREAD_ID,
|
||||
interrupt_feedback: interruptFeedback,
|
||||
resources,
|
||||
auto_accepted_plan: settings.autoAcceptedPlan,
|
||||
enable_background_investigation:
|
||||
settings.enableBackgroundInvestigation ?? true,
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--brand: #5494f3;
|
||||
--brand: rgb(17, 103, 234);
|
||||
|
||||
--novel-highlight-default: #000000;
|
||||
--novel-highlight-purple: #3f2c4b;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@import "./globals.css";
|
||||
|
||||
.prose {
|
||||
color: inherit;
|
||||
max-width: inherit;
|
||||
}
|
||||
|
||||
.prose .placeholder {
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
@@ -15,6 +17,7 @@
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.ProseMirror p.is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
@@ -23,6 +26,14 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.ProseMirror .mention {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
color: var(--brand);
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
|
||||
/* Custom image styles */
|
||||
|
||||
.ProseMirror img {
|
||||
|
||||
Reference in New Issue
Block a user