feat(playground/prompts): enable fulltext search across message windows (#12578)
* feat(playground): enable fulltext search across message windows * up libs * fix dep * fix lock * fix dependencies * refactor search to be usable in prompts too * fix controller
This commit is contained in:
Generated
+62
-116
@@ -366,14 +366,17 @@ importers:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
'@codemirror/language':
|
||||
specifier: ^6.11.3
|
||||
version: 6.11.3
|
||||
specifier: ^6.12.2
|
||||
version: 6.12.2
|
||||
'@codemirror/lint':
|
||||
specifier: ^6.9.2
|
||||
version: 6.9.2
|
||||
specifier: ^6.9.5
|
||||
version: 6.9.5
|
||||
'@codemirror/search':
|
||||
specifier: ^6.6.0
|
||||
version: 6.6.0
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.2
|
||||
version: 6.5.2
|
||||
specifier: ^6.6.0
|
||||
version: 6.6.0
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -580,10 +583,10 @@ importers:
|
||||
version: 11.8.0(typescript@5.9.2)
|
||||
'@uiw/codemirror-themes':
|
||||
specifier: ^4.23.7
|
||||
version: 4.23.7(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)
|
||||
version: 4.23.7(@codemirror/language@6.12.2)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)
|
||||
'@uiw/react-codemirror':
|
||||
specifier: ^4.25.4
|
||||
version: 4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.17.0(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/common@1.5.1))(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.8)(codemirror@6.0.1(@lezer/common@1.5.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
specifier: ^4.25.8
|
||||
version: 4.25.8(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.17.0(@codemirror/language@6.12.2)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)(@lezer/common@1.5.1))(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.8)(codemirror@6.0.1(@lezer/common@1.5.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
ai:
|
||||
specifier: ^3.4.9
|
||||
version: 3.4.9(openai@4.104.0(zod@3.25.62))(react@19.2.3)(solid-js@1.8.18)(sswr@2.1.0(svelte@4.2.19))(svelte@4.2.19)(vue@3.4.27(typescript@5.9.2))(zod@3.25.62)
|
||||
@@ -2066,35 +2069,23 @@ packages:
|
||||
'@codemirror/view': ^6.0.0
|
||||
'@lezer/common': ^1.0.0
|
||||
|
||||
'@codemirror/commands@6.10.0':
|
||||
resolution: {integrity: sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==}
|
||||
|
||||
'@codemirror/commands@6.10.1':
|
||||
resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==}
|
||||
|
||||
'@codemirror/lang-json@6.0.2':
|
||||
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
|
||||
|
||||
'@codemirror/language@6.11.3':
|
||||
resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
|
||||
'@codemirror/language@6.12.2':
|
||||
resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==}
|
||||
|
||||
'@codemirror/language@6.12.1':
|
||||
resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==}
|
||||
'@codemirror/lint@6.9.5':
|
||||
resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==}
|
||||
|
||||
'@codemirror/lint@6.9.2':
|
||||
resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==}
|
||||
'@codemirror/search@6.6.0':
|
||||
resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==}
|
||||
|
||||
'@codemirror/lint@6.9.3':
|
||||
resolution: {integrity: sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==}
|
||||
|
||||
'@codemirror/search@6.5.6':
|
||||
resolution: {integrity: sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==}
|
||||
|
||||
'@codemirror/state@6.5.2':
|
||||
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
|
||||
|
||||
'@codemirror/state@6.5.4':
|
||||
resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==}
|
||||
'@codemirror/state@6.6.0':
|
||||
resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==}
|
||||
|
||||
'@codemirror/theme-one-dark@6.1.2':
|
||||
resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==}
|
||||
@@ -3132,9 +3123,6 @@ packages:
|
||||
peerDependencies:
|
||||
'@langchain/core': ^1.0.0
|
||||
|
||||
'@lezer/common@1.4.0':
|
||||
resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==}
|
||||
|
||||
'@lezer/common@1.5.1':
|
||||
resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
|
||||
|
||||
@@ -3144,9 +3132,6 @@ packages:
|
||||
'@lezer/json@1.0.3':
|
||||
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
|
||||
|
||||
'@lezer/lr@1.4.4':
|
||||
resolution: {integrity: sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==}
|
||||
|
||||
'@lezer/lr@1.4.8':
|
||||
resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==}
|
||||
|
||||
@@ -6213,8 +6198,8 @@ packages:
|
||||
resolution: {integrity: sha512-/G74DRDNrq2pkBjne3aGzycdy2bKa2Hs4DrkLMRq+cDODjyqvHJIZV7hjRRYph3cvGnd0bFd5G6LE5zNp+ap8w==}
|
||||
hasBin: true
|
||||
|
||||
'@uiw/codemirror-extensions-basic-setup@4.25.4':
|
||||
resolution: {integrity: sha512-YzNwkm0AbPv1EXhCHYR5v0nqfemG2jEB0Z3Att4rBYqKrlG7AA9Rhjc3IyBaOzsBu18wtrp9/+uhTyu7TXSRng==}
|
||||
'@uiw/codemirror-extensions-basic-setup@4.25.8':
|
||||
resolution: {integrity: sha512-9Rr+liiBmK4xzZHszL+twNRJApthqmITBwDP3emNTtTrkBFN4gHlqfp+nodKmoVt1+bUH1qQCtyqt+7dbDTHiw==}
|
||||
peerDependencies:
|
||||
'@codemirror/autocomplete': '>=6.0.0'
|
||||
'@codemirror/commands': '>=6.0.0'
|
||||
@@ -6231,8 +6216,8 @@ packages:
|
||||
'@codemirror/state': '>=6.0.0'
|
||||
'@codemirror/view': '>=6.0.0'
|
||||
|
||||
'@uiw/react-codemirror@4.25.4':
|
||||
resolution: {integrity: sha512-ipO067oyfUw+DVaXhQCxkB0ZD9b7RnY+ByrprSYSKCHaULvJ3sqWYC/Zen6zVQ8/XC4o5EPBfatGiX20kC7XGA==}
|
||||
'@uiw/react-codemirror@4.25.8':
|
||||
resolution: {integrity: sha512-A0aLOuJZm2yJ+U9GlMFwxwFciztjd5LhcAG4SMqFxdD58wH+sCQXuY4UU5J2hqgS390qAlShtUgREvJPUonbuQ==}
|
||||
peerDependencies:
|
||||
'@babel/runtime': '>=7.11.0'
|
||||
'@codemirror/state': '>=6.0.0'
|
||||
@@ -14872,93 +14857,60 @@ snapshots:
|
||||
dependencies:
|
||||
'@clickhouse/client-common': 1.13.0
|
||||
|
||||
'@codemirror/autocomplete@6.17.0(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/common@1.5.1)':
|
||||
'@codemirror/autocomplete@6.17.0(@codemirror/language@6.12.2)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)(@lezer/common@1.5.1)':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/language': 6.12.2
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.38.8
|
||||
'@lezer/common': 1.5.1
|
||||
|
||||
'@codemirror/autocomplete@6.17.0(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.38.8)(@lezer/common@1.5.1)':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/view': 6.38.8
|
||||
'@lezer/common': 1.5.1
|
||||
|
||||
'@codemirror/commands@6.10.0':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.8
|
||||
'@lezer/common': 1.4.0
|
||||
|
||||
'@codemirror/commands@6.10.1':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/language': 6.12.2
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.38.8
|
||||
'@lezer/common': 1.5.1
|
||||
|
||||
'@codemirror/lang-json@6.0.2':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/language': 6.12.2
|
||||
'@lezer/json': 1.0.3
|
||||
|
||||
'@codemirror/language@6.11.3':
|
||||
'@codemirror/language@6.12.2':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/view': 6.38.8
|
||||
'@lezer/common': 1.4.0
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.4
|
||||
style-mod: 4.1.3
|
||||
|
||||
'@codemirror/language@6.12.1':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.38.8
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.8
|
||||
style-mod: 4.1.3
|
||||
|
||||
'@codemirror/lint@6.9.2':
|
||||
'@codemirror/lint@6.9.5':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.38.8
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/lint@6.9.3':
|
||||
'@codemirror/search@6.6.0':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.38.8
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/search@6.5.6':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/view': 6.38.8
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/state@6.5.2':
|
||||
dependencies:
|
||||
'@marijn/find-cluster-break': 1.0.2
|
||||
|
||||
'@codemirror/state@6.5.4':
|
||||
'@codemirror/state@6.6.0':
|
||||
dependencies:
|
||||
'@marijn/find-cluster-break': 1.0.2
|
||||
|
||||
'@codemirror/theme-one-dark@6.1.2':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/language': 6.12.2
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.38.8
|
||||
'@lezer/highlight': 1.2.3
|
||||
|
||||
'@codemirror/view@6.38.8':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/state': 6.6.0
|
||||
crelt: 1.0.6
|
||||
style-mod: 4.1.3
|
||||
w3c-keyname: 2.2.8
|
||||
@@ -16119,23 +16071,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- ws
|
||||
|
||||
'@lezer/common@1.4.0': {}
|
||||
|
||||
'@lezer/common@1.5.1': {}
|
||||
|
||||
'@lezer/highlight@1.2.3':
|
||||
dependencies:
|
||||
'@lezer/common': 1.4.0
|
||||
'@lezer/common': 1.5.1
|
||||
|
||||
'@lezer/json@1.0.3':
|
||||
dependencies:
|
||||
'@lezer/common': 1.4.0
|
||||
'@lezer/common': 1.5.1
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.4
|
||||
|
||||
'@lezer/lr@1.4.4':
|
||||
dependencies:
|
||||
'@lezer/common': 1.4.0
|
||||
'@lezer/lr': 1.4.8
|
||||
|
||||
'@lezer/lr@1.4.8':
|
||||
dependencies:
|
||||
@@ -19863,30 +19809,30 @@ snapshots:
|
||||
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260122.3
|
||||
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260122.3
|
||||
|
||||
'@uiw/codemirror-extensions-basic-setup@4.25.4(@codemirror/autocomplete@6.17.0(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/common@1.5.1))(@codemirror/commands@6.10.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)':
|
||||
'@uiw/codemirror-extensions-basic-setup@4.25.8(@codemirror/autocomplete@6.17.0(@codemirror/language@6.12.2)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)(@lezer/common@1.5.1))(@codemirror/commands@6.10.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.17.0(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/common@1.5.1)
|
||||
'@codemirror/commands': 6.10.0
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/lint': 6.9.2
|
||||
'@codemirror/search': 6.5.6
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/autocomplete': 6.17.0(@codemirror/language@6.12.2)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)(@lezer/common@1.5.1)
|
||||
'@codemirror/commands': 6.10.1
|
||||
'@codemirror/language': 6.12.2
|
||||
'@codemirror/lint': 6.9.5
|
||||
'@codemirror/search': 6.6.0
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.38.8
|
||||
|
||||
'@uiw/codemirror-themes@4.23.7(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)':
|
||||
'@uiw/codemirror-themes@4.23.7(@codemirror/language@6.12.2)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.11.3
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/language': 6.12.2
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.38.8
|
||||
|
||||
'@uiw/react-codemirror@4.25.4(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.17.0(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/common@1.5.1))(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.8)(codemirror@6.0.1(@lezer/common@1.5.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
'@uiw/react-codemirror@4.25.8(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.17.0(@codemirror/language@6.12.2)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)(@lezer/common@1.5.1))(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.38.8)(codemirror@6.0.1(@lezer/common@1.5.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@codemirror/commands': 6.10.0
|
||||
'@codemirror/state': 6.5.2
|
||||
'@codemirror/commands': 6.10.1
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/theme-one-dark': 6.1.2
|
||||
'@codemirror/view': 6.38.8
|
||||
'@uiw/codemirror-extensions-basic-setup': 4.25.4(@codemirror/autocomplete@6.17.0(@codemirror/language@6.11.3)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/common@1.5.1))(@codemirror/commands@6.10.0)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.6)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)
|
||||
'@uiw/codemirror-extensions-basic-setup': 4.25.8(@codemirror/autocomplete@6.17.0(@codemirror/language@6.12.2)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)(@lezer/common@1.5.1))(@codemirror/commands@6.10.1)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.5)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)
|
||||
codemirror: 6.0.1(@lezer/common@1.5.1)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
@@ -20857,12 +20803,12 @@ snapshots:
|
||||
|
||||
codemirror@6.0.1(@lezer/common@1.5.1):
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.17.0(@codemirror/language@6.12.1)(@codemirror/state@6.5.4)(@codemirror/view@6.38.8)(@lezer/common@1.5.1)
|
||||
'@codemirror/autocomplete': 6.17.0(@codemirror/language@6.12.2)(@codemirror/state@6.6.0)(@codemirror/view@6.38.8)(@lezer/common@1.5.1)
|
||||
'@codemirror/commands': 6.10.1
|
||||
'@codemirror/language': 6.12.1
|
||||
'@codemirror/lint': 6.9.3
|
||||
'@codemirror/search': 6.5.6
|
||||
'@codemirror/state': 6.5.4
|
||||
'@codemirror/language': 6.12.2
|
||||
'@codemirror/lint': 6.9.5
|
||||
'@codemirror/search': 6.6.0
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.38.8
|
||||
transitivePeerDependencies:
|
||||
- '@lezer/common'
|
||||
|
||||
+5
-4
@@ -29,9 +29,10 @@
|
||||
"@appsignal/opentelemetry-instrumentation-bullmq": "^0.7.3",
|
||||
"@baselime/trpc-opentelemetry-middleware": "^0.1.2",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/language": "^6.11.3",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/lint": "^6.9.5",
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
@@ -101,7 +102,7 @@
|
||||
"@trpc/react-query": "^11.8.0",
|
||||
"@trpc/server": "^11.8.0",
|
||||
"@uiw/codemirror-themes": "^4.23.7",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.8",
|
||||
"ai": "^3.4.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bullmq": "^5.34.10",
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { GripVertical, MinusCircleIcon } from "lucide-react";
|
||||
import { memo, useState, useCallback } from "react";
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { type ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||||
import {
|
||||
type ChatMessage,
|
||||
ChatMessageRole,
|
||||
@@ -24,6 +32,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/src/components/ui/select";
|
||||
import { useOptionalPlaygroundContext } from "@/src/features/playground/page/context";
|
||||
import {
|
||||
useOptionalMessageSearchActions,
|
||||
useOptionalMessageSearchPageId,
|
||||
} from "./MessageSearch";
|
||||
|
||||
type ChatMessageProps = Pick<
|
||||
MessagesContext,
|
||||
@@ -83,6 +96,17 @@ export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
|
||||
toolCallIds,
|
||||
}) => {
|
||||
const [roleIndex, setRoleIndex] = useState(1);
|
||||
const playgroundContext = useOptionalPlaygroundContext();
|
||||
const searchPageId = useOptionalMessageSearchPageId();
|
||||
const messageSearchActions = useOptionalMessageSearchActions();
|
||||
const pageId = playgroundContext?.windowId ?? searchPageId;
|
||||
const registerMessageTarget = messageSearchActions?.registerMessageTarget;
|
||||
const unregisterMessageTarget = messageSearchActions?.unregisterMessageTarget;
|
||||
const shouldUseMessageSearch = Boolean(
|
||||
pageId && registerMessageTarget && unregisterMessageTarget,
|
||||
);
|
||||
const rowRef = useRef<HTMLDivElement | null>(null);
|
||||
const editorRef = useRef<ReactCodeMirrorRef>(null);
|
||||
|
||||
const {
|
||||
attributes,
|
||||
@@ -93,6 +117,14 @@ export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
|
||||
isDragging,
|
||||
} = useSortable({ id: message.id });
|
||||
|
||||
const setCardRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
rowRef.current = node;
|
||||
setNodeRef(node);
|
||||
},
|
||||
[setNodeRef],
|
||||
);
|
||||
|
||||
const toggleRole = () => {
|
||||
// Only allow role toggling for messages that have a role property (not placeholder messages)
|
||||
if (!("role" in message)) return;
|
||||
@@ -190,9 +222,41 @@ export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
|
||||
const showToolCallSelect = message.type === ChatMessageType.ToolResult;
|
||||
const isPlaceholder = message.type === ChatMessageType.Placeholder;
|
||||
|
||||
useEffect(() => {
|
||||
if (!pageId || !registerMessageTarget || !unregisterMessageTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerMessageTarget(pageId, message.id, {
|
||||
rowRef,
|
||||
editorRef,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregisterMessageTarget(pageId, message.id);
|
||||
};
|
||||
}, [
|
||||
editorRef,
|
||||
message.id,
|
||||
pageId,
|
||||
registerMessageTarget,
|
||||
unregisterMessageTarget,
|
||||
]);
|
||||
|
||||
const handleEditorMount = useCallback(() => {
|
||||
if (!pageId || !registerMessageTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerMessageTarget(pageId, message.id, {
|
||||
rowRef,
|
||||
editorRef,
|
||||
});
|
||||
}, [message.id, pageId, registerMessageTarget]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={setNodeRef}
|
||||
ref={setCardRef}
|
||||
style={{
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
@@ -263,12 +327,18 @@ export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
|
||||
value={(message as PlaceholderMessage).name || ""}
|
||||
onChange={onPlaceholderNameChange}
|
||||
role={message.type}
|
||||
editorRef={editorRef}
|
||||
onEditorMount={handleEditorMount}
|
||||
enableSearchKeymap={!shouldUseMessageSearch}
|
||||
/>
|
||||
) : (
|
||||
<MemoizedEditor
|
||||
value={message.content}
|
||||
onChange={onValueChange}
|
||||
role={message.role}
|
||||
editorRef={editorRef}
|
||||
onEditorMount={handleEditorMount}
|
||||
enableSearchKeymap={!shouldUseMessageSearch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -296,8 +366,18 @@ const MemoizedEditor = memo(function MemoizedEditor(props: {
|
||||
value: string;
|
||||
role: ChatMessage["role"];
|
||||
onChange: (value: string) => void;
|
||||
editorRef: RefObject<ReactCodeMirrorRef | null>;
|
||||
onEditorMount: () => void;
|
||||
enableSearchKeymap: boolean;
|
||||
}) {
|
||||
const { value, role, onChange } = props;
|
||||
const {
|
||||
value,
|
||||
role,
|
||||
onChange,
|
||||
editorRef,
|
||||
onEditorMount,
|
||||
enableSearchKeymap,
|
||||
} = props;
|
||||
const placeholder = `Enter ${getRoleNamePlaceholder(role)} here.`;
|
||||
|
||||
return (
|
||||
@@ -309,6 +389,9 @@ const MemoizedEditor = memo(function MemoizedEditor(props: {
|
||||
editable={true}
|
||||
lineNumbers={false}
|
||||
placeholder={placeholder}
|
||||
editorRef={editorRef}
|
||||
enableSearchKeymap={enableSearchKeymap}
|
||||
onEditorMount={onEditorMount}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export {
|
||||
MessageSearchPageProvider,
|
||||
MessageSearchProvider,
|
||||
useMessageSearch,
|
||||
useMessageSearchActions,
|
||||
useOptionalMessageSearchActions,
|
||||
useOptionalMessageSearchPageId,
|
||||
useSyncMessageSearchMessages,
|
||||
} from "./messageSearch/context";
|
||||
export { MessageSearchToolbar } from "./messageSearch/toolbar";
|
||||
export type {
|
||||
MessageSearchController,
|
||||
MessageSearchMatch,
|
||||
MessageSearchPageLabelResolver,
|
||||
MessageSearchSnapshot,
|
||||
} from "./messageSearch/controller";
|
||||
@@ -0,0 +1,167 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { type ChatMessageWithId } from "@langfuse/shared";
|
||||
|
||||
import {
|
||||
createMessageSearchController,
|
||||
type MessageSearchController,
|
||||
type MessageSearchPageLabelResolver,
|
||||
} from "./controller";
|
||||
|
||||
const MessageSearchContext = createContext<MessageSearchController | null>(
|
||||
null,
|
||||
);
|
||||
const MessageSearchPageContext = createContext<string | null>(null);
|
||||
|
||||
export function MessageSearchProvider({
|
||||
children,
|
||||
pageIds,
|
||||
captureRootRef,
|
||||
getPageLabel,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
pageIds: string[];
|
||||
captureRootRef?: RefObject<HTMLElement | null>;
|
||||
getPageLabel?: MessageSearchPageLabelResolver;
|
||||
}) {
|
||||
const [controller] = useState(() =>
|
||||
createMessageSearchController(pageIds, getPageLabel),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
controller.setPageIds(pageIds);
|
||||
}, [controller, pageIds]);
|
||||
|
||||
useEffect(() => {
|
||||
controller.setPageLabelResolver(getPageLabel);
|
||||
}, [controller, getPageLabel]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
controller.dispose();
|
||||
};
|
||||
}, [controller]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const captureRoot = captureRootRef?.current;
|
||||
if (captureRoot) {
|
||||
const eventTarget = event.target;
|
||||
const activeElement = document.activeElement;
|
||||
const targetIsInsideRoot =
|
||||
eventTarget instanceof Node && captureRoot.contains(eventTarget);
|
||||
const activeElementIsInsideRoot =
|
||||
activeElement instanceof Node && captureRoot.contains(activeElement);
|
||||
|
||||
if (!targetIsInsideRoot && !activeElementIsInsideRoot) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
event.key.toLocaleLowerCase() === "f"
|
||||
) {
|
||||
event.preventDefault();
|
||||
controller.openSearch();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown, true);
|
||||
};
|
||||
}, [captureRootRef, controller]);
|
||||
|
||||
return (
|
||||
<MessageSearchContext.Provider value={controller}>
|
||||
{children}
|
||||
</MessageSearchContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useMessageSearchController() {
|
||||
const controller = useContext(MessageSearchContext);
|
||||
if (!controller) {
|
||||
throw new Error(
|
||||
"useMessageSearch must be used within MessageSearchProvider",
|
||||
);
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
export function useMessageSearchActions() {
|
||||
return useMessageSearchController();
|
||||
}
|
||||
|
||||
export function useOptionalMessageSearchActions() {
|
||||
return useContext(MessageSearchContext);
|
||||
}
|
||||
|
||||
export function MessageSearchPageProvider({
|
||||
children,
|
||||
pageId,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
pageId: string;
|
||||
}) {
|
||||
return (
|
||||
<MessageSearchPageContext.Provider value={pageId}>
|
||||
{children}
|
||||
</MessageSearchPageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useOptionalMessageSearchPageId() {
|
||||
return useContext(MessageSearchPageContext);
|
||||
}
|
||||
|
||||
export function useMessageSearch() {
|
||||
const controller = useMessageSearchController();
|
||||
const snapshot = useSyncExternalStore(
|
||||
controller.subscribe,
|
||||
controller.getSnapshot,
|
||||
controller.getSnapshot,
|
||||
);
|
||||
|
||||
return {
|
||||
...snapshot,
|
||||
openSearch: controller.openSearch,
|
||||
closeSearch: controller.closeSearch,
|
||||
setQueryInput: controller.setQueryInput,
|
||||
nextMatch: controller.nextMatch,
|
||||
previousMatch: controller.previousMatch,
|
||||
};
|
||||
}
|
||||
|
||||
export function useSyncMessageSearchMessages(
|
||||
pageId: string,
|
||||
messages: ChatMessageWithId[],
|
||||
) {
|
||||
const actions = useOptionalMessageSearchActions();
|
||||
|
||||
useEffect(() => {
|
||||
if (!actions) {
|
||||
return;
|
||||
}
|
||||
|
||||
actions.registerPageMessages(pageId, messages);
|
||||
|
||||
return () => {
|
||||
actions.unregisterPageMessages(pageId);
|
||||
};
|
||||
}, [actions, messages, pageId]);
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
"use client";
|
||||
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { type ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||||
import { ChatMessageType, type ChatMessageWithId } from "@langfuse/shared";
|
||||
import { type RefObject } from "react";
|
||||
|
||||
import {
|
||||
applyCodeMirrorSearchQuery,
|
||||
selectCodeMirrorRange,
|
||||
} from "@/src/components/editor";
|
||||
|
||||
export type MessageSearchMatch = {
|
||||
key: string;
|
||||
pageId: string;
|
||||
messageId: string;
|
||||
label: string;
|
||||
locationLabel: string;
|
||||
from: number;
|
||||
to: number;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type MessageSearchSnapshot = {
|
||||
isOpen: boolean;
|
||||
openRequestCount: number;
|
||||
queryInput: string;
|
||||
query: string;
|
||||
matches: MessageSearchMatch[];
|
||||
activeMatch: MessageSearchMatch | null;
|
||||
activeMatchIndex: number;
|
||||
};
|
||||
|
||||
export type MessageSearchPageLabelResolver = (
|
||||
pageId: string,
|
||||
pageIndex: number,
|
||||
) => string | null;
|
||||
|
||||
type MessageSearchState = {
|
||||
isOpen: boolean;
|
||||
openRequestCount: number;
|
||||
queryInput: string;
|
||||
searchQuery: string;
|
||||
activeMatchKey: string | null;
|
||||
matches: MessageSearchMatch[];
|
||||
pageIds: string[];
|
||||
getPageLabel?: MessageSearchPageLabelResolver;
|
||||
pageMessagesById: Record<string, ChatMessageWithId[]>;
|
||||
};
|
||||
|
||||
type MessageSearchPageTarget = {
|
||||
pageRef: RefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
type MessageSearchMessageTarget = {
|
||||
rowRef: RefObject<HTMLDivElement | null>;
|
||||
editorRef: RefObject<ReactCodeMirrorRef | null>;
|
||||
};
|
||||
|
||||
export type MessageSearchController = {
|
||||
subscribe: (listener: () => void) => () => void;
|
||||
getSnapshot: () => MessageSearchSnapshot;
|
||||
dispose: () => void;
|
||||
openSearch: () => void;
|
||||
closeSearch: () => void;
|
||||
setQueryInput: (value: string) => void;
|
||||
nextMatch: () => void;
|
||||
previousMatch: () => void;
|
||||
setPageIds: (pageIds: string[]) => void;
|
||||
setPageLabelResolver: (getPageLabel?: MessageSearchPageLabelResolver) => void;
|
||||
registerPageMessages: (pageId: string, messages: ChatMessageWithId[]) => void;
|
||||
unregisterPageMessages: (pageId: string) => void;
|
||||
registerPageTarget: (pageId: string, target: MessageSearchPageTarget) => void;
|
||||
unregisterPageTarget: (pageId: string) => void;
|
||||
registerMessageTarget: (
|
||||
pageId: string,
|
||||
messageId: string,
|
||||
target: MessageSearchMessageTarget,
|
||||
) => void;
|
||||
unregisterMessageTarget: (pageId: string, messageId: string) => void;
|
||||
};
|
||||
|
||||
const SEARCH_INPUT_DEBOUNCE_MS = 150;
|
||||
|
||||
function getMessageSearchText(message: ChatMessageWithId) {
|
||||
if (message.type === ChatMessageType.Placeholder) {
|
||||
return message.name ?? "";
|
||||
}
|
||||
|
||||
return typeof message.content === "string" ? message.content : "";
|
||||
}
|
||||
|
||||
function getMessageSearchLabel(message: ChatMessageWithId, index: number) {
|
||||
if (message.type === ChatMessageType.Placeholder) {
|
||||
return `Placeholder ${index + 1}`;
|
||||
}
|
||||
|
||||
if ("role" in message) {
|
||||
return `${capitalize(message.role)} message ${index + 1}`;
|
||||
}
|
||||
|
||||
return `Message ${index + 1}`;
|
||||
}
|
||||
|
||||
function getMatchKey(match: Omit<MessageSearchMatch, "key">) {
|
||||
return `${match.pageId}:${match.messageId}:${match.from}:${match.to}`;
|
||||
}
|
||||
|
||||
function getMessageTargetKey(pageId: string, messageId: string) {
|
||||
return `${pageId}:${messageId}`;
|
||||
}
|
||||
|
||||
function getCommittedQuery(state: MessageSearchState) {
|
||||
return state.searchQuery;
|
||||
}
|
||||
|
||||
function getActiveMatchIndex(state: MessageSearchState) {
|
||||
if (!state.activeMatchKey) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return state.matches.findIndex((match) => match.key === state.activeMatchKey);
|
||||
}
|
||||
|
||||
function getActiveMatch(state: MessageSearchState) {
|
||||
const activeMatchIndex = getActiveMatchIndex(state);
|
||||
|
||||
return activeMatchIndex >= 0
|
||||
? (state.matches[activeMatchIndex] ?? null)
|
||||
: null;
|
||||
}
|
||||
|
||||
function buildMatches(state: MessageSearchState) {
|
||||
const searchQuery = getCommittedQuery(state);
|
||||
if (!searchQuery) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lowerQuery = searchQuery.toLocaleLowerCase();
|
||||
const allMatches: MessageSearchMatch[] = [];
|
||||
|
||||
for (const [pageIndex, pageId] of state.pageIds.entries()) {
|
||||
const pageMessages = state.pageMessagesById[pageId];
|
||||
if (!pageMessages) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [messageIndex, message] of pageMessages.entries()) {
|
||||
const text = getMessageSearchText(message);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lowerText = text.toLocaleLowerCase();
|
||||
let from = lowerText.indexOf(lowerQuery);
|
||||
|
||||
while (from !== -1) {
|
||||
const label = getMessageSearchLabel(message, messageIndex);
|
||||
const pageLabel = state.getPageLabel?.(pageId, pageIndex);
|
||||
const matchWithoutKey = {
|
||||
pageId,
|
||||
messageId: message.id,
|
||||
label,
|
||||
locationLabel: pageLabel ? `${pageLabel} · ${label}` : label,
|
||||
from,
|
||||
to: from + searchQuery.length,
|
||||
text,
|
||||
};
|
||||
|
||||
allMatches.push({
|
||||
key: getMatchKey(matchWithoutKey),
|
||||
...matchWithoutKey,
|
||||
});
|
||||
|
||||
from = lowerText.indexOf(
|
||||
lowerQuery,
|
||||
from + Math.max(1, lowerQuery.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allMatches;
|
||||
}
|
||||
|
||||
function arePageIdsEqual(currentPageIds: string[], nextPageIds: string[]) {
|
||||
if (currentPageIds.length !== nextPageIds.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currentPageIds.every((pageId, index) => pageId === nextPageIds[index]);
|
||||
}
|
||||
|
||||
export function createMessageSearchController(
|
||||
initialPageIds: string[],
|
||||
initialPageLabelResolver?: MessageSearchPageLabelResolver,
|
||||
): MessageSearchController {
|
||||
const state: MessageSearchState = {
|
||||
isOpen: false,
|
||||
openRequestCount: 0,
|
||||
queryInput: "",
|
||||
searchQuery: "",
|
||||
activeMatchKey: null,
|
||||
matches: [],
|
||||
pageIds: initialPageIds,
|
||||
getPageLabel: initialPageLabelResolver,
|
||||
pageMessagesById: {},
|
||||
};
|
||||
const listeners = new Set<() => void>();
|
||||
const pageTargets = new Map<string, MessageSearchPageTarget>();
|
||||
const messageTargets = new Map<string, MessageSearchMessageTarget>();
|
||||
let pendingQueryTimeout: number | null = null;
|
||||
let cachedSnapshot: MessageSearchSnapshot = {
|
||||
isOpen: state.isOpen,
|
||||
openRequestCount: state.openRequestCount,
|
||||
queryInput: state.queryInput,
|
||||
query: getCommittedQuery(state),
|
||||
matches: state.matches,
|
||||
activeMatch: getActiveMatch(state),
|
||||
activeMatchIndex: getActiveMatchIndex(state),
|
||||
};
|
||||
|
||||
const refreshSnapshot = () => {
|
||||
cachedSnapshot = {
|
||||
isOpen: state.isOpen,
|
||||
openRequestCount: state.openRequestCount,
|
||||
queryInput: state.queryInput,
|
||||
query: getCommittedQuery(state),
|
||||
matches: state.matches,
|
||||
activeMatch: getActiveMatch(state),
|
||||
activeMatchIndex: getActiveMatchIndex(state),
|
||||
};
|
||||
};
|
||||
|
||||
const emit = () => {
|
||||
refreshSnapshot();
|
||||
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
};
|
||||
|
||||
const clearPendingQueryTimeout = () => {
|
||||
if (pendingQueryTimeout === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.clearTimeout(pendingQueryTimeout);
|
||||
pendingQueryTimeout = null;
|
||||
};
|
||||
|
||||
const syncEditorsToQuery = () => {
|
||||
const query = getCommittedQuery(state);
|
||||
|
||||
for (const target of messageTargets.values()) {
|
||||
applyCodeMirrorSearchQuery(target.editorRef, query);
|
||||
}
|
||||
};
|
||||
|
||||
const syncActiveMatchTarget = () => {
|
||||
const activeMatch = getActiveMatch(state);
|
||||
if (!activeMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
pageTargets.get(activeMatch.pageId)?.pageRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
|
||||
const messageTarget = messageTargets.get(
|
||||
getMessageTargetKey(activeMatch.pageId, activeMatch.messageId),
|
||||
);
|
||||
|
||||
messageTarget?.rowRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
inline: "nearest",
|
||||
});
|
||||
|
||||
selectCodeMirrorRange(messageTarget?.editorRef, {
|
||||
from: activeMatch.from,
|
||||
to: activeMatch.to,
|
||||
});
|
||||
};
|
||||
|
||||
const recomputeMatches = () => {
|
||||
const committedQuery = getCommittedQuery(state);
|
||||
|
||||
if (!committedQuery) {
|
||||
if (state.matches.length === 0 && state.activeMatchKey === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.matches = [];
|
||||
const previousActiveMatchKey = state.activeMatchKey;
|
||||
state.activeMatchKey = null;
|
||||
|
||||
return previousActiveMatchKey !== null;
|
||||
}
|
||||
|
||||
const previousActiveMatchKey = state.activeMatchKey;
|
||||
state.matches = buildMatches(state);
|
||||
|
||||
if (state.matches.length === 0) {
|
||||
state.activeMatchKey = null;
|
||||
} else if (
|
||||
!state.activeMatchKey ||
|
||||
!state.matches.some((match) => match.key === state.activeMatchKey)
|
||||
) {
|
||||
state.activeMatchKey = state.matches[0]?.key ?? null;
|
||||
}
|
||||
|
||||
return previousActiveMatchKey !== state.activeMatchKey;
|
||||
};
|
||||
|
||||
const refreshSearchResults = (shouldSyncEditors: boolean) => {
|
||||
const activeMatchChanged = recomputeMatches();
|
||||
|
||||
if (shouldSyncEditors) {
|
||||
syncEditorsToQuery();
|
||||
}
|
||||
|
||||
if (shouldSyncEditors || activeMatchChanged) {
|
||||
syncActiveMatchTarget();
|
||||
}
|
||||
};
|
||||
|
||||
const refreshSearchResultsIfSearching = (shouldSyncEditors: boolean) => {
|
||||
if (!getCommittedQuery(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshSearchResults(shouldSyncEditors);
|
||||
emit();
|
||||
};
|
||||
|
||||
const commitSearchQuery = (nextSearchQuery: string) => {
|
||||
if (state.searchQuery === nextSearchQuery) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state.searchQuery = nextSearchQuery;
|
||||
refreshSearchResults(true);
|
||||
return true;
|
||||
};
|
||||
|
||||
const flushPendingQuery = () => {
|
||||
if (pendingQueryTimeout === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearPendingQueryTimeout();
|
||||
|
||||
const queryChanged = commitSearchQuery(state.queryInput.trim());
|
||||
if (queryChanged) {
|
||||
emit();
|
||||
}
|
||||
|
||||
return queryChanged;
|
||||
};
|
||||
|
||||
const moveActiveMatch = (direction: 1 | -1) => {
|
||||
flushPendingQuery();
|
||||
|
||||
if (state.matches.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.activeMatchKey) {
|
||||
state.activeMatchKey =
|
||||
direction > 0
|
||||
? (state.matches[0]?.key ?? null)
|
||||
: (state.matches[state.matches.length - 1]?.key ?? null);
|
||||
} else {
|
||||
const currentIndex = state.matches.findIndex(
|
||||
(match) => match.key === state.activeMatchKey,
|
||||
);
|
||||
const fallbackIndex = direction > 0 ? 0 : state.matches.length - 1;
|
||||
const nextIndex =
|
||||
currentIndex < 0
|
||||
? fallbackIndex
|
||||
: (currentIndex + direction + state.matches.length) %
|
||||
state.matches.length;
|
||||
|
||||
state.activeMatchKey = state.matches[nextIndex]?.key ?? null;
|
||||
}
|
||||
|
||||
syncActiveMatchTarget();
|
||||
emit();
|
||||
};
|
||||
|
||||
return {
|
||||
subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
},
|
||||
|
||||
getSnapshot() {
|
||||
return cachedSnapshot;
|
||||
},
|
||||
|
||||
dispose() {
|
||||
clearPendingQueryTimeout();
|
||||
listeners.clear();
|
||||
pageTargets.clear();
|
||||
messageTargets.clear();
|
||||
},
|
||||
|
||||
openSearch() {
|
||||
state.isOpen = true;
|
||||
state.openRequestCount += 1;
|
||||
emit();
|
||||
},
|
||||
|
||||
closeSearch() {
|
||||
const hadQuery = getCommittedQuery(state).length > 0;
|
||||
clearPendingQueryTimeout();
|
||||
|
||||
state.isOpen = false;
|
||||
state.queryInput = "";
|
||||
state.searchQuery = "";
|
||||
state.matches = [];
|
||||
state.activeMatchKey = null;
|
||||
|
||||
if (hadQuery) {
|
||||
syncEditorsToQuery();
|
||||
}
|
||||
|
||||
emit();
|
||||
},
|
||||
|
||||
setQueryInput(value) {
|
||||
if (state.queryInput === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.queryInput = value;
|
||||
clearPendingQueryTimeout();
|
||||
|
||||
const nextSearchQuery = value.trim();
|
||||
if (nextSearchQuery === "") {
|
||||
commitSearchQuery("");
|
||||
emit();
|
||||
return;
|
||||
}
|
||||
|
||||
emit();
|
||||
|
||||
if (nextSearchQuery === state.searchQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingQueryTimeout = window.setTimeout(() => {
|
||||
pendingQueryTimeout = null;
|
||||
|
||||
const queryChanged = commitSearchQuery(nextSearchQuery);
|
||||
if (queryChanged) {
|
||||
emit();
|
||||
}
|
||||
}, SEARCH_INPUT_DEBOUNCE_MS);
|
||||
},
|
||||
|
||||
nextMatch() {
|
||||
moveActiveMatch(1);
|
||||
},
|
||||
|
||||
previousMatch() {
|
||||
moveActiveMatch(-1);
|
||||
},
|
||||
|
||||
setPageIds(pageIds) {
|
||||
if (arePageIdsEqual(state.pageIds, pageIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.pageIds = pageIds;
|
||||
refreshSearchResultsIfSearching(false);
|
||||
},
|
||||
|
||||
setPageLabelResolver(getPageLabel) {
|
||||
if (state.getPageLabel === getPageLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.getPageLabel = getPageLabel;
|
||||
refreshSearchResultsIfSearching(false);
|
||||
},
|
||||
|
||||
registerPageMessages(pageId, messages) {
|
||||
state.pageMessagesById[pageId] = messages;
|
||||
refreshSearchResultsIfSearching(false);
|
||||
},
|
||||
|
||||
unregisterPageMessages(pageId) {
|
||||
if (!(pageId in state.pageMessagesById)) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete state.pageMessagesById[pageId];
|
||||
refreshSearchResultsIfSearching(false);
|
||||
},
|
||||
|
||||
registerPageTarget(pageId, target) {
|
||||
pageTargets.set(pageId, target);
|
||||
|
||||
if (getActiveMatch(state)?.pageId === pageId) {
|
||||
syncActiveMatchTarget();
|
||||
}
|
||||
},
|
||||
|
||||
unregisterPageTarget(pageId) {
|
||||
pageTargets.delete(pageId);
|
||||
},
|
||||
|
||||
registerMessageTarget(pageId, messageId, target) {
|
||||
messageTargets.set(getMessageTargetKey(pageId, messageId), target);
|
||||
|
||||
applyCodeMirrorSearchQuery(target.editorRef, getCommittedQuery(state));
|
||||
|
||||
const activeMatch = getActiveMatch(state);
|
||||
if (
|
||||
activeMatch?.pageId === pageId &&
|
||||
activeMatch.messageId === messageId
|
||||
) {
|
||||
syncActiveMatchTarget();
|
||||
}
|
||||
},
|
||||
|
||||
unregisterMessageTarget(pageId, messageId) {
|
||||
messageTargets.delete(getMessageTargetKey(pageId, messageId));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Search,
|
||||
X,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
import { useMessageSearch } from "./context";
|
||||
|
||||
export function MessageSearchToolbar({ className }: { className?: string }) {
|
||||
const {
|
||||
isOpen,
|
||||
openRequestCount,
|
||||
queryInput,
|
||||
matches,
|
||||
activeMatch,
|
||||
activeMatchIndex,
|
||||
openSearch,
|
||||
closeSearch,
|
||||
setQueryInput,
|
||||
nextMatch,
|
||||
previousMatch,
|
||||
} = useMessageSearch();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, [isOpen, openRequestCount]);
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("h-8 gap-2", className)}
|
||||
onClick={openSearch}
|
||||
aria-label="Find in messages"
|
||||
>
|
||||
<Search className="h-3.5 w-3.5" />
|
||||
<span className="hidden lg:inline">Find</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const activeCountText =
|
||||
matches.length === 0 || activeMatchIndex < 0
|
||||
? "0 / 0"
|
||||
: `${activeMatchIndex + 1} / ${matches.length}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded-md border bg-background p-1",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Search className="ml-1 h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={queryInput}
|
||||
onChange={(event) => setQueryInput(event.target.value)}
|
||||
placeholder="Find in messages"
|
||||
className="h-6 min-w-40 border-0 px-1 text-xs shadow-none focus-visible:ring-0 sm:min-w-56"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
previousMatch();
|
||||
} else {
|
||||
nextMatch();
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
if (queryInput) {
|
||||
setQueryInput("");
|
||||
} else {
|
||||
closeSearch();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="min-w-16 px-1 text-right text-xs text-muted-foreground">
|
||||
{activeCountText}
|
||||
</div>
|
||||
<IconButton
|
||||
icon={ChevronUp}
|
||||
label="Previous result"
|
||||
onClick={previousMatch}
|
||||
disabled={matches.length === 0}
|
||||
/>
|
||||
<IconButton
|
||||
icon={ChevronDown}
|
||||
label="Next result"
|
||||
onClick={nextMatch}
|
||||
disabled={matches.length === 0}
|
||||
/>
|
||||
<div className="hidden max-w-48 truncate px-1 text-xs text-muted-foreground lg:block">
|
||||
{activeMatch?.locationLabel ?? "No matches"}
|
||||
</div>
|
||||
<IconButton icon={X} label="Close search" onClick={closeSearch} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IconButton({
|
||||
icon: Icon,
|
||||
label,
|
||||
onClick,
|
||||
disabled,
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={label}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -7,11 +7,17 @@ import CodeMirror, {
|
||||
type ViewUpdate,
|
||||
} from "@uiw/react-codemirror";
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { SearchQuery, search, setSearchQuery } from "@codemirror/search";
|
||||
import { json, jsonParseLinter } from "@codemirror/lang-json";
|
||||
import { linter, type Diagnostic } from "@codemirror/lint";
|
||||
import { useTheme } from "next-themes";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
useState,
|
||||
useCallback,
|
||||
type MutableRefObject,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { LanguageSupport, StreamLanguage } from "@codemirror/language";
|
||||
import type { StringStream } from "@codemirror/language";
|
||||
import {
|
||||
@@ -159,6 +165,44 @@ const bidiSupport = [
|
||||
),
|
||||
];
|
||||
|
||||
export function applyCodeMirrorSearchQuery(
|
||||
editorRef: RefObject<ReactCodeMirrorRef | null> | undefined,
|
||||
searchValue: string,
|
||||
) {
|
||||
const view = editorRef?.current?.view;
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
effects: setSearchQuery.of(
|
||||
new SearchQuery({
|
||||
search: searchValue,
|
||||
caseSensitive: false,
|
||||
literal: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export function selectCodeMirrorRange(
|
||||
editorRef: RefObject<ReactCodeMirrorRef | null> | undefined,
|
||||
range: { from: number; to: number } | null,
|
||||
) {
|
||||
const view = editorRef?.current?.view;
|
||||
if (!view || !range) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
selection: {
|
||||
anchor: range.from,
|
||||
head: range.to,
|
||||
},
|
||||
scrollIntoView: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function CodeMirrorEditor({
|
||||
value,
|
||||
onChange,
|
||||
@@ -172,6 +216,8 @@ export function CodeMirrorEditor({
|
||||
maxHeight,
|
||||
placeholder,
|
||||
editorRef,
|
||||
enableSearchKeymap = true,
|
||||
onEditorMount,
|
||||
}: {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
@@ -184,7 +230,9 @@ export function CodeMirrorEditor({
|
||||
minHeight?: number | string;
|
||||
maxHeight?: number | string;
|
||||
placeholder?: string;
|
||||
editorRef?: React.RefObject<ReactCodeMirrorRef | null>;
|
||||
editorRef?: RefObject<ReactCodeMirrorRef | null>;
|
||||
enableSearchKeymap?: boolean;
|
||||
onEditorMount?: () => void;
|
||||
}) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const codeMirrorTheme = resolvedTheme === "dark" ? darkTheme : lightTheme;
|
||||
@@ -193,18 +241,34 @@ export function CodeMirrorEditor({
|
||||
!!value && value !== "",
|
||||
);
|
||||
|
||||
const handleEditorRef = useCallback(
|
||||
(instance: ReactCodeMirrorRef | null) => {
|
||||
if (editorRef) {
|
||||
(editorRef as MutableRefObject<ReactCodeMirrorRef | null>).current =
|
||||
instance;
|
||||
}
|
||||
|
||||
if (instance) {
|
||||
onEditorMount?.();
|
||||
}
|
||||
},
|
||||
[editorRef, onEditorMount],
|
||||
);
|
||||
|
||||
return (
|
||||
<CodeMirror
|
||||
value={value}
|
||||
theme={codeMirrorTheme}
|
||||
ref={editorRef}
|
||||
ref={editorRef || onEditorMount ? handleEditorRef : undefined}
|
||||
basicSetup={{
|
||||
foldGutter: lineNumbers,
|
||||
highlightActiveLine: false,
|
||||
lineNumbers: lineNumbers,
|
||||
searchKeymap: enableSearchKeymap,
|
||||
}}
|
||||
lang={mode === "json" ? "json" : undefined}
|
||||
extensions={[
|
||||
search(),
|
||||
// RTL/bidi support - must be early for proper line decoration
|
||||
...bidiSupport,
|
||||
// Remove outline if field is focussed
|
||||
|
||||
@@ -96,6 +96,7 @@ export const MessagePlaceholderComponent: React.FC<{
|
||||
editable={true}
|
||||
lineNumbers={false}
|
||||
placeholder={`[\n {\n "role": "user",\n "content": "Hello!"\n },\n {\n "role": "assistant",\n "content": "Hi there!"\n }\n]`}
|
||||
enableSearchKeymap={false}
|
||||
/>
|
||||
|
||||
{hasConflict && (
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ModelParameters } from "@/src/components/ModelParameters";
|
||||
import { usePlaygroundContext } from "../context";
|
||||
import { Messages } from "@/src/features/playground/page/components/Messages";
|
||||
import { ConfigurationDropdowns } from "@/src/features/playground/page/components/ConfigurationDropdowns";
|
||||
import { useMessageSearchActions } from "@/src/components/ChatMessages/MessageSearch";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -153,6 +154,9 @@ function PlaygroundWindowContent({
|
||||
isMobile?: boolean;
|
||||
}) {
|
||||
const playgroundContext = usePlaygroundContext();
|
||||
const { registerPageTarget, unregisterPageTarget } =
|
||||
useMessageSearchActions();
|
||||
const windowContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
onRemove(windowId);
|
||||
@@ -162,8 +166,21 @@ function PlaygroundWindowContent({
|
||||
onCopy(windowId);
|
||||
}, [windowId, onCopy]);
|
||||
|
||||
useEffect(() => {
|
||||
registerPageTarget(windowId, {
|
||||
pageRef: windowContainerRef,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregisterPageTarget(windowId);
|
||||
};
|
||||
}, [registerPageTarget, unregisterPageTarget, windowId]);
|
||||
|
||||
return (
|
||||
<div className="playground-window flex h-full min-w-0 flex-col rounded-lg border bg-background shadow-sm @container">
|
||||
<div
|
||||
ref={windowContainerRef}
|
||||
className="playground-window flex h-full min-w-0 flex-col rounded-lg border bg-background shadow-sm @container"
|
||||
>
|
||||
{/* Window Header */}
|
||||
<div className="relative flex-shrink-0 border-b bg-muted/50 px-3 py-1">
|
||||
<div className="flex items-center pr-32 @xl:pr-96">
|
||||
|
||||
@@ -69,6 +69,7 @@ export const PromptVariableComponent: React.FC<{
|
||||
editable={true}
|
||||
lineNumbers={false}
|
||||
placeholder={name}
|
||||
enableSearchKeymap={false}
|
||||
/>
|
||||
|
||||
{hasConflict && (
|
||||
|
||||
@@ -47,9 +47,11 @@ import {
|
||||
getPlaygroundEventBus,
|
||||
useWindowCoordination,
|
||||
} from "@/src/features/playground/page/hooks/useWindowCoordination";
|
||||
import { useSyncMessageSearchMessages } from "@/src/components/ChatMessages/MessageSearch";
|
||||
import { getFinalModelParams } from "@/src/utils/getFinalModelParams";
|
||||
|
||||
type PlaygroundContextType = {
|
||||
windowId: string;
|
||||
promptVariables: PromptVariable[];
|
||||
updatePromptVariableValue: (variable: string, value: string) => void;
|
||||
deletePromptVariable: (variable: string) => void;
|
||||
@@ -88,10 +90,13 @@ export const usePlaygroundContext = () => {
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useOptionalPlaygroundContext = () => useContext(PlaygroundContext);
|
||||
|
||||
export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({
|
||||
children,
|
||||
windowId,
|
||||
}) => {
|
||||
const effectiveWindowId = windowId || MULTI_WINDOW_CONFIG.DEFAULT_WINDOW_ID;
|
||||
const capture = usePostHogClientCapture();
|
||||
const projectId = useProjectIdFromURL();
|
||||
const { playgroundCache, setPlaygroundCache } = usePlaygroundCache(windowId);
|
||||
@@ -233,6 +238,7 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({
|
||||
}, [messages]);
|
||||
|
||||
useEffect(updatePromptVariables, [messages, updatePromptVariables]);
|
||||
useSyncMessageSearchMessages(effectiveWindowId, messages);
|
||||
|
||||
const addMessage: PlaygroundContextType["addMessage"] = useCallback(
|
||||
(message) => {
|
||||
@@ -553,7 +559,6 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({
|
||||
// This effect registers the window with the global coordination system
|
||||
// and sets up event listeners for global actions like "Run All" and "Stop All"
|
||||
useEffect(() => {
|
||||
const effectiveWindowId = windowId || MULTI_WINDOW_CONFIG.DEFAULT_WINDOW_ID;
|
||||
const playgroundEventBus = getPlaygroundEventBus();
|
||||
|
||||
const playgroundHandle: PlaygroundHandle = {
|
||||
@@ -620,7 +625,7 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({
|
||||
);
|
||||
};
|
||||
}, [
|
||||
windowId,
|
||||
effectiveWindowId,
|
||||
handleSubmit,
|
||||
registerWindow,
|
||||
unregisterWindow,
|
||||
@@ -638,12 +643,12 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({
|
||||
playgroundEventBus.dispatchEvent(
|
||||
new CustomEvent(PLAYGROUND_EVENTS.WINDOW_EXECUTION_STATE_CHANGE, {
|
||||
detail: {
|
||||
windowId: windowId || MULTI_WINDOW_CONFIG.DEFAULT_WINDOW_ID,
|
||||
windowId: effectiveWindowId,
|
||||
isStreaming,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [windowId, isStreaming]);
|
||||
}, [effectiveWindowId, isStreaming]);
|
||||
|
||||
// Notify when model configuration changes
|
||||
useEffect(() => {
|
||||
@@ -651,18 +656,19 @@ export const PlaygroundProvider: React.FC<PlaygroundProviderProps> = ({
|
||||
playgroundEventBus.dispatchEvent(
|
||||
new CustomEvent(PLAYGROUND_EVENTS.WINDOW_MODEL_CONFIG_CHANGE, {
|
||||
detail: {
|
||||
windowId: windowId || MULTI_WINDOW_CONFIG.DEFAULT_WINDOW_ID,
|
||||
windowId: effectiveWindowId,
|
||||
hasModel: Boolean(
|
||||
modelParams.provider.value && modelParams.model.value,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [windowId, modelParams.provider.value, modelParams.model.value]);
|
||||
}, [effectiveWindowId, modelParams.provider.value, modelParams.model.value]);
|
||||
|
||||
return (
|
||||
<PlaygroundContext.Provider
|
||||
value={{
|
||||
windowId: effectiveWindowId,
|
||||
promptVariables,
|
||||
updatePromptVariableValue,
|
||||
deletePromptVariable,
|
||||
|
||||
@@ -9,6 +9,10 @@ import { type MultiWindowState } from "@/src/features/playground/page/types";
|
||||
import Page from "@/src/components/layouts/page";
|
||||
import MultiWindowPlayground from "@/src/features/playground/page/components/MultiWindowPlayground";
|
||||
import { NoModelConfiguredAlert } from "@/src/features/playground/page/components/NoModelConfiguredAlert";
|
||||
import {
|
||||
MessageSearchProvider,
|
||||
MessageSearchToolbar,
|
||||
} from "@/src/components/ChatMessages/MessageSearch";
|
||||
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
|
||||
|
||||
/**
|
||||
@@ -83,6 +87,12 @@ export default function PlaygroundPage() {
|
||||
executeAllWindows();
|
||||
});
|
||||
|
||||
const getMessageSearchPageLabel = useCallback(
|
||||
(_pageId: string, pageIndex: number) =>
|
||||
windowIds.length > 1 ? `Window ${pageIndex + 1}` : null,
|
||||
[windowIds.length],
|
||||
);
|
||||
|
||||
// Don't render until window IDs are loaded
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
@@ -117,75 +127,82 @@ export default function PlaygroundPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Page
|
||||
scrollable={false}
|
||||
withPadding={false}
|
||||
headerProps={{
|
||||
title: "Playground",
|
||||
help: {
|
||||
description:
|
||||
"A sandbox to test and iterate your prompts across multiple windows",
|
||||
href: "https://langfuse.com/docs/prompt-management/features/playground",
|
||||
},
|
||||
actionButtonsRight: (
|
||||
<div className="flex flex-nowrap items-center gap-2">
|
||||
{/* Window Count Display - Hidden on mobile */}
|
||||
<div className="hidden items-center gap-2 text-sm text-muted-foreground md:flex">
|
||||
<span className="whitespace-nowrap">
|
||||
{windowIds.length} window
|
||||
{windowIds.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
{executionStatus && (
|
||||
<>
|
||||
<span className="hidden sm:inline">•</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span className="hidden whitespace-nowrap sm:inline">
|
||||
{executionStatus}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Multi-Window Controls - Hidden on mobile */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleExecuteAll}
|
||||
disabled={isRunAllDisabled}
|
||||
className="hidden flex-shrink-0 gap-1 md:flex"
|
||||
title={
|
||||
!hasAnyModelConfigured
|
||||
? "Please configure a model in Project Settings first"
|
||||
: "Execute all playground windows simultaneously"
|
||||
}
|
||||
>
|
||||
{globalIsExecutingAll ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Play className="h-3 w-3" />
|
||||
)}
|
||||
<span className="hidden lg:inline">Run All (Ctrl + Enter)</span>
|
||||
</Button>
|
||||
|
||||
{/* Reset Playground Button */}
|
||||
<ResetPlaygroundButton />
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
<MessageSearchProvider
|
||||
pageIds={windowIds}
|
||||
getPageLabel={getMessageSearchPageLabel}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
{!hasAnyModelConfigured && projectId && (
|
||||
<NoModelConfiguredAlert projectId={projectId} />
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<MultiWindowPlayground
|
||||
windowState={windowState}
|
||||
onRemoveWindow={removeWindow}
|
||||
onAddWindow={addWindow}
|
||||
/>
|
||||
<Page
|
||||
scrollable={false}
|
||||
withPadding={false}
|
||||
headerProps={{
|
||||
title: "Playground",
|
||||
help: {
|
||||
description:
|
||||
"A sandbox to test and iterate your prompts across multiple windows",
|
||||
href: "https://langfuse.com/docs/prompt-management/features/playground",
|
||||
},
|
||||
actionButtonsRight: (
|
||||
<div className="flex flex-nowrap items-center gap-2">
|
||||
<MessageSearchToolbar className="max-w-[24rem]" />
|
||||
|
||||
{/* Window Count Display - Hidden on mobile */}
|
||||
<div className="hidden items-center gap-2 text-sm text-muted-foreground md:flex">
|
||||
<span className="whitespace-nowrap">
|
||||
{windowIds.length} window
|
||||
{windowIds.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
{executionStatus && (
|
||||
<>
|
||||
<span className="hidden sm:inline">•</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span className="hidden whitespace-nowrap sm:inline">
|
||||
{executionStatus}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Multi-Window Controls - Hidden on mobile */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleExecuteAll}
|
||||
disabled={isRunAllDisabled}
|
||||
className="hidden flex-shrink-0 gap-1 md:flex"
|
||||
title={
|
||||
!hasAnyModelConfigured
|
||||
? "Please configure a model in Project Settings first"
|
||||
: "Execute all playground windows simultaneously"
|
||||
}
|
||||
>
|
||||
{globalIsExecutingAll ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Play className="h-3 w-3" />
|
||||
)}
|
||||
<span className="hidden lg:inline">Run All (Ctrl + Enter)</span>
|
||||
</Button>
|
||||
|
||||
{/* Reset Playground Button */}
|
||||
<ResetPlaygroundButton />
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
{!hasAnyModelConfigured && projectId && (
|
||||
<NoModelConfiguredAlert projectId={projectId} />
|
||||
)}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<MultiWindowPlayground
|
||||
windowState={windowState}
|
||||
onRemoveWindow={removeWindow}
|
||||
onAddWindow={addWindow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
</Page>
|
||||
</MessageSearchProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
|
||||
@@ -15,6 +15,12 @@ import {
|
||||
|
||||
import { type NewPromptFormSchemaType } from "./validation";
|
||||
import { PromptSelectionDialog } from "../PromptSelectionDialog";
|
||||
import {
|
||||
MessageSearchPageProvider,
|
||||
MessageSearchProvider,
|
||||
MessageSearchToolbar,
|
||||
useSyncMessageSearchMessages,
|
||||
} from "@/src/components/ChatMessages/MessageSearch";
|
||||
|
||||
import type { ControllerRenderProps } from "react-hook-form";
|
||||
import type { MessagesContext } from "@/src/components/ChatMessages/types";
|
||||
@@ -24,11 +30,24 @@ type PromptChatMessagesProps = ControllerRenderProps<
|
||||
"chatPrompt"
|
||||
> & { initialMessages: unknown; projectId: string | undefined };
|
||||
|
||||
const PROMPT_CHAT_MESSAGES_SEARCH_PAGE_ID = "prompt-chat-messages";
|
||||
|
||||
const PromptChatMessagesSearchSync = ({
|
||||
messages,
|
||||
}: {
|
||||
messages: ChatMessageWithId[];
|
||||
}) => {
|
||||
useSyncMessageSearchMessages(PROMPT_CHAT_MESSAGES_SEARCH_PAGE_ID, messages);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const PromptChatMessages: React.FC<PromptChatMessagesProps> = ({
|
||||
onChange,
|
||||
initialMessages,
|
||||
projectId,
|
||||
}) => {
|
||||
const searchRootRef = useRef<HTMLDivElement | null>(null);
|
||||
const [messages, setMessages] = useState<ChatMessageWithId[]>([]);
|
||||
const [availableRoles, setAvailableRoles] = useState<string[]>([]);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
@@ -123,38 +142,51 @@ export const PromptChatMessages: React.FC<PromptChatMessagesProps> = ({
|
||||
}, [messages, onChange]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="my-2 flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex items-center gap-1 px-2 py-1"
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-xs">Add prompt reference</span>
|
||||
</Button>
|
||||
<MessageSearchProvider
|
||||
pageIds={[PROMPT_CHAT_MESSAGES_SEARCH_PAGE_ID]}
|
||||
captureRootRef={searchRootRef}
|
||||
>
|
||||
<MessageSearchPageProvider pageId={PROMPT_CHAT_MESSAGES_SEARCH_PAGE_ID}>
|
||||
<PromptChatMessagesSearchSync messages={messages} />
|
||||
|
||||
{projectId && (
|
||||
<PromptSelectionDialog
|
||||
isOpen={isDialogOpen}
|
||||
onClose={() => setIsDialogOpen(false)}
|
||||
projectId={projectId}
|
||||
<div ref={searchRootRef}>
|
||||
<div className="my-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<MessageSearchToolbar />
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex items-center gap-1 px-2 py-1"
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span className="text-xs">Add prompt reference</span>
|
||||
</Button>
|
||||
|
||||
{projectId && (
|
||||
<PromptSelectionDialog
|
||||
isOpen={isDialogOpen}
|
||||
onClose={() => setIsDialogOpen(false)}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChatMessages
|
||||
{...{
|
||||
messages,
|
||||
addMessage,
|
||||
setMessages,
|
||||
deleteMessage,
|
||||
updateMessage,
|
||||
replaceMessage,
|
||||
availableRoles,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ChatMessages
|
||||
{...{
|
||||
messages,
|
||||
addMessage,
|
||||
setMessages,
|
||||
deleteMessage,
|
||||
updateMessage,
|
||||
replaceMessage,
|
||||
availableRoles,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MessageSearchPageProvider>
|
||||
</MessageSearchProvider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user