Compare commits

..

3 Commits

Author SHA1 Message Date
CaiHQ
30aa0faaa1 update configs 2025-08-20 18:36:48 +08:00
CaiHQ
8d6a9b39bb update prompt 2025-08-19 17:39:17 +08:00
zhaoweijie
1104fb2733 refactor(layout): 重构布局组件并添加新功能
- 更新 Header 组件,增加项目标题和历史记录切换按钮
- 新增 DataNavigation 组件用于数据导航
- 添加 Playground 相关新组件,包括数据、场景、团队等信息展示
- 重构 Layout 组件,使用 Context 管理历史记录状态
- 更新 zh/option.json 文件,添加新的项目标题和对话相关翻译
2025-08-19 16:20:37 +08:00
35 changed files with 1125 additions and 333 deletions

BIN
operation/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

BIN
operation/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 KiB

BIN
operation/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 KiB

BIN
operation/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

1
src/assets/icons/a.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon" viewBox="0 0 64 64" width="24" height="24"><defs></defs><g><path fill="#3e4347" d="M64 57.6c0 3.5-2.9 6.4-6.4 6.4H6.4C2.9 64 0 61.1 0 57.6V6.4C0 2.9 2.9 0 6.4 0h51.2C61.1 0 64 2.9 64 6.4z"></path><circle cx="32" cy="32" r="32" fill="#42ade2"></circle><path fill="#fff" d="M32 40.4c-4.6 0-8.4-3.8-8.4-8.4s3.8-8.4 8.4-8.4s8.4 3.8 8.4 8.4s-3.8 8.4-8.4 8.4"></path><path fill="#3e4347" d="M38.4 24c-3.5 0-6.4 2.9-6.4 6.4v3.2c0 3.5 2.9 6.4 6.4 6.4H64V24z"></path></g></svg>

After

Width:  |  Height:  |  Size: 590 B

1
src/assets/icons/b.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon" viewBox="0 0 64 64" width="24" height="24"><defs></defs><g><g fill="#333"><path d="M0 0h4v64H0z"></path><path d="M0 60h64v4H0z"></path></g><path fill="#fb4f00" d="M38.7 60h12V6.7L38.7 20z"></path><path fill="#5c750a" d="M21.3 60h12V20l-12 13.3z"></path><path fill="#106995" d="M4 60h12V33.3L4 46.7z"></path><path fill="#9aca0a" d="M33.3 20h13.3v40H33.3z"></path><path fill="#21adf1" d="M16 33.3h13.3V60H16z"></path><path fill="#fc9100" d="M50.7 6.7H64V60H50.7z"></path></g></svg>

After

Width:  |  Height:  |  Size: 601 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon" viewBox="0 0 64 64" width="24" height="24"><defs></defs><g><path fill="#616466" d="M28 58c0 2.2 1.8 4 4 4s4-1.8 4-4z"></path><path fill="#ffce31" d="M24.9 48H39c.8-4.3 3.5-8.5 6.3-12.9C48.6 30 52 24.7 52 19.6C52 9.9 43 2 32 2S12 9.9 12 19.6c0 5.1 3.4 10.4 6.6 15.5c2.8 4.4 5.5 8.6 6.3 12.9"></path><path fill="#c79127" d="M26.4 33.6c.1.6.3 1.2.4 1.8c.3 1.1.5 2.1.8 3.2c.9 3.8 1.7 7 2.4 9.5h.6c-.5-2.5-1.2-5.8-2.1-9.6c-.2-1-.5-2.1-.7-3.2c-.1-.5-.2-1.1-.4-1.6c.8-.2 2.7-.8 4.6-2.9c1.9 2.1 3.8 2.7 4.6 2.9c-.1.6-.2 1.1-.4 1.6c-.2 1.1-.5 2.2-.7 3.2c-.9 3.8-1.6 7.1-2.1 9.6h.6c.6-2.5 1.5-5.7 2.4-9.5c.2-1 .5-2.1.8-3.2c.1-.6.3-1.2.4-1.8c.8-.1 1.5-.3 2-.8c.6-.6.9-1.3.7-2.1c-.1-.4-.3-.9-.9-1c-.3-.1-.5-.1-.8 0s-.5.3-.6.4c-.5.6-.7 1.2-.9 1.8l-.3.9s-.8.1-2.7-1.2c-1-.7-1.3-1.2-1.6-1.5c.3-.4.6-.7.9-1.2c.1-.3.2-.5.3-.9c0-.3 0-.7-.2-1s-.4-.6-.9-.8c-.2-.1-.4-.1-.6-.1s-.4 0-.6.1c-.4.2-.7.5-.9.8s-.2.7-.2 1q0 .45.3.9q.3.75.9 1.2c-.3.3-.6.8-1.6 1.5c-1.8 1.2-2.7 1.2-2.7 1.2l-.3-.9c-.2-.6-.4-1.2-.9-1.8c-.1-.1-.3-.3-.6-.4s-.6-.1-.8 0c-.6.2-.8.7-.9 1c-.1.8.2 1.5.7 2.1c.6.5 1.3.7 2 .8M38 32.1c.2-.5.4-1.1.8-1.4c.1-.1.2-.1.2-.1h.2c.1 0 .2.1.3.4c.1.4-.1 1-.5 1.3c-.3.2-.7.4-1.1.5c0-.3 0-.5.1-.7m-6.6-4.8c.2-.2.4-.4.6-.4s.4.1.6.4s.1.7-.1 1.1c-.1.3-.3.6-.5.8c-.2-.3-.4-.5-.5-.8c-.2-.3-.3-.7-.1-1.1m-6.8 3.5c0-.2.1-.3.3-.4h.2s.1.1.2.1c.3.3.6.9.8 1.4c.1.2.1.4.2.6c-.4-.1-.8-.2-1.1-.5c-.4-.2-.6-.7-.6-1.2"></path><path fill="#94989b" d="M24.9 50h14.3v1.8H24.9zm1 3.6h12.3v1.8H25.9z"></path><path fill="#616466" d="M25.9 51.8h12.3v1.8H25.9z"></path><path fill="#94989b" d="m39.2 50l-13.3 3.6v1.9l13.3-3.7zm-12.3 7.3h10.3v1.8H26.9z"></path><path fill="#616466" d="M26.9 55.5h10.3v1.8H26.9z"></path><path fill="#94989b" d="m38.2 53.6l-11.3 3.7v1.9l11.3-3.7z"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

1
src/assets/icons/c.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon" viewBox="0 0 64 64" width="24" height="24"><defs></defs><g><path fill="#d0d0d0" d="M23.6 36.7H2.9l10.3-26.2zm-18-1.9h15.3l-7.7-19.5zm55.5 1.9H40.4l10.3-26.2zm-18-1.9h15.3l-7.7-19.5z"></path><path fill="#b8c2c4" d="M50.3 10.2s-2.2-.8-3.4-1.3C42.6 7.1 37.3 5 32 5S21.4 7.2 17.1 9c-1.2.5-3.4 1.3-3.4 1.3l-3.3 2.5s2.7 1.7 4 1.2c1.1-.4 2.2-.9 3.5-1.4C22 10.9 27.1 8.8 32 8.8s10 2.1 14.1 3.8c1.2.5 2.4 1 3.5 1.4c1.4.5 4-1.2 4-1.2z"></path><path fill="#428bc1" d="M2 34.8C2 41 7 46 13.2 46s11.2-5 11.2-11.2zm37.5 0c0 6.2 5 11.2 11.2 11.2S62 41 62 34.8z"></path><path fill="#b8c2c4" d="M30.1 12.3h3.8v41.2h-3.8z"></path><path fill="#d0d0d0" d="M29 18.6h6.1v34.9H29z"></path><path fill="#545b60" d="M27.7 36.7h8.6v19.7h-8.6z"></path><circle cx="13.2" cy="13.2" r="3.8" fill="#dbb471"></circle><g fill="#545b60"><circle cx="50.8" cy="13.2" r="3.8"></circle><circle cx="13.2" cy="13.2" r="3.8"></circle></g><g fill="#fff"><circle cx="13.2" cy="13.2" r="1.9"></circle><circle cx="50.8" cy="13.2" r="1.9"></circle></g><g fill="#d0d0d0"><circle cx="32" cy="7.6" r="5.6"></circle><path d="M32 45.1c-8.3 0-15 4.2-15 9.4h30c0-5.2-6.7-9.4-15-9.4"></path></g><path fill="#545b60" d="M15.1 54.5h33.8v3.8H15.1z"></path><path fill="#6b767c" d="M11.4 58.2h41.2V62H11.4z"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
src/assets/icons/d.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon" viewBox="0 0 64 64" width="24" height="24"><defs></defs><g><path fill="#979797" d="M64 31.7h-5.6c-7.3-5-11.9-13.2-12.4-22.1l2.8-5l-1.6-1l-2.8 5c-7.8 4-17 4-24.8 0l-2.8-5l-1.6 1l2.8 5c-.5 8.9-5.1 17.2-12.4 22.1H0v1.9h5.6c7.3 4.9 11.9 13.2 12.4 22.1l-2.8 5l1.6 1l2.8-5c7.8-4 17-4 24.8 0l2.8 5l1.6-1l-2.8-5c.5-8.9 5.1-17.2 12.4-22.1H64zm-8.7 0h-5.5c-4.7-3.2-7.8-8.6-8.1-14.4l2.8-4.9c1 7.6 4.9 14.5 10.8 19.3m-23.3-1l-2.3-4.1c.7.2 1.5.3 2.3.3s1.6-.1 2.3-.3zm3.9-3.1c.5 1.5 1.3 2.9 2.3 4.1h-4.6zm-5.5 4.1h-4.6c1-1.2 1.8-2.6 2.3-4.1zm0 1.9l-2.3 4.1c-.5-1.5-1.3-2.9-2.3-4.1zm1.6.9l2.3 4.1c-1.5-.3-3.1-.3-4.6 0zm1.6-.9h4.6c-1 1.2-1.8 2.6-2.3 4.1zm7.4-1.9c-2.1-1.5-3.5-3.9-3.7-6.6l2.9-5.1c.8 4.5 3.1 8.7 6.5 11.7zm-5.3-7.6c-2.3 1.2-5.1 1.2-7.4 0L25.5 19c2.1.8 4.3 1.1 6.5 1.1s4.4-.4 6.5-1.1zm-9 .9c-.2 2.7-1.5 5.1-3.7 6.6h-5.7c3.4-3 5.7-7.1 6.5-11.7zM23 33.6c2.1 1.5 3.5 3.9 3.7 6.6l-2.9 5.1c-.8-4.5-3.1-8.7-6.5-11.7zm5.3 7.5c2.3-1.2 5.1-1.2 7.4 0l2.9 5.1c-4.2-1.5-8.9-1.5-13.1 0zm9-.9c.2-2.7 1.5-5.1 3.7-6.6h5.7c-3.4 3-5.7 7.1-6.5 11.7zM32 13.5c3.7 0 7.4-.7 10.8-2.1L40 16.3c-5.1 2.6-11.1 2.6-16.1 0l-2.8-4.9c3.5 1.4 7.2 2.1 10.9 2.1m-12.4-1.1l2.8 4.9c-.4 5.8-3.4 11.2-8.1 14.4H8.7c5.9-4.8 9.8-11.7 10.9-19.3M8.8 33.6h5.5c4.7 3.2 7.7 8.5 8 14.4l-2.8 4.9c-1-7.6-4.9-14.6-10.7-19.3m12.4 20.2l2.8-4.9c5.1-2.6 11.1-2.6 16.1 0l2.8 4.9c-7-2.8-14.8-2.8-21.7 0m23.2-.9L41.7 48c.3-5.8 3.3-11.2 8.1-14.4h5.5c-5.9 4.7-9.8 11.7-10.9 19.3"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
src/assets/icons/e.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon" viewBox="0 0 64 64" width="24" height="24"><defs></defs><g><path fill="#212528" d="M21.6 16.1c0 1.2-1 2.2-2.2 2.2H9.7c-1.2 0-2.2-1-2.2-2.2v-2.7c0-1.2 1-2.2 2.2-2.2h9.7c1.2 0 2.2 1 2.2 2.2zm39.1-.2c0 .6-.5 1.2-1.2 1.2h-5.2c-.6 0-1.2-.5-1.2-1.2v-1.4c0-.6.5-1.2 1.2-1.2h5.2c.6 0 1.2.5 1.2 1.2zM64 50.3c0 3-2.4 5.4-5.4 5.4H5.4c-3 0-5.4-2.4-5.4-5.4v-1.6h64z"></path><path fill="#51575b" d="M0 20.1c0-3 2.4-5.4 5.4-5.4h53.1c3 0 5.4 2.4 5.4 5.4v1.6H0z"></path><path fill="#3e4347" d="M0 21.5h64v28.3H0z"></path><path fill="#51575b" d="M54.7 18H22.6l3.2-10.8c.3-.6 1.6-1.6 2.4-1.8C33.1 4 44.1 4 49 5.4c.8.2 2.1 1.2 2.4 1.8z"></path><path fill="#3e4347" d="M53.1 29.6h-29L27 15c.2-.8 1.5-2.1 2.2-2.4c4.4-1.8 14.4-1.8 18.8 0c.7.3 1.9 1.6 2.2 2.4z"></path><path fill="#788287" d="M60.6 37.6c0 12.2-9.9 22-22 22s-22-9.8-22-22c0-12.1 9.9-22 22-22c12.2 0 22 9.9 22 22"></path><path fill="#212528" d="M58.2 37.6c0 10.8-8.8 19.6-19.6 19.6S19 48.4 19 37.6S27.8 18 38.6 18c10.8.1 19.6 8.8 19.6 19.6"></path><circle cx="38.6" cy="37.6" r="15.9" fill="#3e4347"></circle><circle cx="38.6" cy="37.6" r="8.6" fill="#212528"></circle><g fill="#f5f5f5"><path d="M50.3 30.9c0 2.7-2.2 4.9-4.9 4.9s-4.9-2.2-4.9-4.9s2.2-4.9 4.9-4.9c2.7-.1 4.9 2.2 4.9 4.9" opacity=".5"></path><circle cx="35.6" cy="40.7" r="3.1" opacity=".5"></circle><circle cx="30.1" cy="46.2" r="1.9" opacity=".5"></circle></g><path fill="#636c72" d="M15 45.3c0 1.2-1 2.2-2.2 2.2H3.6c-1.2 0-2.2-1-2.2-2.2V25.9c0-1.2 1-2.2 2.2-2.2h9.2c1.2 0 2.2 1 2.2 2.2z"></path><circle cx="10.1" cy="18.6" r="3.2" fill="#212528"></circle></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

1
src/assets/icons/eye.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon" viewBox="0 0 64 64" width="24" height="24"><defs></defs><g><path fill="#231f20" d="M62 32S51.9 52 32 52S2 32 2 32s10.1-20 30-20s30 20 30 20"></path><path fill="#fff" d="M57 32s-8.4 16.7-25 16.7S7 32 7 32s8.4-16.7 25-16.7S57 32 57 32"></path><path fill="#42ade2" d="M45.4 32c0 7.5-6 13.5-13.5 13.5s-13.5-6-13.5-13.5s6-13.5 13.5-13.5s13.5 6 13.5 13.5"></path><path fill="#231f20" d="M39.4 32c0 4.1-3.4 7.5-7.5 7.5s-7.5-3.4-7.5-7.5s3.4-7.5 7.5-7.5s7.5 3.4 7.5 7.5"></path></g></svg>

After

Width:  |  Height:  |  Size: 601 B

1
src/assets/icons/f.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon" viewBox="0 0 64 64" width="24" height="24"><defs></defs><g><path fill="#0071bc" d="M57.9 32c.2-.2.3-.4.5-.6c3.9-5.6 4.7-10.7 2.3-14.5c-2.2-3.5-7-5.5-13.4-5.5c-1.1 0-2.3.1-3.5.2C40.9 5.7 36.7 2 32 2s-8.9 3.7-11.8 9.6c-1.2-.1-2.4-.2-3.5-.2c-6.4 0-11.2 1.9-13.4 5.5c-2.4 3.8-1.6 9 2.3 14.5c.2.2.4.4.5.6c-.2.2-.3.4-.5.6C1.8 38.2 1 43.4 3.4 47.2c2.2 3.5 7 5.5 13.4 5.5c1.1 0 2.3-.1 3.5-.2c2.8 5.8 7 9.5 11.7 9.5s8.9-3.7 11.8-9.6c1.2.1 2.4.2 3.5.2c6.4 0 11.2-1.9 13.4-5.5c2.4-3.8 1.6-9-2.3-14.5c-.2-.2-.4-.4-.5-.6M47.2 13.1c5.7 0 9.9 1.6 11.8 4.6c2 3.2 1.2 7.7-2.3 12.7l-.1.1c-2.5-3-5.8-6-9.7-8.6c-.6-3.2-1.4-6.1-2.5-8.7c1-.1 2-.1 2.8-.1m-7.4 31.3c-2.6 1.4-5.2 2.5-7.8 3.5c-2.6-1-5.3-2.1-7.8-3.5c-1.9-1-3.7-2.1-5.5-3.3c-.5-2.9-.8-6-.8-9.2s.3-6.3.8-9.2c1.7-1.2 3.5-2.2 5.5-3.3c2.6-1.4 5.2-2.5 7.8-3.5c2.6 1 5.3 2.1 7.8 3.5c1.9 1 3.7 2.1 5.5 3.3c.5 2.9.8 6 .8 9.2s-.3 6.3-.8 9.2c-1.7 1.2-3.6 2.3-5.5 3.3m5-.8c-.6 2.5-1.3 4.9-2.3 7c-2.6-.3-5.2-.9-7.9-1.8c2.1-.8 4.1-1.8 6.1-2.8c1.5-.8 2.8-1.6 4.1-2.4m-15.4 5.2c-2.7.8-5.4 1.4-7.9 1.8c-.9-2.1-1.7-4.4-2.3-7c1.3.8 2.6 1.6 4 2.3c2.1 1.1 4.1 2 6.2 2.9m-12.8-9.1c-3.2-2.4-6-5-8.1-7.7c2.1-2.7 4.9-5.3 8.1-7.7c-.3 2.4-.5 5-.5 7.7s.1 5.2.5 7.7m2.6-19.3c.6-2.5 1.3-4.9 2.3-7c2.6.3 5.2.9 7.9 1.8c-2.1.8-4.1 1.8-6.1 2.9c-1.5.7-2.8 1.5-4.1 2.3m15.4-5.2c2.7-.8 5.4-1.4 7.9-1.8c.9 2.1 1.7 4.4 2.3 7c-1.3-.8-2.6-1.6-4-2.3c-2.1-1.1-4.1-2-6.2-2.9m12.8 9.1c3.2 2.4 6 5 8.1 7.7c-2.1 2.7-4.9 5.3-8.1 7.7c.3-2.4.5-5 .5-7.7s-.1-5.2-.5-7.7M32 3.9c3.8 0 7.2 3 9.8 7.9c-3.1.5-6.4 1.3-9.8 2.5c-3.3-1.2-6.6-2-9.8-2.5c2.6-4.9 6-7.9 9.8-7.9M7.2 30.4c-3.5-5-4.3-9.5-2.3-12.7c1.9-3 6.1-4.6 11.8-4.6c.9 0 1.8 0 2.7.1c-1.1 2.6-1.9 5.5-2.5 8.7c-3.8 2.6-7.1 5.6-9.7 8.5m9.6 20.5c-5.7 0-9.9-1.6-11.8-4.6c-2-3.2-1.2-7.7 2.3-12.7c0 0 0-.1.1-.1c2.5 3 5.8 6 9.7 8.6c.6 3.2 1.4 6.1 2.5 8.7c-1 .1-1.9.1-2.8.1M32 60.1c-3.8 0-7.2-3-9.8-7.9c3.1-.5 6.4-1.3 9.8-2.5c3.3 1.2 6.6 2 9.8 2.5c-2.6 4.9-6 7.9-9.8 7.9m27-13.8c-1.9 3-6.1 4.6-11.8 4.6c-.9 0-1.8 0-2.7-.1c1.1-2.6 1.9-5.5 2.5-8.7c3.8-2.6 7.1-5.6 9.7-8.6c0 0 0 .1.1.1c3.5 5 4.3 9.5 2.2 12.7"></path><circle cx="32" cy="32" r="5.6" fill="#ed4c5c"></circle></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" class="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon" viewBox="0 0 64 64" width="24" height="24"><defs></defs><g><g fill="#ff9d27"><path d="M10.9 48.7c4-4 4.4-5 6.9-2.5s1.5 2.8-2.5 6.9c-3 3-6.8 2.4-6.8 2.4s-.6-3.8 2.4-6.8"></path><path d="M18.5 52.8c1.6-4.2 2.1-4.7-.2-6s-2.3-.4-3.8 3.8c-1.2 3.1.2 5.9.2 5.9s2.7-.5 3.8-3.7"></path></g><path fill="#fdf516" d="M16.2 48.9c.9-2.3.9-2.8 2.1-2.1c1.3.7 1 1 .1 3.3c-.6 1.7-2.1 2.1-2.1 2.1s-.7-1.5-.1-3.3"></path><path fill="#ff9d27" d="M17.1 45.7c-1.3-2.3-1.8-1.8-6-.2c-3.1 1.2-3.7 3.8-3.7 3.8s2.8 1.4 5.9.2c4.2-1.6 5.1-1.6 3.8-3.8"></path><g fill="#fdf516"><path d="M15 47.8c2.3-.9 2.8-.9 2.1-2.1c-.7-1.3-1-1-3.3-.1c-1.7.6-2.1 2.1-2.1 2.1s1.6.7 3.3.1"></path><path d="M13.9 47.6c2.2-2.2 2.4-2.8 3.8-1.4s.8 1.6-1.4 3.8c-1.7 1.7-3.8 1.3-3.8 1.3s-.2-2 1.4-3.7"></path></g><path fill="#3baacf" d="M18.5 38C12.3 27.6 2 31.9 2 31.9s14.7-14.7 24.6-4.8z"></path><path fill="#428bc1" d="m23.3 30.3l3.2-3.2C16.7 17.2 2 31.9 2 31.9s12.9-9.2 21.3-1.6"></path><path fill="#3baacf" d="M26 45.5C36.4 51.7 32.1 62 32.1 62s14.7-14.7 4.8-24.6z"></path><path fill="#428bc1" d="m33.7 40.7l3.2-3.2c9.9 9.9-4.8 24.6-4.8 24.6s9.2-13 1.6-21.4"></path><path fill="#c5d0d8" d="M48.8 30.9C37.1 42.5 24.2 48.8 19.7 44.3s1.8-17.4 13.4-29.1c13.6-13.6 28.7-13 28.7-13s.5 15.1-13 28.7"></path><path fill="#dae3ea" d="M45.8 27.6C34.2 39.2 22.6 46.8 19.9 44.1s4.9-14.3 16.5-25.9C50 4.6 62 2 62 2s-2.6 12-16.2 25.6"></path><path fill="#c94747" d="M24.3 47.5c-.5.5-1.3.5-1.8 0l-6-6c-.5-.5-.5-1.4 0-1.9l1.8-1.8l7.8 7.8z"></path><path fill="#f15744" d="M22.6 45.7c-.5.5-1.1.7-1.4.4l-3.4-3.4c-.3-.3-.1-.9.4-1.4l1.8-1.8l4.4 4.4z"></path><path fill="#3e4347" d="M20.9 48.2c-.3.3-1 .3-1.3 0l-3.9-3.9c-.3-.3-.2-.9.1-1.2l1.2-1.2l5.1 5.1z"></path><path fill="#62727a" d="M20.1 47.4c-.3.3-.9.4-1.1.2l-2.7-2.7c-.2-.2-.1-.7.3-1l1.2-1.2l3.5 3.5z"></path><path fill="#c94747" d="M61.8 2.2S56.4 2 49.1 4.8l10.1 10.1C62 7.6 61.8 2.2 61.8 2.2"></path><path fill="#f15744" d="M61.8 2.2s-4.3.9-10.8 4.6l6.2 6.2c3.7-6.5 4.6-10.8 4.6-10.8"></path><circle cx="43.5" cy="20.5" r="5" fill="#edf4f9"></circle><circle cx="43.5" cy="20.5" r="3.3" fill="#3baacf"></circle><circle cx="33.5" cy="30.5" r="5" fill="#edf4f9"></circle><circle cx="33.5" cy="30.5" r="3.3" fill="#3baacf"></circle><g fill="#fff"><path d="M48.9 6.9c-.3.3-.9.3-1.2 0s-.3-.9 0-1.2s.9-.3 1.2 0s.3.9 0 1.2"></path><circle cx="50.6" cy="8.6" r=".8"></circle><circle cx="53" cy="11" r=".8"></circle><circle cx="55.3" cy="13.4" r=".8"></circle><circle cx="57.7" cy="15.7" r=".8"></circle></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,29 +1,30 @@
{ {
"newChat": "新聊天", "projectTitle": "数联网科创智能体",
"selectAPrompt": "选择一个提示词", "newChat": "新对话",
"githubRepository": "GitHub 仓库", "selectAPrompt": "选择一个提示词",
"settings": "设置", "githubRepository": "GitHub 仓库",
"metering": "计量", "settings": "设置",
"sidebarTitle": "聊天历史", "metering": "计量",
"error": "错误", "sidebarTitle": "对话历史",
"somethingWentWrong": "出现了错误", "error": "错误",
"validationSelectModel": "请选择一个模型以继续", "somethingWentWrong": "出现了错误",
"deleteHistoryConfirmation": "你确定要删除这个历史记录吗?", "validationSelectModel": "请选择一个模型以继续",
"editHistoryTitle": "输入一个新的标题", "deleteHistoryConfirmation": "你确定要删除这个历史记录吗?",
"temporaryChat": "临时聊天", "editHistoryTitle": "输入一个新的标题",
"more": { "temporaryChat": "临时对话",
"copy": { "more": {
"group": "复制", "copy": {
"asText": "复制为文本", "group": "复制",
"asMarkdown": "复制为 Markdown", "asText": "复制为文本",
"success": "已复制到剪贴板!" "asMarkdown": "复制为 Markdown",
}, "success": "已复制到剪贴板!"
"download": { },
"group": "下载", "download": {
"text": "文本文件 (.txt)", "group": "下载",
"markdown": "Markdown 文件 (.md)", "text": "文本文件 (.txt)",
"json": "JSON 文件 (.json)" "markdown": "Markdown 文件 (.md)",
}, "json": "JSON 文件 (.json)"
"share": "分享" },
} "share": "分享"
}
} }

View File

@ -0,0 +1,38 @@
import React from "react";
import { Typography, Button } from "antd";
import { AcademicCapIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
const { Title } = Typography;
type Props = {
Header: React.ReactNode;
showButton?: boolean;
onClick?: () => void;
};
export const DataNavigation: React.FC<Props> = ({ Header, showButton = true, onClick }) => {
return (
<div className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg mb-3">
{/* 左侧部分 */}
<div className="flex items-center">
<Title
level={4}
style={{ marginBottom: 0, color: "#1F2937", fontSize: "16px" }}>
{Header}
</Title>
</div>
{/* 右侧部分 */}
{showButton && (
<Button
color="default"
variant="link"
style={{ gap: 4 }}
onClick={onClick}>
<span className="text-sm"></span>
<ChevronRightIcon className="w-4 h-4" />
</Button>
)}
</div>
)
};

View File

@ -12,7 +12,7 @@ import { preprocessLaTeX } from "@/utils/latex"
function Markdown({ function Markdown({
message, message,
className = "prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark" className = "prose-lg break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
}: { }: {
message: string message: string
className?: string className?: string

View File

@ -0,0 +1,175 @@
import React from "react"
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Drawer, List } from "antd"
import { useCallback, useState } from "react"
export const PlaygroundData = () => {
// 模拟数据
const data: {
title: string
description: string
time: string
metadata?: string
}[] = [
{
title: "2019-2024年黄海清浅海域中河湖代数生物物种数据集",
description:
"数字对象标识: CSTR:13452.11.01.11.2021.242 国家海洋科学数据中心",
time: "包括2019年8月2021年8月和2024年6月",
metadata: "热 榜 第"
},
{
title: "祁连山老虎沟大本营10米气象每日值数据集V1.02018-2023",
description:
"中国科学院西北生态环境资源研究院2021年8月3日发布2021年8月3日20:48更新",
time: "包括2019年8月2021年8月和2024年6月",
metadata: "热 榜 第"
},
{
title: "李嘉图为研究老虎沟大本营2014-2018年...",
description:
"中国科学院西北生态环境资源研究院2021年8月3日发布2021年8月3日20:48更新",
time: "包括2019年8月2021年8月和2024年6月",
metadata: "热 榜 第"
},
{
title: "青海玉树B1区俄日矿勘探数据2017-2023",
description:
"数字中国集团CSTR:3260.11.1528414774204895456DT2023年地质勘探补充调查",
time: "包括2019年8月2021年8月和2024年6月",
metadata: "热 榜 第"
}
]
for (let i = 0; i < 10; i++) {
data.push({
title: "中国资源环境网",
description: "中国资源环境网2021年8月3日发布2021年8月3日20:48更新",
time: "包括2019年8月2021年8月和2024年6月"
})
}
const [open, setOpen] = useState(false)
const showDrawer = () => {
setOpen(true)
}
const onClose = () => {
setOpen(false)
}
return (
<div className="flex flex-col h-full overflow-y-hidden">
{/* 数据导航 */}
<DataNavigation
Header={
<div className="flex items-center text-[#1d4eD8] gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 32 32"
width="20"
height="20">
<defs></defs>
<g>
<circle cx="22" cy="24" r="2" fill="rgb(29, 78, 216)"></circle>
<path
fill="rgb(29, 78, 216)"
d="M29.777 23.479A8.64 8.64 0 0 0 22 18a8.64 8.64 0 0 0-7.777 5.479L14 24l.223.522A8.64 8.64 0 0 0 22 30a8.64 8.64 0 0 0 7.777-5.478L30 24zM22 28a4 4 0 1 1 4-4a4.005 4.005 0 0 1-4 4M7 17h5v2H7zm0-5h12v2H7zm0-5h12v2H7z"></path>
<path
fill="rgb(29, 78, 216)"
d="M22 2H4a2.006 2.006 0 0 0-2 2v24a2.006 2.006 0 0 0 2 2h8v-2H4V4h18v11h2V4a2.006 2.006 0 0 0-2-2"></path>
</g>
</svg>
</div>
}
onClick={showDrawer}
/>
{/* 数据列表 */}
<div className="space-y-1.5 flex-1 overflow-y-auto">
{data.slice(0, 3).map((item, index) => (
<Card key={index} hoverable className="[&>*:first-child]:!p-3 h-[148px]">
<div className="flex flex-col gap-0.5">
<h3 className="text-sm font-medium text-gray-900 line-clamp-2">
{item.title}
</h3>
<p className="text-xs text-gray-500 line-clamp-2">
{item.description}
</p>
<p className="text-gray-700 truncate">{item.time}</p>
{item.metadata && (
<div className="text-green-500 text-xs px-2 py-1 rounded-full flex items-center gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 24 24"
width="12"
height="12">
<defs></defs>
<g>
<path
fill="rgb(34, 197, 94)"
d="m16 11.78l4.24-7.33l1.73 1l-5.23 9.05l-6.51-3.75L5.46 19H22v2H2V3h2v14.54L9.5 8z"></path>
</g>
</svg>
{item.metadata}
</div>
)}
</div>
</Card>
))}
</div>
{/* 抽屉 */}
<Drawer
title="相关数据"
closable={{ "aria-label": "Close Button" }}
onClose={onClose}
open={open}
width={600}>
<List
itemLayout="vertical"
dataSource={data}
renderItem={(item, index) => (
<List.Item>
<List.Item.Meta
title={
<h3 className="text-sm font-medium text-gray-900">
{item.title}
</h3>
}
description={
<div className="space-y-1">
<p className="text-xs text-gray-500">{item.description}</p>
<p className="text-gray-700">{item.time}</p>
{item.metadata && (
<div className="text-green-500 text-xs px-2 py-1 rounded-full flex items-center gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 24 24"
width="12"
height="12">
<defs></defs>
<g>
<path
fill="rgb(34, 197, 94)"
d="m16 11.78l4.24-7.33l1.73 1l-5.23 9.05l-6.51-3.75L5.46 19H22v2H2V3h2v14.54L9.5 8z"></path>
</g>
</svg>
{item.metadata}
</div>
)}
</div>
}
/>
</List.Item>
)}
/>
</Drawer>
</div>
)
}

View File

@ -0,0 +1,84 @@
import { Sidebar } from "@/components/Option/Sidebar.tsx"
import React, { useContext, useState } from "react"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useStoreChatModelSettings } from "@/store/model.tsx"
import { Card, Tooltip } from "antd"
import { PageAssitDatabase } from "@/db"
import { EraserIcon } from "lucide-react"
import { useTranslation } from "react-i18next"
import { useQueryClient } from "@tanstack/react-query"
import { HistoryContext } from "@/components/Layouts/Layout.tsx"
export const PlaygroundHistory = () => {
const { setSystemPrompt } = useStoreChatModelSettings()
const { show, setShow } = useContext(HistoryContext)
const {
setMessages,
setHistory,
setHistoryId,
historyId,
clearChat,
setSelectedModel,
temporaryChat,
setSelectedSystemPrompt
} = useMessageOption()
const { t } = useTranslation(["option", "common", "settings"])
const queryClient = useQueryClient()
return (
<Card
className={`flex flex-col [&>:nth-child(2)]:flex-1 [&>:nth-child(2)]:overflow-y-auto w-[300px] h-full pt-16 pb-5 transition-all duration-300 ease-in-out transform ${
show
? 'opacity-100 translate-x-0'
: 'opacity-0 -translate-x-full absolute'
}`}
style={{ paddingTop: "4rem" }}
title={
<div className="flex items-center justify-between w-full">
{t("sidebarTitle")}
<Tooltip
title={t("settings:generalSettings.system.deleteChatHistory.label")}
placement="right">
<button
onClick={async () => {
const confirm = window.confirm(
t("settings:generalSettings.system.deleteChatHistory.confirm")
)
if (confirm) {
const db = new PageAssitDatabase()
await db.deleteAllChatHistory()
await queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
clearChat()
}
}}
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100">
<EraserIcon className="size-5" />
</button>
</Tooltip>
</div>
}>
<div className="overflow-y-auto">
<Sidebar
onClose={() => setShow(true)}
setMessages={setMessages}
setHistory={setHistory}
setHistoryId={setHistoryId}
setSelectedModel={setSelectedModel}
setSelectedSystemPrompt={setSelectedSystemPrompt}
clearChat={clearChat}
historyId={historyId}
setSystemPrompt={setSystemPrompt}
temporaryChat={temporaryChat}
history={history}
/>
</div>
</Card>
)
}

View File

@ -0,0 +1,95 @@
import React from 'react';
const SuccessIcon = () => {
return (<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-full h-full text-green-500">
<path
d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>)
}
const LoadingIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 24 24"
width="18"
height="18">
<defs></defs>
<g>
<path
fill="rgb(59, 130, 246)"
d="M13 2.03v2.02c4.39.54 7.5 4.53 6.96 8.92c-.46 3.64-3.32 6.53-6.96 6.96v2c5.5-.55 9.5-5.43 8.95-10.93c-.45-4.75-4.22-8.5-8.95-8.97m-2 .03c-1.95.19-3.81.94-5.33 2.2L7.1 5.74c1.12-.9 2.47-1.48 3.9-1.68zM4.26 5.67A9.9 9.9 0 0 0 2.05 11h2c.19-1.42.75-2.77 1.64-3.9zM2.06 13c.2 1.96.97 3.81 2.21 5.33l1.42-1.43A8 8 0 0 1 4.06 13zm5.04 5.37l-1.43 1.37A10 10 0 0 0 11 22v-2a8 8 0 0 1-3.9-1.63M12.5 7v5.25l4.5 2.67l-.75 1.23L11 13V7z"></path>
</g>
</svg>
)
}
export const PlaygroundIodRelevant: React.FC = () => {
const data = [
{
title: "已在29个科学数据中心的50万个科学数据集中进行搜索",
description: "已发现4个数据集",
status: "success"
},
{
title: "已在100万篇论文、2800个科创场景中进行搜索",
description: "已发现4个数据集",
status: "success"
},
{
title: "正在1000位智库专家、12万个创新机构中进行搜索",
status: "loading"
},
]
for (let i = 0; i < 10; i++) {
data.push({
title: "已在29个科学数据中心的50万个科学数据集中进行搜索" + i,
description: "已发现4个数据集",
status: "success"
})
}
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold text-gray-900">
</h2>
<span className="text-sm text-blue-600 font-medium">{data.length}</span>
</div>
{/* Content */}
<div className="space-y-3 flex-1 overflow-y-auto">
{
data.map((item, index) => (
<div className="flex items-start space-x-3">
<div className="w-5 h-5 mt-1 flex-shrink-0">
{item.status === "success" ? <SuccessIcon /> : <LoadingIcon />}
</div>
<div className="flex-1">
<p className="text-sm text-gray-700">
{item.title}
</p>
{item.description && <p className="text-xs text-gray-500 mt-1">{item.description}</p>}
</div>
</div>
))
}
</div>
</div>
)
}

View File

@ -58,7 +58,7 @@ export const PlaygroundMessage = (props: Props) => {
const { t } = useTranslation("common") const { t } = useTranslation("common")
const { cancel, isSpeaking, speak } = useTTS() const { cancel, isSpeaking, speak } = useTTS()
return ( return (
<div className="group relative flex w-full max-w-3xl flex-col items-end justify-center pb-2 md:px-4 lg:w-4/5 text-gray-800 dark:text-gray-100"> <div className="group relative flex w-full flex-col items-end justify-center pb-2 md:px-4 text-gray-800 dark:text-gray-100">
{/* <div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> */} {/* <div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> */}
<div className="flex flex-row gap-4 md:gap-6 my-2 m-auto w-full"> <div className="flex flex-row gap-4 md:gap-6 my-2 m-auto w-full">
<div className="w-8 flex flex-col relative items-end"> <div className="w-8 flex flex-col relative items-end">
@ -138,7 +138,7 @@ export const PlaygroundMessage = (props: Props) => {
</> </>
) : ( ) : (
<p <p
className={`prose dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${ className={`prose-lg dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${
props.message_type && props.message_type &&
"italic text-gray-500 dark:text-gray-400 text-sm" "italic text-gray-500 dark:text-gray-400 text-sm"
}`}> }`}>

View File

@ -0,0 +1,142 @@
import React, { useState } from "react"
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Drawer, List } from "antd"
export const PlaygroundScene = () => {
// 模拟数据
const data = [
{
title: "绿色化工工艺项目",
description:
"基于生物基原料采用repeal2.0可降解材料技术,开发新型环保材料。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "智能农业解决方案",
description: "利用物联网技术,实现精准农业管理,提高农作物产量。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "新能源汽车电池技术",
description: "研发高能量密度、长寿命的新型电池材料,推动电动汽车发展。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "碳捕集与封存技术",
description: "开发高效的碳捕集技术,减少工业排放,助力碳中和目标。",
demander: "奥赛康药业 供方美国Propella公司"
}
]
for (let i = 0; i < 10; i++) {
data.push({
title: "开发新型电池材料",
description: "研发高能量密度、长寿命的新型电池材料,推动电动汽车发展。",
demander: "奥赛康药业 供方美国Propella公司"
})
}
const [open, setOpen] = useState(false)
const showDrawer = () => {
setOpen(true)
}
const onClose = () => {
setOpen(false)
}
return (
<div className="h-full overflow-y-hidden flex flex-col">
{/* 数据导航 */}
<DataNavigation
Header={
<div className="flex items-center text-[#15803d] gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 32 32"
width="20"
height="20">
<defs></defs>
<g>
<path
fill="rgb(21, 128, 61)"
d="M16 18H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2M6 6v10h10V6zm20 6v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2m0 12v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2m-10 2v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2"></path>
</g>
</svg>
</div>
}
onClick={showDrawer}
/>
{/* 场景列表 */}
<div className="space-y-1.5 flex-1 overflow-y-auto">
{data.slice(0,3).map((item, index) => (
<Card key={index} hoverable className="[&>*:first-child]:!p-4 h-[148px]" >
<div className="flex flex-col gap-0.5">
<h3 className="text-sm font-medium text-gray-900 line-clamp-2">
{item.title}
</h3>
<p className="flex items-center gap-1.5">
<span className="inline-block bg-blue-100 text-green-800 text-xs px-2 py-1 rounded-full">
</span>
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
</span>
</p>
<p className="text-xs text-gray-500 truncate">{item.demander}</p>
<span className="text-gray-700 line-clamp-2">
{item.description}
</span>
</div>
</Card>
))}
</div>
{/* 抽屉 */}
<Drawer
title="相关场景"
closable={{ "aria-label": "Close Button" }}
onClose={onClose}
open={open}
width={600}>
<List
itemLayout="vertical"
dataSource={data}
renderItem={(item, index) => (
<List.Item>
<List.Item.Meta
title={
<h3 className="text-sm font-medium text-gray-900">
{item.title}
</h3>
}
description={
<div className="space-y-1">
<p className="flex items-center gap-1.5">
<span className="inline-block bg-blue-100 text-green-800 text-xs px-2 py-1 rounded-full">
</span>
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
</span>
</p>
<p className="text-xs text-gray-500">
{item.demander}
</p>
<span className="text-gray-700">
{item.description}
</span>
</div>
}
/>
</List.Item>
)}
/>
</Drawer>
</div>
)
}

View File

@ -0,0 +1,136 @@
import React, { useState } from "react"
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Drawer, List } from "antd"
export const PlaygroundTeam = () => {
// 模拟数据
const data = [
{
title: "绿色化工工艺项目",
description:
"基于生物基原料采用repeal2.0可降解材料技术,开发新型环保材料。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "智能农业解决方案",
description: "利用物联网技术,实现精准农业管理,提高农作物产量。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "新能源汽车电池技术",
description: "研发高能量密度、长寿命的新型电池材料,推动电动汽车发展。",
demander: "奥赛康药业 供方美国Propella公司"
},
{
title: "碳捕集与封存技术",
description: "开发高效的碳捕集技术,减少工业排放,助力碳中和目标。",
demander: "奥赛康药业 供方美国Propella公司"
}
]
for (let i = 0; i < 10; i++) {
data.push({
title: "开发新型电池材料",
description: "研发高能量密度、长寿命的新型电池材料,推动电动汽车发展。",
demander: "奥赛康药业 供方美国Propella公司"
})
}
const [open, setOpen] = useState(false)
const showDrawer = () => {
setOpen(true)
}
const onClose = () => {
setOpen(false)
}
return (
<div className="h-full overflow-y-hidden flex flex-col">
{/* 数据导航 */}
<DataNavigation
Header={
<div className="flex items-center text-[#15803d] gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
className="styles__StyledSVGIconPathComponent-sc-i3aj97-0 bxMexi svg-icon-path-icon"
viewBox="0 0 32 32"
width="20"
height="20">
<defs></defs>
<g>
<path
fill="rgb(21, 128, 61)"
d="M16 18H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2M6 6v10h10V6zm20 6v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2m0 12v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2m-10 2v4h-4v-4zm0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2"></path>
</g>
</svg>
</div>
}
onClick={showDrawer}
/>
{/* 场景列表 */}
<div className="grid grid-cols-2 gap-3">
{data.slice(0,2).map((item, index) => (
<Card key={index} hoverable className="[&>*:first-child]:!p-3" >
<div className="flex flex-col gap-0.5">
<h3 className="text-sm font-medium text-gray-900 line-clamp-2">
{item.title}
</h3>
<p className="flex items-center gap-1.5">
<span className="inline-block bg-blue-100 text-green-800 text-xs px-2 py-1 rounded-full">
</span>
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
</span>
</p>
<p className="text-xs text-gray-500 line-clamp-2">{item.description}</p>
</div>
</Card>
))}
</div>
{/* 抽屉 */}
<Drawer
title="相关团队"
closable={{ "aria-label": "Close Button" }}
onClose={onClose}
open={open}
width={600}>
<List
itemLayout="vertical"
dataSource={data}
renderItem={(item, index) => (
<List.Item>
<List.Item.Meta
title={
<h3 className="text-sm font-medium text-gray-900">
{item.title}
</h3>
}
description={
<div className="space-y-1">
<p className="flex items-center gap-1.5">
<span className="inline-block bg-blue-100 text-green-800 text-xs px-2 py-1 rounded-full">
</span>
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
</span>
</p>
<p className="text-xs text-gray-500">
{item.description}
</p>
</div>
}
/>
</List.Item>
)}
/>
</Drawer>
</div>
)
}

View File

@ -0,0 +1,46 @@
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Descriptions, DescriptionsProps, Drawer, List, Spin } from "antd"
import { useCallback, useMemo, useState } from "react"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useStoreMessageOption } from "@/store/option.tsx"
export const PlaygroundTokenStatistics = () => {
const { currentMeteringEntry } = useStoreMessageOption()
const items = useMemo<DescriptionsProps["items"]>(() => {
const { data } = currentMeteringEntry
return [
// {
// key: "relatedDataCount",
// label: "关联数据个数",
// children: data.relatedDataCount
// },
{
key: "iodTokenCount",
label: "数联网引用token总数",
children: data.iodTokenCount ?? 0
},
{
key: "modelInputTokenCount",
label: "大模型输入token数量",
children: data.modelInputTokenCount ?? 0
},
{
key: "modelOutputTokenCount",
label: "大模型输出token数量",
children: data.modelOutputTokenCount ?? 0
}
]
}, [currentMeteringEntry])
return (
<Card
style={{ marginBottom: "1rem" }}
className="h-full"
title={<DataNavigation title="Token统计" showButton={false} />}>
<Spin spinning={currentMeteringEntry.loading}>
<Descriptions layout="horizontal" items={items} column={2} />
</Spin>
</Card>
)
}

View File

@ -24,18 +24,24 @@ import { ProviderIcons } from "../Common/ProviderIcon"
import { NewChat } from "./NewChat" import { NewChat } from "./NewChat"
import { PageAssistSelect } from "../Select" import { PageAssistSelect } from "../Select"
import { MoreOptions } from "./MoreOptions" import { MoreOptions } from "./MoreOptions"
import { useContext } from "react"
import { HistoryContext } from "@/components/Layouts/Layout.tsx"
type Props = { type Props = {
setSidebarOpen: (open: boolean) => void sidebarOpen: boolean
setSidebarOpen: () => void
setOpenModelSettings: (open: boolean) => void setOpenModelSettings: (open: boolean) => void
} }
export const Header: React.FC<Props> = ({ export const Header: React.FC<Props> = ({
setOpenModelSettings, setOpenModelSettings,
setSidebarOpen setSidebarOpen,
sidebarOpen
}) => { }) => {
const { t, i18n } = useTranslation(["option", "common"]) const { t, i18n } = useTranslation(["option", "common"])
const isRTL = i18n?.dir() === "rtl" const isRTL = i18n?.dir() === "rtl"
const [shareModeEnabled] = useStorage("shareMode", false) const [shareModeEnabled] = useStorage("shareMode", false)
const [hideCurrentChatModelSettings] = useStorage( const [hideCurrentChatModelSettings] = useStorage(
"hideCurrentChatModelSettings", "hideCurrentChatModelSettings",
@ -109,10 +115,16 @@ export const Header: React.FC<Props> = ({
</NavLink> </NavLink>
</div> </div>
)} )}
<div> <div style={{width: sidebarOpen ? "288px" : "205px"}} className="flex items-center justify-between transition-all duration-300 ease-in-out">
<h2
className="text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3"
style={{ lineHeight: "0" }}>
{t("projectTitle")}
</h2>
<button <button
className="text-gray-500 dark:text-gray-400" className="text-gray-500 dark:text-gray-400"
onClick={() => setSidebarOpen(true)}> onClick={() => setSidebarOpen()}>
<PanelLeftIcon className="w-6 h-6" /> <PanelLeftIcon className="w-6 h-6" />
</button> </button>
</div> </div>

View File

@ -1,103 +1,50 @@
import React, { useState } from "react" import React, { useCallback, useEffect, useState } from "react"
import { Sidebar } from "../Option/Sidebar"
import { Drawer, Tooltip } from "antd"
import { useTranslation } from "react-i18next"
import { CurrentChatModelSettings } from "../Common/Settings/CurrentChatModelSettings" import { CurrentChatModelSettings } from "../Common/Settings/CurrentChatModelSettings"
import { Header } from "./Header" import { Header } from "./Header"
import { EraserIcon } from "lucide-react"
import { PageAssitDatabase } from "@/db" interface History {
import { useMessageOption } from "@/hooks/useMessageOption" show: boolean
import { useQueryClient } from "@tanstack/react-query" setShow: (show: boolean) => void
import { useStoreChatModelSettings } from "@/store/model" }
export const HistoryContext = React.createContext<History>({
show: true,
setShow: () => {}
})
export default function OptionLayout({ export default function OptionLayout({
children children
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
const [sidebarOpen, setSidebarOpen] = useState(false) const [showHistory, setShowHistory] = useState(true)
const { t } = useTranslation(["option", "common", "settings"])
const [openModelSettings, setOpenModelSettings] = useState(false) const [openModelSettings, setOpenModelSettings] = useState(false)
const {
setMessages,
setHistory,
setHistoryId,
historyId,
clearChat,
setSelectedModel,
temporaryChat,
setSelectedSystemPrompt
} = useMessageOption()
const queryClient = useQueryClient() const historyContextValue = {
const { setSystemPrompt } = useStoreChatModelSettings() show: showHistory,
setShow: setShowHistory
}
const useToggle = useCallback(() => {
setShowHistory(!showHistory)
}, [showHistory])
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<main className="relative h-dvh w-full"> <main className="relative h-dvh w-full">
<div className="relative z-10 w-full"> <div className="relative z-10 w-full">
<Header <Header
setSidebarOpen={setSidebarOpen} sidebarOpen={showHistory}
setSidebarOpen={useToggle}
setOpenModelSettings={setOpenModelSettings} setOpenModelSettings={setOpenModelSettings}
/> />
</div> </div>
{/* <div className="relative flex h-full flex-col items-center"> */} {/* <div className="relative flex h-full flex-col items-center"> */}
{children} <HistoryContext.Provider value={historyContextValue}>
{children}
</HistoryContext.Provider>
{/* </div> */} {/* </div> */}
<Drawer
title={
<div className="flex items-center justify-between">
{t("sidebarTitle")}
<Tooltip
title={t(
"settings:generalSettings.system.deleteChatHistory.label"
)}
placement="right">
<button
onClick={async () => {
const confirm = window.confirm(
t(
"settings:generalSettings.system.deleteChatHistory.confirm"
)
)
if (confirm) {
const db = new PageAssitDatabase()
await db.deleteAllChatHistory()
await queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
clearChat()
}
}}
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100">
<EraserIcon className="size-5" />
</button>
</Tooltip>
</div>
}
placement="left"
closeIcon={null}
onClose={() => setSidebarOpen(false)}
open={sidebarOpen}>
<Sidebar
onClose={() => setSidebarOpen(false)}
setMessages={setMessages}
setHistory={setHistory}
setHistoryId={setHistoryId}
setSelectedModel={setSelectedModel}
setSelectedSystemPrompt={setSelectedSystemPrompt}
clearChat={clearChat}
historyId={historyId}
setSystemPrompt={setSystemPrompt}
temporaryChat={temporaryChat}
history={history}
/>
</Drawer>
<CurrentChatModelSettings <CurrentChatModelSettings
open={openModelSettings} open={openModelSettings}

View File

@ -1,8 +1,14 @@
import React from "react" import React from "react"
import { Card } from "antd"
import { PlaygroundForm } from "./PlaygroundForm" import { PlaygroundForm } from "./PlaygroundForm"
import { PlaygroundChat } from "./PlaygroundChat" import { PlaygroundChat } from "./PlaygroundChat"
import { useMessageOption } from "@/hooks/useMessageOption" import { useMessageOption } from "@/hooks/useMessageOption"
import { webUIResumeLastChat } from "@/services/app" import { webUIResumeLastChat } from "@/services/app"
import { PlaygroundData } from '@/components/Common/Playground/Data.tsx'
import { PlaygroundScene } from "@/components/Common/Playground/Scene.tsx"
import { import {
formatToChatHistory, formatToChatHistory,
formatToMessage, formatToMessage,
@ -13,6 +19,11 @@ import { getLastUsedChatSystemPrompt } from "@/services/model-settings"
import { useStoreChatModelSettings } from "@/store/model" import { useStoreChatModelSettings } from "@/store/model"
import { useSmartScroll } from "@/hooks/useSmartScroll" import { useSmartScroll } from "@/hooks/useSmartScroll"
import { ChevronDown } from "lucide-react" import { ChevronDown } from "lucide-react"
import { PlaygroundTeam } from "@/components/Common/Playground/Team.tsx"
import { PlaygroundTokenStatistics } from "@/components/Common/Playground/TokenStatistics.tsx"
import { PlaygroundHistory } from "@/components/Common/Playground/History.tsx"
import { PlaygroundIodRelevant } from "@/components/Common/Playground/IodRelevant.tsx"
export const Playground = () => { export const Playground = () => {
const drop = React.useRef<HTMLDivElement>(null) const drop = React.useRef<HTMLDivElement>(null)
@ -132,26 +143,43 @@ export const Playground = () => {
return ( return (
<div <div
ref={drop} ref={drop}
className={`relative flex h-full flex-col items-center ${ className={`relative flex gap-3 h-full items-center ${
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800" : "" dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800" : ""
} bg-white dark:bg-[#171717]`}> } bg-white dark:bg-[#171717]`}>
<div <PlaygroundHistory />
ref={containerRef} <div className="relative h-full flex-1 prose-lg flex justify-center [&>*]:max-w-[848px]">
className="custom-scrollbar bg-bottom-mask-light dark:bg-bottom-mask-dark mask-bottom-fade will-change-mask flex h-full w-full flex-col items-center overflow-x-hidden overflow-y-auto px-5"> <div
<PlaygroundChat /> ref={containerRef}
className="custom-scrollbar bg-bottom-mask-light dark:bg-bottom-mask-dark mask-bottom-fade will-change-mask flex h-full w-full flex-col items-center overflow-x-hidden overflow-y-auto px-5">
<PlaygroundChat />
</div>
<div className="absolute bottom-0 w-full">
{!isAtBottom && (
<div className="absolute bottom-36 z-20 left-0 right-0 flex justify-center">
<button
onClick={scrollToBottom}
className="bg-gray-50 shadow border border-gray-200 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto">
<ChevronDown className="size-4 text-gray-600 dark:text-gray-300" />
</button>
</div>
)}
<PlaygroundForm dropedFile={dropedFile} />
</div>
</div> </div>
<div className="absolute bottom-0 w-full"> {messages.length && (
{!isAtBottom && ( <div className="w-1/4 h-full grid grid-rows-[auto_530px_165px] pt-16 pr-5 pb-0 border-l border-gray-200" style={{"paddingTop": "4rem"}}>
<div className="fixed bottom-36 z-20 left-0 right-0 flex justify-center"> <div className="w-full overflow-y-auto border-gray-200 border-b p-3">
<button <PlaygroundIodRelevant />
onClick={scrollToBottom}
className="bg-gray-50 shadow border border-gray-200 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto">
<ChevronDown className="size-4 text-gray-600 dark:text-gray-300" />
</button>
</div> </div>
)} <div className="w-full grid grid-cols-2 gap-3 custom-scrollbar border-gray-200 border-b p-3">
<PlaygroundForm dropedFile={dropedFile} /> <PlaygroundData />
</div> <PlaygroundScene />
</div>
<div className="w-full p-3 pb-0">
<PlaygroundTeam />
</div>
</div>
)}
</div> </div>
) )
} }

View File

@ -20,7 +20,7 @@ export const PlaygroundChat = () => {
<> <>
<div className="relative flex w-full flex-col items-center pt-16 pb-4"> <div className="relative flex w-full flex-col items-center pt-16 pb-4">
{messages.length === 0 && ( {messages.length === 0 && (
<div className="mt-32 w-full"> <div className="mt-3 w-full">
<PlaygroundEmpty /> <PlaygroundEmpty />
</div> </div>
)} )}

View File

@ -1,130 +1,113 @@
import { cleanUrl } from "@/libs/clean-url" import { Card, Col, Row } from "antd"
import { useStorage } from "@plasmohq/storage/hook"
import { useQuery } from "@tanstack/react-query" import RocketSvg from '@/assets/icons/rocket.svg'
import { RotateCcw } from "lucide-react" import BulbSvg from '@/assets/icons/bulb.svg'
import { useEffect, useState } from "react" import EyeSvg from '@/assets/icons/eye.svg'
import { Trans, useTranslation } from "react-i18next" import ASvg from '@/assets/icons/a.svg'
import { import BSvg from '@/assets/icons/b.svg'
getOllamaURL, import CSvg from '@/assets/icons/c.svg'
isOllamaRunning, import DSvg from '@/assets/icons/d.svg'
setOllamaURL as saveOllamaURL import ESvg from '@/assets/icons/e.svg'
} from "~/services/ollama" import FSvg from '@/assets/icons/f.svg'
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useMutation, useQueryClient } from "@tanstack/react-query"
export const PlaygroundEmpty = () => { export const PlaygroundEmpty = () => {
const [ollamaURL, setOllamaURL] = useState<string>("")
const { t } = useTranslation(["playground", "common"])
const [checkOllamaStatus] = useStorage("checkOllamaStatus", true)
const { const {
data: ollamaInfo, onSubmit,
status: ollamaStatus, setMessages,
refetch, setHistory,
isRefetching setHistoryId,
} = useQuery({ historyId,
queryKey: ["ollamaStatus"], clearChat,
queryFn: async () => { setSelectedModel,
const ollamaURL = await getOllamaURL() temporaryChat,
const isOk = await isOllamaRunning() setSelectedSystemPrompt
} = useMessageOption()
if (ollamaURL) { const queryClient = useQueryClient()
saveOllamaURL(ollamaURL)
}
return {
isOk, const questions = [
ollamaURL {
} title: "如何开发一个适合超大型城市的碳普惠方法学?",
icon: <img src={RocketSvg} alt="Rocket" className="w-10 my-0" />,
}, },
enabled: checkOllamaStatus {
title: "如何开发一个零碳园区的数字化评价系统?",
icon: <img src={BulbSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "如何开发一个碳定价预测系统?",
icon: <img src={EyeSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "新药临床研究如何提升实验安全性?",
icon: <img src={ASvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "如何加速新药申报和审批?",
icon: <img src={BSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "如何研制与司美格鲁肽相似的新药?",
icon: <img src={CSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "如何解决固态电池的成本和寿命难题?",
icon: <img src={DSvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "如何解决船舶制造中的材料腐蚀难题?",
icon: <img src={ESvg} alt="Rocket" className="w-10 my-0" />,
},
{
title: "如何解决船舶制造中流体模拟和建模优化难题?",
icon: <img src={FSvg} alt="Rocket" className="w-10 my-0" />,
},
]
const { mutateAsync: sendMessage } = useMutation({
mutationFn: onSubmit,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
}
}) })
useEffect(() => {
if (ollamaInfo?.ollamaURL) {
setOllamaURL(ollamaInfo.ollamaURL)
}
}, [ollamaInfo])
function handleQuestion(message: string) {
if (!checkOllamaStatus) { void sendMessage({message, image: ''})
return (
<div className="mx-auto sm:max-w-xl px-4 mt-10">
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-gray-50 dark:bg-[#262626] dark:border-gray-600">
<h1 className="text-sm font-medium text-center text-gray-500 dark:text-gray-400 flex gap-3 items-center justify-center">
<span >👋</span>
<span className="text-gray-700 dark:text-gray-300">
{t("welcome")}
</span>
</h1>
</div>
</div>
)
} }
return ( return (
<div className="mx-auto sm:max-w-xl px-4 mt-10"> <div className="w-full p-4">
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-gray-50 dark:bg-[#262626] dark:border-gray-600"> {/* 标题区域 */}
{(ollamaStatus === "pending" || isRefetching) && ( <div className="mb-4">
<div className="inline-flex items-center space-x-2"> <h2 className="text-xl font-bold text-gray-800" style={{lineHeight: '0'}}></h2>
<div className="w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div> <p className="text-sm text-gray-500"></p>
<p className="dark:text-gray-400 text-gray-900">
{t("ollamaState.searching")}
</p>
</div>
)}
{!isRefetching && ollamaStatus === "success" ? (
ollamaInfo.isOk ? (
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
<p className="dark:text-gray-400 text-gray-900">
{t("ollamaState.running")}
</p>
</div>
) : (
<div className="flex flex-col space-y-2 justify-center items-center">
<div className="inline-flex space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
<p className="dark:text-gray-400 text-gray-900">
{t("ollamaState.notRunning")}
</p>
</div>
<input
className="bg-gray-100 dark:bg-[#262626] dark:text-gray-100 rounded-md px-4 py-2 mt-2 w-full"
type="url"
value={ollamaURL}
onChange={(e) => setOllamaURL(e.target.value)}
/>
<button
onClick={() => {
saveOllamaURL(ollamaURL)
refetch()
}}
className="inline-flex mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
<RotateCcw className="h-4 w-4 mr-3" />
{t("common:retry")}
</button>
{ollamaURL &&
cleanUrl(ollamaURL) !== "http://127.0.0.1:11434" && (
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4 text-center">
<Trans
i18nKey="playground:ollamaState.connectionError"
components={{
anchor: (
<a
href="https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md"
target="__blank"
className="text-blue-600 dark:text-blue-400"></a>
)
}}
/>
</p>
)}
</div>
)
) : null}
</div> </div>
{/* 卡片网格布局 */}
<Row gutter={[16, 16]} className="w-full">
{questions.map((item, index) => (
<Col key={index} xs={24} sm={12} md={8}>
<Card
hoverable
style={{backgroundColor: "#f3f4f6"}}
className="border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 cursor-pointer"
onClick={() => handleQuestion(item.title)}
>
<div className="flex items-center">
<div className="text-blue-500 mr-2">{item.icon}</div>
<div className="font-medium text-sm text-gray-800">{item.title}</div>
</div>
</Card>
</Col>
))}
</Row>
</div> </div>
) )
} }

View File

@ -207,11 +207,11 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
} }
return ( return (
<div className="flex w-full flex-col items-center p-2 pt-1 pb-4"> <div className="flex w-full flex-col items-center p-2 px-5 pt-1 pb-4">
<div className="relative z-10 flex w-full flex-col items-center justify-center gap-2 text-base"> <div className="relative z-10 flex w-full flex-col items-center justify-center gap-2 text-base">
<div className="relative flex w-full flex-row justify-center gap-2 lg:w-4/5"> <div className="relative flex w-full flex-row justify-center gap-2 lg:w-5/5">
<div <div
className={` bg-neutral-50 dark:bg-[#262626] relative w-full max-w-[48rem] p-1 backdrop-blur-lg duration-100 border border-gray-300 rounded-xl dark:border-gray-600 className={` bg-neutral-50 dark:bg-[#262626] relative w-full max-w-[65rem] p-1 backdrop-blur-lg duration-100 border border-gray-300 rounded-xl dark:border-gray-600
${temporaryChat ? "!bg-gray-200 dark:!bg-black " : ""} ${temporaryChat ? "!bg-gray-200 dark:!bg-black " : ""}
`}> `}>
<div <div

View File

@ -60,6 +60,7 @@ export const useMessageOption = () => {
setHistory, setHistory,
meteringEntries, meteringEntries,
setMeteringEntries, setMeteringEntries,
setCurrentMeteringEntry,
setStreaming, setStreaming,
streaming, streaming,
setIsFirstMessage, setIsFirstMessage,
@ -200,6 +201,11 @@ export const useMessageOption = () => {
date: new Date().getTime() date: new Date().getTime()
} as MeteringEntry } as MeteringEntry
setCurrentMeteringEntry({
loading: true,
data: meter
})
if (!isRegenerate) { if (!isRegenerate) {
newMessage = [ newMessage = [
...messages, ...messages,
@ -500,7 +506,7 @@ export const useMessageOption = () => {
// Save metering entry // Save metering entry
const { cot, content } = responseResolver(fullText) const { cot, content } = responseResolver(fullText)
const _meteringEntries = [{ const currentMeteringEntry = {
...meter, ...meter,
modelInputTokenCount: prompt.length, modelInputTokenCount: prompt.length,
modelOutputTokenCount: fullText.length, modelOutputTokenCount: fullText.length,
@ -511,9 +517,15 @@ export const useMessageOption = () => {
cot, cot,
responseContent: content, responseContent: content,
modelResponseContent: fullText, modelResponseContent: fullText,
}, }
const _meteringEntries = [
currentMeteringEntry,
...meteringEntries, ...meteringEntries,
] ]
setCurrentMeteringEntry({
loading: false,
data: currentMeteringEntry,
})
setMeteringEntries(_meteringEntries) setMeteringEntries(_meteringEntries)
localStorage.setItem("meteringEntries", JSON.stringify(_meteringEntries)) localStorage.setItem("meteringEntries", JSON.stringify(_meteringEntries))
} catch (e) { } catch (e) {
@ -633,6 +645,16 @@ export const useMessageOption = () => {
let newMessage: Message[] = [] let newMessage: Message[] = []
let generateMessageId = generateID() let generateMessageId = generateID()
const meter: MeteringEntry = {
id: generateMessageId,
queryContent: message,
date: new Date().getTime()
} as MeteringEntry
setCurrentMeteringEntry({
loading: true,
data: meter,
})
if (!isRegenerate) { if (!isRegenerate) {
newMessage = [ newMessage = [
@ -759,6 +781,7 @@ export const useMessageOption = () => {
let reasoningStartTime: Date | null = null let reasoningStartTime: Date | null = null
let reasoningEndTime: Date | null = null let reasoningEndTime: Date | null = null
let apiReasoning: boolean = false let apiReasoning: boolean = false
const chatStartTime = new Date()
for await (const chunk of chunks) { for await (const chunk of chunks) {
if (chunk?.additional_kwargs?.reasoning_content) { if (chunk?.additional_kwargs?.reasoning_content) {
@ -859,6 +882,31 @@ export const useMessageOption = () => {
setStreaming(false) setStreaming(false)
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
// Save metering entry
const { cot, content } = responseResolver(fullText)
const currentMeteringEntry = {
...meter,
modelInputTokenCount: prompt.length,
modelOutputTokenCount: fullText.length,
model: ollama.modelName ?? ollama.model,
relatedDataCount: 0,
timeTaken: new Date().getTime() - chatStartTime.getTime(),
date: chatStartTime.getTime(),
cot,
responseContent: content,
modelResponseContent: fullText,
}
const _meteringEntries = [
currentMeteringEntry,
...meteringEntries,
]
setCurrentMeteringEntry({
loading: false,
data: currentMeteringEntry,
})
setMeteringEntries(_meteringEntries)
} catch (e) { } catch (e) {
const errorSave = await saveMessageOnError({ const errorSave = await saveMessageOnError({
e, e,

View File

@ -21,8 +21,27 @@ const DEFAULT_RAG_QUESTION_PROMPT =
const DEFAUTL_RAG_SYSTEM_PROMPT = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say you don't know. DO NOT try to make up an answer. If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context. {context} Question: {question} Helpful answer:` const DEFAUTL_RAG_SYSTEM_PROMPT = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say you don't know. DO NOT try to make up an answer. If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context. {context} Question: {question} Helpful answer:`
const DEFAULT_WEBSEARCH_PROMPT = `你是一个中文AI助手。根据用户的问题请筛选出<iod-search-results>中的相关信息,并结合<iod-search-results>中的数据、场景(项目)、人员团队等类型的相关信息,
<iod-search-results> {current_date_time}.
<iod-search-results> Internet of Data)
\`<result doId="{doId}" name="{title}" authors="{authors}" dataType="{paper,dataset or algorithm}" year="{year}" url="{url}" id="{id}">{abstract}</result>\`
\`doId\`\`name\` :
\`[IoD source [id] doId: {doId} "{name}"]({url})\`
Or in Chinese:
\`[数联网引用[id] doId: {doId} "{name}"]({url})\`
For example, in English:
\`[IoD source [1] doId: 10.48550/arXiv.1803.05591v2 "On the insufficiency of existing momentum schemes for Stochastic Optimization"](http://arxiv.org/pdf/1803.05591v2.pdf)\`
Or in Chinese:
\`[数联网引用[1] doId: 10.48550/arXiv.1803.05591v2 "On the insufficiency of existing momentum schemes for Stochastic Optimization"](http://arxiv.org/pdf/1803.05591v2.pdf)\`
const DEFAULT_WEBSEARCH_PROMPT = `You are an AI assistant specialized in retrieving and analyzing academic papers from Neo4j graph database. <iod-search-results>
{iod_search_results}
</iod-search-results>
`
const DEFAULT_WEBSEARCH_PROMPT2 = `You are an AI assistant specialized in retrieving and analyzing academic papers from Neo4j graph database.
Generate a response that how can user achieve his request based on provided search results. The current date and time are {current_date_time}. Generate a response that how can user achieve his request based on provided search results. The current date and time are {current_date_time}.
@ -48,7 +67,6 @@ Use this information to generate a meaningful response that includes:
<iod-search-results> <iod-search-results>
{iod_search_results} {iod_search_results}
</iod-search-results> </iod-search-results>
` `
const DEFAULT_WEBSEARCH2_PROMPT = `You are an AI model who is expert at searching the web and answering user's queries. const DEFAULT_WEBSEARCH2_PROMPT = `You are an AI model who is expert at searching the web and answering user's queries.

View File

@ -35,6 +35,8 @@ type State = {
setMessages: (messages: Message[]) => void setMessages: (messages: Message[]) => void
history: ChatHistory history: ChatHistory
setHistory: (history: ChatHistory) => void setHistory: (history: ChatHistory) => void
currentMeteringEntry: {data: MeteringEntry, loading: boolean}
setCurrentMeteringEntry: (meteringEntry: {data: MeteringEntry, loading: boolean}) => void
meteringEntries: MeteringEntry[] meteringEntries: MeteringEntry[]
setMeteringEntries: (meteringEntries: MeteringEntry[]) => void setMeteringEntries: (meteringEntries: MeteringEntry[]) => void
streaming: boolean streaming: boolean
@ -120,6 +122,8 @@ export const useStoreMessageOption = create<State>((set) => ({
setMessages: (messages) => set({ messages }), setMessages: (messages) => set({ messages }),
history: [], history: [],
setHistory: (history) => set({ history }), setHistory: (history) => set({ history }),
currentMeteringEntry: {data: {} as MeteringEntry, loading: false},
setCurrentMeteringEntry: (currentMeteringEntry) => set({ currentMeteringEntry }),
meteringEntries: JSON.parse(localStorage.getItem("meteringEntries") || JSON.stringify([])), meteringEntries: JSON.parse(localStorage.getItem("meteringEntries") || JSON.stringify([])),
setMeteringEntries: (meteringEntries) => set({ meteringEntries }), setMeteringEntries: (meteringEntries) => set({ meteringEntries }),
streaming: false, streaming: false,

View File

@ -26,6 +26,12 @@ export const tokenizeInput = function (input: string): string[] {
} }
//doipUrl = tcp://reg01.public.internetofdata.cn:21037 //doipUrl = tcp://reg01.public.internetofdata.cn:21037
export const iodConfig = { export const iodConfig = {
"gatewayUrl": "tcp://reg01.public.internetofdata.cn:21037",
"registry":"data/Registry",
"localRepository":"data/Repository",
"doBrowser":"http://021.node.internetapi.cn:21030/SCIDE/SCManager"
}
export const iodConfigLocal = {
"gatewayUrl": "tcp://127.0.0.1:21036", "gatewayUrl": "tcp://127.0.0.1:21036",
"registry":"bdware/Registry", "registry":"bdware/Registry",
"localRepository":"bdtest.local/myrepo1", "localRepository":"bdtest.local/myrepo1",
@ -34,6 +40,51 @@ export const iodConfig = {
function inGrepList(str: string){ function inGrepList(str: string){
return "什么|问题|需要|合适|设计|考虑|合作|精度|传感器|最新|研究|药物".indexOf(str)!=-1; return "什么|问题|需要|合适|设计|考虑|合作|精度|传感器|最新|研究|药物".indexOf(str)!=-1;
} }
export const makeSearchParamsWithDataType = function(count: number, keyword: string| string[], dataType: string){
const searchMode = [];
searchMode.push({"key":"data_type", "type":"MUST", "value":dataType})
if (typeof keyword === 'string') {
// 如果 keyword 是字符串,则直接添加一个 searchMode 条目
searchMode.push({
key: "description",
type: "MUST",
value: keyword
});
} else if (Array.isArray(keyword)) {
// 如果 keyword 是数组,则为每个元素添加一个 searchMode 条目
keyword.forEach(str => {
if (!inGrepList(str))
searchMode.push({
key: "description",
type: "SHOULD",
value: str
});
});
}
return {
action: "executeContract",
contractID: "BDBrowser",
operation: "sendRequestDirectly",
arg: {
id: iodConfig.registry,
//doipUrl:"tcp://127.0.0.1:21039",
doipUrl: iodConfig.gatewayUrl,
op: "Search",
vars:{
timeout:15000
},
attributes: {
offset: 0,
count,
bodyBase64Encoded: false,
searchMode:searchMode
},
body: ""
}
}
}
export const makeRegSearchParams = function(count: number, keyword: string| string[]){ export const makeRegSearchParams = function(count: number, keyword: string| string[]){
const searchMode = []; const searchMode = [];
if (typeof keyword === 'string') { if (typeof keyword === 'string') {
@ -185,92 +236,61 @@ export async function localIodSearch(
const abortController = new AbortController(); const abortController = new AbortController();
setTimeout(() => abortController.abort(), 10000); setTimeout(() => abortController.abort(), 10000);
const params = makeRegSearchParams(TOTAL_SEARCH_RESULTS, keywords); const params = makeRegSearchParams(TOTAL_SEARCH_RESULTS, keywords);
const dataParams = makeSearchParamsWithDataType(TOTAL_SEARCH_RESULTS,keywords, "data");
const scenarioParams = makeSearchParamsWithDataType(TOTAL_SEARCH_RESULTS,keywords, "scenario");
const orgParams = makeSearchParamsWithDataType(TOTAL_SEARCH_RESULTS,keywords, "organization");
try { try {
const response = await fetch(iodConfig.doBrowser, { console.log('params------->',params)
method: "POST", const requests = [
body: JSON.stringify(params), fetch(iodConfig.doBrowser,{method: "POST", body: JSON.stringify(dataParams),signal: abortController.signal}),
signal: abortController.signal fetch(iodConfig.doBrowser,{method: "POST", body: JSON.stringify(scenarioParams),signal: abortController.signal}),
}); fetch(iodConfig.doBrowser,{method: "POST", body: JSON.stringify(orgParams),signal: abortController.signal})
];
//TODO @Zhaoweijie 这三类分别是数据、场景、团队的搜索请求。
const responses = await Promise.all(requests);
const results = await Promise.all(responses.map(res => res.json()));
const allResults: IodRegistryEntry[] = [];
for (const res of results) {
// 检查顶层状态
if (res.status !== "Success") {
continue; // 跳过失败的请求
}
let body;
try {
body = JSON.parse(res.result.body);
} catch (e) {
console.warn("Failed to parse result.body as JSON", e);
continue;
}
const res = await response.json(); if (body.code !== 0) {
if (res.status !== "Success") { continue;
console.log(res); }
return []; const entries: IodRegistryEntry[] = body.data?.results || [];
// 数据清洗:补全 url 和 doId
for (const r of entries) {
r.url = r.url || r.pdf_url;
r.doId = r.doId || r.doid;
}
// 合并到总结果
allResults.push(...entries);
} }
const seenDoIds = new Set<string>();
const body = JSON.parse(res.result.body); const prunedResults: IodRegistryEntry[] = [];
if (body.code !== 0) { for (const r of allResults) {
console.log(body); if (r.doId && !seenDoIds.has(r.doId)) {
return []; seenDoIds.add(r.doId);
prunedResults.push(r);
}
} }
return prunedResults;
let results: IodRegistryEntry[] = body.data?.results || [];
for (const r of results) {
r.url = r.url || r.pdf_url;
}
for (const r of results) {
r.doId = r.doId || r.doid;
}
// results 根据 doId 去重
const map = new Map<string, IodRegistryEntry>();
for (const r of results) {
map.set(r.doId, r);
}
return Array.from(map.values());
} catch (e) { } catch (e) {
console.log(e); console.log(e);
return []; return [];
} }
/*
const results = (
await Promise.all(
keywords.map(async (keyword) => {
const abortController = new AbortController()
setTimeout(() => abortController.abort(), 10000)
//http://47.93.156.31:21033/SCIDE/SCManager
const params = makeRegSearchParams(TOTAL_SEARCH_RESULTS, keyword)
return fetch(iodConfig.doBrowser, {
method: "POST",
body: JSON.stringify(params),
signal: abortController.signal
})
.then((response) => response.json())
.then((res) => {
if (res.status !== "Success") {
console.log(res)
return []
}
const body = JSON.parse(res.result.body)
if (body.code !== 0) {
console.log(body)
return []
}
const results: IodRegistryEntry[] = body.data?.results || []
for (const r of results) {
r.url = r.url || r.pdf_url
}
for (const r of results) {
r.doId = r.doId || r.doid
}
return results
})
.catch((e) => {
console.log(e)
return []
})
})
)
).flat()
// results 根据 doId 去重
const map = new Map<string, IodRegistryEntry>()
for (const r of results) {
map.set(r.doId, r)
}
return Array.from(map.values())
*/
} }
const ARXIV_URL_PATTERN = /^https?:\/\/arxiv\.org\// const ARXIV_URL_PATTERN = /^https?:\/\/arxiv\.org\//
@ -281,6 +301,7 @@ export const searchIod = async (query: string, keywords: string[]) => {
const isSimpleMode = await getIsSimpleInternetSearch() const isSimpleMode = await getIsSimpleInternetSearch()
console.log("searchMode:"+isSimpleMode+"\n kw:"+JSON.stringify(keywords)+"\n"+" ->searchResult:\n"+JSON.stringify(searchResults)) console.log("searchMode:"+isSimpleMode+"\n kw:"+JSON.stringify(keywords)+"\n"+" ->searchResult:\n"+JSON.stringify(searchResults))
console.log("pruned Search Result:"+JSON.stringify(searchResults.map(r=>r.doId+" "+r.name)))
if (isSimpleMode) { if (isSimpleMode) {
await getOllamaURL() await getOllamaURL()
return searchResults return searchResults

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,9 @@ module.exports = {
content: ["./src/**/*.tsx"], content: ["./src/**/*.tsx"],
theme: { theme: {
extend: { extend: {
width: {
'1/10': '10%',
},
backgroundImage: { backgroundImage: {
'bottom-mask-light': 'linear-gradient(0deg, transparent 0, #ffffff 160px)', 'bottom-mask-light': 'linear-gradient(0deg, transparent 0, #ffffff 160px)',
'bottom-mask-dark': 'linear-gradient(0deg, transparent 0, #171717 160px)', 'bottom-mask-dark': 'linear-gradient(0deg, transparent 0, #171717 160px)',