mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-11 06:00:15 -07:00
Compare commits
777 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f86ad36ef | |||
| cd421b2f6e | |||
| 41b66e009a | |||
| d96467f850 | |||
| 258b19e2f4 | |||
| 01a87158b9 | |||
| 46b768f9cf | |||
| 4f0b6a5d09 | |||
| bfbcdb70fe | |||
| 2c4602cf39 | |||
| 68f5d3946b | |||
| 14724a864c | |||
| 5e2b196c4d | |||
| e7686f60b1 | |||
| 380f666d35 | |||
| 3e277b1a35 | |||
| 9bac46ea75 | |||
| 2af4ee7dbe | |||
| 590fc0d021 | |||
| 746b0dad40 | |||
| b327da3ded | |||
| 7a818bdcd0 | |||
| c92ee2e22a | |||
| 632a4a565f | |||
| d712c275a0 | |||
| 1adff7481b | |||
| 393e19dda9 | |||
| 39c5c7c9ba | |||
| e64a5e59ef | |||
| 574c2cf0e3 | |||
| 1d3321b336 | |||
| de25e5822d | |||
| 6a904c9f37 | |||
| 30c3283572 | |||
| 10bba19079 | |||
| 713079f2f2 | |||
| 6e16e989ac | |||
| 4e89e0b1e4 | |||
| 6067c289ab | |||
| 32ebfea9cb | |||
| dec280d54d | |||
| 4823e97783 | |||
| 6a5685995f | |||
| 3b4d5691d7 | |||
| 45c09d021a | |||
| 8ef759fe0f | |||
| c06735fdd2 | |||
| cf4297a1af | |||
| 5d458d68bd | |||
| c3db077ae8 | |||
| 779b265b20 | |||
| 7d6d7e619b | |||
| 34caa16e39 | |||
| 976426dbd3 | |||
| b4d8e39d56 | |||
| 11c41e7381 | |||
| 358d8a54ff | |||
| 3c8fedce68 | |||
| 1744b5b9d0 | |||
| 0c15476dd2 | |||
| 94ef76c67e | |||
| bd5bf6f94f | |||
| 1fbf454c3c | |||
| 07b62fe5c1 | |||
| 7fbf6ee2e8 | |||
| ba66fc30c5 | |||
| 45b7ed3220 | |||
| 20f1c4c0ae | |||
| 97b6fc5e2b | |||
| 44d8c30187 | |||
| e3957bf08b | |||
| acfe0aba21 | |||
| 6247b5411b | |||
| 5cc0b0a011 | |||
| 1fed2fb18c | |||
| 8a0e7a4e3d | |||
| 29a784c6c6 | |||
| 409a3ee194 | |||
| 54caa3e01a | |||
| e1a723a39f | |||
| 463ea35d7c | |||
| f751c91c68 | |||
| ad24c8771a | |||
| 6f82e2c3ed | |||
| f4b39071f0 | |||
| 621c968f3f | |||
| 564cf0fed0 | |||
| dee9492d4c | |||
| 6ae026f7c5 | |||
| 6bcbe286f3 | |||
| 6f35f72607 | |||
| 3a7aa75538 | |||
| e4e7ac260a | |||
| b8aaa4bb42 | |||
| 7793e2694b | |||
| 83f2c72f29 | |||
| 1caeaee7f0 | |||
| f354134234 | |||
| 66219d30e0 | |||
| b9e3942ed8 | |||
| 2354cdc1d1 | |||
| d929438df9 | |||
| 1acaed1de7 | |||
| 16195f8a55 | |||
| d7fc8c178f | |||
| 2894e16706 | |||
| c2340f3432 | |||
| 3b7b3106db | |||
| cff92819f9 | |||
| 2f981d852b | |||
| 8eef74d776 | |||
| 60e46204dc | |||
| 6a5d783435 | |||
| 0223e076c4 | |||
| ce80c78319 | |||
| cc0085ae61 | |||
| f28e243b9d | |||
| 2e4532593f | |||
| 1f10905a03 | |||
| 88762db484 | |||
| 3b5ab0ac70 | |||
| 8903c9296b | |||
| 97858a3c94 | |||
| 0ec3e83518 | |||
| 8c007b5bf7 | |||
| 768236b0e2 | |||
| 495d78b885 | |||
| 34b1e515fe | |||
| 79edbd3fa5 | |||
| f50d9994e2 | |||
| 1603d3085f | |||
| ccf7036f33 | |||
| a0a1a5e3c1 | |||
| fbf9120859 | |||
| 8a770beec3 | |||
| 6b31669765 | |||
| 26d72fc2d8 | |||
| 5eb56d0994 | |||
| dbc4a922d5 | |||
| 141f423842 | |||
| 667f2433ab | |||
| fd930ef548 | |||
| 7eadfb1a63 | |||
| 67cb07ac92 | |||
| 96d28c43fc | |||
| e57e3f5f0a | |||
| 7b99bd71da | |||
| 861a037321 | |||
| 84cbe6c434 | |||
| 2cbb811523 | |||
| 8ef4faa10f | |||
| f6a1c9bf52 | |||
| 5d9f6fb4f5 | |||
| 66840a8ecd | |||
| a8ee6b255a | |||
| bd73d1c533 | |||
| e33c0ebc42 | |||
| 57e4a35fee | |||
| d490b57410 | |||
| 0416602e5f | |||
| ddc27b2eb9 | |||
| 374deb147b | |||
| d2eabd1ad0 | |||
| efbc625cc3 | |||
| 91ae0b8cb0 | |||
| ddc5741b00 | |||
| 4729aca6b0 | |||
| bb4fc3a70c | |||
| 5d8084b650 | |||
| f316b892f5 | |||
| cbda1d7cd0 | |||
| 2f8e879976 | |||
| cc0ac5ae3c | |||
| 0185d24fb3 | |||
| 97dbdc9c31 | |||
| a07c66c9a3 | |||
| 308bd25bc0 | |||
| 70066a03b6 | |||
| a7f3872af3 | |||
| 22e10e675a | |||
| 89679e946d | |||
| 1d1bb9d3df | |||
| 8faf2b2595 | |||
| e47ad9700e | |||
| 372b19a057 | |||
| cbe156a868 | |||
| 181a3881e2 | |||
| 3eef03b303 | |||
| ad56e3165c | |||
| b1a96b6e75 | |||
| 56419b1b4e | |||
| 372f14a9c5 | |||
| e1ec56a120 | |||
| 5bb11249d6 | |||
| 9fbcca1ff2 | |||
| 323f2b2c3e | |||
| b971d38dd5 | |||
| 278f479a3a | |||
| 03aea5678d | |||
| b62b8ee7e6 | |||
| 63f55551e5 | |||
| b185fbc57d | |||
| ceb9d58e72 | |||
| a0bb515a4f | |||
| 2cfac2f18b | |||
| d412f538b2 | |||
| 94f90ad861 | |||
| 4a402e7937 | |||
| c226d6c391 | |||
| 67410e6c59 | |||
| 419c361147 | |||
| 3769a53ffa | |||
| ec4aaa3bfb | |||
| be52680fcd | |||
| 9d41ab9339 | |||
| f126fc3087 | |||
| 764377037c | |||
| 8e09eaab45 | |||
| 6523da186c | |||
| 6471fd8b6f | |||
| 247a74881a | |||
| 3ef09f0a5f | |||
| b924d331f9 | |||
| 14041b6012 | |||
| 2c6cc5ecec | |||
| ac022b1df0 | |||
| 0a2081de08 | |||
| 64a8e554c7 | |||
| 082d29fd2f | |||
| ba5cf9d002 | |||
| 57a55318df | |||
| e70f4f7a59 | |||
| 1d217fad67 | |||
| e95d46f085 | |||
| f4577878e1 | |||
| 1bd1e5c8e3 | |||
| c975dee965 | |||
| 9d690f4219 | |||
| 29ddb3f58d | |||
| 8626bc0b1c | |||
| c362cf6596 | |||
| 97264fc5ff | |||
| 494c4409c1 | |||
| d46e366c81 | |||
| 6afe33ee9c | |||
| 903c9e1cc3 | |||
| 3ef43fc3f5 | |||
| b1c3be05dd | |||
| efee23b4a7 | |||
| 06b67a7586 | |||
| 889a2dbf9d | |||
| 2f80fcc888 | |||
| f7ee479c1d | |||
| 94fa0981fe | |||
| 4c74afe438 | |||
| f76cea22de | |||
| 3d49110808 | |||
| 88a4579f7a | |||
| 241bde0333 | |||
| 73c7867cd6 | |||
| b35254f7ad | |||
| 213e78c956 | |||
| 7bf552c491 | |||
| 3bf9923f86 | |||
| a6a8a28f59 | |||
| 56a8e452bf | |||
| 6bec0bf70d | |||
| 5dc9c8f90e | |||
| e3290e12b1 | |||
| 9f37ce9e42 | |||
| 8904c0c811 | |||
| b0d021b7f2 | |||
| 0175f3b8a1 | |||
| 0fa9d5bf62 | |||
| 4919e38e3e | |||
| 2e99533f96 | |||
| f095645d89 | |||
| 757c83142e | |||
| 36d274ca9f | |||
| ec11b61f67 | |||
| 7765271d63 | |||
| 7c2464bba7 | |||
| 17e010f93c | |||
| 452d630a2a | |||
| f317a3e38f | |||
| f56195058e | |||
| 2e93dbb10c | |||
| f862456d73 | |||
| d99b0b2137 | |||
| 1d390f9aa7 | |||
| 514beb7940 | |||
| c7bdfce734 | |||
| e5fe4b06ad | |||
| 89b7c265d3 | |||
| 698c31943e | |||
| b70060d46e | |||
| 6ddc5ef53e | |||
| 212023c7e4 | |||
| b687f23c95 | |||
| 7a05d01554 | |||
| 78e3a57857 | |||
| 79d0c96b20 | |||
| 21ed38a20e | |||
| d8b1f99114 | |||
| b0fb1b9890 | |||
| a63932cff2 | |||
| 0b22165d2a | |||
| 41b1951abe | |||
| 353431e54c | |||
| 7b232dd7d8 | |||
| d32adf9dbf | |||
| 940d490217 | |||
| 46e41e38cf | |||
| 276ff8f995 | |||
| 030837fccf | |||
| a7d38aefb1 | |||
| 230a0d7caf | |||
| 6e14e43c78 | |||
| e6389f08be | |||
| a4edeb098e | |||
| 093c536415 | |||
| 7479b50fea | |||
| ebce36d043 | |||
| 77bab1aa74 | |||
| ebcac3405c | |||
| d2781a6f87 | |||
| f5954f5bb3 | |||
| 6baf694d6f | |||
| cb3b586d4d | |||
| f68789ab20 | |||
| 0c6a3f1917 | |||
| 05fccaf982 | |||
| 7340b9ecc2 | |||
| 78eb4ebe0b | |||
| b1453a34ec | |||
| c357e9e2f5 | |||
| 98717bf8a9 | |||
| d7077ada0e | |||
| 64f63ed1d3 | |||
| 2a27f6c30d | |||
| 9fdddeaba8 | |||
| 2cfa5e93e4 | |||
| 778ac14344 | |||
| 85fcf8be61 | |||
| b31eb09015 | |||
| 5154dd1740 | |||
| 274f11ef1d | |||
| aeb1acf458 | |||
| a204f4a58e | |||
| 8e4a57aa01 | |||
| 797ed0a553 | |||
| 663bc0d471 | |||
| 8d7e2d2c46 | |||
| 19d96bb30b | |||
| 47f2f20d9c | |||
| 12c7c634c0 | |||
| 9a322c150a | |||
| 1a3bc4f666 | |||
| d4881b1ce5 | |||
| a2ad2df473 | |||
| 541c5bd1c3 | |||
| b744e9673b | |||
| bb94b7c5c6 | |||
| e9ff57d5e1 | |||
| 179245457c | |||
| 1493f74691 | |||
| 4857503ed3 | |||
| a0e38b4f0c | |||
| 1d62cad9e9 | |||
| 855761020c | |||
| 0950d06dfb | |||
| 1496402325 | |||
| 77e2c4babb | |||
| a465082984 | |||
| 025fdac686 | |||
| 6bde5ec64c | |||
| f099a9ec39 | |||
| 5bfcef92ee | |||
| 79a8fbd881 | |||
| 7f96a14cf6 | |||
| 5fe6d70713 | |||
| dcba4dd4bc | |||
| ccbe77913b | |||
| 2844cb81c2 | |||
| d86e8e5920 | |||
| 9665fa1eb4 | |||
| 2788ef679b | |||
| e1a88e1fd8 | |||
| 32163c5302 | |||
| 2d3d5efe87 | |||
| e1bbba392c | |||
| ed642c856b | |||
| 927e462f7a | |||
| e250499a3b | |||
| 91d96a6639 | |||
| 104ec4c87c | |||
| 0a7e8436c3 | |||
| 9e597e0a28 | |||
| 01fbb5d47c | |||
| 6517d16337 | |||
| 0e636adf28 | |||
| 0bb281237b | |||
| 2b224376c2 | |||
| e510b369d7 | |||
| a0de1f7230 | |||
| 4591132269 | |||
| a03de8d490 | |||
| 27bcfec17e | |||
| f6dbec3e1d | |||
| aebc45f705 | |||
| 310c60b9d9 | |||
| bcba67c209 | |||
| fc013aed52 | |||
| 8ad41c059b | |||
| 8eaf8db850 | |||
| 896883766c | |||
| 258dacf3ed | |||
| 242243f485 | |||
| a18436dce1 | |||
| 5323cbc00e | |||
| ddd3b137ac | |||
| 94550088e5 | |||
| 1375ca6f5c | |||
| e4c4fe0495 | |||
| 2fa5277e56 | |||
| b73ad8fdc1 | |||
| 9cc281e65e | |||
| d62107d39b | |||
| 4a8d20ad72 | |||
| 5acb72c39b | |||
| 67e8236a60 | |||
| 18b8853f82 | |||
| 65c7df7938 | |||
| 15678cdfa2 | |||
| 6cd6c62046 | |||
| dbf92805a2 | |||
| 11fc9a7b85 | |||
| 8bc970ff57 | |||
| a16eefd97b | |||
| ca5e5b820c | |||
| f73ad52441 | |||
| 729ec1d1bf | |||
| 4adb30b861 | |||
| 999f6de45f | |||
| 70686502b4 | |||
| d17a980151 | |||
| 7fa5947030 | |||
| de8f120fd4 | |||
| 9b54603264 | |||
| 698c77d7ba | |||
| 18d83a4d18 | |||
| 8e849d93b2 | |||
| 4ca42f028b | |||
| 3118337879 | |||
| db4490affb | |||
| 51ab79384e | |||
| 3ee30a252d | |||
| b883566ebb | |||
| ac78fb85b8 | |||
| 0d2b11d0c4 | |||
| 5b610c88c1 | |||
| bf444ce043 | |||
| c91c027dab | |||
| 81fd87c510 | |||
| 9da174a962 | |||
| 84f54a7e65 | |||
| baeecf1464 | |||
| f2fdd39c96 | |||
| 53b074d78e | |||
| f4fc1e6775 | |||
| dba791b8db | |||
| 750fa02621 | |||
| 7a67816111 | |||
| 613625644e | |||
| 0e25071ef0 | |||
| ed1932cd26 | |||
| 67b89213d0 | |||
| 814f142c5f | |||
| 16cd3e7d5a | |||
| c5dcb8faef | |||
| 6b46c022f9 | |||
| 88ef05fc72 | |||
| 445ea367fc | |||
| c819554f43 | |||
| bbc8a79ded | |||
| 3d181bc10d | |||
| ba5478f382 | |||
| 136c993c8d | |||
| 6cf18ea4e8 | |||
| fe7f56c82e | |||
| 6c580f1e43 | |||
| f171cd4f03 | |||
| ea109e6c30 | |||
| f514eed226 | |||
| 274ba80149 | |||
| 46b4dfc458 | |||
| 4af8f4ff6a | |||
| df5810d695 | |||
| d9ad96c374 | |||
| 06cc93fd82 | |||
| 41da63765f | |||
| 3975411c78 | |||
| fc2e75ef61 | |||
| ef0f2dd3d0 | |||
| 548c3c5d72 | |||
| d2e3a0cb8e | |||
| 9cdace6f81 | |||
| 12f020570e | |||
| bef2551eec | |||
| 7e20f8c189 | |||
| 56e8390e55 | |||
| 89fff16385 | |||
| 2cf15a24eb | |||
| 512e867034 | |||
| ce8c55c3c7 | |||
| 8e0d904d9a | |||
| 6c846a8ae7 | |||
| 5004469fe9 | |||
| 14d0af74ed | |||
| 5a76cf9486 | |||
| 82901ccd02 | |||
| 1dc9d66673 | |||
| a0cbfaf390 | |||
| 9a01ae61ef | |||
| 91837d5acd | |||
| 1b9ebdda22 | |||
| b6f6177af3 | |||
| d35486196b | |||
| 1603637e3b | |||
| 8f20840169 | |||
| 4fff2394de | |||
| afb74e68ee | |||
| d5fa7844c5 | |||
| b8470cd640 | |||
| 9a23f573a6 | |||
| efe8fa0fda | |||
| 2d16e8bb4f | |||
| bbd95eebff | |||
| ceb00b4e93 | |||
| cc60d26d1c | |||
| ba3ff739f6 | |||
| 6062647705 | |||
| 070c1c2de9 | |||
| d3aaa69409 | |||
| 0ac7753e35 | |||
| eba9d53d2e | |||
| d04d4ec8e7 | |||
| c7c3efcbe7 | |||
| 2b8d53a44c | |||
| ef6b573e08 | |||
| 61eedd41df | |||
| b265bcda20 | |||
| d703d32a1f | |||
| aab9334404 | |||
| c2570f6955 | |||
| 8e936a6334 | |||
| 46bfc22869 | |||
| db1620dd56 | |||
| e59f8a42a3 | |||
| 17d18bd85d | |||
| fb256cf578 | |||
| 1b6b5db76d | |||
| 41647ca83a | |||
| 07d2a17a87 | |||
| 6d744dfb7e | |||
| b9b946c35f | |||
| 17adfe2117 | |||
| 1e5e21102d | |||
| 4af992222f | |||
| a9447c6a11 | |||
| db71323313 | |||
| b9b2748e05 | |||
| 387231f743 | |||
| 2216a89aa3 | |||
| 4faa6326fa | |||
| cb22b3d9a1 | |||
| 152a3873bd | |||
| adc2760a89 | |||
| dde64acb06 | |||
| 008adbd8bc | |||
| 0e4866a5a2 | |||
| 5cb96cae3a | |||
| 8cbb82a67f | |||
| 848ddbe477 | |||
| 083c1cde8b | |||
| b792971062 | |||
| 07dde8f4b1 | |||
| 01f94127dd | |||
| 4d457b4e9e | |||
| 8ac93ff2da | |||
| ef33a4b08e | |||
| fdd3b25a27 | |||
| 4dc979da08 | |||
| 8f426e03c4 | |||
| 40cd085bf8 | |||
| 6aa75fc5d1 | |||
| eae5920f9d | |||
| 2f6bfa37cc | |||
| 9d6fd9b9b8 | |||
| 260cd67c96 | |||
| aff76e2d18 | |||
| 52e4343045 | |||
| 1ffbb135c6 | |||
| c3ec522261 | |||
| 4538839376 | |||
| 834edd3a71 | |||
| 581c3d9593 | |||
| 0c672fbaa5 | |||
| 6d96b9a312 | |||
| 691791ccd0 | |||
| f4299121d5 | |||
| 1adfb7eedd | |||
| 33ad583d15 | |||
| a7e2fe2277 | |||
| 5a479d5863 | |||
| 873ff034d2 | |||
| 61d3537617 | |||
| ae068a3f64 | |||
| f7402cd6f5 | |||
| c53f9c8020 | |||
| 798b4d57f4 | |||
| 98d428fb34 | |||
| 3ac5ace216 | |||
| 444a1a7ab9 | |||
| 43ea4bd4b5 | |||
| 6a9272e40a | |||
| 10589a11aa | |||
| a88f898bc0 | |||
| 7a84038b04 | |||
| 111c40732d | |||
| 69bb78c8be | |||
| ad3b327d69 | |||
| dc27f38534 | |||
| 5b0816cb92 | |||
| 57f6955303 | |||
| 78915f878d | |||
| 6ced6d626b | |||
| ee3cb819b4 | |||
| cc17b1d19d | |||
| 2c83240d47 | |||
| 54f18ff120 | |||
| 5e1fe363c3 | |||
| 3d2ec507e1 | |||
| 1dd7af3c8b | |||
| 06ec1fcebf | |||
| 86cb863fd4 | |||
| d5ef1288d8 | |||
| f3354c498d | |||
| 9557141b38 | |||
| 3144b66e73 | |||
| 6dbefa3d2f | |||
| c8f3b139e8 | |||
| 288663325d | |||
| 49947ee01d | |||
| fa7a45ebc7 | |||
| 9a074c222f | |||
| 4e0d7b6ed9 | |||
| 1f3defb04c | |||
| 6c52c43460 | |||
| deae2879f1 | |||
| 5b255a7d8b | |||
| 6e06c24b7a | |||
| 2fde1efdd3 | |||
| aeb29d983a | |||
| c8a7123da9 | |||
| 5c22061415 | |||
| 9a0fda8c02 | |||
| 2f9a17c44a | |||
| 50559015d8 | |||
| a8d4e143c2 | |||
| 2a6c69538d | |||
| 0ba5d61353 | |||
| d436ec5790 | |||
| 759b822b92 | |||
| 9df45af698 | |||
| 3474e81446 | |||
| e1f07eb957 | |||
| 71ff1b98be | |||
| 9b370dfa88 | |||
| 0be0661750 | |||
| eaa7230af7 | |||
| 11cb000481 | |||
| 8ae3554a58 | |||
| dfd4736386 | |||
| feb793c9fa | |||
| ee962fde08 | |||
| c08dd96de3 | |||
| b52f771133 | |||
| 4631232551 | |||
| df7f5047aa | |||
| 467d14324d | |||
| cbdce08e96 | |||
| d6bf8f8854 | |||
| 4599da3ded | |||
| 6d50952b2e | |||
| 7066947809 | |||
| e2924aacab | |||
| 1e86d2503f | |||
| eb67eee53a | |||
| dfdad45963 | |||
| 4735508d87 | |||
| c43c47eab8 | |||
| fafb2dc6b9 | |||
| 140e99c465 | |||
| 7ba1974390 | |||
| 51b8510f17 | |||
| 5d6949d471 | |||
| 8e9d0c1fd1 | |||
| 3852a3b779 | |||
| 8b4ba96936 | |||
| 0c17e18491 | |||
| 2bdbab3afc | |||
| b97499a95e | |||
| a70ac57872 | |||
| a9cf457024 | |||
| e5c938ac37 | |||
| edad54efa2 | |||
| f88426758f | |||
| 77a28eb810 | |||
| f834b27562 | |||
| 984e257cc5 | |||
| 729e7612bc | |||
| 59fadeae57 | |||
| bfbf7a298a | |||
| aad5d3bd65 | |||
| 504f19c445 | |||
| 19c47eb442 | |||
| ab6043df60 | |||
| 3305549a0f | |||
| c24c3cb571 | |||
| 952999258b | |||
| 0713eaa52c | |||
| 8fee689f60 | |||
| 75ddb17fed | |||
| 0c6a74626c | |||
| 41e3d0eaf9 | |||
| 8b9cfebd42 | |||
| 16badee259 | |||
| 9d5171dd36 | |||
| e0c0e81b7d | |||
| fd4e8985fc | |||
| 1d9b8503c0 | |||
| b3ef7b914d | |||
| 2f59e12e20 | |||
| 30e8652c2a | |||
| 5ee6aceb60 | |||
| 6940b6a6d1 | |||
| 4e33ce9415 | |||
| 944e22bde6 | |||
| 6054fa0a26 | |||
| 4db13cfed4 | |||
| 6a6adda2e0 | |||
| 4afa55c0db | |||
| bc120bfb2b | |||
| 88966699e7 | |||
| 9a5db3dcfb | |||
| 392aa1e654 | |||
| f2b32e47ff | |||
| 58136d0181 | |||
| 02733e55cb | |||
| 60df8456a7 | |||
| 6d0ecc805c | |||
| a0e9dd24a3 | |||
| d1eb89057d | |||
| 161c6dc83a | |||
| 54848b8a7e | |||
| 990563c604 | |||
| 8489ca8c8d | |||
| b57e2c89e3 | |||
| 66bedf78ac | |||
| 592c5cce60 | |||
| 2ccf9a4e92 | |||
| ed333c0513 | |||
| 89b65b7009 | |||
| 0cc2d346af | |||
| 5f81e78bc4 | |||
| 554b5fd4b5 |
@@ -21,8 +21,9 @@ assignees: enricoros
|
||||
- [ ] Create a temporary tag `git tag v1.2.3 && git push opensource --tags`
|
||||
- [ ] Create a [New Draft GitHub Release](https://github.com/enricoros/big-agi/releases/new), and generate the automated changelog (for new contributors)
|
||||
- [ ] Update the release version in package.json, and `npm i`
|
||||
- [ ] Update in-app News [src/apps/news/news.data.tsx](/src/apps/news/news.data.tsx)
|
||||
- [ ] Update the in-app News version number
|
||||
- [ ] Update in-app News [src/apps/news/news.data.tsx](/src/apps/news/news.data.tsx)
|
||||
- [ ] Update in-app Cover graphics
|
||||
- [ ] Update the README.md with the new release
|
||||
- [ ] Copy the highlights to the [docs/changelog.md](/docs/changelog.md)
|
||||
- Release:
|
||||
@@ -79,11 +80,32 @@ I need the following from you:
|
||||
|
||||
1. a table summarizing all the new features in 1.2.3 with the following columns: 4 words description (exactly what it is), short description, usefulness (what it does for the user), significance, link to the issue number (not the commit)), which will be used for the artifacts later
|
||||
2. then double-check the git log to see if there are any features of significance that are not in the table
|
||||
3. then score each feature in terms of importance for users (1-10), relative impact of the feature (1-10, where 10 applies to the broadest user base), and novelty and uniqueness (1-10, where 10 is truly unique and novel from what exists already)
|
||||
3. then score each feature in terms of importance for users (1-10), relative impact of the feature (1-10, where 10 applies to the broadest user base), and novelty and uniqueness (1-10, where 10 is truly unique and novel from what exists already)
|
||||
4. then improve the table, in decreasing order of importance for features, fixing any detail that's missing, in particular check if there are commits of significance from a user or developer point of view, which are not contained in the table
|
||||
5. then I want you then to update the news.data.tsx for the new release
|
||||
```
|
||||
|
||||
### release name
|
||||
|
||||
```markdown
|
||||
please brainstorm 10 different names for this release. see the former names here: https://big-agi.com/blog
|
||||
```
|
||||
|
||||
You can follow with 'What do you think of Modelmorphic?' or other selected name
|
||||
|
||||
### cover images
|
||||
|
||||
```markdown
|
||||
Great, now I need to generate images for this. Before I used the following prompts (2 releases before).
|
||||
|
||||
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is using a computer with split screen made of origami, split keyboard and is wearing origami sunglasses with very different split reflections. Split halves are very contrasting. Close up photography, bokeh, white background.
|
||||
import coverV113 from '../../../public/images/covers/release-cover-v1.13.0.png';
|
||||
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is calling on a 3D origami old-school pink telephone and the camera is zooming on the telephone. Close up photography, bokeh, white background.
|
||||
import coverV112 from '../../../public/images/covers/release-cover-v1.12.0.png';
|
||||
|
||||
What can I do now as far as images? Give me 4 prompt ideas with the same style as looks as the former, but different scene or action
|
||||
```
|
||||
|
||||
### Readme (and Changelog)
|
||||
|
||||
```markdown
|
||||
|
||||
@@ -11,11 +11,8 @@ name: Create and publish Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
#- main-stable # Disabled as the v* tag is used for stable releases
|
||||
tags:
|
||||
- 'v*' # Trigger on version tags (e.g., v1.7.0)
|
||||
- 'v1.1*' # V1 legacy tags only (v1.10.x - v1.19.x range)
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
@@ -32,6 +29,12 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
@@ -45,17 +48,26 @@ jobs:
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=development,enable=${{ github.ref == 'refs/heads/main' }}
|
||||
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main-stable' }}
|
||||
type=ref,event=tag # Use the tag name as a tag for tag builds
|
||||
type=semver,pattern={{version}} # Generate semantic versioning tags for tag builds
|
||||
# V1 legacy pointers
|
||||
type=raw,value=v1
|
||||
type=raw,value=v1-stable
|
||||
|
||||
# Exact version tags (v1.16.11 and 1.16.11)
|
||||
type=ref,event=tag
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=Big-AGI v1 (Legacy)
|
||||
org.opencontainers.image.description=Big-AGI v1 - Legacy version. For the latest version, see GitHub and big-agi.com
|
||||
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||
org.opencontainers.image.documentation=https://big-agi.com
|
||||
build-args: |
|
||||
NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
|
||||
|
||||
+12
-6
@@ -1,6 +1,6 @@
|
||||
# Base
|
||||
FROM node:18-alpine AS base
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
FROM node:22-alpine AS base
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
|
||||
# Dependencies
|
||||
@@ -11,8 +11,11 @@ WORKDIR /app
|
||||
COPY package*.json ./
|
||||
COPY src/server/prisma ./src/server/prisma
|
||||
|
||||
# link ssl3 for latest Alpine
|
||||
RUN sh -c '[ ! -e /lib/libssl.so.3 ] && ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3 || echo "Link already exists"'
|
||||
|
||||
# Install dependencies, including dev (release builds should use npm ci)
|
||||
ENV NODE_ENV development
|
||||
ENV NODE_ENV=development
|
||||
RUN npm ci
|
||||
|
||||
|
||||
@@ -28,8 +31,11 @@ ENV NEXT_PUBLIC_GA4_MEASUREMENT_ID=${NEXT_PUBLIC_GA4_MEASUREMENT_ID}
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# link ssl3 for latest Alpine
|
||||
RUN sh -c '[ ! -e /lib/libssl.so.3 ] && ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3 || echo "Link already exists"'
|
||||
|
||||
# Build the application
|
||||
ENV NODE_ENV production
|
||||
ENV NODE_ENV=production
|
||||
RUN npm run build
|
||||
|
||||
# Reduce installed packages to production-only
|
||||
@@ -51,8 +57,8 @@ COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/src/server/prisma ./src/server/prisma
|
||||
|
||||
# Minimal ENV for production
|
||||
ENV NODE_ENV production
|
||||
ENV PATH $PATH:/app/node_modules/.bin
|
||||
ENV NODE_ENV=production
|
||||
ENV PATH=$PATH:/app/node_modules/.bin
|
||||
|
||||
# Run as non-root user
|
||||
USER nextjs
|
||||
|
||||
@@ -1,222 +1,41 @@
|
||||
# BIG-AGI 🧠✨
|
||||
|
||||
Welcome to big-AGI 👋, the GPT application for professionals that need function, form,
|
||||
simplicity, and speed. Powered by the latest models from 12 vendors and
|
||||
open-source model servers, `big-AGI` offers best-in-class Voice and Chat with AI Personas,
|
||||
visualizations, coding, drawing, calling, and quite more -- all in a polished UX.
|
||||
|
||||
Pros use big-AGI. 🚀 Developers love big-AGI. 🤖
|
||||
|
||||
[](https://big-agi.com)
|
||||
|
||||
Or fork & run on Vercel
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
|
||||
|
||||
## 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [documentation](docs/README.md)
|
||||
|
||||
big-AGI is an open book; see the **[ready-to-ship and future ideas](https://github.com/users/enricoros/projects/4/views/2)** in our open roadmap
|
||||
|
||||
### What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
|
||||
|
||||
- **Side-by-Side Split Windows**: multitask with parallel conversations. [#208](https://github.com/enricoros/big-AGI/issues/208)
|
||||
- **Multi-Chat Mode**: message everyone, all at once. [#388](https://github.com/enricoros/big-AGI/issues/388)
|
||||
- **Export tables as CSV**: big thanks to @aj47. [#392](https://github.com/enricoros/big-AGI/pull/392)
|
||||
- Adjustable text size: customize density. [#399](https://github.com/enricoros/big-AGI/issues/399)
|
||||
- Dev2 Persona Technology Preview
|
||||
- Better looking chats with improved spacing, fonts, and menus
|
||||
- More: new video player, [LM Studio tutorial](https://github.com/enricoros/big-AGI/blob/main/docs/config-local-lmstudio.md) (thanks @aj47), [MongoDB support](https://github.com/enricoros/big-AGI/blob/main/docs/deploy-database.md) (thanks @ranfysvalle02), and speedups
|
||||
|
||||
<details>
|
||||
<summary>What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline</summary>
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/95ceb03c-945d-4fdd-9a9f-3317beb54f3f
|
||||
|
||||
- **Voice Calls**: real-time voice call your personas out of the blue or in relation to a chat [#354](https://github.com/enricoros/big-AGI/issues/354)
|
||||
- Support **OpenAI 0125** Models. [#364](https://github.com/enricoros/big-AGI/issues/364)
|
||||
- Rename or Auto-Rename chats. [#222](https://github.com/enricoros/big-AGI/issues/222), [#360](https://github.com/enricoros/big-AGI/issues/360)
|
||||
- More control over **Link Sharing** [#356](https://github.com/enricoros/big-AGI/issues/356)
|
||||
- **Accessibility** to screen readers [#358](https://github.com/enricoros/big-AGI/issues/358)
|
||||
- Export chats to Markdown [#337](https://github.com/enricoros/big-AGI/issues/337)
|
||||
- Paste tables from Excel [#286](https://github.com/enricoros/big-AGI/issues/286)
|
||||
- Ollama model updates and context window detection fixes [#309](https://github.com/enricoros/big-AGI/issues/309)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>What's New in 1.11.0 · Jan 16, 2024 · Singularity</summary>
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cfcb110c68
|
||||
|
||||
- **Find chats**: search in titles and content, with frequency ranking. [#329](https://github.com/enricoros/big-AGI/issues/329)
|
||||
- **Commands**: command auto-completion (type '/'). [#327](https://github.com/enricoros/big-AGI/issues/327)
|
||||
- **[Together AI](https://www.together.ai/products#inference)** inference platform support (good speed and newer models). [#346](https://github.com/enricoros/big-AGI/issues/346)
|
||||
- Persona Creator history, deletion, custom creation, fix llm API timeouts
|
||||
- Enable adding up to five custom OpenAI-compatible endpoints
|
||||
- Developer enhancements: new 'Actiles' framework
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>What's New in 1.10.0 · Jan 6, 2024 · The Year of AGI</summary>
|
||||
|
||||
- **New UI**: for both desktop and mobile, sets the stage for future scale. [#201](https://github.com/enricoros/big-AGI/issues/201)
|
||||
- **Conversation Folders**: enhanced conversation organization. [#321](https://github.com/enricoros/big-AGI/issues/321)
|
||||
- **[LM Studio](https://lmstudio.ai/)** support and improved token management
|
||||
- Resizable panes in split-screen conversations.
|
||||
- Large performance optimizations
|
||||
- Developer enhancements: new UI framework, updated documentation for proxy settings on browserless/docker
|
||||
|
||||
</details>
|
||||
|
||||
For full details and former releases, check out the [changelog](docs/changelog.md).
|
||||
|
||||
## ✨ Key Features 👊
|
||||
|
||||
|  |  |  |  |  |
|
||||
|---------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| **Chat**<br/>**Call** AGI<br/>**Draw** images<br/>**Agents**, ... | Local & Cloud<br/>Open & Closed<br/>Cheap & Heavy<br/>Google, Mistral, ... | Attachments<br/>Diagrams<br/>Multi-Chat<br/>Mobile-first UI | Stored Locally<br/>Easy self-Host<br/>Local actions<br/>Data = Gold | AI Personas<br/>Voice Modes<br/>Screen Capture<br/>Camera + OCR |
|
||||
|
||||

|
||||
|
||||
You can easily configure 100s of AI models in big-AGI:
|
||||
|
||||
| **AI models** | _supported vendors_ |
|
||||
|:--------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Opensource Servers | [LocalAI](https://localai.com) (multimodal) · [Ollama](https://ollama.com/) · [Oobabooga](https://github.com/oobabooga/text-generation-webui) |
|
||||
| Local Servers | [LM Studio](https://lmstudio.ai/) |
|
||||
| Multimodal services | [Azure](https://azure.microsoft.com/en-us/products/ai-services/openai-service) · [Google Gemini](https://ai.google.dev/) · [OpenAI](https://platform.openai.com/docs/overview) |
|
||||
| Language services | [Anthropic](https://anthropic.com) · [Groq](https://wow.groq.com/) · [Mistral](https://mistral.ai/) · [OpenRouter](https://openrouter.ai/) · [Perplexity](https://www.perplexity.ai/) · [Together AI](https://www.together.ai/) |
|
||||
| Image services | [Prodia](https://prodia.com/) (SDXL) |
|
||||
| Speech services | [ElevenLabs](https://elevenlabs.io) (Voice synthesis / cloning) |
|
||||
|
||||
Add extra functionality with these integrations:
|
||||
|
||||
| **More** | _integrations_ |
|
||||
|:-------------|:---------------------------------------------------------------------------------------------------------------|
|
||||
| Web Browse | [Browserless](https://www.browserless.io/) · [Puppeteer](https://pptr.dev/)-based |
|
||||
| Web Search | [Google CSE](https://programmablesearchengine.google.com/) |
|
||||
| Code Editors | [CodePen](https://codepen.io/pen/) · [StackBlitz](https://stackblitz.com/) · [JSFiddle](https://jsfiddle.net/) |
|
||||
| Sharing | [Paste.gg](https://paste.gg/) (Paste chats) |
|
||||
| Tracking | [Helicone](https://www.helicone.ai) (LLM Observability) |
|
||||
|
||||
[//]: # (- [x] **Flow-state UX** for uncompromised productivity)
|
||||
|
||||
[//]: # (- [x] **AI Personas**: Tailor your AI interactions with customizable personas)
|
||||
|
||||
[//]: # (- [x] **Sleek UI/UX**: A smooth, intuitive, and mobile-responsive interface)
|
||||
|
||||
[//]: # (- [x] **Efficient Interaction**: Voice commands, OCR, and drag-and-drop file uploads)
|
||||
|
||||
[//]: # (- [x] **Privacy First**: Self-host and use your own API keys for full control)
|
||||
|
||||
[//]: # (- [x] **Advanced Tools**: Execute code, import PDFs, and summarize documents)
|
||||
|
||||
[//]: # (- [x] **Seamless Integrations**: Enhance functionality with various third-party services)
|
||||
|
||||
[//]: # (- [x] **Open Roadmap**: Contribute to the progress of big-AGI)
|
||||
|
||||
<br/>
|
||||
|
||||
# 🌟 Get Involved!
|
||||
|
||||
[//]: # ([](https://discord.gg/MkH4qj2Jp9))
|
||||
[](https://discord.gg/MkH4qj2Jp9)
|
||||
|
||||
- [ ] 📢️ [**Chat with us** on Discord](https://discord.gg/MkH4qj2Jp9)
|
||||
- [ ] ⭐ **Give us a star** on GitHub 👆
|
||||
- [ ] 🚀 **Do you like code**? You'll love this gem of a project! [_Pick up a task!_](https://github.com/users/enricoros/projects/4/views/4) - _easy_ to _pro_
|
||||
- [ ] 💡 Got a feature suggestion? [_Add your roadmap ideas_](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
|
||||
- [ ] ✨ Deploy your [fork](docs/customizations.md) for your friends and family, or [customize it for work](docs/customizations.md)
|
||||
- [ ] Check out some of the big-AGI [**community projects**](docs/customizations.md)
|
||||
|
||||
| Project | Features | GitHub |
|
||||
|---------|----------------------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| CoolAGI | Code Interpreter, Vision, Mind maps, and much more | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
|
||||
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
|
||||
|
||||
<br/>
|
||||
|
||||
# 🧩 Develop
|
||||
|
||||
[//]: # ()
|
||||
|
||||
[//]: # ()
|
||||
|
||||
[//]: # ()
|
||||
|
||||
To download and run this Typescript/React/Next.js project locally, the only prerequisite is Node.js with the `npm` package manager.
|
||||
Clone this repo, install the dependencies (all local), and run the development server (which auto-watches the
|
||||
files for changes):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/enricoros/big-agi.git
|
||||
cd big-agi
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
# You will see something like:
|
||||
#
|
||||
# ▲ Next.js 14.1.0
|
||||
# - Local: http://localhost:3000
|
||||
# ✓ Ready in 2.6s
|
||||
```
|
||||
|
||||
The development app will be running on `http://localhost:3000`. Development builds have the advantage of not requiring
|
||||
a build step, but can be slower than production builds. Also, development builds won't have timeout on edge functions.
|
||||
|
||||
## 🛠️ Deploy from source
|
||||
|
||||
The _production_ build of the application is optimized for performance and is performed by the `npm run build` command,
|
||||
after installing the required dependencies.
|
||||
|
||||
```bash
|
||||
# .. repeat the steps above up to `npm install`, then:
|
||||
npm run build
|
||||
next start --port 3000
|
||||
```
|
||||
|
||||
The app will be running on the specified port, e.g. `http://localhost:3000`.
|
||||
|
||||
Want to deploy with username/password? See the [Authentication](docs/deploy-authentication.md) guide.
|
||||
|
||||
## 🐳 Deploy with Docker
|
||||
|
||||
For more detailed information on deploying with Docker, please refer to the [docker deployment documentation](docs/deploy-docker.md).
|
||||
|
||||
Build and run:
|
||||
|
||||
```bash
|
||||
docker build -t big-agi .
|
||||
docker run -d -p 3000:3000 big-agi
|
||||
```
|
||||
|
||||
Or run the official container:
|
||||
|
||||
- manually: `docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi`
|
||||
- or, with docker-compose: `docker-compose up` or see [the documentation](docs/deploy-docker.md) for a composer file with integrated browsing
|
||||
|
||||
## ☁️ Deploy on Cloudflare Pages
|
||||
|
||||
Please refer to the [Cloudflare deployment documentation](docs/deploy-cloudflare.md).
|
||||
|
||||
## 🚀 Deploy on Vercel
|
||||
|
||||
Create your GitHub fork, create a Vercel project over that fork, and deploy it. Or press the button below for convenience.
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
|
||||
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/stargazers))
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/network))
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/pulls))
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/LICENSE))
|
||||
# BIG-AGI Legacy ✨
|
||||
|
||||
> **⚠️ IMPORTANT: `v1` Branch - End of Life**
|
||||
>
|
||||
> This is the **v1** (formerly **v1-stable**) branch, which reached end-of-life in October 2025.
|
||||
> - **For the latest Big-AGI**, see the [**main** branch](https://github.com/enricoros/big-AGI/tree/main) and [**big-agi.com**](https://big-agi.com)
|
||||
> - **v1.16.11 is the final legacy release** - No further updates will be provided
|
||||
> - Docker users: `:stable`, `:development` and `:latest` point to the `main` branch
|
||||
|
||||
---
|
||||
|
||||
2023-2024 · Enrico Ros x [big-AGI](https://big-agi.com) · License: [MIT](LICENSE) · Made with 💙
|
||||
**For the latest Big-AGI:**
|
||||
- [**Big-AGI Open**](https://github.com/enricoros/big-AGI/tree/main) - Open Source, for self-hosting, with bleeding edge models support
|
||||
- [**Big-AGI Pro**](https://big-agi.com) - Hosted for Professionals with extra services and Cloud Sync
|
||||
|
||||
---
|
||||
|
||||
### What's New in 1.16.11 · October 2025
|
||||
|
||||
- Final v1 legacy release. Branch reaches end-of-life.
|
||||
|
||||
### What's New in 1.16.1...1.16.10 · 2024-2025 (patch releases)
|
||||
|
||||
- 1.16.10: Openrouter models fixes
|
||||
- 1.16.9: Docker Gemini fix (R1 models are supported in latest Big-AGI)
|
||||
- 1.16.8: OpenAI ChatGPT-4o Latest (o1 models are supported in latest Big-AGI)
|
||||
- 1.16.7: OpenAI support for GPT-4o 2024-08-06
|
||||
- 1.16.6: Groq support for Llama 3.1 models
|
||||
- 1.16.5: GPT-4o Mini support
|
||||
- 1.16.4: 8192 tokens support for Claude 3.5 Sonnet
|
||||
- 1.16.3: Anthropic Claude 3.5 Sonnet model support
|
||||
- 1.16.2: Improve web downloads, as text, markdwon, or HTML
|
||||
- 1.16.2: Proper support for Gemini models
|
||||
- 1.16.2: Added the latest Mistral model
|
||||
- 1.16.2: Tokenizer support for gpt-4o
|
||||
- 1.16.2: Updates to Beam
|
||||
- 1.16.1: Support for the new OpenAI GPT-4o 2024-05-13 model
|
||||
|
||||
---
|
||||
|
||||
2023-2025 · Enrico Ros x [Big-AGI](https://big-agi.com) · License: [MIT](LICENSE) · Made with 💙
|
||||
|
||||
@@ -16,4 +16,8 @@ const handlerNodeRoutes = (req: Request) =>
|
||||
});
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
// NOTE: the following statement breaks the build on non-pro deployments, and conditionals don't work either
|
||||
// so we resorted to raising the timeout from 10s to 25s in the vercel.json file instead
|
||||
// export const maxDuration = 25;
|
||||
export const dynamic = 'force-dynamic';
|
||||
export { handlerNodeRoutes as GET, handlerNodeRoutes as POST };
|
||||
+15
-19
@@ -1,6 +1,8 @@
|
||||
# big-AGI Documentation
|
||||
# big-AGI v1 Documentation (Legacy)
|
||||
|
||||
Find all the information you need to get started, configure, and effectively use big-AGI.
|
||||
> **Note:** This is documentation for the **v1 legacy branch**. For the latest Big-AGI, see the [main branch](https://github.com/enricoros/big-AGI/tree/main) or visit [big-agi.com](https://big-agi.com).
|
||||
|
||||
Find all the information you need to get started, configure, and effectively use big-AGI v1.
|
||||
|
||||
[//]: # (## Quick Start)
|
||||
|
||||
@@ -28,38 +30,32 @@ Detailed guides to configure your big-AGI interface and models.
|
||||
- **Advanced Feature Configuration**:
|
||||
- **[Browse](config-feature-browse.md)**: Enable web page download through third-party services or your own cloud (advanced)
|
||||
- **ElevenLabs API**: Voice and cutom voice generation, only requires their API key
|
||||
- **Google Search API**: guide not yet available, see the Google options in 'Environment Variables'
|
||||
- **Google Search API**: guide not yet available, see the Google options in '[Environment Variables](environment-variables.md)'
|
||||
- **Prodia API**: Stable Diffusion XL image generation, only requires their API key, alternative to DALL·E
|
||||
|
||||
## Deployment
|
||||
|
||||
System integrators, administrators, whitelabelers: instead of using the public big-AGI instance on get.big-agi.com, you can deploy your own instance.
|
||||
System integrators, administrators, whitelabelers: instead of using the public big-AGI instance on app.big-agi.com, you can deploy your own instance.
|
||||
|
||||
Step-by-step deployment and system configuration instructions.
|
||||
|
||||
- **Deploy Your Own**
|
||||
- straightforward: **Local development**, **Vercel 1-Click**
|
||||
- **[Cloudflare Deployment](deploy-cloudflare.md)**
|
||||
- **[Docker Deployment](deploy-docker.md)**: Containers for Local or Cloud deployments
|
||||
- **[Installation](installation.md)**: Set up your own instance of big-AGI and related products
|
||||
- build from source or use pre-built
|
||||
- locally, in the public cloud, or on your own servers
|
||||
|
||||
|
||||
- **Deployment Server Features**
|
||||
- **[Database Setup](deploy-database.md)**: Optional, only required to enable "Chat Link Sharing"
|
||||
- **[Environment Variables](environment-variables.md)**: 📌 Set server-side API keys and special features in your deployments
|
||||
- **[HTTP Basic Authentication](deploy-authentication.md)**: Optional, Secure your big-AGI instance with a username and password
|
||||
|
||||
## Customization & Derivative UIs
|
||||
|
||||
👏 Customize big-AGI to fit your needs.
|
||||
|
||||
- **[Customizing big-AGI](customizations.md)**: how to alter source code and server-side configuration
|
||||
- **Advanced Customizations**:
|
||||
- **[Source code alterations guide](customizations.md)**: source code primer and alterations guidelines
|
||||
- **[Basic Authentication](deploy-authentication.md)**: Optional, adds a username and password wall
|
||||
- **[Database Setup](deploy-database.md)**: Optional, enables "Chat Link Sharing"
|
||||
- **[Environment Variables](environment-variables.md)**: 📌 Pre-configures models and services
|
||||
|
||||
## Support and Community
|
||||
|
||||
Join our community or get support:
|
||||
|
||||
- Visit our [GitHub repository](https://github.com/enricoros/big-AGI) for source code and issue tracking
|
||||
- Check the latest updates and features on [Changelog](changelog.md) or the in-app [News](https://get.big-agi.com/news)
|
||||
- Check the latest updates and features on [Changelog](changelog.md) or the in-app [News](https://big-agi.com/news)
|
||||
- Connect with us and other users on [Discord](https://discord.gg/MkH4qj2Jp9) for discussions, help, and sharing your experiences with big-AGI
|
||||
|
||||
Thank you for choosing big-AGI. We're excited to see what you'll build.
|
||||
|
||||
+59
-9
@@ -1,16 +1,66 @@
|
||||
## Changelog
|
||||
## Changelog (v1 Legacy Branch)
|
||||
|
||||
This is a high-level changelog. Calls out some of the high level features batched
|
||||
by release.
|
||||
This is a high-level changelog for the v1 legacy branch. For the latest Big-AGI, see the [main branch](https://github.com/enricoros/big-AGI).
|
||||
|
||||
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
|
||||
### 1.16.11 · October 2025 (Final Release)
|
||||
|
||||
### 1.13.0 - Feb 2024
|
||||
Final v1 legacy release. Branch reaches end-of-life.
|
||||
|
||||
- milestone: [1.13.0](https://github.com/enricoros/big-agi/milestone/13)
|
||||
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
|
||||
### What's New in 1.16.1...1.16.10 · 2024-2025 (patch releases)
|
||||
|
||||
## What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
|
||||
- 1.16.9: Docker Gemini fix (R1 models are supported in Big-AGI 2)
|
||||
- 1.16.8: OpenAI ChatGPT-4o Latest (o1 models are supported in Big-AGI 2)
|
||||
- 1.16.7: OpenAI support for GPT-4o 2024-08-06
|
||||
- 1.16.6: Groq support for Llama 3.1 models
|
||||
- 1.16.5: GPT-4o Mini support
|
||||
- 1.16.4: 8192 tokens support for Claude 3.5 Sonnet
|
||||
- 1.16.3: Anthropic Claude 3.5 Sonnet model support
|
||||
- 1.16.2: Improve web downloads, as text, markdwon, or HTML
|
||||
- 1.16.2: Proper support for Gemini models
|
||||
- 1.16.2: Added the latest Mistral model
|
||||
- 1.16.2: Tokenizer support for gpt-4o
|
||||
- 1.16.2: Updates to Beam
|
||||
- 1.16.1: Support for the new OpenAI GPT-4o 2024-05-13 model
|
||||
|
||||
### What's New in 1.16.0 · May 9, 2024 · Crystal Clear
|
||||
|
||||
- [Beam](https://big-agi.com/blog/beam-multi-model-ai-reasoning) core and UX improvements based on user feedback
|
||||
- Chat cost estimation 💰 (enable it in Labs / hover the token counter)
|
||||
- Save/load chat files with Ctrl+S / Ctrl+O on desktop
|
||||
- Major enhancements to the Auto-Diagrams tool
|
||||
- YouTube Transcriber Persona for chatting with video content, [#500](https://github.com/enricoros/big-AGI/pull/500)
|
||||
- Improved formula rendering (LaTeX), and dark-mode diagrams, [#508](https://github.com/enricoros/big-AGI/issues/508), [#520](https://github.com/enricoros/big-AGI/issues/520)
|
||||
- Models update: **Anthropic**, **Groq**, **Ollama**, **OpenAI**, **OpenRouter**, **Perplexity**
|
||||
- Code soft-wrap, chat text selection toolbar, 3x faster on Apple silicon, and more [#517](https://github.com/enricoros/big-AGI/issues/517), [507](https://github.com/enricoros/big-AGI/pull/507)
|
||||
- Developers: update the LLMs data structures
|
||||
|
||||
### What's New in 1.15.1 · April 10, 2024 (minor release, models support)
|
||||
|
||||
- Support for the newly released Gemini Pro 1.5 models
|
||||
- Support for the new OpenAI 2024-04-09 Turbo models
|
||||
- Resilience fixes after the large success of 1.15.0
|
||||
|
||||
### What's New in 1.15.0 · April 1, 2024 · Beam
|
||||
|
||||
- ⚠️ [**Beam**: the multi-model AI chat](https://big-agi.com/blog/beam-multi-model-ai-reasoning). find better answers, faster - a game-changer for brainstorming, decision-making, and creativity. [#443](https://github.com/enricoros/big-AGI/issues/443)
|
||||
- Managed Deployments **Auto-Configuration**: simplify the UI mdoels setup with backend-set models. [#436](https://github.com/enricoros/big-AGI/issues/436)
|
||||
- Message **Starring ⭐**: star important messages within chats, to attach them later. [#476](https://github.com/enricoros/big-AGI/issues/476)
|
||||
- Enhanced the default Persona
|
||||
- Fixes to Gemini models and SVGs, improvements to UI and icons
|
||||
- Beast release, over 430 commits, 10,000+ lines changed: [release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.15.0), and changes [v1.14.1...v1.15.0](https://github.com/enricoros/big-AGI/compare/v1.14.1...v1.15.0)
|
||||
|
||||
### What's New in 1.14.1 · March 7, 2024 · Modelmorphic
|
||||
|
||||
- **Anthropic** [Claude-3](https://www.anthropic.com/news/claude-3-family) model family support. [#443](https://github.com/enricoros/big-AGI/issues/443)
|
||||
- New **[Perplexity](https://www.perplexity.ai/)** and **[Groq](https://groq.com/)** integration (thanks @Penagwin). [#407](https://github.com/enricoros/big-AGI/issues/407), [#427](https://github.com/enricoros/big-AGI/issues/427)
|
||||
- **[LocalAI](https://localai.io/models/)** deep integration, including support for [model galleries](https://github.com/enricoros/big-AGI/issues/411)
|
||||
- **Mistral** Large and Google **Gemini 1.5** support
|
||||
- Performance optimizations: runs [much faster](https://twitter.com/enricoros/status/1756553038293303434?utm_source=localhost:3000&utm_medium=big-agi), saves lots of power, reduces memory usage
|
||||
- Enhanced UX with auto-sizing charts, refined search and folder functionalities, perfected scaling
|
||||
- And with more UI improvements, documentation, bug fixes (20 tickets), and developer enhancements
|
||||
- [Release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.14.0), and changes [v1.13.1...v1.14.0](https://github.com/enricoros/big-AGI/compare/v1.13.1...v1.14.0) (233 commits, 8,000+ lines changed)
|
||||
|
||||
### What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
|
||||
|
||||
@@ -22,7 +72,7 @@ https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385
|
||||
- Better looking chats with improved spacing, fonts, and menus
|
||||
- More: new video player, [LM Studio tutorial](https://github.com/enricoros/big-AGI/blob/main/docs/config-local-lmstudio.md) (thanks @aj47), [MongoDB support](https://github.com/enricoros/big-AGI/blob/main/docs/deploy-database.md) (thanks @ranfysvalle02), and speedups
|
||||
|
||||
## What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline
|
||||
### What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/95ceb03c-945d-4fdd-9a9f-3317beb54f3f
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ If you have an `API Endpoint` and `API Key`, you can configure big-AGI as follow
|
||||
The deployed models are now available in the application. If you don't have a configured
|
||||
Azure OpenAI service instance, continue with the next section.
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
## Setting Up Azure
|
||||
|
||||
### Step 1: Azure Account & Subscription
|
||||
|
||||
@@ -68,7 +68,7 @@ The chat agent won't be able to access the web sites if the browserless containe
|
||||
- MAX_CONCURRENT_SESSIONS=10
|
||||
```
|
||||
|
||||
You can then add the proyy lines to your `.env` file.
|
||||
You can then add the proxy lines to your `.env` file.
|
||||
|
||||
```
|
||||
https_proxy=http://PROXY-IP:PROXY-PORT
|
||||
@@ -115,4 +115,4 @@ If you encounter any issues or have questions about configuring the browse funct
|
||||
|
||||
Enjoy the enhanced browsing experience within `big-AGI` and explore the web without ever leaving your chat!
|
||||
|
||||
Last updated on Feb 27, 2024 ([edit on GitHub](https://github.com/enricoros/big-AGI/edit/main/docs/config-feature-browse.md))
|
||||
Last updated on Feb 27, 2024 ([edit on GitHub](https://github.com/enricoros/big-AGI/edit/main/docs/config-feature-browse.md))
|
||||
|
||||
@@ -37,6 +37,9 @@ Check the URL and modify if different.
|
||||
2. Enter the API URL: `http://localhost:1234` (modify if different)
|
||||
3. Refresh by clicking on the `Models` button to load models from LM Studio
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Missing @mui/material**: Execute `npm install @mui/material` or `yarn add @mui/material`
|
||||
|
||||
@@ -36,6 +36,9 @@ Follow the guide at: https://localai.io/basics/getting_started/
|
||||
- Load the models (click on `Models 🔄`)
|
||||
- Select the model and chat
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
### Integration: Models Gallery
|
||||
|
||||
If the running LocalAI instance is configured with a [Model Gallery](https://localai.io/models/):
|
||||
|
||||
@@ -13,7 +13,7 @@ _Last updated Dec 16, 2023_
|
||||
|
||||
1. **Ensure Ollama API Server is Running**: Follow the official instructions to get Ollama up and running on your machine
|
||||
- For detailed instructions on setting up the Ollama API server, please refer to the
|
||||
[Ollama download page](https://ollama.ai/download) and [instructions for linux](https://github.com/jmorganca/ollama/blob/main/docs/linux.md).
|
||||
[Ollama download page](https://ollama.ai/download) and [instructions for linux](https://github.com/jmorganca/ollama/blob/main/docs/linux.md).
|
||||
2. **Add Ollama as a Model Source**: In `big-AGI`, navigate to the **Models** section, select **Add a model source**, and choose **Ollama**
|
||||
3. **Enter Ollama Host URL**: Provide the Ollama Host URL where the API server is accessible (e.g., `http://localhost:11434`)
|
||||
4. **Refresh Model List**: Once connected, refresh the list of available models to include the Ollama models
|
||||
@@ -22,6 +22,9 @@ _Last updated Dec 16, 2023_
|
||||
you'll have to press the 'Pull' button again, until a green message appears.
|
||||
5. **Chat with Ollama models**: select an Ollama model and begin chatting with AI personas
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
**Visual Configuration Guide**:
|
||||
|
||||
* After adding the `Ollama` model vendor, entering the IP address of an Ollama server, and refreshing models:<br/>
|
||||
@@ -37,7 +40,7 @@ _Last updated Dec 16, 2023_
|
||||
|
||||
### ⚠️ Network Troubleshooting
|
||||
|
||||
If you get errors about the server having trouble connecting with Ollama, please see
|
||||
If you get errors about the server having trouble connecting with Ollama, please see
|
||||
[this message](https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483) on Issue #276.
|
||||
|
||||
And in brief, make sure the Ollama endpoint is accessible from the servers where you run big-AGI (which could
|
||||
@@ -69,15 +72,19 @@ Then, edit the nginx configuration file `/etc/nginx/sites-enabled/default` and a
|
||||
|
||||
```nginx
|
||||
location /ollama/ {
|
||||
proxy_pass http://localhost:11434;
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
|
||||
# Disable buffering for the streaming responses (SSE)
|
||||
proxy_set_header Connection '';
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Disable buffering for the streaming responses
|
||||
chunked_transfer_encoding off;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
# Longer timeouts
|
||||
proxy_read_timeout 3600;
|
||||
proxy_connect_timeout 3600;
|
||||
proxy_send_timeout 3600;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -25,15 +25,15 @@ This guide assumes that **big-AGI** is already installed on your system. Note th
|
||||
- Stop the Web UI as we need to modify the startup flags to enable the OpenAI API
|
||||
2. Enable the **openai extension**
|
||||
- Edit `CMD_FLAGS.txt`
|
||||
- Make sure that `--listen --api` is present and uncommented
|
||||
- Make sure that `--listen --api` is present and uncommented
|
||||
3. Restart text-generation-webui
|
||||
- Double-click on "start"
|
||||
- You should see something like:
|
||||
- You should see something like:
|
||||
```
|
||||
2023-12-07 21:51:21 INFO:Loading the extension "openai"...
|
||||
2023-12-07 21:51:21 INFO:OpenAI-compatible API URL:
|
||||
|
||||
http://0.0.0.0:5000
|
||||
|
||||
http://0.0.0.0:5000
|
||||
...
|
||||
INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
|
||||
Running on local URL: http://0.0.0.0:7860
|
||||
|
||||
@@ -22,6 +22,9 @@ This document details the process of integrating OpenRouter with big-AGI.
|
||||

|
||||
4. OpenAI GPT4-32k and other models will now be accessible and selectable in the application.
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
### Pricing
|
||||
|
||||
OpenRouter independently manages its service and pricing and is not affiliated with big-AGI.
|
||||
|
||||
+29
-6
@@ -22,6 +22,25 @@ Understand the Architecture: big-AGI uses Next.js, React for the front end, and
|
||||
|
||||
This necessitates a code change (file renaming) before build initiation, detailed in [deploy-authentication.md](deploy-authentication.md).
|
||||
|
||||
### Increase Vercel Functions Timeout
|
||||
|
||||
For long-running operations, Vercel allows paid deployments to increase the timeout on Functions.
|
||||
Note that this applies to old-style Vercel Functions (based on Node.js) and not the new Edge Functions.
|
||||
|
||||
At time of writing, big-AGI has only 2 operations that run on Node.js Functions:
|
||||
browsing (fetching web pages) and sharing. They both can exceed 10 seconds, especially
|
||||
when fetching large pages or waiting for websites to be completed.
|
||||
|
||||
We provide `vercel_PRODUCTION.json` to raise the duration to 25 seconds (from a default of 10), to use it,
|
||||
make sure to rename it to `vercel.json` before build.
|
||||
|
||||
From the Vercel Project > Settings > General > Build & Development Settings,
|
||||
you can for instance set the build command to:
|
||||
|
||||
```bash
|
||||
mv vercel_PRODUCTION.json vercel.json; next build
|
||||
```
|
||||
|
||||
### Change the Personas
|
||||
|
||||
Edit the `src/data.ts` file to customize personas. This file houses the default personas. You can add, remove, or modify these to meet your project's needs.
|
||||
@@ -43,20 +62,24 @@ Test your application thoroughly using local development (refer to README.md for
|
||||
- [deploy-cloudflare.md](deploy-cloudflare.md): for Cloudflare Workers deployment
|
||||
- [deploy-docker.md](deploy-docker.md): for Docker deployment instructions and examples
|
||||
|
||||
<br/>
|
||||
## Debugging
|
||||
|
||||
We introduced the `/info/debug` page that provides a detailed overview of the application's environment, including the API keys, environment variables, and other configuration settings.
|
||||
|
||||
<br/>
|
||||
|
||||
## Community Projects - Share Your Project
|
||||
|
||||
After deployment, share your project with the community. We will link to your project to help others discover and learn from your work.
|
||||
|
||||
| Project | Features | GitHub |
|
||||
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| 🚀 CoolAGI: Where AI meets Imagination<br/> | Code Interpreter, Vision, Mind maps, Web Searches, Advanced Data Analytics, Large Data Handling and more! | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
|
||||
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
|
||||
| Project | Features | GitHub |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| 🚀 CoolAGI: Where AI meets Imagination<br/> | Code Interpreter, Vision, Mind maps, Web Searches, Advanced Data Analytics, Large Data Handling and more! | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
|
||||
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
|
||||
|
||||
For public projects, update your README.md with your modifications and submit a pull request to add your project to our list, aiding in its discovery.
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## Best Practices
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ As of Feb 27, 2024, this feature is in development.
|
||||
|
||||
## Configurations
|
||||
|
||||
| Scope | Default | Description / Instructions |
|
||||
| Scope | Default | Description / Instructions |
|
||||
|-----------------------------------------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------------|
|
||||
| Your source builds of big-AGI | None | **Vercel**: enable Vercel Analytics from the dashboard. · **Google Analytics**: set environment variable at build time. |
|
||||
| Your docker builds of big-AGI | None | **Vercel**: n/a. · **Google Analytics**: set environment variable at `docker build` time. |
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# Deploying `big-AGI` with Docker
|
||||
# Deploying `big-AGI` with Docker (v1 Legacy)
|
||||
|
||||
> **Note:** This documentation is for the **v1 legacy branch**. For the latest Big-AGI, use Docker tags `:latest`, `:stable`, or `:development` which point to the [main branch](https://github.com/enricoros/big-AGI/tree/main).
|
||||
>
|
||||
> To use v1 legacy specifically, use Docker tags `:v1` or `:v1-stable`.
|
||||
|
||||
Utilize Docker containers to deploy the big-AGI application for an efficient and automated deployment process.
|
||||
Docker ensures faster development cycles, easier collaboration, and seamless environment management.
|
||||
@@ -9,7 +13,7 @@ Docker ensures faster development cycles, easier collaboration, and seamless env
|
||||
```bash
|
||||
git clone https://github.com/enricoros/big-agi.git
|
||||
cd big-agi
|
||||
```
|
||||
```
|
||||
2. **Build the Docker Image**: Build a local docker image from the provided Dockerfile:
|
||||
```bash
|
||||
docker build -t big-agi .
|
||||
|
||||
@@ -91,7 +91,7 @@ requiring the user to enter an API key
|
||||
| `ANTHROPIC_API_HOST` | Changes the backend host for the Anthropic vendor, to enable platforms such as [config-aws-bedrock.md](config-aws-bedrock.md) | Optional |
|
||||
| `GEMINI_API_KEY` | The API key for Google AI's Gemini | Optional |
|
||||
| `GROQ_API_KEY` | The API key for Groq Cloud | Optional |
|
||||
| `LOCALAI_API_HOST` | Sets the URL of the LocalAI server, or defaults to http://127.0.0.1:8080 | Optional |
|
||||
| `LOCALAI_API_HOST` | Sets the URL of the LocalAI server, or defaults to http://127.0.0.1:8080 | Optional |
|
||||
| `LOCALAI_API_KEY` | The (Optional) API key for LocalAI | Optional |
|
||||
| `MISTRAL_API_KEY` | The API key for Mistral | Optional |
|
||||
| `OLLAMA_API_HOST` | Changes the backend host for the Ollama vendor. See [config-local-ollama.md](config-local-ollama) | |
|
||||
@@ -128,7 +128,7 @@ Enable the app to Talk, Draw, and Google things up.
|
||||
| `GOOGLE_CSE_ID` | Google Custom/Programmable Search Engine ID - [Link to PSE](https://programmablesearchengine.google.com/) |
|
||||
| **Browse** | |
|
||||
| `PUPPETEER_WSS_ENDPOINT` | Puppeteer WebSocket endpoint - used for browsing (pade downloadeing), etc. |
|
||||
| **Backend** | |
|
||||
| **Backend** | |
|
||||
| `BACKEND_ANALYTICS` | Semicolon-separated list of analytics flags (see backend.analytics.ts). Flags: `domain` logs the responding domain. |
|
||||
| `HTTP_BASIC_AUTH_USERNAME` | See the [Authentication](deploy-authentication.md) guide. Username for HTTP Basic Authentication. |
|
||||
| `HTTP_BASIC_AUTH_PASSWORD` | Password for HTTP Basic Authentication. |
|
||||
@@ -147,5 +147,5 @@ The value of these variables are passed to the frontend (Web UI) - make sure the
|
||||
|
||||
---
|
||||
|
||||
For a higher level overview of backend code and environemnt customization,
|
||||
For a higher level overview of backend code and environment customization,
|
||||
see the [big-AGI Customization](customizations.md) guide.
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# Installation Guide
|
||||
|
||||
Welcome to the big-AGI Installation Guide - Whether you're a developer
|
||||
eager to explore, a system integrator, or an enterprise looking for a
|
||||
white-label solution, this comprehensive guide ensures a smooth setup
|
||||
process for your own instance of big-AGI and related products.
|
||||
|
||||
**Try big-AGI** - You don't need to install anything if you want to play with big-AGI
|
||||
and have your API keys to various model services. You can access our free instance on [big-AGI.com](https://big-agi.com).
|
||||
The free instance runs the latest `main-stable` branch from this repository.
|
||||
|
||||
## 🧩 Build-your-own
|
||||
|
||||
If you want to change the code, have a deeper configuration,
|
||||
add your own models, or run your own instance, follow the steps below.
|
||||
|
||||
### Local Development
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Node.js and npm installed on your machine.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Clone the big-AGI repository:
|
||||
```bash
|
||||
git clone https://github.com/enricoros/big-AGI.git
|
||||
cd big-AGI
|
||||
```
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
3. Run the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Your big-AGI instance is now running at `http://localhost:3000`.
|
||||
|
||||
### Local Production build
|
||||
|
||||
The production build is optimized for performance and follows
|
||||
the same steps 1 and 2 as for [local development](#local-development).
|
||||
|
||||
3. Build the production version:
|
||||
```bash
|
||||
# .. repeat the steps above up to `npm install`, then:
|
||||
npm run build
|
||||
```
|
||||
4. Start the production server (`npx` may be optional):
|
||||
```bash
|
||||
npx next start --port 3000
|
||||
```
|
||||
Your big-AGI production instance is on `http://localhost:3000`.
|
||||
|
||||
### Advanced Customization
|
||||
|
||||
Want to pre-enable models, customize the interface, or deploy with username/password or alter code to your needs?
|
||||
Check out the [Customizations Guide](README.md) for detailed instructions.
|
||||
|
||||
## ☁️ Cloud Deployment Options
|
||||
|
||||
To deploy big-AGI on a public server, you have several options. Choose the one that best fits your needs.
|
||||
|
||||
### Deploy on Vercel
|
||||
|
||||
Install big-AGI on Vercel with just a few clicks.
|
||||
|
||||
Create your GitHub fork, create a Vercel project over that fork, and deploy it. Or press the button below for convenience.
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
|
||||
|
||||
### Deploy on Cloudflare
|
||||
|
||||
Deploy on Cloudflare's global network by installing big-AGI on
|
||||
Cloudflare Pages. Check out the [Cloudflare Installation Guide](deploy-cloudflare.md)
|
||||
for step-by-step instructions.
|
||||
|
||||
### Docker Deployments
|
||||
|
||||
Containerize your big-AGI installation using Docker for portability and scalability.
|
||||
Our [Docker Deployment Guide](deploy-docker.md) will walk you through the process,
|
||||
or follow the steps below for a quick start.
|
||||
|
||||
1. (optional) Build the Docker image - if you do not want to use the [pre-built Docker images](https://github.com/enricoros/big-AGI/pkgs/container/big-agi):
|
||||
```bash
|
||||
docker build -t big-agi .
|
||||
```
|
||||
2. Run the Docker container with either:
|
||||
```bash
|
||||
# 2A. if you built the image yourself:
|
||||
docker run -d -p 3000:3000 big-agi
|
||||
|
||||
# 2B. or use the pre-built image:
|
||||
docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi
|
||||
|
||||
# 2C. or use docker-compose:
|
||||
docker-compose up
|
||||
```
|
||||
Access your big-AGI instance at `http://localhost:3000`.
|
||||
|
||||
### Midori AI Subsystem for Docker Deployment
|
||||
|
||||
Follow the instructions found on [Midori AI Subsystem Site](https://io.midori-ai.xyz/subsystem/manager/)
|
||||
for your host OS. After completing the setup process, install the Big-AGI docker backend to the Midori AI Subsystem.
|
||||
|
||||
## Enterprise-Grade Installation
|
||||
|
||||
For businesses seeking a fully-managed, scalable solution, consider our managed installations.
|
||||
Enjoy all the features of big-AGI without the hassle of infrastructure management. [hello@big-agi.com](mailto:hello@big-agi.com) to learn more.
|
||||
|
||||
## Support
|
||||
|
||||
Join our vibrant community of developers, researchers, and AI enthusiasts. Share your projects, get help, and collaborate with others.
|
||||
|
||||
- [Discord Community](https://discord.gg/MkH4qj2Jp9)
|
||||
- [Twitter](https://twitter.com/yourusername)
|
||||
|
||||
For any questions or inquiries, please don't hesitate to [reach out to our team](mailto:hello@big-agi.com).
|
||||
Generated
+1433
-797
File diff suppressed because it is too large
Load Diff
+35
-29
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.13.0",
|
||||
"version": "1.16.0",
|
||||
"private": true,
|
||||
"author": "Enrico Ros <enrico.ros@gmail.com>",
|
||||
"repository": "https://github.com/enricoros/big-agi",
|
||||
@@ -19,16 +19,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.11",
|
||||
"@mui/joy": "^5.0.0-beta.29",
|
||||
"@next/bundle-analyzer": "^14.1.0",
|
||||
"@next/third-parties": "^14.1.0",
|
||||
"@prisma/client": "^5.10.2",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@mui/icons-material": "^5.15.17",
|
||||
"@mui/joy": "^5.0.0-beta.36",
|
||||
"@mui/material": "^5.15.17",
|
||||
"@next/bundle-analyzer": "^14.2.3",
|
||||
"@next/third-parties": "^14.2.3",
|
||||
"@prisma/client": "^5.13.0",
|
||||
"@sanity/diff-match-patch": "^3.1.1",
|
||||
"@t3-oss/env-nextjs": "^0.9.2",
|
||||
"@t3-oss/env-nextjs": "^0.10.1",
|
||||
"@tanstack/react-query": "~4.36.1",
|
||||
"@trpc/client": "10.44.1",
|
||||
"@trpc/next": "10.44.1",
|
||||
@@ -37,51 +38,56 @@
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"@vercel/speed-insights": "^1.0.10",
|
||||
"browser-fs-access": "^0.35.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"next": "^14.1.0",
|
||||
"next": "~14.1.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "4.0.379",
|
||||
"pdfjs-dist": "4.2.67",
|
||||
"plantuml-encoder": "^1.4.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-csv": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-player": "^2.14.1",
|
||||
"react-resizable-panels": "^2.0.11",
|
||||
"react-player": "^2.16.0",
|
||||
"react-resizable-panels": "^2.0.19",
|
||||
"react-timeago": "^7.2.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sharp": "^0.33.2",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.33.3",
|
||||
"superjson": "^2.2.1",
|
||||
"tesseract.js": "^5.0.5",
|
||||
"tiktoken": "^1.0.13",
|
||||
"tesseract.js": "^5.1.0",
|
||||
"tiktoken": "^1.0.15",
|
||||
"turndown": "^7.2.0",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.5.1"
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/puppeteer": "^0.0.5",
|
||||
"@types/node": "^20.11.20",
|
||||
"@cloudflare/puppeteer": "0.0.5",
|
||||
"@types/node": "^20.12.11",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.59",
|
||||
"@types/prismjs": "^1.26.4",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-csv": "^1.1.10",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@types/turndown": "^5.0.4",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prisma": "^5.10.2",
|
||||
"typescript": "^5.3.3"
|
||||
"prisma": "^5.13.0",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^18.0.0"
|
||||
"node": "^24.0.0 || ^22.0.0 || ^20.0.0 || ^18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+11
-10
@@ -13,11 +13,11 @@ import '~/common/styles/GithubMarkdown.css';
|
||||
import '~/common/styles/NProgress.css';
|
||||
import '~/common/styles/app.styles.css';
|
||||
|
||||
import { ProviderBackendAndNoSSR } from '~/common/providers/ProviderBackendAndNoSSR';
|
||||
import { ProviderBackendCapabilities } from '~/common/providers/ProviderBackendCapabilities';
|
||||
import { ProviderBootstrapLogic } from '~/common/providers/ProviderBootstrapLogic';
|
||||
import { ProviderSingleTab } from '~/common/providers/ProviderSingleTab';
|
||||
import { ProviderSnacks } from '~/common/providers/ProviderSnacks';
|
||||
import { ProviderTRPCQueryClient } from '~/common/providers/ProviderTRPCQueryClient';
|
||||
import { ProviderTRPCQuerySettings } from '~/common/providers/ProviderTRPCQuerySettings';
|
||||
import { ProviderTheming } from '~/common/providers/ProviderTheming';
|
||||
import { hasGoogleAnalytics, OptionalGoogleAnalytics } from '~/common/components/GoogleAnalytics';
|
||||
import { isVercelFromFrontend } from '~/common/util/pwaUtils';
|
||||
@@ -33,15 +33,16 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
|
||||
|
||||
<ProviderTheming emotionCache={emotionCache}>
|
||||
<ProviderSingleTab>
|
||||
<ProviderBootstrapLogic>
|
||||
<ProviderTRPCQueryClient>
|
||||
<ProviderSnacks>
|
||||
<ProviderBackendAndNoSSR>
|
||||
<ProviderTRPCQuerySettings>
|
||||
<ProviderBackendCapabilities>
|
||||
{/* ^ SSR boundary */}
|
||||
<ProviderBootstrapLogic>
|
||||
<ProviderSnacks>
|
||||
<Component {...pageProps} />
|
||||
</ProviderBackendAndNoSSR>
|
||||
</ProviderSnacks>
|
||||
</ProviderTRPCQueryClient>
|
||||
</ProviderBootstrapLogic>
|
||||
</ProviderSnacks>
|
||||
</ProviderBootstrapLogic>
|
||||
</ProviderBackendCapabilities>
|
||||
</ProviderTRPCQuerySettings>
|
||||
</ProviderSingleTab>
|
||||
</ProviderTheming>
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
|
||||
<link rel='icon' type='image/png' sizes='16x16' href='/icons/favicon-16x16.png' />
|
||||
<link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='black' />
|
||||
|
||||
{/* Opengraph */}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppBeam } from '../../src/apps/beam/AppBeam';
|
||||
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function BeamPage() {
|
||||
return withLayout({ type: 'optima' }, <AppBeam />);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import DownloadIcon from '@mui/icons-material/Download';
|
||||
|
||||
import { AppPlaceholder } from '../../src/apps/AppPlaceholder';
|
||||
|
||||
import { backendCaps } from '~/modules/backend/state-backend';
|
||||
import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities';
|
||||
import { getPlantUmlServerUrl } from '~/modules/blocks/code/RenderCode';
|
||||
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
@@ -17,7 +17,7 @@ import { Brand } from '~/common/app.config';
|
||||
import { ROUTE_APP_CHAT, ROUTE_INDEX } from '~/common/app.routes';
|
||||
|
||||
// apps access
|
||||
import { incrementalNewsVersion } from '../../src/apps/news/news.version';
|
||||
import { incrementalNewsVersion, useAppNewsStateStore } from '../../src/apps/news/news.version';
|
||||
|
||||
// capabilities access
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs, useCapabilityTextToImage } from '~/common/components/useCapabilities';
|
||||
@@ -32,6 +32,7 @@ import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
// utils access
|
||||
import { clientHostName, isChromeDesktop, isFirefox, isIPhoneUser, isMacUser, isPwa, isVercelFromFrontend } from '~/common/util/pwaUtils';
|
||||
import { getGA4MeasurementId } from '~/common/components/GoogleAnalytics';
|
||||
import { prettyTimestampForFilenames } from '~/common/util/timeUtils';
|
||||
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
|
||||
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
|
||||
@@ -76,11 +77,12 @@ function AppDebug() {
|
||||
const [saved, setSaved] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const backendCapabilities = backendCaps();
|
||||
const backendCaps = getBackendCapabilities();
|
||||
const chatsCount = useChatStore.getState().conversations?.length;
|
||||
const uxLabsExperiments = Object.entries(useUXLabsStore.getState()).filter(([_k, v]) => v === true).map(([k, _]) => k).join(', ');
|
||||
const { folders, enableFolders } = useFolderStore.getState();
|
||||
const { lastSeenNewsVersion, usageCount } = useAppStateStore.getState();
|
||||
const { lastSeenNewsVersion } = useAppNewsStateStore.getState();
|
||||
const { usageCount } = useAppStateStore.getState();
|
||||
|
||||
|
||||
// derived state
|
||||
@@ -112,7 +114,7 @@ function AppDebug() {
|
||||
},
|
||||
};
|
||||
const cBackend = {
|
||||
configuration: backendCapabilities,
|
||||
configuration: backendCaps,
|
||||
deployment: {
|
||||
home: Brand.URIs.Home,
|
||||
hostName: clientHostName(),
|
||||
@@ -127,7 +129,7 @@ function AppDebug() {
|
||||
const handleDownload = async () => {
|
||||
fileSave(
|
||||
new Blob([JSON.stringify({ client: cClient, agi: cProduct, backend: cBackend }, null, 2)], { type: 'application/json' }),
|
||||
{ fileName: `big-agi-debug-${new Date().toISOString().replace(/:/g, '-')}.json`, extensions: ['.json'] },
|
||||
{ fileName: `big-agi_debug_${prettyTimestampForFilenames()}.json`, extensions: ['.json'] },
|
||||
)
|
||||
.then(() => setSaved(true))
|
||||
.catch(e => console.error('Error saving debug.json', e));
|
||||
|
||||
@@ -77,9 +77,12 @@ function AppShareTarget() {
|
||||
setIsDownloading(true);
|
||||
callBrowseFetchPage(intentURL)
|
||||
.then(page => {
|
||||
if (page.stopReason !== 'error')
|
||||
queueComposerTextAndLaunchApp('\n\n```' + intentURL + '\n' + page.content + '\n```\n');
|
||||
else
|
||||
if (page.stopReason !== 'error') {
|
||||
let pageContent = page.content.markdown || page.content.text || page.content.html || '';
|
||||
if (pageContent)
|
||||
pageContent = '\n\n```' + intentURL + '\n' + pageContent + '\n```\n';
|
||||
queueComposerTextAndLaunchApp(pageContent);
|
||||
} else
|
||||
setErrorMessage('Could not read any data' + page.error ? ': ' + page.error : '');
|
||||
})
|
||||
.catch(error => setErrorMessage(error?.message || error || 'Unknown error'))
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 348 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
+28
-3
@@ -3,9 +3,16 @@
|
||||
"short_name": "big-AGI",
|
||||
"theme_color": "#32383E",
|
||||
"background_color": "#9FA6AD",
|
||||
"description": "Personal AGI App",
|
||||
"description": "Your Generative AI Suite",
|
||||
"categories": [
|
||||
"productivity",
|
||||
"AI",
|
||||
"tool",
|
||||
"utilities"
|
||||
],
|
||||
"display": "standalone",
|
||||
"start_url": "/",
|
||||
"start_url": "/?source=pwa",
|
||||
"scope": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192x192.png",
|
||||
@@ -24,6 +31,17 @@
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "/link/share_target",
|
||||
"accept": {
|
||||
"application/big-agi": [
|
||||
".agi",
|
||||
".agi.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/link/share_target",
|
||||
"method": "GET",
|
||||
@@ -33,5 +51,12 @@
|
||||
"text": "text",
|
||||
"url": "url"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Call",
|
||||
"url": "/call",
|
||||
"description": "Call a Persona"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+17
-15
@@ -10,7 +10,7 @@ import { useRouterRoute } from '~/common/app.routes';
|
||||
* https://github.com/enricoros/big-AGI/issues/299
|
||||
*/
|
||||
export function AppPlaceholder(props: {
|
||||
title?: string,
|
||||
title?: string | null,
|
||||
text?: React.ReactNode,
|
||||
children?: React.ReactNode,
|
||||
}) {
|
||||
@@ -29,23 +29,25 @@ export function AppPlaceholder(props: {
|
||||
border: '1px solid blue',
|
||||
}}>
|
||||
|
||||
<Box sx={{
|
||||
my: 'auto',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
gap: 4,
|
||||
border: '1px solid red',
|
||||
}}>
|
||||
{(props.title !== null || !!props.text) && (
|
||||
<Box sx={{
|
||||
my: 'auto',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
gap: 4,
|
||||
border: '1px solid red',
|
||||
}}>
|
||||
|
||||
<Typography level='h1'>
|
||||
{placeholderAppName}
|
||||
</Typography>
|
||||
{!!props.text && (
|
||||
<Typography>
|
||||
{props.text}
|
||||
<Typography level='h1'>
|
||||
{placeholderAppName}
|
||||
</Typography>
|
||||
)}
|
||||
{!!props.text && (
|
||||
<Typography>
|
||||
{props.text}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{props.children}
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import * as React from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, Button, Typography } from '@mui/joy';
|
||||
|
||||
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
|
||||
import { BeamView } from '~/modules/beam/BeamView';
|
||||
import { createBeamVanillaStore } from '~/modules/beam/store-beam-vanilla';
|
||||
import { useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { createDConversation, createDMessage, DConversation, DMessage } from '~/common/state/store-chats';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
|
||||
function initTestConversation(): DConversation {
|
||||
const conversation = createDConversation();
|
||||
conversation.messages.push(createDMessage('system', 'You are a helpful assistant.'));
|
||||
conversation.messages.push(createDMessage('user', 'Hello, who are you? (please expand...)'));
|
||||
return conversation;
|
||||
}
|
||||
|
||||
function initTestBeamStore(messages: DMessage[], beamStore: BeamStoreApi = createBeamVanillaStore()): BeamStoreApi {
|
||||
beamStore.getState().open(messages, useModelsStore.getState().chatLLMId, (text) => alert(text));
|
||||
return beamStore;
|
||||
}
|
||||
|
||||
|
||||
export function AppBeam() {
|
||||
|
||||
// state
|
||||
const [showDebug, setShowDebug] = React.useState(false);
|
||||
|
||||
const [conversation, setConversation] = React.useState<DConversation>(() => initTestConversation());
|
||||
const [beamStoreApi] = React.useState(() => createBeamVanillaStore());
|
||||
|
||||
|
||||
// reinit the beam store if the conversation changes
|
||||
React.useEffect(() => {
|
||||
initTestBeamStore(conversation.messages, beamStoreApi);
|
||||
}, [beamStoreApi, conversation]);
|
||||
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const { isOpen, beamState } = useBeamStore(beamStoreApi, useShallow(state => {
|
||||
return {
|
||||
isOpen: state.isOpen,
|
||||
beamState: showDebug ? state : null,
|
||||
};
|
||||
}));
|
||||
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
beamStoreApi.getState().terminateKeepingSettings();
|
||||
}, [beamStoreApi]);
|
||||
|
||||
|
||||
// layout
|
||||
usePluggableOptimaLayout(null, React.useMemo(() => <>
|
||||
{/* button to toggle debug info */}
|
||||
<Button size='sm' variant='plain' color='neutral' onClick={() => setShowDebug(on => !on)}>
|
||||
{showDebug ? 'Hide' : 'Show'} debug
|
||||
</Button>
|
||||
|
||||
{/* 'open' */}
|
||||
<Button size='sm' variant='plain' color='neutral' onClick={() => setConversation(initTestConversation())}>
|
||||
.open
|
||||
</Button>
|
||||
|
||||
{/* 'close' */}
|
||||
<Button size='sm' variant='plain' color='neutral' onClick={handleClose}>
|
||||
.close
|
||||
</Button>
|
||||
</>, [handleClose, showDebug]), null, 'AppBeam');
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto', position: 'relative' }}>
|
||||
|
||||
{isOpen && (
|
||||
<BeamView
|
||||
beamStore={beamStoreApi}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDebug && (
|
||||
<Typography level='body-xs' sx={{
|
||||
whiteSpace: 'pre',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1 /* debug on top of BeamView */,
|
||||
backdropFilter: 'blur(4px)',
|
||||
padding: '1rem',
|
||||
}}>
|
||||
{JSON.stringify(beamState, null, 2)
|
||||
// add an extra newline between first level properties (space, space, double quote) to make it more readable
|
||||
.split('\n').map(line => line.replace(/^\s\s"/g, '\n ')).join('\n')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, Card, CardContent, IconButton, ListItemDecorator, Typography } from '@mui/joy';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import ChatIcon from '@mui/icons-material/Chat';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import MicIcon from '@mui/icons-material/Mic';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
||||
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
||||
|
||||
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { cssRainbowColorKeyframes } from '~/common/app.theme';
|
||||
import { animationColorRainbow } from '~/common/util/animUtils';
|
||||
import { navigateBack } from '~/common/app.routes';
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
import { useUICounter } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
/*export const cssRainbowBackgroundKeyframes = keyframes`
|
||||
100%, 0% {
|
||||
background-color: rgb(128, 0, 0);
|
||||
}
|
||||
8% {
|
||||
background-color: rgb(102, 51, 0);
|
||||
}
|
||||
16% {
|
||||
background-color: rgb(64, 64, 0);
|
||||
}
|
||||
25% {
|
||||
background-color: rgb(38, 76, 0);
|
||||
}
|
||||
33% {
|
||||
background-color: rgb(0, 89, 0);
|
||||
}
|
||||
41% {
|
||||
background-color: rgb(0, 76, 41);
|
||||
}
|
||||
50% {
|
||||
background-color: rgb(0, 64, 64);
|
||||
}
|
||||
58% {
|
||||
background-color: rgb(0, 51, 102);
|
||||
}
|
||||
66% {
|
||||
background-color: rgb(0, 0, 128);
|
||||
}
|
||||
75% {
|
||||
background-color: rgb(63, 0, 128);
|
||||
}
|
||||
83% {
|
||||
background-color: rgb(76, 0, 76);
|
||||
}
|
||||
91% {
|
||||
background-color: rgb(102, 0, 51);
|
||||
}`;*/
|
||||
|
||||
function StatusCard(props: { icon: React.JSX.Element, hasIssue: boolean, text: string, button?: React.JSX.Element }) {
|
||||
return (
|
||||
<Card sx={{ width: '100%' }}>
|
||||
@@ -67,7 +29,7 @@ function StatusCard(props: { icon: React.JSX.Element, hasIssue: boolean, text: s
|
||||
{props.button}
|
||||
</Typography>
|
||||
<ListItemDecorator>
|
||||
{props.hasIssue ? <WarningRoundedIcon color='warning' /> : <CheckIcon color='success' />}
|
||||
{props.hasIssue ? <WarningRoundedIcon color='warning' /> : <CheckRoundedIcon color='success' />}
|
||||
</ListItemDecorator>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -124,7 +86,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
|
||||
|
||||
<Typography level='title-lg' sx={{ fontSize: '3rem', fontWeight: 'sm', textAlign: 'center' }}>
|
||||
Welcome to<br />
|
||||
<Box component='span' sx={{ animation: `${cssRainbowColorKeyframes} 15s linear infinite` }}>
|
||||
<Box component='span' sx={{ animation: `${animationColorRainbow} 15s linear infinite` }}>
|
||||
your first call
|
||||
</Box>
|
||||
</Typography>
|
||||
@@ -167,7 +129,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
|
||||
|
||||
{/* Text to Speech status */}
|
||||
<StatusCard
|
||||
icon={<RecordVoiceOverIcon />}
|
||||
icon={<RecordVoiceOverTwoToneIcon />}
|
||||
text={
|
||||
(synthesis.mayWork ? 'Voice synthesis should be ready.' : 'There might be an issue with ElevenLabs voice synthesis.')
|
||||
+ (synthesis.isConfiguredServerSide ? '' : (synthesis.isConfiguredClientSide ? '' : ' Please add your API key in the settings.'))
|
||||
@@ -208,7 +170,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
|
||||
// boxShadow: allGood ? 'md' : 'none',
|
||||
}}
|
||||
>
|
||||
{allGood ? <ArrowForwardIcon sx={{ fontSize: '1.5em' }} /> : <CloseRoundedIcon sx={{ fontSize: '1.5em' }} />}
|
||||
{allGood ? <ArrowForwardRoundedIcon sx={{ fontSize: '1.5em' }} /> : <CloseRoundedIcon sx={{ fontSize: '1.5em' }} />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Avatar, Box, Card, CardContent, Chip, IconButton, Link as MuiLink, ListDivider, MenuItem, Sheet, Switch, Typography } from '@mui/joy';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
|
||||
import { GitHubProjectIssueCard } from '~/common/components/GitHubProjectIssueCard';
|
||||
import { animationShadowRingLimey } from '~/common/util/animUtils';
|
||||
import { conversationTitle, DConversation, DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
@@ -19,27 +19,6 @@ import { useAppCallStore } from './state/store-app-call';
|
||||
const COLLAPSED_COUNT = 2;
|
||||
|
||||
|
||||
export const niceShadowKeyframes = keyframes`
|
||||
100%, 0% {
|
||||
//background-color: rgb(102, 0, 51);
|
||||
box-shadow: 1px 1px 0 white, 2px 2px 12px rgb(183, 255, 0);
|
||||
}
|
||||
25% {
|
||||
//background-color: rgb(76, 0, 76);
|
||||
box-shadow: 1px 1px 0 white, 2px 2px 12px rgb(255, 251, 0);
|
||||
//scale: 1.2;
|
||||
}
|
||||
50% {
|
||||
//background-color: rgb(63, 0, 128);
|
||||
box-shadow: 1px 1px 0 white, 2px 2px 12px rgba(0, 255, 81);
|
||||
//scale: 0.8;
|
||||
}
|
||||
75% {
|
||||
//background-color: rgb(0, 0, 128);
|
||||
box-shadow: 1px 1px 0 white, 2px 2px 12px rgb(255, 153, 0);
|
||||
}`;
|
||||
|
||||
|
||||
const ContactCardAvatar = (props: { size: string, symbol?: string, imageUrl?: string, onClick?: () => void, sx?: SxProps }) =>
|
||||
<Avatar
|
||||
// variant='outlined'
|
||||
@@ -125,7 +104,6 @@ function CallContactCard(props: {
|
||||
sx={{
|
||||
mx: 'auto',
|
||||
mt: '-2.5rem',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -282,7 +260,7 @@ export function Contacts(props: { setCallIntent: (intent: AppCallIntent) => void
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: 'background.popup',
|
||||
animation: `${niceShadowKeyframes} 5s infinite`,
|
||||
animation: `${animationShadowRingLimey} 5s infinite`,
|
||||
}}>
|
||||
<CallIcon />
|
||||
</IconButton>
|
||||
|
||||
+10
-23
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, Card, ListDivider, ListItemDecorator, MenuItem, Switch, Typography } from '@mui/joy';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
@@ -7,10 +7,10 @@ import CallEndIcon from '@mui/icons-material/CallEnd';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
import MicIcon from '@mui/icons-material/Mic';
|
||||
import MicNoneIcon from '@mui/icons-material/MicNone';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
||||
|
||||
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '../chat/components/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { useChatLLMDropdown } from '../chat/components/useLLMDropdown';
|
||||
|
||||
import { EXPERIMENTAL_speakTextStream } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
@@ -57,7 +57,7 @@ function CallMenuItems(props: {
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleChangeVoiceToggle}>
|
||||
<ListItemDecorator><RecordVoiceOverIcon /></ListItemDecorator>
|
||||
<ListItemDecorator><RecordVoiceOverTwoToneIcon /></ListItemDecorator>
|
||||
Change Voice
|
||||
<Switch checked={props.override} onChange={handleChangeVoiceToggle} sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
@@ -99,7 +99,7 @@ export function Telephone(props: {
|
||||
|
||||
// external state
|
||||
const { chatLLMId, chatLLMDropdown } = useChatLLMDropdown();
|
||||
const { chatTitle, reMessages } = useChatStore(state => {
|
||||
const { chatTitle, reMessages } = useChatStore(useShallow(state => {
|
||||
const conversation = props.callIntent.conversationId
|
||||
? state.conversations.find(conversation => conversation.id === props.callIntent.conversationId) ?? null
|
||||
: null;
|
||||
@@ -107,7 +107,7 @@ export function Telephone(props: {
|
||||
chatTitle: conversation ? conversationTitle(conversation) : null,
|
||||
reMessages: conversation ? conversation.messages : null,
|
||||
};
|
||||
}, shallow);
|
||||
}));
|
||||
const persona = SystemPurposes[props.callIntent.personaId as SystemPurposeId] ?? undefined;
|
||||
const personaCallStarters = persona?.call?.starters ?? undefined;
|
||||
const personaVoiceId = overridePersonaVoice ? undefined : (persona?.voices?.elevenLabs?.voiceId ?? undefined);
|
||||
@@ -225,7 +225,7 @@ export function Telephone(props: {
|
||||
let finalText = '';
|
||||
let error: any | null = null;
|
||||
setPersonaTextInterim('💭...');
|
||||
llmStreamingChatGenerate(chatLLMId, callPrompt, null, null, responseAbortController.current.signal, ({ textSoFar }) => {
|
||||
llmStreamingChatGenerate(chatLLMId, callPrompt, 'call', callMessages[0].id, null, null, responseAbortController.current.signal, ({ textSoFar }) => {
|
||||
const text = textSoFar?.trim();
|
||||
if (text) {
|
||||
finalText = text;
|
||||
@@ -331,22 +331,9 @@ export function Telephone(props: {
|
||||
padding: 0, // move this to the ScrollToBottom component
|
||||
}}>
|
||||
|
||||
<ScrollToBottom
|
||||
// bootToBottom
|
||||
stickToBottom
|
||||
sx={{
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
<ScrollToBottom stickToBottomInitial>
|
||||
|
||||
// content
|
||||
display: 'grid',
|
||||
padding: 1,
|
||||
}}
|
||||
>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Box sx={{ minHeight: '100%', p: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
{/* Call Messages [] */}
|
||||
{callMessages.map((message) =>
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
import { Avatar, Box } from '@mui/joy';
|
||||
|
||||
|
||||
const cssScaleKeyframes = keyframes`
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}`;
|
||||
import { animationScalePulse } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
export function CallAvatar(props: { symbol: string, imageUrl?: string, isRinging?: boolean, onClick: () => void }) {
|
||||
@@ -34,7 +23,7 @@ export function CallAvatar(props: { symbol: string, imageUrl?: string, isRinging
|
||||
<Box
|
||||
sx={{
|
||||
...(props.isRinging
|
||||
? { animation: `${cssScaleKeyframes} 1.4s ease-in-out infinite` }
|
||||
? { animation: `${animationScalePulse} 1.4s ease-in-out infinite` }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
|
||||
+248
-257
@@ -1,47 +1,48 @@
|
||||
import * as React from 'react';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { useTheme } from '@mui/joy';
|
||||
|
||||
import { DEV_MODE_SETTINGS } from '../settings-modal/UxLabsSettings';
|
||||
import { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
|
||||
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
|
||||
import { downloadConversation, openAndLoadConversations } from '~/modules/trade/trade.client';
|
||||
import { getChatLLMId, useChatLLM } from '~/modules/llms/store-llms';
|
||||
import { imaginePromptFromText } from '~/modules/aifn/imagine/imaginePromptFromText';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { useAreBeamsOpen } from '~/modules/beam/store-beam.hooks';
|
||||
import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { PanelResizeInset } from '~/common/components/panes/GoodPanelResizeHandler';
|
||||
import { PreferencesTab, useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, getConversationSystemPurposeId, useConversation } from '~/common/state/store-chats';
|
||||
import { getUXLabsHighPerformance, useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
import { createDMessage, DConversationId, DMessage, DMessageMetadata, getConversation, getConversationSystemPurposeId, useConversation } from '~/common/state/store-chats';
|
||||
import { themeBgAppChatComposer } from '~/common/app.theme';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useRouterQuery } from '~/common/app.routes';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
|
||||
import { Beam } from './components/beam/Beam';
|
||||
import { ChatBarAltBeam } from './components/ChatBarAltBeam';
|
||||
import { ChatBarAltTitle } from './components/ChatBarAltTitle';
|
||||
import { ChatBarDropdowns } from './components/ChatBarDropdowns';
|
||||
import { ChatBeamWrapper } from './components/ChatBeamWrapper';
|
||||
import { ChatDrawerMemo } from './components/ChatDrawer';
|
||||
import { ChatDropdowns } from './components/ChatDropdowns';
|
||||
import { ChatMessageList } from './components/ChatMessageList';
|
||||
import { ChatPageMenuItems } from './components/ChatPageMenuItems';
|
||||
import { ChatTitle } from './components/ChatTitle';
|
||||
import { Composer } from './components/composer/Composer';
|
||||
import { ScrollToBottom } from './components/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from './components/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { getInstantAppChatPanesCount, usePanesManager } from './components/panes/usePanesManager';
|
||||
import { usePanesManager } from './components/panes/usePanesManager';
|
||||
|
||||
import { extractChatCommand, findAllChatCommands } from './commands/commands.registry';
|
||||
import { runAssistantUpdatingState } from './editors/chat-stream';
|
||||
import { runBrowseGetPageUpdatingState } from './editors/browse-load';
|
||||
import { runImageGenerationUpdatingState } from './editors/image-generate';
|
||||
import { runReActUpdatingState } from './editors/react-tangent';
|
||||
import { _handleExecute } from './editors/_handleExecute';
|
||||
|
||||
|
||||
// what to say when a chat is new and has no title
|
||||
@@ -59,6 +60,24 @@ export type ChatModeId =
|
||||
| 'generate-react';
|
||||
|
||||
|
||||
export interface AppChatIntent {
|
||||
initialConversationId: string | null;
|
||||
}
|
||||
|
||||
|
||||
const composerOpenSx: SxProps = {
|
||||
zIndex: 21, // just to allocate a surface, and potentially have a shadow
|
||||
backgroundColor: themeBgAppChatComposer,
|
||||
borderTop: `1px solid`,
|
||||
borderTopColor: 'divider',
|
||||
p: { xs: 1, md: 2 },
|
||||
};
|
||||
|
||||
const composerClosedSx: SxProps = {
|
||||
display: 'none',
|
||||
};
|
||||
|
||||
|
||||
export function AppChat() {
|
||||
|
||||
// state
|
||||
@@ -78,35 +97,55 @@ export function AppChat() {
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const showAltTitleBar = useUXLabsStore(state => state.labsChatBarAlt === 'title');
|
||||
const intent = useRouterQuery<Partial<AppChatIntent>>();
|
||||
|
||||
const { openLlmOptions } = useOptimaLayout();
|
||||
const showAltTitleBar = useUXLabsStore(state => DEV_MODE_SETTINGS && state.labsChatBarAlt === 'title');
|
||||
|
||||
const { openLlmOptions, openModelsSetup, openPreferencesTab } = useOptimaLayout();
|
||||
|
||||
const { chatLLM } = useChatLLM();
|
||||
|
||||
const {
|
||||
// state
|
||||
chatPanes,
|
||||
focusedConversationId,
|
||||
focusedPaneIndex,
|
||||
focusedPaneConversationId,
|
||||
// actions
|
||||
navigateHistoryInFocusedPane,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
focusedPaneIndex,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
setFocusedPaneIndex,
|
||||
} = usePanesManager();
|
||||
|
||||
const chatHandlers = React.useMemo(() => chatPanes.map(pane => {
|
||||
return pane.conversationId ? ConversationsManager.getHandler(pane.conversationId) : null;
|
||||
}), [chatPanes]);
|
||||
|
||||
const beamsStores = React.useMemo(() => chatHandlers.map(handler => {
|
||||
return handler?.getBeamStore() ?? null;
|
||||
}), [chatHandlers]);
|
||||
|
||||
const beamsOpens = useAreBeamsOpen(beamsStores);
|
||||
const beamOpenStoreInFocusedPane = React.useMemo(() => {
|
||||
const open = focusedPaneIndex !== null ? (beamsOpens?.[focusedPaneIndex] ?? false) : false;
|
||||
return open ? beamsStores?.[focusedPaneIndex!] ?? null : null;
|
||||
}, [beamsOpens, beamsStores, focusedPaneIndex]);
|
||||
|
||||
const {
|
||||
// focused
|
||||
title: focusedChatTitle,
|
||||
isChatEmpty: isFocusedChatEmpty,
|
||||
isEmpty: isFocusedChatEmpty,
|
||||
isDeveloper: isFocusedChatDeveloper,
|
||||
areChatsEmpty,
|
||||
conversationIdx: focusedChatNumber,
|
||||
newConversationId,
|
||||
// all
|
||||
hasConversations,
|
||||
recycleNewConversationId,
|
||||
// actions
|
||||
prependNewConversation,
|
||||
branchConversation,
|
||||
deleteConversations,
|
||||
setMessages,
|
||||
} = useConversation(focusedConversationId);
|
||||
} = useConversation(focusedPaneConversationId);
|
||||
|
||||
const { mayWork: capabilityHasT2I } = useCapabilityTextToImage();
|
||||
|
||||
@@ -127,23 +166,25 @@ export function AppChat() {
|
||||
const willMulticast = isComposerMulticast && isMultiConversationId;
|
||||
const disableNewButton = isFocusedChatEmpty && !isMultiPane;
|
||||
|
||||
const chatHandlers = React.useMemo(() => chatPanes.map(pane => {
|
||||
return pane.conversationId ? ConversationManager.getHandler(pane.conversationId) : null;
|
||||
}), [chatPanes]);
|
||||
|
||||
const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => {
|
||||
const handleOpenConversationInFocusedPane = React.useCallback((conversationId: DConversationId | null) => {
|
||||
conversationId && openConversationInFocusedPane(conversationId);
|
||||
}, [openConversationInFocusedPane]);
|
||||
|
||||
const openSplitConversationId = React.useCallback((conversationId: DConversationId | null) => {
|
||||
const handleOpenConversationInSplitPane = React.useCallback((conversationId: DConversationId | null) => {
|
||||
conversationId && openConversationInSplitPane(conversationId);
|
||||
}, [openConversationInSplitPane]);
|
||||
|
||||
const handleNavigateHistory = React.useCallback((direction: 'back' | 'forward') => {
|
||||
const handleNavigateHistoryInFocusedPane = React.useCallback((direction: 'back' | 'forward') => {
|
||||
if (navigateHistoryInFocusedPane(direction))
|
||||
showNextTitleChange.current = true;
|
||||
}, [navigateHistoryInFocusedPane]);
|
||||
|
||||
// [effect] Handle the initial conversation intent
|
||||
React.useEffect(() => {
|
||||
intent.initialConversationId && handleOpenConversationInFocusedPane(intent.initialConversationId);
|
||||
}, [handleOpenConversationInFocusedPane, intent.initialConversationId]);
|
||||
|
||||
// [effect] Show snackbar with the focused chat title after a history navigation in focused pane
|
||||
React.useEffect(() => {
|
||||
if (showNextTitleChange.current) {
|
||||
showNextTitleChange.current = false;
|
||||
@@ -156,101 +197,20 @@ export function AppChat() {
|
||||
|
||||
// Execution
|
||||
|
||||
const _handleExecute = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]): Promise<void> => {
|
||||
const chatLLMId = getChatLLMId();
|
||||
if (!chatModeId || !conversationId || !chatLLMId) return;
|
||||
const handleExecuteAndOutcome = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) => {
|
||||
const outcome = await _handleExecute(chatModeId, conversationId, history);
|
||||
if (outcome === 'err-no-chatllm')
|
||||
openModelsSetup();
|
||||
else if (outcome === 'err-t2i-unconfigured')
|
||||
openPreferencesTab(PreferencesTab.Draw);
|
||||
else if (outcome === 'err-no-persona')
|
||||
addSnackbar({ key: 'chat-no-persona', message: 'No persona selected.', type: 'issue' });
|
||||
else if (outcome === 'err-no-conversation')
|
||||
addSnackbar({ key: 'chat-no-conversation', message: 'No active conversation.', type: 'issue' });
|
||||
return outcome === true;
|
||||
}, [openModelsSetup, openPreferencesTab]);
|
||||
|
||||
// "/command ...": overrides the chat mode
|
||||
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
|
||||
if (lastMessage?.role === 'user') {
|
||||
const chatCommand = extractChatCommand(lastMessage.text)[0];
|
||||
if (chatCommand && chatCommand.type === 'cmd') {
|
||||
switch (chatCommand.providerId) {
|
||||
case 'ass-beam':
|
||||
return ConversationManager.getHandler(conversationId).beamStore.create(history);
|
||||
|
||||
case 'ass-browse':
|
||||
setMessages(conversationId, history);
|
||||
return await runBrowseGetPageUpdatingState(conversationId, chatCommand.params!);
|
||||
|
||||
case 'ass-t2i':
|
||||
setMessages(conversationId, history);
|
||||
return await runImageGenerationUpdatingState(conversationId, chatCommand.params!);
|
||||
|
||||
case 'ass-react':
|
||||
setMessages(conversationId, history);
|
||||
return await runReActUpdatingState(conversationId, chatCommand.params!, chatLLMId);
|
||||
|
||||
case 'chat-alter':
|
||||
if (chatCommand.command === '/clear') {
|
||||
if (chatCommand.params === 'all')
|
||||
return setMessages(conversationId, []);
|
||||
const helpMessage = createDMessage('assistant', 'This command requires the \'all\' parameter to confirm the operation.');
|
||||
helpMessage.originLLM = Brand.Title.Base;
|
||||
return setMessages(conversationId, [...history, helpMessage]);
|
||||
}
|
||||
Object.assign(lastMessage, {
|
||||
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
|
||||
sender: 'Bot',
|
||||
text: chatCommand.params || '',
|
||||
} satisfies Partial<DMessage>);
|
||||
return setMessages(conversationId, history);
|
||||
|
||||
case 'cmd-help':
|
||||
const chatCommandsText = findAllChatCommands()
|
||||
.map(cmd => ` - ${cmd.primary}` + (cmd.alternatives?.length ? ` (${cmd.alternatives.join(', ')})` : '') + `: ${cmd.description}`)
|
||||
.join('\n');
|
||||
const helpMessage = createDMessage('assistant', 'Available Chat Commands:\n' + chatCommandsText);
|
||||
helpMessage.originLLM = Brand.Title.Base;
|
||||
return setMessages(conversationId, [...history, helpMessage]);
|
||||
|
||||
default:
|
||||
return setMessages(conversationId, [...history, createDMessage('assistant', 'This command is not supported.')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get the focused system purpose (note: we don't react to it, or it would invalidate half UI components..)
|
||||
const conversationSystemPurposeId = getConversationSystemPurposeId(conversationId);
|
||||
if (!conversationSystemPurposeId)
|
||||
return setMessages(conversationId, [...history, createDMessage('assistant', 'No persona selected.')]);
|
||||
|
||||
// synchronous long-duration tasks, which update the state as they go
|
||||
if (chatLLMId) {
|
||||
switch (chatModeId) {
|
||||
case 'generate-text':
|
||||
return await runAssistantUpdatingState(conversationId, history, chatLLMId, conversationSystemPurposeId, getUXLabsHighPerformance() ? 0 : getInstantAppChatPanesCount());
|
||||
|
||||
case 'generate-text-beam':
|
||||
return ConversationManager.getHandler(conversationId).beamStore.create(history);
|
||||
|
||||
case 'append-user':
|
||||
return setMessages(conversationId, history);
|
||||
|
||||
case 'generate-image':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
// also add a 'fake' user message with the '/draw' command
|
||||
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
|
||||
...message,
|
||||
text: `/draw ${lastMessage.text}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(conversationId, lastMessage.text);
|
||||
|
||||
case 'generate-react':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
setMessages(conversationId, history);
|
||||
return await runReActUpdatingState(conversationId, lastMessage.text, chatLLMId);
|
||||
}
|
||||
}
|
||||
|
||||
// ISSUE: if we're here, it means we couldn't do the job, at least sync the history
|
||||
console.log('handleExecuteConversation: issue running', chatModeId, conversationId, lastMessage);
|
||||
setMessages(conversationId, history);
|
||||
}, [setMessages]);
|
||||
|
||||
const handleComposerAction = React.useCallback((chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart): boolean => {
|
||||
const handleComposerAction = React.useCallback((conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: ComposerOutputMultiPart, metadata?: DMessageMetadata): boolean => {
|
||||
// validate inputs
|
||||
if (multiPartMessage.length !== 1 || multiPartMessage[0].type !== 'text-block') {
|
||||
addSnackbar({
|
||||
@@ -266,50 +226,63 @@ export function AppChat() {
|
||||
const userText = multiPartMessage[0].text;
|
||||
|
||||
// multicast: send the message to all the panes
|
||||
const uniqueIds = new Set([conversationId]);
|
||||
const uniqueConversationIds = new Set([conversationId]);
|
||||
if (willMulticast)
|
||||
chatPanes.forEach(pane => pane.conversationId && uniqueIds.add(pane.conversationId));
|
||||
chatPanes.forEach(pane => pane.conversationId && uniqueConversationIds.add(pane.conversationId));
|
||||
|
||||
// we loop to handle both the normal and multicast modes
|
||||
let enqueued = false;
|
||||
for (const _cId of uniqueIds) {
|
||||
const _conversation = getConversation(_cId);
|
||||
if (_conversation) {
|
||||
// start execution fire/forget
|
||||
void _handleExecute(chatModeId, _cId, [..._conversation.messages, createDMessage('user', userText)]);
|
||||
enqueued = true;
|
||||
}
|
||||
let enqueuedAny = false;
|
||||
for (const _cId of uniqueConversationIds) {
|
||||
const history = getConversation(_cId)?.messages;
|
||||
if (!history) continue;
|
||||
|
||||
const newUserMessage = createDMessage('user', userText);
|
||||
if (metadata) newUserMessage.metadata = metadata;
|
||||
|
||||
// fire/forget
|
||||
void handleExecuteAndOutcome(chatModeId, _cId, [...history, newUserMessage]);
|
||||
enqueuedAny = true;
|
||||
}
|
||||
return enqueued;
|
||||
}, [chatPanes, willMulticast, _handleExecute]);
|
||||
return enqueuedAny;
|
||||
}, [chatPanes, handleExecuteAndOutcome, willMulticast]);
|
||||
|
||||
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[], chatEffectBeam: boolean): Promise<void> => {
|
||||
await _handleExecute(!chatEffectBeam ? 'generate-text' : 'generate-text-beam', conversationId, history);
|
||||
}, [_handleExecute]);
|
||||
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[]) => {
|
||||
await handleExecuteAndOutcome('generate-text', conversationId, history);
|
||||
}, [handleExecuteAndOutcome]);
|
||||
|
||||
const handleMessageRegenerateLast = React.useCallback(async () => {
|
||||
const focusedConversation = getConversation(focusedConversationId);
|
||||
const handleMessageRegenerateLastInFocusedPane = React.useCallback(async () => {
|
||||
const focusedConversation = getConversation(focusedPaneConversationId);
|
||||
if (focusedConversation?.messages?.length) {
|
||||
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
|
||||
return await _handleExecute('generate-text', focusedConversation.id, lastMessage.role === 'assistant'
|
||||
? focusedConversation.messages.slice(0, -1)
|
||||
: [...focusedConversation.messages],
|
||||
);
|
||||
const history = lastMessage.role === 'assistant' ? focusedConversation.messages.slice(0, -1) : [...focusedConversation.messages];
|
||||
await handleExecuteAndOutcome('generate-text', focusedConversation.id, history);
|
||||
}
|
||||
}, [focusedConversationId, _handleExecute]);
|
||||
}, [focusedPaneConversationId, handleExecuteAndOutcome]);
|
||||
|
||||
const handleMessageBeamLastInFocusedPane = React.useCallback(async () => {
|
||||
// Ctrl + Shift + B
|
||||
const focusedConversation = getConversation(focusedPaneConversationId);
|
||||
if (focusedConversation?.messages?.length) {
|
||||
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
|
||||
if (lastMessage.role === 'assistant')
|
||||
ConversationsManager.getHandler(focusedConversation.id).beamInvoke(focusedConversation.messages.slice(0, -1), [lastMessage], lastMessage.id);
|
||||
else if (lastMessage.role === 'user')
|
||||
ConversationsManager.getHandler(focusedConversation.id).beamInvoke(focusedConversation.messages, [], null);
|
||||
}
|
||||
}, [focusedPaneConversationId]);
|
||||
|
||||
const handleTextDiagram = React.useCallback((diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig), []);
|
||||
|
||||
const handleTextImagine = React.useCallback(async (conversationId: DConversationId, messageText: string): Promise<void> => {
|
||||
const handleTextImagine = React.useCallback(async (conversationId: DConversationId, messageText: string) => {
|
||||
const conversation = getConversation(conversationId);
|
||||
if (!conversation)
|
||||
return;
|
||||
const imaginedPrompt = await imaginePromptFromText(messageText) || 'An error sign.';
|
||||
return await _handleExecute('generate-image', conversationId, [
|
||||
const imaginedPrompt = await imaginePromptFromText(messageText, conversationId) || 'An error sign.';
|
||||
await handleExecuteAndOutcome('generate-image', conversationId, [
|
||||
...conversation.messages,
|
||||
createDMessage('user', imaginedPrompt),
|
||||
]);
|
||||
}, [_handleExecute]);
|
||||
}, [handleExecuteAndOutcome]);
|
||||
|
||||
const handleTextSpeak = React.useCallback(async (text: string): Promise<void> => {
|
||||
await speakText(text);
|
||||
@@ -318,13 +291,15 @@ export function AppChat() {
|
||||
|
||||
// Chat actions
|
||||
|
||||
const handleConversationNew = React.useCallback((forceNoRecycle?: boolean) => {
|
||||
const handleConversationNewInFocusedPane = React.useCallback((forceNoRecycle?: boolean) => {
|
||||
|
||||
// activate an existing new conversation if present, or create another
|
||||
const conversationId = (newConversationId && !forceNoRecycle)
|
||||
? newConversationId
|
||||
: prependNewConversation(getConversationSystemPurposeId(focusedConversationId) ?? undefined);
|
||||
setFocusedConversationId(conversationId);
|
||||
// create conversation (or recycle the existing top-of-stack empty conversation)
|
||||
const conversationId = (recycleNewConversationId && !forceNoRecycle)
|
||||
? recycleNewConversationId
|
||||
: prependNewConversation(getConversationSystemPurposeId(focusedPaneConversationId) ?? undefined);
|
||||
|
||||
// switch the focused pane to the new conversation
|
||||
handleOpenConversationInFocusedPane(conversationId);
|
||||
|
||||
// if a folder is active, add the new conversation to the folder
|
||||
if (activeFolderId && conversationId)
|
||||
@@ -333,7 +308,7 @@ export function AppChat() {
|
||||
// focus the composer
|
||||
composerTextAreaRef.current?.focus();
|
||||
|
||||
}, [activeFolderId, focusedConversationId, newConversationId, prependNewConversation, setFocusedConversationId]);
|
||||
}, [activeFolderId, focusedPaneConversationId, handleOpenConversationInFocusedPane, prependNewConversation, recycleNewConversationId]);
|
||||
|
||||
const handleConversationImportDialog = React.useCallback(() => setTradeConfig({ dir: 'import' }), []);
|
||||
|
||||
@@ -341,6 +316,32 @@ export function AppChat() {
|
||||
setTradeConfig({ dir: 'export', conversationId, exportAll });
|
||||
}, []);
|
||||
|
||||
const handleFileOpenConversation = React.useCallback(() => {
|
||||
openAndLoadConversations(true)
|
||||
.then((outcome) => {
|
||||
// activate the last (most recent) imported conversation
|
||||
if (outcome?.activateConversationId) {
|
||||
showNextTitleChange.current = true;
|
||||
handleOpenConversationInFocusedPane(outcome.activateConversationId);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
addSnackbar({ key: 'chat-import-fail', message: 'Could not open the file.', type: 'issue' });
|
||||
});
|
||||
}, [handleOpenConversationInFocusedPane]);
|
||||
|
||||
const handleFileSaveConversation = React.useCallback((conversationId: DConversationId | null) => {
|
||||
const conversation = getConversation(conversationId);
|
||||
conversation && downloadConversation(conversation, 'json')
|
||||
.then(() => {
|
||||
addSnackbar({ key: 'chat-save-as-ok', message: 'File saved.', type: 'success' });
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (err?.name !== 'AbortError')
|
||||
addSnackbar({ key: 'chat-save-as-fail', message: `Could not save the file. ${err?.message || ''}`, type: 'issue' });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConversationBranch = React.useCallback((srcConversationId: DConversationId, messageId: string | null): DConversationId | null => {
|
||||
// clone data
|
||||
const branchedConversationId = branchConversation(srcConversationId, messageId);
|
||||
@@ -351,22 +352,22 @@ export function AppChat() {
|
||||
|
||||
// replace/open a new pane with this
|
||||
showNextTitleChange.current = true;
|
||||
if (isMultiAddable)
|
||||
openSplitConversationId(branchedConversationId);
|
||||
if (!isMultiAddable)
|
||||
handleOpenConversationInFocusedPane(branchedConversationId);
|
||||
else
|
||||
setFocusedConversationId(branchedConversationId);
|
||||
handleOpenConversationInSplitPane(branchedConversationId);
|
||||
|
||||
return branchedConversationId;
|
||||
}, [activeFolderId, branchConversation, isMultiAddable, openSplitConversationId, setFocusedConversationId]);
|
||||
}, [activeFolderId, branchConversation, handleOpenConversationInFocusedPane, handleOpenConversationInSplitPane, isMultiAddable]);
|
||||
|
||||
const handleConversationFlatten = React.useCallback((conversationId: DConversationId) => setFlattenConversationId(conversationId), []);
|
||||
|
||||
const handleConfirmedClearConversation = React.useCallback(() => {
|
||||
if (clearConversationId) {
|
||||
setMessages(clearConversationId, []);
|
||||
ConversationsManager.getHandler(clearConversationId).messagesReplace([]);
|
||||
setClearConversationId(null);
|
||||
}
|
||||
}, [clearConversationId, setMessages]);
|
||||
}, [clearConversationId]);
|
||||
|
||||
const handleConversationClear = React.useCallback((conversationId: DConversationId) => setClearConversationId(conversationId), []);
|
||||
|
||||
@@ -374,13 +375,14 @@ export function AppChat() {
|
||||
if (!bypassConfirmation)
|
||||
return setDeleteConversationIds(conversationIds);
|
||||
|
||||
// perform deletion
|
||||
// perform deletion, and return the next (or a new) conversation
|
||||
const nextConversationId = deleteConversations(conversationIds, /*focusedSystemPurposeId ??*/ undefined);
|
||||
|
||||
setFocusedConversationId(nextConversationId);
|
||||
// switch the focused pane to the new conversation - NOTE: this makes the assumption that deletion had impact on the focused pane
|
||||
handleOpenConversationInFocusedPane(nextConversationId);
|
||||
|
||||
setDeleteConversationIds(null);
|
||||
}, [deleteConversations, setFocusedConversationId]);
|
||||
}, [deleteConversations, handleOpenConversationInFocusedPane]);
|
||||
|
||||
const handleConfirmedDeleteConversations = React.useCallback(() => {
|
||||
!!deleteConversationIds?.length && handleDeleteConversations(deleteConversationIds, true);
|
||||
@@ -396,17 +398,22 @@ export function AppChat() {
|
||||
}, [openLlmOptions]);
|
||||
|
||||
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
|
||||
// focused conversation
|
||||
['b', true, true, false, handleMessageBeamLastInFocusedPane],
|
||||
['r', true, true, false, handleMessageRegenerateLastInFocusedPane],
|
||||
['n', true, false, true, handleConversationNewInFocusedPane],
|
||||
['o', true, false, false, handleFileOpenConversation],
|
||||
['s', true, false, false, () => handleFileSaveConversation(focusedPaneConversationId)],
|
||||
['b', true, false, true, () => isFocusedChatEmpty || (focusedPaneConversationId && handleConversationBranch(focusedPaneConversationId, null))],
|
||||
['x', true, false, true, () => isFocusedChatEmpty || (focusedPaneConversationId && handleConversationClear(focusedPaneConversationId))],
|
||||
['d', true, false, true, () => focusedPaneConversationId && handleDeleteConversations([focusedPaneConversationId], false)],
|
||||
[ShortcutKeyName.Left, true, false, true, () => handleNavigateHistoryInFocusedPane('back')],
|
||||
[ShortcutKeyName.Right, true, false, true, () => handleNavigateHistoryInFocusedPane('forward')],
|
||||
// global
|
||||
['o', true, true, false, handleOpenChatLlmOptions],
|
||||
['r', true, true, false, handleMessageRegenerateLast],
|
||||
['n', true, false, true, handleConversationNew],
|
||||
['b', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationBranch(focusedConversationId, null))],
|
||||
['x', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationClear(focusedConversationId))],
|
||||
['d', true, false, true, () => focusedConversationId && handleDeleteConversations([focusedConversationId], false)],
|
||||
['+', true, true, false, useUIPreferencesStore.getState().increaseContentScaling],
|
||||
['-', true, true, false, useUIPreferencesStore.getState().decreaseContentScaling],
|
||||
[ShortcutKeyName.Left, true, false, true, () => handleNavigateHistory('back')],
|
||||
[ShortcutKeyName.Right, true, false, true, () => handleNavigateHistory('forward')],
|
||||
], [focusedConversationId, handleConversationBranch, handleConversationClear, handleConversationNew, handleDeleteConversations, handleMessageRegenerateLast, handleNavigateHistory, handleOpenChatLlmOptions, isFocusedChatEmpty]);
|
||||
], [focusedPaneConversationId, handleConversationBranch, handleConversationClear, handleConversationNewInFocusedPane, handleFileOpenConversation, handleFileSaveConversation, handleDeleteConversations, handleMessageBeamLastInFocusedPane, handleMessageRegenerateLastInFocusedPane, handleNavigateHistoryInFocusedPane, handleOpenChatLlmOptions, isFocusedChatEmpty]);
|
||||
useGlobalShortcuts(shortcuts);
|
||||
|
||||
|
||||
@@ -414,48 +421,50 @@ export function AppChat() {
|
||||
|
||||
const barAltTitle = showAltTitleBar ? focusedChatTitle ?? 'No Chat' : null;
|
||||
|
||||
const barContent = React.useMemo(() =>
|
||||
(barAltTitle === null)
|
||||
? <ChatDropdowns conversationId={focusedConversationId} />
|
||||
: <ChatTitle conversationId={focusedConversationId} conversationTitle={barAltTitle} />
|
||||
, [focusedConversationId, barAltTitle],
|
||||
const focusedBarContent = React.useMemo(() => beamOpenStoreInFocusedPane
|
||||
? <ChatBarAltBeam beamStore={beamOpenStoreInFocusedPane} isMobile={isMobile} />
|
||||
: (barAltTitle === null)
|
||||
? <ChatBarDropdowns conversationId={focusedPaneConversationId} />
|
||||
: <ChatBarAltTitle conversationId={focusedPaneConversationId} conversationTitle={barAltTitle} />
|
||||
, [barAltTitle, beamOpenStoreInFocusedPane, focusedPaneConversationId, isMobile],
|
||||
);
|
||||
|
||||
const drawerContent = React.useMemo(() =>
|
||||
<ChatDrawerMemo
|
||||
isMobile={isMobile}
|
||||
activeConversationId={focusedConversationId}
|
||||
activeConversationId={focusedPaneConversationId}
|
||||
activeFolderId={activeFolderId}
|
||||
chatPanesConversationIds={chatPanes.map(pane => pane.conversationId).filter(Boolean) as DConversationId[]}
|
||||
disableNewButton={disableNewButton}
|
||||
onConversationActivate={setFocusedConversationId}
|
||||
onConversationActivate={handleOpenConversationInFocusedPane}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationNew={handleConversationNew}
|
||||
onConversationNew={handleConversationNewInFocusedPane}
|
||||
onConversationsDelete={handleDeleteConversations}
|
||||
onConversationsExportDialog={handleConversationExport}
|
||||
onConversationsImportDialog={handleConversationImportDialog}
|
||||
setActiveFolderId={setActiveFolderId}
|
||||
/>,
|
||||
[activeFolderId, chatPanes, disableNewButton, focusedConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNew, handleDeleteConversations, isMobile, setFocusedConversationId],
|
||||
[activeFolderId, chatPanes, disableNewButton, focusedPaneConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNewInFocusedPane, handleDeleteConversations, handleOpenConversationInFocusedPane, isMobile],
|
||||
);
|
||||
|
||||
const menuItems = React.useMemo(() =>
|
||||
const focusedMenuItems = React.useMemo(() =>
|
||||
<ChatPageMenuItems
|
||||
isMobile={isMobile}
|
||||
conversationId={focusedConversationId}
|
||||
disableItems={!focusedConversationId || isFocusedChatEmpty}
|
||||
hasConversations={!areChatsEmpty}
|
||||
conversationId={focusedPaneConversationId}
|
||||
disableItems={!focusedPaneConversationId || isFocusedChatEmpty}
|
||||
hasConversations={hasConversations}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationClear={handleConversationClear}
|
||||
onConversationFlatten={handleConversationFlatten}
|
||||
// onConversationNew={handleConversationNew}
|
||||
// onConversationNew={handleConversationNewInFocusedPane}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
/>,
|
||||
[areChatsEmpty, focusedConversationId, handleConversationBranch, handleConversationClear, handleConversationFlatten, /*handleConversationNew,*/ isFocusedChatEmpty, isMessageSelectionMode, isMobile],
|
||||
[focusedPaneConversationId, handleConversationBranch, handleConversationClear, handleConversationFlatten, hasConversations, isFocusedChatEmpty, isMessageSelectionMode, isMobile],
|
||||
);
|
||||
|
||||
usePluggableOptimaLayout(drawerContent, barContent, menuItems, 'AppChat');
|
||||
usePluggableOptimaLayout(drawerContent, focusedBarContent, focusedMenuItems, 'AppChat');
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
@@ -465,11 +474,14 @@ export function AppChat() {
|
||||
>
|
||||
|
||||
{chatPanes.map((pane, idx) => {
|
||||
const _paneIsFocused = idx === focusedPaneIndex;
|
||||
const _paneConversationId = pane.conversationId;
|
||||
const _paneChatHandler = chatHandlers[idx] ?? null;
|
||||
const _paneBeamStore = beamsStores[idx] ?? null;
|
||||
const _paneBeamIsOpen = !!beamsOpens?.[idx] && !!_paneBeamStore;
|
||||
const _panesCount = chatPanes.length;
|
||||
const _keyAndId = `chat-pane-${idx}-${_paneConversationId}`;
|
||||
const _sepId = `sep-pane-${idx}-${_paneConversationId}`;
|
||||
const _keyAndId = `chat-pane-${pane.paneId}`;
|
||||
const _sepId = `sep-pane-${idx}`;
|
||||
return <React.Fragment key={_keyAndId}>
|
||||
|
||||
<Panel
|
||||
@@ -480,7 +492,7 @@ export function AppChat() {
|
||||
minSize={20}
|
||||
onClick={(event) => {
|
||||
const setFocus = chatPanes.length < 2 || !event.altKey;
|
||||
setFocusedPane(setFocus ? idx : -1);
|
||||
setFocusedPaneIndex(setFocus ? idx : -1);
|
||||
}}
|
||||
onCollapse={() => {
|
||||
// NOTE: despite the delay to try to let the draggin settle, there seems to be an issue with the Pane locking the screen
|
||||
@@ -493,12 +505,13 @@ export function AppChat() {
|
||||
position: 'relative',
|
||||
...(isMultiPane ? {
|
||||
borderRadius: '0.375rem',
|
||||
border: `2px solid ${idx === focusedPaneIndex
|
||||
border: `2px solid ${_paneIsFocused
|
||||
? ((willMulticast || !isMultiConversationId) ? theme.palette.primary.solidBg : theme.palette.primary.solidBg)
|
||||
: ((willMulticast || !isMultiConversationId) ? theme.palette.warning.softActiveBg : theme.palette.background.level1)}`,
|
||||
filter: (!willMulticast && idx !== focusedPaneIndex)
|
||||
? (!isMultiConversationId ? 'grayscale(66.67%)' /* clone of the same */ : 'grayscale(66.67%)')
|
||||
: undefined,
|
||||
: ((willMulticast || !isMultiConversationId) ? theme.palette.primary.softActiveBg : theme.palette.background.level1)}`,
|
||||
// DISABLED on 2024-03-13, it gets in the way quite a lot
|
||||
// filter: (!willMulticast && !_paneIsFocused)
|
||||
// ? (!isMultiConversationId ? 'grayscale(66.67%)' /* clone of the same */ : 'grayscale(66.67%)')
|
||||
// : undefined,
|
||||
} : {
|
||||
// NOTE: this is a workaround for the 'stuck-after-collapse-close' issue. We will collapse the 'other' pane, which
|
||||
// will get it removed (onCollapse), and somehow this pane will be stuck with a pointerEvents: 'none' style, which de-facto
|
||||
@@ -512,62 +525,46 @@ export function AppChat() {
|
||||
|
||||
<ScrollToBottom
|
||||
bootToBottom
|
||||
stickToBottom
|
||||
sx={{
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
}}
|
||||
stickToBottomInitial
|
||||
sx={{ display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
|
||||
<ChatMessageList
|
||||
conversationId={_paneConversationId}
|
||||
conversationHandler={_paneChatHandler}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
|
||||
fitScreen={isMobile || isMultiPane}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationExecuteHistory={handleConversationExecuteHistory}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImagine}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
sx={{
|
||||
minHeight: '100%', // ensures filling of the blank space on newer chats
|
||||
}}
|
||||
/>
|
||||
{!_paneBeamIsOpen && (
|
||||
<ChatMessageList
|
||||
conversationId={_paneConversationId}
|
||||
conversationHandler={_paneChatHandler}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
|
||||
fitScreen={isMobile || isMultiPane}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationExecuteHistory={handleConversationExecuteHistory}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImagine}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/*<Ephemerals*/}
|
||||
{/* conversationId={_paneConversationId}*/}
|
||||
{/* sx={{*/}
|
||||
{/* // TODO: Fixme post panels?*/}
|
||||
{/* // flexGrow: 0.1,*/}
|
||||
{/* flexShrink: 0.5,*/}
|
||||
{/* overflowY: 'auto',*/}
|
||||
{/* minHeight: 64,*/}
|
||||
{/* }}*/}
|
||||
{/*/>*/}
|
||||
{_paneBeamIsOpen && (
|
||||
<ChatBeamWrapper
|
||||
beamStore={_paneBeamStore}
|
||||
isMobile={isMobile}
|
||||
inlineSx={{
|
||||
flexGrow: 1,
|
||||
// minHeight: 'calc(100vh - 69px - var(--AGI-Nav-width))',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Visibility and actions are handled via Context */}
|
||||
<ScrollToBottomButton />
|
||||
|
||||
</ScrollToBottom>
|
||||
|
||||
{/* Best-Of Mode */}
|
||||
<Beam
|
||||
conversationHandler={_paneChatHandler}
|
||||
isMobile={isMobile}
|
||||
sx={{
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'background.level2',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1, // stay on top of Chips :shrug:
|
||||
}}
|
||||
/>
|
||||
|
||||
</Panel>
|
||||
|
||||
{/* Panel Separators & Resizers */}
|
||||
@@ -586,20 +583,14 @@ export function AppChat() {
|
||||
isMobile={isMobile}
|
||||
chatLLM={chatLLM}
|
||||
composerTextAreaRef={composerTextAreaRef}
|
||||
conversationId={focusedConversationId}
|
||||
conversationId={focusedPaneConversationId}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
isMulticast={!isMultiConversationId ? null : isComposerMulticast}
|
||||
isDeveloperMode={isFocusedChatDeveloper}
|
||||
onAction={handleComposerAction}
|
||||
onTextImagine={handleTextImagine}
|
||||
setIsMulticast={setIsComposerMulticast}
|
||||
sx={{
|
||||
zIndex: 21, // position: 'sticky', bottom: 0,
|
||||
backgroundColor: themeBgAppChatComposer,
|
||||
borderTop: `1px solid`,
|
||||
borderTopColor: 'divider',
|
||||
p: { xs: 1, md: 2 },
|
||||
}}
|
||||
sx={beamOpenStoreInFocusedPane ? composerClosedSx : composerOpenSx}
|
||||
/>
|
||||
|
||||
{/* Diagrams */}
|
||||
@@ -618,7 +609,7 @@ export function AppChat() {
|
||||
{!!tradeConfig && (
|
||||
<TradeModal
|
||||
config={tradeConfig}
|
||||
onConversationActivate={setFocusedConversationId}
|
||||
onConversationActivate={handleOpenConversationInFocusedPane}
|
||||
onClose={() => setTradeConfig(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { getUXLabsChatBeam } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsBeam: ICommandsProvider = {
|
||||
id: 'ass-beam',
|
||||
id: 'mode-beam',
|
||||
rank: 9,
|
||||
|
||||
getCommands: () => getUXLabsChatBeam() ? [{
|
||||
getCommands: () => [{
|
||||
primary: '/beam',
|
||||
arguments: ['prompt'],
|
||||
description: 'Best of multiple replies',
|
||||
description: 'Combine the smarts of models',
|
||||
Icon: ChatBeamIcon,
|
||||
}] : [],
|
||||
}],
|
||||
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
@@ -11,7 +11,7 @@ export const CommandsDraw: ICommandsProvider = {
|
||||
alternatives: ['/imagine', '/img'],
|
||||
arguments: ['prompt'],
|
||||
description: 'Assistant will draw the text',
|
||||
Icon: FormatPaintIcon,
|
||||
Icon: FormatPaintTwoToneIcon,
|
||||
}],
|
||||
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CommandsHelp } from './CommandsHelp';
|
||||
import { CommandsReact } from './CommandsReact';
|
||||
|
||||
|
||||
export type CommandsProviderId = 'ass-beam' | 'ass-browse' | 'ass-t2i' | 'ass-react' | 'chat-alter' | 'cmd-help';
|
||||
export type CommandsProviderId = 'ass-browse' | 'ass-t2i' | 'ass-react' | 'chat-alter' | 'cmd-help' | 'mode-beam';
|
||||
|
||||
type TextCommandPiece =
|
||||
| { type: 'text'; value: string; }
|
||||
@@ -16,12 +16,12 @@ type TextCommandPiece =
|
||||
|
||||
|
||||
const ChatCommandsProviders: Record<CommandsProviderId, ICommandsProvider> = {
|
||||
'ass-beam': CommandsBeam,
|
||||
'ass-browse': CommandsBrowse,
|
||||
'ass-react': CommandsReact,
|
||||
'ass-t2i': CommandsDraw,
|
||||
'chat-alter': CommandsAlter,
|
||||
'cmd-help': CommandsHelp,
|
||||
'mode-beam': CommandsBeam,
|
||||
};
|
||||
|
||||
export function findAllChatCommands(): ChatCommand[] {
|
||||
@@ -40,7 +40,10 @@ export function extractChatCommand(input: string): TextCommandPiece[] {
|
||||
|
||||
// Find the first space to separate the command from its parameters (if any)
|
||||
const firstSpaceIndex = inputTrimmed.indexOf(' ');
|
||||
const potentialCommand = inputTrimmed.substring(0, firstSpaceIndex >= 0 ? firstSpaceIndex : inputTrimmed.length);
|
||||
const commandMatch = inputTrimmed.match(/^\/\S+/);
|
||||
const potentialCommand = commandMatch ? commandMatch[0] : inputTrimmed;
|
||||
|
||||
const textAfterCommand = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
|
||||
// Check if the potential command is an actual command
|
||||
for (const provider of Object.values(ChatCommandsProviders)) {
|
||||
@@ -48,22 +51,33 @@ export function extractChatCommand(input: string): TextCommandPiece[] {
|
||||
if (cmd.primary === potentialCommand || cmd.alternatives?.includes(potentialCommand)) {
|
||||
|
||||
// command needs arguments: take the rest of the input as parameters
|
||||
if (cmd.arguments?.length) {
|
||||
const params = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
return [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: params || undefined, isError: !params || undefined }];
|
||||
}
|
||||
if (cmd.arguments?.length) return [{
|
||||
type: 'cmd',
|
||||
providerId: provider.id,
|
||||
command: potentialCommand,
|
||||
params: textAfterCommand || undefined,
|
||||
isError: !textAfterCommand || undefined,
|
||||
}];
|
||||
|
||||
// command without arguments, treat any text after as a separate text piece
|
||||
const pieces: TextCommandPiece[] = [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: undefined }];
|
||||
const textAfterCommand = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
if (textAfterCommand)
|
||||
pieces.push({ type: 'text', value: textAfterCommand });
|
||||
const pieces: TextCommandPiece[] = [{
|
||||
type: 'cmd',
|
||||
providerId: provider.id,
|
||||
command: potentialCommand,
|
||||
params: undefined,
|
||||
}];
|
||||
textAfterCommand && pieces.push({
|
||||
type: 'text',
|
||||
value: textAfterCommand,
|
||||
});
|
||||
return pieces;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No command found, return the entire input as text
|
||||
return [{ type: 'text', value: input }];
|
||||
return [{
|
||||
type: 'text',
|
||||
value: input,
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import * as React from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, IconButton, Typography } from '@mui/joy';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import FullscreenRoundedIcon from '@mui/icons-material/FullscreenRounded';
|
||||
|
||||
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
|
||||
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { animationBackgroundBeamGather, animationColorBeamScatterINV, animationEnterBelow } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
export function ChatBarAltBeam(props: {
|
||||
beamStore: BeamStoreApi,
|
||||
isMobile?: boolean
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [showCloseConfirmation, setShowCloseConfirmation] = React.useState(false);
|
||||
|
||||
|
||||
// external beam state
|
||||
const { isScattering, isGatheringAny, requiresConfirmation, setIsMaximized, terminateBeam } = useBeamStore(props.beamStore, useShallow((store) => ({
|
||||
// state
|
||||
isScattering: store.isScattering,
|
||||
isGatheringAny: store.isGatheringAny,
|
||||
requiresConfirmation: store.isScattering || store.isGatheringAny || store.raysReady > 0,
|
||||
// actions
|
||||
setIsMaximized: store.setIsMaximized,
|
||||
terminateBeam: store.terminateKeepingSettings,
|
||||
})));
|
||||
|
||||
|
||||
// closure handlers
|
||||
|
||||
const handleCloseBeam = React.useCallback(() => {
|
||||
if (requiresConfirmation)
|
||||
setShowCloseConfirmation(true);
|
||||
else
|
||||
terminateBeam();
|
||||
}, [requiresConfirmation, terminateBeam]);
|
||||
|
||||
const handleCloseConfirmation = React.useCallback(() => {
|
||||
terminateBeam();
|
||||
setShowCloseConfirmation(false);
|
||||
}, [terminateBeam]);
|
||||
|
||||
const handleCloseDenial = React.useCallback(() => {
|
||||
setShowCloseConfirmation(false);
|
||||
}, []);
|
||||
|
||||
const handleMaximizeBeam = React.useCallback(() => {
|
||||
setIsMaximized(true);
|
||||
}, [setIsMaximized]);
|
||||
|
||||
|
||||
// intercept esc this beam is focused
|
||||
useGlobalShortcut(ShortcutKeyName.Esc, false, false, false, handleCloseBeam);
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: { xs: 1, md: 2 }, alignItems: 'center' }}>
|
||||
|
||||
{/* Title & Status */}
|
||||
<Typography level='title-md'>
|
||||
<Box
|
||||
component='span'
|
||||
sx={
|
||||
isGatheringAny ? { animation: `${animationBackgroundBeamGather} 3s infinite, ${animationEnterBelow} 0.6s`, px: 1.5, py: 0.5 }
|
||||
: isScattering ? { animation: `${animationColorBeamScatterINV} 5s infinite, ${animationEnterBelow} 0.6s` }
|
||||
: { fontWeight: 'lg' }
|
||||
}>
|
||||
{isGatheringAny ? 'Merging...' : isScattering ? 'Beaming...' : 'Beam'}
|
||||
</Box>
|
||||
{(!isGatheringAny && !isScattering) && ' Mode'}
|
||||
</Typography>
|
||||
|
||||
{/* Right Close Icon */}
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
|
||||
{/* [desktop] maximize button, or a disabled spacer */}
|
||||
{!props.isMobile && (
|
||||
<GoodTooltip usePlain title={<Box sx={{ p: 1 }}>Maximize</Box>}>
|
||||
<IconButton size='sm' onClick={handleMaximizeBeam}>
|
||||
<FullscreenRoundedIcon />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
)}
|
||||
|
||||
<GoodTooltip usePlain title={<Box sx={{ p: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>Back to Chat <KeyStroke combo='Esc' /></Box>}>
|
||||
<IconButton aria-label='Close' size='sm' onClick={handleCloseBeam}>
|
||||
<CloseRoundedIcon />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
{showCloseConfirmation && (
|
||||
<ConfirmationModal
|
||||
open
|
||||
onClose={handleCloseDenial}
|
||||
onPositive={handleCloseConfirmation}
|
||||
lowStakes
|
||||
noTitleBar
|
||||
confirmationText='Are you sure you want to close Beam Mode? Unsaved text will be lost.'
|
||||
positiveActionText='Yes, close'
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { CHAT_NOVEL_TITLE } from '../AppChat';
|
||||
import { FadeInButton } from './ChatDrawerItem';
|
||||
|
||||
|
||||
export function ChatTitle(props: {
|
||||
export function ChatBarAltTitle(props: {
|
||||
conversationId: DConversationId | null,
|
||||
conversationTitle: string,
|
||||
}) {
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { usePersonaIdDropdown } from './usePersonaDropdown';
|
||||
import { useFolderDropdown } from './folders/useFolderDropdown';
|
||||
|
||||
|
||||
export function ChatDropdowns(props: {
|
||||
export function ChatBarDropdowns(props: {
|
||||
conversationId: DConversationId | null
|
||||
}) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Modal, ModalClose } from '@mui/joy';
|
||||
|
||||
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
|
||||
import { BeamView } from '~/modules/beam/BeamView';
|
||||
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
|
||||
|
||||
/*const overlaySx: SxProps = {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: themeZIndexBeamView, // stay on top of Message > Chips (:1), and Overlays (:2) - note: Desktop Drawer (:26)
|
||||
}*/
|
||||
|
||||
|
||||
export function ChatBeamWrapper(props: {
|
||||
beamStore: BeamStoreApi,
|
||||
isMobile: boolean,
|
||||
inlineSx?: SxProps,
|
||||
}) {
|
||||
|
||||
// state
|
||||
const isMaximized = useBeamStore(props.beamStore, state => state.isMaximized);
|
||||
|
||||
const handleUnMaximize = React.useCallback(() => {
|
||||
props.beamStore.getState().setIsMaximized(false);
|
||||
}, [props.beamStore]);
|
||||
|
||||
// memo the beamview
|
||||
const beamView = React.useMemo(() => (
|
||||
<BeamView
|
||||
beamStore={props.beamStore}
|
||||
isMobile={props.isMobile}
|
||||
showExplainer
|
||||
/>
|
||||
), [props.beamStore, props.isMobile]);
|
||||
|
||||
return isMaximized ? (
|
||||
<Modal open onClose={handleUnMaximize}>
|
||||
<Box sx={{
|
||||
backgroundColor: 'background.level1',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
}}>
|
||||
<ScrollToBottom disableAutoStick>
|
||||
{beamView}
|
||||
</ScrollToBottom>
|
||||
<ModalClose sx={{ color: 'white', backgroundColor: 'background.surface', boxShadow: 'xs', mr: 2 }} />
|
||||
</Box>
|
||||
</Modal>
|
||||
) : (
|
||||
<Box sx={props.inlineSx}>
|
||||
{beamView}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, Dropdown, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
|
||||
import { Box, Button, Card, CardContent, Dropdown, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import RocketLaunchRoundedIcon from '@mui/icons-material/RocketLaunchRounded';
|
||||
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
@@ -17,8 +19,10 @@ import { DFolder, useFolderStore } from '~/common/state/store-folders';
|
||||
import { DebounceInputMemo } from '~/common/components/DebounceInput';
|
||||
import { FoldersToggleOff } from '~/common/components/icons/FoldersToggleOff';
|
||||
import { FoldersToggleOn } from '~/common/components/icons/FoldersToggleOn';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { PageDrawerHeader } from '~/common/layout/optima/components/PageDrawerHeader';
|
||||
import { PageDrawerList } from '~/common/layout/optima/components/PageDrawerList';
|
||||
import { ROUTE_APP_NEWS } from '~/common/app.routes';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { themeScalingMap, themeZIndexOverMobileDrawer } from '~/common/app.theme';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
@@ -26,9 +30,9 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { ChatDrawerItemMemo, FolderChangeRequest } from './ChatDrawerItem';
|
||||
import { ChatFolderList } from './folders/ChatFolderList';
|
||||
import { ChatNavGrouping, useChatNavRenderItems } from './useChatNavRenderItems';
|
||||
import { ChatNavGrouping, ChatSearchSorting, isDrawerSearching, useChatDrawerRenderItems } from './useChatDrawerRenderItems';
|
||||
import { ClearFolderText } from './folders/useFolderDropdown';
|
||||
import { useChatShowRelativeSize } from '../store-app-chat';
|
||||
import { useChatDrawerFilters } from '../store-app-chat';
|
||||
|
||||
|
||||
// this is here to make shallow comparisons work on the next hook
|
||||
@@ -37,7 +41,7 @@ const noFolders: DFolder[] = [];
|
||||
/*
|
||||
* Lists folders and returns the active folder
|
||||
*/
|
||||
export const useFolders = (activeFolderId: string | null) => useFolderStore(({ enableFolders, folders, toggleEnableFolders }) => {
|
||||
export const useFolders = (activeFolderId: string | null) => useFolderStore(useShallow(({ enableFolders, folders, toggleEnableFolders }) => {
|
||||
|
||||
// finds the active folder if any
|
||||
const activeFolder = (enableFolders && activeFolderId)
|
||||
@@ -50,7 +54,7 @@ export const useFolders = (activeFolderId: string | null) => useFolderStore(({ e
|
||||
enableFolders,
|
||||
toggleEnableFolders,
|
||||
};
|
||||
}, shallow);
|
||||
}));
|
||||
|
||||
|
||||
export const ChatDrawerMemo = React.memo(ChatDrawer);
|
||||
@@ -74,20 +78,32 @@ function ChatDrawer(props: {
|
||||
|
||||
// local state
|
||||
const [navGrouping, setNavGrouping] = React.useState<ChatNavGrouping>('date');
|
||||
const [searchSorting, setSearchSorting] = React.useState<ChatSearchSorting>('frequency');
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = React.useState('');
|
||||
const [folderChangeRequest, setFolderChangeRequest] = React.useState<FolderChangeRequest | null>(null);
|
||||
const [bigAgi2CalloutDismissed, setBigAgi2CalloutDismissed] = React.useState(() => {
|
||||
try {
|
||||
return localStorage.getItem('dismissedBA2ChatDrawerNotice') === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// external state
|
||||
const { closeDrawer, closeDrawerOnMobile } = useOptimaDrawers();
|
||||
const { showRelativeSize, toggleRelativeSize } = useChatShowRelativeSize();
|
||||
const {
|
||||
filterHasStars, toggleFilterHasStars,
|
||||
showPersonaIcons, toggleShowPersonaIcons,
|
||||
showRelativeSize, toggleShowRelativeSize,
|
||||
} = useChatDrawerFilters();
|
||||
const { activeFolder, allFolders, enableFolders, toggleEnableFolders } = useFolders(props.activeFolderId);
|
||||
const { filteredChatsCount, filteredChatIDs, filteredChatsAreEmpty, filteredChatsBarBasis, filteredChatsIncludeActive, renderNavItems } = useChatNavRenderItems(
|
||||
props.activeConversationId, props.chatPanesConversationIds, debouncedSearchQuery, activeFolder, allFolders, navGrouping, showRelativeSize,
|
||||
const { filteredChatsCount, filteredChatIDs, filteredChatsAreEmpty, filteredChatsBarBasis, filteredChatsIncludeActive, renderNavItems } = useChatDrawerRenderItems(
|
||||
props.activeConversationId, props.chatPanesConversationIds, debouncedSearchQuery, activeFolder, allFolders, filterHasStars, navGrouping, searchSorting, showRelativeSize,
|
||||
);
|
||||
const { contentScaling, showSymbols } = useUIPreferencesStore(state => ({
|
||||
const { contentScaling, showSymbols } = useUIPreferencesStore(useShallow(state => ({
|
||||
contentScaling: state.contentScaling,
|
||||
showSymbols: state.zenMode !== 'cleaner',
|
||||
}), shallow);
|
||||
})));
|
||||
|
||||
|
||||
// New/Activate/Delete Conversation
|
||||
@@ -119,6 +135,15 @@ function ChatDrawer(props: {
|
||||
props.activeConversationId && onConversationsExportDialog(props.activeConversationId, true);
|
||||
}, [onConversationsExportDialog, props.activeConversationId]);
|
||||
|
||||
const handleDismissBigAgi2Callout = React.useCallback(() => {
|
||||
setBigAgi2CalloutDismissed(true);
|
||||
try {
|
||||
localStorage.setItem('dismissedBA2ChatDrawerNotice', 'true');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
// Folder change request
|
||||
|
||||
@@ -140,6 +165,7 @@ function ChatDrawer(props: {
|
||||
|
||||
|
||||
// memoize the group dropdown
|
||||
const { isSearching } = isDrawerSearching(debouncedSearchQuery);
|
||||
const groupingComponent = React.useMemo(() => (
|
||||
<Dropdown>
|
||||
<MenuButton
|
||||
@@ -147,34 +173,67 @@ function ChatDrawer(props: {
|
||||
slots={{ root: IconButton }}
|
||||
slotProps={{ root: { size: 'sm' } }}
|
||||
>
|
||||
<MoreVertIcon sx={{ fontSize: 'xl' }} />
|
||||
<MoreVertIcon />
|
||||
</MenuButton>
|
||||
<Menu placement='bottom-start' sx={{ minWidth: 180, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Group By</Typography>
|
||||
</ListItem>
|
||||
{(['date', 'persona'] as const).map(_gName => (
|
||||
<MenuItem
|
||||
key={'group-' + _gName}
|
||||
aria-label={`Group by ${_gName}`}
|
||||
selected={navGrouping === _gName}
|
||||
onClick={() => setNavGrouping(grouping => grouping === _gName ? false : _gName)}
|
||||
>
|
||||
<ListItemDecorator>{navGrouping === _gName && <CheckIcon />}</ListItemDecorator>
|
||||
{capitalizeFirstLetter(_gName)}
|
||||
|
||||
{!isSearching ? (
|
||||
// Search/Filter default menu: Grouping, Filtering, ...
|
||||
<Menu placement='bottom-start' sx={{ minWidth: 180, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Group By</Typography>
|
||||
</ListItem>
|
||||
{(['date', 'persona'] as const).map(_gName => (
|
||||
<MenuItem
|
||||
key={'group-' + _gName}
|
||||
aria-label={`Group by ${_gName}`}
|
||||
selected={navGrouping === _gName}
|
||||
onClick={() => setNavGrouping(grouping => grouping === _gName ? false : _gName)}
|
||||
>
|
||||
<ListItemDecorator>{navGrouping === _gName && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
{capitalizeFirstLetter(_gName)}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<ListDivider />
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Filter</Typography>
|
||||
</ListItem>
|
||||
<MenuItem onClick={toggleFilterHasStars}>
|
||||
<ListItemDecorator>{filterHasStars && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Starred <StarOutlineRoundedIcon />
|
||||
</MenuItem>
|
||||
))}
|
||||
<ListDivider />
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Show</Typography>
|
||||
</ListItem>
|
||||
<MenuItem onClick={toggleRelativeSize}>
|
||||
<ListItemDecorator>{showRelativeSize && <CheckIcon />}</ListItemDecorator>
|
||||
Relative Size
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<ListDivider />
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Show</Typography>
|
||||
</ListItem>
|
||||
<MenuItem onClick={toggleShowPersonaIcons}>
|
||||
<ListItemDecorator>{showPersonaIcons && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Icons
|
||||
</MenuItem>
|
||||
<MenuItem onClick={toggleShowRelativeSize}>
|
||||
<ListItemDecorator>{showRelativeSize && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Relative Size
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
) : (
|
||||
// While searching, show the sorting options
|
||||
<Menu placement='bottom-start' sx={{ minWidth: 180, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Sort By</Typography>
|
||||
</ListItem>
|
||||
<MenuItem selected={searchSorting === 'frequency'} onClick={() => setSearchSorting('frequency')}>
|
||||
<ListItemDecorator>{searchSorting === 'frequency' && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Matches
|
||||
</MenuItem>
|
||||
<MenuItem selected={searchSorting === 'date'} onClick={() => setSearchSorting('date')}>
|
||||
<ListItemDecorator>{searchSorting === 'date' && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Date
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)}
|
||||
</Dropdown>
|
||||
), [navGrouping, showRelativeSize, toggleRelativeSize]);
|
||||
), [filterHasStars, isSearching, navGrouping, searchSorting, showPersonaIcons, showRelativeSize, toggleFilterHasStars, toggleShowPersonaIcons, toggleShowRelativeSize]);
|
||||
|
||||
|
||||
return <>
|
||||
@@ -182,13 +241,13 @@ function ChatDrawer(props: {
|
||||
{/* Drawer Header */}
|
||||
<PageDrawerHeader title='Chats' onClose={closeDrawer}>
|
||||
<Tooltip title={enableFolders ? 'Hide Folders' : 'Use Folders'}>
|
||||
<IconButton onClick={toggleEnableFolders}>
|
||||
<IconButton size='sm' onClick={toggleEnableFolders}>
|
||||
{enableFolders ? <FoldersToggleOn /> : <FoldersToggleOff />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</PageDrawerHeader>
|
||||
|
||||
{/* Folders List */}
|
||||
{/* Folders List (shrink at twice the rate as the Titles) */}
|
||||
{/*<Box sx={{*/}
|
||||
{/* display: 'grid',*/}
|
||||
{/* gridTemplateRows: !enableFolders ? '0fr' : '1fr',*/}
|
||||
@@ -205,6 +264,12 @@ function ChatDrawer(props: {
|
||||
contentScaling={contentScaling}
|
||||
activeFolderId={props.activeFolderId}
|
||||
onFolderSelect={props.setActiveFolderId}
|
||||
sx={{
|
||||
// shrink this at twice the rate as the Titles list
|
||||
flexGrow: 0, flexShrink: 2, overflow: 'hidden',
|
||||
minHeight: '7.5rem',
|
||||
p: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/*</Box>*/}
|
||||
@@ -214,89 +279,129 @@ function ChatDrawer(props: {
|
||||
|
||||
{enableFolders && <ListDivider sx={{ mb: 0 }} />}
|
||||
|
||||
{/* Search Input Field */}
|
||||
<DebounceInputMemo
|
||||
minChars={2}
|
||||
onDebounce={setDebouncedSearchQuery}
|
||||
debounceTimeout={300}
|
||||
placeholder='Search...'
|
||||
aria-label='Search'
|
||||
endDecorator={groupingComponent}
|
||||
sx={{ m: 2 }}
|
||||
/>
|
||||
{/* Search / New Chat */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', m: 2, gap: 2 }}>
|
||||
|
||||
{/* New Chat Button */}
|
||||
<ListItem sx={{ mx: '0.25rem', mb: 0.5 }}>
|
||||
<ListItemButton
|
||||
{/* Search Input Field */}
|
||||
<DebounceInputMemo
|
||||
minChars={2}
|
||||
onDebounce={setDebouncedSearchQuery}
|
||||
debounceTimeout={300}
|
||||
placeholder='Search...'
|
||||
aria-label='Search'
|
||||
endDecorator={groupingComponent}
|
||||
/>
|
||||
|
||||
{/* New Chat Button */}
|
||||
<Button
|
||||
// variant='outlined'
|
||||
variant={disableNewButton ? undefined : 'outlined'}
|
||||
variant={disableNewButton ? undefined : 'soft'}
|
||||
disabled={disableNewButton}
|
||||
onClick={handleButtonNew}
|
||||
sx={{
|
||||
// ...PageDrawerTallItemSx,
|
||||
px: 'calc(var(--ListItem-paddingX) - 0.25rem)',
|
||||
|
||||
// text size
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'lg',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '0px 0.75rem',
|
||||
|
||||
// style
|
||||
borderRadius: 'md',
|
||||
boxShadow: (disableNewButton || props.isMobile) ? 'none' : 'sm',
|
||||
backgroundColor: 'background.popup',
|
||||
transition: 'box-shadow 0.2s',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
borderRadius: 'sm',
|
||||
'--ListItemDecorator-size': 'calc(2.5rem - 1px)', // compensate for the border
|
||||
// backgroundColor: 'background.popup',
|
||||
// boxShadow: (disableNewButton || props.isMobile) ? 'none' : 'xs',
|
||||
// transition: 'box-shadow 0.2s',
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator><AddIcon sx={{ '--Icon-fontSize': 'var(--joy-fontSize-xl)', pl: '0.125rem' }} /></ListItemDecorator>
|
||||
<ListItemDecorator><AddIcon sx={{ fontSize: '' }} /></ListItemDecorator>
|
||||
New chat
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Button>
|
||||
|
||||
{/*<ListDivider sx={{ mt: 0 }} />*/}
|
||||
|
||||
{/* List of Chat Titles (and actions) */}
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', ...themeScalingMap[contentScaling].chatDrawerItemSx }}>
|
||||
{/*<ListItem sticky sx={{ justifyContent: 'space-between', boxShadow: 'sm' }}>*/}
|
||||
{/* <Typography level='body-sm'>*/}
|
||||
{/* Conversations*/}
|
||||
{/* </Typography>*/}
|
||||
{/* <ToggleButtonGroup variant='soft' size='sm' value={grouping} onChange={(_event, newValue) => newValue && setGrouping(newValue)}>*/}
|
||||
{/* <IconButton value='off'>*/}
|
||||
{/* <AccessTimeIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* <IconButton value='persona'>*/}
|
||||
{/* <PersonIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* </ToggleButtonGroup>*/}
|
||||
{/*</ListItem>*/}
|
||||
</Box>
|
||||
|
||||
{/* Chat Titles List (shrink as half the rate as the Folders List) */}
|
||||
<Box sx={{ flexGrow: 1, flexShrink: 1, flexBasis: '20rem', overflowY: 'auto', ...themeScalingMap[contentScaling].chatDrawerItemSx }}>
|
||||
{renderNavItems.map((item, idx) => item.type === 'nav-item-chat-data' ? (
|
||||
<ChatDrawerItemMemo
|
||||
key={'nav-chat-' + item.conversationId}
|
||||
item={item}
|
||||
showSymbols={showSymbols}
|
||||
showSymbols={showPersonaIcons && showSymbols}
|
||||
bottomBarBasis={filteredChatsBarBasis}
|
||||
onConversationActivate={handleConversationActivate}
|
||||
onConversationBranch={onConversationBranch}
|
||||
onConversationDelete={handleConversationDeleteNoConfirmation}
|
||||
onConversationDeleteNoConfirmation={handleConversationDeleteNoConfirmation}
|
||||
onConversationExport={onConversationsExportDialog}
|
||||
onConversationFolderChange={handleConversationFolderChange}
|
||||
/>
|
||||
) : item.type === 'nav-item-group' ? (
|
||||
<Typography key={'nav-divider-' + idx} level='body-xs' sx={{ textAlign: 'center', my: 'calc(var(--ListItem-minHeight) / 4)' }}>
|
||||
<Typography key={'nav-divider-' + idx} level='body-xs' sx={{
|
||||
textAlign: 'center',
|
||||
my: 'calc(var(--ListItem-minHeight) / 4)',
|
||||
// keeps the group header sticky to the top
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
backgroundColor: 'background.popup',
|
||||
zIndex: 1,
|
||||
}}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
) : item.type === 'nav-item-info-message' ? (
|
||||
<Typography key={'nav-info-' + idx} level='body-xs' sx={{ textAlign: 'center', my: 'calc(var(--ListItem-minHeight) / 2)' }}>
|
||||
<Typography key={'nav-info-' + idx} level='body-xs' sx={{ textAlign: 'center', color: 'primary.softColor', my: 'calc(var(--ListItem-minHeight) / 4)' }}>
|
||||
{filterHasStars && <StarOutlineRoundedIcon sx={{ color: 'primary.softColor', fontSize: 'xl', mb: -0.5, mr: 1 }} />}
|
||||
{item.message}
|
||||
{filterHasStars && <>
|
||||
<Button variant='soft' size='sm' onClick={toggleFilterHasStars} sx={{ display: 'block', mt: 2, mx: 'auto' }}>
|
||||
remove filters
|
||||
</Button>
|
||||
</>}
|
||||
</Typography>
|
||||
) : null,
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Big-AGI 2.0 Callout */}
|
||||
{!bigAgi2CalloutDismissed && (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Card variant='solid' color='primary' invertedColors>
|
||||
<CardContent sx={{ gap: 1, position: 'relative' }}>
|
||||
<IconButton
|
||||
size='sm'
|
||||
onClick={handleDismissBigAgi2Callout}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: -8,
|
||||
}}
|
||||
>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
<Typography level='title-sm'>
|
||||
Big-AGI 2.0 ✨ is Live!
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ mb: 1 }}>
|
||||
Experience Beam 2, Personas, and Cloud Sync.
|
||||
</Typography>
|
||||
<Button
|
||||
fullWidth
|
||||
size='sm'
|
||||
variant='solid'
|
||||
color='neutral'
|
||||
endDecorator={<RocketLaunchRoundedIcon />}
|
||||
component={Link}
|
||||
href={ROUTE_APP_NEWS}
|
||||
noLinkStyle
|
||||
>
|
||||
Learn More
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<ListDivider sx={{ my: 0 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{/* Bottom commands */}
|
||||
<Box sx={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
|
||||
<ListItemButton onClick={props.onConversationsImportDialog} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator>
|
||||
<FileUploadOutlinedIcon />
|
||||
|
||||
@@ -5,7 +5,7 @@ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
|
||||
@@ -21,6 +21,7 @@ import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { isDeepEqual } from '~/common/util/jsUtils';
|
||||
|
||||
import { CHAT_NOVEL_TITLE } from '../AppChat';
|
||||
import { STREAM_TEXT_INDICATOR } from '../editors/chat-stream';
|
||||
|
||||
|
||||
// set to true to display the conversation IDs
|
||||
@@ -41,7 +42,7 @@ export const ChatDrawerItemMemo = React.memo(ChatDrawerItem, (prev, next) =>
|
||||
prev.bottomBarBasis === next.bottomBarBasis &&
|
||||
prev.onConversationActivate === next.onConversationActivate &&
|
||||
prev.onConversationBranch === next.onConversationBranch &&
|
||||
prev.onConversationDelete === next.onConversationDelete &&
|
||||
prev.onConversationDeleteNoConfirmation === next.onConversationDeleteNoConfirmation &&
|
||||
prev.onConversationExport === next.onConversationExport &&
|
||||
prev.onConversationFolderChange === next.onConversationFolderChange,
|
||||
);
|
||||
@@ -53,6 +54,7 @@ export interface ChatNavigationItemData {
|
||||
isAlsoOpen: string | false;
|
||||
isEmpty: boolean;
|
||||
title: string;
|
||||
userFlagsSummary: string | undefined;
|
||||
folder: DFolder | null | undefined; // null: 'All', undefined: do not show folder select
|
||||
updatedAt: number;
|
||||
messageCount: number;
|
||||
@@ -74,7 +76,7 @@ function ChatDrawerItem(props: {
|
||||
bottomBarBasis: number,
|
||||
onConversationActivate: (conversationId: DConversationId, closeMenu: boolean) => void,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string | null) => void,
|
||||
onConversationDelete: (conversationId: DConversationId) => void,
|
||||
onConversationDeleteNoConfirmation: (conversationId: DConversationId) => void,
|
||||
onConversationExport: (conversationId: DConversationId, exportAll: boolean) => void,
|
||||
onConversationFolderChange: (folderChangeRequest: FolderChangeRequest) => void,
|
||||
}) {
|
||||
@@ -86,7 +88,7 @@ function ChatDrawerItem(props: {
|
||||
|
||||
// derived state
|
||||
const { onConversationBranch, onConversationExport, onConversationFolderChange } = props;
|
||||
const { conversationId, isActive, isAlsoOpen, title, folder, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
|
||||
const { conversationId, isActive, isAlsoOpen, title, userFlagsSummary, folder, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
|
||||
const isNew = messageCount === 0;
|
||||
|
||||
|
||||
@@ -153,7 +155,16 @@ function ChatDrawerItem(props: {
|
||||
|
||||
// Delete
|
||||
|
||||
const handleDeleteButtonShow = React.useCallback(() => setDeleteArmed(true), []);
|
||||
const { onConversationDeleteNoConfirmation } = props;
|
||||
const handleDeleteButtonShow = React.useCallback((event: React.MouseEvent) => {
|
||||
// special case: if 'Shift' is pressed, delete immediately
|
||||
if (event.shiftKey) {
|
||||
event.stopPropagation();
|
||||
onConversationDeleteNoConfirmation(conversationId);
|
||||
return;
|
||||
}
|
||||
setDeleteArmed(true);
|
||||
}, [conversationId, onConversationDeleteNoConfirmation]);
|
||||
|
||||
const handleDeleteButtonHide = React.useCallback(() => setDeleteArmed(false), []);
|
||||
|
||||
@@ -161,9 +172,9 @@ function ChatDrawerItem(props: {
|
||||
if (deleteArmed) {
|
||||
setDeleteArmed(false);
|
||||
event.stopPropagation();
|
||||
props.onConversationDelete(conversationId);
|
||||
onConversationDeleteNoConfirmation(conversationId);
|
||||
}
|
||||
}, [conversationId, deleteArmed, props]);
|
||||
}, [conversationId, deleteArmed, onConversationDeleteNoConfirmation]);
|
||||
|
||||
|
||||
const textSymbol = SystemPurposes[systemPurposeId]?.symbol || '❓';
|
||||
@@ -204,7 +215,7 @@ function ChatDrawerItem(props: {
|
||||
}}
|
||||
>
|
||||
{/*{DEBUG_CONVERSATION_IDS && `${conversationId} - `}*/}
|
||||
{title.trim() ? title : CHAT_NOVEL_TITLE}{assistantTyping && '...'}
|
||||
{title.trim() ? title : CHAT_NOVEL_TITLE}{assistantTyping && STREAM_TEXT_INDICATOR}
|
||||
</Box>
|
||||
) : (
|
||||
<InlineTextarea
|
||||
@@ -219,16 +230,19 @@ function ChatDrawerItem(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Display search frequency if it exists and is greater than 0 */}
|
||||
{searchFrequency > 0 && (
|
||||
<Box sx={{ ml: 1 }}>
|
||||
<Typography level='body-sm'>
|
||||
{searchFrequency}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{/* Right text */}
|
||||
{searchFrequency > 0 ? (
|
||||
// Display search frequency if it exists and is greater than 0
|
||||
<Typography level='body-sm'>
|
||||
{searchFrequency}
|
||||
</Typography>
|
||||
) : (userFlagsSummary && props.showSymbols) ? (
|
||||
<Typography sx={{ mr: '5px' }}>
|
||||
{userFlagsSummary}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
</>, [assistantTyping, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive, isEditingTitle, isNew, props.showSymbols, searchFrequency, textSymbol, title]);
|
||||
</>, [assistantTyping, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive, isEditingTitle, isNew, props.showSymbols, searchFrequency, textSymbol, title, userFlagsSummary]);
|
||||
|
||||
const progressBarFixedComponent = React.useMemo(() =>
|
||||
progress > 0 && (
|
||||
@@ -278,7 +292,7 @@ function ChatDrawerItem(props: {
|
||||
{/* buttons row */}
|
||||
{isActive && (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, minHeight: '2.25rem', alignItems: 'center' }}>
|
||||
<ListItemDecorator />
|
||||
{props.showSymbols && <ListItemDecorator />}
|
||||
|
||||
{/* Current Folder color, and change initiator */}
|
||||
{!deleteArmed && <>
|
||||
@@ -300,7 +314,7 @@ function ChatDrawerItem(props: {
|
||||
|
||||
<Tooltip disableInteractive title='Rename'>
|
||||
<FadeInButton size='sm' disabled={isEditingTitle || isAutoEditingTitle} onClick={handleTitleEditBegin}>
|
||||
<EditIcon />
|
||||
<EditRoundedIcon />
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, List } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
|
||||
@@ -10,17 +10,17 @@ import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { createDMessage, DConversationId, DMessage, DMessageUserFlag, getConversation, messageToggleUserFlag, useChatStore } from '~/common/state/store-chats';
|
||||
import { useBrowserTranslationWarning } from '~/common/components/useIsBrowserTranslating';
|
||||
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useEphemerals } from '~/common/chats/EphemeralsStore';
|
||||
import { useScrollToBottom } from '~/common/scroll-to-bottom/useScrollToBottom';
|
||||
|
||||
import { ChatMessage, ChatMessageMemo } from './message/ChatMessage';
|
||||
import { CleanerMessage, MessagesSelectionHeader } from './message/CleanerMessage';
|
||||
import { Ephemerals } from './Ephemerals';
|
||||
import { PersonaSelector } from './persona-selector/PersonaSelector';
|
||||
import { useChatShowSystemMessages } from '../store-app-chat';
|
||||
import { useScrollToBottom } from './scroll-to-bottom/useScrollToBottom';
|
||||
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ export function ChatMessageList(props: {
|
||||
fitScreen: boolean,
|
||||
isMessageSelectionMode: boolean,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
|
||||
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[], chatEffectBeam: boolean) => Promise<void>,
|
||||
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[]) => Promise<void>,
|
||||
onTextDiagram: (diagramConfig: DiagramConfig | null) => void,
|
||||
onTextImagine: (conversationId: DConversationId, selectedText: string) => Promise<void>,
|
||||
onTextSpeak: (selectedText: string) => Promise<void>,
|
||||
@@ -52,7 +52,7 @@ export function ChatMessageList(props: {
|
||||
const { openPreferencesTab } = useOptimaLayout();
|
||||
const [showSystemMessages] = useChatShowSystemMessages();
|
||||
const optionalTranslationWarning = useBrowserTranslationWarning();
|
||||
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(state => {
|
||||
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(useShallow(state => {
|
||||
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
|
||||
return {
|
||||
conversationMessages: conversation ? conversation.messages : [],
|
||||
@@ -61,7 +61,7 @@ export function ChatMessageList(props: {
|
||||
editMessage: state.editMessage,
|
||||
setMessages: state.setMessages,
|
||||
};
|
||||
}, shallow);
|
||||
}));
|
||||
const ephemerals = useEphemerals(props.conversationHandler);
|
||||
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
|
||||
|
||||
@@ -71,26 +71,50 @@ export function ChatMessageList(props: {
|
||||
|
||||
// text actions
|
||||
|
||||
const handleRunExample = React.useCallback(async (text: string) => {
|
||||
conversationId && await onConversationExecuteHistory(conversationId, [...conversationMessages, createDMessage('user', text)], false);
|
||||
const handleRunExample = React.useCallback(async (examplePrompt: string) => {
|
||||
conversationId && await onConversationExecuteHistory(conversationId, [...conversationMessages, createDMessage('user', examplePrompt)]);
|
||||
}, [conversationId, conversationMessages, onConversationExecuteHistory]);
|
||||
|
||||
|
||||
// message menu methods proxy
|
||||
|
||||
const handleConversationBranch = React.useCallback((messageId: string) => {
|
||||
conversationId && onConversationBranch(conversationId, messageId);
|
||||
}, [conversationId, onConversationBranch]);
|
||||
|
||||
const handleConversationRestartFrom = React.useCallback(async (messageId: string, offset: number, chatEffectBeam: boolean) => {
|
||||
const handleMessageAssistantFrom = React.useCallback(async (messageId: string, offset: number) => {
|
||||
const messages = getConversation(conversationId)?.messages;
|
||||
if (messages) {
|
||||
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + offset + 1);
|
||||
conversationId && await onConversationExecuteHistory(conversationId, truncatedHistory, chatEffectBeam);
|
||||
conversationId && await onConversationExecuteHistory(conversationId, truncatedHistory);
|
||||
}
|
||||
}, [conversationId, onConversationExecuteHistory]);
|
||||
|
||||
const handleConversationTruncate = React.useCallback((messageId: string) => {
|
||||
const handleMessageBeam = React.useCallback(async (messageId: string) => {
|
||||
// Right-click menu Beam
|
||||
if (!conversationId || !props.conversationHandler) return;
|
||||
const messages = getConversation(conversationId)?.messages;
|
||||
if (messages?.length) {
|
||||
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + 1);
|
||||
const lastMessage = truncatedHistory[truncatedHistory.length - 1];
|
||||
if (lastMessage) {
|
||||
// assistant: do an in-place beam
|
||||
if (lastMessage.role === 'assistant') {
|
||||
if (truncatedHistory.length >= 2)
|
||||
props.conversationHandler.beamInvoke(truncatedHistory.slice(0, -1), [lastMessage], lastMessage.id);
|
||||
} else {
|
||||
// user: truncate and append (but if the next message is an assistant message, import it)
|
||||
const nextMessage = messages[truncatedHistory.length];
|
||||
if (nextMessage?.role === 'assistant')
|
||||
props.conversationHandler.beamInvoke(truncatedHistory, [nextMessage], null);
|
||||
else
|
||||
props.conversationHandler.beamInvoke(truncatedHistory, [], null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [conversationId, props.conversationHandler]);
|
||||
|
||||
const handleMessageBranch = React.useCallback((messageId: string) => {
|
||||
conversationId && onConversationBranch(conversationId, messageId);
|
||||
}, [conversationId, onConversationBranch]);
|
||||
|
||||
const handleMessageTruncate = React.useCallback((messageId: string) => {
|
||||
const messages = getConversation(conversationId)?.messages;
|
||||
if (conversationId && messages) {
|
||||
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + 1);
|
||||
@@ -106,6 +130,16 @@ export function ChatMessageList(props: {
|
||||
conversationId && editMessage(conversationId, messageId, { text: newText }, true);
|
||||
}, [conversationId, editMessage]);
|
||||
|
||||
const handleMessageToggleUserFlag = React.useCallback((messageId: string, userFlag: DMessageUserFlag) => {
|
||||
conversationId && editMessage(conversationId, messageId, (message) => ({
|
||||
userFlags: messageToggleUserFlag(message, userFlag),
|
||||
}), false);
|
||||
}, [conversationId, editMessage]);
|
||||
|
||||
const handleReplyTo = React.useCallback((_messageId: string, text: string) => {
|
||||
props.conversationHandler?.getOverlayStore().getState().setReplyToText(text);
|
||||
}, [props.conversationHandler]);
|
||||
|
||||
const handleTextDiagram = React.useCallback(async (messageId: string, text: string) => {
|
||||
conversationId && onTextDiagram({ conversationId: conversationId, messageId, text });
|
||||
}, [conversationId, onTextDiagram]);
|
||||
@@ -195,12 +229,15 @@ export function ChatMessageList(props: {
|
||||
|
||||
return (
|
||||
<List sx={{
|
||||
p: 0, ...(props.sx || {}),
|
||||
// this makes sure that the the window is scrolled to the bottom (column-reverse)
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
p: 0,
|
||||
...(props.sx || {}),
|
||||
|
||||
// fix for the double-border on the last message (one by the composer, one to the bottom of the message)
|
||||
// marginBottom: '-1px',
|
||||
|
||||
// layout
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
|
||||
{optionalTranslationWarning}
|
||||
@@ -239,14 +276,17 @@ export function ChatMessageList(props: {
|
||||
isBottom={idx === count - 1}
|
||||
isImagining={isImagining}
|
||||
isSpeaking={isSpeaking}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationRestartFrom={handleConversationRestartFrom}
|
||||
onConversationTruncate={handleConversationTruncate}
|
||||
onMessageAssistantFrom={handleMessageAssistantFrom}
|
||||
onMessageBeam={handleMessageBeam}
|
||||
onMessageBranch={handleMessageBranch}
|
||||
onMessageDelete={handleMessageDelete}
|
||||
onMessageEdit={handleMessageEdit}
|
||||
onMessageToggleUserFlag={handleMessageToggleUserFlag}
|
||||
onMessageTruncate={handleMessageTruncate}
|
||||
// onReplyTo={handleReplyTo}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImagine}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
onTextImagine={capabilityHasT2I ? handleTextImagine : undefined}
|
||||
onTextSpeak={isSpeakable ? handleTextSpeak : undefined}
|
||||
/>
|
||||
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Box, Grid, IconButton, Sheet, styled, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { DConversationId } from '~/common/state/store-chats';
|
||||
import { DEphemeral } from '~/common/chats/EphemeralsStore';
|
||||
import { lineHeightChatTextMd } from '~/common/app.theme';
|
||||
@@ -78,7 +78,7 @@ function StateRenderer(props: { state: object }) {
|
||||
function EphemeralItem({ conversationId, ephemeral }: { conversationId: string, ephemeral: DEphemeral }) {
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
ConversationManager.getHandler(conversationId).ephemeralsStore.delete(ephemeral.id);
|
||||
ConversationsManager.getHandler(conversationId).ephemeralsStore.delete(ephemeral.id);
|
||||
}, [conversationId, ephemeral.id]);
|
||||
|
||||
return <Box
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Alert, Box, Sheet, Typography } from '@mui/joy';
|
||||
|
||||
import { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
import { useBeam } from '~/common/chats/BeamStore';
|
||||
import { useLLMSelect } from '~/common/components/forms/useLLMSelect';
|
||||
|
||||
|
||||
export function Beam(props: {
|
||||
conversationHandler: ConversationHandler | null,
|
||||
isMobile: boolean,
|
||||
sx?: SxProps
|
||||
}) {
|
||||
|
||||
// state
|
||||
const { config, candidates } = useBeam(props.conversationHandler);
|
||||
|
||||
// external state
|
||||
const [allChatLlm, allChatLlmComponent] = useLLMSelect(true, 'Beam LLM');
|
||||
|
||||
if (!config)
|
||||
return null;
|
||||
|
||||
const lastMessage = config.history.slice(-1)[0] ?? null;
|
||||
|
||||
return (
|
||||
<Box sx={{ ...props.sx, px: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
|
||||
{/* Issues */}
|
||||
{!!config.configError && (
|
||||
<Alert>
|
||||
{config.configError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Models, [x] all same, */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 2 }}>
|
||||
<Box sx={{ minWidth: 200 }}>
|
||||
{allChatLlmComponent}
|
||||
</Box>
|
||||
|
||||
{!!lastMessage && (
|
||||
<Box sx={{
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: 'xs',
|
||||
borderRadius: 'lg',
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
py: 1,
|
||||
px: 1,
|
||||
mb: 'auto',
|
||||
|
||||
|
||||
flex: 1,
|
||||
}}>
|
||||
{lastMessage.text}
|
||||
</Box>
|
||||
// <ChatMessageMemo
|
||||
// message={lastMessage}
|
||||
// fitScreen={props.isMobile}
|
||||
// sx={{
|
||||
// borderRadius: 'lg',
|
||||
// borderBottomRightRadius: lastMessage.role === 'assistant' ? undefined : 0,
|
||||
// borderBottomLeftRadius: lastMessage.role === 'user' ? undefined : 0,
|
||||
// boxShadow: 'xs',
|
||||
// my: 2,
|
||||
// px: 0,
|
||||
// py: 1,
|
||||
// alignSelf: 'self-end',
|
||||
// flex: 1,
|
||||
// maxHeight: '5rem',
|
||||
// overflow: 'hidden',
|
||||
// }}
|
||||
// />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Grid */}
|
||||
<Box sx={{
|
||||
// my: 'auto',
|
||||
// display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
border: '1px solid purple',
|
||||
minHeight: '300px',
|
||||
|
||||
// layout
|
||||
display: 'grid',
|
||||
gridTemplateColumns: props.isMobile ? 'repeat(auto-fit, minmax(320px, 1fr))' : 'repeat(auto-fit, minmax(400px, 1fr))',
|
||||
gap: { xs: 2, md: 2 },
|
||||
}}>
|
||||
<Sheet sx={{ minHeight: '50%' }}>
|
||||
b
|
||||
</Sheet>
|
||||
<Sheet>
|
||||
a
|
||||
</Sheet>
|
||||
<Sheet>
|
||||
a
|
||||
</Sheet>
|
||||
<Sheet>
|
||||
a
|
||||
</Sheet>
|
||||
</Box>
|
||||
|
||||
{/* Auto-Gatherer: All-in-one, Best-Of */}
|
||||
<Box>
|
||||
Gatherer
|
||||
</Box>
|
||||
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography level='body-sm' sx={{ whiteSpace: 'break-spaces' }}>
|
||||
{/*{JSON.stringify(config, null, 2)}*/}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
height: '100%',
|
||||
borderRadius: 'lg',
|
||||
borderBottomLeftRadius: 0,
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: 'lg',
|
||||
m: 2,
|
||||
p: '0.25rem 1rem',
|
||||
}}>
|
||||
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
a
|
||||
</Box>
|
||||
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export function CameraCaptureModal(props: {
|
||||
}}>
|
||||
|
||||
{/* Top bar */}
|
||||
<Sheet variant='solid' invertedColors sx={{ zIndex: 10, display: 'flex', justifyContent: 'space-between', p: 1 }}>
|
||||
<Sheet variant='solid' invertedColors sx={{ display: 'flex', justifyContent: 'space-between', p: 1 }}>
|
||||
<Select
|
||||
variant='solid' color='neutral'
|
||||
value={cameraIdx} onChange={(_event: any, value: number | null) => setCameraIdx(value === null ? -1 : value)}
|
||||
@@ -116,7 +116,7 @@ export function CameraCaptureModal(props: {
|
||||
|
||||
{showInfo && !!info && <Typography
|
||||
sx={{
|
||||
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 1,
|
||||
position: 'absolute', inset: 0, zIndex: 1, /* camera info on top of video */
|
||||
background: 'rgba(0,0,0,0.5)', color: 'white',
|
||||
whiteSpace: 'pre', overflowY: 'scroll',
|
||||
}}>
|
||||
@@ -127,7 +127,7 @@ export function CameraCaptureModal(props: {
|
||||
</Box>
|
||||
|
||||
{/* Bottom controls (zoom, ocr, download) & progress */}
|
||||
<Sheet variant='soft' sx={{ display: 'flex', flexDirection: 'column', zIndex: 20, gap: 1, p: 1 }}>
|
||||
<Sheet variant='soft' sx={{ display: 'flex', flexDirection: 'column', gap: 1, p: 1 }}>
|
||||
|
||||
{!!error && <InlineError error={error} />}
|
||||
|
||||
@@ -137,7 +137,7 @@ export function CameraCaptureModal(props: {
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'space-between' }}>
|
||||
{/* Info */}
|
||||
<IconButton size='lg' disabled={!info} variant='soft' onClick={() => setShowInfo(info => !info)} sx={{ zIndex: 30 }}>
|
||||
<IconButton size='lg' disabled={!info} variant='soft' onClick={() => setShowInfo(info => !info)}>
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
{/*<Button disabled={ocrProgress !== null} fullWidth variant='solid' size='lg' onClick={handleVideoOCRClicked} sx={{ flex: 1, maxWidth: 260 }}>*/}
|
||||
|
||||
@@ -3,17 +3,18 @@ import * as React from 'react';
|
||||
import { Box, MenuItem, Radio, Typography } from '@mui/joy';
|
||||
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { KeyStroke, platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { ChatModeId } from '../../AppChat';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
|
||||
interface ChatModeDescription {
|
||||
label: string;
|
||||
description: string | React.JSX.Element;
|
||||
highlight?: boolean;
|
||||
shortcut?: string;
|
||||
hideOnDesktop?: boolean;
|
||||
requiresTTI?: boolean;
|
||||
}
|
||||
|
||||
@@ -22,9 +23,15 @@ const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
|
||||
label: 'Chat',
|
||||
description: 'Persona replies',
|
||||
},
|
||||
'generate-text-beam': {
|
||||
label: 'Beam', // Best of, Auto-Prime, Top Pick, Select Best
|
||||
description: 'Combine multiple models', // Smarter: combine...
|
||||
shortcut: 'Ctrl + Enter',
|
||||
hideOnDesktop: true,
|
||||
},
|
||||
'append-user': {
|
||||
label: 'Write',
|
||||
description: 'Appends a message',
|
||||
description: 'Append a message',
|
||||
shortcut: 'Alt + Enter',
|
||||
},
|
||||
'generate-image': {
|
||||
@@ -32,13 +39,9 @@ const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
|
||||
description: 'AI Image Generation',
|
||||
requiresTTI: true,
|
||||
},
|
||||
'generate-text-beam': {
|
||||
label: 'Best-Of', // Best of, Auto-Prime, Top Pick, Select Best
|
||||
description: 'Smarter: best of multiple replies',
|
||||
},
|
||||
'generate-react': {
|
||||
label: 'Reason + Act', // · α
|
||||
description: 'Answers questions in multiple steps',
|
||||
description: 'Answer questions in multiple steps',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -50,13 +53,15 @@ function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) {
|
||||
}
|
||||
|
||||
export function ChatModeMenu(props: {
|
||||
anchorEl: HTMLAnchorElement | null, onClose: () => void,
|
||||
chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void
|
||||
isMobile: boolean,
|
||||
anchorEl: HTMLAnchorElement | null,
|
||||
onClose: () => void,
|
||||
chatModeId: ChatModeId,
|
||||
onSetChatModeId: (chatMode: ChatModeId) => void,
|
||||
capabilityHasTTI: boolean,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const labsChatBeam = useUXLabsStore(state => state.labsChatBeam);
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
|
||||
return (
|
||||
@@ -74,17 +79,17 @@ export function ChatModeMenu(props: {
|
||||
|
||||
{/* ChatMode items */}
|
||||
{Object.entries(ChatModeItems)
|
||||
.filter(([key, data]) => key !== 'generate-text-beam' || labsChatBeam)
|
||||
.filter(([_key, data]) => !data.hideOnDesktop || props.isMobile)
|
||||
.map(([key, data]) =>
|
||||
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
|
||||
<Radio checked={key === props.chatModeId} />
|
||||
<Radio color={data.highlight ? 'success' : undefined} checked={key === props.chatModeId} />
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography>{data.label}</Typography>
|
||||
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.capabilityHasTTI) ? 'Unconfigured' : ''}</Typography>
|
||||
</Box>
|
||||
{(key === props.chatModeId || !!data.shortcut) && (
|
||||
<KeyStroke combo={fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline)} />
|
||||
<KeyStroke combo={platformAwareKeystrokes(fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline))} />
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { fileOpen, FileWithHandle } from 'browser-fs-access';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
|
||||
@@ -10,7 +9,7 @@ import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import AutoModeIcon from '@mui/icons-material/AutoMode';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
|
||||
@@ -24,27 +23,34 @@ import type { LLMOptionsOpenAI } from '~/modules/llms/vendors/openai/openai.vend
|
||||
import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
import { conversationTitle, DConversationId, DMessageMetadata, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { countModelTokens } from '~/common/util/token-counter';
|
||||
import { isMacUser } from '~/common/util/pwaUtils';
|
||||
import { launchAppCall } from '~/common/app.routes';
|
||||
import { lineHeightTextareaMd } from '~/common/app.theme';
|
||||
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
import { playSoundUrl } from '~/common/util/audioUtils';
|
||||
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
|
||||
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
import { useChatOverlayStore } from '~/common/chats/store-chat-overlay-vanilla';
|
||||
import { useDebouncer } from '~/common/components/useDebouncer';
|
||||
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { useUICounter, useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ActileItem, ActileProvider } from './actile/ActileProvider';
|
||||
import type { ActileItem } from './actile/ActileProvider';
|
||||
import { providerCommands } from './actile/providerCommands';
|
||||
import { providerStarredMessage, StarredMessageItem } from './actile/providerStarredMessage';
|
||||
import { useActileManager } from './actile/useActileManager';
|
||||
|
||||
import type { AttachmentId } from './attachments/store-attachments';
|
||||
import { Attachments } from './attachments/Attachments';
|
||||
import { getTextBlockText, useLLMAttachments } from './attachments/useLLMAttachments';
|
||||
import { getSingleTextBlockText, useLLMAttachments } from './attachments/useLLMAttachments';
|
||||
import { useAttachments } from './attachments/useAttachments';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './composer.types';
|
||||
@@ -52,27 +58,21 @@ import { ButtonAttachCameraMemo, useCameraCaptureModal } from './buttons/ButtonA
|
||||
import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard';
|
||||
import { ButtonAttachFileMemo } from './buttons/ButtonAttachFile';
|
||||
import { ButtonAttachScreenCaptureMemo } from './buttons/ButtonAttachScreenCapture';
|
||||
import { ButtonBeamMemo } from './buttons/ButtonBeam';
|
||||
import { ButtonCallMemo } from './buttons/ButtonCall';
|
||||
import { ButtonMicContinuationMemo } from './buttons/ButtonMicContinuation';
|
||||
import { ButtonMicMemo } from './buttons/ButtonMic';
|
||||
import { ButtonMultiChatMemo } from './buttons/ButtonMultiChat';
|
||||
import { ButtonOptionsDraw } from './buttons/ButtonOptionsDraw';
|
||||
import { ChatModeMenu } from './ChatModeMenu';
|
||||
import { ReplyToBubble } from '../message/ReplyToBubble';
|
||||
import { TokenBadgeMemo } from './TokenBadge';
|
||||
import { TokenProgressbarMemo } from './TokenProgressbar';
|
||||
import { useComposerStartupText } from './store-composer';
|
||||
|
||||
|
||||
export const animationStopEnter = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px)
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
`;
|
||||
const zIndexComposerOverlayDrop = 10;
|
||||
const zIndexComposerOverlayMic = 20;
|
||||
|
||||
const dropperCardSx: SxProps = {
|
||||
display: 'none',
|
||||
@@ -81,7 +81,7 @@ const dropperCardSx: SxProps = {
|
||||
border: '2px dashed',
|
||||
borderRadius: 'xs',
|
||||
boxShadow: 'none',
|
||||
zIndex: 10,
|
||||
zIndex: zIndexComposerOverlayDrop,
|
||||
} as const;
|
||||
|
||||
const dropppedCardDraggingSx: SxProps = {
|
||||
@@ -101,13 +101,14 @@ export function Composer(props: {
|
||||
capabilityHasT2I: boolean;
|
||||
isMulticast: boolean | null;
|
||||
isDeveloperMode: boolean;
|
||||
onAction: (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart) => boolean;
|
||||
onAction: (conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: ComposerOutputMultiPart, metadata?: DMessageMetadata) => boolean;
|
||||
onTextImagine: (conversationId: DConversationId, text: string) => void;
|
||||
setIsMulticast: (on: boolean) => void;
|
||||
sx?: SxProps;
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
|
||||
const [composeText, debouncedText, setComposeText] = useDebouncer('', 300, 1200, true);
|
||||
const [micContinuation, setMicContinuation] = React.useState(false);
|
||||
const [speechInterimResult, setSpeechInterimResult] = React.useState<SpeechResult | null>(null);
|
||||
@@ -116,16 +117,19 @@ export function Composer(props: {
|
||||
|
||||
// external state
|
||||
const { openPreferencesTab /*, setIsFocusedMode*/ } = useOptimaLayout();
|
||||
const { labsAttachScreenCapture, labsCameraDesktop } = useUXLabsStore(state => ({
|
||||
const { labsAttachScreenCapture, labsCameraDesktop, labsShowCost } = useUXLabsStore(useShallow(state => ({
|
||||
labsAttachScreenCapture: state.labsAttachScreenCapture,
|
||||
labsCameraDesktop: state.labsCameraDesktop,
|
||||
}), shallow);
|
||||
labsShowCost: state.labsShowCost,
|
||||
})));
|
||||
const timeToShowTips = useAppStateStore(state => state.usageCount > 2);
|
||||
const { novel: explainShiftEnter, touch: touchShiftEnter } = useUICounter('composer-shift-enter');
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
|
||||
const { novel: explainAltEnter, touch: touchAltEnter } = useUICounter('composer-alt-enter');
|
||||
const { novel: explainCtrlEnter, touch: touchCtrlEnter } = useUICounter('composer-ctrl-enter');
|
||||
const [startupText, setStartupText] = useComposerStartupText();
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
const chatMicTimeoutMs = useChatMicTimeoutMsValue();
|
||||
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(state => {
|
||||
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(useShallow(state => {
|
||||
const conversation = state.conversations.find(_c => _c.id === props.conversationId);
|
||||
return {
|
||||
assistantAbortible: conversation ? !!conversation.abortController : false,
|
||||
@@ -133,11 +137,18 @@ export function Composer(props: {
|
||||
tokenCount: conversation ? conversation.tokenCount : 0,
|
||||
stopTyping: state.stopTyping,
|
||||
};
|
||||
}, shallow);
|
||||
}));
|
||||
const { inComposer: browsingInComposer } = useBrowseCapability();
|
||||
const { attachAppendClipboardItems, attachAppendDataTransfer, attachAppendFile, attachments: _attachments, clearAttachments, removeAttachment } =
|
||||
const { attachAppendClipboardItems, attachAppendDataTransfer, attachAppendEgoMessage, attachAppendFile, attachments: _attachments, clearAttachments, removeAttachment } =
|
||||
useAttachments(browsingInComposer && !composeText.startsWith('/'));
|
||||
|
||||
// external overlay state (extra conversationId-dependent state)
|
||||
const conversationHandler = props.conversationId ? ConversationsManager.getHandler(props.conversationId) : null;
|
||||
const conversationOverlayStore = conversationHandler?.getOverlayStore() ?? null;
|
||||
const { replyToGenerateText } = useChatOverlayStore(conversationOverlayStore, useShallow(store => ({
|
||||
replyToGenerateText: chatModeId === 'generate-text' ? store.replyToText?.trim() || null : null,
|
||||
})));
|
||||
|
||||
|
||||
// derived state
|
||||
|
||||
@@ -162,6 +173,8 @@ export function Composer(props: {
|
||||
const tokensHistory = _historyTokenCount;
|
||||
const tokensReponseMax = (props.chatLLM?.options as LLMOptionsOpenAI /* FIXME: BIG ASSUMPTION */)?.llmResponseTokens || 0;
|
||||
const tokenLimit = props.chatLLM?.contextTokens || 0;
|
||||
const tokenPriceIn = props.chatLLM?.pricing?.chatIn;
|
||||
const tokenPriceOut = props.chatLLM?.pricing?.chatOut;
|
||||
|
||||
|
||||
// Effect: load initial text if queued up (e.g. by /link/share_targe)
|
||||
@@ -173,6 +186,18 @@ export function Composer(props: {
|
||||
}, [setComposeText, setStartupText, startupText]);
|
||||
|
||||
|
||||
// Overlay actions
|
||||
|
||||
const handleReplyToCleared = React.useCallback(() => {
|
||||
conversationOverlayStore?.getState().setReplyToText(null);
|
||||
}, [conversationOverlayStore]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (replyToGenerateText)
|
||||
setTimeout(() => props.composerTextAreaRef.current?.focus(), 1 /* prevent focus theft */);
|
||||
}, [replyToGenerateText, props.composerTextAreaRef]);
|
||||
|
||||
|
||||
// Primary button
|
||||
|
||||
const { conversationId, onAction } = props;
|
||||
@@ -181,25 +206,33 @@ export function Composer(props: {
|
||||
if (!conversationId)
|
||||
return false;
|
||||
|
||||
// get attachments
|
||||
const multiPartMessage = llmAttachments.getAttachmentsOutputs(composerText || null);
|
||||
// get the multipart output including all attachments
|
||||
const multiPartMessage = llmAttachments.collapseWithAttachments(composerText || null);
|
||||
if (!multiPartMessage.length)
|
||||
return false;
|
||||
|
||||
// metadata
|
||||
const metadata = replyToGenerateText ? { inReplyToText: replyToGenerateText } : undefined;
|
||||
|
||||
// send the message
|
||||
const enqueued = onAction(_chatModeId, conversationId, multiPartMessage);
|
||||
const enqueued = onAction(conversationId, _chatModeId, multiPartMessage, metadata);
|
||||
if (enqueued) {
|
||||
clearAttachments();
|
||||
handleReplyToCleared();
|
||||
setComposeText('');
|
||||
}
|
||||
|
||||
return enqueued;
|
||||
}, [clearAttachments, conversationId, llmAttachments, onAction, setComposeText]);
|
||||
}, [clearAttachments, conversationId, handleReplyToCleared, llmAttachments, onAction, replyToGenerateText, setComposeText]);
|
||||
|
||||
const handleSendClicked = React.useCallback(() => {
|
||||
handleSendAction(chatModeId, composeText);
|
||||
}, [chatModeId, composeText, handleSendAction]);
|
||||
|
||||
const handleSendTextBeamClicked = React.useCallback(() => {
|
||||
handleSendAction('generate-text-beam', composeText);
|
||||
}, [composeText, handleSendAction]);
|
||||
|
||||
const handleStopClicked = React.useCallback(() => {
|
||||
!!props.conversationId && stopTyping(props.conversationId);
|
||||
}, [props.conversationId, stopTyping]);
|
||||
@@ -241,7 +274,7 @@ export function Composer(props: {
|
||||
|
||||
// Actiles
|
||||
|
||||
const onActileCommandSelect = React.useCallback((item: ActileItem) => {
|
||||
const onActileCommandPaste = React.useCallback((item: ActileItem) => {
|
||||
if (props.composerTextAreaRef.current) {
|
||||
const textArea = props.composerTextAreaRef.current;
|
||||
const currentText = textArea.value;
|
||||
@@ -262,9 +295,22 @@ export function Composer(props: {
|
||||
}
|
||||
}, [props.composerTextAreaRef, setComposeText]);
|
||||
|
||||
const actileProviders: ActileProvider[] = React.useMemo(() => {
|
||||
return [providerCommands(onActileCommandSelect)];
|
||||
}, [onActileCommandSelect]);
|
||||
const onActileMessageAttach = React.useCallback((item: StarredMessageItem) => {
|
||||
// get the message
|
||||
const conversation = getConversation(item.conversationId);
|
||||
const messageToAttach = conversation?.messages.find(m => m.id === item.messageId);
|
||||
if (conversation && messageToAttach && messageToAttach.text) {
|
||||
// Testing with this serialization for LLM. Note it will still be within a multi-part message,
|
||||
// this could be in a titled markdown block. Don't know yet how this fares with different LLMs.
|
||||
const chatTitle = conversationTitle(conversation);
|
||||
const textPlain = `---\nitem id: ${messageToAttach.id}\ncontext title: ${chatTitle}\n---\n${messageToAttach.text.trim()}\n`;
|
||||
void attachAppendEgoMessage('context-item', textPlain, `${chatTitle} > ${messageToAttach.text.slice(0, 10)}...`);
|
||||
}
|
||||
}, [attachAppendEgoMessage]);
|
||||
|
||||
const actileProviders = React.useMemo(() => {
|
||||
return [providerCommands(onActileCommandPaste), providerStarredMessage(onActileMessageAttach)];
|
||||
}, [onActileCommandPaste, onActileMessageAttach]);
|
||||
|
||||
const { actileComponent, actileInterceptKeydown, actileInterceptTextChange } = useActileManager(actileProviders, props.composerTextAreaRef);
|
||||
|
||||
@@ -284,9 +330,17 @@ export function Composer(props: {
|
||||
// Enter: primary action
|
||||
if (e.key === 'Enter') {
|
||||
|
||||
// Alt: append the message instead
|
||||
// Alt (Windows) or Option (Mac) + Enter: append the message instead of sending it
|
||||
if (e.altKey) {
|
||||
handleSendAction('append-user', composeText);
|
||||
if (handleSendAction('append-user', composeText))
|
||||
touchAltEnter();
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
// Ctrl (Windows) or Command (Mac) + Enter: send for beaming
|
||||
if ((isMacUser && e.metaKey && !e.ctrlKey) || (!isMacUser && e.ctrlKey && !e.metaKey)) {
|
||||
if (handleSendAction('generate-text-beam', composeText))
|
||||
touchCtrlEnter();
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -300,7 +354,7 @@ export function Composer(props: {
|
||||
}
|
||||
}
|
||||
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, touchShiftEnter]);
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, touchAltEnter, touchCtrlEnter, touchShiftEnter]);
|
||||
|
||||
|
||||
// Focus mode
|
||||
@@ -401,8 +455,8 @@ export function Composer(props: {
|
||||
|
||||
const handleAttachmentInlineText = React.useCallback((attachmentId: AttachmentId) => {
|
||||
setComposeText(currentText => {
|
||||
const attachmentOutputs = llmAttachments.getAttachmentOutputs(currentText, attachmentId);
|
||||
const inlinedText = getTextBlockText(attachmentOutputs) || '';
|
||||
const inlinedMultiPart = llmAttachments.collapseWithAttachment(currentText, attachmentId);
|
||||
const inlinedText = getSingleTextBlockText(inlinedMultiPart) || '';
|
||||
removeAttachment(attachmentId);
|
||||
return inlinedText;
|
||||
});
|
||||
@@ -410,8 +464,8 @@ export function Composer(props: {
|
||||
|
||||
const handleAttachmentsInlineText = React.useCallback(() => {
|
||||
setComposeText(currentText => {
|
||||
const attachmentsOutputs = llmAttachments.getAttachmentsOutputs(currentText);
|
||||
const inlinedText = getTextBlockText(attachmentsOutputs) || '';
|
||||
const inlinedMultiPart = llmAttachments.collapseWithAttachments(currentText);
|
||||
const inlinedText = getSingleTextBlockText(inlinedMultiPart) || '';
|
||||
clearAttachments();
|
||||
return inlinedText;
|
||||
});
|
||||
@@ -469,19 +523,22 @@ export function Composer(props: {
|
||||
const isReAct = chatModeId === 'generate-react';
|
||||
const isDraw = chatModeId === 'generate-image';
|
||||
|
||||
const showCall = isText || isAppend;
|
||||
const showChatReplyTo = !!replyToGenerateText;
|
||||
const showChatExtras = isText && !showChatReplyTo;
|
||||
|
||||
const buttonVariant: VariantProp = (isAppend || (isMobile && isTextBeam)) ? 'outlined' : 'solid';
|
||||
|
||||
const buttonColor: ColorPaletteProp =
|
||||
assistantAbortible ? 'warning'
|
||||
: isReAct ? 'success'
|
||||
: isTextBeam ? 'success'
|
||||
: isTextBeam ? 'primary'
|
||||
: isDraw ? 'warning'
|
||||
: 'primary';
|
||||
|
||||
const buttonText =
|
||||
isAppend ? 'Write'
|
||||
: isReAct ? 'ReAct'
|
||||
: isTextBeam ? 'Best-Of'
|
||||
: isTextBeam ? 'Beam'
|
||||
: isDraw ? 'Draw'
|
||||
: 'Chat';
|
||||
|
||||
@@ -490,18 +547,25 @@ export function Composer(props: {
|
||||
: isAppend ? <SendIcon sx={{ fontSize: 18 }} />
|
||||
: isReAct ? <PsychologyIcon />
|
||||
: isTextBeam ? <ChatBeamIcon /> /* <GavelIcon /> */
|
||||
: isDraw ? <FormatPaintIcon />
|
||||
: isDraw ? <FormatPaintTwoToneIcon />
|
||||
: <TelegramIcon />;
|
||||
|
||||
let textPlaceholder: string =
|
||||
isDraw ? 'Describe an idea or a drawing...'
|
||||
: isReAct ? 'Multi-step reasoning question...'
|
||||
: isTextBeam ? 'Multi-chat with this persona...'
|
||||
: props.isDeveloperMode ? 'Chat with me' + (isDesktop ? ' · drop source' : '') + ' · attach code...'
|
||||
: props.capabilityHasT2I ? 'Chat · /react · /draw · drop files...'
|
||||
: 'Chat · /react · drop files...';
|
||||
if (isDesktop && explainShiftEnter)
|
||||
textPlaceholder += !enterIsNewline ? '\nShift+Enter to add a new line' : '\nShift+Enter to send';
|
||||
: isTextBeam ? 'Beam: combine the smarts of models...'
|
||||
: showChatReplyTo ? 'Chat about this'
|
||||
: props.isDeveloperMode ? 'Chat with me' + (isDesktop ? ' · drop source' : '') + ' · attach code...'
|
||||
: props.capabilityHasT2I ? 'Chat · /beam · /draw · drop files...'
|
||||
: 'Chat · /react · drop files...';
|
||||
if (isDesktop && timeToShowTips) {
|
||||
if (explainShiftEnter)
|
||||
textPlaceholder += !enterIsNewline ? '\n\n💡 Shift + Enter to add a new line' : '\n\n💡 Shift + Enter to send';
|
||||
else if (explainAltEnter)
|
||||
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Alt + Enter to just append the message');
|
||||
else if (explainCtrlEnter)
|
||||
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Ctrl + Enter to beam');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box aria-label='User Message' component='section' sx={props.sx}>
|
||||
@@ -584,7 +648,7 @@ export function Composer(props: {
|
||||
variant='outlined'
|
||||
color={isDraw ? 'warning' : isReAct ? 'success' : undefined}
|
||||
autoFocus
|
||||
minRows={isMobile ? 4 : 5}
|
||||
minRows={isMobile ? 4 : showChatReplyTo ? 4 : 5}
|
||||
maxRows={isMobile ? 8 : 10}
|
||||
placeholder={textPlaceholder}
|
||||
value={composeText}
|
||||
@@ -595,6 +659,7 @@ export function Composer(props: {
|
||||
onPasteCapture={handleAttachCtrlV}
|
||||
// onFocusCapture={handleFocusModeOn}
|
||||
// onBlurCapture={handleFocusModeOff}
|
||||
endDecorator={showChatReplyTo && <ReplyToBubble replyToText={replyToGenerateText} onClear={handleReplyToCleared} className='reply-to-bubble' />}
|
||||
slotProps={{
|
||||
textarea: {
|
||||
enterKeyHint: enterIsNewline ? 'enter' : 'send',
|
||||
@@ -607,16 +672,16 @@ export function Composer(props: {
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: 'background.level1',
|
||||
'&:focus-within': { backgroundColor: 'background.popup' },
|
||||
'&:focus-within': { backgroundColor: 'background.popup', '.reply-to-bubble': { backgroundColor: 'background.popup' } },
|
||||
lineHeight: lineHeightTextareaMd,
|
||||
}} />
|
||||
|
||||
{tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
|
||||
<TokenProgressbarMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} />
|
||||
{!showChatReplyTo && tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
|
||||
<TokenProgressbarMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} tokenPriceIn={tokenPriceIn} tokenPriceOut={tokenPriceOut} />
|
||||
)}
|
||||
|
||||
{!!tokenLimit && (
|
||||
<TokenBadgeMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} showExcess absoluteBottomRight />
|
||||
{!showChatReplyTo && tokenLimit > 0 && (
|
||||
<TokenBadgeMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} tokenPriceIn={tokenPriceIn} tokenPriceOut={tokenPriceOut} showCost={labsShowCost} showExcess absoluteBottomRight />
|
||||
)}
|
||||
|
||||
</Box>
|
||||
@@ -625,7 +690,7 @@ export function Composer(props: {
|
||||
{isSpeechEnabled && (
|
||||
<Box sx={{
|
||||
position: 'absolute', top: 0, right: 0,
|
||||
zIndex: 21,
|
||||
zIndex: zIndexComposerOverlayMic + 1,
|
||||
mt: isDesktop ? 1 : 0.25,
|
||||
mr: isDesktop ? 1 : 0.25,
|
||||
display: 'flex', flexDirection: 'column', gap: isDesktop ? 1 : 0.25,
|
||||
@@ -644,20 +709,32 @@ export function Composer(props: {
|
||||
{/* overlay: Mic */}
|
||||
{micIsRunning && (
|
||||
<Card
|
||||
color='primary' variant='soft' invertedColors
|
||||
color='primary' variant='soft'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0, top: 0,
|
||||
// alignItems: 'center', justifyContent: 'center',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.solidBg',
|
||||
borderRadius: 'sm',
|
||||
zIndex: 20,
|
||||
px: 1.5, py: 1,
|
||||
zIndex: zIndexComposerOverlayMic,
|
||||
pl: 1.5,
|
||||
pr: { xs: 1.5, md: 5 },
|
||||
py: 0.625,
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
<Typography sx={{
|
||||
color: 'primary.softColor',
|
||||
lineHeight: lineHeightTextareaMd,
|
||||
'& .interim': {
|
||||
textDecoration: 'underline',
|
||||
textDecorationThickness: '0.25em',
|
||||
textDecorationColor: 'rgba(var(--joy-palette-primary-mainChannel) / 0.1)',
|
||||
textDecorationSkipInk: 'none',
|
||||
textUnderlineOffset: '0.25em',
|
||||
},
|
||||
}}>
|
||||
<Typography>
|
||||
{speechInterimResult.transcript}{' '}
|
||||
<span style={{ opacity: 0.8 }}>{speechInterimResult.interimTranscript}</span>
|
||||
<span className={speechInterimResult.interimTranscript !== 'Listening...' ? 'interim' : undefined}>{speechInterimResult.interimTranscript}</span>
|
||||
</Typography>
|
||||
</Card>
|
||||
)}
|
||||
@@ -696,11 +773,12 @@ export function Composer(props: {
|
||||
<Grid xs={12} md={3}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, height: '100%' } as const}>
|
||||
|
||||
{/* This row is here only for the [mobile] bottom-start corner item */}
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
{/* [mobile] This row is here only for the [mobile] bottom-start corner item */}
|
||||
{/* [desktop] This column arrangement will have the [desktop] beam button right under call */}
|
||||
<Box sx={isMobile ? { display: 'flex' } : { display: 'grid', gap: 1 }}>
|
||||
|
||||
{/* [mobile] bottom-corner secondary button */}
|
||||
{isMobile && (showCall
|
||||
{isMobile && (showChatExtras
|
||||
? <ButtonCallMemo isMobile disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />
|
||||
: isDraw
|
||||
? <ButtonOptionsDraw isMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />
|
||||
@@ -709,11 +787,12 @@ export function Composer(props: {
|
||||
|
||||
{/* Responsive Send/Stop buttons */}
|
||||
<ButtonGroup
|
||||
variant={isAppend ? 'outlined' : 'solid'}
|
||||
variant={buttonVariant}
|
||||
color={buttonColor}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
boxShadow: isMobile ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${buttonColor}-mainChannel) / 20%)`,
|
||||
backgroundColor: (isMobile && buttonVariant === 'outlined') ? 'background.popup' : undefined,
|
||||
boxShadow: (isMobile && buttonVariant !== 'outlined') ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${buttonColor}-mainChannel) / 20%)`,
|
||||
}}
|
||||
>
|
||||
{!assistantAbortible ? (
|
||||
@@ -732,12 +811,19 @@ export function Composer(props: {
|
||||
fullWidth variant='soft' disabled={!props.conversationId}
|
||||
onClick={handleStopClicked}
|
||||
endDecorator={<StopOutlinedIcon sx={{ fontSize: 18 }} />}
|
||||
sx={{ animation: `${animationStopEnter} 0.1s ease-out` }}
|
||||
sx={{ animation: `${animationEnterBelow} 0.1s ease-out` }}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* [Beam] Open Beam */}
|
||||
{/*{isText && <Tooltip title='Open Beam'>*/}
|
||||
{/* <IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleSendTextBeamClicked}>*/}
|
||||
{/* <ChatBeamIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/*</Tooltip>}*/}
|
||||
|
||||
{/* [Draw] Imagine */}
|
||||
{isDraw && !!composeText && <Tooltip title='Imagine a drawing prompt'>
|
||||
<IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleTextImagineClicked}>
|
||||
@@ -755,6 +841,15 @@ export function Composer(props: {
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
|
||||
{/* [desktop] secondary-top buttons */}
|
||||
{isDesktop && showChatExtras && !assistantAbortible && (
|
||||
<ButtonBeamMemo
|
||||
disabled={!props.conversationId || !chatLLMId || !llmAttachments.isOutputAttacheable}
|
||||
hasContent={!!composeText}
|
||||
onClick={handleSendTextBeamClicked}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* [desktop] Multicast switch (under the Chat button) */}
|
||||
@@ -764,7 +859,7 @@ export function Composer(props: {
|
||||
{isDesktop && <Box sx={{ mt: 'auto', display: 'grid', gap: 1 }}>
|
||||
|
||||
{/* [desktop] Call secondary button */}
|
||||
{showCall && <ButtonCallMemo disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
|
||||
{showChatExtras && <ButtonCallMemo disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
|
||||
|
||||
{/* [desktop] Draw Options secondary button */}
|
||||
{isDraw && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
|
||||
@@ -779,6 +874,7 @@ export function Composer(props: {
|
||||
{/* Mode selector */}
|
||||
{!!chatModeMenuAnchor && (
|
||||
<ChatModeMenu
|
||||
isMobile={isMobile}
|
||||
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
|
||||
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
|
||||
capabilityHasTTI={props.capabilityHasT2I}
|
||||
|
||||
@@ -3,41 +3,81 @@ import * as React from 'react';
|
||||
import { Badge, Box, ColorPaletteProp, Tooltip } from '@mui/joy';
|
||||
|
||||
|
||||
function alignRight(value: number, columnSize: number = 7) {
|
||||
function alignRight(value: number, columnSize: number = 8) {
|
||||
const str = value.toLocaleString();
|
||||
return str.padStart(columnSize);
|
||||
}
|
||||
|
||||
function formatCost(cost: number) {
|
||||
return cost < 1
|
||||
? (cost * 100).toFixed(cost < 0.010 ? 2 : 1) + ' ¢'
|
||||
: '$ ' + cost.toFixed(2);
|
||||
}
|
||||
|
||||
export function tokensPrettyMath(tokenLimit: number | 0, directTokens: number, historyTokens?: number, responseMaxTokens?: number): {
|
||||
color: ColorPaletteProp, message: string, remainingTokens: number
|
||||
|
||||
export function tokensPrettyMath(tokenLimit: number | 0, directTokens: number, historyTokens?: number, responseMaxTokens?: number, tokenPriceIn?: number, tokenPriceOut?: number): {
|
||||
color: ColorPaletteProp,
|
||||
message: string,
|
||||
remainingTokens: number,
|
||||
costMax?: number,
|
||||
costMin?: number,
|
||||
} {
|
||||
const usedTokens = directTokens + (historyTokens || 0) + (responseMaxTokens || 0);
|
||||
const remainingTokens = tokenLimit - usedTokens;
|
||||
const usedInputTokens = directTokens + (historyTokens || 0);
|
||||
const usedMaxTokens = usedInputTokens + (responseMaxTokens || 0);
|
||||
const remainingTokens = tokenLimit - usedMaxTokens;
|
||||
const gteLimit = (remainingTokens <= 0 && tokenLimit > 0);
|
||||
|
||||
// message
|
||||
let message: string = gteLimit ? '⚠️ ' : '';
|
||||
|
||||
// costs
|
||||
let costMax: number | undefined = undefined;
|
||||
let costMin: number | undefined = undefined;
|
||||
|
||||
// no limit: show used tokens only
|
||||
if (!tokenLimit) {
|
||||
message += `Requested: ${usedTokens.toLocaleString()} tokens`;
|
||||
message += `Requested: ${usedMaxTokens.toLocaleString()} tokens`;
|
||||
}
|
||||
// has full information (d + i < l)
|
||||
else if (historyTokens || responseMaxTokens) {
|
||||
message +=
|
||||
`${Math.abs(remainingTokens).toLocaleString()} ${remainingTokens >= 0 ? 'available' : 'excess'} message tokens\n\n` +
|
||||
`▶ ${Math.abs(remainingTokens).toLocaleString()} ${remainingTokens >= 0 ? 'available' : 'excess'} message tokens\n\n` +
|
||||
` = Model max tokens: ${alignRight(tokenLimit)}\n` +
|
||||
` - This message: ${alignRight(directTokens)}\n` +
|
||||
` - History: ${alignRight(historyTokens || 0)}\n` +
|
||||
` - Max response: ${alignRight(responseMaxTokens || 0)}`;
|
||||
|
||||
// add the price, if available
|
||||
if (tokenPriceIn || tokenPriceOut) {
|
||||
costMin = tokenPriceIn ? usedInputTokens * tokenPriceIn / 1E6 : undefined;
|
||||
const costOutMax = (tokenPriceOut && responseMaxTokens) ? responseMaxTokens * tokenPriceOut / 1E6 : undefined;
|
||||
if (costMin || costOutMax) {
|
||||
message += `\n\n\n▶ Chat Turn Cost (max, approximate)\n`;
|
||||
|
||||
if (costMin) message += '\n' +
|
||||
` Input tokens: ${alignRight(usedInputTokens)}\n` +
|
||||
` Input Price $/M: ${tokenPriceIn!.toFixed(2).padStart(8)}\n` +
|
||||
` Input cost: ${('$' + costMin!.toFixed(4)).padStart(8)}\n`;
|
||||
|
||||
if (costOutMax) message += '\n' +
|
||||
` Max output tokens: ${alignRight(responseMaxTokens!)}\n` +
|
||||
` Output Price $/M: ${tokenPriceOut!.toFixed(2).padStart(8)}\n` +
|
||||
` Max output cost: ${('$' + costOutMax!.toFixed(4)).padStart(8)}\n`;
|
||||
|
||||
if (costMin) message += '\n' +
|
||||
` > Min turn cost: ${formatCost(costMin).padStart(8)}`;
|
||||
costMax = (costMin && costOutMax) ? costMin + costOutMax : undefined;
|
||||
if (costMax) message += '\n' +
|
||||
` < Max turn cost: ${formatCost(costMax).padStart(8)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cleaner mode: d + ? < R (total is the remaining in this case)
|
||||
else {
|
||||
message +=
|
||||
`${(tokenLimit + usedTokens).toLocaleString()} available tokens after deleting this\n\n` +
|
||||
`${(tokenLimit + usedMaxTokens).toLocaleString()} available tokens after deleting this\n\n` +
|
||||
` = Currently free: ${alignRight(tokenLimit)}\n` +
|
||||
` + This message: ${alignRight(usedTokens)}`;
|
||||
` + This message: ${alignRight(usedMaxTokens)}`;
|
||||
}
|
||||
|
||||
const color: ColorPaletteProp =
|
||||
@@ -47,23 +87,21 @@ export function tokensPrettyMath(tokenLimit: number | 0, directTokens: number, h
|
||||
? 'warning'
|
||||
: 'primary';
|
||||
|
||||
return { color, message, remainingTokens };
|
||||
return { color, message, remainingTokens, costMax, costMin };
|
||||
}
|
||||
|
||||
|
||||
export const TokenTooltip = (props: { message: string | null, color: ColorPaletteProp, placement?: 'top' | 'top-end', children: React.JSX.Element }) =>
|
||||
export const TokenTooltip = (props: { message: string | null, color: ColorPaletteProp, placement?: 'top' | 'top-end', children: React.ReactElement }) =>
|
||||
<Tooltip
|
||||
placement={props.placement}
|
||||
variant={props.color !== 'primary' ? 'solid' : 'soft'} color={props.color}
|
||||
title={props.message
|
||||
? <Box sx={{ p: 2, whiteSpace: 'pre' }}>
|
||||
{props.message}
|
||||
</Box>
|
||||
: null
|
||||
}
|
||||
title={props.message ? <Box sx={{ p: 2, whiteSpace: 'pre' }}>{props.message}</Box> : null}
|
||||
sx={{
|
||||
fontFamily: 'code',
|
||||
boxShadow: 'xl',
|
||||
// fontSize: '0.8125rem',
|
||||
border: '1px solid',
|
||||
borderColor: `${props.color}.outlinedColor`,
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
@@ -76,38 +114,65 @@ export const TokenTooltip = (props: { message: string | null, color: ColorPalett
|
||||
export const TokenBadgeMemo = React.memo(TokenBadge);
|
||||
|
||||
function TokenBadge(props: {
|
||||
direct: number, history?: number, responseMax?: number, limit: number,
|
||||
showExcess?: boolean, absoluteBottomRight?: boolean, inline?: boolean,
|
||||
direct: number,
|
||||
history?: number,
|
||||
responseMax?: number,
|
||||
limit: number,
|
||||
|
||||
tokenPriceIn?: number,
|
||||
tokenPriceOut?: number,
|
||||
|
||||
showCost?: boolean
|
||||
showExcess?: boolean,
|
||||
absoluteBottomRight?: boolean,
|
||||
inline?: boolean,
|
||||
}) {
|
||||
|
||||
const { message, color, remainingTokens } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax);
|
||||
const { message, color, remainingTokens, costMax, costMin } =
|
||||
tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax, props.tokenPriceIn, props.tokenPriceOut);
|
||||
|
||||
// show the direct tokens, unless we exceed the limit and 'showExcess' is enabled
|
||||
const value = (props.showExcess && (props.limit && remainingTokens <= 0))
|
||||
? Math.abs(remainingTokens)
|
||||
: props.direct;
|
||||
let badgeValue: string;
|
||||
|
||||
const showAltCosts = !!props.showCost && !!costMax && costMin !== undefined;
|
||||
if (showAltCosts) {
|
||||
badgeValue = '< ' + formatCost(costMax);
|
||||
} else {
|
||||
|
||||
// show the direct tokens, unless we exceed the limit and 'showExcess' is enabled
|
||||
const value = (props.showExcess && (props.limit && remainingTokens <= 0))
|
||||
? Math.abs(remainingTokens)
|
||||
: props.direct;
|
||||
|
||||
badgeValue = value.toLocaleString();
|
||||
}
|
||||
|
||||
const shallHide = !props.direct && remainingTokens >= 0 && !showAltCosts;
|
||||
if (shallHide) return null;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant='solid' color={color} max={100000}
|
||||
invisible={!props.direct && remainingTokens >= 0}
|
||||
badgeContent={
|
||||
<TokenTooltip color={color} message={message}>
|
||||
<span>{value.toLocaleString()}</span>
|
||||
</TokenTooltip>
|
||||
}
|
||||
sx={{
|
||||
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: 8 }),
|
||||
cursor: 'help',
|
||||
}}
|
||||
slotProps={{
|
||||
badge: {
|
||||
sx: {
|
||||
fontFamily: 'code',
|
||||
...((props.absoluteBottomRight || props.inline) && { position: 'static', transform: 'none' }),
|
||||
<TokenTooltip color={color} message={message} placement='top-end'>
|
||||
<Badge
|
||||
variant='soft' color={color} max={1000000}
|
||||
// invisible={shallHide}
|
||||
badgeContent={badgeValue}
|
||||
slotProps={{
|
||||
root: {
|
||||
sx: {
|
||||
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: 8 }),
|
||||
cursor: 'help',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
badge: {
|
||||
sx: {
|
||||
// the badge (not the tooltip)
|
||||
// boxShadow: 'sm',
|
||||
fontFamily: 'code',
|
||||
fontSize: 'xs',
|
||||
...((props.absoluteBottomRight || props.inline) && { position: 'static', transform: 'none' }),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TokenTooltip>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,15 @@ import { tokensPrettyMath, TokenTooltip } from './TokenBadge';
|
||||
*/
|
||||
export const TokenProgressbarMemo = React.memo(TokenProgressbar);
|
||||
|
||||
function TokenProgressbar(props: { direct: number, history: number, responseMax: number, limit: number }) {
|
||||
function TokenProgressbar(props: {
|
||||
direct: number,
|
||||
history: number,
|
||||
responseMax: number,
|
||||
limit: number,
|
||||
|
||||
tokenPriceIn?: number,
|
||||
tokenPriceOut?: number,
|
||||
}) {
|
||||
// external state
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -40,7 +48,7 @@ function TokenProgressbar(props: { direct: number, history: number, responseMax:
|
||||
const overflowColor = theme.palette.danger.softColor;
|
||||
|
||||
// tooltip message/color
|
||||
const { message, color } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax);
|
||||
const { message, color } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax, props.tokenPriceIn, props.tokenPriceOut);
|
||||
|
||||
// sizes
|
||||
const containerHeight = 8;
|
||||
|
||||
@@ -49,7 +49,7 @@ export function ActilePopup(props: {
|
||||
const labelNormal = item.label.slice(props.activePrefixLength);
|
||||
return (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
key={item.key}
|
||||
variant={isActive ? 'soft' : undefined}
|
||||
color={isActive ? 'primary' : undefined}
|
||||
onClick={() => props.onItemClick(item)}
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import type { FunctionComponent } from 'react';
|
||||
|
||||
export interface ActileItem {
|
||||
id: string;
|
||||
key: string;
|
||||
label: string;
|
||||
argument?: string;
|
||||
description?: string;
|
||||
Icon?: FunctionComponent;
|
||||
}
|
||||
|
||||
type ActileProviderIds = 'actile-commands' | 'actile-attach-reference';
|
||||
|
||||
export interface ActileProvider {
|
||||
id: ActileProviderIds;
|
||||
title: string;
|
||||
searchPrefix: string;
|
||||
|
||||
checkTriggerText: (trailingText: string) => boolean;
|
||||
|
||||
fetchItems: () => Promise<ActileItem[]>;
|
||||
export interface ActileProvider<TItem extends ActileItem = ActileItem> {
|
||||
fastCheckTriggerText: (trailingText: string) => boolean;
|
||||
fetchItems: () => Promise<{ title: string, searchPrefix: string, items: TItem[] }>;
|
||||
onItemSelect: (item: ActileItem) => void;
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
|
||||
|
||||
/*export const providerAttachReference: ActileProvider = {
|
||||
id: 'actile-attach-reference',
|
||||
title: 'Attach Reference',
|
||||
searchPrefix: '@',
|
||||
|
||||
checkTriggerText: (trailingText: string) =>
|
||||
trailingText.endsWith(' @'),
|
||||
|
||||
fetchItems: async () => {
|
||||
return [{
|
||||
id: 'test-1',
|
||||
label: 'Attach This',
|
||||
description: 'Attach this to the message',
|
||||
Icon: undefined,
|
||||
}];
|
||||
},
|
||||
|
||||
onItemSelect: (item: ActileItem) => {
|
||||
console.log('Selected item:', item);
|
||||
},
|
||||
};*/
|
||||
@@ -2,23 +2,25 @@ import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
import { findAllChatCommands } from '../../../commands/commands.registry';
|
||||
|
||||
|
||||
export const providerCommands = (onItemSelect: (item: ActileItem) => void): ActileProvider => ({
|
||||
id: 'actile-commands',
|
||||
title: 'Chat Commands',
|
||||
searchPrefix: '/',
|
||||
export function providerCommands(onCommandSelect: (item: ActileItem) => void): ActileProvider {
|
||||
return {
|
||||
|
||||
checkTriggerText: (trailingText: string) =>
|
||||
trailingText.trim() === '/',
|
||||
// only the literal '/' is a trigger
|
||||
fastCheckTriggerText: (trailingText: string) => trailingText === '/',
|
||||
|
||||
fetchItems: async () => {
|
||||
return findAllChatCommands().map((cmd) => ({
|
||||
id: cmd.primary,
|
||||
label: cmd.primary,
|
||||
argument: cmd.arguments?.join(' ') ?? undefined,
|
||||
description: cmd.description,
|
||||
Icon: cmd.Icon,
|
||||
}));
|
||||
},
|
||||
// no real need to be async
|
||||
fetchItems: async () => ({
|
||||
title: 'Chat Commands',
|
||||
searchPrefix: '/',
|
||||
items: findAllChatCommands().map((cmd) => ({
|
||||
key: cmd.primary,
|
||||
label: cmd.primary,
|
||||
argument: cmd.arguments?.join(' ') ?? undefined,
|
||||
description: cmd.description,
|
||||
Icon: cmd.Icon,
|
||||
} satisfies ActileItem)),
|
||||
}),
|
||||
|
||||
onItemSelect,
|
||||
});
|
||||
onItemSelect: onCommandSelect,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { conversationTitle, DConversationId, messageHasUserFlag, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
|
||||
|
||||
export interface StarredMessageItem extends ActileItem {
|
||||
conversationId: DConversationId,
|
||||
messageId: string,
|
||||
}
|
||||
|
||||
export function providerStarredMessage(onMessageSeelect: (item: StarredMessageItem) => void): ActileProvider<StarredMessageItem> {
|
||||
return {
|
||||
|
||||
// only the literal '@' at start of chat, or ' @' at end of chat
|
||||
fastCheckTriggerText: (trailingText: string) => trailingText === '@' || trailingText.endsWith(' @'),
|
||||
|
||||
// finds all the starred messages in all the conversations - this could be heavy
|
||||
fetchItems: async () => {
|
||||
const { conversations } = useChatStore.getState();
|
||||
|
||||
const starredMessages: StarredMessageItem[] = [];
|
||||
conversations.forEach((conversation) => {
|
||||
conversation.messages.forEach((message) => {
|
||||
messageHasUserFlag(message, 'starred') && starredMessages.push({
|
||||
// data
|
||||
conversationId: conversation.id,
|
||||
messageId: message.id,
|
||||
// looks
|
||||
key: message.id,
|
||||
label: conversationTitle(conversation) + ' - ' + message.text.slice(0, 32) + '...',
|
||||
// description: message.text.slice(32, 100),
|
||||
Icon: undefined,
|
||||
} satisfies StarredMessageItem);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
title: 'Starred Messages',
|
||||
searchPrefix: '',
|
||||
items: starredMessages,
|
||||
};
|
||||
},
|
||||
|
||||
onItemSelect: item => onMessageSeelect(item as StarredMessageItem),
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
const [popupOpen, setPopupOpen] = React.useState(false);
|
||||
const [provider, setProvider] = React.useState<ActileProvider | null>(null);
|
||||
|
||||
const [title, setTitle] = React.useState<string>('');
|
||||
const [items, setItems] = React.useState<ActileItem[]>([]);
|
||||
const [activeSearchString, setActiveSearchString] = React.useState<string>('');
|
||||
const [activeItemIndex, setActiveItemIndex] = React.useState<number>(0);
|
||||
@@ -17,7 +18,7 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
// derived state
|
||||
const activeItems = React.useMemo(() => {
|
||||
const search = activeSearchString.trim().toLowerCase();
|
||||
return items.filter(item => item.label.toLowerCase().startsWith(search));
|
||||
return items.filter(item => item.label?.toLowerCase().startsWith(search));
|
||||
}, [items, activeSearchString]);
|
||||
const activeItem = activeItemIndex >= 0 && activeItemIndex < activeItems.length ? activeItems[activeItemIndex] : null;
|
||||
|
||||
@@ -25,6 +26,7 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
const handleClose = React.useCallback(() => {
|
||||
setPopupOpen(false);
|
||||
setProvider(null);
|
||||
setTitle('');
|
||||
setItems([]);
|
||||
setActiveSearchString('');
|
||||
setActiveItemIndex(0);
|
||||
@@ -42,13 +44,19 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
|
||||
const actileInterceptTextChange = React.useCallback((trailingText: string) => {
|
||||
for (const provider of providers) {
|
||||
if (provider.checkTriggerText(trailingText)) {
|
||||
setProvider(provider);
|
||||
setPopupOpen(true);
|
||||
setActiveSearchString(provider.searchPrefix);
|
||||
if (provider.fastCheckTriggerText(trailingText)) {
|
||||
provider
|
||||
.fetchItems()
|
||||
.then(items => setItems(items))
|
||||
.then(({ title, searchPrefix, items }) => {
|
||||
// if there are no items, ignore
|
||||
if (items.length) {
|
||||
setPopupOpen(true);
|
||||
setProvider(provider);
|
||||
setTitle(title);
|
||||
setItems(items);
|
||||
setActiveSearchString(searchPrefix);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
handleClose();
|
||||
console.error('Failed to fetch popup items:', error);
|
||||
@@ -100,14 +108,14 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
<ActilePopup
|
||||
anchorEl={anchorRef.current}
|
||||
onClose={handleClose}
|
||||
title={provider?.title}
|
||||
title={title}
|
||||
items={activeItems}
|
||||
activeItemIndex={activeItemIndex}
|
||||
activePrefixLength={activeSearchString.length}
|
||||
onItemClick={handlePopupItemClicked}
|
||||
/>
|
||||
);
|
||||
}, [activeItemIndex, activeItems, activeSearchString.length, anchorRef, handleClose, handlePopupItemClicked, popupOpen, provider?.title]);
|
||||
}, [activeItemIndex, activeItems, activeSearchString.length, anchorRef, handleClose, handlePopupItemClicked, popupOpen, title]);
|
||||
|
||||
return {
|
||||
actileComponent,
|
||||
|
||||
@@ -6,6 +6,7 @@ import CodeIcon from '@mui/icons-material/Code';
|
||||
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import PivotTableChartIcon from '@mui/icons-material/PivotTableChart';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
import TextureIcon from '@mui/icons-material/Texture';
|
||||
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
||||
@@ -73,6 +74,7 @@ const converterTypeToIconMap: { [key in AttachmentConverterType]: React.Componen
|
||||
'pdf-images': PictureAsPdfIcon,
|
||||
'image': ImageOutlinedIcon,
|
||||
'image-ocr': AbcIcon,
|
||||
'ego-message-md': TelegramIcon,
|
||||
'unhandled': TextureIcon,
|
||||
};
|
||||
|
||||
@@ -126,7 +128,7 @@ export function AttachmentItem(props: {
|
||||
|
||||
|
||||
const handleToggleMenu = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
|
||||
onItemMenuToggle(attachment.id, event.currentTarget);
|
||||
}, [attachment, onItemMenuToggle]);
|
||||
|
||||
@@ -179,6 +181,7 @@ export function AttachmentItem(props: {
|
||||
size='sm'
|
||||
variant={variant} color={color}
|
||||
onClick={handleToggleMenu}
|
||||
onContextMenu={handleToggleMenu}
|
||||
sx={{
|
||||
backgroundColor: props.menuShown ? `${color}.softActiveBg` : variant === 'outlined' ? 'background.popup' : undefined,
|
||||
border: variant === 'soft' ? '1px solid' : undefined,
|
||||
|
||||
@@ -153,7 +153,11 @@ export function AttachmentMenu(props: {
|
||||
{/* Converters: {aConverters.map(((converter, idx) => ` ${converter.id}${(idx === aConverterIdx) ? '*' : ''}`)).join(', ')}*/}
|
||||
{/*</Typography>*/}
|
||||
<Typography level='body-xs'>
|
||||
🡒 {isOutputMissing ? 'empty' : aOutputs.map(output => `${output.type}, ${output.type === 'text-block' ? output.text.length.toLocaleString() : '(base64 image)'} bytes`).join(' · ')}
|
||||
🡒 {isOutputMissing ? 'empty' : aOutputs.map(output => `${output.type}, ${output.type === 'text-block'
|
||||
? output.text.length.toLocaleString()
|
||||
: output.type === 'image-part'
|
||||
? output.base64Url.length.toLocaleString()
|
||||
: '(other)'} bytes`).join(' · ')}
|
||||
</Typography>
|
||||
{!!tokenCountApprox && <Typography level='body-xs'>
|
||||
🡒 {tokenCountApprox.toLocaleString()} tokens
|
||||
|
||||
@@ -68,8 +68,10 @@ export function Attachments(props: {
|
||||
|
||||
const handleOverallMenuHide = () => setOverallMenuAnchor(null);
|
||||
|
||||
const handleOverallMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) =>
|
||||
const handleOverallMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
|
||||
setOverallMenuAnchor(anchor => anchor ? null : event.currentTarget);
|
||||
};
|
||||
|
||||
|
||||
// overall operations
|
||||
@@ -112,6 +114,7 @@ export function Attachments(props: {
|
||||
{/* Overall Menu button */}
|
||||
<IconButton
|
||||
onClick={handleOverallMenuToggle}
|
||||
onContextMenu={handleOverallMenuToggle}
|
||||
sx={{
|
||||
// borderRadius: 'sm',
|
||||
borderRadius: 0,
|
||||
@@ -150,7 +153,7 @@ export function Attachments(props: {
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClearAttachments}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
Clear
|
||||
Clear{attachments.length > 5 ? <span style={{ opacity: 0.5 }}> {attachments.length} attachments</span> : null}
|
||||
</MenuItem>
|
||||
</CloseableMenu>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { callBrowseFetchPage } from '~/modules/browse/browse.client';
|
||||
|
||||
import { createBase36Uid } from '~/common/util/textUtils';
|
||||
import { htmlTableToMarkdown } from '~/common/util/htmlTableToMarkdown';
|
||||
import { pdfToText } from '~/common/util/pdfUtils';
|
||||
import { pdfToImageDataURLs, pdfToText } from '~/common/util/pdfUtils';
|
||||
|
||||
import type { Attachment, AttachmentConverter, AttachmentId, AttachmentInput, AttachmentSource } from './store-attachments';
|
||||
import type { ComposerOutputMultiPart } from '../composer.types';
|
||||
@@ -58,16 +58,12 @@ export async function attachmentLoadInputAsync(source: Readonly<AttachmentSource
|
||||
edit({ label: source.refUrl, ref: source.refUrl });
|
||||
try {
|
||||
const page = await callBrowseFetchPage(source.url);
|
||||
if (page.content) {
|
||||
edit({
|
||||
input: {
|
||||
mimeType: 'text/plain',
|
||||
data: page.content,
|
||||
dataSize: page.content.length,
|
||||
},
|
||||
});
|
||||
} else
|
||||
edit({ inputError: 'No content found at this link' });
|
||||
edit(
|
||||
page.content.markdown ? { input: { mimeType: 'text/markdown', data: page.content.markdown, dataSize: page.content.markdown.length } }
|
||||
: page.content.text ? { input: { mimeType: 'text/plain', data: page.content.text, dataSize: page.content.text.length } }
|
||||
: page.content.html ? { input: { mimeType: 'text/html', data: page.content.html, dataSize: page.content.html.length } }
|
||||
: { inputError: 'No content found at this link' },
|
||||
);
|
||||
} catch (error: any) {
|
||||
edit({ inputError: `Issue downloading page: ${error?.message || (typeof error === 'string' ? error : JSON.stringify(error))}` });
|
||||
}
|
||||
@@ -132,6 +128,18 @@ export async function attachmentLoadInputAsync(source: Readonly<AttachmentSource
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ego':
|
||||
edit({
|
||||
label: source.label,
|
||||
ref: source.blockTitle,
|
||||
input: {
|
||||
mimeType: 'ego/message',
|
||||
data: source.textPlain,
|
||||
dataSize: source.textPlain.length,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
edit({ inputLoading: false });
|
||||
@@ -192,6 +200,11 @@ export function attachmentDefineConverters(sourceType: AttachmentSource['media']
|
||||
converters.push({ id: 'image-ocr', name: 'As Text (OCR)' });
|
||||
break;
|
||||
|
||||
// EGO
|
||||
case input.mimeType === 'ego/message':
|
||||
converters.push({ id: 'ego-message-md', name: 'Message' });
|
||||
break;
|
||||
|
||||
// catch-all
|
||||
default:
|
||||
converters.push({ id: 'unhandled', name: `${input.mimeType}`, unsupported: true });
|
||||
@@ -280,7 +293,7 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
|
||||
|
||||
case 'pdf-text':
|
||||
if (!(input.data instanceof ArrayBuffer)) {
|
||||
console.log('Expected ArrayBuffer for PDF converter, got:', typeof input.data);
|
||||
console.log('Expected ArrayBuffer for PDF text converter, got:', typeof input.data);
|
||||
break;
|
||||
}
|
||||
// duplicate the ArrayBuffer to avoid mutation
|
||||
@@ -295,7 +308,29 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
|
||||
break;
|
||||
|
||||
case 'pdf-images':
|
||||
// TODO: extract all pages as individual images
|
||||
if (!(input.data instanceof ArrayBuffer)) {
|
||||
console.log('Expected ArrayBuffer for PDF images converter, got:', typeof input.data);
|
||||
break;
|
||||
}
|
||||
// duplicate the ArrayBuffer to avoid mutation
|
||||
const pdfData2 = new Uint8Array(input.data.slice(0));
|
||||
try {
|
||||
const imageDataURLs = await pdfToImageDataURLs(pdfData2);
|
||||
imageDataURLs.forEach((pdfImg, index) => {
|
||||
outputs.push({
|
||||
type: 'image-part',
|
||||
base64Url: pdfImg.base64Url,
|
||||
metadata: {
|
||||
title: `Page ${index + 1}`,
|
||||
width: pdfImg.width,
|
||||
height: pdfImg.height,
|
||||
},
|
||||
collapsible: false,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error converting PDF to images:', error);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
@@ -333,6 +368,15 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ego-message-md':
|
||||
outputs.push({
|
||||
type: 'text-block',
|
||||
text: inputDataToString(input.data),
|
||||
title: ref,
|
||||
collapsible: true,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'unhandled':
|
||||
// force the user to explicitly select 'as text' if they want to proceed
|
||||
break;
|
||||
|
||||
@@ -24,6 +24,12 @@ export type AttachmentSource = {
|
||||
method: 'clipboard-read' | AttachmentSourceOriginDTO;
|
||||
textPlain?: string;
|
||||
textHtml?: string;
|
||||
} | {
|
||||
media: 'ego';
|
||||
method: 'ego-message';
|
||||
label: string;
|
||||
blockTitle: string;
|
||||
textPlain: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -41,6 +47,7 @@ export type AttachmentConverterType =
|
||||
| 'text' | 'rich-text' | 'rich-text-table'
|
||||
| 'pdf-text' | 'pdf-images'
|
||||
| 'image' | 'image-ocr'
|
||||
| 'ego-message-md'
|
||||
| 'unhandled';
|
||||
|
||||
export type AttachmentConverter = {
|
||||
@@ -62,7 +69,7 @@ export type Attachment = {
|
||||
readonly id: AttachmentId;
|
||||
readonly source: AttachmentSource,
|
||||
label: string;
|
||||
ref: string;
|
||||
ref: string; // will be used in ```ref\n...``` for instance
|
||||
|
||||
inputLoading: boolean;
|
||||
inputError: string | null;
|
||||
|
||||
@@ -100,6 +100,16 @@ export const useAttachments = (enableLoadURLs: boolean) => {
|
||||
}, [attachAppendFile, createAttachment, enableLoadURLs]);
|
||||
|
||||
|
||||
const attachAppendEgoMessage = React.useCallback((blockTitle: string, textPlain: string, attachmentLabel: string) => {
|
||||
if (ATTACHMENTS_DEBUG_INTAKE)
|
||||
console.log('attachAppendEgo', { blockTitle, textPlain, attachmentLabel });
|
||||
|
||||
return createAttachment({
|
||||
media: 'ego', method: 'ego-message', label: attachmentLabel, blockTitle: blockTitle, textPlain: textPlain,
|
||||
});
|
||||
}, [createAttachment]);
|
||||
|
||||
|
||||
const attachAppendClipboardItems = React.useCallback(async () => {
|
||||
|
||||
// if there's an issue accessing the clipboard, show it passively
|
||||
@@ -178,6 +188,7 @@ export const useAttachments = (enableLoadURLs: boolean) => {
|
||||
// create attachments
|
||||
attachAppendClipboardItems,
|
||||
attachAppendDataTransfer,
|
||||
attachAppendEgoMessage,
|
||||
attachAppendFile,
|
||||
|
||||
// manage attachments
|
||||
|
||||
@@ -10,8 +10,8 @@ import type { ComposerOutputMultiPart, ComposerOutputPartType } from '../compose
|
||||
|
||||
export interface LLMAttachments {
|
||||
attachments: LLMAttachment[];
|
||||
getAttachmentOutputs: (initialTextBlockText: string | null, attachmentId: AttachmentId) => ComposerOutputMultiPart;
|
||||
getAttachmentsOutputs: (initialTextBlockText: string | null) => ComposerOutputMultiPart;
|
||||
collapseWithAttachment: (initialTextBlockText: string | null, attachmentId: AttachmentId) => ComposerOutputMultiPart;
|
||||
collapseWithAttachments: (initialTextBlockText: string | null) => ComposerOutputMultiPart;
|
||||
isOutputAttacheable: boolean;
|
||||
isOutputTextInlineable: boolean;
|
||||
tokenCountApprox: number;
|
||||
@@ -37,13 +37,13 @@ export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId |
|
||||
|
||||
const llmAttachments = attachments.map(attachment => toLLMAttachment(attachment, supportedOutputPartTypes, chatLLMId));
|
||||
|
||||
const getAttachmentOutputs = (initialTextBlockText: string | null, attachmentId: AttachmentId): ComposerOutputMultiPart => {
|
||||
const collapseWithAttachment = (initialTextBlockText: string | null, attachmentId: AttachmentId): ComposerOutputMultiPart => {
|
||||
// get outputs of a specific attachment
|
||||
const outputs = attachments.find(a => a.id === attachmentId)?.outputs || [];
|
||||
return attachmentCollapseOutputs(initialTextBlockText, outputs);
|
||||
};
|
||||
|
||||
const getAttachmentsOutputs = (initialTextBlockText: string | null): ComposerOutputMultiPart => {
|
||||
const collapseWithAttachments = (initialTextBlockText: string | null): ComposerOutputMultiPart => {
|
||||
// accumulate all outputs of all attachments
|
||||
const allOutputs = llmAttachments.reduce((acc, a) => acc.concat(a.attachment.outputs), [] as ComposerOutputMultiPart);
|
||||
return attachmentCollapseOutputs(initialTextBlockText, allOutputs);
|
||||
@@ -51,8 +51,8 @@ export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId |
|
||||
|
||||
return {
|
||||
attachments: llmAttachments,
|
||||
getAttachmentOutputs,
|
||||
getAttachmentsOutputs,
|
||||
collapseWithAttachment,
|
||||
collapseWithAttachments,
|
||||
isOutputAttacheable: llmAttachments.every(a => a.isOutputAttachable),
|
||||
isOutputTextInlineable: llmAttachments.every(a => a.isOutputTextInlineable),
|
||||
tokenCountApprox: llmAttachments.reduce((acc, a) => acc + (a.tokenCountApprox || 0), 0),
|
||||
@@ -60,7 +60,7 @@ export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId |
|
||||
}, [attachments, chatLLMId]);
|
||||
}
|
||||
|
||||
export function getTextBlockText(outputs: ComposerOutputMultiPart): string | null {
|
||||
export function getSingleTextBlockText(outputs: ComposerOutputMultiPart): string | null {
|
||||
const textOutputs = outputs.filter(part => part.type === 'text-block');
|
||||
return (textOutputs.length === 1 && textOutputs[0].type === 'text-block') ? textOutputs[0].text : null;
|
||||
}
|
||||
@@ -131,11 +131,13 @@ function attachmentCollapseOutputs(initialTextBlockText: string | null, outputs:
|
||||
// start a new part
|
||||
else {
|
||||
if (output.type === 'text-block') {
|
||||
// THIS IS NOT CORRECT - we seem to be doing it just for downstream token counting - FIX IT
|
||||
// Do not serialize here
|
||||
accumulatedOutputs.push({
|
||||
type: 'text-block',
|
||||
text: `\n\n\`\`\`${output.title}\n${output.text}\n\`\`\``,
|
||||
title: null,
|
||||
collapsible: false,
|
||||
collapsible: false, // Wrong
|
||||
});
|
||||
} else {
|
||||
accumulatedOutputs.push(output);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
const desktopLegend =
|
||||
<Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
|
||||
Combine the answers from multiple models<br />
|
||||
<KeyStroke combo='Ctrl + Enter' sx={{ mt: 0.5, mb: 0.25 }} />
|
||||
</Box>;
|
||||
|
||||
const desktopLegendNoContent =
|
||||
<Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
|
||||
Enter the text to Beam, then press this
|
||||
</Box>;
|
||||
|
||||
const mobileSx: SxProps = {
|
||||
mr: { xs: 1, md: 2 },
|
||||
};
|
||||
|
||||
const desktopSx: SxProps = {
|
||||
'--Button-gap': '1rem',
|
||||
backgroundColor: 'background.popup',
|
||||
// border: '1px solid',
|
||||
// borderColor: 'primary.outlinedBorder',
|
||||
boxShadow: '0 4px 16px -4px rgb(var(--joy-palette-primary-mainChannel) / 10%)',
|
||||
animation: `${animationEnterBelow} 0.1s ease-out`,
|
||||
};
|
||||
|
||||
|
||||
export const ButtonBeamMemo = React.memo(ButtonBeam);
|
||||
|
||||
function ButtonBeam(props: { isMobile?: boolean, disabled?: boolean, hasContent?: boolean, onClick: () => void }) {
|
||||
return props.isMobile ? (
|
||||
<IconButton variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} sx={mobileSx}>
|
||||
<ChatBeamIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Tooltip disableInteractive variant='solid' arrow placement='right' title={props.hasContent ? desktopLegend : desktopLegendNoContent}>
|
||||
<Button variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} endDecorator={<ChatBeamIcon />} sx={desktopSx}>
|
||||
Beam
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const desktopSx: SxProps = {
|
||||
|
||||
export const ButtonCallMemo = React.memo(ButtonCall);
|
||||
|
||||
export function ButtonCall(props: { isMobile?: boolean, disabled?: boolean, onClick: () => void }) {
|
||||
function ButtonCall(props: { isMobile?: boolean, disabled?: boolean, onClick: () => void }) {
|
||||
return props.isMobile ? (
|
||||
<IconButton variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} sx={mobileSx}>
|
||||
<CallIcon />
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ButtonMultiChat(props: { isMobile?: boolean, multiChat: boolean,
|
||||
<FormControl orientation='horizontal' sx={{ minHeight: '2.25rem', justifyContent: 'space-between' }}>
|
||||
<FormLabel sx={{ gap: 1, flexFlow: 'row nowrap' }}>
|
||||
<Box sx={{ display: { xs: 'none', lg: 'inline-block' } }}>
|
||||
{multiChat ? <ChatMulticastOnIcon sx={{ color: 'warning.solidBg' }} /> : <ChatMulticastOffIcon />}
|
||||
{multiChat ? <ChatMulticastOnIcon color='primary' /> : <ChatMulticastOffIcon />}
|
||||
</Box>
|
||||
{multiChat ? 'Multichat · On' : 'Multichat'}
|
||||
</FormLabel>
|
||||
|
||||
@@ -2,13 +2,13 @@ import * as React from 'react';
|
||||
|
||||
import { Button, IconButton } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
|
||||
|
||||
export function ButtonOptionsDraw(props: { isMobile?: boolean, onClick: () => void, sx?: SxProps }) {
|
||||
return props.isMobile ? (
|
||||
<IconButton variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
|
||||
<FormatPaintIcon />
|
||||
<FormatPaintTwoToneIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
|
||||
|
||||
@@ -9,6 +9,13 @@ export type ComposerOutputPart = {
|
||||
// TODO: not implemented yet
|
||||
type: 'image-part',
|
||||
base64Url: string,
|
||||
metadata: {
|
||||
title?: string,
|
||||
generatedBy?: string,
|
||||
altText?: string,
|
||||
width?: number,
|
||||
height?: number,
|
||||
},
|
||||
collapsible: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { DragDropContext, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { List, ListItem, ListItemButton, ListItemDecorator, Sheet } from '@mui/joy';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import { ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import { DFolder, useFolderStore } from '~/common/state/store-folders';
|
||||
import { StrictModeDroppable } from '~/common/components/StrictModeDroppable';
|
||||
|
||||
import { AddFolderButton } from './AddFolderButton';
|
||||
import { FolderListItem } from './FolderListItem';
|
||||
import { StrictModeDroppable } from './StrictModeDroppable';
|
||||
|
||||
|
||||
export function ChatFolderList(props: {
|
||||
@@ -17,6 +18,7 @@ export function ChatFolderList(props: {
|
||||
contentScaling: ContentScaling;
|
||||
activeFolderId: string | null;
|
||||
onFolderSelect: (folderId: string | null) => void;
|
||||
sx?: SxProps;
|
||||
}) {
|
||||
|
||||
// derived props
|
||||
@@ -31,13 +33,18 @@ export function ChatFolderList(props: {
|
||||
|
||||
|
||||
return (
|
||||
<Sheet variant='soft' sx={{ p: 2 }}>
|
||||
<Sheet variant='soft' sx={props.sx}>
|
||||
<List
|
||||
variant='plain'
|
||||
sx={(theme) => ({
|
||||
// added to be responsive to parent's layout sizing
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
|
||||
// original list properties
|
||||
'& ul': {
|
||||
'--List-gap': '0px',
|
||||
bgcolor: 'background.surface',
|
||||
bgcolor: 'background.popup',
|
||||
'& > li:first-of-type > [role="button"]': {
|
||||
borderTopRightRadius: 'var(--List-radius)',
|
||||
borderTopLeftRadius: 'var(--List-radius)',
|
||||
@@ -131,6 +138,6 @@ export function ChatFolderList(props: {
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
</Sheet>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FormLabel, IconButton, ListItem, ListItemButton, ListItemContent, ListI
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import Done from '@mui/icons-material/Done';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
|
||||
@@ -36,8 +36,9 @@ export function FolderListItem(props: {
|
||||
|
||||
|
||||
// Menu
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
const handleMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
|
||||
setMenuAnchorEl(anchor => anchor ? null : event.currentTarget);
|
||||
setDeleteArmed(false); // Reset delete armed state
|
||||
};
|
||||
|
||||
@@ -188,9 +189,11 @@ export function FolderListItem(props: {
|
||||
|
||||
{/* Icon to show the Popup menu */}
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
className='menu-icon'
|
||||
onClick={handleMenuOpen}
|
||||
onClick={handleMenuToggle}
|
||||
onContextMenu={handleMenuToggle}
|
||||
sx={{
|
||||
visibility: 'hidden',
|
||||
my: '-0.25rem', /* absorb the button padding */
|
||||
@@ -214,7 +217,7 @@ export function FolderListItem(props: {
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<EditIcon />
|
||||
<EditRoundedIcon />
|
||||
</ListItemDecorator>
|
||||
Edit
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Droppable, DroppableProps } from "react-beautiful-dnd";
|
||||
|
||||
export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const animation = requestAnimationFrame(() => setEnabled(true));
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animation);
|
||||
setEnabled(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Droppable {...props}>{children}</Droppable>;
|
||||
};
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Avatar, Box, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import { Avatar, Box, ButtonGroup, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import { ClickAwayListener, Popper } from '@mui/base';
|
||||
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import DifferenceIcon from '@mui/icons-material/Difference';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import Face6Icon from '@mui/icons-material/Face6';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintOutlinedIcon from '@mui/icons-material/FormatPaintOutlined';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import RecordVoiceOverOutlinedIcon from '@mui/icons-material/RecordVoiceOverOutlined';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import ReplyRoundedIcon from '@mui/icons-material/ReplyRounded';
|
||||
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
|
||||
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
|
||||
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
|
||||
|
||||
@@ -26,32 +30,35 @@ import { useSanityTextDiffs } from '~/modules/blocks/RenderTextDiff';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { DMessage } from '~/common/state/store-chats';
|
||||
import { DMessage, DMessageUserFlag, messageHasUserFlag } from '~/common/state/store-chats';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { adjustContentScaling, themeScalingMap, themeZIndexPageBar } from '~/common/app.theme';
|
||||
import { animationColorRainbow } from '~/common/util/animUtils';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { cssRainbowColorKeyframes, themeScalingMap } from '~/common/app.theme';
|
||||
import { prettyBaseModel } from '~/common/util/modelUtils';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { ReplyToBubble } from './ReplyToBubble';
|
||||
import { useChatShowTextDiff } from '../../store-app-chat';
|
||||
|
||||
|
||||
// Enable the menu on text selection
|
||||
const ENABLE_SELECTION_RIGHT_CLICK_MENU: boolean = true;
|
||||
const ENABLE_SELECTION_RIGHT_CLICK_MENU = false;
|
||||
const ENABLE_SELECTION_TOOLBAR = true;
|
||||
const SELECTION_TOOLBAR_MIN_LENGTH = 3;
|
||||
|
||||
// Enable the hover button to copy the whole message. The Copy button is also available in Blocks, or in the Avatar Menu.
|
||||
const ENABLE_COPY_MESSAGE_OVERLAY: boolean = false;
|
||||
|
||||
|
||||
export function messageBackground(messageRole: DMessage['role'] | string, wasEdited: boolean, unknownAssistantIssue: boolean): string {
|
||||
export function messageBackground(messageRole: DMessage['role'] | string, wasEdited: boolean, isAssistantIssue: boolean): string {
|
||||
switch (messageRole) {
|
||||
case 'user':
|
||||
return 'primary.plainHoverBg'; // was .background.level1
|
||||
case 'assistant':
|
||||
return unknownAssistantIssue ? 'danger.softBg' : 'background.surface';
|
||||
return isAssistantIssue ? 'danger.softBg' : 'background.surface';
|
||||
case 'system':
|
||||
return wasEdited ? 'warning.softHoverBg' : 'neutral.softBg';
|
||||
default:
|
||||
@@ -59,11 +66,31 @@ export function messageBackground(messageRole: DMessage['role'] | string, wasEdi
|
||||
}
|
||||
}
|
||||
|
||||
const avatarIconSx = { width: 36, height: 36 };
|
||||
const avatarIconSx = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
};
|
||||
|
||||
const personaSx: SxProps = {
|
||||
// make this stick to the top of the screen
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
|
||||
// flexBasis: 0, // this won't let the item grow
|
||||
minWidth: { xs: 50, md: 64 },
|
||||
maxWidth: 80,
|
||||
textAlign: 'center',
|
||||
// layout
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
|
||||
export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['role'] | string, messageOriginLLM: string | undefined, messagePurposeId: SystemPurposeId | undefined, messageSender: string, messageTyping: boolean, size: 'sm' | undefined = undefined): React.JSX.Element {
|
||||
if (typeof messageAvatar === 'string' && messageAvatar)
|
||||
return <Avatar alt={messageSender} src={messageAvatar} />;
|
||||
|
||||
const mascotSx = size === 'sm' ? avatarIconSx : { width: 64, height: 64 };
|
||||
switch (messageRole) {
|
||||
case 'system':
|
||||
@@ -74,36 +101,40 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
|
||||
|
||||
case 'assistant':
|
||||
// typing gif (people seem to love this, so keeping it after april fools')
|
||||
const isDownload = messageOriginLLM === 'web';
|
||||
const isTextToImage = messageOriginLLM === 'DALL·E' || messageOriginLLM === 'Prodia';
|
||||
const isReact = messageOriginLLM?.startsWith('react-');
|
||||
if (messageTyping) {
|
||||
|
||||
// animation: message typing
|
||||
if (messageTyping)
|
||||
return <Avatar
|
||||
alt={messageSender} variant='plain'
|
||||
src={isTextToImage ? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp'
|
||||
: isReact ? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp'
|
||||
: 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'}
|
||||
src={isDownload ? 'https://i.giphy.com/26u6dIwIphLj8h10A.webp' // hourglass: https://i.giphy.com/TFSxpAIYz5inJGuY8f.webp, small-lq: https://i.giphy.com/131tNuGktpXGhy.webp, floppy: https://i.giphy.com/RxR1KghIie2iI.webp
|
||||
: isTextToImage ? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp' // brush
|
||||
: isReact ? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp' // mind
|
||||
: 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'} // typing
|
||||
sx={{ ...mascotSx, borderRadius: 'sm' }}
|
||||
/>;
|
||||
}
|
||||
|
||||
// text-to-image: icon
|
||||
// icon: text-to-image
|
||||
if (isTextToImage)
|
||||
return <FormatPaintIcon sx={{
|
||||
return <FormatPaintOutlinedIcon sx={{
|
||||
...avatarIconSx,
|
||||
animation: `${cssRainbowColorKeyframes} 1s linear 2.66`,
|
||||
animation: `${animationColorRainbow} 1s linear 2.66`,
|
||||
}} />;
|
||||
|
||||
// purpose symbol (if present)
|
||||
const symbol = SystemPurposes[messagePurposeId!]?.symbol;
|
||||
if (symbol) return <Box sx={{
|
||||
fontSize: '24px',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
minWidth: `${avatarIconSx.width}px`,
|
||||
lineHeight: `${avatarIconSx.height}px`,
|
||||
}}>
|
||||
{symbol}
|
||||
</Box>;
|
||||
if (symbol)
|
||||
return <Box sx={{
|
||||
fontSize: '24px',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
minWidth: `${avatarIconSx.width}px`,
|
||||
lineHeight: `${avatarIconSx.height}px`,
|
||||
}}>
|
||||
{symbol}
|
||||
</Box>;
|
||||
|
||||
// default assistant avatar
|
||||
return <SmartToyOutlinedIcon sx={avatarIconSx} />; // https://mui.com/static/images/avatar/2.jpg
|
||||
@@ -189,12 +220,19 @@ export function ChatMessage(props: {
|
||||
isBottom?: boolean,
|
||||
isImagining?: boolean,
|
||||
isSpeaking?: boolean,
|
||||
blocksShowDate?: boolean,
|
||||
onConversationBranch?: (messageId: string) => void,
|
||||
onConversationRestartFrom?: (messageId: string, offset: number, chatEffectBeam: boolean) => Promise<void>,
|
||||
onConversationTruncate?: (messageId: string) => void,
|
||||
showAvatar?: boolean, // auto if undefined
|
||||
showBlocksDate?: boolean,
|
||||
showUnsafeHtml?: boolean,
|
||||
adjustContentScaling?: number,
|
||||
topDecorator?: React.ReactNode,
|
||||
onMessageAssistantFrom?: (messageId: string, offset: number) => Promise<void>,
|
||||
onMessageBeam?: (messageId: string) => Promise<void>,
|
||||
onMessageBranch?: (messageId: string) => void,
|
||||
onMessageDelete?: (messageId: string) => void,
|
||||
onMessageEdit?: (messageId: string, text: string) => void,
|
||||
onMessageToggleUserFlag?: (messageId: string, flag: DMessageUserFlag) => void,
|
||||
onMessageTruncate?: (messageId: string) => void,
|
||||
onReplyTo?: (messageId: string, selectedText: string) => void,
|
||||
onTextDiagram?: (messageId: string, text: string) => Promise<void>
|
||||
onTextImagine?: (text: string) => Promise<void>
|
||||
onTextSpeak?: (text: string) => Promise<void>
|
||||
@@ -202,20 +240,21 @@ export function ChatMessage(props: {
|
||||
}) {
|
||||
|
||||
// state
|
||||
const blocksRendererRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isHovering, setIsHovering] = React.useState(false);
|
||||
const [opsMenuAnchor, setOpsMenuAnchor] = React.useState<HTMLElement | null>(null);
|
||||
const [selMenuAnchor, setSelMenuAnchor] = React.useState<HTMLElement | null>(null);
|
||||
const [selMenuText, setSelMenuText] = React.useState<string | null>(null);
|
||||
const [selToolbarAnchor, setSelToolbarAnchor] = React.useState<HTMLElement | null>(null);
|
||||
const [selText, setSelText] = React.useState<string | null>(null);
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const labsChatBeam = useUXLabsStore(state => state.labsChatBeam);
|
||||
const { cleanerLooks, contentScaling, doubleClickToEdit, renderMarkdown } = useUIPreferencesStore(state => ({
|
||||
cleanerLooks: state.zenMode === 'cleaner',
|
||||
contentScaling: state.contentScaling,
|
||||
const { showAvatar, contentScaling, doubleClickToEdit, renderMarkdown } = useUIPreferencesStore(useShallow(state => ({
|
||||
showAvatar: props.showAvatar !== undefined ? props.showAvatar : state.zenMode !== 'cleaner',
|
||||
contentScaling: adjustContentScaling(state.contentScaling, props.adjustContentScaling),
|
||||
doubleClickToEdit: state.doubleClickToEdit,
|
||||
renderMarkdown: state.renderMarkdown,
|
||||
}), shallow);
|
||||
})));
|
||||
const [showDiff, setShowDiff] = useChatShowTextDiff();
|
||||
const textDiffs = useSanityTextDiffs(props.message.text, props.diffPreviousText, showDiff);
|
||||
|
||||
@@ -229,20 +268,22 @@ export function ChatMessage(props: {
|
||||
role: messageRole,
|
||||
purposeId: messagePurposeId,
|
||||
originLLM: messageOriginLLM,
|
||||
metadata: messageMetadata,
|
||||
created: messageCreated,
|
||||
updated: messageUpdated,
|
||||
} = props.message;
|
||||
|
||||
const isUserStarred = messageHasUserFlag(props.message, 'starred');
|
||||
|
||||
const fromAssistant = messageRole === 'assistant';
|
||||
const fromSystem = messageRole === 'system';
|
||||
const wasEdited = !!messageUpdated;
|
||||
|
||||
const showAvatars = !cleanerLooks;
|
||||
|
||||
const textSel = selMenuText ? selMenuText : messageText;
|
||||
const textSel = selText ? selText : messageText;
|
||||
// WARNING: if you get an issue here, you're downgrading from the new Big-AGI 2 data format to 1.x.
|
||||
const isSpecialT2I = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/draw ') || textSel.startsWith('/imagine ') || textSel.startsWith('/img ');
|
||||
const couldDiagram = textSel?.length >= 100 && !isSpecialT2I;
|
||||
const couldImagine = textSel?.length >= 2 && !isSpecialT2I;
|
||||
const couldDiagram = textSel.length >= 100 && !isSpecialT2I;
|
||||
const couldImagine = textSel.length >= 3 && !isSpecialT2I;
|
||||
const couldSpeak = couldImagine;
|
||||
|
||||
|
||||
@@ -255,39 +296,51 @@ export function ChatMessage(props: {
|
||||
|
||||
// Operations Menu
|
||||
|
||||
const closeOpsMenu = () => setOpsMenuAnchor(null);
|
||||
const { onMessageToggleUserFlag } = props;
|
||||
|
||||
const handleOpsMenuToggle = React.useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
|
||||
setOpsMenuAnchor(anchor => anchor ? null : event.currentTarget);
|
||||
}, []);
|
||||
|
||||
const handleCloseOpsMenu = React.useCallback(() => setOpsMenuAnchor(null), []);
|
||||
|
||||
const handleOpsCopy = (e: React.MouseEvent) => {
|
||||
copyToClipboard(textSel, 'Text');
|
||||
e.preventDefault();
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
};
|
||||
|
||||
const handleOpsEdit = React.useCallback((e: React.MouseEvent) => {
|
||||
if (messageTyping && !isEditing) return; // don't allow editing while typing
|
||||
setIsEditing(!isEditing);
|
||||
e.preventDefault();
|
||||
closeOpsMenu();
|
||||
}, [isEditing, messageTyping]);
|
||||
handleCloseOpsMenu();
|
||||
}, [handleCloseOpsMenu, isEditing, messageTyping]);
|
||||
|
||||
const handleOpsConversationBranch = (e: React.MouseEvent) => {
|
||||
const handleOpsToggleStarred = React.useCallback(() => {
|
||||
onMessageToggleUserFlag?.(messageId, 'starred');
|
||||
}, [messageId, onMessageToggleUserFlag]);
|
||||
|
||||
const handleOpsAssistantFrom = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
handleCloseOpsMenu();
|
||||
await props.onMessageAssistantFrom?.(messageId, fromAssistant ? -1 : 0);
|
||||
};
|
||||
|
||||
const handleOpsBeamFrom = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleCloseOpsMenu();
|
||||
await props.onMessageBeam?.(messageId);
|
||||
};
|
||||
|
||||
const handleOpsBranch = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // to try to not steal the focus from the banched conversation
|
||||
props.onConversationBranch && props.onConversationBranch(messageId);
|
||||
closeOpsMenu();
|
||||
};
|
||||
|
||||
const handleOpsConversationRestartFrom = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
closeOpsMenu();
|
||||
props.onConversationRestartFrom && await props.onConversationRestartFrom(messageId, fromAssistant ? -1 : 0, false);
|
||||
};
|
||||
|
||||
const handleOpsConversationRestartFromBeam = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
closeOpsMenu();
|
||||
props.onConversationRestartFrom && labsChatBeam && await props.onConversationRestartFrom(messageId, fromAssistant ? -1 : 0, true);
|
||||
props.onMessageBranch?.(messageId);
|
||||
handleCloseOpsMenu();
|
||||
};
|
||||
|
||||
const handleOpsToggleShowDiff = () => setShowDiff(!showDiff);
|
||||
@@ -296,8 +349,9 @@ export function ChatMessage(props: {
|
||||
e.preventDefault();
|
||||
if (props.onTextDiagram) {
|
||||
await props.onTextDiagram(messageId, textSel);
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -305,8 +359,19 @@ export function ChatMessage(props: {
|
||||
e.preventDefault();
|
||||
if (props.onTextImagine) {
|
||||
await props.onTextImagine(textSel);
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpsReplyTo = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (props.onReplyTo && textSel.trim().length >= SELECTION_TOOLBAR_MIN_LENGTH) {
|
||||
props.onReplyTo(messageId, textSel.trim());
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -314,18 +379,19 @@ export function ChatMessage(props: {
|
||||
e.preventDefault();
|
||||
if (props.onTextSpeak) {
|
||||
await props.onTextSpeak(textSel);
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpsTruncate = (_e: React.MouseEvent) => {
|
||||
props.onConversationTruncate && props.onConversationTruncate(messageId);
|
||||
closeOpsMenu();
|
||||
props.onMessageTruncate?.(messageId);
|
||||
handleCloseOpsMenu();
|
||||
};
|
||||
|
||||
const handleOpsDelete = (_e: React.MouseEvent) => {
|
||||
props.onMessageDelete && props.onMessageDelete(messageId);
|
||||
props.onMessageDelete?.(messageId);
|
||||
};
|
||||
|
||||
|
||||
@@ -356,17 +422,17 @@ export function ChatMessage(props: {
|
||||
document.body.appendChild(anchorEl);
|
||||
|
||||
setSelMenuAnchor(anchorEl);
|
||||
setSelMenuText(selectedText);
|
||||
setSelText(selectedText);
|
||||
}, [removeSelectionAnchor]);
|
||||
|
||||
const closeSelectionMenu = React.useCallback(() => {
|
||||
// window.getSelection()?.removeAllRanges?.();
|
||||
removeSelectionAnchor();
|
||||
setSelMenuAnchor(null);
|
||||
setSelMenuText(null);
|
||||
setSelText(null);
|
||||
}, [removeSelectionAnchor]);
|
||||
|
||||
const handleMouseUp = React.useCallback((event: MouseEvent) => {
|
||||
const handleContextMenu = React.useCallback((event: MouseEvent) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
@@ -377,16 +443,74 @@ export function ChatMessage(props: {
|
||||
}, [openSelectionMenu]);
|
||||
|
||||
|
||||
// Selection Toolbar
|
||||
|
||||
const closeToolbar = React.useCallback((anchorEl?: HTMLElement) => {
|
||||
window.getSelection()?.removeAllRanges?.();
|
||||
try {
|
||||
const anchor = anchorEl || selToolbarAnchor;
|
||||
anchor && document.body.removeChild(anchor);
|
||||
} catch (e) {
|
||||
// ignore...
|
||||
}
|
||||
setSelToolbarAnchor(null);
|
||||
setSelText(null);
|
||||
}, [selToolbarAnchor]);
|
||||
|
||||
const handleOpenToolbar = React.useCallback((_event: MouseEvent) => {
|
||||
// check for selection
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount <= 0) return;
|
||||
|
||||
// check for enought selection
|
||||
const selectionText = selection.toString().trim();
|
||||
if (selectionText.length < SELECTION_TOOLBAR_MIN_LENGTH) return;
|
||||
|
||||
// check for the selection being inside the blocks renderer (core of the message)
|
||||
const selectionRange = selection.getRangeAt(0);
|
||||
const blocksElement = blocksRendererRef.current;
|
||||
if (!blocksElement || !blocksElement.contains(selectionRange.commonAncestorContainer)) return;
|
||||
|
||||
const rangeRects = selectionRange.getClientRects();
|
||||
if (rangeRects.length <= 0) return;
|
||||
|
||||
const firstRect = rangeRects[0];
|
||||
const anchorEl = document.createElement('div');
|
||||
anchorEl.style.position = 'fixed';
|
||||
anchorEl.style.left = `${firstRect.left + window.scrollX}px`;
|
||||
anchorEl.style.top = `${firstRect.top + window.scrollY}px`;
|
||||
document.body.appendChild(anchorEl);
|
||||
anchorEl.setAttribute('role', 'dialog');
|
||||
|
||||
// auto-close logic on unselect
|
||||
const closeOnUnselect = () => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.toString().trim() === '') {
|
||||
closeToolbar(anchorEl);
|
||||
document.removeEventListener('selectionchange', closeOnUnselect);
|
||||
}
|
||||
};
|
||||
document.addEventListener('selectionchange', closeOnUnselect);
|
||||
|
||||
setSelToolbarAnchor(anchorEl);
|
||||
setSelText(selectionText);
|
||||
}, [closeToolbar]);
|
||||
|
||||
|
||||
// Blocks renderer
|
||||
|
||||
const handleBlocksContextMenu = React.useCallback((event: React.MouseEvent) => {
|
||||
handleMouseUp(event.nativeEvent);
|
||||
}, [handleMouseUp]);
|
||||
handleContextMenu(event.nativeEvent);
|
||||
}, [handleContextMenu]);
|
||||
|
||||
const handleBlocksDoubleClick = React.useCallback((event: React.MouseEvent) => {
|
||||
doubleClickToEdit && props.onMessageEdit && handleOpsEdit(event);
|
||||
}, [doubleClickToEdit, handleOpsEdit, props.onMessageEdit]);
|
||||
|
||||
const handleBlocksMouseUp = React.useCallback((event: React.MouseEvent) => {
|
||||
handleOpenToolbar(event.nativeEvent);
|
||||
}, [handleOpenToolbar]);
|
||||
|
||||
|
||||
// prettier upstream errors
|
||||
const { isAssistantError, errorMessage } = React.useMemo(
|
||||
@@ -399,92 +523,128 @@ export function ChatMessage(props: {
|
||||
|
||||
// avatar
|
||||
const avatarEl: React.JSX.Element | null = React.useMemo(
|
||||
() => showAvatars ? makeAvatar(messageAvatar, messageRole, messageOriginLLM, messagePurposeId, messageSender, messageTyping) : null,
|
||||
[messageAvatar, messageOriginLLM, messagePurposeId, messageRole, messageSender, messageTyping, showAvatars],
|
||||
() => showAvatar ? makeAvatar(messageAvatar, messageRole, messageOriginLLM, messagePurposeId, messageSender, messageTyping) : null,
|
||||
[messageAvatar, messageOriginLLM, messagePurposeId, messageRole, messageSender, messageTyping, showAvatar],
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
role='chat-message'
|
||||
onMouseUp={(ENABLE_SELECTION_TOOLBAR && !fromSystem && !isAssistantError) ? handleBlocksMouseUp : undefined}
|
||||
sx={{
|
||||
display: 'flex', flexDirection: !fromAssistant ? 'row-reverse' : 'row', alignItems: 'flex-start',
|
||||
gap: { xs: 0, md: 1 },
|
||||
// style
|
||||
backgroundColor: backgroundColor,
|
||||
px: { xs: 1, md: themeScalingMap[contentScaling]?.chatMessagePadding ?? 2 },
|
||||
py: themeScalingMap[contentScaling]?.chatMessagePadding ?? 2,
|
||||
backgroundColor,
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
...(ENABLE_COPY_MESSAGE_OVERLAY && { position: 'relative' }),
|
||||
|
||||
// style: omit border if set externally
|
||||
...(!('borderBottom' in (props.sx || {})) && {
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
}),
|
||||
|
||||
// style: when starred
|
||||
...(isUserStarred && {
|
||||
outline: '3px solid',
|
||||
outlineColor: 'primary.solidBg',
|
||||
boxShadow: 'lg',
|
||||
borderRadius: 'lg',
|
||||
zIndex: 1,
|
||||
}),
|
||||
|
||||
// style: make room for a top decorator if set
|
||||
'&:hover > button': { opacity: 1 },
|
||||
|
||||
// layout
|
||||
display: 'block', // this is Needed, otherwise there will be a horizontal overflow
|
||||
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Avatar */}
|
||||
{showAvatars && (
|
||||
<Box
|
||||
onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}
|
||||
onClick={event => setOpsMenuAnchor(event.currentTarget)}
|
||||
sx={{
|
||||
// flexBasis: 0, // this won't let the item grow
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
minWidth: { xs: 50, md: 64 },
|
||||
maxWidth: 80,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{/* (Optional) underlayed top decorator */}
|
||||
{props.topDecorator}
|
||||
|
||||
{isHovering ? (
|
||||
<IconButton variant='soft' color={(fromAssistant || fromSystem) ? 'neutral' : 'primary'} sx={avatarIconSx}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
avatarEl
|
||||
)}
|
||||
{/* Message Row: Avatar, Blocks (1 text -> blocksRenderer) */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: !fromAssistant ? 'row-reverse' : 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: { xs: 0, md: 1 },
|
||||
}}>
|
||||
|
||||
{/* Assistant model name */}
|
||||
{fromAssistant && (
|
||||
<Tooltip title={messageTyping ? null : (messageOriginLLM || 'unk-model')} variant='solid'>
|
||||
<Typography level='body-xs' sx={{
|
||||
overflowWrap: 'anywhere',
|
||||
...(messageTyping ? { animation: `${cssRainbowColorKeyframes} 5s linear infinite` } : {}),
|
||||
}}>
|
||||
{prettyBaseModel(messageOriginLLM)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Avatar (Persona) */}
|
||||
{showAvatar && (
|
||||
<Box sx={personaSx}>
|
||||
|
||||
</Box>
|
||||
)}
|
||||
{/* Persona Avatar or Menu Button */}
|
||||
<Box
|
||||
onClick={handleOpsMenuToggle}
|
||||
onContextMenu={handleOpsMenuToggle}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
sx={{ display: 'flex' }}
|
||||
>
|
||||
{(isHovering || opsMenuAnchor) ? (
|
||||
<IconButton variant={opsMenuAnchor ? 'solid' : 'soft'} color={(fromAssistant || fromSystem) ? 'neutral' : 'primary'} sx={avatarIconSx}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
avatarEl
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Assistant model name */}
|
||||
{fromAssistant && (
|
||||
<Tooltip arrow title={messageTyping ? null : (messageOriginLLM || 'unk-model')} variant='solid'>
|
||||
<Typography level='body-xs' sx={{
|
||||
overflowWrap: 'anywhere',
|
||||
...(messageTyping ? { animation: `${animationColorRainbow} 5s linear infinite` } : {}),
|
||||
}}>
|
||||
{prettyBaseModel(messageOriginLLM)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{/* Edit / Blocks */}
|
||||
{isEditing ? (
|
||||
{/* Edit / Blocks */}
|
||||
{isEditing ? (
|
||||
|
||||
<InlineTextarea
|
||||
initialText={messageText} onEdit={handleTextEdited}
|
||||
sx={editBlocksSx}
|
||||
/>
|
||||
<InlineTextarea
|
||||
initialText={messageText} onEdit={handleTextEdited}
|
||||
sx={editBlocksSx}
|
||||
/>
|
||||
|
||||
) : (
|
||||
) : (
|
||||
|
||||
<BlocksRenderer
|
||||
text={messageText}
|
||||
fromRole={messageRole}
|
||||
contentScaling={contentScaling}
|
||||
errorMessage={errorMessage}
|
||||
fitScreen={props.fitScreen}
|
||||
isBottom={props.isBottom}
|
||||
renderTextAsMarkdown={renderMarkdown}
|
||||
renderTextDiff={textDiffs || undefined}
|
||||
showDate={props.blocksShowDate === true ? messageUpdated || messageCreated || undefined : undefined}
|
||||
wasUserEdited={wasEdited}
|
||||
onContextMenu={(props.onMessageEdit && ENABLE_SELECTION_RIGHT_CLICK_MENU) ? handleBlocksContextMenu : undefined}
|
||||
onDoubleClick={(props.onMessageEdit && doubleClickToEdit) ? handleBlocksDoubleClick : undefined}
|
||||
optiAllowMemo={messageTyping}
|
||||
/>
|
||||
<BlocksRenderer
|
||||
ref={blocksRendererRef}
|
||||
text={messageText}
|
||||
fromRole={messageRole}
|
||||
contentScaling={contentScaling}
|
||||
errorMessage={errorMessage}
|
||||
fitScreen={props.fitScreen}
|
||||
isBottom={props.isBottom}
|
||||
renderTextAsMarkdown={renderMarkdown}
|
||||
renderTextDiff={textDiffs || undefined}
|
||||
showDate={props.showBlocksDate === true ? messageUpdated || messageCreated || undefined : undefined}
|
||||
showUnsafeHtml={props.showUnsafeHtml}
|
||||
wasUserEdited={wasEdited}
|
||||
onContextMenu={(props.onMessageEdit && ENABLE_SELECTION_RIGHT_CLICK_MENU) ? handleBlocksContextMenu : undefined}
|
||||
onDoubleClick={(props.onMessageEdit && doubleClickToEdit) ? handleBlocksDoubleClick : undefined}
|
||||
optiAllowMemo={messageTyping}
|
||||
/>
|
||||
|
||||
)}
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* Reply-To Bubble */}
|
||||
{!!messageMetadata?.inReplyToText && <ReplyToBubble inlineMessage replyToText={messageMetadata.inReplyToText} className='reply-to-bubble' />}
|
||||
|
||||
|
||||
{/* Overlay copy icon */}
|
||||
@@ -506,7 +666,7 @@ export function ChatMessage(props: {
|
||||
{!!opsMenuAnchor && (
|
||||
<CloseableMenu
|
||||
dense placement='bottom-end'
|
||||
open anchorEl={opsMenuAnchor} onClose={closeOpsMenu}
|
||||
open anchorEl={opsMenuAnchor} onClose={handleCloseOpsMenu}
|
||||
sx={{ minWidth: 280 }}
|
||||
>
|
||||
|
||||
@@ -520,29 +680,33 @@ export function ChatMessage(props: {
|
||||
|
||||
{/* Edit / Copy */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{/* Edit */}
|
||||
{!!props.onMessageEdit && (
|
||||
<MenuItem variant='plain' disabled={messageTyping} onClick={handleOpsEdit} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator><EditIcon /></ListItemDecorator>
|
||||
<ListItemDecorator><EditRoundedIcon /></ListItemDecorator>
|
||||
{isEditing ? 'Discard' : 'Edit'}
|
||||
{/*{!isEditing && <span style={{ opacity: 0.5, marginLeft: '8px' }}>{doubleClickToEdit ? '(double-click)' : ''}</span>}*/}
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Copy */}
|
||||
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
|
||||
Copy
|
||||
</MenuItem>
|
||||
{/* Starred */}
|
||||
{!!onMessageToggleUserFlag && (
|
||||
<MenuItem onClick={handleOpsToggleStarred} sx={{ flexGrow: 0, px: 1 }}>
|
||||
{isUserStarred
|
||||
? <StarRoundedIcon color='primary' sx={{ fontSize: 'xl2' }} />
|
||||
: <StarOutlineRoundedIcon sx={{ fontSize: 'xl2' }} />
|
||||
}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Box>
|
||||
{/* Delete / Branch / Truncate */}
|
||||
{!!props.onMessageDelete && <ListDivider />}
|
||||
{!!props.onMessageDelete && (
|
||||
<MenuItem onClick={handleOpsDelete} disabled={false /*fromSystem*/}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
Delete
|
||||
<span style={{ opacity: 0.5 }}>message</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onConversationBranch && (
|
||||
<MenuItem onClick={handleOpsConversationBranch} disabled={fromSystem}>
|
||||
{!!props.onMessageBranch && <ListDivider />}
|
||||
{!!props.onMessageBranch && (
|
||||
<MenuItem onClick={handleOpsBranch} disabled={fromSystem}>
|
||||
<ListItemDecorator>
|
||||
<ForkRightIcon />
|
||||
</ListItemDecorator>
|
||||
@@ -550,13 +714,40 @@ export function ChatMessage(props: {
|
||||
{!props.isBottom && <span style={{ opacity: 0.5 }}>from here</span>}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onConversationTruncate && (
|
||||
{!!props.onMessageDelete && (
|
||||
<MenuItem onClick={handleOpsDelete} disabled={false /*fromSystem*/}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
Delete
|
||||
<span style={{ opacity: 0.5 }}>message</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onMessageTruncate && (
|
||||
<MenuItem onClick={handleOpsTruncate} disabled={props.isBottom}>
|
||||
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
|
||||
Truncate
|
||||
<span style={{ opacity: 0.5 }}>after this</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Diagram / Draw / Speak */}
|
||||
{!!props.onTextDiagram && <ListDivider />}
|
||||
{!!props.onTextDiagram && (
|
||||
<MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram}>
|
||||
<ListItemDecorator><AccountTreeOutlinedIcon /></ListItemDecorator>
|
||||
Auto-Diagram ...
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextImagine && (
|
||||
<MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintOutlinedIcon />}</ListItemDecorator>
|
||||
Auto-Draw
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextSpeak && (
|
||||
<MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverOutlinedIcon />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Diff Viewer */}
|
||||
{!!props.diffPreviousText && <ListDivider />}
|
||||
{!!props.diffPreviousText && (
|
||||
@@ -566,56 +757,98 @@ export function ChatMessage(props: {
|
||||
<Switch checked={showDiff} onChange={handleOpsToggleShowDiff} sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Diagram / Draw / Speak */}
|
||||
{!!props.onTextDiagram && <ListDivider />}
|
||||
{!!props.onTextDiagram && (
|
||||
<MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram}>
|
||||
<ListItemDecorator><AccountTreeIcon color='success' /></ListItemDecorator>
|
||||
Diagram ...
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextImagine && (
|
||||
<MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
|
||||
Draw ...
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextSpeak && (
|
||||
<MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Restart/try */}
|
||||
{!!props.onConversationRestartFrom && <ListDivider />}
|
||||
{!!props.onConversationRestartFrom && (
|
||||
<MenuItem onClick={handleOpsConversationRestartFrom}>
|
||||
{/* Beam/Restart */}
|
||||
{(!!props.onMessageAssistantFrom || !!props.onMessageBeam) && <ListDivider />}
|
||||
{!!props.onMessageAssistantFrom && (
|
||||
<MenuItem disabled={fromSystem} onClick={handleOpsAssistantFrom}>
|
||||
<ListItemDecorator>{fromAssistant ? <ReplayIcon color='primary' /> : <TelegramIcon color='primary' />}</ListItemDecorator>
|
||||
{!fromAssistant
|
||||
? <>Restart <span style={{ opacity: 0.5 }}>from here</span></>
|
||||
: !props.isBottom
|
||||
? <>Retry <span style={{ opacity: 0.5 }}>from here</span></>
|
||||
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Retry
|
||||
<KeyStroke combo='Ctrl + Shift + R' />
|
||||
</Box>}
|
||||
{labsChatBeam && (
|
||||
<Tooltip title={messageTyping ? null : 'Best-Of'}>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='outlined' color='primary'
|
||||
onClick={handleOpsConversationRestartFromBeam}
|
||||
sx={{ ml: 'auto', my: '-0.25rem' /* absorb the menuItem padding */ }}
|
||||
>
|
||||
<ChatBeamIcon /> {/*<GavelIcon />*/}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>Retry<KeyStroke combo='Ctrl + Shift + R' /></Box>}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onMessageBeam && (
|
||||
<MenuItem disabled={fromSystem} onClick={handleOpsBeamFrom}>
|
||||
<ListItemDecorator>
|
||||
<ChatBeamIcon color={fromSystem ? undefined : 'primary'} />
|
||||
</ListItemDecorator>
|
||||
{!fromAssistant
|
||||
? <>Beam <span style={{ opacity: 0.5 }}>from here</span></>
|
||||
: !props.isBottom
|
||||
? <>Beam <span style={{ opacity: 0.5 }}>this message</span></>
|
||||
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>Beam<KeyStroke combo='Ctrl + Shift + B' /></Box>}
|
||||
</MenuItem>
|
||||
)}
|
||||
</CloseableMenu>
|
||||
)}
|
||||
|
||||
|
||||
{/* Selection Toolbar */}
|
||||
{ENABLE_SELECTION_TOOLBAR && !!selToolbarAnchor && (
|
||||
<Popper placement='top-start' open anchorEl={selToolbarAnchor} slotProps={{
|
||||
root: { style: { zIndex: themeZIndexPageBar + 1 } },
|
||||
}}>
|
||||
<ClickAwayListener onClickAway={() => closeToolbar()}>
|
||||
<ButtonGroup
|
||||
variant='plain'
|
||||
sx={{
|
||||
'--ButtonGroup-separatorColor': 'none !important',
|
||||
'--ButtonGroup-separatorSize': 0,
|
||||
borderRadius: '0',
|
||||
backgroundColor: 'background.popup',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.outlinedBorder',
|
||||
boxShadow: '0px 4px 12px -4px rgb(var(--joy-palette-neutral-darkChannel) / 50%)',
|
||||
mb: 1,
|
||||
ml: -1,
|
||||
alignItems: 'center',
|
||||
'& > button': {
|
||||
'--Icon-fontSize': '1rem',
|
||||
minHeight: '2.5rem',
|
||||
minWidth: '2.75rem',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!!props.onReplyTo && fromAssistant && <Tooltip disableInteractive arrow placement='top' title='Reply'>
|
||||
<IconButton color='primary' onClick={handleOpsReplyTo}>
|
||||
<ReplyRoundedIcon sx={{ fontSize: 'xl' }} />
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
{/*{!!props.onMessageBeam && fromAssistant && <Tooltip disableInteractive arrow placement='top' title='Beam'>*/}
|
||||
{/* <IconButton color='primary'>*/}
|
||||
{/* <ChatBeamIcon sx={{ fontSize: 'xl' }} />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/*</Tooltip>}*/}
|
||||
{!!props.onReplyTo && fromAssistant && <MoreVertIcon sx={{ color: 'neutral.outlinedBorder', fontSize: 'md' }} />}
|
||||
<Tooltip disableInteractive arrow placement='top' title='Copy'>
|
||||
<IconButton onClick={handleOpsCopy}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{(!!props.onTextDiagram || !!props.onTextSpeak) && <MoreVertIcon sx={{ color: 'neutral.outlinedBorder', fontSize: 'md' }} />}
|
||||
{!!props.onTextDiagram && <Tooltip disableInteractive arrow placement='top' title={couldDiagram ? 'Auto-Diagram...' : 'Too short to Auto-Diagram'}>
|
||||
<IconButton onClick={couldDiagram ? handleOpsDiagram : undefined}>
|
||||
<AccountTreeOutlinedIcon sx={{ color: couldDiagram ? 'primary' : 'neutral.plainDisabledColor' }} />
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
{/*{!!props.onTextImagine && <Tooltip disableInteractive arrow placement='top' title='Auto-Draw'>*/}
|
||||
{/* <IconButton onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>*/}
|
||||
{/* {!props.isImagining ? <FormatPaintOutlinedIcon /> : <CircularProgress sx={{ '--CircularProgress-size': '16px' }} />}*/}
|
||||
{/* </IconButton>*/}
|
||||
{/*</Tooltip>}*/}
|
||||
{!!props.onTextSpeak && <Tooltip disableInteractive arrow placement='top' title='Speak'>
|
||||
<IconButton onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
{!props.isSpeaking ? <RecordVoiceOverOutlinedIcon /> : <CircularProgress sx={{ '--CircularProgress-size': '16px' }} />}
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
</ButtonGroup>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
)}
|
||||
|
||||
|
||||
{/* Selection (Contextual) Menu */}
|
||||
{!!selMenuAnchor && (
|
||||
<CloseableMenu
|
||||
@@ -623,20 +856,21 @@ export function ChatMessage(props: {
|
||||
open anchorEl={selMenuAnchor} onClose={closeSelectionMenu}
|
||||
sx={{ minWidth: 220 }}
|
||||
>
|
||||
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1 }}>
|
||||
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1, alignItems: 'center' }}>
|
||||
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
|
||||
Copy <span style={{ opacity: 0.5 }}>selection</span>
|
||||
Copy
|
||||
</MenuItem>
|
||||
{!!props.onTextDiagram && <ListDivider />}
|
||||
{!!props.onTextDiagram && <MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram || props.isImagining}>
|
||||
<ListItemDecorator><AccountTreeIcon color='success' /></ListItemDecorator>
|
||||
Diagram ...
|
||||
<ListItemDecorator><AccountTreeOutlinedIcon /></ListItemDecorator>
|
||||
Auto-Diagram ...
|
||||
</MenuItem>}
|
||||
{!!props.onTextImagine && <MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
|
||||
Imagine
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintOutlinedIcon />}</ListItemDecorator>
|
||||
Auto-Draw
|
||||
</MenuItem>}
|
||||
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverOutlinedIcon />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>}
|
||||
</CloseableMenu>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { makeAvatar, messageBackground } from './ChatMessage';
|
||||
export const MessagesSelectionHeader = (props: { hasSelected: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
|
||||
<Sheet color='warning' variant='solid' invertedColors sx={{
|
||||
display: 'flex', flexDirection: 'row', alignItems: 'center',
|
||||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 101,
|
||||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 101 /* Cleanup Selection Header on top of messages */,
|
||||
boxShadow: 'md',
|
||||
gap: { xs: 1, sm: 2 }, px: { xs: 1, md: 2 }, py: 1,
|
||||
}}>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, IconButton, Tooltip, Typography } from '@mui/joy';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import ReplyRoundedIcon from '@mui/icons-material/ReplyRounded';
|
||||
|
||||
|
||||
// configuration
|
||||
const INLINE_COLOR = 'primary';
|
||||
|
||||
|
||||
const bubbleComposerSx: SxProps = {
|
||||
// contained
|
||||
width: '100%',
|
||||
zIndex: 2, // stays on top of the 'tokens' bubble in the composer
|
||||
|
||||
// style
|
||||
backgroundColor: 'background.surface',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
borderRadius: 'sm',
|
||||
boxShadow: 'xs',
|
||||
padding: '0.5rem 0.25rem 0.5rem 0.5rem',
|
||||
|
||||
// layout
|
||||
display: 'flex',
|
||||
alignItems: 'start',
|
||||
};
|
||||
|
||||
const inlineMessageSx: SxProps = {
|
||||
...bubbleComposerSx,
|
||||
|
||||
// redefine
|
||||
// border: 'none',
|
||||
mt: 1,
|
||||
borderColor: `${INLINE_COLOR}.outlinedColor`,
|
||||
borderRadius: 'sm',
|
||||
boxShadow: 'xs',
|
||||
width: undefined,
|
||||
padding: '0.375rem 0.25rem 0.375rem 0.5rem',
|
||||
|
||||
// self-layout (parent: 'block', as 'grid' was not working and the user would scroll the app on the x-axis on mobile)
|
||||
// ml: 'auto',
|
||||
float: 'inline-end',
|
||||
mr: { xs: 7.75, md: 10.5 }, // personaSx.minWidth + gap (md: 1) + 1.5 (text margin)
|
||||
|
||||
};
|
||||
|
||||
|
||||
export function ReplyToBubble(props: {
|
||||
replyToText: string | null,
|
||||
inlineMessage?: boolean
|
||||
onClear?: () => void,
|
||||
className?: string,
|
||||
}) {
|
||||
return (
|
||||
<Box className={props.className} sx={!props.inlineMessage ? bubbleComposerSx : inlineMessageSx}>
|
||||
<Tooltip disableInteractive arrow title='Referring to this assistant text' placement='top'>
|
||||
<ReplyRoundedIcon sx={{
|
||||
color: props.inlineMessage ? `${INLINE_COLOR}.outlinedColor` : 'primary.solidBg',
|
||||
fontSize: 'xl',
|
||||
mt: 0.125,
|
||||
}} />
|
||||
</Tooltip>
|
||||
<Typography level='body-sm' sx={{
|
||||
flex: 1,
|
||||
ml: 1,
|
||||
mr: 0.5,
|
||||
overflow: 'auto',
|
||||
maxHeight: '5.75rem',
|
||||
lineHeight: 'xl',
|
||||
color: /*props.inlineMessage ? 'text.tertiary' :*/ 'text.secondary',
|
||||
whiteSpace: 'break-spaces', // 'balance'
|
||||
}}>
|
||||
{props.replyToText}
|
||||
</Typography>
|
||||
{!!props.onClear && (
|
||||
<IconButton size='sm' onClick={props.onClear} sx={{ my: -0.5, background: 'none' }}>
|
||||
<CloseRoundedIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
@@ -9,25 +10,36 @@ import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
// change this to increase/decrease the number history steps per pane
|
||||
const MAX_HISTORY_LENGTH = 10;
|
||||
|
||||
// change this to allow for more/less panes
|
||||
const MAX_CONCURRENT_PANES = 4;
|
||||
|
||||
// change to true to enable verbose console logging
|
||||
const DEBUG_PANES_MANAGER = false;
|
||||
|
||||
|
||||
interface ChatPane {
|
||||
|
||||
paneId: string;
|
||||
|
||||
conversationId: DConversationId | null;
|
||||
|
||||
// other per-pane storage? or would this be cluttering the panes(view)-only abstaction?
|
||||
// ... we are currently creating companion ConversationHandler obects for this
|
||||
|
||||
history: DConversationId[]; // History of the conversationIds for this pane
|
||||
historyIndex: number; // Current position in the history for this pane
|
||||
|
||||
}
|
||||
|
||||
interface AppChatPanesStore {
|
||||
interface AppChatPanesState {
|
||||
|
||||
// state
|
||||
chatPanes: ChatPane[];
|
||||
chatPaneFocusIndex: number | null;
|
||||
|
||||
}
|
||||
|
||||
interface AppChatPanesStore extends AppChatPanesState {
|
||||
|
||||
// actions
|
||||
openConversationInFocusedPane: (conversationId: DConversationId) => void;
|
||||
openConversationInSplitPane: (conversationId: DConversationId) => void;
|
||||
@@ -35,19 +47,29 @@ interface AppChatPanesStore {
|
||||
duplicateFocusedPane: (/*paneIndex: number*/) => void;
|
||||
removeOtherPanes: () => void;
|
||||
removePane: (paneIndex: number) => void;
|
||||
setFocusedPane: (paneIndex: number) => void;
|
||||
onConversationsChanged: (conversationIds: DConversationId[]) => void;
|
||||
setFocusedPaneIndex: (paneIndex: number) => void;
|
||||
_onConversationsChanged: (conversationIds: DConversationId[]) => void;
|
||||
|
||||
}
|
||||
|
||||
function createPane(conversationId: DConversationId | null = null): ChatPane {
|
||||
return {
|
||||
paneId: uuidv4(),
|
||||
conversationId,
|
||||
history: conversationId ? [conversationId] : [],
|
||||
historyIndex: conversationId ? 0 : -1,
|
||||
};
|
||||
}
|
||||
|
||||
function duplicatePane(pane: ChatPane): ChatPane {
|
||||
return {
|
||||
paneId: uuidv4(),
|
||||
conversationId: pane.conversationId,
|
||||
history: [...pane.history],
|
||||
historyIndex: pane.historyIndex,
|
||||
};
|
||||
}
|
||||
|
||||
const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
(_set, _get) => ({
|
||||
|
||||
@@ -68,8 +90,14 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the conversation is already open in the focused pane.
|
||||
// Sanity check: Get the focused pane
|
||||
const focusedPane = chatPanes[chatPaneFocusIndex];
|
||||
if (!focusedPane) {
|
||||
console.warn('openConversationInFocusedPane: focusedPane is null', chatPaneFocusIndex, chatPanes);
|
||||
return state;
|
||||
}
|
||||
|
||||
// Check if the conversation is already open in the focused pane.
|
||||
if (focusedPane.conversationId === conversationId) {
|
||||
if (DEBUG_PANES_MANAGER)
|
||||
console.log(`open-focuses: ${conversationId} is open in focused pane`, chatPaneFocusIndex, chatPanes);
|
||||
@@ -80,7 +108,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
const truncatedHistory = focusedPane.history.slice(0, focusedPane.historyIndex + 1);
|
||||
const newHistory = [...truncatedHistory, conversationId].slice(-MAX_HISTORY_LENGTH);
|
||||
|
||||
// Update the focused pane with the new conversation.
|
||||
// Update the focused pane with the new conversation and history.
|
||||
const newPanes = [...chatPanes];
|
||||
newPanes[chatPaneFocusIndex] = {
|
||||
...focusedPane,
|
||||
@@ -103,21 +131,30 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
// Open a conversation in a new pane, reusing an existing pane if possible.
|
||||
const { chatPanes, chatPaneFocusIndex, openConversationInFocusedPane } = _get();
|
||||
|
||||
// one pane open: split it
|
||||
if (chatPanes.length === 1) {
|
||||
_set({
|
||||
chatPanes: Array.from({ length: 2 }, () => ({ ...chatPanes[0] })),
|
||||
chatPaneFocusIndex: 1,
|
||||
});
|
||||
// Copy from the focused pane, if there's one
|
||||
const focusedPane = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex] ?? null : null;
|
||||
|
||||
// if fewer than the maximum panes, create a new pane and focus it
|
||||
if (chatPanes.length < MAX_CONCURRENT_PANES) {
|
||||
const insertIndex = chatPaneFocusIndex !== null ? chatPaneFocusIndex + 1 : chatPanes.length;
|
||||
_set((state) => ({
|
||||
chatPanes: [
|
||||
...state.chatPanes.slice(0, insertIndex),
|
||||
focusedPane ? duplicatePane(focusedPane) : createPane(null),
|
||||
...state.chatPanes.slice(insertIndex),
|
||||
],
|
||||
chatPaneFocusIndex: insertIndex,
|
||||
}));
|
||||
}
|
||||
// more than 2 panes, reuse the alt pane
|
||||
else if (chatPanes.length >= 2 && chatPaneFocusIndex !== null) {
|
||||
// max reached, replace the next pane (with wraparound) - note the outside logic won't get us here
|
||||
else {
|
||||
const replaceIndex = (chatPaneFocusIndex !== null ? chatPaneFocusIndex + 1 : 0) % MAX_CONCURRENT_PANES;
|
||||
_set({
|
||||
chatPaneFocusIndex: chatPaneFocusIndex === 0 ? 1 : 0,
|
||||
chatPaneFocusIndex: replaceIndex,
|
||||
});
|
||||
}
|
||||
|
||||
// will create a pane if none exists, or load the conversation in the focused pane
|
||||
// Open the conversation in the newly created or updated pane
|
||||
openConversationInFocusedPane(conversationId);
|
||||
|
||||
if (DEBUG_PANES_MANAGER)
|
||||
@@ -171,21 +208,18 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
|
||||
// Clone the pane at the specified index, including a deep copy of the history array
|
||||
const paneToDuplicate = chatPanes[_srcIndex];
|
||||
const duplicatedPane = {
|
||||
...paneToDuplicate,
|
||||
history: [...paneToDuplicate.history], // Deep copy of the history array
|
||||
};
|
||||
const dstIndex = _srcIndex + 1;
|
||||
|
||||
// Insert the duplicated pane into the array, right after the original pane
|
||||
const newPanes = [
|
||||
...chatPanes.slice(0, _srcIndex + 1),
|
||||
duplicatedPane,
|
||||
...chatPanes.slice(_srcIndex + 1),
|
||||
...chatPanes.slice(0, dstIndex),
|
||||
duplicatePane(paneToDuplicate),
|
||||
...chatPanes.slice(dstIndex),
|
||||
];
|
||||
|
||||
return {
|
||||
chatPanes: newPanes,
|
||||
chatPaneFocusIndex: _srcIndex + 1,
|
||||
chatPaneFocusIndex: dstIndex,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -217,7 +251,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
};
|
||||
}),
|
||||
|
||||
setFocusedPane: (paneIndex: number) =>
|
||||
setFocusedPaneIndex: (paneIndex: number) =>
|
||||
_set(state => {
|
||||
if (state.chatPaneFocusIndex === paneIndex)
|
||||
return state;
|
||||
@@ -232,7 +266,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
* It takes care of `creating the first pane` as well as `removing invalid history items, reassiging
|
||||
* conversationIds, and re-focusing the pane`.
|
||||
*/
|
||||
onConversationsChanged: (conversationIds: DConversationId[]) =>
|
||||
_onConversationsChanged: (conversationIds: DConversationId[]) =>
|
||||
_set(state => {
|
||||
const { chatPanes, chatPaneFocusIndex } = state;
|
||||
|
||||
@@ -284,7 +318,8 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
}),
|
||||
|
||||
}), {
|
||||
name: 'app-app-chat-panes',
|
||||
// note: added the '-2' suffix on 20240308 to invalidate the persisted state, as we are adding a paneId
|
||||
name: 'app-app-chat-panes-2',
|
||||
},
|
||||
));
|
||||
|
||||
@@ -294,40 +329,29 @@ export function getInstantAppChatPanesCount() {
|
||||
|
||||
export function usePanesManager() {
|
||||
// use Panes
|
||||
const { onConversationsChanged, ...panesFunctions } = useAppChatPanesStore(state => {
|
||||
const {
|
||||
chatPaneFocusIndex,
|
||||
chatPanes,
|
||||
navigateHistoryInFocusedPane,
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
} = state;
|
||||
const focusedConversationId = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex]?.conversationId ?? null : null;
|
||||
return {
|
||||
chatPanes: chatPanes as Readonly<ChatPane[]>,
|
||||
focusedConversationId,
|
||||
navigateHistoryInFocusedPane,
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
focusedPaneIndex: chatPaneFocusIndex,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
};
|
||||
}, shallow);
|
||||
const { _onConversationsChanged, ...panesFunctions } = useAppChatPanesStore(useShallow(state => ({
|
||||
// state
|
||||
chatPanes: state.chatPanes as Readonly<ChatPane[]>,
|
||||
focusedPaneIndex: state.chatPaneFocusIndex,
|
||||
focusedPaneConversationId: state.chatPaneFocusIndex !== null ? state.chatPanes[state.chatPaneFocusIndex]?.conversationId ?? null : null,
|
||||
// methods
|
||||
openConversationInFocusedPane: state.openConversationInFocusedPane,
|
||||
openConversationInSplitPane: state.openConversationInSplitPane,
|
||||
navigateHistoryInFocusedPane: state.navigateHistoryInFocusedPane,
|
||||
removePane: state.removePane,
|
||||
setFocusedPaneIndex: state.setFocusedPaneIndex,
|
||||
_onConversationsChanged: state._onConversationsChanged,
|
||||
})));
|
||||
|
||||
// use Conversation IDs[]
|
||||
const conversationIDs: DConversationId[] = useChatStore(state => {
|
||||
return state.conversations.map(_c => _c.id);
|
||||
}, shallow);
|
||||
const conversationIDs: DConversationId[] = useChatStore(useShallow(state =>
|
||||
state.conversations.map(_c => _c.id),
|
||||
));
|
||||
|
||||
// [Effect] Ensure all Panes have a valid Conversation ID
|
||||
React.useEffect(() => {
|
||||
onConversationsChanged(conversationIDs);
|
||||
}, [conversationIDs, onConversationsChanged]);
|
||||
_onConversationsChanged(conversationIDs);
|
||||
}, [conversationIDs, _onConversationsChanged]);
|
||||
|
||||
return {
|
||||
...panesFunctions,
|
||||
@@ -335,10 +359,12 @@ export function usePanesManager() {
|
||||
}
|
||||
|
||||
export function usePaneDuplicateOrClose() {
|
||||
return useAppChatPanesStore(state => ({
|
||||
canAddPane: state.chatPanes.length < 4,
|
||||
return useAppChatPanesStore(useShallow(state => ({
|
||||
// state
|
||||
canAddPane: state.chatPanes.length < MAX_CONCURRENT_PANES,
|
||||
isMultiPane: state.chatPanes.length > 1,
|
||||
// actions
|
||||
duplicateFocusedPane: state.duplicateFocusedPane,
|
||||
removeOtherPanes: state.removeOtherPanes,
|
||||
}), shallow);
|
||||
})));
|
||||
}
|
||||
@@ -1,34 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Alert, Avatar, Box, Button, Card, CardContent, Checkbox, IconButton, Input, List, ListItem, ListItemButton, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import EditNoteIcon from '@mui/icons-material/EditNote';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { SystemPurposeData, SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
|
||||
import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
|
||||
import { useChatLLM } from '~/modules/llms/store-llms';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { DConversationId, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
import { lineHeightTextareaMd } from '~/common/app.theme';
|
||||
import { navigateToPersonas } from '~/common/app.routes';
|
||||
import { useChipBoolean } from '~/common/components/useChipBoolean';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { SystemPurposeData, SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
import { YouTubeURLInput } from './YouTubeURLInput';
|
||||
import { usePurposeStore } from './store-purposes';
|
||||
|
||||
|
||||
// 'special' purpose IDs, for tile hiding purposes
|
||||
const PURPOSE_ID_PERSONA_CREATOR = '__persona-creator__';
|
||||
const TILE_ACTIVE_COLOR = 'primary' as const;
|
||||
|
||||
// defined looks
|
||||
const tileSize = 7.5; // rem
|
||||
const tileSize = 7; // rem
|
||||
const tileGap = 0.5; // rem
|
||||
|
||||
|
||||
@@ -46,32 +50,36 @@ function Tile(props: {
|
||||
return (
|
||||
<Button
|
||||
variant={(!props.isEditMode && props.isActive) ? 'solid' : props.isHighlighted ? 'soft' : 'soft'}
|
||||
color={(!props.isEditMode && props.isActive) ? 'primary' : props.isHighlighted ? 'primary' : 'neutral'}
|
||||
color={(!props.isEditMode && props.isActive) ? 'primary' : props.isHighlighted ? 'primary' : TILE_ACTIVE_COLOR}
|
||||
onClick={props.onClick}
|
||||
sx={{
|
||||
aspectRatio: 1,
|
||||
height: `${tileSize}rem`,
|
||||
fontWeight: 'md',
|
||||
lineHeight: 'xs',
|
||||
...((props.isEditMode || !props.isActive) ? {
|
||||
boxShadow: props.isHighlighted ? '0 2px 8px -2px rgb(var(--joy-palette-primary-mainChannel) / 50%)' : 'sm',
|
||||
backgroundColor: props.isHighlighted ? undefined : 'background.surface',
|
||||
...(props.imageUrl && {
|
||||
backgroundImage: `linear-gradient(rgba(255 255 255 /0.85), rgba(255 255 255 /1)), url(${props.imageUrl})`,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover',
|
||||
'&:hover': {
|
||||
backgroundImage: 'none',
|
||||
},
|
||||
}),
|
||||
boxShadow: `0 2px 8px -3px rgb(var(--joy-palette-${TILE_ACTIVE_COLOR}-darkChannel) / 30%)`,
|
||||
// boxShadow: props.isHighlighted
|
||||
// ? '0 2px 8px -2px rgb(var(--joy-palette-primary-darkChannel) / 30%)'
|
||||
// : 'sm',
|
||||
backgroundColor: props.isHighlighted ? undefined : 'background.popup',
|
||||
// ...(props.imageUrl && {
|
||||
// backgroundImage: `linear-gradient(rgba(255 255 255 /0.85), rgba(255 255 255 /1)), url(${props.imageUrl})`,
|
||||
// backgroundPosition: 'center',
|
||||
// backgroundSize: 'cover',
|
||||
// '&:hover': {
|
||||
// backgroundImage: 'none',
|
||||
// },
|
||||
// }),
|
||||
} : {}),
|
||||
flexDirection: 'column', gap: 1,
|
||||
flexDirection: 'column', gap: props.symbol === '🎭' ? 0.5 : 1.25, pt: 1.25,
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{/* [Edit mode checkbox] */}
|
||||
{props.isEditMode && (
|
||||
<Checkbox
|
||||
variant='soft' color='neutral'
|
||||
variant='soft' color={TILE_ACTIVE_COLOR}
|
||||
checked={!props.isHidden}
|
||||
// label={<Typography level='body-xs'>show</Typography>}
|
||||
sx={{ position: 'absolute', left: `${tileGap}rem`, top: `${tileGap}rem` }}
|
||||
@@ -111,6 +119,8 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [filteredIDs, setFilteredIDs] = React.useState<SystemPurposeId[] | null>(null);
|
||||
const [editMode, setEditMode] = React.useState(false);
|
||||
const [isYouTubeTranscriberActive, setIsYouTubeTranscriberActive] = React.useState(false);
|
||||
|
||||
|
||||
// external state
|
||||
const showFinder = useUIPreferencesStore(state => state.showPersonaFinder);
|
||||
@@ -148,11 +158,52 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
|
||||
// Handlers
|
||||
|
||||
// Modify the handlePurposeChanged function to check for the YouTube Transcriber
|
||||
const handlePurposeChanged = React.useCallback((purposeId: SystemPurposeId | null) => {
|
||||
if (purposeId && setSystemPurposeId)
|
||||
setSystemPurposeId(props.conversationId, purposeId);
|
||||
if (purposeId) {
|
||||
if (purposeId === 'YouTubeTranscriber') {
|
||||
// If the YouTube Transcriber tile is clicked, set the state accordingly
|
||||
setIsYouTubeTranscriberActive(true);
|
||||
} else {
|
||||
setIsYouTubeTranscriberActive(false);
|
||||
}
|
||||
if (setSystemPurposeId) {
|
||||
setSystemPurposeId(props.conversationId, purposeId);
|
||||
}
|
||||
}
|
||||
}, [props.conversationId, setSystemPurposeId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const isTranscriberActive = systemPurposeId === 'YouTubeTranscriber';
|
||||
setIsYouTubeTranscriberActive(isTranscriberActive);
|
||||
}, [systemPurposeId]);
|
||||
|
||||
|
||||
// Implement handleAddMessage function
|
||||
const handleAddMessage = (messageText: string) => {
|
||||
// Retrieve the appendMessage action from the useChatStore
|
||||
const { appendMessage } = useChatStore.getState();
|
||||
|
||||
const conversationId = props.conversationId;
|
||||
|
||||
// Create a new message object
|
||||
const newMessage: DMessage = {
|
||||
id: uuidv4(),
|
||||
text: messageText,
|
||||
sender: 'Bot',
|
||||
avatar: null,
|
||||
typing: false,
|
||||
role: 'assistant' as 'assistant',
|
||||
tokenCount: 0,
|
||||
created: Date.now(),
|
||||
updated: null,
|
||||
};
|
||||
|
||||
// Append the new message to the conversation
|
||||
appendMessage(conversationId, newMessage);
|
||||
};
|
||||
|
||||
|
||||
const handleCustomSystemMessageChange = React.useCallback((v: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
// TODO: persist this change? Right now it's reset every time.
|
||||
// maybe we shall have a "save" button just save on a state to persist between sessions
|
||||
@@ -259,7 +310,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
</Typography>
|
||||
<Tooltip disableInteractive title={editMode ? 'Done Editing' : 'Edit Tiles'}>
|
||||
<IconButton size='sm' onClick={toggleEditMode} sx={{ my: '-0.25rem' /* absorb the button padding */ }}>
|
||||
{editMode ? <DoneIcon /> : <EditIcon />}
|
||||
{editMode ? <DoneIcon /> : <EditRoundedIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@@ -293,6 +344,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
isHidden={hidePersonaCreator}
|
||||
onClick={() => editMode ? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR) : void navigateToPersonas()}
|
||||
sx={{
|
||||
fontSize: 'xs',
|
||||
boxShadow: 'xs',
|
||||
backgroundColor: 'neutral.softDisabledBg',
|
||||
}}
|
||||
@@ -326,24 +378,24 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
sx={{
|
||||
// example items 2-col layout
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${tileSize * 2 + 1}rem, 1fr))`,
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${tileSize * 3 + 1}rem, 1fr))`,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{fourExamples?.map((example, idx) => (
|
||||
<ListItem
|
||||
key={idx}
|
||||
variant='soft'
|
||||
variant='outlined'
|
||||
sx={{
|
||||
// padding: '0.25rem 0.5rem',
|
||||
backgroundColor: 'background.popup',
|
||||
borderRadius: 'md',
|
||||
// boxShadow: 'xs',
|
||||
padding: '0.25rem 0.5rem',
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: 'xs',
|
||||
'& svg': { opacity: 0.1, transition: 'opacity 0.2s' },
|
||||
'&:hover svg': { opacity: 1 },
|
||||
}}
|
||||
>
|
||||
<ListItemButton onClick={() => props.runExample(example)} sx={{ justifyContent: 'space-between' }}>
|
||||
<ListItemButton onClick={() => props.runExample(example)} sx={{ justifyContent: 'space-between', borderRadius: 'md' }}>
|
||||
<Typography level='body-sm'>
|
||||
{example}
|
||||
</Typography>
|
||||
@@ -412,6 +464,17 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* [row -1] YouTube URL */}
|
||||
{isYouTubeTranscriberActive && (
|
||||
<YouTubeURLInput
|
||||
onSubmit={(url) => handleAddMessage(url)}
|
||||
isFetching={false}
|
||||
sx={{
|
||||
gridColumn: '1 / -1',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, Input } from '@mui/joy';
|
||||
import YouTubeIcon from '@mui/icons-material/YouTube';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { useYouTubeTranscript, YTVideoTranscript } from '~/modules/youtube/useYouTubeTranscript';
|
||||
|
||||
|
||||
interface YouTubeURLInputProps {
|
||||
onSubmit: (transcript: string) => void;
|
||||
isFetching: boolean;
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
export const YouTubeURLInput: React.FC<YouTubeURLInputProps> = ({ onSubmit, isFetching, sx }) => {
|
||||
const [url, setUrl] = React.useState('');
|
||||
const [submitFlag, setSubmitFlag] = React.useState(false);
|
||||
|
||||
// Function to extract video ID from URL
|
||||
function extractVideoID(videoURL: string): string | null {
|
||||
const regExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^#&?]*).*/;
|
||||
const match = videoURL.match(regExp);
|
||||
return (match && match[1]?.length == 11) ? match[1] : null;
|
||||
}
|
||||
|
||||
const videoID = extractVideoID(url);
|
||||
|
||||
// Callback function to handle new transcript
|
||||
const handleNewTranscript = (newTranscript: YTVideoTranscript) => {
|
||||
onSubmit(newTranscript.transcript); // Pass the transcript text to the onSubmit handler
|
||||
setSubmitFlag(false); // Reset submit flag after handling
|
||||
};
|
||||
|
||||
const { transcript, isFetching: isTranscriptFetching, isError, error } = useYouTubeTranscript(videoID && submitFlag ? videoID : null, handleNewTranscript);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUrl(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault(); // Prevent form from causing a page reload
|
||||
setSubmitFlag(true); // Set flag to indicate a submit action
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 1, ...sx }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
required
|
||||
type='url'
|
||||
fullWidth
|
||||
disabled={isFetching || isTranscriptFetching}
|
||||
variant='outlined'
|
||||
placeholder='Enter YouTube Video URL'
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />}
|
||||
sx={{ mb: 1.5, backgroundColor: 'background.popup' }}
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='solid'
|
||||
disabled={isFetching || isTranscriptFetching || !url}
|
||||
loading={isFetching || isTranscriptFetching}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
Get Transcript
|
||||
</Button>
|
||||
{isError && <div>Error fetching transcript. Please try again.</div>}
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ export const usePurposeStore = create<PurposeStore>()(
|
||||
(set) => ({
|
||||
|
||||
// default state
|
||||
hiddenPurposeIDs: ['Developer', 'Designer'],
|
||||
hiddenPurposeIDs: ['Developer', 'Designer', 'YouTubeTranscriber'],
|
||||
|
||||
toggleHiddenPurposeId: (purposeId: string) => {
|
||||
set(state => {
|
||||
@@ -37,14 +37,19 @@ export const usePurposeStore = create<PurposeStore>()(
|
||||
|
||||
/* versioning:
|
||||
* 1: hide 'Developer' as 'DeveloperPreview' is best
|
||||
* 2: add a hidden 'YouTubeTranscriber' purpose
|
||||
*/
|
||||
version: 1,
|
||||
version: 2,
|
||||
|
||||
migrate: (state: any, fromVersion: number): PurposeStore => {
|
||||
// 0 -> 1: rename 'enterToSend' to 'enterIsNewline' (flip the meaning)
|
||||
if (state && fromVersion === 0)
|
||||
if (!state.hiddenPurposeIDs.includes('Developer'))
|
||||
state.hiddenPurposeIDs.push('Developer');
|
||||
// 1 -> 2: add a hidden 'YouTubeTranscriber' purpose
|
||||
if (state && fromVersion === 1)
|
||||
if (!state.hiddenPurposeIDs.includes('YouTubeTranscriber'))
|
||||
state.hiddenPurposeIDs.push('YouTubeTranscriber');
|
||||
return state;
|
||||
},
|
||||
}),
|
||||
|
||||
+67
-39
@@ -1,7 +1,7 @@
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import type { DFolder } from '~/common/state/store-folders';
|
||||
import { conversationTitle, DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { conversationTitle, DConversationId, DMessageUserFlag, messageHasUserFlag, messageUserFlagToEmoji, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
import type { ChatNavigationItemData } from './ChatDrawerItem';
|
||||
|
||||
@@ -12,6 +12,8 @@ const SEARCH_MIN_CHARS = 3;
|
||||
|
||||
export type ChatNavGrouping = false | 'date' | 'persona';
|
||||
|
||||
export type ChatSearchSorting = 'frequency' | 'date';
|
||||
|
||||
interface ChatNavigationGroupData {
|
||||
type: 'nav-item-group',
|
||||
title: string,
|
||||
@@ -66,18 +68,28 @@ function getTimeBucketEn(currentTime: number, midnightTime: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function isDrawerSearching(filterByQuery: string): { isSearching: boolean, lcTextQuery: string } {
|
||||
const lcTextQuery = filterByQuery.trim().toLowerCase();
|
||||
return {
|
||||
isSearching: lcTextQuery.length >= SEARCH_MIN_CHARS,
|
||||
lcTextQuery,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Optimization: return a reduced version of the DConversation object for 'Drawer Items' purposes,
|
||||
* to avoid unnecessary re-renders on each new character typed by the assistant
|
||||
*/
|
||||
export function useChatNavRenderItems(
|
||||
export function useChatDrawerRenderItems(
|
||||
activeConversationId: DConversationId | null,
|
||||
chatPanesConversationIds: DConversationId[],
|
||||
filterByQuery: string,
|
||||
activeFolder: DFolder | null,
|
||||
allFolders: DFolder[],
|
||||
filterHasStars: boolean,
|
||||
grouping: ChatNavGrouping,
|
||||
searchSorting: ChatSearchSorting,
|
||||
showRelativeSize: boolean,
|
||||
): {
|
||||
renderNavItems: ChatRenderItemData[],
|
||||
@@ -93,50 +105,59 @@ export function useChatNavRenderItems(
|
||||
const selectedConversations = !activeFolder ? conversations : conversations.filter(_c => activeFolder.conversationIds.includes(_c.id));
|
||||
|
||||
// filter 2: preparation: lowercase the query
|
||||
const lcTextQuery = filterByQuery.trim().toLowerCase();
|
||||
const isSearching = lcTextQuery.length >= SEARCH_MIN_CHARS;
|
||||
const { isSearching, lcTextQuery } = isDrawerSearching(filterByQuery);
|
||||
|
||||
// transform (the conversations into ChatNavigationItemData) + filter2 (if searching)
|
||||
const chatNavItems = selectedConversations.map((_c): ChatNavigationItemData => {
|
||||
// rich properties
|
||||
const title = conversationTitle(_c);
|
||||
const isAlsoOpen = findOpenInViewNumbers(chatPanesConversationIds, _c.id);
|
||||
const chatNavItems = selectedConversations
|
||||
.filter(_c => !filterHasStars || _c.messages.some(m => messageHasUserFlag(m, 'starred')))
|
||||
.map((_c): ChatNavigationItemData => {
|
||||
// rich properties
|
||||
const title = conversationTitle(_c);
|
||||
const isAlsoOpen = findOpenInViewNumbers(chatPanesConversationIds, _c.id);
|
||||
|
||||
// set the frequency counters if filtering is enabled
|
||||
let searchFrequency: number = 0;
|
||||
if (isSearching) {
|
||||
const titleFrequency = title.toLowerCase().split(lcTextQuery).length - 1;
|
||||
const messageFrequency = _c.messages.reduce((count, message) => count + (message.text.toLowerCase().split(lcTextQuery).length - 1), 0);
|
||||
searchFrequency = titleFrequency + messageFrequency;
|
||||
}
|
||||
// set the frequency counters if filtering is enabled
|
||||
let searchFrequency: number = 0;
|
||||
if (isSearching) {
|
||||
const titleFrequency = title.toLowerCase().split(lcTextQuery).length - 1;
|
||||
const messageFrequency = _c.messages.reduce((count, message) => count + (message.text.toLowerCase().split(lcTextQuery).length - 1), 0);
|
||||
searchFrequency = titleFrequency + messageFrequency;
|
||||
}
|
||||
|
||||
// create the ChatNavigationData
|
||||
return {
|
||||
type: 'nav-item-chat-data',
|
||||
conversationId: _c.id,
|
||||
isActive: _c.id === activeConversationId,
|
||||
isAlsoOpen,
|
||||
isEmpty: !_c.messages.length && !_c.userTitle,
|
||||
title,
|
||||
folder: !allFolders.length
|
||||
? undefined // don't show folder select if folders are disabled
|
||||
: _c.id === activeConversationId // only show the folder for active conversation(s)
|
||||
? allFolders.find(folder => folder.conversationIds.includes(_c.id)) ?? null
|
||||
: null,
|
||||
updatedAt: _c.updated || _c.created || 0,
|
||||
messageCount: _c.messages.length,
|
||||
assistantTyping: !!_c.abortController,
|
||||
systemPurposeId: _c.systemPurposeId,
|
||||
searchFrequency,
|
||||
};
|
||||
}).filter(item => !isSearching || item.searchFrequency > 0);
|
||||
// union of message flags -> emoji string
|
||||
const allFlags = new Set<DMessageUserFlag>();
|
||||
_c.messages.forEach(_m => _m.userFlags?.forEach(flag => allFlags.add(flag)));
|
||||
const userFlagsSummary = !allFlags.size ? undefined : Array.from(allFlags).map(messageUserFlagToEmoji).join('');
|
||||
|
||||
// create the ChatNavigationData
|
||||
return {
|
||||
type: 'nav-item-chat-data',
|
||||
conversationId: _c.id,
|
||||
isActive: _c.id === activeConversationId,
|
||||
isAlsoOpen,
|
||||
isEmpty: !_c.messages.length && !_c.userTitle,
|
||||
title,
|
||||
userFlagsSummary,
|
||||
folder: !allFolders.length
|
||||
? undefined // don't show folder select if folders are disabled
|
||||
: _c.id === activeConversationId // only show the folder for active conversation(s)
|
||||
? allFolders.find(folder => folder.conversationIds.includes(_c.id)) ?? null
|
||||
: null,
|
||||
updatedAt: _c.updated || _c.created || 0,
|
||||
messageCount: _c.messages.length,
|
||||
assistantTyping: !!_c.abortController,
|
||||
systemPurposeId: _c.systemPurposeId,
|
||||
searchFrequency,
|
||||
};
|
||||
})
|
||||
.filter(item => !isSearching || item.searchFrequency > 0);
|
||||
|
||||
// check if the active conversation has an item in the list
|
||||
const filteredChatsIncludeActive = chatNavItems.some(_c => _c.conversationId === activeConversationId);
|
||||
|
||||
|
||||
// [sort by frequency, don't group] if there's a search query
|
||||
chatNavItems.sort((a, b) => b.searchFrequency - a.searchFrequency);
|
||||
if (isSearching && searchSorting === 'frequency')
|
||||
chatNavItems.sort((a, b) => b.searchFrequency - a.searchFrequency);
|
||||
|
||||
// Render List
|
||||
let renderNavItems: ChatRenderItemData[] = chatNavItems;
|
||||
@@ -179,7 +200,12 @@ export function useChatNavRenderItems(
|
||||
|
||||
// [empty message] if there are no items
|
||||
if (!renderNavItems.length)
|
||||
renderNavItems.push({ type: 'nav-item-info-message', message: isSearching ? 'No results found' : 'No conversations in folder' });
|
||||
renderNavItems.push({
|
||||
type: 'nav-item-info-message',
|
||||
message: filterHasStars ? 'No starred results'
|
||||
: isSearching ? 'No results found'
|
||||
: 'No conversations in folder',
|
||||
});
|
||||
|
||||
// other derived state
|
||||
const filteredChatIDs = chatNavItems.map(_c => _c.conversationId);
|
||||
@@ -203,8 +229,10 @@ export function useChatNavRenderItems(
|
||||
return a.renderNavItems.length === b.renderNavItems.length
|
||||
&& a.renderNavItems.every((_a, i) => shallow(_a, b.renderNavItems[i]))
|
||||
&& shallow(a.filteredChatIDs, b.filteredChatIDs)
|
||||
// we also compare this, as it changes with a parameter
|
||||
&& a.filteredChatsBarBasis === b.filteredChatsBarBasis;
|
||||
&& a.filteredChatsCount === b.filteredChatsCount
|
||||
&& a.filteredChatsAreEmpty === b.filteredChatsAreEmpty
|
||||
&& a.filteredChatsBarBasis === b.filteredChatsBarBasis
|
||||
&& a.filteredChatsIncludeActive === b.filteredChatsIncludeActive;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { getChatLLMId } from '~/modules/llms/store-llms';
|
||||
import { updateHistoryForReplyTo } from '~/modules/aifn/replyto/replyTo';
|
||||
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { createDMessage, DConversationId, DMessage, getConversationSystemPurposeId } from '~/common/state/store-chats';
|
||||
import { getUXLabsHighPerformance } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { extractChatCommand, findAllChatCommands } from '../commands/commands.registry';
|
||||
import { getInstantAppChatPanesCount } from '../components/panes/usePanesManager';
|
||||
|
||||
import { runAssistantUpdatingState } from './chat-stream';
|
||||
import { runBrowseGetPageUpdatingState } from './browse-load';
|
||||
import { runImageGenerationUpdatingState } from './image-generate';
|
||||
import { runReActUpdatingState } from './react-tangent';
|
||||
|
||||
import type { ChatModeId } from '../AppChat';
|
||||
|
||||
|
||||
export async function _handleExecute(chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) {
|
||||
|
||||
// Handle missing conversation
|
||||
if (!conversationId)
|
||||
return 'err-no-conversation';
|
||||
|
||||
const chatLLMId = getChatLLMId();
|
||||
|
||||
// Update the system message from the active persona to the history
|
||||
// NOTE: this does NOT call setMessages anymore (optimization). make sure to:
|
||||
// 1. all the callers need to pass a new array
|
||||
// 2. all the exit points need to call setMessages
|
||||
const cHandler = ConversationsManager.getHandler(conversationId);
|
||||
cHandler.inlineUpdatePurposeInHistory(history, chatLLMId || undefined);
|
||||
|
||||
// FIXME: shouldn't do this for all the code paths. The advantage for having it here (vs Composer output only) is re-executing history
|
||||
// TODO: move this to the server side after transferring metadata?
|
||||
updateHistoryForReplyTo(history);
|
||||
|
||||
// Handle unconfigured
|
||||
if (!chatLLMId || !chatModeId) {
|
||||
// set the history (e.g. the updated system prompt and the user prompt) at least, see #523
|
||||
cHandler.messagesReplace(history);
|
||||
return !chatLLMId ? 'err-no-chatllm' : 'err-no-chatmode';
|
||||
}
|
||||
|
||||
// Valid /commands are intercepted here, and override chat modes, generally for mechanics or sidebars
|
||||
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
|
||||
if (lastMessage?.role === 'user') {
|
||||
const chatCommand = extractChatCommand(lastMessage.text)[0];
|
||||
if (chatCommand && chatCommand.type === 'cmd') {
|
||||
switch (chatCommand.providerId) {
|
||||
case 'ass-browse':
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runBrowseGetPageUpdatingState(cHandler, chatCommand.params);
|
||||
|
||||
case 'ass-t2i':
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runImageGenerationUpdatingState(cHandler, chatCommand.params);
|
||||
|
||||
case 'ass-react':
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runReActUpdatingState(cHandler, chatCommand.params, chatLLMId);
|
||||
|
||||
case 'chat-alter':
|
||||
// /clear
|
||||
if (chatCommand.command === '/clear') {
|
||||
if (chatCommand.params === 'all') {
|
||||
cHandler.messagesReplace([]);
|
||||
} else {
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Issue: this command requires the \'all\' parameter to confirm the operation.', undefined, 'issue', false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// /assistant, /system
|
||||
Object.assign(lastMessage, {
|
||||
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
|
||||
sender: 'Bot',
|
||||
text: chatCommand.params || '',
|
||||
} satisfies Partial<DMessage>);
|
||||
cHandler.messagesReplace(history);
|
||||
return true;
|
||||
|
||||
case 'cmd-help':
|
||||
const chatCommandsText = findAllChatCommands()
|
||||
.map(cmd => ` - ${cmd.primary}` + (cmd.alternatives?.length ? ` (${cmd.alternatives.join(', ')})` : '') + `: ${cmd.description}`)
|
||||
.join('\n');
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Available Chat Commands:\n' + chatCommandsText, undefined, 'help', false);
|
||||
return true;
|
||||
|
||||
case 'mode-beam':
|
||||
if (chatCommand.isError) {
|
||||
cHandler.messagesReplace(history);
|
||||
return false;
|
||||
}
|
||||
// remove '/beam ', as we want to be a user chat message
|
||||
Object.assign(lastMessage, { text: chatCommand.params || '' });
|
||||
cHandler.messagesReplace(history);
|
||||
ConversationsManager.getHandler(conversationId).beamInvoke(history, [], null);
|
||||
return true;
|
||||
|
||||
default:
|
||||
cHandler.messagesReplace([...history, createDMessage('assistant', 'This command is not supported.')]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get the system purpose (note: we don't react to it, or it would invalidate half UI components..)
|
||||
if (!getConversationSystemPurposeId(conversationId)) {
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Issue: no Persona selected.', undefined, 'issue', false);
|
||||
return 'err-no-persona';
|
||||
}
|
||||
|
||||
// synchronous long-duration tasks, which update the state as they go
|
||||
switch (chatModeId) {
|
||||
case 'generate-text':
|
||||
cHandler.messagesReplace(history);
|
||||
return await runAssistantUpdatingState(conversationId, history, chatLLMId, getUXLabsHighPerformance() ? 0 : getInstantAppChatPanesCount());
|
||||
|
||||
case 'generate-text-beam':
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.beamInvoke(history, [], null);
|
||||
return true;
|
||||
|
||||
case 'append-user':
|
||||
cHandler.messagesReplace(history);
|
||||
return true;
|
||||
|
||||
case 'generate-image':
|
||||
if (!lastMessage?.text) break;
|
||||
// also add a 'fake' user message with the '/draw' command
|
||||
cHandler.messagesReplace(history.map(message => (message.id !== lastMessage.id) ? message : {
|
||||
...message,
|
||||
text: `/draw ${lastMessage.text}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(cHandler, lastMessage.text);
|
||||
|
||||
case 'generate-react':
|
||||
if (!lastMessage?.text) break;
|
||||
cHandler.messagesReplace(history);
|
||||
return await runReActUpdatingState(cHandler, lastMessage.text, chatLLMId);
|
||||
}
|
||||
|
||||
// ISSUE: if we're here, it means we couldn't do the job, at least sync the history
|
||||
console.log('Chat execute: issue running', chatModeId, conversationId, lastMessage);
|
||||
cHandler.messagesReplace(history);
|
||||
return false;
|
||||
}
|
||||
@@ -1,20 +1,26 @@
|
||||
import { callBrowseFetchPage } from '~/modules/browse/browse.client';
|
||||
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
|
||||
|
||||
export const runBrowseGetPageUpdatingState = async (conversationId: string, url: string) => {
|
||||
const cHandler = ConversationManager.getHandler(conversationId);
|
||||
export const runBrowseGetPageUpdatingState = async (cHandler: ConversationHandler, url?: string) => {
|
||||
if (!url) {
|
||||
cHandler.messageAppendAssistant('Issue: no URL provided.', undefined, 'issue', false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// noinspection HttpUrlsUsage
|
||||
const shortUrl = url.replace('https://www.', '').replace('https://', '').replace('http://', '').replace('www.', '');
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(`Loading page at ${shortUrl}...`, 'web', undefined);
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(`Loading page at ${shortUrl}...`, undefined, 'web', true);
|
||||
|
||||
try {
|
||||
const page = await callBrowseFetchPage(url);
|
||||
cHandler.messageEdit(assistantMessageId, { text: page.content || 'Issue: page load did not produce an answer: no text found', typing: false }, true);
|
||||
const pageContent = page.content.markdown || page.content.text || page.content.html || 'Issue: page load did not produce an answer: no text found';
|
||||
cHandler.messageEdit(assistantMessageId, { text: pageContent, typing: false }, true);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
cHandler.messageEdit(assistantMessageId, { text: 'Issue: browse did not produce an answer (error: ' + (error?.message || error?.toString() || 'unknown') + ').', typing: false }, true);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,40 +1,41 @@
|
||||
import type { DLLMId } from '~/modules/llms/store-llms';
|
||||
import type { StreamingClientUpdate } from '~/modules/llms/vendors/unifiedStreamingClient';
|
||||
import { SystemPurposeId } from '../../../data';
|
||||
import { autoSuggestions } from '~/modules/aifn/autosuggestions/autoSuggestions';
|
||||
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
import { llmStreamingChatGenerate } from '~/modules/llms/llm.client';
|
||||
import { llmStreamingChatGenerate, VChatContextRef, VChatMessageIn, VChatStreamContextName } from '~/modules/llms/llm.client';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
|
||||
import type { DMessage } from '~/common/state/store-chats';
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
|
||||
import { ChatAutoSpeakType, getChatAutoAI } from '../store-app-chat';
|
||||
|
||||
|
||||
export const STREAM_TEXT_INDICATOR = '...';
|
||||
|
||||
|
||||
/**
|
||||
* The main "chat" function. TODO: this is here so we can soon move it to the data model.
|
||||
*/
|
||||
export async function runAssistantUpdatingState(conversationId: string, history: DMessage[], assistantLlmId: DLLMId, systemPurpose: SystemPurposeId, parallelViewCount: number) {
|
||||
const cHandler = ConversationManager.getHandler(conversationId);
|
||||
export async function runAssistantUpdatingState(conversationId: string, history: DMessage[], assistantLlmId: DLLMId, parallelViewCount: number) {
|
||||
const cHandler = ConversationsManager.getHandler(conversationId);
|
||||
|
||||
// ai follow-up operations (fire/forget)
|
||||
const { autoSpeak, autoSuggestDiagrams, autoSuggestQuestions, autoTitleChat } = getChatAutoAI();
|
||||
|
||||
// update the system message from the active Purpose, if not manually edited
|
||||
history = cHandler.resyncPurposeInHistory(history, assistantLlmId, systemPurpose);
|
||||
|
||||
// create a blank and 'typing' message for the assistant
|
||||
const assistantMessageId = cHandler.messageAppendAssistant('...', assistantLlmId, history[0].purposeId);
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(STREAM_TEXT_INDICATOR, history[0].purposeId, assistantLlmId, true);
|
||||
|
||||
// when an abort controller is set, the UI switches to the "stop" mode
|
||||
const abortController = new AbortController();
|
||||
cHandler.setAbortController(abortController);
|
||||
|
||||
// stream the assistant's messages
|
||||
await streamAssistantMessage(
|
||||
const messageStatus = await streamAssistantMessage(
|
||||
assistantLlmId,
|
||||
history,
|
||||
history.map((m): VChatMessageIn => ({ role: m.role, content: m.text })),
|
||||
'conversation',
|
||||
conversationId,
|
||||
parallelViewCount,
|
||||
autoSpeak,
|
||||
(update) => cHandler.messageEdit(assistantMessageId, update, false),
|
||||
@@ -42,6 +43,7 @@ export async function runAssistantUpdatingState(conversationId: string, history:
|
||||
);
|
||||
|
||||
// clear to send, again
|
||||
// FIXME: race condition?
|
||||
cHandler.setAbortController(null);
|
||||
|
||||
if (autoTitleChat) {
|
||||
@@ -51,24 +53,32 @@ export async function runAssistantUpdatingState(conversationId: string, history:
|
||||
|
||||
if (autoSuggestDiagrams || autoSuggestQuestions)
|
||||
autoSuggestions(conversationId, assistantMessageId, autoSuggestDiagrams, autoSuggestQuestions);
|
||||
|
||||
return messageStatus.outcome === 'success';
|
||||
}
|
||||
|
||||
type StreamMessageOutcome = 'success' | 'aborted' | 'errored';
|
||||
type StreamMessageStatus = { outcome: StreamMessageOutcome, errorMessage?: string };
|
||||
|
||||
async function streamAssistantMessage(
|
||||
export async function streamAssistantMessage(
|
||||
llmId: DLLMId,
|
||||
history: DMessage[],
|
||||
messagesHistory: VChatMessageIn[],
|
||||
contextName: VChatStreamContextName,
|
||||
contextRef: VChatContextRef,
|
||||
throttleUnits: number, // 0: disable, 1: default throttle (12Hz), 2+ reduce the message frequency with the square root
|
||||
autoSpeak: ChatAutoSpeakType,
|
||||
editMessage: (update: Partial<DMessage>) => void,
|
||||
abortSignal: AbortSignal,
|
||||
) {
|
||||
): Promise<StreamMessageStatus> {
|
||||
|
||||
const returnStatus: StreamMessageStatus = {
|
||||
outcome: 'success',
|
||||
errorMessage: undefined,
|
||||
};
|
||||
|
||||
// speak once
|
||||
let spokenLine = false;
|
||||
|
||||
const messages = history.map(({ role, text }) => ({ role, content: text }));
|
||||
|
||||
|
||||
// Throttling setup
|
||||
let lastCallTime = 0;
|
||||
let throttleDelay = 1000 / 12; // 12 messages per second works well for 60Hz displays (single chat, and 24 in 4 chats, see the square root below)
|
||||
@@ -86,7 +96,7 @@ async function streamAssistantMessage(
|
||||
const incrementalAnswer: Partial<DMessage> = { text: '' };
|
||||
|
||||
try {
|
||||
await llmStreamingChatGenerate(llmId, messages, null, null, abortSignal, (update: StreamingClientUpdate) => {
|
||||
await llmStreamingChatGenerate(llmId, messagesHistory, contextName, contextRef, null, null, abortSignal, (update: StreamingClientUpdate) => {
|
||||
const textSoFar = update.textSoFar;
|
||||
|
||||
// grow the incremental message
|
||||
@@ -116,7 +126,10 @@ async function streamAssistantMessage(
|
||||
console.error('Fetch request error:', error);
|
||||
const errorText = ` [Issue: ${error.message || (typeof error === 'string' ? error : 'Chat stopped.')}]`;
|
||||
incrementalAnswer.text = (incrementalAnswer.text || '') + errorText;
|
||||
}
|
||||
returnStatus.outcome = 'errored';
|
||||
returnStatus.errorMessage = error.message;
|
||||
} else
|
||||
returnStatus.outcome = 'aborted';
|
||||
}
|
||||
|
||||
// Optimized:
|
||||
@@ -127,4 +140,6 @@ async function streamAssistantMessage(
|
||||
// 📢 TTS: all
|
||||
if ((autoSpeak === 'all' || autoSpeak === 'firstLine') && incrementalAnswer.text && !spokenLine && !abortSignal.aborted)
|
||||
void speakText(incrementalAnswer.text);
|
||||
|
||||
return returnStatus;
|
||||
}
|
||||
@@ -1,23 +1,25 @@
|
||||
import { getActiveTextToImageProviderOrThrow, t2iGenerateImageOrThrow } from '~/modules/t2i/t2i.client';
|
||||
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import { TextToImageProvider } from '~/common/components/useCapabilities';
|
||||
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
import type { TextToImageProvider } from '~/common/components/useCapabilities';
|
||||
|
||||
|
||||
/**
|
||||
* Text to image, appended as an 'assistant' message
|
||||
*/
|
||||
export async function runImageGenerationUpdatingState(conversationId: string, imageText: string) {
|
||||
const handler = ConversationManager.getHandler(conversationId);
|
||||
export async function runImageGenerationUpdatingState(cHandler: ConversationHandler, imageText?: string) {
|
||||
if (!imageText) {
|
||||
cHandler.messageAppendAssistant('Issue: no image description provided.', undefined, 'issue', false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Acquire the active TextToImageProvider
|
||||
let t2iProvider: TextToImageProvider | undefined = undefined;
|
||||
try {
|
||||
t2iProvider = getActiveTextToImageProviderOrThrow();
|
||||
} catch (error: any) {
|
||||
const assistantErrorMessageId = handler.messageAppendAssistant(`[Issue] Sorry, I can't generate images right now. ${error?.message || error?.toString() || 'Unknown error'}.`, 'issue', undefined);
|
||||
handler.messageEdit(assistantErrorMessageId, { typing: false }, true);
|
||||
return;
|
||||
cHandler.messageAppendAssistant(`[Issue] Sorry, I can't generate images right now. ${error?.message || error?.toString() || 'Unknown error'}.`, undefined, 'issue', false);
|
||||
return 'err-t2i-unconfigured';
|
||||
}
|
||||
|
||||
// if the imageText ends with " xN" or " [N]" (where N is a number), then we'll generate N images
|
||||
@@ -26,17 +28,18 @@ export async function runImageGenerationUpdatingState(conversationId: string, im
|
||||
if (repeat > 1)
|
||||
imageText = imageText.replace(/x(\d+)$|\[(\d+)]$/, '').trim(); // Remove the "xN" or "[N]" part from the imageText
|
||||
|
||||
const assistantMessageId = handler.messageAppendAssistant(
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(
|
||||
`Give me ${t2iProvider.vendor === 'openai' ? 'a dozen' : 'a few'} seconds while I draw ${imageText?.length > 20 ? 'that' : '"' + imageText + '"'}...`,
|
||||
'', undefined,
|
||||
undefined, t2iProvider.painter, true,
|
||||
);
|
||||
handler.messageEdit(assistantMessageId, { originLLM: t2iProvider.painter }, false);
|
||||
|
||||
try {
|
||||
const imageUrls = await t2iGenerateImageOrThrow(t2iProvider, imageText, repeat);
|
||||
handler.messageEdit(assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
|
||||
cHandler.messageEdit(assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || error?.toString() || 'Unknown error';
|
||||
handler.messageEdit(assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
|
||||
cHandler.messageEdit(assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import { Agent } from '~/modules/aifn/react/react';
|
||||
import { DLLMId } from '~/modules/llms/store-llms';
|
||||
import { useBrowseStore } from '~/modules/browse/store-module-browsing';
|
||||
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
|
||||
import { STREAM_TEXT_INDICATOR } from './chat-stream';
|
||||
|
||||
const EPHEMERAL_DELETION_DELAY = 5 * 1000;
|
||||
|
||||
@@ -10,12 +12,15 @@ const EPHEMERAL_DELETION_DELAY = 5 * 1000;
|
||||
/**
|
||||
* Synchronous ReAct chat function - TODO: event loop, auto-ui, cleanups, etc.
|
||||
*/
|
||||
export async function runReActUpdatingState(conversationId: string, question: string, assistantLlmId: DLLMId) {
|
||||
const cHandler = ConversationManager.getHandler(conversationId);
|
||||
export async function runReActUpdatingState(cHandler: ConversationHandler, question: string | undefined, assistantLlmId: DLLMId) {
|
||||
if (!question) {
|
||||
cHandler.messageAppendAssistant('Issue: no question provided.', undefined, 'issue', false);
|
||||
return false;
|
||||
}
|
||||
|
||||
// create a blank and 'typing' message for the assistant - to be filled when we're done
|
||||
const assistantModelLabel = 'react-' + assistantLlmId.slice(4, 7); // HACK: this is used to change the Avatar animation
|
||||
const assistantMessageId = cHandler.messageAppendAssistant('...', assistantModelLabel, undefined);
|
||||
const assistantModelLabel = 'react-' + assistantLlmId; //.slice(4, 7); // HACK: this is used to change the Avatar animation
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(STREAM_TEXT_INDICATOR, undefined, assistantModelLabel, true);
|
||||
const { enableReactTool: enableBrowse } = useBrowseStore.getState();
|
||||
|
||||
// create an ephemeral space
|
||||
@@ -37,9 +42,11 @@ export async function runReActUpdatingState(conversationId: string, question: st
|
||||
cHandler.messageEdit(assistantMessageId, { text: reactResult, typing: false }, false);
|
||||
setTimeout(() => eHandler.delete(), EPHEMERAL_DELETION_DELAY);
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
logToEphemeral(ephemeralText + `\nIssue: ${error || 'unknown'}`);
|
||||
cHandler.messageEdit(assistantMessageId, { text: 'Issue: ReAct did not produce an answer.', typing: false }, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
|
||||
export type ChatAutoSpeakType = 'off' | 'firstLine' | 'all';
|
||||
@@ -26,9 +27,15 @@ interface AppChatStore {
|
||||
|
||||
// chat UI
|
||||
|
||||
filterHasStars: boolean;
|
||||
setFilterHasStars: (filterHasStars: boolean) => void;
|
||||
|
||||
micTimeoutMs: number;
|
||||
setMicTimeoutMs: (micTimeoutMs: number) => void;
|
||||
|
||||
showPersonaIcons: boolean;
|
||||
setShowPersonaIcons: (showPersonaIcons: boolean) => void;
|
||||
|
||||
showRelativeSize: boolean;
|
||||
setShowRelativeSize: (showRelativeSize: boolean) => void;
|
||||
|
||||
@@ -56,9 +63,15 @@ const useAppChatStore = create<AppChatStore>()(persist(
|
||||
autoTitleChat: true,
|
||||
setAutoTitleChat: (autoTitleChat: boolean) => _set({ autoTitleChat }),
|
||||
|
||||
filterHasStars: false,
|
||||
setFilterHasStars: (filterHasStars: boolean) => _set({ filterHasStars }),
|
||||
|
||||
micTimeoutMs: 2000,
|
||||
setMicTimeoutMs: (micTimeoutMs: number) => _set({ micTimeoutMs }),
|
||||
|
||||
showPersonaIcons: true,
|
||||
setShowPersonaIcons: (showPersonaIcons: boolean) => _set({ showPersonaIcons }),
|
||||
|
||||
showRelativeSize: false,
|
||||
setShowRelativeSize: (showRelativeSize: boolean) => _set({ showRelativeSize }),
|
||||
|
||||
@@ -113,10 +126,18 @@ export const useChatMicTimeoutMsValue = (): number =>
|
||||
export const useChatMicTimeoutMs = (): [number, (micTimeoutMs: number) => void] =>
|
||||
useAppChatStore(state => [state.micTimeoutMs, state.setMicTimeoutMs], shallow);
|
||||
|
||||
export const useChatShowRelativeSize = (): { showRelativeSize: boolean, toggleRelativeSize: () => void } => {
|
||||
const showRelativeSize = useAppChatStore(state => state.showRelativeSize);
|
||||
const toggleRelativeSize = () => useAppChatStore.getState().setShowRelativeSize(!showRelativeSize);
|
||||
return { showRelativeSize, toggleRelativeSize };
|
||||
export const useChatDrawerFilters = () => {
|
||||
const values = useAppChatStore(useShallow(state => ({
|
||||
filterHasStars: state.filterHasStars,
|
||||
showPersonaIcons: state.showPersonaIcons,
|
||||
showRelativeSize: state.showRelativeSize,
|
||||
})));
|
||||
return {
|
||||
...values,
|
||||
toggleFilterHasStars: () => useAppChatStore.getState().setFilterHasStars(!values.filterHasStars),
|
||||
toggleShowPersonaIcons: () => useAppChatStore.getState().setShowPersonaIcons(!values.showPersonaIcons),
|
||||
toggleShowRelativeSize: () => useAppChatStore.getState().setShowRelativeSize(!values.showRelativeSize),
|
||||
};
|
||||
};
|
||||
|
||||
export const useChatShowTextDiff = (): [boolean, (showDiff: boolean) => void] =>
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
export function Gallery() {
|
||||
return (
|
||||
|
||||
<AppPlaceholder text='Drawing App is under development. v1.13 or v1.14.' />
|
||||
<AppPlaceholder text='Drawing App is under development. v1.16.' />
|
||||
|
||||
);
|
||||
}
|
||||
@@ -137,7 +137,9 @@ export function TextToImage(props: {
|
||||
|
||||
// layout
|
||||
display: 'grid',
|
||||
gridTemplateColumns: props.isMobile ? 'repeat(auto-fit, minmax(320px, 1fr))' : 'repeat(auto-fit, minmax(400px, 1fr))',
|
||||
gridTemplateColumns: props.isMobile
|
||||
? 'repeat(auto-fit, minmax(320px, 1fr))'
|
||||
: 'repeat(auto-fit, minmax(max(min(100%, 400px), 100%/5), 1fr))',
|
||||
gap: { xs: 2, md: 2 },
|
||||
}}>
|
||||
{prompts.map((prompt, index) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, ButtonGroup, IconButton, Tooltip } from '@mui/joy';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import LightbulbOutlinedIcon from '@mui/icons-material/LightbulbOutlined';
|
||||
|
||||
// const desktopButtonLegend =
|
||||
@@ -48,7 +48,7 @@ export function ButtonPromptFromIdea(props: {
|
||||
</Button>
|
||||
<Tooltip disableInteractive title='Use Idea'>
|
||||
<IconButton size='sm' onClick={onIdeaUse}>
|
||||
<ArrowForwardIcon />
|
||||
<ArrowForwardRoundedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, ButtonGroup, Chip, Divider, IconButton, Typography } from '@mui/joy';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
|
||||
import { niceShadowKeyframes } from '../../call/Contacts';
|
||||
import { animationShadowRingLimey } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
export function DrawHeading(props: {
|
||||
@@ -30,9 +30,9 @@ export function DrawHeading(props: {
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: 'background.popup',
|
||||
animation: `${niceShadowKeyframes} 5s infinite`,
|
||||
animation: `${animationShadowRingLimey} 5s infinite`,
|
||||
}}>
|
||||
<FormatPaintIcon />
|
||||
<FormatPaintTwoToneIcon />
|
||||
</IconButton>
|
||||
|
||||
{/* Messaging */}
|
||||
|
||||
@@ -4,17 +4,16 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, ButtonGroup, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Typography } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
import MoreTimeIcon from '@mui/icons-material/MoreTime';
|
||||
import RemoveIcon from '@mui/icons-material/Remove';
|
||||
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
|
||||
|
||||
import { animationStopEnter } from '../../chat/components/composer/Composer';
|
||||
|
||||
import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
import { lineHeightTextareaMd } from '~/common/app.theme';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
@@ -219,7 +218,7 @@ export function PromptDesigner(props: {
|
||||
|
||||
<Dropdown>
|
||||
<MenuButton slots={{ root: IconButton }}>
|
||||
<ArrowForwardIcon />
|
||||
<ArrowForwardRoundedIcon />
|
||||
</MenuButton>
|
||||
<Menu placement='top'>
|
||||
{/* Add From History? */}
|
||||
@@ -288,10 +287,10 @@ export function PromptDesigner(props: {
|
||||
<Button
|
||||
key='draw-queue'
|
||||
variant='solid' color='primary'
|
||||
endDecorator={<FormatPaintIcon />}
|
||||
endDecorator={<FormatPaintTwoToneIcon />}
|
||||
onClick={handlePromptEnqueue}
|
||||
sx={{
|
||||
animation: `${animationStopEnter} 0.1s ease-out`,
|
||||
animation: `${animationEnterBelow} 0.1s ease-out`,
|
||||
boxShadow: !props.isMobile ? `0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)` : 'none',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
@@ -306,7 +305,7 @@ export function PromptDesigner(props: {
|
||||
endDecorator={<StopOutlinedIcon sx={{ fontSize: 18 }} />}
|
||||
onClick={handleDrawStop}
|
||||
sx={{
|
||||
// animation: `${animationStopEnter} 0.1s ease-out`,
|
||||
// animation: `${animationEnterBelow} 0.1s ease-out`,
|
||||
boxShadow: !props.isMobile ? `0 8px 24px -4px rgb(var(--joy-palette-warning-mainChannel) / 20%)` : 'none',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
@@ -321,7 +320,7 @@ export function PromptDesigner(props: {
|
||||
endDecorator={<MoreTimeIcon sx={{ fontSize: 18 }} />}
|
||||
onClick={handlePromptEnqueue}
|
||||
sx={{
|
||||
animation: `${animationStopEnter} 0.1s ease-out`,
|
||||
animation: `${animationEnterBelow} 0.1s ease-out`,
|
||||
boxShadow: !props.isMobile ? `0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)` : 'none',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FormControl, FormLabel, ListItemDecorator, Option, Select } from '@mui/joy';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
|
||||
import type { TextToImageProvider } from '~/common/components/useCapabilities';
|
||||
import { OpenAIIcon } from '~/common/components/icons/vendors/OpenAIIcon';
|
||||
@@ -22,7 +22,7 @@ export function ProviderSelect(props: {
|
||||
label: provider.label + (provider.painter !== provider.label ? ` ${provider.painter}` : ''),
|
||||
value: provider.id,
|
||||
configured: provider.configured,
|
||||
Icon: provider.vendor === 'openai' ? OpenAIIcon : FormatPaintIcon,
|
||||
Icon: provider.vendor === 'openai' ? OpenAIIcon : FormatPaintTwoToneIcon,
|
||||
});
|
||||
});
|
||||
return options;
|
||||
@@ -41,7 +41,7 @@ export function ProviderSelect(props: {
|
||||
value={props.activeProviderId}
|
||||
placeholder='Select a service'
|
||||
onChange={(_event, value) => value && props.setActiveProviderId(value)}
|
||||
// startDecorator={<FormatPaintIcon sx={{ display: { xs: 'none', sm: 'inherit' } }} />}
|
||||
// startDecorator={<FormatPaintTwoToneIcon sx={{ display: { xs: 'none', sm: 'inherit' } }} />}
|
||||
sx={{
|
||||
minWidth: '12rem',
|
||||
}}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Box, Button, Card, CardContent, List, ListItem, Tooltip, Typography } f
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { ChatMessageMemo } from '../chat/components/message/ChatMessage';
|
||||
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
|
||||
import { useChatShowSystemMessages } from '../chat/store-app-chat';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { conversationTitle, DConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { launchAppChat } from '~/common/app.routes';
|
||||
@@ -63,7 +63,7 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
|
||||
const handleClone = async (canOverwrite: boolean) => {
|
||||
setCloning(true);
|
||||
const importedId = useChatStore.getState().importConversation({ ...props.conversation }, !canOverwrite);
|
||||
await launchAppChat(importedId);
|
||||
void launchAppChat(importedId);
|
||||
setCloning(false);
|
||||
};
|
||||
|
||||
@@ -108,17 +108,10 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
|
||||
p: 0,
|
||||
}}>
|
||||
|
||||
<ScrollToBottom
|
||||
bootToBottom bootSmoothly
|
||||
sx={{
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<ScrollToBottom bootToBottom bootSmoothly>
|
||||
|
||||
<List sx={{
|
||||
minHeight: '100%',
|
||||
p: 0,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
@@ -142,7 +135,7 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
|
||||
key={'msg-' + message.id}
|
||||
message={message}
|
||||
fitScreen={isMobile}
|
||||
blocksShowDate={idx === 0 || idx === filteredMessages.length - 1 /* first and last message */}
|
||||
showBlocksDate={idx === 0 || idx === filteredMessages.length - 1 /* first and last message */}
|
||||
onMessageEdit={(_messageId, text: string) => message.text = text}
|
||||
/>,
|
||||
)}
|
||||
|
||||
+137
-53
@@ -1,45 +1,76 @@
|
||||
import * as React from 'react';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import NextImage from 'next/image';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { AspectRatio, Box, Button, Card, CardContent, CardOverflow, Container, IconButton, Typography } from '@mui/joy';
|
||||
import { AspectRatio, Box, Button, Card, CardContent, CardOverflow, Container, Grid, Typography } from '@mui/joy';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { ROUTE_INDEX } from '~/common/app.routes';
|
||||
import { animationColorBlues, animationColorRainbow } from '~/common/util/animUtils';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { cssRainbowColorKeyframes } from '~/common/app.theme';
|
||||
|
||||
import { NewsItems, newsRoadmapCallout } from './news.data';
|
||||
import { NewsItems } from './news.data';
|
||||
import { beamNewsCallout } from './beam.data';
|
||||
import { bigAgi2NewsCallout, bigAgi2Url } from './bigAgi2.data';
|
||||
|
||||
import { downloadAllConversationsJson } from '~/modules/trade/trade.client';
|
||||
|
||||
|
||||
// number of news items to show by default, before the expander
|
||||
const DEFAULT_NEWS_COUNT = 3;
|
||||
const NEWS_INITIAL_COUNT = 3;
|
||||
const NEWS_LOAD_STEP = 2;
|
||||
|
||||
export const cssColorKeyframes = keyframes`
|
||||
0%, 100% {
|
||||
color: #636B74; /* Neutral main color (500) */
|
||||
}
|
||||
25% {
|
||||
color: #12467B; /* Primary darker shade (700) */
|
||||
}
|
||||
50% {
|
||||
color: #0B6BCB; /* Primary main color (500) */
|
||||
}
|
||||
75% {
|
||||
color: #083e75; /* Primary lighter shade (300) */
|
||||
}`;
|
||||
|
||||
export const newsRoadmapCallout =
|
||||
<Card variant='solid' invertedColors>
|
||||
<CardContent sx={{ gap: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
Open Roadmap
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Take a peek at our roadmap to see what's in the pipeline.
|
||||
Discover upcoming features and let us know what excites you the most!
|
||||
</Typography>
|
||||
<Grid container spacing={1}>
|
||||
<Grid xs={12} sm={7}>
|
||||
<Button
|
||||
fullWidth variant='soft' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={Brand.URIs.OpenProject} noLinkStyle target='_blank'
|
||||
>
|
||||
Explore
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid xs={12} sm={5} sx={{ display: 'flex', flexAlign: 'center', justifyContent: 'center' }}>
|
||||
<Button
|
||||
fullWidth variant='plain' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={Brand.URIs.OpenRepo + '/issues/new?template=roadmap-request.md&title=%5BSuggestion%5D'} noLinkStyle target='_blank'
|
||||
>
|
||||
Suggest a Feature
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>;
|
||||
|
||||
|
||||
export function AppNews() {
|
||||
// state
|
||||
const [lastNewsIdx, setLastNewsIdx] = React.useState<number>(DEFAULT_NEWS_COUNT - 1);
|
||||
const [lastNewsIdx, setLastNewsIdx] = React.useState<number>(NEWS_INITIAL_COUNT - 1);
|
||||
|
||||
// news selection
|
||||
const news = NewsItems.filter((_, idx) => idx <= lastNewsIdx);
|
||||
const firstNews = news[0] ?? null;
|
||||
|
||||
// show expander
|
||||
const canExpand = news.length < NewsItems.length;
|
||||
|
||||
const currentVer = '1.6.9'; // firstNews?.versionCode;
|
||||
|
||||
return (
|
||||
|
||||
<Box sx={{
|
||||
@@ -54,26 +85,41 @@ export function AppNews() {
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
}}>
|
||||
|
||||
<Typography level='h1' sx={{ fontSize: '2.9rem', mb: 4 }}>
|
||||
Welcome to {Brand.Title.Base} <Box component='span' sx={{ animation: `${cssColorKeyframes} 10s infinite`, zIndex: 1 }}>{firstNews?.versionCode}</Box>!
|
||||
<Typography level='h1' sx={{ fontSize: '2.7rem', mb: 4 }}>
|
||||
Welcome to {Brand.Title.Base} <Box component='span' sx={{ animation: `${animationColorBlues} 10s infinite`, zIndex: 1 /* perf-opt */ }}>{currentVer}</Box>!
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ mb: 2 }} level='title-sm'>
|
||||
{capitalizeFirstLetter(Brand.Title.Base)} has been updated to version {firstNews?.versionCode}
|
||||
<Typography level='title-sm' sx={{ mb: 2, textAlign: 'center', lineHeight: 'lg' }} >
|
||||
{capitalizeFirstLetter(Brand.Title.Base)} has been updated to version {currentVer}.<br/>
|
||||
<b>And a whole-new 2.0 is waiting!</b>
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 5 }}>
|
||||
<Box sx={{ mb: 5, display: 'flex', gap: 2, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<Button
|
||||
variant='solid' color='primary' size='lg'
|
||||
variant='solid' color='neutral' size='lg'
|
||||
component={Link} href={ROUTE_INDEX} noLinkStyle
|
||||
endDecorator='✨'
|
||||
endDecorator={<ArrowForwardRoundedIcon />}
|
||||
// endDecorator='✨'
|
||||
sx={{
|
||||
boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)',
|
||||
// boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)',
|
||||
minWidth: 180,
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<Button
|
||||
variant='solid' color='primary' size='lg'
|
||||
component={Link} href={bigAgi2Url} noLinkStyle
|
||||
endDecorator={<><ArrowOutwardRoundedIcon /></>}
|
||||
// endDecorator='✨'
|
||||
sx={{
|
||||
boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)',
|
||||
minWidth: 180,
|
||||
transform: 'translateY(-1px)',
|
||||
}}
|
||||
>
|
||||
Big-AGI 2 ✨
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/*<Typography level='title-sm' sx={{ mb: 1, placeSelf: 'start', ml: 1 }}>*/}
|
||||
@@ -82,14 +128,26 @@ export function AppNews() {
|
||||
|
||||
<Container disableGutters maxWidth='sm'>
|
||||
{news?.map((ni, idx) => {
|
||||
// const firstCard = idx === 0;
|
||||
const hasCardAfter = news.length < NewsItems.length;
|
||||
const showExpander = hasCardAfter && (idx === news.length - 1);
|
||||
const firstCard = idx === 0;
|
||||
const addPadding = false; //!firstCard; // || showExpander;
|
||||
return <React.Fragment key={idx}>
|
||||
|
||||
{/* Inject the Big-AGI 2.0 item here*/}
|
||||
{idx === 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{bigAgi2NewsCallout}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Inject the Beam item here*/}
|
||||
{idx === 2 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{beamNewsCallout}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* News Item */}
|
||||
<Card key={'news-' + idx} sx={{ mb: 3, minHeight: 32, gap: 1 }}>
|
||||
<Card color={firstCard ? 'primary' : undefined} key={'news-' + idx} sx={{ mb: 3, minHeight: 32, gap: 1 }}>
|
||||
<CardContent sx={{ position: 'relative', pr: addPadding ? 4 : 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography level='title-sm' component='div'>
|
||||
@@ -97,9 +155,9 @@ export function AppNews() {
|
||||
<Box
|
||||
component='span'
|
||||
sx={idx ? {} : {
|
||||
animation: `${cssRainbowColorKeyframes} 5s infinite`,
|
||||
animation: `${animationColorRainbow} 5s infinite`,
|
||||
fontWeight: 'lg',
|
||||
zIndex: 1,
|
||||
zIndex: 1, /* perf-opt */
|
||||
}}
|
||||
>
|
||||
{ni.versionName}
|
||||
@@ -111,28 +169,39 @@ export function AppNews() {
|
||||
</Box>
|
||||
|
||||
{!!ni.items && (ni.items.length > 0) && (
|
||||
<ul style={{ marginTop: 8, marginBottom: 8, paddingInlineStart: '1.5rem' }}>
|
||||
{ni.items.filter(item => item.dev !== true).map((item, idx) => <li key={idx}>
|
||||
< Typography component='div' level='body-sm'>
|
||||
{item.text}
|
||||
</Typography>
|
||||
</li>)}
|
||||
<ul style={{ marginTop: 8, marginBottom: 8, paddingInlineStart: '1.5rem', listStyleType: '"- "' }}>
|
||||
{ni.items.filter(item => item.dev !== true).map((item, idx) => (
|
||||
<li key={idx} style={{ listStyle: (item.icon || item.noBullet) ? '" "' : '"- "', marginLeft: item.icon ? '-1.125rem' : undefined }}>
|
||||
<Typography component='div' sx={{ fontSize: 'sm' }}>
|
||||
{item.icon && <item.icon sx={{ fontSize: 'xs', mr: 0.75 }} />}
|
||||
{item.text}
|
||||
</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{showExpander && (
|
||||
<IconButton
|
||||
variant='solid'
|
||||
onClick={() => setLastNewsIdx(idx + 1)}
|
||||
sx={{
|
||||
position: 'absolute', right: 0, bottom: 0, mr: -1, mb: -1,
|
||||
// backgroundColor: 'background.surface',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
{firstCard && <Box sx={{ mt: 2 }}>
|
||||
<Card variant='soft' color='primary' invertedColors>
|
||||
<CardContent>
|
||||
<Typography level='title-sm' sx={{ lineHeight: 'lg' }}>
|
||||
<b>Migrate your chats:</b> you can download all your conversations at any time by clicking
|
||||
on "Export" > "Download All" or the button below. Then open Big-AGI 2 and
|
||||
import the conversation by clicking "Organize" > "Import".
|
||||
</Typography>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color='primary'
|
||||
sx={{ mt: 1 }}
|
||||
onClick={downloadAllConversationsJson}
|
||||
>
|
||||
Download All Conversations
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>}
|
||||
|
||||
</CardContent>
|
||||
|
||||
{!!ni.versionCoverImage && (
|
||||
@@ -146,14 +215,16 @@ export function AppNews() {
|
||||
// commented: we scale the images to 600px wide (>300 px tall)
|
||||
// sizes='(max-width: 1200px) 100vw, 50vw'
|
||||
priority={idx === 0}
|
||||
quality={90}
|
||||
/>
|
||||
</AspectRatio>
|
||||
</CardOverflow>
|
||||
)}
|
||||
|
||||
</Card>
|
||||
|
||||
{/* Inject the roadmap item here*/}
|
||||
{idx === 0 && (
|
||||
{idx === 3 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{newsRoadmapCallout}
|
||||
</Box>
|
||||
@@ -161,6 +232,19 @@ export function AppNews() {
|
||||
|
||||
</React.Fragment>;
|
||||
})}
|
||||
|
||||
{canExpand && (
|
||||
<Button
|
||||
fullWidth
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
onClick={() => setLastNewsIdx(index => index + NEWS_LOAD_STEP)}
|
||||
endDecorator={<ExpandMoreIcon />}
|
||||
>
|
||||
Previous News
|
||||
</Button>
|
||||
)}
|
||||
|
||||
</Container>
|
||||
|
||||
{/*<Typography sx={{ textAlign: 'center' }}>*/}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user