Compare commits
17 Commits
90228512f7
...
feat/layou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
224ae55d6b | ||
|
|
8a5c5f1c26 | ||
|
|
9e379d13cb | ||
|
|
1104fb2733 | ||
|
|
ef0e315bdc | ||
|
|
3fb66b4c36 | ||
|
|
3cbf4454da | ||
|
|
6a597da44f | ||
|
|
dba196d777 | ||
|
|
c5fa739a95 | ||
|
|
70d1f40333 | ||
|
|
2866bcc7af | ||
|
|
2a57034c9d | ||
|
|
79a03ab6fc | ||
|
|
50f9e4354f | ||
|
|
8f27ca2e4e | ||
|
|
ce333714b7 |
@@ -49,7 +49,9 @@ cd page-assist
|
||||
2. Install the dependencies
|
||||
|
||||
```bash
|
||||
export PATH="/Users/huaqiancai/.bun/bin/:$PATH"
|
||||
bun install
|
||||
|
||||
```
|
||||
|
||||
3. Build the extension (by default it will build for Chrome)
|
||||
|
||||
BIN
operation/1.png
Normal file
|
After Width: | Height: | Size: 659 KiB |
BIN
operation/2.png
Normal file
|
After Width: | Height: | Size: 763 KiB |
BIN
operation/3.png
Normal file
|
After Width: | Height: | Size: 941 KiB |
BIN
operation/4.png
Normal file
|
After Width: | Height: | Size: 610 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pageassist",
|
||||
"displayName": "Page Assist - A Web UI for Local AI Models",
|
||||
"displayName": "IoD Bot - A Web UI for Local AI Models",
|
||||
"version": "1.0.9",
|
||||
"description": "Use your locally running AI models to assist you in your web browsing.",
|
||||
"author": "n4ze3m",
|
||||
@@ -58,6 +58,8 @@
|
||||
"rehype-mathjax": "4.0.3",
|
||||
"remark-gfm": "3.0.1",
|
||||
"remark-math": "5.1.1",
|
||||
"segmentit": "^2.0.3",
|
||||
"styled-components": "^6.1.19",
|
||||
"tesseract.js": "^5.1.1",
|
||||
"turndown": "^7.1.3",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
@@ -75,6 +77,7 @@
|
||||
"@types/react-dom": "18.2.18",
|
||||
"@types/react-speech-recognition": "^3.9.5",
|
||||
"@types/react-syntax-highlighter": "^15.5.11",
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"@types/turndown": "^5.0.4",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"cross-env": "^7.0.3",
|
||||
|
||||
1
src/assets/icons/a.svg
Normal 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
@@ -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 |
1
src/assets/icons/bulb.svg
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 |
1
src/assets/icons/rocket.svg
Normal 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 |
@@ -1,30 +1,30 @@
|
||||
{
|
||||
"newChat": "New Chat",
|
||||
"selectAPrompt": "Select a Prompt",
|
||||
"githubRepository": "GitHub Repository",
|
||||
"settings": "Settings",
|
||||
"metering": "Metering",
|
||||
"sidebarTitle": "Chat History",
|
||||
"error": "Error",
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
"validationSelectModel": "Please select a model to continue",
|
||||
"deleteHistoryConfirmation": "Are you sure you want to delete this history?",
|
||||
"editHistoryTitle": "Enter a new title",
|
||||
"temporaryChat": "Temporary Chat",
|
||||
"more": {
|
||||
"copy": {
|
||||
"group": "Copy",
|
||||
"asText": "Copy as Text",
|
||||
"asMarkdown": "Copy as Markdown",
|
||||
"success": "Copied to clipboard!"
|
||||
},
|
||||
"download": {
|
||||
"group": "Download",
|
||||
"text": "Text File (.txt)",
|
||||
"markdown": "Markdown (.md)",
|
||||
"json": "JSON File (.json)",
|
||||
"image": "Image (.png)"
|
||||
},
|
||||
"share": "Share"
|
||||
}
|
||||
"newChat": "New Chat",
|
||||
"selectAPrompt": "Select a Prompt",
|
||||
"githubRepository": "GitHub Repository",
|
||||
"settings": "Settings",
|
||||
"metering": "Metering",
|
||||
"sidebarTitle": "Chat History",
|
||||
"error": "Error",
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
"validationSelectModel": "Please select a model to continue",
|
||||
"deleteHistoryConfirmation": "Are you sure you want to delete this history?",
|
||||
"editHistoryTitle": "Enter a new title",
|
||||
"temporaryChat": "Temporary Chat",
|
||||
"more": {
|
||||
"copy": {
|
||||
"group": "Copy",
|
||||
"asText": "Copy as Text",
|
||||
"asMarkdown": "Copy as Markdown",
|
||||
"success": "Copied to clipboard!"
|
||||
},
|
||||
"download": {
|
||||
"group": "Download",
|
||||
"text": "Text File (.txt)",
|
||||
"markdown": "Markdown (.md)",
|
||||
"json": "JSON File (.json)",
|
||||
"image": "Image (.png)"
|
||||
},
|
||||
"share": "Share"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"pageAssist": "Page Assist",
|
||||
"pageAssist": "IoD Bot",
|
||||
"selectAModel": "选择一个模型",
|
||||
"save": "保存",
|
||||
"saved": "已保存",
|
||||
@@ -38,7 +38,7 @@
|
||||
}
|
||||
},
|
||||
"copyToClipboard": "复制到剪贴板",
|
||||
"webSearch": "搜索万维网",
|
||||
"webSearch": "搜索中...",
|
||||
"iodSearch": "搜索数联网",
|
||||
"regenerate": "重新生成",
|
||||
"edit": "编辑",
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"newChat": "新聊天",
|
||||
"projectTitle": "数联网科创智能体",
|
||||
"newChat": "新对话",
|
||||
"selectAPrompt": "选择一个提示词",
|
||||
"githubRepository": "GitHub 仓库",
|
||||
"settings": "设置",
|
||||
"metering": "计量",
|
||||
"sidebarTitle": "聊天历史",
|
||||
"sidebarTitle": "对话历史",
|
||||
"error": "错误",
|
||||
"somethingWentWrong": "出现了错误",
|
||||
"validationSelectModel": "请选择一个模型以继续",
|
||||
"deleteHistoryConfirmation": "你确定要删除这个历史记录吗?",
|
||||
"editHistoryTitle": "输入一个新的标题",
|
||||
"temporaryChat": "临时聊天",
|
||||
"temporaryChat": "临时对话",
|
||||
"more": {
|
||||
"copy": {
|
||||
"group": "复制",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"searchInternet": "搜索万维网",
|
||||
"searchInternet": "深度搜索",
|
||||
"searchIod": "搜索数联网",
|
||||
"speechToText": "语音到文本",
|
||||
"uploadImage": "上传图片",
|
||||
|
||||
35
src/components/Common/DataNavigation.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
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={3}
|
||||
className="flex items-center"
|
||||
style={{ marginBottom: 0, color: "#1F2937", fontSize: "18px" }}>
|
||||
{Header}
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
{/* 右侧部分 */}
|
||||
{showButton && (
|
||||
<div className="flex items-center text-[#3a3a3a] cursor-pointer space-x-0.5 hover:text-[#00c0ef] transition" onClick={onClick}>
|
||||
<span className="text-[12px]">更多</span>
|
||||
<ChevronRightIcon className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
@@ -12,7 +12,7 @@ import { preprocessLaTeX } from "@/utils/latex"
|
||||
|
||||
function Markdown({
|
||||
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
|
||||
className?: string
|
||||
|
||||
150
src/components/Common/Playground/Data.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
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: "热 榜 第2"
|
||||
},
|
||||
{
|
||||
title: "祁连山老虎沟大本营10米气象每日值数据集(V1.0)(2018-2023)",
|
||||
description:
|
||||
"中国科学院西北生态环境资源研究院,2021年8月3日发布,2021年8月3日20:48更新",
|
||||
time: "包括2019年8月,2021年8月和2024年6月",
|
||||
metadata: "热 榜 第2"
|
||||
},
|
||||
{
|
||||
title: "李嘉图为研究老虎沟大本营2014-2018年...",
|
||||
description:
|
||||
"中国科学院西北生态环境资源研究院,2021年8月3日发布,2021年8月3日20:48更新",
|
||||
time: "包括2019年8月,2021年8月和2024年6月",
|
||||
metadata: "热 榜 第2"
|
||||
},
|
||||
{
|
||||
title: "青海玉树B1区俄日矿勘探数据2017-2023",
|
||||
description:
|
||||
"数字中国集团,CSTR:3260.11.1528414774204895456,DT2023年地质勘探补充调查",
|
||||
time: "包括2019年8月,2021年8月和2024年6月",
|
||||
metadata: "热 榜 第2"
|
||||
}
|
||||
]
|
||||
|
||||
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 (
|
||||
<Card
|
||||
className="h-full [&_.ant-card-body]:h-full [&_.ant-card-body]:!p-[20px] overflow-y-hidden"
|
||||
hoverable>
|
||||
<div className="h-full flex flex-col relative">
|
||||
{/* 数据导航 */}
|
||||
<DataNavigation
|
||||
Header={
|
||||
<div className="flex items-center gap-0.5 text-[#3480e3]">
|
||||
<svg
|
||||
className="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="3572"
|
||||
width="18"
|
||||
height="18">
|
||||
<path
|
||||
d="M877.714286 54.857143H754.285714V9.142857c0-5.028571-4.114286-9.142857-9.142857-9.142857h-64c-5.028571 0-9.142857 4.114286-9.142857 9.142857v45.714286H498.285714V9.142857c0-5.028571-4.114286-9.142857-9.142857-9.142857h-64c-5.028571 0-9.142857 4.114286-9.142857 9.142857v45.714286H292.571429c-20.228571 0-36.571429 16.342857-36.571429 36.571428v137.142858h-109.714286c-20.228571 0-36.571429 16.342857-36.571428 36.571428v722.285714c0 20.228571 16.342857 36.571429 36.571428 36.571429h585.142857c20.228571 0 36.571429-16.342857 36.571429-36.571429v-109.714285h109.714286c20.228571 0 36.571429-16.342857 36.571428-36.571429V91.428571c0-20.228571-16.342857-36.571429-36.571428-36.571428zM685.714286 941.714286H192V310.857143h249.142857v198.857143c0 25.257143 20.457143 45.714286 45.714286 45.714285h198.857143v386.285715z m0-459.428572H514.285714V310.857143h0.228572L685.714286 482.057143v0.228571z m146.285714 313.142857h-64V448L548.571429 228.571429H338.285714v-91.428572h77.714286v36.571429c0 5.028571 4.114286 9.142857 9.142857 9.142857h64c5.028571 0 9.142857-4.114286 9.142857-9.142857v-36.571429h173.714286v36.571429c0 5.028571 4.114286 9.142857 9.142857 9.142857h64c5.028571 0 9.142857-4.114286 9.142857-9.142857v-36.571429h77.714286v658.285714z"
|
||||
p-id="3573"
|
||||
fill="#3480e3"></path>
|
||||
</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}
|
||||
className="[&_.ant-card-body]:!p-2 !bg-[gb(248, 248, 248)] border !border-[#e9e9e9]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<h3 className="text-base font-medium mb-1 text-[#222222] line-clamp-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-sm text-[#383838] line-clamp-2">
|
||||
{item.description}
|
||||
</p>
|
||||
<p className="text-[#828282] text-xs truncate">{item.time}</p>
|
||||
{item.metadata && (
|
||||
<div>
|
||||
<span className="inline-block text-[#D90000] bg-[#eb1c1c30] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
{item.metadata}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 抽屉 */}
|
||||
<Drawer
|
||||
title="相关数据"
|
||||
closable={{ "aria-label": "Close Button" }}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
width="33.33%">
|
||||
<div className="grid grid-cols-1 gap-3 overflow-y-auto">
|
||||
{data.slice(0, 3).map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
hoverable
|
||||
className="[&_.ant-card-body]:!p-2 !bg-[gb(248, 248, 248)] border !border-[#e9e9e9]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<h3 className="text-base font-medium mb-1 text-[#222222]">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-sm text-[#383838]">{item.description}</p>
|
||||
<p className="text-[#828282] text-xs">{item.time}</p>
|
||||
{item.metadata && (
|
||||
<div>
|
||||
<span className="inline-block text-[#D90000] bg-[#eb1c1c30] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
{item.metadata}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Drawer>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
263
src/components/Common/Playground/History.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import { Sidebar } from "@/components/Option/Sidebar.tsx"
|
||||
import React, { useContext, useMemo, useState } from "react"
|
||||
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
|
||||
import { useStoreChatModelSettings } from "@/store/model.tsx"
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
List,
|
||||
Menu,
|
||||
MenuProps,
|
||||
Popover,
|
||||
Select,
|
||||
Tooltip
|
||||
} from "antd"
|
||||
import { PageAssitDatabase } from "@/db"
|
||||
import { EraserIcon, PanelLeftIcon } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { HistoryContext } from "@/components/Layouts/Layout.tsx"
|
||||
import { PlusOutlined, RightOutlined } from "@ant-design/icons"
|
||||
import { qaPrompt } from "@/libs/playground.tsx"
|
||||
import { ProviderIcons } from "@/components/Common/ProviderIcon.tsx"
|
||||
import { fetchChatModels } from "@/services/ollama.ts"
|
||||
|
||||
const ModelIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
className="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="9426"
|
||||
width="16"
|
||||
height="16">
|
||||
<path
|
||||
d="M509.952 161.512727c148.945455-82.850909 300.730182-91.229091 371.479273-20.945454s62.324364 221.509818-20.526546 370.501818h-0.465454a429.335273 429.335273 0 0 1 65.163636 284.392727 168.727273 168.727273 0 0 1-44.683636 85.643637 173.754182 173.754182 0 0 1-86.109091 44.683636 435.665455 435.665455 0 0 1-285.277091-65.675636 430.731636 430.731636 0 0 1-282.530909 63.813818 172.218182 172.218182 0 0 1-86.109091-44.683637c-70.283636-69.818182-62.370909-220.206545 19.502545-368.174545-81.966545-148.48-89.786182-298.309818-19.502545-368.686546s220.625455-62.324364 369.058909 19.130182z m291.886545 440.785455a901.818182 901.818182 0 0 1-92.16 106.589091 934.027636 934.027636 0 0 1-108.916363 93.602909 586.891636 586.891636 0 0 0 58.600727 21.410909c74.938182 22.341818 127.069091 19.502545 155.508364-8.843636l-0.465455 0.884363c28.811636-28.392727 31.697455-80.523636 8.843637-155.508363a546.443636 546.443636 0 0 0-21.41091-58.135273z m-582.74909-0.465455a539.927273 539.927273 0 0 0-20.433455 55.854546c-22.295273 75.357091-19.549091 127.022545 8.797091 155.368727s80.151273 31.697455 155.508364 8.936727h-0.558546a539.927273 539.927273 0 0 0 55.854546-20.526545 967.400727 967.400727 0 0 1-199.214546-199.726546z m290.90909-332.753454a851.781818 851.781818 0 0 0-131.258181 108.404363 823.296 823.296 0 0 0-109.847273 133.12 823.854545 823.854545 0 0 0 109.847273 133.12v-0.884363a852.293818 852.293818 0 0 0 131.211636 108.357818 846.754909 846.754909 0 0 0 133.538909-109.800727 856.436364 856.436364 0 0 0 108.962909-131.258182 852.852364 852.852364 0 0 0-108.962909-131.211637 829.998545 829.998545 0 0 0-133.538909-109.847272zM503.994182 418.909091a94.347636 94.347636 0 1 1-35.84 10.705454 92.811636 92.811636 0 0 1 35.84-10.705454z m310.877091-212.340364c-28.253091-28.299636-80.151273-31.557818-155.508364-8.750545a591.592727 591.592727 0 0 0-58.600727 21.876363 933.794909 933.794909 0 0 1 108.869818 93.556364 947.060364 947.060364 0 0 1 92.718545 107.054546 545.326545 545.326545 0 0 0 21.41091-58.181819q33.559273-113.058909-8.843637-155.508363zM363.054545 199.68c-74.938182-22.295273-127.069091-19.549091-155.508363 8.843636v-0.465454c-28.997818 28.392727-31.744 80.523636-8.936727 155.508363a507.345455 507.345455 0 0 0 20.433454 56.273455A976.663273 976.663273 0 0 1 418.909091 220.206545a541.230545 541.230545 0 0 0-55.854546-20.526545z m0 0"
|
||||
fill="#696F85"
|
||||
p-id="9427"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const PlaygroundHistory = () => {
|
||||
const { setSystemPrompt } = useStoreChatModelSettings()
|
||||
|
||||
const { show, setShow } = useContext(HistoryContext)
|
||||
|
||||
const {
|
||||
setMessages,
|
||||
setHistory,
|
||||
setHistoryId,
|
||||
historyId,
|
||||
clearChat,
|
||||
selectedModel,
|
||||
setSelectedModel,
|
||||
temporaryChat,
|
||||
setSelectedSystemPrompt
|
||||
} = useMessageOption()
|
||||
|
||||
const { t } = useTranslation(["option", "common", "settings"])
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
type MenuItem = Required<MenuProps>["items"][number]
|
||||
const qaPromptItems = useMemo<MenuItem[]>(() => {
|
||||
return [
|
||||
{
|
||||
key: "qaPrompt",
|
||||
label: "热门搜索",
|
||||
type: "group" as const,
|
||||
children: qaPrompt.map((item) => {
|
||||
return {
|
||||
key: item.id,
|
||||
label: <span title={item.title}>{item.title}</span>,
|
||||
icon: <p className="w-3.5">{item.icon}</p>
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
}, [])
|
||||
|
||||
const { onSubmit } = useMessageOption()
|
||||
|
||||
const { mutateAsync: sendMessage } = useMutation({
|
||||
mutationFn: onSubmit,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["fetchChatHistory"]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const onClickQaPromptItem: MenuProps["onClick"] = (e) => {
|
||||
const record = qaPrompt.find((item) => item.id === e.key)
|
||||
void sendMessage({ message: record.title, image: "" })
|
||||
}
|
||||
|
||||
// 大模型
|
||||
const {
|
||||
data: models,
|
||||
isLoading: isModelsLoading,
|
||||
refetch
|
||||
} = useQuery({
|
||||
queryKey: ["fetchModel"],
|
||||
queryFn: () => fetchChatModels({ returnEmpty: true }),
|
||||
refetchIntervalInBackground: false,
|
||||
placeholderData: (prev) => prev
|
||||
})
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`flex flex-col [&_.ant-card-body]:h-full w-[300px] overflow-hidden h-full pb-5 transition-all duration-300 ease-in-out !bg-[#f3f4f6]`}
|
||||
style={{ width: show ? "300px" : "0" }}>
|
||||
{/*Header*/}
|
||||
<div className="flex flex-col overflow-y-hidden h-full">
|
||||
<div className="flex items-center justify-between transition-all duration-300 ease-in-out w-[250px]">
|
||||
<h2 className="text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3">
|
||||
<span className="text-[#d30100]">数联网</span>科创智能体
|
||||
</h2>
|
||||
|
||||
<button
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
onClick={() => {
|
||||
setShow(!show)
|
||||
}}>
|
||||
<PanelLeftIcon className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{/*新建对话*/}
|
||||
<Button
|
||||
color="purple"
|
||||
variant="filled"
|
||||
size="large"
|
||||
className="w-full mt-4 hover:!bg-[#0057ff1a]"
|
||||
style={{
|
||||
color: "#0057ff",
|
||||
background: "#0057ff0f",
|
||||
border: "1px solid #0066ff26"
|
||||
}}
|
||||
onClick={clearChat}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center">
|
||||
<PlusOutlined
|
||||
className="text-sm"
|
||||
style={{ fontSize: "16px", fontWeight: 500 }}
|
||||
/>
|
||||
<span className="font-medium ml-2.5">{t("newChat")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
{/*选择智能体*/}
|
||||
<Popover
|
||||
placement="right"
|
||||
content={
|
||||
<Select
|
||||
className="w-80"
|
||||
placeholder={t("common:selectAModel")}
|
||||
// loadingText={t("common:selectAModel")}
|
||||
value={selectedModel}
|
||||
onChange={(e) => {
|
||||
setSelectedModel(e)
|
||||
localStorage.setItem("selectedModel", e)
|
||||
}}
|
||||
filterOption={(input, option) => {
|
||||
//@ts-ignore
|
||||
return (
|
||||
option?.label?.props["data-title"]
|
||||
?.toLowerCase()
|
||||
?.indexOf(input.toLowerCase()) >= 0
|
||||
)
|
||||
}}
|
||||
showSearch
|
||||
loading={isModelsLoading}
|
||||
options={models?.map((model) => ({
|
||||
label: (
|
||||
<span
|
||||
key={model.model}
|
||||
data-title={model.name}
|
||||
className="flex flex-row gap-3 items-center ">
|
||||
<ProviderIcons
|
||||
provider={model?.provider}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
<span className="line-clamp-2">{model.name}</span>
|
||||
</span>
|
||||
),
|
||||
value: model.model
|
||||
}))}
|
||||
size="large"
|
||||
// onRefresh={() => {
|
||||
// refetch()
|
||||
// }}
|
||||
/>
|
||||
}>
|
||||
<Button
|
||||
size="large"
|
||||
color="default"
|
||||
variant="text"
|
||||
className="w-full !justify-between !text-[#000000d9] font-normal">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<ModelIcon />
|
||||
<span className="!text-[#000000d9] font-normal text-sm">
|
||||
选择智能体
|
||||
</span>
|
||||
</div>
|
||||
<RightOutlined style={{ color: "#0000004d" }} />
|
||||
</Button>
|
||||
</Popover>
|
||||
<Divider size="small" />
|
||||
{/*热门搜索*/}
|
||||
<Menu
|
||||
items={qaPromptItems}
|
||||
onClick={onClickQaPromptItem}
|
||||
className="!bg-[#f3f4f6] !border-r-0"
|
||||
/>
|
||||
</div>
|
||||
<Divider size="small" />
|
||||
<div className="pb-1.5 pl-4 text-sm text-[#00000073] flex items-center justify-between pr-2">
|
||||
<span>最近对话</span>
|
||||
<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 flex-1 pl-7">
|
||||
<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>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
185
src/components/Common/Playground/IodRelevant.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import React from "react"
|
||||
import { Button, Card } from "antd"
|
||||
|
||||
// 使用 CSS-in-JS 方式
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
|
||||
const rotate = keyframes`
|
||||
0% {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(360deg);
|
||||
}
|
||||
`
|
||||
|
||||
const breathe = keyframes`
|
||||
0% {
|
||||
box-shadow: 0 0 5px rgba(37, 231, 232, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(37, 231, 232, 0.8);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 5px rgba(37, 231, 232, 0.3);
|
||||
}
|
||||
`
|
||||
|
||||
const CircleElement = styled.div<{ delay: number }>`
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
height: 160px;
|
||||
background: #3b82f6; // blue-500
|
||||
opacity: 0.2;
|
||||
border-radius: 50%;
|
||||
top: 55%;
|
||||
left: 50%;
|
||||
animation: ${rotate} 6s linear infinite, ${breathe} 2s infinite alternate;
|
||||
animation-delay: ${props => props.delay}s;
|
||||
`
|
||||
|
||||
|
||||
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
|
||||
className="icon animate-spin"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="8408"
|
||||
width="18"
|
||||
height="18">
|
||||
<path
|
||||
d="M512 128C299.936 128 128 296.672 128 504.736c0 130.784 67.904 245.984 170.976 313.536l35.52-52.256C248.576 709.696 192 613.696 192 504.736c0-173.376 143.264-313.92 320-313.92s320 140.544 320 313.92c0 98.112-45.856 185.696-117.696 243.296l-73.792-72.416V864h192l-72.768-71.36C843.072 723.52 896 620.16 896 504.704 896 296.672 724.064 128 512 128z"
|
||||
fill="#52c41a"
|
||||
p-id="8409"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const SearchIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
className="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="2585"
|
||||
width="22px"
|
||||
height="22px">
|
||||
<path
|
||||
d="M913.365333 842.794667l-188.16-188.16a347.648 347.648 0 0 0 69.674667-209.194667c0-192.682667-156.757333-349.44-349.44-349.44s-349.44 156.757333-349.44 349.44 156.757333 349.44 349.44 349.44a347.648 347.648 0 0 0 209.152-69.674667l188.16 188.16a49.962667 49.962667 0 0 0 70.613333-70.570666zM195.84 445.44a249.6 249.6 0 1 1 249.6 249.6 249.898667 249.898667 0 0 1-249.6-249.6z"
|
||||
fill="#08307f"
|
||||
p-id="2586"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const PlaygroundIodRelevant: React.FC = () => {
|
||||
const data = [
|
||||
{
|
||||
title: <p>已在<span className="text-[#9d0000]">29个科学数据中心</span>的<span className="text-[#9d0000]">50万个科学数据集</span>中进行搜索</p>,
|
||||
description: <p>已发现<span className="text-green-700"> 4个 </span>数据集</p>,
|
||||
status: "success"
|
||||
},
|
||||
{
|
||||
title: <p>已在<span className="text-[#9d0000]">100万篇论文</span>、<span className="text-[#9d0000]">2800个科创场景</span>中进行搜索</p>,
|
||||
description: "已发现4个数据集",
|
||||
status: "success"
|
||||
},
|
||||
{
|
||||
title: <p>正在<span className="text-[#9d0000]">1000位智库专家</span>、<span className="text-[#9d0000]">12万个创新机构</span>中进行搜索</p>,
|
||||
status: "loading"
|
||||
}
|
||||
]
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
data.push({
|
||||
title: <p>正在<span className="text-[#9d0000]">1000位智库专家</span>、<span className="text-[#9d0000]">12万个创新机构</span>中进行搜索{i}</p>,
|
||||
description: "已发现4个数据集",
|
||||
status: "success"
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Card
|
||||
hoverable
|
||||
variant="outlined"
|
||||
className="flex flex-col h-full [&_.ant-card-body]:h-full [&_.ant-card-body]:!p-[20px] translate-y-[-2px] !bg-[#f0f9ff]"
|
||||
>
|
||||
<div className="h-full flex flex-col relative">
|
||||
{/* 花瓣效果 */}
|
||||
<div className="absolute inset-0 pointer-events-none z-0 overflow-hidden">
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-64 h-64">
|
||||
<CircleElement delay={0} />
|
||||
<CircleElement delay={1} />
|
||||
<CircleElement delay={2} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="p-3">
|
||||
<h2 className="text-xl font-semibold text-[#08307f] flex justify-between items-center">
|
||||
<div className='flex items-center gap-2'>
|
||||
<SearchIcon />
|
||||
数联网搜索相关内容
|
||||
</div>
|
||||
<button className="bg-[#2563eb1a] text-[#08307f] font-medium py-1 px-3 rounded-full text-sm hover:bg-[#2563eb1a] transition-colors float-right">
|
||||
{data.length}个结果
|
||||
</button>
|
||||
</h2>
|
||||
<p className="text-sm text-[#08307f] mt-1 align-middle">
|
||||
下面是在数联网上进行深度搜索的相关内容
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="space-y-2 flex-1 overflow-y-auto">
|
||||
{data.map((item, index) => (
|
||||
<Card
|
||||
className="[&>*:first-child]:!p-3 shadow-md"
|
||||
key={index}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<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>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
Pen,
|
||||
PlayIcon,
|
||||
RotateCcw,
|
||||
Square
|
||||
Square,
|
||||
Star,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
MessageSquareShare,
|
||||
ArrowUpSquare
|
||||
} from "lucide-react"
|
||||
import { EditMessageForm } from "./EditMessageForm"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@@ -53,14 +58,14 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
const { t } = useTranslation("common")
|
||||
const { cancel, isSpeaking, speak } = useTTS()
|
||||
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="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={`flex flex-row gap-1 md:gap-1 my-2 m-auto w-full ${props.isBot ? "" : "flex-row-reverse"}`}>
|
||||
<div className="w-8 flex flex-col relative items-center justify-center bottom-[8px]">
|
||||
<div className="relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center text-opacity-100r">
|
||||
{props.isBot ? (
|
||||
!props.botAvatar ? (
|
||||
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400"></div>
|
||||
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400 hidden"></div>
|
||||
) : (
|
||||
props.botAvatar
|
||||
)
|
||||
@@ -73,13 +78,6 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
</div>
|
||||
<div className="flex w-[calc(100%-50px)] flex-col gap-2 lg:w-[calc(100%-115px)]">
|
||||
<span className="text-xs font-bold text-gray-800 dark:text-white">
|
||||
{props.isBot
|
||||
? props.name === "chrome::gemini-nano::page-assist"
|
||||
? "Gemini Nano"
|
||||
: removeModelSuffix(
|
||||
props.name?.replaceAll(/accounts\/[^\/]+\/models\//g, "")
|
||||
)
|
||||
: "You"}
|
||||
</span>
|
||||
|
||||
{props.isBot &&
|
||||
@@ -94,7 +92,7 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col">
|
||||
<div className={`flex flex-grow flex-col`}>
|
||||
{!editMode ? (
|
||||
props.isBot ? (
|
||||
<>
|
||||
@@ -133,11 +131,12 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
</>
|
||||
) : (
|
||||
<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 &&
|
||||
"italic text-gray-500 dark:text-gray-400 text-sm"
|
||||
}`}>
|
||||
"italic dark:text-gray-400"
|
||||
} flex flex-row-reverse`}>
|
||||
{props.message}
|
||||
{/*<span className="bg-[#0000000a] inline-block py-[9px] text-[#000000d9] rounded-xl px-4 font-light text-sm">{props.message}</span>*/}
|
||||
</p>
|
||||
)
|
||||
) : (
|
||||
@@ -316,6 +315,51 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="收藏">
|
||||
<button
|
||||
aria-label="收藏"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<Star className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="发布语用">
|
||||
<button
|
||||
aria-label="发布语用"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<ArrowUpSquare className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="发布对话">
|
||||
<button
|
||||
aria-label="发布对话"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<MessageSquareShare className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="点赞">
|
||||
<button
|
||||
aria-label="点赞"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<ThumbsUp className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="点踩">
|
||||
<button
|
||||
aria-label="点踩"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<ThumbsDown className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// add invisible div to prevent layout shift
|
||||
|
||||
143
src/components/Common/Playground/Scene.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
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 (
|
||||
<Card
|
||||
className="h-full [&_.ant-card-body]:h-full [&_.ant-card-body]:!p-[20px] overflow-y-hidden"
|
||||
hoverable>
|
||||
<div className="h-full flex flex-col relative">
|
||||
{/* 数据导航 */}
|
||||
<DataNavigation
|
||||
Header={
|
||||
<div className="flex items-center text-[#52c41a] gap-1">
|
||||
<svg
|
||||
className="icon"
|
||||
viewBox="0 0 1025 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="6235"
|
||||
width="18"
|
||||
height="18">
|
||||
<path
|
||||
d="M980.34571 1.143792c-4.850903 0-9.824354 0.888481-14.797806 2.930966L229.773215 299.724504H20.428686c-11.233669 0-20.424853 9.446494-20.424853 21.180572V702.584302c0 11.74429 9.191184 21.180572 20.424853 21.180573h129.820365c-4.728353 14.808018-7.271248 30.51473-7.271248 46.46654 0 84.119757 68.678568 152.543014 153.176184 152.543014 70.721053 0 130.330986-47.998404 147.93721-112.847312l521.569043 209.59984c4.983664 1.919936 9.957116 2.930966 14.808019 2.930967 21.568645 0 40.839493-18.127057 40.839493-42.371358V43.525362C1021.195415 19.270849 1002.047116 1.143792 980.34571 1.143792zM296.153987 831.250663c-33.833769 0-61.274559-27.308028-61.274558-61.009035 0-14.297397 4.983664-27.951411 14.042086-38.807221l108.374269 43.525362c-2.553107 31.403211-28.972654 56.290895-61.141797 56.290894z m633.12959 74.550713L263.984844 638.501326l-16.462431-6.638077H91.915671V391.626129h155.606742l16.462431-6.638077 665.298733-267.30005v788.113374z m0 0"
|
||||
fill="#52c41a"
|
||||
p-id="6236"></path>
|
||||
</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}
|
||||
className="[&_.ant-card-body]:!p-2 !bg-[gb(248, 248, 248)] border !border-[#e9e9e9]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<h3 className="text-base font-medium mb-1 text-[#222222] line-clamp-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
<span className="text-sm text-[#383838] line-clamp-2">
|
||||
{item.description}
|
||||
</span>
|
||||
<p className="text-[#828282] text-xs truncate">
|
||||
{item.demander}
|
||||
</p>
|
||||
<p className="flex items-center gap-1.5">
|
||||
<span className="inline-block text-[#003AD4] bg-[#003ad430] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
技术应用
|
||||
</span>
|
||||
<span className="inline-block text-[#00BF68] bg-[#00bf6830] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
制造业
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 抽屉 */}
|
||||
<Drawer
|
||||
title="相关场景"
|
||||
closable={{ "aria-label": "Close Button" }}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
width="33.33%">
|
||||
<div className="grid grid-cols-1 gap-3 overflow-y-auto">
|
||||
{data.slice(0, 3).map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
hoverable
|
||||
className="[&_.ant-card-body]:!p-2 !bg-[gb(248, 248, 248)] border !border-[#e9e9e9]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<h3 className="text-base font-medium mb-1 text-[#222222]">
|
||||
{item.title}
|
||||
</h3>
|
||||
<span className="text-sm text-[#383838]">
|
||||
{item.description}
|
||||
</span>
|
||||
<p className="text-[#828282] text-xs">{item.demander}</p>
|
||||
<p className="flex items-center gap-1.5">
|
||||
<span className="inline-block text-[#003AD4] bg-[#003ad430] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
技术应用
|
||||
</span>
|
||||
<span className="inline-block text-[#00BF68] bg-[#00bf6830] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
制造业
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Drawer>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
141
src/components/Common/Playground/Team.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
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 (
|
||||
<Card
|
||||
className="h-full [&_.ant-card-body]:h-full [&_.ant-card-body]:!p-[20px] overflow-y-hidden"
|
||||
hoverable>
|
||||
<div className="h-full flex flex-col relative">
|
||||
{/* 数据导航 */}
|
||||
<DataNavigation
|
||||
Header={
|
||||
<div className="flex items-center text-[#BE0BAC] gap-1">
|
||||
<svg
|
||||
className="icon"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="7272"
|
||||
width="18"
|
||||
height="18">
|
||||
<path
|
||||
d="M824.2 699.9c-25.4-25.4-54.7-45.7-86.4-60.4C783.1 602.8 812 546.8 812 484c0-110.8-92.4-201.7-203.2-200-109.1 1.7-197 90.6-197 200 0 62.8 29 118.8 74.2 155.5-31.7 14.7-60.9 34.9-86.4 60.4C345 754.6 314 826.8 312 903.8c-0.1 4.5 3.5 8.2 8 8.2h56c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5C493.8 707.7 551.1 684 612 684c60.9 0 118.2 23.7 161.3 66.8C814.5 792 838 846.3 840 904.3c0.1 4.3 3.7 7.7 8 7.7h56c4.5 0 8.1-3.7 8-8.2-2-77-33-149.2-87.8-203.9zM612 612c-34.2 0-66.4-13.3-90.5-37.5-24.5-24.5-37.9-57.1-37.5-91.8 0.3-32.8 13.4-64.5 36.3-88 24-24.6 56.1-38.3 90.4-38.7 33.9-0.3 66.8 12.9 91 36.6 24.8 24.3 38.4 56.8 38.4 91.4 0 34.2-13.3 66.3-37.5 90.5-24.2 24.2-56.4 37.5-90.6 37.5z"
|
||||
p-id="7273"
|
||||
fill="#BE0BAC"></path>
|
||||
<path
|
||||
d="M361.5 510.4c-0.9-8.7-1.4-17.5-1.4-26.4 0-15.9 1.5-31.4 4.3-46.5 0.7-3.6-1.2-7.3-4.5-8.8-13.6-6.1-26.1-14.5-36.9-25.1-25.8-25.2-39.7-59.3-38.7-95.4 0.9-32.1 13.8-62.6 36.3-85.6 24.7-25.3 57.9-39.1 93.2-38.7 31.9 0.3 62.7 12.6 86 34.4 7.9 7.4 14.7 15.6 20.4 24.4 2 3.1 5.9 4.4 9.3 3.2 17.6-6.1 36.2-10.4 55.3-12.4 5.6-0.6 8.8-6.6 6.3-11.6-32.5-64.3-98.9-108.7-175.7-109.9-110.9-1.7-203.3 89.2-203.3 199.9 0 62.8 28.9 118.8 74.2 155.5-31.8 14.7-61.1 35-86.5 60.4-54.8 54.7-85.8 126.9-87.8 204-0.1 4.5 3.5 8.2 8 8.2h56.1c4.3 0 7.9-3.4 8-7.7 1.9-58 25.4-112.3 66.7-153.5 29.4-29.4 65.4-49.8 104.7-59.7 3.9-1 6.5-4.7 6-8.7z"
|
||||
p-id="7274"
|
||||
fill="#BE0BAC"></path>
|
||||
</svg>
|
||||
相关团队
|
||||
</div>
|
||||
}
|
||||
onClick={showDrawer}
|
||||
/>
|
||||
|
||||
{/* 场景列表 */}
|
||||
<div className="grid grid-cols-3 gap-3 flex-1 overflow-y-auto">
|
||||
{data.slice(0, 5).map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="[&_.ant-card-body]:!p-2 !bg-[gb(248, 248, 248)] border !border-[#e9e9e9]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<h3 className="text-base font-medium mb-1 text-[#222222] line-clamp-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-[#828282] text-xs truncate">
|
||||
{item.description}
|
||||
</p>
|
||||
<p className="flex items-center gap-1.5">
|
||||
<span className="inline-block text-[#BE0BAC] bg-[#be0bac30] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
晶体材料
|
||||
</span>
|
||||
<span className="inline-block text-[#EB1C1C] bg-[#eb1c1c30] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
中国
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 抽屉 */}
|
||||
<Drawer
|
||||
title="相关团队"
|
||||
closable={{ "aria-label": "Close Button" }}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
width="33.33%">
|
||||
<div className="grid grid-cols-1 gap-3 overflow-y-auto">
|
||||
{data.slice(0, 5).map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
hoverable
|
||||
className="[&_.ant-card-body]:!p-2 !bg-[gb(248, 248, 248)] border !border-[#e9e9e9]">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<h3 className="text-base font-medium mb-1 text-[#222222]">
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-[#828282] text-xs">{item.description}</p>
|
||||
<p className="flex items-center gap-1.5">
|
||||
<span className="inline-block text-[#BE0BAC] bg-[#be0bac30] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
晶体材料
|
||||
</span>
|
||||
<span className="inline-block text-[#EB1C1C] bg-[#eb1c1c30] h-5 leading-5 mt-1 text-xs rounded-full px-2.5">
|
||||
中国
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Drawer>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
46
src/components/Common/Playground/TokenStatistics.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,255 +1,66 @@
|
||||
import { useStorage } from "@plasmohq/storage/hook"
|
||||
import {
|
||||
BrainCog,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CogIcon,
|
||||
ComputerIcon,
|
||||
Slice,
|
||||
GithubIcon,
|
||||
PanelLeftIcon,
|
||||
ZapIcon
|
||||
} from "lucide-react"
|
||||
import React, { useContext } from "react"
|
||||
import { HistoryContext } from "@/components/Layouts/Layout.tsx"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
import { Button } from "antd"
|
||||
import { PlusOutlined } from "@ant-design/icons"
|
||||
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useLocation, NavLink } from "react-router-dom"
|
||||
import { SelectedKnowledge } from "../Option/Knowledge/SelectedKnowledge"
|
||||
import { ModelSelect } from "../Common/ModelSelect"
|
||||
import { PromptSelect } from "../Common/PromptSelect"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { fetchChatModels } from "~/services/ollama"
|
||||
import { useMessageOption } from "~/hooks/useMessageOption"
|
||||
import { Select, Tooltip } from "antd"
|
||||
import { getAllPrompts } from "@/db"
|
||||
import { ProviderIcons } from "../Common/ProviderIcon"
|
||||
import { NewChat } from "./NewChat"
|
||||
import { PageAssistSelect } from "../Select"
|
||||
import { MoreOptions } from "./MoreOptions"
|
||||
type Props = {
|
||||
setSidebarOpen: (open: boolean) => void
|
||||
setOpenModelSettings: (open: boolean) => void
|
||||
}
|
||||
|
||||
export const Header: React.FC<Props> = ({
|
||||
setOpenModelSettings,
|
||||
setSidebarOpen
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation(["option", "common"])
|
||||
const isRTL = i18n?.dir() === "rtl"
|
||||
export const Header = () => {
|
||||
const { show, setShow } = useContext(HistoryContext)
|
||||
|
||||
const [shareModeEnabled] = useStorage("shareMode", false)
|
||||
const [hideCurrentChatModelSettings] = useStorage(
|
||||
"hideCurrentChatModelSettings",
|
||||
false
|
||||
)
|
||||
const {
|
||||
selectedModel,
|
||||
setSelectedModel,
|
||||
clearChat,
|
||||
selectedSystemPrompt,
|
||||
setSelectedQuickPrompt,
|
||||
setSelectedSystemPrompt,
|
||||
messages,
|
||||
streaming,
|
||||
historyId,
|
||||
temporaryChat
|
||||
} = useMessageOption()
|
||||
const {
|
||||
data: models,
|
||||
isLoading: isModelsLoading,
|
||||
refetch
|
||||
} = useQuery({
|
||||
queryKey: ["fetchModel"],
|
||||
queryFn: () => fetchChatModels({ returnEmpty: true }),
|
||||
refetchIntervalInBackground: false,
|
||||
placeholderData: (prev) => prev
|
||||
})
|
||||
|
||||
const { data: prompts, isLoading: isPromptLoading } = useQuery({
|
||||
queryKey: ["fetchAllPromptsLayout"],
|
||||
queryFn: getAllPrompts
|
||||
})
|
||||
|
||||
const { pathname } = useLocation()
|
||||
|
||||
const getPromptInfoById = (id: string) => {
|
||||
return prompts?.find((prompt) => prompt.id === id)
|
||||
}
|
||||
|
||||
const handlePromptChange = (value?: string) => {
|
||||
if (!value) {
|
||||
setSelectedSystemPrompt(undefined)
|
||||
setSelectedQuickPrompt(undefined)
|
||||
return
|
||||
}
|
||||
const prompt = getPromptInfoById(value)
|
||||
if (prompt?.is_system) {
|
||||
setSelectedSystemPrompt(prompt.id)
|
||||
} else {
|
||||
setSelectedSystemPrompt(undefined)
|
||||
setSelectedQuickPrompt(prompt!.content)
|
||||
}
|
||||
}
|
||||
const { t } = useTranslation(["option", "common", "settings"])
|
||||
|
||||
const { clearChat } = useMessageOption()
|
||||
return (
|
||||
<div
|
||||
className={`absolute top-0 z-10 flex h-14 w-full flex-row items-center justify-center p-3 overflow-x-auto lg:overflow-x-visible bg-gray-50 border-b dark:bg-[#171717] dark:border-gray-600 ${
|
||||
temporaryChat && "!bg-gray-200 dark:!bg-black"
|
||||
}`}>
|
||||
<div className="flex gap-2 items-center">
|
||||
{pathname !== "/" && (
|
||||
<div>
|
||||
<NavLink
|
||||
to="/"
|
||||
className="text-gray-500 items-center dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
{isRTL ? (
|
||||
<ChevronRight className={`w-8 h-8`} />
|
||||
) : (
|
||||
<ChevronLeft className={`w-8 h-8`} />
|
||||
)}
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
className={`w-full h-[60px] absolute inset-0 pl-5 z-10 flex items-center transition-all duration-300 ease-in-out ${show ? "left-[300px]" : ""}`}>
|
||||
{/*控制侧边栏显示隐藏与新建对话*/}
|
||||
{!show && (
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
onClick={() => setSidebarOpen(true)}>
|
||||
onClick={() => {
|
||||
setShow(!show)
|
||||
}}>
|
||||
<PanelLeftIcon className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
<NewChat clearChat={clearChat} />
|
||||
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
|
||||
{"/"}
|
||||
</span>
|
||||
<div className="hidden lg:block">
|
||||
<Select
|
||||
className="w-80"
|
||||
placeholder={t("common:selectAModel")}
|
||||
// loadingText={t("common:selectAModel")}
|
||||
value={selectedModel}
|
||||
onChange={(e) => {
|
||||
setSelectedModel(e)
|
||||
localStorage.setItem("selectedModel", e)
|
||||
<Button
|
||||
color="cyan"
|
||||
variant="filled"
|
||||
shape="round"
|
||||
style={{
|
||||
color: "#0057ff",
|
||||
background: "#0057ff0f",
|
||||
border: "1px solid #0066ff26"
|
||||
}}
|
||||
filterOption={(input, option) => {
|
||||
//@ts-ignore
|
||||
return (
|
||||
option?.label?.props["data-title"]
|
||||
?.toLowerCase()
|
||||
?.indexOf(input.toLowerCase()) >= 0
|
||||
)
|
||||
}}
|
||||
showSearch
|
||||
loading={isModelsLoading}
|
||||
options={models?.map((model) => ({
|
||||
label: (
|
||||
<span
|
||||
key={model.model}
|
||||
data-title={model.name}
|
||||
className="flex flex-row gap-3 items-center ">
|
||||
<ProviderIcons
|
||||
provider={model?.provider}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
<span className="line-clamp-2">{model.name}</span>
|
||||
</span>
|
||||
),
|
||||
value: model.model
|
||||
}))}
|
||||
size="large"
|
||||
// onRefresh={() => {
|
||||
// refetch()
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:hidden">
|
||||
<ModelSelect />
|
||||
</div>
|
||||
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
|
||||
{"/"}
|
||||
</span>
|
||||
<div className="hidden lg:block">
|
||||
<Select
|
||||
size="large"
|
||||
loading={isPromptLoading}
|
||||
showSearch
|
||||
placeholder={t("selectAPrompt")}
|
||||
className="w-60"
|
||||
allowClear
|
||||
onChange={handlePromptChange}
|
||||
value={selectedSystemPrompt}
|
||||
filterOption={(input, option) =>
|
||||
//@ts-ignore
|
||||
option.label.key.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
options={prompts?.map((prompt) => ({
|
||||
label: (
|
||||
<span
|
||||
key={prompt.title}
|
||||
className="flex flex-row gap-3 items-center">
|
||||
{prompt.is_system ? (
|
||||
<ComputerIcon className="w-4 h-4" />
|
||||
) : (
|
||||
<ZapIcon className="w-4 h-4" />
|
||||
)}
|
||||
{prompt.title}
|
||||
</span>
|
||||
),
|
||||
value: prompt.id
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:hidden">
|
||||
<PromptSelect
|
||||
selectedSystemPrompt={selectedSystemPrompt}
|
||||
setSelectedSystemPrompt={setSelectedSystemPrompt}
|
||||
setSelectedQuickPrompt={setSelectedQuickPrompt}
|
||||
/>
|
||||
</div>
|
||||
<SelectedKnowledge />
|
||||
</div>
|
||||
<div className="flex flex-1 justify-end px-4">
|
||||
<div className="ml-4 flex items-center md:ml-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
{messages.length > 0 && !streaming && (
|
||||
<MoreOptions
|
||||
shareModeEnabled={shareModeEnabled}
|
||||
historyId={historyId}
|
||||
messages={messages}
|
||||
/>
|
||||
)}
|
||||
{!hideCurrentChatModelSettings && (
|
||||
<Tooltip title={t("common:currentChatModelSettings")}>
|
||||
<button
|
||||
onClick={() => setOpenModelSettings(true)}
|
||||
className="!text-gray-500 dark:text-gray-300 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<BrainCog className="w-6 h-6" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t("githubRepository")}>
|
||||
<a
|
||||
href="https://github.com/n4ze3m/page-assist"
|
||||
target="_blank"
|
||||
className="!text-gray-500 hidden lg:block dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("settings")}>
|
||||
<NavLink
|
||||
to="/settings"
|
||||
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<CogIcon className="w-6 h-6" />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("metering")}>
|
||||
<NavLink
|
||||
to="/metering"
|
||||
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<Slice className="w-6 h-6" />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
onClick={clearChat}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center">
|
||||
<PlusOutlined
|
||||
className="text-sm"
|
||||
style={{ fontSize: "16px", fontWeight: 500 }}
|
||||
/>
|
||||
<span>{t("newChat")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* 项目标题 */}
|
||||
<div
|
||||
className={`
|
||||
absolute left-1/2 transform -translate-x-1/2
|
||||
w-[600px] h-[55px] bg-white dark:bg-black
|
||||
flex items-center justify-center
|
||||
shadow-[0px_0px_5px_rgba(0,0,0,0.2)]
|
||||
rounded-b-[15px]
|
||||
transition-[top]
|
||||
${show ? "-top-[56px]" : "-top-[1px] delay-200"}
|
||||
`}>
|
||||
<h2 className="text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3">
|
||||
<span className="text-[#d30100]">数联网</span>科创智能体
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,103 +1,46 @@
|
||||
import React, { useState } from "react"
|
||||
|
||||
import { Sidebar } from "../Option/Sidebar"
|
||||
import { Drawer, Tooltip } from "antd"
|
||||
|
||||
import { useTranslation } from "react-i18next"
|
||||
import React, { useCallback, useEffect, useState } from "react"
|
||||
|
||||
import { CurrentChatModelSettings } from "../Common/Settings/CurrentChatModelSettings"
|
||||
import { Header } from "./Header"
|
||||
import { EraserIcon } from "lucide-react"
|
||||
import { PageAssitDatabase } from "@/db"
|
||||
import { useMessageOption } from "@/hooks/useMessageOption"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { useStoreChatModelSettings } from "@/store/model"
|
||||
import { Header } from "./Header.tsx"
|
||||
|
||||
interface History {
|
||||
show: boolean
|
||||
setShow: (show: boolean) => void
|
||||
}
|
||||
|
||||
export const HistoryContext = React.createContext<History>({
|
||||
show: true,
|
||||
setShow: () => {}
|
||||
})
|
||||
|
||||
export default function OptionLayout({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const { t } = useTranslation(["option", "common", "settings"])
|
||||
const [showHistory, setShowHistory] = useState(true)
|
||||
const [openModelSettings, setOpenModelSettings] = useState(false)
|
||||
const {
|
||||
setMessages,
|
||||
setHistory,
|
||||
setHistoryId,
|
||||
historyId,
|
||||
clearChat,
|
||||
setSelectedModel,
|
||||
temporaryChat,
|
||||
setSelectedSystemPrompt
|
||||
} = useMessageOption()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { setSystemPrompt } = useStoreChatModelSettings()
|
||||
const historyContextValue = {
|
||||
show: showHistory,
|
||||
setShow: setShowHistory
|
||||
}
|
||||
|
||||
const useToggle = useCallback(() => {
|
||||
setShowHistory(!showHistory)
|
||||
}, [showHistory])
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
<main className="relative h-dvh w-full">
|
||||
<div className="relative z-10 w-full">
|
||||
<Header
|
||||
setSidebarOpen={setSidebarOpen}
|
||||
setOpenModelSettings={setOpenModelSettings}
|
||||
/>
|
||||
</div>
|
||||
{/*<div className="relative z-10 w-full">*/}
|
||||
{/*</div>*/}
|
||||
{/* <div className="relative flex h-full flex-col items-center"> */}
|
||||
{children}
|
||||
<HistoryContext.Provider value={historyContextValue}>
|
||||
<Header />
|
||||
{children}
|
||||
</HistoryContext.Provider>
|
||||
{/* </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
|
||||
open={openModelSettings}
|
||||
|
||||
264
src/components/Layouts/_Header.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { useStorage } from "@plasmohq/storage/hook"
|
||||
import {
|
||||
BrainCog,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CogIcon,
|
||||
ComputerIcon,
|
||||
GaugeCircle,
|
||||
GithubIcon,
|
||||
PanelLeftIcon,
|
||||
ZapIcon
|
||||
} from "lucide-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useLocation, NavLink } from "react-router-dom"
|
||||
import { SelectedKnowledge } from "../Option/Knowledge/SelectedKnowledge"
|
||||
import { ModelSelect } from "../Common/ModelSelect"
|
||||
import { PromptSelect } from "../Common/PromptSelect"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { fetchChatModels } from "~/services/ollama"
|
||||
import { useMessageOption } from "~/hooks/useMessageOption"
|
||||
import { Select, Tooltip } from "antd"
|
||||
import { getAllPrompts } from "@/db"
|
||||
import { ProviderIcons } from "../Common/ProviderIcon"
|
||||
import { NewChat } from "./NewChat"
|
||||
import { MoreOptions } from "./MoreOptions"
|
||||
type Props = {
|
||||
sidebarOpen: boolean
|
||||
setSidebarOpen: () => void
|
||||
setOpenModelSettings: (open: boolean) => void
|
||||
}
|
||||
|
||||
export const Header: React.FC<Props> = ({
|
||||
setOpenModelSettings,
|
||||
setSidebarOpen,
|
||||
sidebarOpen
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation(["option", "common"])
|
||||
const isRTL = i18n?.dir() === "rtl"
|
||||
|
||||
|
||||
|
||||
const [shareModeEnabled] = useStorage("shareMode", false)
|
||||
const [hideCurrentChatModelSettings] = useStorage(
|
||||
"hideCurrentChatModelSettings",
|
||||
false
|
||||
)
|
||||
const {
|
||||
selectedModel,
|
||||
setSelectedModel,
|
||||
clearChat,
|
||||
selectedSystemPrompt,
|
||||
setSelectedQuickPrompt,
|
||||
setSelectedSystemPrompt,
|
||||
messages,
|
||||
streaming,
|
||||
historyId,
|
||||
temporaryChat
|
||||
} = useMessageOption()
|
||||
const {
|
||||
data: models,
|
||||
isLoading: isModelsLoading,
|
||||
} = useQuery({
|
||||
queryKey: ["fetchModel"],
|
||||
queryFn: () => fetchChatModels({ returnEmpty: true }),
|
||||
refetchIntervalInBackground: false,
|
||||
placeholderData: (prev) => prev
|
||||
})
|
||||
|
||||
const { data: prompts, isLoading: isPromptLoading } = useQuery({
|
||||
queryKey: ["fetchAllPromptsLayout"],
|
||||
queryFn: getAllPrompts
|
||||
})
|
||||
|
||||
const { pathname } = useLocation()
|
||||
|
||||
const getPromptInfoById = (id: string) => {
|
||||
return prompts?.find((prompt) => prompt.id === id)
|
||||
}
|
||||
|
||||
const handlePromptChange = (value?: string) => {
|
||||
if (!value) {
|
||||
setSelectedSystemPrompt(undefined)
|
||||
setSelectedQuickPrompt(undefined)
|
||||
return
|
||||
}
|
||||
const prompt = getPromptInfoById(value)
|
||||
if (prompt?.is_system) {
|
||||
setSelectedSystemPrompt(prompt.id)
|
||||
} else {
|
||||
setSelectedSystemPrompt(undefined)
|
||||
setSelectedQuickPrompt(prompt!.content)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute top-0 z-10 flex h-14 w-full flex-row items-center justify-center p-3 overflow-x-auto lg:overflow-x-visible bg-gray-50 border-b dark:bg-[#171717] dark:border-gray-600 ${
|
||||
temporaryChat && "!bg-gray-200 dark:!bg-black"
|
||||
}`}>
|
||||
<div className="flex gap-2 items-center">
|
||||
{pathname !== "/" && (
|
||||
<div>
|
||||
<NavLink
|
||||
to="/"
|
||||
className="text-gray-500 items-center dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
{isRTL ? (
|
||||
<ChevronRight className={`w-8 h-8`} />
|
||||
) : (
|
||||
<ChevronLeft className={`w-8 h-8`} />
|
||||
)}
|
||||
</NavLink>
|
||||
</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" }}>
|
||||
<span className="text-[#d30100]">数联网</span>科创智能体
|
||||
</h2>
|
||||
|
||||
<button
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
onClick={() => setSidebarOpen()}>
|
||||
<PanelLeftIcon className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
<NewChat clearChat={clearChat} />
|
||||
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
|
||||
{"/"}
|
||||
</span>
|
||||
<div className="hidden lg:block">
|
||||
<Select
|
||||
className="w-80"
|
||||
placeholder={t("common:selectAModel")}
|
||||
// loadingText={t("common:selectAModel")}
|
||||
value={selectedModel}
|
||||
onChange={(e) => {
|
||||
setSelectedModel(e)
|
||||
localStorage.setItem("selectedModel", e)
|
||||
}}
|
||||
filterOption={(input, option) => {
|
||||
//@ts-ignore
|
||||
return (
|
||||
option?.label?.props["data-title"]
|
||||
?.toLowerCase()
|
||||
?.indexOf(input.toLowerCase()) >= 0
|
||||
)
|
||||
}}
|
||||
showSearch
|
||||
loading={isModelsLoading}
|
||||
options={models?.map((model) => ({
|
||||
label: (
|
||||
<span
|
||||
key={model.model}
|
||||
data-title={model.name}
|
||||
className="flex flex-row gap-3 items-center ">
|
||||
<ProviderIcons
|
||||
provider={model?.provider}
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
<span className="line-clamp-2">{model.name}</span>
|
||||
</span>
|
||||
),
|
||||
value: model.model
|
||||
}))}
|
||||
size="large"
|
||||
// onRefresh={() => {
|
||||
// refetch()
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:hidden">
|
||||
<ModelSelect />
|
||||
</div>
|
||||
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
|
||||
{"/"}
|
||||
</span>
|
||||
<div className="hidden lg:block">
|
||||
<Select
|
||||
size="large"
|
||||
loading={isPromptLoading}
|
||||
showSearch
|
||||
placeholder={t("selectAPrompt")}
|
||||
className="w-60"
|
||||
allowClear
|
||||
onChange={handlePromptChange}
|
||||
value={selectedSystemPrompt}
|
||||
filterOption={(input, option) =>
|
||||
//@ts-ignore
|
||||
option.label.key.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
options={prompts?.map((prompt) => ({
|
||||
label: (
|
||||
<span
|
||||
key={prompt.title}
|
||||
className="flex flex-row gap-3 items-center">
|
||||
{prompt.is_system ? (
|
||||
<ComputerIcon className="w-4 h-4" />
|
||||
) : (
|
||||
<ZapIcon className="w-4 h-4" />
|
||||
)}
|
||||
{prompt.title}
|
||||
</span>
|
||||
),
|
||||
value: prompt.id
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:hidden">
|
||||
<PromptSelect
|
||||
selectedSystemPrompt={selectedSystemPrompt}
|
||||
setSelectedSystemPrompt={setSelectedSystemPrompt}
|
||||
setSelectedQuickPrompt={setSelectedQuickPrompt}
|
||||
/>
|
||||
</div>
|
||||
<SelectedKnowledge />
|
||||
</div>
|
||||
<div className="flex flex-1 justify-end px-4">
|
||||
<div className="ml-4 flex items-center md:ml-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
{messages.length > 0 && !streaming && (
|
||||
<MoreOptions
|
||||
shareModeEnabled={shareModeEnabled}
|
||||
historyId={historyId}
|
||||
messages={messages}
|
||||
/>
|
||||
)}
|
||||
{!hideCurrentChatModelSettings && (
|
||||
<Tooltip title={t("common:currentChatModelSettings")}>
|
||||
<button
|
||||
onClick={() => setOpenModelSettings(true)}
|
||||
className="!text-gray-500 dark:text-gray-300 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<BrainCog className="w-6 h-6" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t("githubRepository")}>
|
||||
<a
|
||||
href="https://github.com/n4ze3m/page-assist"
|
||||
target="_blank"
|
||||
className="!text-gray-500 hidden lg:block dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<GithubIcon className="w-6 h-6" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("settings")}>
|
||||
<NavLink
|
||||
to="/settings"
|
||||
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<CogIcon className="w-6 h-6" />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("metering")}>
|
||||
<NavLink
|
||||
to="/metering"
|
||||
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<GaugeCircle className="w-6 h-6" />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { ChatMessage, useStoreMessageOption } from "@/store/option"
|
||||
import { MeteringEntry, useStoreMessageOption } from "@/store/option"
|
||||
import { Card, List, Table, Tag, Space, TableProps, Tooltip } from "antd"
|
||||
import { NavLink } from "react-router-dom"
|
||||
import { formatDate } from "@/utils/date"
|
||||
|
||||
const columns: TableProps<ChatMessage>["columns"] = [
|
||||
const columns: TableProps<MeteringEntry>["columns"] = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
@@ -32,8 +32,8 @@ const columns: TableProps<ChatMessage>["columns"] = [
|
||||
},
|
||||
{
|
||||
title: "思维链",
|
||||
key: "thinkingChain",
|
||||
dataIndex: "thinkingChain",
|
||||
key: "cot",
|
||||
dataIndex: "cot",
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
@@ -66,8 +66,8 @@ const columns: TableProps<ChatMessage>["columns"] = [
|
||||
},
|
||||
{
|
||||
title: "数联网token",
|
||||
dataIndex: "iodDataTokenCount",
|
||||
key: "iodDataTokenCount"
|
||||
dataIndex: "iodTokenCount",
|
||||
key: "iodTokenCount"
|
||||
},
|
||||
{
|
||||
title: "大模型token",
|
||||
@@ -84,7 +84,7 @@ const columns: TableProps<ChatMessage>["columns"] = [
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
render: (date) => {
|
||||
return <div>{formatDate(date ?? new Date())}</div>
|
||||
return <div>{formatDate(new Date(date))}</div>
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -108,17 +108,17 @@ const columns: TableProps<ChatMessage>["columns"] = [
|
||||
]
|
||||
|
||||
export const MeteringDetail = () => {
|
||||
const { chatMessages } = useStoreMessageOption()
|
||||
const { meteringEntries } = useStoreMessageOption()
|
||||
|
||||
const data = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "对话数量",
|
||||
value: chatMessages.length
|
||||
value: meteringEntries.length
|
||||
},
|
||||
{
|
||||
key: "数联网输入token数",
|
||||
value: chatMessages.reduce((acc, cur) => {
|
||||
value: meteringEntries.reduce((acc, cur) => {
|
||||
for (const item of cur.iodKeywords) {
|
||||
acc += item.length
|
||||
}
|
||||
@@ -127,24 +127,24 @@ export const MeteringDetail = () => {
|
||||
},
|
||||
{
|
||||
key: "数联网输出token数",
|
||||
value: chatMessages.reduce((acc, cur) => acc + cur.iodDataTokenCount, 0)
|
||||
value: meteringEntries.reduce((acc, cur) => acc + cur.iodTokenCount, 0)
|
||||
},
|
||||
{
|
||||
key: "大模型输入token数",
|
||||
value: chatMessages.reduce(
|
||||
value: meteringEntries.reduce(
|
||||
(acc, cur) => acc + cur.modelInputTokenCount,
|
||||
0
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "大模型输出token数",
|
||||
value: chatMessages.reduce(
|
||||
value: meteringEntries.reduce(
|
||||
(acc, cur) => acc + cur.modelOutputTokenCount,
|
||||
0
|
||||
)
|
||||
}
|
||||
],
|
||||
[chatMessages]
|
||||
[meteringEntries]
|
||||
)
|
||||
return (
|
||||
<div className="p-4 pt-[4rem]">
|
||||
@@ -159,7 +159,7 @@ export const MeteringDetail = () => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<Table<ChatMessage> columns={columns} dataSource={chatMessages} />
|
||||
<Table<MeteringEntry> columns={columns} dataSource={meteringEntries} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ import {
|
||||
Card,
|
||||
List,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
TableProps,
|
||||
Divider,
|
||||
Typography,
|
||||
Tooltip
|
||||
} from "antd"
|
||||
import { NavLink, useParams } from "react-router-dom"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { useStoreMessageOption } from "@/store/option.tsx"
|
||||
import { useMemo } from "react"
|
||||
|
||||
@@ -24,26 +23,28 @@ interface DataType {
|
||||
|
||||
const columns: TableProps<DataType>["columns"] = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
title: "序号",
|
||||
key: "index",
|
||||
width: 100,
|
||||
render: (_text, _record, index) => index + 1, // 索引从0开始,+1后从1显示
|
||||
render: (_text, _record, index) => index + 1 // 索引从0开始,+1后从1显示
|
||||
},
|
||||
{
|
||||
title: "标识",
|
||||
dataIndex: "doId",
|
||||
key: "doId"
|
||||
key: "doId",
|
||||
width: 350
|
||||
},
|
||||
{
|
||||
title: "提供方",
|
||||
dataIndex: "data_space",
|
||||
key: "data_space"
|
||||
key: "data_space",
|
||||
width: 250
|
||||
},
|
||||
{
|
||||
title: "token数量",
|
||||
title: "token数",
|
||||
key: "tokenCount",
|
||||
dataIndex: "tokenCount",
|
||||
width: 100
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: "内容",
|
||||
@@ -61,15 +62,19 @@ const columns: TableProps<DataType>["columns"] = [
|
||||
]
|
||||
|
||||
export const ListDetail = () => {
|
||||
const { chatMessages } = useStoreMessageOption()
|
||||
const { meteringEntries } = useStoreMessageOption()
|
||||
const { id } = useParams()
|
||||
const record = useMemo(
|
||||
() => chatMessages.find((item) => item.id === id),
|
||||
[chatMessages]
|
||||
() => meteringEntries.find((item) => item.id === id),
|
||||
[meteringEntries]
|
||||
)
|
||||
|
||||
const modelData = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "数联网引用token总数",
|
||||
value: record.iodTokenCount
|
||||
},
|
||||
{
|
||||
key: "大模型输入token数",
|
||||
value: record.modelInputTokenCount
|
||||
@@ -129,7 +134,7 @@ export const ListDetail = () => {
|
||||
return (
|
||||
<div className="p-[1rem] pt-[4rem]">
|
||||
<List
|
||||
grid={{ gutter: 16, column: 3 }}
|
||||
grid={{ gutter: 16, column: 4 }}
|
||||
dataSource={modelData}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
@@ -139,7 +144,12 @@ export const ListDetail = () => {
|
||||
style={{ marginBottom: "2rem" }}
|
||||
/>
|
||||
|
||||
<Space direction="vertical" size={10}>
|
||||
<Space direction="vertical" className="w-full" size={10}>
|
||||
<Divider orientation="left">数联网引用数据</Divider>
|
||||
<Table<DataType> columns={columns} dataSource={record.iodData} />
|
||||
</Space>
|
||||
|
||||
<Space direction="vertical" className="w-full" size={10}>
|
||||
<Divider orientation="left">输入token详情</Divider>
|
||||
<List
|
||||
bordered
|
||||
@@ -147,22 +157,24 @@ export const ListDetail = () => {
|
||||
dataSource={inputTokenData}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ justifyContent: "flex-start" }}>
|
||||
<Typography.Paragraph style={{ marginBottom: 0 }} className="mr-1">
|
||||
<Typography.Paragraph
|
||||
style={{ marginBottom: 0 }}
|
||||
className="mr-1">
|
||||
{item.key}
|
||||
</Typography.Paragraph>
|
||||
<Tooltip placement="topLeft" style={{ marginBottom: 0 }} title={item.value}>
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
style={{ marginBottom: 0 }}
|
||||
title={item.value}>
|
||||
{item.value}
|
||||
</Tooltip>
|
||||
</List.Item>
|
||||
)}
|
||||
style={{ marginBottom: "1rem" }}
|
||||
/>
|
||||
<Card title="数联网引用数据">
|
||||
<Table<DataType> columns={columns} dataSource={record.iodData} />
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
<Space direction="vertical" size={10}>
|
||||
<Space direction="vertical" className="w-full" size={10}>
|
||||
<Divider orientation="left">输出token详情</Divider>
|
||||
<List
|
||||
bordered
|
||||
@@ -170,8 +182,13 @@ export const ListDetail = () => {
|
||||
header={<div>数联网搜索关键词</div>}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ justifyContent: "flex-start" }}>
|
||||
<Typography.Text className="mr-1" style={{ marginBottom: 0 }}>{item.key}</Typography.Text>
|
||||
<Tooltip style={{ marginBottom: 0 }} placement="topLeft" title={item.value}>
|
||||
<Typography.Text className="mr-1" style={{ marginBottom: 0 }}>
|
||||
{item.key}
|
||||
</Typography.Text>
|
||||
<Tooltip
|
||||
style={{ marginBottom: 0 }}
|
||||
placement="topLeft"
|
||||
title={item.value}>
|
||||
{item.value}
|
||||
</Tooltip>
|
||||
</List.Item>
|
||||
@@ -1,8 +1,14 @@
|
||||
import React from "react"
|
||||
import React, { useContext } from "react"
|
||||
|
||||
import { Card } from "antd"
|
||||
|
||||
import { PlaygroundForm } from "./PlaygroundForm"
|
||||
import { PlaygroundChat } from "./PlaygroundChat"
|
||||
import { useMessageOption } from "@/hooks/useMessageOption"
|
||||
import { webUIResumeLastChat } from "@/services/app"
|
||||
import { PlaygroundData } from '@/components/Common/Playground/Data.tsx'
|
||||
import { PlaygroundScene } from "@/components/Common/Playground/Scene.tsx"
|
||||
|
||||
import {
|
||||
formatToChatHistory,
|
||||
formatToMessage,
|
||||
@@ -13,6 +19,10 @@ import { getLastUsedChatSystemPrompt } from "@/services/model-settings"
|
||||
import { useStoreChatModelSettings } from "@/store/model"
|
||||
import { useSmartScroll } from "@/hooks/useSmartScroll"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import { PlaygroundTeam } from "@/components/Common/Playground/Team.tsx"
|
||||
import { PlaygroundHistory } from "@/components/Common/Playground/History.tsx"
|
||||
import { PlaygroundIodRelevant } from "@/components/Common/Playground/IodRelevant.tsx"
|
||||
|
||||
|
||||
export const Playground = () => {
|
||||
const drop = React.useRef<HTMLDivElement>(null)
|
||||
@@ -132,26 +142,46 @@ export const Playground = () => {
|
||||
return (
|
||||
<div
|
||||
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" : ""
|
||||
} bg-white dark:bg-[#171717]`}>
|
||||
<div
|
||||
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 />
|
||||
<PlaygroundHistory />
|
||||
<div className="relative h-full flex-1 prose-lg flex flex-col items-center [&>*]:max-w-[848px] pt-[60px]">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="custom-scrollbar flex h-auto w-full flex-col items-center overflow-x-hidden overflow-y-auto px-5">
|
||||
<PlaygroundChat />
|
||||
</div>
|
||||
<div className="relative 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 className="absolute bottom-0 w-full">
|
||||
{!isAtBottom && (
|
||||
<div className="fixed 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>
|
||||
{/*auto_530px_165px*/}
|
||||
{messages.length && (
|
||||
<div
|
||||
className="w-4/12 h-full grid grid-rows-10 gap-3 pt-16 pr-5 pb-0"
|
||||
style={{ paddingTop: "4rem" }}>
|
||||
<div className="w-full row-span-4">
|
||||
<PlaygroundIodRelevant />
|
||||
</div>
|
||||
)}
|
||||
<PlaygroundForm dropedFile={dropedFile} />
|
||||
</div>
|
||||
<div className="w-full row-span-4 grid grid-cols-2 gap-3 custom-scrollbar">
|
||||
<PlaygroundData />
|
||||
<PlaygroundScene />
|
||||
</div>
|
||||
<div className="w-full row-span-2 pb-3">
|
||||
<PlaygroundTeam />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ export const PlaygroundChat = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex w-full flex-col items-center pt-16 pb-4">
|
||||
<div className="relative flex w-full flex-col items-center pb-4">
|
||||
{messages.length === 0 && (
|
||||
<div className="mt-32 w-full">
|
||||
<div className="mt-3 w-full">
|
||||
<PlaygroundEmpty />
|
||||
</div>
|
||||
)}
|
||||
@@ -52,8 +52,7 @@ export const PlaygroundChat = () => {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full pb-[157px]"></div>
|
||||
|
||||
{/*<div className="w-full pb-[0px]"></div>*/}
|
||||
<MessageSourcePopup
|
||||
open={isSourceOpen}
|
||||
setOpen={setIsSourceOpen}
|
||||
|
||||
@@ -1,130 +1,58 @@
|
||||
import { cleanUrl } from "@/libs/clean-url"
|
||||
import { useStorage } from "@plasmohq/storage/hook"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { RotateCcw } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
import {
|
||||
getOllamaURL,
|
||||
isOllamaRunning,
|
||||
setOllamaURL as saveOllamaURL
|
||||
} from "~/services/ollama"
|
||||
import { Card, Col, Row } from "antd"
|
||||
|
||||
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { qaPrompt } from "@/libs/playground.tsx"
|
||||
|
||||
export const PlaygroundEmpty = () => {
|
||||
const [ollamaURL, setOllamaURL] = useState<string>("")
|
||||
const { t } = useTranslation(["playground", "common"])
|
||||
const { onSubmit } = useMessageOption()
|
||||
|
||||
const [checkOllamaStatus] = useStorage("checkOllamaStatus", true)
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const {
|
||||
data: ollamaInfo,
|
||||
status: ollamaStatus,
|
||||
refetch,
|
||||
isRefetching
|
||||
} = useQuery({
|
||||
queryKey: ["ollamaStatus"],
|
||||
queryFn: async () => {
|
||||
const ollamaURL = await getOllamaURL()
|
||||
const isOk = await isOllamaRunning()
|
||||
|
||||
if (ollamaURL) {
|
||||
saveOllamaURL(ollamaURL)
|
||||
}
|
||||
|
||||
return {
|
||||
isOk,
|
||||
ollamaURL
|
||||
}
|
||||
},
|
||||
enabled: checkOllamaStatus
|
||||
const { mutateAsync: sendMessage } = useMutation({
|
||||
mutationFn: onSubmit,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["fetchChatHistory"]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (ollamaInfo?.ollamaURL) {
|
||||
setOllamaURL(ollamaInfo.ollamaURL)
|
||||
}
|
||||
}, [ollamaInfo])
|
||||
|
||||
|
||||
if (!checkOllamaStatus) {
|
||||
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>
|
||||
)
|
||||
function handleQuestion(message: string) {
|
||||
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">
|
||||
{(ollamaStatus === "pending" || isRefetching) && (
|
||||
<div className="inline-flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div>
|
||||
<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 className="w-full p-4">
|
||||
{/* 标题区域 */}
|
||||
<div className="mb-4">
|
||||
<h2
|
||||
className="text-xl font-bold text-gray-800"
|
||||
style={{ lineHeight: "0" }}>
|
||||
数联网科创智能体
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">您好!请问有什么可以帮您?</p>
|
||||
</div>
|
||||
|
||||
{/* 卡片网格布局 */}
|
||||
<Row gutter={[16, 16]} className="w-full">
|
||||
{qaPrompt.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 w-10">{item.icon}</div>
|
||||
<div className="font-medium text-sm text-gray-800">
|
||||
{item.title}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -207,11 +207,11 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
||||
}
|
||||
|
||||
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 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
|
||||
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 " : ""}
|
||||
`}>
|
||||
<div
|
||||
@@ -357,6 +357,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
||||
if (isListening) {
|
||||
stopSpeechRecognition()
|
||||
} else {
|
||||
console.log("开始语音识别,语言:", speechToTextLanguage);
|
||||
resetTranscript()
|
||||
startListening({
|
||||
continuous: true,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
getLastUsedChatSystemPrompt,
|
||||
lastUsedChatModelEnabled
|
||||
} from "@/services/model-settings"
|
||||
import { useState } from "react"
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
@@ -169,7 +170,12 @@ export const Sidebar = ({
|
||||
{group.items.map((chat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex py-2 px-2 items-center gap-3 relative rounded-md truncate hover:pr-4 group transition-opacity duration-300 ease-in-out bg-gray-100 dark:bg-[#232222] dark:text-gray-100 text-gray-800 border hover:bg-gray-200 dark:hover:bg-[#2d2d2d] dark:border-gray-800">
|
||||
className={`
|
||||
flex py-2 px-2 items-center gap-3 relative rounded-md truncate hover:pr-4 group transition-opacity duration-300 ease-in-out border
|
||||
hover:text-[#000000d9] hover:bg-[#f3f2ff] dark:hover:bg-[#2d2d2d] dark:border-gray-800
|
||||
hover:[&_.more-vertical]:text-[#000000d9]
|
||||
${historyId === chat.id ? 'text-[#000000d9] bg-[#f3f2ff] border-[#000000d9]' : 'dark:text-gray-100 text-gray-800'}
|
||||
`}>
|
||||
{chat?.message_source === "copilot" && (
|
||||
<Tooltip title={t("common:sidebarChat")} placement="top">
|
||||
<BotIcon className="size-3 text-green-500" />
|
||||
@@ -265,7 +271,7 @@ export const Sidebar = ({
|
||||
trigger={["click"]}
|
||||
placement="bottomRight">
|
||||
<button className="text-gray-500 dark:text-gray-400 opacity-80 hover:opacity-100">
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
<MoreVertical className={`group-hover:text-[#000000d9] w-4 h-4 more-vertical ${historyId === chat.id ? 'text-[#000000d9]' : ''}`} />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
507
src/entries/auto-deeplink.content.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
main(ctx) {
|
||||
setTimeout(getDeepScript,1000)
|
||||
},
|
||||
});
|
||||
async function getDeepScript(){
|
||||
console.log("getDeepScript!!")
|
||||
const href = document.location.href;
|
||||
let id = "unknown";
|
||||
if (href.startsWith("http://39.105.188.3:3838/topic3/missing/?autoexecute="))
|
||||
id = "id1";
|
||||
if (href.startsWith("http://39.105.188.3:3838/topic3/PKUCausalEfficacy/?autoexecute="))
|
||||
id = "id2";
|
||||
if (href.startsWith("http://39.105.188.3:3838/topic3/ADR23/?autoexecute="))
|
||||
id = "id3";
|
||||
if (idToScript[id]!=undefined){
|
||||
idToScript[id]();
|
||||
}
|
||||
//sendMessageToServiceWorker({});
|
||||
}
|
||||
async function sendMessageToServiceWorker(message) {
|
||||
chrome.runtime.sendMessage({ type: 'retrieveDeepScript', doId:"10.1002/2014JA019817" }, response => {
|
||||
console.log(response);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const idToScript = {
|
||||
"id1":(function() {
|
||||
// 等待函数
|
||||
function wait(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function automate() {
|
||||
try {
|
||||
console.log("开始执行第二部分脚本...");
|
||||
|
||||
// 等待页面加载
|
||||
await wait(2000);
|
||||
|
||||
// 点击"缺失数据填补"链接
|
||||
const dataFillLink = Array.from(document.querySelectorAll('a')).find(a =>
|
||||
a.textContent.includes("缺失数据填补")
|
||||
);
|
||||
if (dataFillLink) {
|
||||
dataFillLink.click();
|
||||
console.log("已点击'缺失数据填补'链接");
|
||||
} else {
|
||||
console.error("未找到'缺失数据填补'链接");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"选择数字对象"选项卡
|
||||
const numObjTab = Array.from(document.querySelectorAll('[role="tab"]')).find(tab =>
|
||||
tab.textContent.includes("选择数字对象")
|
||||
);
|
||||
if (numObjTab) {
|
||||
numObjTab.click();
|
||||
console.log("已点击'选择数字对象'选项卡");
|
||||
} else {
|
||||
console.error("未找到'选择数字对象'选项卡");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"多重填补数字对象"
|
||||
const multipleNumObjs = Array.from(document.querySelectorAll('div')).filter(div =>
|
||||
div.textContent.trim() === "多重填补数字对象"
|
||||
);
|
||||
if (multipleNumObjs.length > 1) {
|
||||
multipleNumObjs[1].click();
|
||||
console.log("已点击'多重填补数字对象'");
|
||||
} else if (multipleNumObjs.length > 0) {
|
||||
multipleNumObjs[0].click();
|
||||
console.log("已点击'多重填补数字对象'");
|
||||
} else {
|
||||
console.error("未找到'多重填补数字对象'");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 选择"围术期处理后"
|
||||
const periOption = Array.from(document.querySelectorAll('[role="option"]')).find(option =>
|
||||
option.textContent.includes("围术期处理后")
|
||||
);
|
||||
if (periOption) {
|
||||
periOption.click();
|
||||
console.log("已选择'围术期处理后'");
|
||||
} else {
|
||||
console.error("未找到'围术期处理后'选项");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 勾选特定复选框
|
||||
const checkbox = document.querySelector('#col_pro');
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
console.log("已勾选复选框");
|
||||
} else {
|
||||
console.error("未找到指定复选框");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"开始填补"按钮
|
||||
const startFillBtn = Array.from(document.querySelectorAll('button')).find(btn =>
|
||||
btn.textContent.includes("开始填补")
|
||||
);
|
||||
if (startFillBtn) {
|
||||
startFillBtn.click();
|
||||
console.log("已点击'开始填补'按钮");
|
||||
} else {
|
||||
console.error("未找到'开始填补'按钮");
|
||||
}
|
||||
await wait(2000);
|
||||
|
||||
// 点击"进入数据分析"按钮
|
||||
const enterAnalysisBtn = Array.from(document.querySelectorAll('button')).find(btn =>
|
||||
btn.textContent.includes("进入数据分析")
|
||||
);
|
||||
if (enterAnalysisBtn) {
|
||||
enterAnalysisBtn.click();
|
||||
console.log("已点击'进入数据分析'按钮");
|
||||
} else {
|
||||
console.error("未找到'进入数据分析'按钮");
|
||||
}
|
||||
await wait(2000);
|
||||
|
||||
// 点击"中心序号"
|
||||
const centerNumDivs = Array.from(document.querySelectorAll('[id^="tab-"][id$="-3"] div')).filter(div =>
|
||||
div.textContent.includes("中心序号")
|
||||
);
|
||||
if (centerNumDivs.length > 1) {
|
||||
centerNumDivs[1].click();
|
||||
console.log("已点击'中心序号'");
|
||||
} else if (centerNumDivs.length > 0) {
|
||||
centerNumDivs[0].click();
|
||||
console.log("已点击'中心序号'");
|
||||
} else {
|
||||
console.error("未找到'中心序号'");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 选择"术后血红蛋白HB"
|
||||
const hbOption = Array.from(document.querySelectorAll('[role="option"]')).find(option =>
|
||||
option.textContent.includes("术后血红蛋白HB")
|
||||
);
|
||||
if (hbOption) {
|
||||
hbOption.click();
|
||||
console.log("已选择'术后血红蛋白HB'");
|
||||
} else {
|
||||
console.error("未找到'术后血红蛋白HB'选项");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击X变量选择框
|
||||
const xSelector = document.querySelector('[id="X"] > .form-group > div > .selectize-control > .selectize-input');
|
||||
if (xSelector) {
|
||||
xSelector.click();
|
||||
console.log("已点击X变量选择框");
|
||||
} else {
|
||||
console.error("未找到X变量选择框");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 选择"性别"
|
||||
const genderOption = Array.from(document.querySelectorAll('[role="option"]')).find(option =>
|
||||
option.textContent.includes("性别")
|
||||
);
|
||||
if (genderOption) {
|
||||
genderOption.click();
|
||||
console.log("已选择'性别'");
|
||||
} else {
|
||||
console.error("未找到'性别'选项");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击变量选择区域
|
||||
const varSelectors = Array.from(document.querySelectorAll('div')).filter(div =>
|
||||
div.textContent.includes("1. 根据预览数据选择变量 *选择处理变量(Z")
|
||||
);
|
||||
if (varSelectors.length > 3) {
|
||||
varSelectors[3].click();
|
||||
console.log("已点击变量选择区域");
|
||||
} else if (varSelectors.length > 0) {
|
||||
varSelectors[varSelectors.length - 1].click();
|
||||
console.log("已点击变量选择区域");
|
||||
} else {
|
||||
console.error("未找到变量选择区域");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"开始分析"按钮
|
||||
const startAnalysisBtn = Array.from(document.querySelectorAll('button')).find(btn =>
|
||||
btn.textContent.includes("开始分析")
|
||||
);
|
||||
if (startAnalysisBtn) {
|
||||
startAnalysisBtn.click();
|
||||
console.log("已点击'开始分析'按钮");
|
||||
} else {
|
||||
console.error("未找到'开始分析'按钮");
|
||||
}
|
||||
|
||||
console.log("第二部分脚本执行完成");
|
||||
} catch (error) {
|
||||
console.error("执行过程中出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行自动化操作
|
||||
automate();
|
||||
}),
|
||||
"id2":(function() {
|
||||
function wait(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function automate() {
|
||||
try {
|
||||
console.log("开始执行 Case02 第二部分脚本...");
|
||||
|
||||
// 等待页面加载
|
||||
await wait(2000);
|
||||
|
||||
// 点击"准备数据"链接
|
||||
const prepDataLink = Array.from(document.querySelectorAll('a')).find(a =>
|
||||
a.textContent.includes("准备数据")
|
||||
);
|
||||
if (prepDataLink) {
|
||||
prepDataLink.click();
|
||||
console.log("已点击'准备数据'链接");
|
||||
} else {
|
||||
console.error("未找到'准备数据'链接");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"选择数字对象"选项卡
|
||||
const numObjTab = Array.from(document.querySelectorAll('[role="tab"]')).find(tab =>
|
||||
tab.textContent.includes("选择数字对象")
|
||||
);
|
||||
if (numObjTab) {
|
||||
numObjTab.click();
|
||||
console.log("已点击'选择数字对象'选项卡");
|
||||
} else {
|
||||
console.error("未找到'选择数字对象'选项卡");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"RS"
|
||||
const rsDivs = Array.from(document.querySelectorAll('div')).filter(div =>
|
||||
div.textContent.trim() === "RS"
|
||||
);
|
||||
if (rsDivs.length > 1) {
|
||||
rsDivs[1].click();
|
||||
console.log("已点击'RS'");
|
||||
} else if (rsDivs.length > 0) {
|
||||
rsDivs[0].click();
|
||||
console.log("已点击'RS'");
|
||||
} else {
|
||||
console.error("未找到'RS'");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 选择"TQ-B2303-III-01_merged"
|
||||
const mergedOption = Array.from(document.querySelectorAll('[role="option"]')).find(option =>
|
||||
option.textContent.includes("TQ-B2303-III-01_merged")
|
||||
);
|
||||
if (mergedOption) {
|
||||
mergedOption.click();
|
||||
console.log("已选择'TQ-B2303-III-01_merged'");
|
||||
} else {
|
||||
console.error("未找到'TQ-B2303-III-01_merged'选项");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击特定元素
|
||||
const noncomplianceElement = document.querySelector('[id="data\\.goto\\.noncompliance1"]');
|
||||
if (noncomplianceElement) {
|
||||
noncomplianceElement.click();
|
||||
console.log("已点击特定元素");
|
||||
} else {
|
||||
console.error("未找到特定元素");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"AGEGR1"下拉框
|
||||
const agegr1Combobox = Array.from(document.querySelectorAll('[role="combobox"]')).find(box =>
|
||||
box.getAttribute('name') === "AGEGR1"
|
||||
);
|
||||
if (agegr1Combobox) {
|
||||
agegr1Combobox.click();
|
||||
console.log("已点击'AGEGR1'下拉框");
|
||||
await wait(500);
|
||||
|
||||
// 选择特定选项
|
||||
const agegr1Option = document.querySelector('#bs-select-9-29');
|
||||
if (agegr1Option) {
|
||||
agegr1Option.click();
|
||||
console.log("已选择AGEGR1选项");
|
||||
} else {
|
||||
console.error("未找到AGEGR1选项");
|
||||
}
|
||||
} else {
|
||||
console.error("未找到'AGEGR1'下拉框");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"SUBJID"下拉框
|
||||
const subjidCombobox = Array.from(document.querySelectorAll('[role="combobox"]')).find(box =>
|
||||
box.getAttribute('name') === "SUBJID"
|
||||
);
|
||||
if (subjidCombobox) {
|
||||
subjidCombobox.click();
|
||||
console.log("已点击'SUBJID'下拉框");
|
||||
await wait(500);
|
||||
|
||||
// 选择特定选项
|
||||
const subjidOption = document.querySelector('#bs-select-14-42');
|
||||
if (subjidOption) {
|
||||
subjidOption.click();
|
||||
console.log("已选择SUBJID选项");
|
||||
} else {
|
||||
console.error("未找到SUBJID选项");
|
||||
}
|
||||
} else {
|
||||
console.error("未找到'SUBJID'下拉框");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"Nothing selected"下拉框
|
||||
const nothingSelectedCombobox = Array.from(document.querySelectorAll('[role="combobox"]')).find(box =>
|
||||
box.getAttribute('name') === "Nothing selected"
|
||||
);
|
||||
if (nothingSelectedCombobox) {
|
||||
nothingSelectedCombobox.click();
|
||||
console.log("已点击'Nothing selected'下拉框");
|
||||
await wait(500);
|
||||
|
||||
// 选择特定选项
|
||||
const nothingSelectedOption = document.querySelector('#bs-select-16-6');
|
||||
if (nothingSelectedOption) {
|
||||
nothingSelectedOption.click();
|
||||
console.log("已选择选项");
|
||||
} else {
|
||||
console.error("未找到选项");
|
||||
}
|
||||
} else {
|
||||
console.error("未找到'Nothing selected'下拉框");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"IPI"下拉框
|
||||
const ipiCombobox = Array.from(document.querySelectorAll('[role="combobox"]')).find(box =>
|
||||
box.getAttribute('name') === "IPI"
|
||||
);
|
||||
if (ipiCombobox) {
|
||||
ipiCombobox.click();
|
||||
console.log("已点击'IPI'下拉框");
|
||||
await wait(500);
|
||||
|
||||
// 取消勾选第四个复选框
|
||||
const checkboxes = document.querySelectorAll('[role="checkbox"]');
|
||||
if (checkboxes.length > 3) {
|
||||
checkboxes[3].click(); // 取消勾选
|
||||
console.log("已取消勾选复选框");
|
||||
} else {
|
||||
console.error("未找到足够的复选框");
|
||||
}
|
||||
} else {
|
||||
console.error("未找到'IPI'下拉框");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"计算估计结果"按钮
|
||||
const calculateBtn = Array.from(document.querySelectorAll('button')).find(btn =>
|
||||
btn.textContent.includes("计算估计结果")
|
||||
);
|
||||
if (calculateBtn) {
|
||||
calculateBtn.click();
|
||||
console.log("已点击'计算估计结果'按钮");
|
||||
} else {
|
||||
console.error("未找到'计算估计结果'按钮");
|
||||
}
|
||||
|
||||
console.log("Case02 第二部分脚本执行完成");
|
||||
} catch (error) {
|
||||
console.error("执行过程中出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行自动化操作
|
||||
automate();
|
||||
}),
|
||||
"id3":(function(){
|
||||
function wait(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function automate() {
|
||||
try {
|
||||
console.log("开始执行 Case03 第二部分脚本...");
|
||||
|
||||
// 等待页面加载
|
||||
await wait(2000);
|
||||
|
||||
// 点击"模型预测"链接
|
||||
const modelPredictLink = Array.from(document.querySelectorAll('a')).find(a =>
|
||||
a.textContent.includes("模型预测")
|
||||
);
|
||||
if (modelPredictLink) {
|
||||
modelPredictLink.click();
|
||||
console.log("已点击'模型预测'链接");
|
||||
} else {
|
||||
console.error("未找到'模型预测'链接");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"选择数字对象"选项卡
|
||||
const numObjTab = Array.from(document.querySelectorAll('[role="tab"]')).find(tab =>
|
||||
tab.textContent.includes("选择数字对象")
|
||||
);
|
||||
if (numObjTab) {
|
||||
numObjTab.click();
|
||||
console.log("已点击'选择数字对象'选项卡");
|
||||
} else {
|
||||
console.error("未找到'选择数字对象'选项卡");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"多重填补数字对象"
|
||||
const multipleNumObjs = Array.from(document.querySelectorAll('div')).filter(div =>
|
||||
div.textContent.trim() === "多重填补数字对象"
|
||||
);
|
||||
if (multipleNumObjs.length > 1) {
|
||||
multipleNumObjs[1].click();
|
||||
console.log("已点击'多重填补数字对象'");
|
||||
} else if (multipleNumObjs.length > 0) {
|
||||
multipleNumObjs[0].click();
|
||||
console.log("已点击'多重填补数字对象'");
|
||||
} else {
|
||||
console.error("未找到'多重填补数字对象'");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 选择"Clopidogrel"
|
||||
const clopidogrelOption = Array.from(document.querySelectorAll('[role="option"]')).find(option =>
|
||||
option.textContent.includes("Clopidogrel")
|
||||
);
|
||||
if (clopidogrelOption) {
|
||||
clopidogrelOption.click();
|
||||
console.log("已选择'Clopidogrel'");
|
||||
} else {
|
||||
console.error("未找到'Clopidogrel'选项");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 勾选特定复选框
|
||||
const checkbox = document.querySelector('#col_pro');
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
console.log("已勾选复选框");
|
||||
} else {
|
||||
console.error("未找到指定复选框");
|
||||
}
|
||||
await wait(1000);
|
||||
|
||||
// 点击"查看上传的数据"按钮
|
||||
const viewDataBtn = Array.from(document.querySelectorAll('button')).find(btn =>
|
||||
btn.textContent.includes("查看上传的数据")
|
||||
);
|
||||
if (viewDataBtn) {
|
||||
viewDataBtn.click();
|
||||
console.log("已点击'查看上传的数据'按钮");
|
||||
} else {
|
||||
console.error("未找到'查看上传的数据'按钮");
|
||||
}
|
||||
await wait(2000);
|
||||
|
||||
// 点击"计算模型预测结果"按钮
|
||||
const calculateBtn = Array.from(document.querySelectorAll('button')).find(btn =>
|
||||
btn.textContent.includes("计算模型预测结果")
|
||||
);
|
||||
if (calculateBtn) {
|
||||
calculateBtn.click();
|
||||
console.log("已点击'计算模型预测结果'按钮");
|
||||
} else {
|
||||
console.error("未找到'计算模型预测结果'按钮");
|
||||
}
|
||||
|
||||
console.log("Case03 第二部分脚本执行完成");
|
||||
} catch (error) {
|
||||
console.error("执行过程中出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行自动化操作
|
||||
automate();
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,24 +5,30 @@ import { clearBadge, streamDownload } from "@/utils/pull-ollama"
|
||||
export default defineBackground({
|
||||
main() {
|
||||
let isCopilotRunning: boolean = false
|
||||
browser.runtime.onMessage.addListener(async (message) => {
|
||||
if (message.type === "sidepanel") {
|
||||
await browser.sidebarAction.open()
|
||||
} else if (message.type === "pull_model") {
|
||||
const ollamaURL = await getOllamaURL()
|
||||
browser.runtime.onMessage.addListener(async (message,sender,sendResponse) => {
|
||||
switch(message.type){
|
||||
case "sidepanel":
|
||||
await browser.sidebarAction.open()
|
||||
break;
|
||||
case "pull_model":
|
||||
const ollamaURL = await getOllamaURL()
|
||||
|
||||
const isRunning = await isOllamaRunning()
|
||||
const isRunning = await isOllamaRunning()
|
||||
|
||||
if (!isRunning) {
|
||||
setBadgeText({ text: "E" })
|
||||
setBadgeBackgroundColor({ color: "#FF0000" })
|
||||
setTitle({ title: "Ollama is not running" })
|
||||
setTimeout(() => {
|
||||
clearBadge()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
await streamDownload(ollamaURL, message.modelName)
|
||||
if (!isRunning) {
|
||||
setBadgeText({ text: "E" })
|
||||
setBadgeBackgroundColor({ color: "#FF0000" })
|
||||
setTitle({ title: "Ollama is not running" })
|
||||
setTimeout(() => {
|
||||
clearBadge()
|
||||
}, 5000)
|
||||
}
|
||||
await streamDownload(ollamaURL, message.modelName)
|
||||
break;
|
||||
case "retrieveDeepScript":
|
||||
return retrieveDeepScript(message);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -180,3 +186,52 @@ export default defineBackground({
|
||||
},
|
||||
persistent: true
|
||||
})
|
||||
|
||||
const iodConfig = {
|
||||
"gatewayUrl": "tcp://127.0.0.1:21051",
|
||||
"registry":"bdware/Registry",
|
||||
"localRepository":"bdtest.local/myrepo1",
|
||||
"doBrowser":"http://127.0.0.1:21030/SCIDE/SCManager"
|
||||
}
|
||||
|
||||
|
||||
|
||||
const makeDOIPParams = (doId:string, op:string, attributes:Object, requestBody: string) => ({
|
||||
action: "executeContract",
|
||||
contractID: "BDBrowser",
|
||||
operation: "sendRequestDirectly",
|
||||
arg: {
|
||||
id: doId,
|
||||
doipUrl: iodConfig.gatewayUrl,
|
||||
op: op,
|
||||
attributes: attributes,
|
||||
body: requestBody
|
||||
}
|
||||
})
|
||||
const retrieveDeepScript = async function(message) {
|
||||
console.log(message);
|
||||
const doId = message.doId;
|
||||
console.log("retriveDoc:"+doId)
|
||||
const params = makeDOIPParams(doId,"Retrieve",{
|
||||
bodyBase64Encoded: false
|
||||
}, "");
|
||||
const abortController = new AbortController()
|
||||
setTimeout(() => abortController.abort(), 10000)
|
||||
return await fetch(iodConfig.doBrowser, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
signal: abortController.signal
|
||||
}).then((response) => {
|
||||
console.log("responseIn retrieveDoc:");
|
||||
console.log(response);
|
||||
return response.json()})
|
||||
.then((res) => {
|
||||
console.log("res:");
|
||||
console.log(res.result.body);
|
||||
//TODO
|
||||
return {
|
||||
metadata:{traceId:res.result.header.attributes?.traceId},
|
||||
pageContent:res.result.body
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Page Assist - A Web UI for Local AI Models</title>
|
||||
<title>IoD Bot - A Web UI for Local AI Models</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
<meta name="manifest.browser_style" content="false" />
|
||||
|
||||
@@ -2,7 +2,7 @@ import { saveHistory, saveMessage } from "@/db"
|
||||
import { setLastUsedChatModel, setLastUsedChatSystemPrompt } from "@/services/model-settings"
|
||||
import { generateTitle } from "@/services/title"
|
||||
import { ChatHistory } from "@/store/option"
|
||||
|
||||
import { updateDialog } from "@/web/iod"
|
||||
export const saveMessageOnError = async ({
|
||||
e,
|
||||
history,
|
||||
@@ -154,6 +154,7 @@ export const saveMessageOnSuccess = async ({
|
||||
prompt_content?: string
|
||||
reasoning_time_taken?: number
|
||||
}) => {
|
||||
var botMessage;
|
||||
if (historyId) {
|
||||
if (!isRegenerate) {
|
||||
await saveMessage(
|
||||
@@ -170,7 +171,7 @@ export const saveMessageOnSuccess = async ({
|
||||
reasoning_time_taken
|
||||
)
|
||||
}
|
||||
await saveMessage(
|
||||
botMessage = await saveMessage(
|
||||
historyId,
|
||||
selectedModel!,
|
||||
"assistant",
|
||||
@@ -183,6 +184,7 @@ export const saveMessageOnSuccess = async ({
|
||||
generationInfo,
|
||||
reasoning_time_taken
|
||||
)
|
||||
updateDialog(historyId, botMessage)
|
||||
await setLastUsedChatModel(historyId, selectedModel!)
|
||||
if (prompt_id || prompt_content) {
|
||||
await setLastUsedChatSystemPrompt(historyId, { prompt_content, prompt_id })
|
||||
@@ -203,7 +205,7 @@ export const saveMessageOnSuccess = async ({
|
||||
generationInfo,
|
||||
reasoning_time_taken
|
||||
)
|
||||
await saveMessage(
|
||||
botMessage = await saveMessage(
|
||||
newHistoryId.id,
|
||||
selectedModel!,
|
||||
"assistant",
|
||||
@@ -216,6 +218,7 @@ export const saveMessageOnSuccess = async ({
|
||||
generationInfo,
|
||||
reasoning_time_taken
|
||||
)
|
||||
updateDialog(newHistoryId.id, botMessage)
|
||||
setHistoryId(newHistoryId.id)
|
||||
await setLastUsedChatModel(newHistoryId.id, selectedModel!)
|
||||
if (prompt_id || prompt_content) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
promptForRag,
|
||||
systemPromptForNonRagOption
|
||||
} from "~/services/ollama"
|
||||
import { type ChatHistory, ChatMessage, type Message } from "~/store/option"
|
||||
import type { ChatHistory, Message, MeteringEntry } from "~/store/option"
|
||||
import { SystemMessage } from "@langchain/core/messages"
|
||||
import { useStoreMessageOption } from "~/store/option"
|
||||
import {
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { notification } from "antd"
|
||||
import { getSystemPromptForWeb } from "~/web/web"
|
||||
import { tokenizeInput } from "~/web/iod"
|
||||
|
||||
import { generateHistory } from "@/utils/generate-history"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
@@ -38,6 +40,7 @@ import { pageAssistModel } from "@/models"
|
||||
import { getNoOfRetrievedDocs } from "@/services/app"
|
||||
import { humanMessageFormatter } from "@/utils/human-message"
|
||||
import { pageAssistEmbeddingModel } from "@/models/embedding"
|
||||
|
||||
import {
|
||||
isReasoningEnded,
|
||||
isReasoningStarted,
|
||||
@@ -55,8 +58,9 @@ export const useMessageOption = () => {
|
||||
const {
|
||||
history,
|
||||
setHistory,
|
||||
chatMessages,
|
||||
setChatMessages,
|
||||
meteringEntries,
|
||||
setMeteringEntries,
|
||||
setCurrentMeteringEntry,
|
||||
setStreaming,
|
||||
streaming,
|
||||
setIsFirstMessage,
|
||||
@@ -114,23 +118,24 @@ export const useMessageOption = () => {
|
||||
setWebSearch(true)
|
||||
}
|
||||
}
|
||||
// 从最后的结果中解析出 思维链 和 结果
|
||||
|
||||
// 从最后的结果中解析出 思维链 (Chain-of-Thought) 和 结果
|
||||
const responseResolver = (msg: string) => {
|
||||
const thinkStart = msg.indexOf("<think>")
|
||||
const thinkEnd = msg.indexOf("</think>")
|
||||
let think = ""
|
||||
const cotStart = msg.indexOf("<think>")
|
||||
const cotEnd = msg.indexOf("</think>")
|
||||
let cot = ""
|
||||
let content = ""
|
||||
if (thinkStart > -1 && thinkEnd > -1) {
|
||||
think = msg.substring(thinkStart + 7, thinkEnd)
|
||||
content = msg.substring(thinkEnd + 8)
|
||||
if (cotStart > -1 && cotEnd > -1) {
|
||||
cot = msg.substring(cotStart + 7, cotEnd)
|
||||
content = msg.substring(cotEnd + 8)
|
||||
} else {
|
||||
content = msg
|
||||
}
|
||||
// 去掉换行符
|
||||
think = think.replace(/\n/g, "")
|
||||
cot = cot.replace(/\n/g, "")
|
||||
content = content.replace(/\n/g, "")
|
||||
return {
|
||||
think,
|
||||
cot: cot,
|
||||
content
|
||||
}
|
||||
}
|
||||
@@ -190,11 +195,16 @@ export const useMessageOption = () => {
|
||||
})
|
||||
let newMessage: Message[] = []
|
||||
let generateMessageId = generateID()
|
||||
const chatMessage: ChatMessage = {
|
||||
const meter: MeteringEntry = {
|
||||
id: generateMessageId,
|
||||
queryContent: message,
|
||||
date: new Date()
|
||||
} as ChatMessage
|
||||
date: new Date().getTime()
|
||||
} as MeteringEntry
|
||||
|
||||
setCurrentMeteringEntry({
|
||||
loading: true,
|
||||
data: meter
|
||||
})
|
||||
|
||||
if (!isRegenerate) {
|
||||
newMessage = [
|
||||
@@ -306,6 +316,9 @@ export const useMessageOption = () => {
|
||||
// Currently only IoD search use keywords
|
||||
if (iodSearch) {
|
||||
// Extract keywords
|
||||
console.log("query:"+query+" --> "+JSON.stringify(tokenizeInput(query)));
|
||||
keywords = tokenizeInput(query)
|
||||
/*
|
||||
const questionPrompt = await geWebSearchKeywordsPrompt()
|
||||
const promptForQuestion = questionPrompt.replaceAll("{query}", query)
|
||||
const response = await ollama.invoke(promptForQuestion)
|
||||
@@ -313,18 +326,23 @@ export const useMessageOption = () => {
|
||||
res = removeReasoning(res)
|
||||
keywords = res
|
||||
.replace(/^Keywords:/i, "")
|
||||
.split(", ")
|
||||
.replace(/^关键词:/i, "")
|
||||
.replace(/^:/i, "")
|
||||
.replace(/^:/i, "")
|
||||
.replaceAll(" ", "")
|
||||
.split(",")
|
||||
.map((k) => k.trim())
|
||||
*/
|
||||
}
|
||||
|
||||
const { prompt, webSources, iodSources, iodData, iodDataTokenCount } =
|
||||
const { prompt, webSources, iodSources, iodSearchResults: iodData, iodTokenCount } =
|
||||
await getSystemPromptForWeb(query, keywords, webSearch, iodSearch)
|
||||
console.log("prompt:\n" + prompt)
|
||||
setIsSearchingInternet(false)
|
||||
chatMessage.prompt = prompt
|
||||
chatMessage.iodKeywords = keywords
|
||||
chatMessage.iodData = iodData
|
||||
chatMessage.iodDataTokenCount = iodDataTokenCount
|
||||
meter.prompt = prompt
|
||||
meter.iodKeywords = keywords
|
||||
meter.iodData = iodData
|
||||
meter.iodTokenCount = iodTokenCount
|
||||
|
||||
// message = message.trim().replaceAll("\n", " ")
|
||||
|
||||
@@ -385,6 +403,7 @@ export const useMessageOption = () => {
|
||||
}
|
||||
)
|
||||
let count = 0
|
||||
const chatStartTime = new Date()
|
||||
let reasoningStartTime: Date | undefined = undefined
|
||||
let reasoningEndTime: Date | undefined = undefined
|
||||
let apiReasoning = false
|
||||
@@ -485,16 +504,30 @@ export const useMessageOption = () => {
|
||||
setIsProcessing(false)
|
||||
setStreaming(false)
|
||||
|
||||
chatMessage.modelInputTokenCount = prompt.length
|
||||
chatMessage.modelOutputTokenCount = fullText.length
|
||||
chatMessage.model = ollama.modelName
|
||||
chatMessage.relatedDataCount = iodData?.length ?? 0
|
||||
chatMessage.timeTaken = new Date().getTime() - chatMessage.date.getTime()
|
||||
const { think, content } = responseResolver(fullText)
|
||||
chatMessage.thinkingChain = think
|
||||
chatMessage.responseContent = content
|
||||
chatMessage.modelResponseContent = fullText
|
||||
setChatMessages([chatMessage, ...chatMessages])
|
||||
// Save metering entry
|
||||
const { cot, content } = responseResolver(fullText)
|
||||
const currentMeteringEntry = {
|
||||
...meter,
|
||||
modelInputTokenCount: prompt.length,
|
||||
modelOutputTokenCount: fullText.length,
|
||||
model: ollama.modelName ?? ollama.model,
|
||||
relatedDataCount: iodData?.length ?? 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)
|
||||
localStorage.setItem("meteringEntries", JSON.stringify(_meteringEntries))
|
||||
} catch (e) {
|
||||
const errorSave = await saveMessageOnError({
|
||||
e,
|
||||
@@ -612,6 +645,16 @@ export const useMessageOption = () => {
|
||||
|
||||
let newMessage: Message[] = []
|
||||
let generateMessageId = generateID()
|
||||
const meter: MeteringEntry = {
|
||||
id: generateMessageId,
|
||||
queryContent: message,
|
||||
date: new Date().getTime()
|
||||
} as MeteringEntry
|
||||
|
||||
setCurrentMeteringEntry({
|
||||
loading: true,
|
||||
data: meter,
|
||||
})
|
||||
|
||||
if (!isRegenerate) {
|
||||
newMessage = [
|
||||
@@ -738,6 +781,7 @@ export const useMessageOption = () => {
|
||||
let reasoningStartTime: Date | null = null
|
||||
let reasoningEndTime: Date | null = null
|
||||
let apiReasoning: boolean = false
|
||||
const chatStartTime = new Date()
|
||||
|
||||
for await (const chunk of chunks) {
|
||||
if (chunk?.additional_kwargs?.reasoning_content) {
|
||||
@@ -838,6 +882,31 @@ export const useMessageOption = () => {
|
||||
setStreaming(false)
|
||||
setIsProcessing(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) {
|
||||
const errorSave = await saveMessageOnError({
|
||||
e,
|
||||
|
||||
51
src/libs/playground.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import RocketSvg from "@/assets/icons/rocket.svg"
|
||||
import BulbSvg from "@/assets/icons/bulb.svg"
|
||||
import EyeSvg from "@/assets/icons/eye.svg"
|
||||
import ASvg from "@/assets/icons/a.svg"
|
||||
import BSvg from "@/assets/icons/b.svg"
|
||||
import CSvg from "@/assets/icons/c.svg"
|
||||
import DSvg from "@/assets/icons/d.svg"
|
||||
import ESvg from "@/assets/icons/e.svg"
|
||||
import FSvg from "@/assets/icons/f.svg"
|
||||
|
||||
export const qaPrompt = [
|
||||
{
|
||||
title: "最近一年大型语言模型的技术进展有哪些?",
|
||||
icon: <img src={RocketSvg} alt="Rocket" className="w-full my-0" />,
|
||||
},
|
||||
{
|
||||
title: "生成式AI在企业中有哪些具体应用场景?",
|
||||
icon: <img src={BulbSvg} alt="Rocket" className="w-full my-0" />,
|
||||
},
|
||||
{
|
||||
title: "多模态学习技术的最新研究方向是什么?",
|
||||
icon: <img src={EyeSvg} alt="Rocket" className="w-full my-0" />,
|
||||
},
|
||||
{
|
||||
title: "当前AI芯片市场格局和未来三年发展趋势如何?",
|
||||
icon: <img src={ASvg} alt="Rocket" className="w-full my-0" />,
|
||||
},
|
||||
{
|
||||
title: "主流深度学习框架性能与易用性对比分析?",
|
||||
icon: <img src={BSvg} alt="Rocket" className="w-full my-0" />,
|
||||
},
|
||||
{
|
||||
title: "国内外AI伦理治理框架有哪些最佳实践?",
|
||||
icon: <img src={CSvg} alt="Rocket" className="w-full my-0" />,
|
||||
},
|
||||
{
|
||||
title: "大规模知识图谱构建与应用最新进展?",
|
||||
icon: <img src={DSvg} alt="Rocket" className="w-full my-0" />,
|
||||
},
|
||||
{
|
||||
title: "计算机视觉领域近期突破性技术有哪些?",
|
||||
icon: <img src={ESvg} alt="Rocket" className="w-full my-0" />,
|
||||
},
|
||||
{
|
||||
title: "量子计算对AI算法的影响与应用前景?",
|
||||
icon: <img src={FSvg} alt="Rocket" className="w-full my-0" />,
|
||||
},
|
||||
].map((item, index) => ({
|
||||
...item,
|
||||
id: index.toString(),
|
||||
}))
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extName": {
|
||||
"message": "Page Assist - 本地 AI 模型的 Web UI"
|
||||
"message": "IoD Bot - 本地 AI 模型的 Web UI"
|
||||
},
|
||||
"extDescription": {
|
||||
"message": "使用本地运行的 AI 模型来辅助您的网络浏览。"
|
||||
|
||||
@@ -21,7 +21,37 @@ 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 DEFAULT_WEBSEARCH_PROMPT = `You are an AI model who is expert at searching the web and answering user's queries.
|
||||
|
||||
const DEFAULT_WEBSEARCH_PROMPT = `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}.
|
||||
|
||||
|
||||
The \`iod-search-results\` block provides information retrieved from Internet of Data. Each search result has a format of:
|
||||
\`<result doId="{doId}" name="{title}" authors="{authors}" dataType="{paper,dataset or algorithm}" year="{year}" url="{url}" id="{id}">{abstract}</result>\`
|
||||
|
||||
Please show the \`doId\` and \`name\` of the search result when you refer to search result in your response and chain of thought, in the following format, in English:
|
||||
\`[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)\`
|
||||
|
||||
Use this information to generate a meaningful response that includes:
|
||||
0. 如果搜索结果看着和用户想做的事儿无关,那么直接忽略它,不需要在思维链和回答中体现。
|
||||
1. 从搜索结果中,用户可以参考哪些论文来实现他的目标。
|
||||
2. 从搜索结果中,用户可以使用哪些数据集(dataset)
|
||||
3. 用户想干的这个事儿,如何结合这些数据来实现。
|
||||
4. 请用中文回答这个问题。
|
||||
<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.
|
||||
|
||||
Generate a response that is informative and relevant to the user's query based on provided search results. the current date and time are {current_date_time}.
|
||||
|
||||
@@ -86,24 +116,25 @@ const DEFAULT_WEBSEARCH_KEYWORDS_PROMPT = `Extract the most important keywords f
|
||||
|
||||
The result format should be: keyword_1, keyword_2, ..., keyword_n
|
||||
|
||||
注意,以下关键词请不要输出:"research", "研究", "data analysis", "data", "数据" 。
|
||||
注意,英文单词的输出首字母应该小写,仅需输出Keywords部分,Query部分不用输出。以下是一些例子。
|
||||
Example:
|
||||
|
||||
Query: What are the symptoms of a heart attack?
|
||||
|
||||
Keywords: symptoms, 症状, heart attack, 心臟病
|
||||
你的输出: symptoms, 症状, heart attack, 心臟病
|
||||
|
||||
Query: 什么是物联网?
|
||||
|
||||
Keywords: Internet of Things, IoT, 物联网
|
||||
你的输出: Internet of Things, IoT, 物联网
|
||||
|
||||
Query: 人工智能的发展趋势?
|
||||
|
||||
Keywords: Artificial Intelligence, AI, 人工智能, trend, 趋势
|
||||
|
||||
你的输出: Artificial Intelligence, AI, 人工智能, trend, 趋势
|
||||
|
||||
接下来,开始你的关键词提取吧。
|
||||
Query: {query}
|
||||
|
||||
Keywords:
|
||||
`
|
||||
|
||||
export const getOllamaURL = async () => {
|
||||
|
||||
@@ -30,49 +30,15 @@ export type ChatHistory = {
|
||||
messageType?: string
|
||||
}[]
|
||||
|
||||
export type ChatMessage = {
|
||||
id: string
|
||||
// 问题
|
||||
queryContent: string
|
||||
// 提示词全文
|
||||
prompt: string
|
||||
// 思维链(只有深度思考时有)
|
||||
thinkingChain?: string
|
||||
// 回答
|
||||
responseContent: string
|
||||
// 关联数据个数
|
||||
relatedDataCount: number
|
||||
// 数联网输入token
|
||||
iodInputToken: string
|
||||
// 数联网输出token
|
||||
iodOutputToken: string
|
||||
// 大模型输入token数量
|
||||
modelInputTokenCount: number
|
||||
// 大模型输出token数量
|
||||
modelOutputTokenCount: number
|
||||
// 日期
|
||||
date: Date
|
||||
// 耗时
|
||||
timeTaken: number
|
||||
// 大模型回答的全部内容
|
||||
modelResponseContent: string
|
||||
// iod的全部内容的token数量
|
||||
iodDataTokenCount: number
|
||||
// iod返回的数据
|
||||
iodData: any[]
|
||||
// iod keywords
|
||||
iodKeywords: string[]
|
||||
// 模型
|
||||
model: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
messages: Message[]
|
||||
setMessages: (messages: Message[]) => void
|
||||
history: ChatHistory
|
||||
setHistory: (history: ChatHistory) => void
|
||||
chatMessages: ChatMessage[]
|
||||
setChatMessages: (chatMessages: ChatMessage[]) => void
|
||||
currentMeteringEntry: {data: MeteringEntry, loading: boolean}
|
||||
setCurrentMeteringEntry: (meteringEntry: {data: MeteringEntry, loading: boolean}) => void
|
||||
meteringEntries: MeteringEntry[]
|
||||
setMeteringEntries: (meteringEntries: MeteringEntry[]) => void
|
||||
streaming: boolean
|
||||
setStreaming: (streaming: boolean) => void
|
||||
isFirstMessage: boolean
|
||||
@@ -115,13 +81,51 @@ type State = {
|
||||
setUseOCR: (useOCR: boolean) => void
|
||||
}
|
||||
|
||||
export type MeteringEntry = {
|
||||
id: string
|
||||
// 问题
|
||||
queryContent: string
|
||||
// 提示词全文
|
||||
prompt: string
|
||||
// 思维链(只有深度思考时有)
|
||||
cot?: string
|
||||
// 回答
|
||||
responseContent: string
|
||||
// 关联数据个数
|
||||
relatedDataCount: number
|
||||
// 数联网输入token
|
||||
iodInputToken: string
|
||||
// 数联网输出token
|
||||
iodOutputToken: string
|
||||
// 大模型输入token数量
|
||||
modelInputTokenCount: number
|
||||
// 大模型输出token数量
|
||||
modelOutputTokenCount: number
|
||||
// 日期
|
||||
date: number
|
||||
// 耗时
|
||||
timeTaken: number
|
||||
// 大模型回答的全部内容
|
||||
modelResponseContent: string
|
||||
// iod的全部内容的token数量
|
||||
iodTokenCount: number
|
||||
// iod返回的数据
|
||||
iodData: any[]
|
||||
// iod keywords
|
||||
iodKeywords: string[]
|
||||
// 模型
|
||||
model: string
|
||||
}
|
||||
|
||||
export const useStoreMessageOption = create<State>((set) => ({
|
||||
messages: [],
|
||||
setMessages: (messages) => set({ messages }),
|
||||
history: [],
|
||||
setHistory: (history) => set({ history }),
|
||||
chatMessages: [],
|
||||
setChatMessages: (chatMessages) => set({ chatMessages }),
|
||||
currentMeteringEntry: {data: {} as MeteringEntry, loading: false},
|
||||
setCurrentMeteringEntry: (currentMeteringEntry) => set({ currentMeteringEntry }),
|
||||
meteringEntries: JSON.parse(localStorage.getItem("meteringEntries") || JSON.stringify([])),
|
||||
setMeteringEntries: (meteringEntries) => set({ meteringEntries }),
|
||||
streaming: false,
|
||||
setStreaming: (streaming) => set({ streaming }),
|
||||
isFirstMessage: true,
|
||||
|
||||
@@ -6,4 +6,6 @@ export type IodRegistryEntry = {
|
||||
description: string
|
||||
content?: string
|
||||
data_space?: string
|
||||
data_type?:string
|
||||
traceId?:string
|
||||
}
|
||||
|
||||
8
src/types/segmentit.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Segment, useDefault, cnPOSTag, enPOSTag } from 'segmentit';
|
||||
|
||||
declare module 'segmentit' {
|
||||
export = Segment;
|
||||
export = useDefault;
|
||||
export = cnPOSTag;
|
||||
export = enPOSTag;
|
||||
}
|
||||
21
src/web/1.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"action": "executeContract",
|
||||
"contractID": "BDBrowser",
|
||||
"operation": "sendRequestDirectly",
|
||||
"arg": {
|
||||
"id": "670E241C9937B3537047C87053E3AA36",
|
||||
"doipUrl": "tcp://reg01.public.internetofdata.cn:21037",
|
||||
"op": "Search",
|
||||
"attributes": {
|
||||
"offset": 2100,
|
||||
"count": 5,
|
||||
"bodyBase64Encoded": false,
|
||||
"searchMode": [
|
||||
{ "key": "data_type", "type": "MUST", "value": "paper" },
|
||||
{ "key": "title", "type": "MUST", "value": "Number_1" },
|
||||
{ "key": "description", "type": "MUST", "value": "Number_1" }
|
||||
]
|
||||
},
|
||||
"body": ""
|
||||
}
|
||||
}
|
||||
33
src/web/2.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
const ollama = await pageAssistModel({
|
||||
model: selectedModel!,
|
||||
baseUrl: cleanUrl(url),
|
||||
keepAlive:
|
||||
currentChatModelSettings?.keepAlive ?? userDefaultModelSettings?.keepAlive,
|
||||
temperature:
|
||||
currentChatModelSettings?.temperature ??
|
||||
userDefaultModelSettings?.temperature,
|
||||
topK: currentChatModelSettings?.topK ?? userDefaultModelSettings?.topK,
|
||||
topP: currentChatModelSettings?.topP ?? userDefaultModelSettings?.topP,
|
||||
numCtx: currentChatModelSettings?.numCtx ?? userDefaultModelSettings?.numCtx,
|
||||
seed: currentChatModelSettings?.seed,
|
||||
numGpu: currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
||||
numPredict:
|
||||
currentChatModelSettings?.numPredict ??
|
||||
userDefaultModelSettings?.numPredict,
|
||||
useMMap:
|
||||
currentChatModelSettings?.useMMap ?? userDefaultModelSettings?.useMMap,
|
||||
minP: currentChatModelSettings?.minP ?? userDefaultModelSettings?.minP,
|
||||
repeatLastN:
|
||||
currentChatModelSettings?.repeatLastN ??
|
||||
userDefaultModelSettings?.repeatLastN,
|
||||
repeatPenalty:
|
||||
currentChatModelSettings?.repeatPenalty ??
|
||||
userDefaultModelSettings?.repeatPenalty,
|
||||
tfsZ: currentChatModelSettings?.tfsZ ?? userDefaultModelSettings?.tfsZ,
|
||||
numKeep:
|
||||
currentChatModelSettings?.numKeep ?? userDefaultModelSettings?.numKeep,
|
||||
numThread:
|
||||
currentChatModelSettings?.numThread ?? userDefaultModelSettings?.numThread,
|
||||
useMlock:
|
||||
currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock
|
||||
})
|
||||
263
src/web/iod.ts
@@ -3,64 +3,234 @@ import { PageAssistHtmlLoader } from "@/loader/html"
|
||||
import { PageAssistPDFUrlLoader } from "@/loader/pdf-url"
|
||||
import { pageAssistEmbeddingModel } from "@/models/embedding"
|
||||
import { defaultEmbeddingModelForRag, getOllamaURL } from "@/services/ollama"
|
||||
|
||||
import {
|
||||
getIsSimpleInternetSearch,
|
||||
totalSearchResults
|
||||
} from "@/services/search"
|
||||
import { getPageAssistTextSplitter } from "@/utils/text-splitter"
|
||||
import type { Document } from "@langchain/core/documents"
|
||||
import { Document } from "@langchain/core/documents"
|
||||
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
||||
import type { IodRegistryEntry } from "~/types/iod"
|
||||
|
||||
const makeRegSearchParams = (count: number, keyword: string) => ({
|
||||
|
||||
import { PageAssitDatabase } from "@/db"
|
||||
import exp from "constants"
|
||||
|
||||
import { Segment, useDefault, cnPOSTag, enPOSTag} from 'segmentit';
|
||||
const segment = useDefault(new Segment());
|
||||
export const tokenizeInput = function (input: string): string[] {
|
||||
const words = segment.doSegment(input, { simple: false });
|
||||
console.log(words.map(function(word){return {w:word.w, p:enPOSTag(word.p)}}) );
|
||||
return words.filter(word =>( word.w.length > 1)).map(word=>word.w);
|
||||
}
|
||||
//doipUrl = tcp://reg01.public.internetofdata.cn:21037
|
||||
export const iodConfig = {
|
||||
"gatewayUrl": "tcp://021.node.internetapi.cn:21052",
|
||||
"registry":"data/Registry",
|
||||
"localRepository":"data/Repository",
|
||||
"doBrowser":"http://021.node.internetapi.cn:21030/SCIDE/SCManager"
|
||||
}
|
||||
function inGrepList(str: string){
|
||||
return "什么|问题|需要|合适|设计|考虑|合作|精度|传感器|最新|研究|药物".indexOf(str)!=-1;
|
||||
}
|
||||
export const makeRegSearchParams = function(count: number, keyword: string| string[]){
|
||||
const searchMode = [];
|
||||
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 makeDOIPParams = (doId:string, op:string, attributes:Object, requestBody: string) => ({
|
||||
action: "executeContract",
|
||||
contractID: "BDBrowser",
|
||||
operation: "sendRequestDirectly",
|
||||
arg: {
|
||||
id: "670E241C9937B3537047C87053E3AA36",
|
||||
doipUrl: "tcp://reg01.public.internetofdata.cn:21037",
|
||||
op: "Search",
|
||||
attributes: {
|
||||
offset: 0,
|
||||
count,
|
||||
bodyBase64Encoded: false,
|
||||
searchMode: [
|
||||
{
|
||||
key: "data_type",
|
||||
type: "MUST",
|
||||
value: "paper"
|
||||
},
|
||||
// {
|
||||
// key: "title",
|
||||
// type: "MUST",
|
||||
// value: keyword,
|
||||
// },
|
||||
{
|
||||
key: "description",
|
||||
type: "MUST",
|
||||
value: keyword
|
||||
}
|
||||
]
|
||||
},
|
||||
body: ""
|
||||
id: doId,
|
||||
doipUrl: iodConfig.gatewayUrl,
|
||||
op: op,
|
||||
attributes: attributes,
|
||||
body: requestBody
|
||||
}
|
||||
})
|
||||
|
||||
export const retrieveDoc = function(doId: string) : Promise<Document> {
|
||||
console.log("retriveDoc:"+doId)
|
||||
const params = makeDOIPParams(doId,"Retrieve",{
|
||||
bodyBase64Encoded: false
|
||||
}, "");
|
||||
const abortController = new AbortController()
|
||||
setTimeout(() => abortController.abort(), 10000)
|
||||
return fetch(iodConfig.doBrowser, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
signal: abortController.signal
|
||||
}).then((response) => {
|
||||
console.log("responseIn retrieveDoc:");
|
||||
console.log(response);
|
||||
return response.json()})
|
||||
.then((res) => {
|
||||
console.log("res:");
|
||||
console.log(res.result.body);
|
||||
//TODO
|
||||
return {
|
||||
metadata:{traceId:res.result.header.attributes?.traceId},
|
||||
pageContent:res.result.body
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const updateInLocalRepo = function(historyId: string, requestBody: Object) : Promise<string> {
|
||||
const params = makeDOIPParams(iodConfig.localRepository,"Update",{
|
||||
"aiDialogID": historyId,
|
||||
bodyBase64Encoded: false
|
||||
}, JSON.stringify(requestBody));
|
||||
const abortController = new AbortController()
|
||||
setTimeout(() => abortController.abort(), 10000)
|
||||
return fetch(iodConfig.doBrowser, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
signal: abortController.signal
|
||||
}).then((response) => response.json())
|
||||
.then((res) => {
|
||||
console.log("update dialog:"+JSON.stringify(res))
|
||||
return res.body;
|
||||
})
|
||||
}
|
||||
export const updateDialog = async function(histroyId : string, botMessage: any): Promise<string> {
|
||||
//TODO @Nex confused by Message/MessageType in ./db/index.ts!
|
||||
const db = new PageAssitDatabase()
|
||||
const chatHistory = await db.getChatHistory(histroyId)
|
||||
var userMessage = null;
|
||||
for (var i=0;i<chatHistory.length;i++){
|
||||
userMessage = chatHistory[i];
|
||||
if (userMessage.role=='user') break;
|
||||
}
|
||||
let updateBody:any = {};
|
||||
// !!!IMPORTANT!!! traceId = histroyId+"/"+userMessage.id;
|
||||
// Update traceId in retrieveDoc!
|
||||
updateBody.traceId = histroyId+"/"+userMessage.id;
|
||||
updateBody.question = {
|
||||
"id": histroyId+"/"+userMessage.id,
|
||||
"content": userMessage.content,
|
||||
"tokenCount": userMessage.content.length
|
||||
}
|
||||
updateBody.answer = {
|
||||
"id": histroyId+"/"+botMessage.id,
|
||||
"content": botMessage.content,
|
||||
"tokenCount": botMessage.content.length
|
||||
}
|
||||
//TODO set a correct model ID
|
||||
updateBody.model = {"id":"bdware.ollama/" + userMessage.name}
|
||||
|
||||
//TODO incorrect tokenCount calculated!!
|
||||
updateBody.webSources = botMessage.webSources?.map((r) => ({
|
||||
url: r.url,
|
||||
tokenCount: r.url.length,
|
||||
content: r.url,
|
||||
traceId: r?.traceId
|
||||
})) ?? [];
|
||||
updateBody.IoDSources = botMessage.iodSources?.map((r) => ({
|
||||
id: r.doId,
|
||||
tokenCount: (r.content || r.description)?calculateTokenCount((r.content || r.description)):0,
|
||||
content: r.content || r.description,
|
||||
traceId: r?.traceId
|
||||
})) ?? [];
|
||||
console.log("updateBody:");
|
||||
console.log(updateBody)
|
||||
return updateInLocalRepo(histroyId,updateBody)
|
||||
}
|
||||
|
||||
export async function localIodSearch(
|
||||
query: string,
|
||||
keywords: string[]
|
||||
): Promise<IodRegistryEntry[]> {
|
||||
const TOTAL_SEARCH_RESULTS = await totalSearchResults()
|
||||
const abortController = new AbortController();
|
||||
setTimeout(() => abortController.abort(), 10000);
|
||||
const params = makeRegSearchParams(TOTAL_SEARCH_RESULTS, keywords);
|
||||
console.log('params------->',params)
|
||||
try {
|
||||
const response = await fetch(iodConfig.doBrowser, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
signal: abortController.signal
|
||||
});
|
||||
|
||||
const res = await response.json();
|
||||
if (res.status !== "Success") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const body = JSON.parse(res.result.body);
|
||||
if (body.code !== 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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) {
|
||||
console.log(e);
|
||||
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("http://47.93.156.31:21033/SCIDE/SCManager", {
|
||||
return fetch(iodConfig.doBrowser, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(params),
|
||||
signal: abortController.signal
|
||||
@@ -76,11 +246,13 @@ export async function localIodSearch(
|
||||
console.log(body)
|
||||
return []
|
||||
}
|
||||
const results: IodRegistryEntry[] =
|
||||
body.data?.results?.filter((r) => r.url || r.pdf_url) || []
|
||||
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) => {
|
||||
@@ -96,8 +268,8 @@ export async function localIodSearch(
|
||||
for (const r of results) {
|
||||
map.set(r.doId, r)
|
||||
}
|
||||
|
||||
return Array.from(map.values())
|
||||
*/
|
||||
}
|
||||
|
||||
const ARXIV_URL_PATTERN = /^https?:\/\/arxiv\.org\//
|
||||
@@ -107,7 +279,7 @@ export const searchIod = async (query: string, keywords: string[]) => {
|
||||
const searchResults = await localIodSearch(query, keywords)
|
||||
|
||||
const isSimpleMode = await getIsSimpleInternetSearch()
|
||||
|
||||
console.log("searchMode:"+isSimpleMode+"\n kw:"+JSON.stringify(keywords)+"\n"+" ->searchResult:\n"+JSON.stringify(searchResults))
|
||||
if (isSimpleMode) {
|
||||
await getOllamaURL()
|
||||
return searchResults
|
||||
@@ -117,7 +289,18 @@ export const searchIod = async (query: string, keywords: string[]) => {
|
||||
const resMap = new Map<string, IodRegistryEntry>()
|
||||
for (const result of searchResults) {
|
||||
const url = result.url
|
||||
if (!url) continue
|
||||
if (result.doId){
|
||||
//TODO !!!!@Nex traceId should be the id of history/question!
|
||||
let docFromRetrieve = await retrieveDoc(result.doId);
|
||||
console.log("doc from Retrieve:"+result.doId+" -->"+JSON.stringify(docFromRetrieve))
|
||||
docs.push(docFromRetrieve)
|
||||
result.description = docFromRetrieve.pageContent;
|
||||
result.traceId = docFromRetrieve.metadata?.traceId;
|
||||
continue;
|
||||
}
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let htmlUrl = ""
|
||||
if (ARXIV_URL_PATTERN.test(url)) {
|
||||
@@ -178,6 +361,9 @@ export const searchIod = async (query: string, keywords: string[]) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
return searchResults
|
||||
|
||||
/*
|
||||
const ollamaUrl = await getOllamaURL()
|
||||
|
||||
const embeddingModle = await defaultEmbeddingModelForRag()
|
||||
@@ -208,4 +394,11 @@ export const searchIod = async (query: string, keywords: string[]) => {
|
||||
}).filter((r) => r)
|
||||
|
||||
return searchResult
|
||||
*/
|
||||
}
|
||||
|
||||
export const calculateTokenCount = function(str:string){
|
||||
const byteArray = new TextEncoder().encode(str);
|
||||
return byteArray.length;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { webBaiduSearch } from "./search-engines/baidu"
|
||||
import { searchIod } from "./iod"
|
||||
import type { WebSearchResult } from "~/types/web"
|
||||
import type { IodRegistryEntry } from "~/types/iod"
|
||||
import {calculateTokenCount} from "./iod"
|
||||
|
||||
const getHostName = (url: string) => {
|
||||
try {
|
||||
@@ -100,18 +101,24 @@ export const getSystemPromptForWeb = async (
|
||||
doId: res.doId,
|
||||
name: res.name,
|
||||
url: res.url,
|
||||
data_type: res.data_type,
|
||||
data_space: res.data_space,
|
||||
tokenCount: (res.content || res.description)?.length ?? 0,
|
||||
content: res.content || res.description
|
||||
content: res.content || res.description,
|
||||
tokenCount: (res.content || res.description)?calculateTokenCount((res.content || res.description)):0,
|
||||
traceId:res?.traceId
|
||||
}))
|
||||
|
||||
const iod_search_results = _iodSearchResults
|
||||
.map(
|
||||
(result, idx) =>
|
||||
`<result doId="${result.doId}" name="${result.name}" source="${result.url}" id="${idx + 1}">${result.content}</result>`
|
||||
(result, idx) =>{
|
||||
const nameAttr = result.name ? ` name="${result.name}"` : '';
|
||||
const sourceAttr = result.url ? ` source="${result.url}"` : '';
|
||||
const dataTypeAttr = result.data_type ? ` dataType="${result.data_type}"` : '';
|
||||
const dataSourceAttr = result.data_space ?` 数据来源="${result.data_space}"`:''
|
||||
return `<result doId="${result.doId}"${nameAttr}${sourceAttr}${dataTypeAttr}${dataSourceAttr}" >${result.content}</result>`
|
||||
}
|
||||
)
|
||||
.join("\n")
|
||||
console.log("iod_search_result: " + iod_search_results)
|
||||
|
||||
const web_search_results = webSearchResults
|
||||
.map(
|
||||
@@ -119,7 +126,6 @@ export const getSystemPromptForWeb = async (
|
||||
`<result source="${result.url}" name="${result.name}" id="${idx + 1}">${result.content}</result>`
|
||||
)
|
||||
.join("\n")
|
||||
console.log("web_search_result: " + web_search_results)
|
||||
|
||||
const current_date_time = new Date().toLocaleString()
|
||||
|
||||
@@ -140,15 +146,17 @@ export const getSystemPromptForWeb = async (
|
||||
}
|
||||
}),
|
||||
iodSources: iodSearchResults,
|
||||
iodData: _iodSearchResults,
|
||||
iodDataTokenCount: _iodSearchResults.reduce((acc, cur) => (acc + cur.content.length), 0)
|
||||
iodSearchResults: _iodSearchResults,
|
||||
iodTokenCount: _iodSearchResults.reduce((acc, cur) => (acc + cur.content.length), 0)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return {
|
||||
prompt: "",
|
||||
webSources: [],
|
||||
iodSources: []
|
||||
iodSources: [],
|
||||
iodSearchResults: [],
|
||||
iodTokenCount: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ module.exports = {
|
||||
content: ["./src/**/*.tsx"],
|
||||
theme: {
|
||||
extend: {
|
||||
width: {
|
||||
'1/10': '10%',
|
||||
},
|
||||
backgroundImage: {
|
||||
'bottom-mask-light': 'linear-gradient(0deg, transparent 0, #ffffff 160px)',
|
||||
'bottom-mask-dark': 'linear-gradient(0deg, transparent 0, #171717 160px)',
|
||||
|
||||
@@ -54,7 +54,7 @@ export default defineConfig({
|
||||
version: "1.5.0",
|
||||
name:
|
||||
process.env.TARGET === "firefox"
|
||||
? "Page Assist - A Web UI for Local AI Models"
|
||||
? "IoD Bot - A Web UI for Local AI Models"
|
||||
: "__MSG_extName__",
|
||||
description: "__MSG_extDescription__",
|
||||
default_locale: "en",
|
||||
|
||||