28 Commits

Author SHA1 Message Date
zhaoweijie
224ae55d6b feat/playground: 重构 playground组件
- 更新 Data 和 History组件的样式和布局
- 添加新的功能和交互,如热门搜索和智能体选择
- 优化组件性能和可维护性
2025-08-21 14:08:07 +08:00
zhaoweijie
8a5c5f1c26 refactor(layout): 优化团队页面布局和滚动
- 在 Team组件中添加 overflow-y-auto 以启用垂直滚动
- 在 Playground组件中调整网格布局,移除不必要的导入
- 优化消息列表布局,确保内容可以滚动
2025-08-19 18:13:15 +08:00
zhaoweijie
9e379d13cb refactor(components): 重构历史记录组件和 playground 布局- 更新 History 组件样式和动画效果
- 调整 Playground 布局结构
-优化 Sidebar 聊天记录样式
2025-08-19 17:42:13 +08:00
zhaoweijie
1104fb2733 refactor(layout): 重构布局组件并添加新功能
- 更新 Header 组件,增加项目标题和历史记录切换按钮
- 新增 DataNavigation 组件用于数据导航
- 添加 Playground 相关新组件,包括数据、场景、团队等信息展示
- 重构 Layout 组件,使用 Context 管理历史记录状态
- 更新 zh/option.json 文件,添加新的项目标题和对话相关翻译
2025-08-19 16:20:37 +08:00
CaiHQ
ef0e315bdc feat: upgrade registry 2025-08-17 22:39:12 +08:00
CaiHQ
3fb66b4c36 update iod search 2025-03-24 13:21:49 +08:00
zhaoweijie
3cbf4454da Merge branch 'main' of gitea.internetapi.cn:iod/page-assist 2025-02-28 09:13:37 +08:00
zhaoweijie
6a597da44f refactor(components): 优化计量详情页面布局和内容展示
-调整表格列宽和样式,提高可读性
- 添加数联网引用token总数字段
- 修改数联网引用数据卡片为表格形式
- 优化输入和输出token详情的样式
-调整列表布局,增加全宽样式
2025-02-28 09:12:17 +08:00
zhaoweijie
dba196d777 feat: add localStroage 2025-02-24 11:09:29 +08:00
zhaoweijie
c5fa739a95 feat: change token get 2025-02-24 10:17:05 +08:00
CaiHQ
70d1f40333 fix: file name lower case 2025-02-24 10:05:06 +08:00
CaiHQ
2866bcc7af feat: mock 4 button 2025-02-24 10:02:12 +08:00
CaiHQ
2a57034c9d feat: change filename 2025-02-24 10:02:12 +08:00
Nex Zhu
79a03ab6fc fix: file name case 2025-02-24 09:23:19 +08:00
Nex Zhu
50f9e4354f fix 2025-02-24 08:36:42 +08:00
Nex Zhu
8f27ca2e4e fix: fix no meteringEntry date when no cot, and style 2025-02-24 08:30:37 +08:00
Nex Zhu
ce333714b7 style: revert locale json format 2025-02-23 22:22:43 +08:00
zhaoweijie
7b8879a7a8 feat: add metering data 2025-02-23 13:02:32 +08:00
李芳
c50bb49b37 feat: metring and detail pages 2025-02-22 18:20:11 +08:00
李芳
970ffdac15 Merge branch 'feat/metering' of gitea.internetapi.cn:iod/page-assist into feat/page 2025-02-22 17:00:58 +08:00
zhaoweijie
da162be01d feat: add metering data 2025-02-22 16:57:19 +08:00
zhaoweijie
6d79d42925 feat: add metering data 2025-02-22 14:09:57 +08:00
Nex Zhu
f617a05483 feat: improve DEFAULT_WEBSEARCH_PROMPT for IoD and 3W citations 2025-02-17 19:03:38 +08:00
Nex Zhu
4c5d5cfe99 feat: IoD search process HTML/PDF content 2025-02-17 16:44:33 +08:00
CaiHQ
51188b1428 update search result in prompt 2025-02-15 13:02:24 +08:00
Nex Zhu
a56e46a98d feat: IoD search process HTML/PDF content 2025-02-14 23:24:27 +08:00
Nex Zhu
e8471f1802 feat: add IoD search 2025-02-14 18:17:12 +08:00
Muhammed Nazeem
691575e449 Update README.md 2025-02-12 17:06:23 +05:30
88 changed files with 6230 additions and 802 deletions

View File

@@ -10,6 +10,8 @@ Page Assist supports Chromium-based browsers like Chrome, Brave, and Edge, as we
[![Chrome Web Store](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/page-assist/jfgfiigpkhlkbnfnbobbkinehhfdhndo)
[![Firefox Add-on](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/get-the-addon.png)](https://addons.mozilla.org/en-US/firefox/addon/page-assist/)
[![Edge Add-on](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/edge-addon.png)](https://microsoftedge.microsoft.com/addons/detail/page-assist-a-web-ui-fo/ogkogooadflifpmmidmhjedogicnhooa)
Checkout the Demo (v1.0.0):
@@ -47,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)

2500
bun.lock Normal file

File diff suppressed because one or more lines are too long

BIN
bun.lockb

Binary file not shown.

BIN
operation/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

BIN
operation/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 KiB

BIN
operation/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 KiB

BIN
operation/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

View File

@@ -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
View File

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

After

Width:  |  Height:  |  Size: 590 B

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

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

After

Width:  |  Height:  |  Size: 601 B

View File

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

After

Width:  |  Height:  |  Size: 1.8 KiB

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

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

After

Width:  |  Height:  |  Size: 1.4 KiB

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

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

After

Width:  |  Height:  |  Size: 1.5 KiB

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

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

After

Width:  |  Height:  |  Size: 1.7 KiB

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

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

After

Width:  |  Height:  |  Size: 601 B

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

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

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -109,7 +109,7 @@
"translate": "ترجمة",
"custom": "مخصص"
},
"citations": "الاقتباسات",
"webCitations": "الاقتباسات",
"segmented": {
"ollama": "نماذج Ollama",
"custom": "نماذج مخصصة"

View File

@@ -106,7 +106,7 @@
"translate": "Oversæt",
"custom": "Brugerdefineret"
},
"citations": "Citater",
"webCitations": "Citater",
"downloadCode": "Download Kode",
"date": {
"pinned": "Fastgjort",

View File

@@ -106,7 +106,7 @@
"translate": "Übersetzen",
"custom": "Benutzerdefiniert"
},
"citations": "Zitate",
"webCitations": "Zitate",
"downloadCode": "Code herunterladen",
"date": {
"pinned": "Angepinnt",

View File

@@ -39,6 +39,7 @@
},
"copyToClipboard": "Copy to clipboard",
"webSearch": "Searching the web",
"iodSearch": "Searching the Internet of Data",
"regenerate": "Regenerate",
"edit": "Edit",
"delete": "Delete",
@@ -136,7 +137,8 @@
"translate": "Translate",
"custom": "Custom"
},
"citations": "Citations",
"webCitations": "Web Citations",
"iodCitations": "Internet of Data Citations",
"segmented": {
"ollama": "Ollama Models",
"custom": "Custom Models"

View File

@@ -3,6 +3,7 @@
"selectAPrompt": "Select a Prompt",
"githubRepository": "GitHub Repository",
"settings": "Settings",
"metering": "Metering",
"sidebarTitle": "Chat History",
"error": "Error",
"somethingWentWrong": "Something went wrong",

View File

@@ -20,6 +20,7 @@
},
"tooltip": {
"searchInternet": "Search Internet",
"searchIod": "Search Internet of Data",
"speechToText": "Speech to Text",
"uploadImage": "Upload Image",
"stopStreaming": "Stop Streaming",

View File

@@ -105,7 +105,7 @@
"rephrase": "Reformular",
"translate": "Traducir"
},
"citations": "Citas",
"webCitations": "Citas",
"downloadCode": "Descargar Código",
"date": {
"pinned": "Fijado",

View File

@@ -99,7 +99,7 @@
},
"advanced": "تنظیمات بیشتر مدل"
},
"citations": "منابع",
"webCitations": "منابع",
"downloadCode": "دانلود کد",
"date": {
"pinned": "پین شده",

View File

@@ -105,7 +105,7 @@
"rephrase": "Reformuler",
"translate": "Traduire"
},
"citations": "Citations",
"webCitations": "Citations",
"downloadCode": "Télécharger le code",
"date": {
"pinned": "Épinglé",

View File

@@ -105,7 +105,7 @@
"rephrase": "Riformulare",
"translate": "Tradurre"
},
"citations": "Citazioni",
"webCitations": "Citazioni",
"downloadCode": "Scarica Codice",
"date": {
"pinned": "Fissato",

View File

@@ -105,8 +105,7 @@
"rephrase": "言い換え",
"translate": "翻訳"
},
"citations": "万维网引用",
"iodcitations":"数联网引用",
"webCitations": "引用",
"downloadCode": "コードをダウンロード",
"date": {
"pinned": "固定",

View File

@@ -105,7 +105,7 @@
"rephrase": "다르게 표현",
"translate": "번역"
},
"citations": "인용",
"webCitations": "인용",
"downloadCode": "코드 다운로드",
"date": {
"pinned": "고정됨",

View File

@@ -104,7 +104,7 @@
"rephrase": "പുനഃരൂപീകരിക്കുക",
"translate": "വിവർത്തനം ചെയ്യുക"
},
"citations": "ഉദ്ധരണികൾ",
"webCitations": "ഉദ്ധരണികൾ",
"downloadCode": "കോഡ് ഡൗൺലോഡ് ചെയ്യുക",
"date": {
"pinned": "പിൻ ചെയ്തത്",

View File

@@ -106,7 +106,7 @@
"translate": "Oversett",
"custom": "Egendefinert"
},
"citations": "Sitater",
"webCitations": "Sitater",
"downloadCode": "Last ned kode",
"date": {
"pinned": "Festet",

View File

@@ -105,7 +105,7 @@
"rephrase": "Reformular",
"translate": "Traduzir"
},
"citations": "Citações",
"webCitations": "Citações",
"downloadCode": "Baixar Código",
"date": {
"pinned": "Fixado",

View File

@@ -105,7 +105,7 @@
"rephrase": "Перефразировать",
"translate": "Перевести"
},
"citations": "Цитаты",
"webCitations": "Цитаты",
"downloadCode": "Скачать код",
"date": {
"pinned": "Закреплено",

View File

@@ -106,7 +106,7 @@
"translate": "Översätt",
"custom": "Custom"
},
"citations": "Citat",
"webCitations": "Citat",
"segmented": {
"ollama": "Ollama-modeller",
"custom": "Custom modeller"

View File

@@ -106,7 +106,7 @@
"translate": "Перекласти",
"custom": "Власне"
},
"citations": "Цитати",
"webCitations": "Цитати",
"segmented": {
"ollama": "Моделі Ollama",
"custom": "Власні моделі"

View File

@@ -1,5 +1,5 @@
{
"pageAssist": "Page Assist",
"pageAssist": "IoD Bot",
"selectAModel": "选择一个模型",
"save": "保存",
"saved": "已保存",
@@ -38,7 +38,8 @@
}
},
"copyToClipboard": "复制到剪贴板",
"webSearch": "搜索网络",
"webSearch": "搜索中...",
"iodSearch": "搜索数联网",
"regenerate": "重新生成",
"edit": "编辑",
"delete": "删除",
@@ -105,8 +106,8 @@
"rephrase": "重述",
"translate": "翻译"
},
"citations": "万维网引用",
"iodcitations":"数联网引用",
"webCitations": "万维网引用",
"iodCitations": "数联网引用",
"downloadCode": "下载代码",
"date": {
"pinned": "已置顶",

View File

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

View File

@@ -19,7 +19,8 @@
}
},
"tooltip": {
"searchInternet": "搜索互联网",
"searchInternet": "深度搜索",
"searchIod": "搜索数联网",
"speechToText": "语音到文本",
"uploadImage": "上传图片",
"stopStreaming": "停止流媒体",

View 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>
)
};

View File

@@ -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

View 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.02018-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.1528414774204895456DT2023年地质勘探补充调查",
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>
)
}

View 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>
)
}

View 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>
)
}

View File

@@ -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"
@@ -18,7 +23,7 @@ import { useTTS } from "@/hooks/useTTS"
import { tagColors } from "@/utils/color"
import { removeModelSuffix } from "@/db/models"
import { GenerationInfo } from "./GenerationInfo"
import { parseReasoning, } from "@/libs/reasoning"
import { parseReasoning } from "@/libs/reasoning"
import { humanizeMilliseconds } from "@/utils/humanize-milliseconds"
type Props = {
message: string
@@ -36,8 +41,8 @@ type Props = {
isProcessing: boolean
webSearch?: {}
isSearchingInternet?: boolean
sources?: any[]
iodSources?:any[]
webSources?: any[]
iodSources?: any[]
hideEditAndRegenerate?: boolean
onSourceClick?: (source: any) => void
isTTSEnabled?: boolean
@@ -49,17 +54,18 @@ type Props = {
export const PlaygroundMessage = (props: Props) => {
const [isBtnPressed, setIsBtnPressed] = React.useState(false)
const [editMode, setEditMode] = React.useState(false)
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
)
@@ -72,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 &&
@@ -93,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 ? (
<>
@@ -132,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>
)
) : (
@@ -166,6 +166,34 @@ export const PlaygroundMessage = (props: Props) => {
</div>
)}
{props.isBot && props?.webSources && props?.webSources.length > 0 && (
<Collapse
className="mt-6"
ghost
items={[
{
key: "1",
label: (
<div className="italic text-gray-500 dark:text-gray-400">
{t("webCitations")}
</div>
),
children: (
<div className="mb-3 flex flex-wrap gap-2">
{props?.webSources?.map((source, index) => (
<MessageSource
onSourceClick={props.onSourceClick}
key={index}
index={index}
source={source}
/>
))}
</div>
)
}
]}
/>
)}
{props.isBot && props?.iodSources && props?.iodSources.length > 0 && (
<Collapse
className="mt-6"
@@ -175,45 +203,17 @@ export const PlaygroundMessage = (props: Props) => {
key: "1",
label: (
<div className="italic text-gray-500 dark:text-gray-400">
{t("iodcitations")}
{t("iodCitations")}
</div>
),
children: (
<div className="block">
<div className="mb-3 flex flex-wrap gap-2">
{props?.iodSources?.map((source, index) => (
<MessageSource
onSourceClick={props.onSourceClick}
key={index}
index={index}
source={source}
index = {index}
/>
))}
</div>
)
}
]}
/>
)}
{props.isBot && props?.sources && props?.sources.length > 0 && (
<Collapse
className="mt-6"
ghost
items={[
{
key: "1",
label: (
<div className="italic text-gray-500 dark:text-gray-400">
{t("citations")}
</div>
),
children: (
<div className="block">
{props?.sources?.map((source, index) => (
<MessageSource
onSourceClick={props.onSourceClick}
key={index}
source={source}
index = {index}
/>
))}
</div>
@@ -315,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

View File

@@ -1,6 +1,9 @@
import { useState } from "react"
import type React from "react"
import { KnowledgeIcon } from "@/components/Option/Knowledge/KnowledgeIcon"
type Props = {
index: number
source: {
name?: string
url?: string
@@ -8,42 +11,72 @@ type Props = {
type?: string
pageContent?: string
content?: string
doId?: string
description?: string
}
key: number
onSourceClick?: (source: any) => void
index: number
}
export const MessageSource: React.FC<Props> = ({ source, key, onSourceClick, index}) => {
export const MessageSource: React.FC<Props> = ({
index,
source,
onSourceClick
}) => {
// Add state for tracking and content visibility
const [showContent, setShowContent] = useState(false)
if (source?.mode === "rag" || source?.mode === "chat") {
return (
<div className="block items-center gap-1 text-xs text-gray-800 dark:text-gray-100 mb-1">
<span className="text-xs font-medium">[{index + 1}]</span> {/* 显示序号 */}
<button
onClick={() => {
onSourceClick && onSourceClick(source)
}}
className="inline-flex gap-2 cursor-pointer transition-shadow duration-300 ease-in-out hover:shadow-lg items-center rounded-md bg-gray-100 p-1 text-xs text-gray-800 border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-100 opacity-80 hover:opacity-100">
<KnowledgeIcon type={source.type} className="h-3 w-3" />
<span className="text-xs">{source.name}</span>
<a
href={source?.url}
target="_blank"
className="text-xs text-blue-500 hover:underline"
onClick={(e) => {
e.preventDefault(); // 阻止默认的链接行为
onSourceClick && onSourceClick(source); // 调用自定义点击事件
}}
>
{source.url}
</a>
</div>
);
</button>
)
}
const onContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation
setShowContent(true)
}
return (
<div className="block items-center gap-1 text-xs text-gray-800 dark:text-gray-100 mb-1">
<span className="text-xs font-medium">[{index + 1}]</span> {/* 显示序号 */}
<span className="text-xs font-medium"></span>{" "}
<a
href={source?.url}
target="_blank"
className="text-xs text-blue-500 hover:underline"
>
{source.name}
onContextMenu={onContextMenu}
className="inline-block cursor-pointer transition-shadow duration-300 ease-in-out hover:shadow-lg items-center rounded-md bg-gray-100 p-1 text-xs text-gray-800 border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-100 opacity-80 hover:opacity-100">
{source.doId ? (
<>
<span className="text-xs">
[{index + 1}] doid: {source.doId}
</span>
<br />
<span className="text-xs">{source.name}</span>
{showContent && (
<div className="mt-2 p-2 border-t border-gray-200 dark:border-gray-700">
{source.content || source.pageContent || source.description}
</div>
)}
</>
) : (
<>
<span className="text-xs">
[{index + 1}] {source.name}
</span>
{showContent && (
<div className="mt-2 p-2 border-t border-gray-200 dark:border-gray-700">
{source.content || source.pageContent}
</div>
)}
</>
)}
</a>
</div>
)

View 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>
)
}

View 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>
)
}

View File

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

View File

@@ -1,247 +1,66 @@
import { useStorage } from "@plasmohq/storage/hook"
import {
BrainCog,
ChevronLeft,
ChevronRight,
CogIcon,
ComputerIcon,
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>
</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>
)

View File

@@ -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}

View 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>
)
}

View File

@@ -0,0 +1,165 @@
import React, { useMemo } from "react"
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<MeteringEntry>["columns"] = [
{
title: '序号',
key: 'index',
width: 100,
render: (_text, _record, index) => index + 1, // 索引从0开始+1后从1显示
},
{
title: "问题",
dataIndex: "queryContent",
key: "queryContent"
},
{
title: "提示词全文",
dataIndex: "prompt",
key: "prompt",
ellipsis: {
showTitle: false
},
render: (prompt) => (
<Tooltip placement="topLeft" title={prompt}>
{prompt}
</Tooltip>
),
width: "10%"
},
{
title: "思维链",
key: "cot",
dataIndex: "cot",
ellipsis: {
showTitle: false
},
render: (responseContent) => (
<Tooltip placement="topLeft" title={responseContent}>
{responseContent}
</Tooltip>
),
width: "10%"
},
{
title: "回答",
dataIndex: "responseContent",
key: "responseContent",
ellipsis: {
showTitle: false
},
render: (responseContent) => (
<Tooltip placement="topLeft" title={responseContent}>
{responseContent}
</Tooltip>
),
width: "10%"
},
{
title: "关联数据个数",
dataIndex: "relatedDataCount",
key: "relatedDataCount"
},
{
title: "数联网token",
dataIndex: "iodTokenCount",
key: "iodTokenCount"
},
{
title: "大模型token",
key: "largeModelToken",
dataIndex: "largeModelToken",
render: (_, record) => {
return (
<div>{record.modelInputTokenCount + record.modelOutputTokenCount}</div>
)
}
},
{
title: "日期",
dataIndex: "date",
key: "date",
render: (date) => {
return <div>{formatDate(new Date(date))}</div>
}
},
{
title: "耗时",
key: "timeTaken",
dataIndex: "timeTaken"
},
{
title: "操作",
key: "action",
render: (_, record) => (
<Space size="middle">
{/* <a>Invite {record.name}</a> */}
<NavLink to={`/metering/list/${record.id}`}>
<a></a>
</NavLink>
</Space>
)
}
]
export const MeteringDetail = () => {
const { meteringEntries } = useStoreMessageOption()
const data = useMemo(
() => [
{
key: "对话数量",
value: meteringEntries.length
},
{
key: "数联网输入token数",
value: meteringEntries.reduce((acc, cur) => {
for (const item of cur.iodKeywords) {
acc += item.length
}
return acc
}, 0)
},
{
key: "数联网输出token数",
value: meteringEntries.reduce((acc, cur) => acc + cur.iodTokenCount, 0)
},
{
key: "大模型输入token数",
value: meteringEntries.reduce(
(acc, cur) => acc + cur.modelInputTokenCount,
0
)
},
{
key: "大模型输出token数",
value: meteringEntries.reduce(
(acc, cur) => acc + cur.modelOutputTokenCount,
0
)
}
],
[meteringEntries]
)
return (
<div className="p-4 pt-[4rem]">
<List
grid={{ gutter: 16, column: 5 }}
dataSource={data}
split={false}
renderItem={(item) => (
<List.Item>
<Card title={item.key}>{item.value}</Card>
</List.Item>
)}
/>
<Table<MeteringEntry> columns={columns} dataSource={meteringEntries} />
</div>
)
}

View File

@@ -0,0 +1,220 @@
import {
Card,
List,
Table,
Space,
TableProps,
Divider,
Typography,
Tooltip
} from "antd"
import { useParams } from "react-router-dom"
import { useStoreMessageOption } from "@/store/option.tsx"
import { useMemo } from "react"
interface DataType {
key: string
name: string
doId: number
data_space: string
content: string
tokenCount: number
}
const columns: TableProps<DataType>["columns"] = [
{
title: "序号",
key: "index",
width: 100,
render: (_text, _record, index) => index + 1 // 索引从0开始+1后从1显示
},
{
title: "标识",
dataIndex: "doId",
key: "doId",
width: 350
},
{
title: "提供方",
dataIndex: "data_space",
key: "data_space",
width: 250
},
{
title: "token数",
key: "tokenCount",
dataIndex: "tokenCount",
width: 120
},
{
title: "内容",
key: "content",
dataIndex: "content",
ellipsis: {
showTitle: false
},
render: (content) => (
<Tooltip placement="topLeft" title={content}>
{content}
</Tooltip>
)
}
]
export const ListDetail = () => {
const { meteringEntries } = useStoreMessageOption()
const { id } = useParams()
const record = useMemo(
() => meteringEntries.find((item) => item.id === id),
[meteringEntries]
)
const modelData = useMemo(
() => [
{
key: "数联网引用token总数",
value: record.iodTokenCount
},
{
key: "大模型输入token数",
value: record.modelInputTokenCount
},
{
key: "大模型输出token数",
value: record.modelOutputTokenCount
},
{
key: "模型",
value: record.model
}
],
[record]
)
const inputTokenData = useMemo(
() => [
{
key: "内容:",
value: record.queryContent
},
{
key: "token数量:",
value: record.queryContent.length
}
],
[record]
)
const keywordsData = useMemo(
() => [
{
key: "token数量:",
value: record.iodKeywords.reduce((acc, cur) => acc + cur.length, 0)
},
{
key: "内容:",
value: record.iodKeywords.join(", ")
}
],
[record]
)
const responseContent = useMemo(
() => [
{
key: "token数量:",
value: record.modelResponseContent.length
},
{
key: "内容:",
value: record.modelResponseContent
}
],
[record]
)
return (
<div className="p-[1rem] pt-[4rem]">
<List
grid={{ gutter: 16, column: 4 }}
dataSource={modelData}
renderItem={(item) => (
<List.Item>
<Card title={item.key}>{item.value}</Card>
</List.Item>
)}
style={{ marginBottom: "2rem" }}
/>
<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
header={<div></div>}
dataSource={inputTokenData}
renderItem={(item) => (
<List.Item style={{ justifyContent: "flex-start" }}>
<Typography.Paragraph
style={{ marginBottom: 0 }}
className="mr-1">
{item.key}
</Typography.Paragraph>
<Tooltip
placement="topLeft"
style={{ marginBottom: 0 }}
title={item.value}>
{item.value}
</Tooltip>
</List.Item>
)}
style={{ marginBottom: "1rem" }}
/>
</Space>
<Space direction="vertical" className="w-full" size={10}>
<Divider orientation="left">token详情</Divider>
<List
bordered
dataSource={keywordsData}
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}>
{item.value}
</Tooltip>
</List.Item>
)}
/>
<List
bordered
dataSource={responseContent}
header={<div></div>}
renderItem={(item) => (
<List.Item
style={{ justifyContent: "flex-start", alignItems: "center" }}>
<Typography.Text
className="mt-0 mr-1 w-20"
style={{ marginBottom: 0 }}>
{item.key}
</Typography.Text>
<Typography.Paragraph
style={{ marginBottom: 0 }}
ellipsis={{ tooltip: item.value, rows: 2, expandable: true }}>
{item.value}
</Typography.Paragraph>
</List.Item>
)}
/>
</Space>
</div>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)}
@@ -36,7 +36,7 @@ export const PlaygroundChat = () => {
onRengerate={regenerateLastMessage}
isProcessing={streaming}
isSearchingInternet={isSearchingInternet}
sources={message.sources}
webSources={message.webSources}
iodSources={message.iodSources}
onEditFormSubmit={(value, isSend) => {
editMessage(index, value, !message.isBot, isSend)
@@ -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}

View File

@@ -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>
)
}

View File

@@ -13,7 +13,7 @@ import { getVariable } from "@/utils/select-variable"
import { useTranslation } from "react-i18next"
import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect"
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"
import { PiGlobe } from "react-icons/pi"
import { PiGlobe, PiNetwork } from "react-icons/pi"
import { handleChatInputKeyDown } from "@/utils/key-down"
import { getIsSimpleInternetSearch } from "@/services/search"
@@ -34,6 +34,8 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
streaming: isSending,
webSearch,
setWebSearch,
iodSearch,
setIodSearch,
selectedQuickPrompt,
textareaRef,
setSelectedQuickPrompt,
@@ -126,7 +128,6 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
}
}, [transcript])
/*
React.useEffect(() => {
if (selectedQuickPrompt) {
const word = getVariable(selectedQuickPrompt)
@@ -143,7 +144,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
}
}
}, [selectedQuickPrompt])
*/
const queryClient = useQueryClient()
const { mutateAsync: sendMessage } = useMutation({
@@ -206,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
@@ -300,6 +301,38 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
{...form.getInputProps("message")}
/>
<div className="mt-2 flex justify-between items-center">
<div className="flex">
{!selectedKnowledge && (
<div>
<Tooltip title={t("tooltip.searchInternet")}>
<div className="inline-flex items-center gap-2">
<PiGlobe
className={`h-5 w-5 dark:text-gray-300 `}
/>
<Switch
value={webSearch}
onChange={(e) => setWebSearch(e)}
checkedChildren={t("form.webSearch.on")}
unCheckedChildren={t("form.webSearch.off")}
/>
</div>
</Tooltip>
<Tooltip title={t("tooltip.searchIod")} className="ml-3">
<div className="inline-flex items-center gap-2">
<PiNetwork
className={`h-5 w-5 dark:text-gray-300 `}
/>
<Switch
value={iodSearch}
onChange={(e) => setIodSearch(e)}
checkedChildren={t("form.webSearch.on")}
unCheckedChildren={t("form.webSearch.off")}
/>
</div>
</Tooltip>
</div>
)}
</div>
<div className="flex !justify-end gap-3">
{!selectedKnowledge && (
<Tooltip title={t("tooltip.uploadImage")}>
@@ -324,6 +357,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
if (isListening) {
stopSpeechRecognition()
} else {
console.log("开始语音识别,语言:", speechToTextLanguage);
resetTranscript()
startListening({
continuous: true,

View File

@@ -1,13 +0,0 @@
<Tooltip title={t("tooltip.searchInternet")}>
<div className="inline-flex items-center gap-2">
<PiGlobe
className={`h-5 w-5 dark:text-gray-300 `}
/>
<Switch
value={webSearch}
onChange={(e) => setWebSearch(e)}
checkedChildren={t("form.webSearch.on")}
unCheckedChildren={t("form.webSearch.off")}
/>
</div>
</Tooltip>

View File

@@ -24,6 +24,7 @@ import {
getLastUsedChatSystemPrompt,
lastUsedChatModelEnabled
} from "@/services/model-settings"
import { useState } from "react"
type Props = {
onClose: () => void
@@ -32,7 +33,6 @@ type Props = {
setHistoryId: (historyId: string) => void
setSelectedModel: (model: string) => void
setSelectedSystemPrompt: (prompt: string) => void
setSelectedQuickPrompt: (prompt: string | undefined) => void
setSystemPrompt: (prompt: string) => void
clearChat: () => void
temporaryChat: boolean
@@ -47,7 +47,6 @@ export const Sidebar = ({
setHistoryId,
setSelectedModel,
setSelectedSystemPrompt,
setSelectedQuickPrompt,
clearChat,
historyId,
setSystemPrompt,
@@ -171,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" />
@@ -267,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>

View File

@@ -39,7 +39,7 @@ export const SidePanelBody = () => {
message_type={message.messageType}
isProcessing={streaming}
isSearchingInternet={isSearchingInternet}
sources={message.sources}
webSources={message.webSources}
iodSources={message.iodSources}
onEditFormSubmit={(value) => {
editMessage(index, value, !message.isBot)

0
src/cs.js Normal file
View File

View File

@@ -29,8 +29,8 @@ type Message = {
role: string
content: string
images?: string[]
sources?: string[]
iodSources?:string[]
webSources?: string[]
iodSources?: string[]
search?: WebSearch
createdAt: number
reasoning_time_taken?: number
@@ -239,7 +239,7 @@ export const generateID = () => {
export const saveHistory = async (
title: string,
is_rag?: boolean,
message_source?: "copilot" | "web-ui",
message_source?: "copilot" | "web-ui"
) => {
const id = generateID()
const createdAt = Date.now()
@@ -255,8 +255,8 @@ export const saveMessage = async (
role: string,
content: string,
images: string[],
source?: any[],
iodSource?:any[],
webSources?: any[],
iodSources?: any[],
time?: number,
message_type?: string,
generationInfo?: any,
@@ -275,8 +275,8 @@ export const saveMessage = async (
content,
images,
createdAt,
sources: source,
iodSources:iodSource,
webSources,
iodSources,
messageType: message_type,
generationInfo: generationInfo,
reasoning_time_taken
@@ -306,7 +306,7 @@ export const formatToMessage = (messages: MessageHistory): MessageType[] => {
isBot: message.role === "assistant",
message: message.content,
name: message.name,
sources: message?.sources || [],
webSources: message?.webSources || [],
iodSources: message?.iodSources || [],
images: message.images || [],
generationInfo: message?.generationInfo,

View 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();
})
}

View File

@@ -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
}
})
}

View File

@@ -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" />

View File

@@ -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,
@@ -130,8 +130,8 @@ export const saveMessageOnSuccess = async ({
message,
image,
fullText,
source,
iodSource,
webSources,
iodSources,
message_source = "web-ui",
message_type, generationInfo,
prompt_id,
@@ -145,8 +145,8 @@ export const saveMessageOnSuccess = async ({
message: string
image: string
fullText: string
source: any[]
iodSource: any[]
webSources: any[]
iodSources: any[]
message_source?: "copilot" | "web-ui",
message_type?: string
generationInfo?: any
@@ -154,6 +154,7 @@ export const saveMessageOnSuccess = async ({
prompt_content?: string
reasoning_time_taken?: number
}) => {
var botMessage;
if (historyId) {
if (!isRegenerate) {
await saveMessage(
@@ -170,19 +171,20 @@ export const saveMessageOnSuccess = async ({
reasoning_time_taken
)
}
await saveMessage(
botMessage = await saveMessage(
historyId,
selectedModel!,
"assistant",
fullText,
[],
source,
iodSource,
webSources,
iodSources,
2,
message_type,
generationInfo,
reasoning_time_taken
)
updateDialog(historyId, botMessage)
await setLastUsedChatModel(historyId, selectedModel!)
if (prompt_id || prompt_content) {
await setLastUsedChatSystemPrompt(historyId, { prompt_content, prompt_id })
@@ -203,19 +205,20 @@ export const saveMessageOnSuccess = async ({
generationInfo,
reasoning_time_taken
)
await saveMessage(
botMessage = await saveMessage(
newHistoryId.id,
selectedModel!,
"assistant",
fullText,
[],
source,
iodSource,
webSources,
iodSources,
2,
message_type,
generationInfo,
reasoning_time_taken
)
updateDialog(newHistoryId.id, botMessage)
setHistoryId(newHistoryId.id)
await setLastUsedChatModel(newHistoryId.id, selectedModel!)
if (prompt_id || prompt_content) {

View File

@@ -59,6 +59,8 @@ export const useMessage = () => {
setIsSearchingInternet,
webSearch,
setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet
} = useStoreMessageOption()
const [defaultInternetSearchOn] = useStorage("defaultInternetSearchOn", false)
@@ -185,16 +187,16 @@ export const useMessage = () => {
isBot: false,
name: "You",
message,
sources: [],
iodSources:[],
webSources: [],
iodSources: [],
images: []
},
{
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
iodSources:[],
webSources: [],
iodSources: [],
id: generateMessageId
}
]
@@ -205,8 +207,8 @@ export const useMessage = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
iodSources:[],
webSources: [],
iodSources: [],
id: generateMessageId
}
]
@@ -337,7 +339,16 @@ export const useMessage = () => {
}
let context: string = ""
let source: {
let webSources: {
name: any
type: any
mode: string
url: string
pageContent: string
metadata: Record<string, any>
}[] = []
// TODO: update type
let iodSources: {
name: any
type: any
mode: string
@@ -349,7 +360,7 @@ export const useMessage = () => {
if (chatWithWebsiteEmbedding) {
const docs = await vectorstore.similaritySearch(query, 4)
context = formatDocs(docs)
source = docs.map((doc) => {
webSources = docs.map((doc) => {
return {
...doc,
name: doc?.metadata?.source || "untitled",
@@ -368,7 +379,7 @@ export const useMessage = () => {
.slice(0, maxWebsiteContext)
}
source = [
webSources = [
{
name: embedURL,
type: type,
@@ -479,7 +490,8 @@ export const useMessage = () => {
return {
...message,
message: fullText,
sources: source,
webSources,
iodSources,
generationInfo,
reasoning_time_taken: timetaken
}
@@ -500,7 +512,7 @@ export const useMessage = () => {
content: fullText
}
])
const iodSource = []
await saveMessageOnSuccess({
historyId,
setHistoryId,
@@ -509,8 +521,8 @@ export const useMessage = () => {
message,
image,
fullText,
source,
iodSource,
webSources,
iodSources,
message_source: "copilot",
generationInfo,
reasoning_time_taken: timetaken
@@ -610,15 +622,15 @@ export const useMessage = () => {
isBot: false,
name: "You",
message,
sources: [],
iodSources:[],
webSources: [],
iodSources: [],
images: []
},
{
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -630,7 +642,7 @@ export const useMessage = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -794,8 +806,8 @@ export const useMessage = () => {
message,
image,
fullText,
source: [],
iodSource:[],
webSources: [],
iodSources: [],
message_source: "copilot",
generationInfo,
reasoning_time_taken: timetaken
@@ -899,7 +911,7 @@ export const useMessage = () => {
isBot: false,
name: "You",
message,
sources: [],
webSources: [],
iodSources: [],
images: [image]
},
@@ -907,7 +919,7 @@ export const useMessage = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -919,7 +931,7 @@ export const useMessage = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -1088,8 +1100,8 @@ export const useMessage = () => {
message,
image,
fullText,
source: [],
iodSource:[],
webSources: [],
iodSources: [],
message_source: "copilot",
generationInfo,
reasoning_time_taken: timetaken
@@ -1126,12 +1138,14 @@ export const useMessage = () => {
}
const searchChatMode = async (
webSearch: boolean,
iodSearch,
message: string,
image: string,
isRegenerate: boolean,
messages: Message[],
history: ChatHistory,
signal: AbortSignal
signal: AbortSignal,
) => {
const url = await getOllamaURL()
setStreaming(true)
@@ -1188,7 +1202,7 @@ export const useMessage = () => {
isBot: false,
name: "You",
message,
sources: [],
webSources: [],
iodSources: [],
images: [image]
},
@@ -1196,7 +1210,7 @@ export const useMessage = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -1208,7 +1222,7 @@ export const useMessage = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -1286,10 +1300,10 @@ export const useMessage = () => {
query = removeReasoning(query)
}
const { prompt, source, iodSource } = await getSystemPromptForWeb(query, selectedQuickPrompt)
const { prompt, webSources, iodSources } =
await getSystemPromptForWeb(query, [], webSearch, iodSearch)
setIsSearchingInternet(false)
console.log("iodSource:")
console.log(iodSource)
// message = message.trim().replaceAll("\n", " ")
let humanMessage = await humanMessageFormatter({
@@ -1410,8 +1424,8 @@ export const useMessage = () => {
return {
...message,
message: fullText,
sources: source,
iodSources: iodSource,
webSources,
iodSources,
generationInfo,
reasoning_time_taken: timetaken
}
@@ -1441,8 +1455,8 @@ export const useMessage = () => {
message,
image,
fullText,
source,
iodSource,
webSources,
iodSources,
generationInfo,
reasoning_time_taken: timetaken
})
@@ -1541,7 +1555,7 @@ export const useMessage = () => {
isBot: false,
name: "You",
message,
sources: [],
webSources: [],
iodSources: [],
images: [image],
messageType: messageType
@@ -1550,7 +1564,7 @@ export const useMessage = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -1562,7 +1576,7 @@ export const useMessage = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -1709,8 +1723,8 @@ export const useMessage = () => {
message,
image,
fullText,
source: [],
iodSource:[],
webSources: [],
iodSources: [],
message_source: "copilot",
message_type: messageType,
generationInfo,
@@ -1788,14 +1802,16 @@ export const useMessage = () => {
)
} else {
if (chatMode === "normal") {
if (webSearch) {
if (webSearch || iodSearch) {
await searchChatMode(
webSearch,
iodSearch,
message,
image,
isRegenerate || false,
messages,
memory || history,
signal
signal,
)
} else {
await normalChatMode(
@@ -1928,6 +1944,8 @@ export const useMessage = () => {
regenerateLastMessage,
webSearch,
setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet,
selectedQuickPrompt,
setSelectedQuickPrompt,

View File

@@ -3,11 +3,12 @@ import { cleanUrl } from "~/libs/clean-url"
import {
defaultEmbeddingModelForRag,
geWebSearchFollowUpPrompt,
geWebSearchKeywordsPrompt,
getOllamaURL,
promptForRag,
systemPromptForNonRagOption
} from "~/services/ollama"
import { type ChatHistory, 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 {
@@ -20,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 {
@@ -37,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,
@@ -54,6 +58,9 @@ export const useMessageOption = () => {
const {
history,
setHistory,
meteringEntries,
setMeteringEntries,
setCurrentMeteringEntry,
setStreaming,
streaming,
setIsFirstMessage,
@@ -67,6 +74,8 @@ export const useMessageOption = () => {
setChatMode,
webSearch,
setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet,
setIsSearchingInternet,
selectedQuickPrompt,
@@ -110,7 +119,30 @@ export const useMessageOption = () => {
}
}
// 从最后的结果中解析出 思维链 (Chain-of-Thought) 和 结果
const responseResolver = (msg: string) => {
const cotStart = msg.indexOf("<think>")
const cotEnd = msg.indexOf("</think>")
let cot = ""
let content = ""
if (cotStart > -1 && cotEnd > -1) {
cot = msg.substring(cotStart + 7, cotEnd)
content = msg.substring(cotEnd + 8)
} else {
content = msg
}
// 去掉换行符
cot = cot.replace(/\n/g, "")
content = content.replace(/\n/g, "")
return {
cot: cot,
content
}
}
const searchChatMode = async (
webSearch: boolean,
iodSearch: boolean,
message: string,
image: string,
isRegenerate: boolean,
@@ -161,9 +193,18 @@ export const useMessageOption = () => {
useMlock:
currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock
})
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 = [
@@ -172,7 +213,7 @@ export const useMessageOption = () => {
isBot: false,
name: "You",
message,
sources: [],
webSources: [],
iodSources: [],
images: [image]
},
@@ -180,7 +221,7 @@ export const useMessageOption = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -192,7 +233,7 @@ export const useMessageOption = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -207,7 +248,8 @@ export const useMessageOption = () => {
setIsSearchingInternet(true)
let query = message
/*
let keywords: string[] = []
if (newMessage.length > 2) {
let questionPrompt = await geWebSearchFollowUpPrompt()
const lastTenMessages = newMessage.slice(-10)
@@ -270,17 +312,38 @@ export const useMessageOption = () => {
query = response.content.toString()
query = removeReasoning(query)
}
*/
const quickPrompt = selectedQuickPrompt;
console.log("quick prompt:"+quickPrompt)
const { prompt, source, iodSource } = await getSystemPromptForWeb(query, quickPrompt)
// 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)
let res = response.content.toString()
res = removeReasoning(res)
keywords = res
.replace(/^Keywords:/i, "")
.replace(/^关键词:/i, "")
.replace(/^/i, "")
.replace(/^:/i, "")
.replaceAll(" ", "")
.split(",")
.map((k) => k.trim())
*/
}
const { prompt, webSources, iodSources, iodSearchResults: iodData, iodTokenCount } =
await getSystemPromptForWeb(query, keywords, webSearch, iodSearch)
console.log("prompt:\n" + prompt)
setIsSearchingInternet(false)
console.log("iodSource from useMessageOption:")
console.log(iodSource)
console.log("prompt")
console.log(prompt)
console.log("query")
console.log(query)
meter.prompt = prompt
meter.iodKeywords = keywords
meter.iodData = iodData
meter.iodTokenCount = iodTokenCount
// message = message.trim().replaceAll("\n", " ")
let humanMessage = await humanMessageFormatter({
@@ -340,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
@@ -400,8 +464,8 @@ export const useMessageOption = () => {
return {
...message,
message: fullText,
sources: source,
iodSources:iodSource,
webSources,
iodSources,
generationInfo,
reasoning_time_taken: timetaken
}
@@ -431,14 +495,39 @@ export const useMessageOption = () => {
message,
image,
fullText,
source,
iodSource,
webSources,
iodSources,
generationInfo,
reasoning_time_taken: timetaken
})
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: 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,
@@ -556,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 = [
@@ -564,7 +663,7 @@ export const useMessageOption = () => {
isBot: false,
name: "You",
message,
sources: [],
webSources: [],
iodSources: [],
images: [image]
},
@@ -572,7 +671,7 @@ export const useMessageOption = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -584,7 +683,7 @@ export const useMessageOption = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -682,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) {
@@ -772,7 +872,6 @@ export const useMessageOption = () => {
image,
fullText,
source: [],
iodSource:[],
generationInfo,
prompt_content: promptContent,
prompt_id: promptId,
@@ -783,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,
@@ -871,7 +995,7 @@ export const useMessageOption = () => {
isBot: false,
name: "You",
message,
sources: [],
webSources: [],
iodSources: [],
images: []
},
@@ -879,7 +1003,7 @@ export const useMessageOption = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -891,7 +1015,7 @@ export const useMessageOption = () => {
isBot: true,
name: selectedModel,
message: "▋",
sources: [],
webSources: [],
iodSources: [],
id: generateMessageId
}
@@ -998,8 +1122,7 @@ export const useMessageOption = () => {
}
})
// message = message.trim().replaceAll("\n", " ")
const iodSource = []
//TODO not support iodSource in RAG
let humanMessage = await humanMessageFormatter({
content: [
{
@@ -1096,8 +1219,7 @@ export const useMessageOption = () => {
return {
...message,
message: fullText,
sources: source,
iodSources: iodSource,
webSources: source,
generationInfo,
reasoning_time_taken: timetaken
}
@@ -1128,7 +1250,6 @@ export const useMessageOption = () => {
image,
fullText,
source,
iodSource,
generationInfo,
reasoning_time_taken: timetaken
})
@@ -1197,8 +1318,10 @@ export const useMessageOption = () => {
signal
)
} else {
if (webSearch) {
if (webSearch || iodSearch) {
await searchChatMode(
webSearch,
iodSearch,
message,
image,
isRegenerate,
@@ -1333,6 +1456,8 @@ export const useMessageOption = () => {
regenerateLastMessage,
webSearch,
setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet,
setIsSearchingInternet,
selectedQuickPrompt,

51
src/libs/playground.tsx Normal file
View 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(),
}))

View File

@@ -72,7 +72,7 @@ export const pageAssistModel = async ({
configuration: {
apiKey: providerInfo.apiKey || "temp",
baseURL: providerInfo.baseUrl || ""
}
},
}) as any
}
@@ -85,7 +85,7 @@ export const pageAssistModel = async ({
configuration: {
apiKey: providerInfo.apiKey || "temp",
baseURL: providerInfo.baseUrl || ""
}
},
}) as any
}
return new ChatOllama({

View File

@@ -1,6 +1,6 @@
{
"extName": {
"message": "Page Assist - 本地 AI 模型的 Web UI"
"message": "IoD Bot - 本地 AI 模型的 Web UI"
},
"extDescription": {
"message": "使用本地运行的 AI 模型来辅助您的网络浏览。"

View File

@@ -12,6 +12,8 @@ import SidepanelSettings from "./sidepanel-settings"
import OptionRagSettings from "./option-rag"
import OptionChrome from "./option-settings-chrome"
import OptionOpenAI from "./option-settings-openai"
import OptionMetering from "./option-metering"
import MeteringListDetail from "./metering-list-detail"
export const OptionRoutingChrome = () => {
return (
@@ -27,6 +29,8 @@ export const OptionRoutingChrome = () => {
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
<Route path="/settings/rag" element={<OptionRagSettings />} />
<Route path="/settings/about" element={<OptionAbout />} />
<Route path="/metering" element={<OptionMetering />} />
<Route path="/metering/list/:id" element={<MeteringListDetail />} />
</Routes>
)
}

View File

@@ -10,6 +10,8 @@ const OptionModal = lazy(() => import("./option-settings-model"))
const OptionPrompt = lazy(() => import("./option-settings-prompt"))
const OptionOllamaSettings = lazy(() => import("./options-settings-ollama"))
const OptionSettings = lazy(() => import("./option-settings"))
const OptionMetering = lazy(() => import("./option-metering"))
const MeteringListDetail = lazy(() => import("./metering-list-detail"))
const OptionShare = lazy(() => import("./option-settings-share"))
const OptionKnowledgeBase = lazy(() => import("./option-settings-knowledge"))
const OptionAbout = lazy(() => import("./option-settings-about"))
@@ -29,6 +31,8 @@ export const OptionRoutingFirefox = () => {
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
<Route path="/settings/about" element={<OptionAbout />} />
<Route path="/settings/rag" element={<OptionRagSettings />} />
<Route path="/metering" element={<OptionMetering />} />
<Route path="/metering/list/:id" element={<MeteringListDetail />} />
</Routes>
)
}

View File

@@ -0,0 +1,12 @@
import OptionLayout from "~/components/Layouts/Layout"
import { ListDetail } from "~/components/Option/Metering/listDetail"
const OptionSettings = () => {
return (
<OptionLayout>
<ListDetail />
</OptionLayout>
)
}
export default OptionSettings

View File

@@ -0,0 +1,12 @@
import OptionLayout from "~/components/Layouts/Layout"
import { MeteringDetail } from "~/components/Option/Metering/detail"
const OptionSettings = () => {
return (
<OptionLayout>
<MeteringDetail />
</OptionLayout>
)
}
export default OptionSettings

View File

@@ -21,15 +21,69 @@ 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_PROMP = `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}.
\`search-results\` block provides knowledge from the web search results. You can use this information to generate a meaningful response.
\`iod-search-results\` block provides knowledge from the Internet of Data (数联网) search results. Each search result has a format of:
\`<result doId="{doId}" name="{name}" url="{url}" id="{id}">{content}</result>\`
Please show the \`doId\` and \`name\` of the search result when you cite the Internet of Data search result, 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)\`
<search-results>
{search_results}
</search-results>
\`web-search-results\` block provides knowledge from the World Wide Web (万维网) search results.
Please show the \`doId\` and \`name\` of the search result when you cite the search result, in the following format, in English:
\`[3W source [id] "{name}"]({url})\`
Or in Chinese:
\`[万维网引用[id] "{name}"]({url})\`
For example, in English:
\`[3W source [1] On the insufficiency of existing momentum schemes for Stochastic Optimization](http://arxiv.org/pdf/1803.05591v2.pdf)\`
Or in Chinese:
\`[万维网引用[1] On the insufficiency of existing momentum schemes for Stochastic Optimization](http://arxiv.org/pdf/1803.05591v2.pdf)\`
You can use these information to generate a meaningful response.
<iod-search-results>
{iod_search_results}
</iod-search-results>
<web-search-results>
{web_search_results}
</web-search-results>
`
const DEFAULT_WEBSEARCH_FOLLOWUP_PROMPT = `You will give a follow-up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the AI model to search the internet.
@@ -58,6 +112,31 @@ Follow-up question: {question}
Rephrased question:
`
const DEFAULT_WEBSEARCH_KEYWORDS_PROMPT = `Extract the most important keywords from the query (at most 3), and give me English and Chinese versions of the keywords.
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?
你的输出: symptoms, 症状, heart attack, 心臟病
Query: 什么是物联网?
你的输出: Internet of Things, IoT, 物联网
Query: 人工智能的发展趋势?
你的输出: Artificial Intelligence, AI, 人工智能, trend, 趋势
接下来,开始你的关键词提取吧。
Query: {query}
`
export const getOllamaURL = async () => {
const ollamaURL = await storage.get("ollamaURL")
if (!ollamaURL || ollamaURL.length === 0) {
@@ -385,7 +464,7 @@ export const saveForRag = async (
export const getWebSearchPrompt = async () => {
const prompt = await storage.get("webSearchPrompt")
if (!prompt || prompt.length === 0) {
return DEFAULT_WEBSEARCH_PROMP
return DEFAULT_WEBSEARCH_PROMPT
}
return prompt
}
@@ -411,6 +490,18 @@ export const setWebPrompts = async (prompt: string, followUpPrompt: string) => {
await setWebSearchFollowUpPrompt(followUpPrompt)
}
export const geWebSearchKeywordsPrompt = async () => {
const prompt = await storage.get("webSearchKeywordsPrompt")
if (!prompt || prompt.length === 0) {
return DEFAULT_WEBSEARCH_KEYWORDS_PROMPT
}
return prompt
}
export const setWebSearchKeywordsPrompt = async (prompt: string) => {
await storage.set("webSearchKeywordsPrompt", prompt)
}
export const getPageShareUrl = async () => {
const pageShareUrl = await storage.get("pageShareUrl")
if (!pageShareUrl || pageShareUrl.length === 0) {

View File

@@ -14,7 +14,7 @@ export type Message = {
isBot: boolean
name: string
message: string
sources: any[]
webSources: any[]
iodSources: any[]
images?: string[]
search?: WebSearch
@@ -26,7 +26,7 @@ export type Message = {
export type ChatHistory = {
role: "user" | "assistant" | "system"
content: string
image?: string,
image?: string
messageType?: string
}[]
@@ -35,6 +35,10 @@ type State = {
setMessages: (messages: Message[]) => void
history: ChatHistory
setHistory: (history: ChatHistory) => 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
@@ -53,6 +57,8 @@ type State = {
setIsEmbedding: (isEmbedding: boolean) => void
webSearch: boolean
setWebSearch: (webSearch: boolean) => void
iodSearch: boolean
setIodSearch: (iodSearch: boolean) => void
isSearchingInternet: boolean
setIsSearchingInternet: (isSearchingInternet: boolean) => void
@@ -75,11 +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 }),
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,
@@ -101,6 +147,8 @@ export const useStoreMessageOption = create<State>((set) => ({
setIsEmbedding: (isEmbedding) => set({ isEmbedding }),
webSearch: false,
setWebSearch: (webSearch) => set({ webSearch }),
iodSearch: false,
setIodSearch: (iodSearch) => set({ iodSearch }),
isSearchingInternet: false,
setIsSearchingInternet: (isSearchingInternet) => set({ isSearchingInternet }),
selectedSystemPrompt: null,
@@ -116,5 +164,5 @@ export const useStoreMessageOption = create<State>((set) => ({
setTemporaryChat: (temporaryChat) => set({ temporaryChat }),
useOCR: false,
setUseOCR: (useOCR) => set({ useOCR }),
setUseOCR: (useOCR) => set({ useOCR })
}))

11
src/types/iod.ts Normal file
View File

@@ -0,0 +1,11 @@
export type IodRegistryEntry = {
doId: string
name: string
url?: string
pdf_url?: string
description: string
content?: string
data_space?: string
data_type?:string
traceId?:string
}

View File

@@ -11,7 +11,7 @@ export type Message = {
isBot: boolean
name: string
message: string
sources: any[]
webSources: any[]
iodSources: any[]
images?: string[]
search?: WebSearch

8
src/types/segmentit.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import { Segment, useDefault, cnPOSTag, enPOSTag } from 'segmentit';
declare module 'segmentit' {
export = Segment;
export = useDefault;
export = cnPOSTag;
export = enPOSTag;
}

5
src/types/web.ts Normal file
View File

@@ -0,0 +1,5 @@
export type WebSearchResult = {
url: string
name: string
content: string
}

22
src/utils/date.ts Normal file
View File

@@ -0,0 +1,22 @@
export function formatDate(date) {
// 获取年份
const year = date.getFullYear()
// 获取月份注意月份是从0开始计数的所以需要加1并且确保月份是两位数
const month = String(date.getMonth() + 1).padStart(2, "0")
// 获取日期,确保日期是两位数
const day = String(date.getDate()).padStart(2, "0")
// 获取小时24小时制并确保小时是两位数
const hours = String(date.getHours()).padStart(2, "0")
// 获取分钟,并确保分钟是两位数
const minutes = String(date.getMinutes()).padStart(2, "0")
// 组合成所需的格式
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 示例使用
const now = new Date()

21
src/web/1.json Normal file
View 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
View 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
})

404
src/web/iod.ts Normal file
View File

@@ -0,0 +1,404 @@
import { cleanUrl } from "@/libs/clean-url"
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 { Document } from "@langchain/core/documents"
import { MemoryVectorStore } from "langchain/vectorstores/memory"
import type { IodRegistryEntry } from "~/types/iod"
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: 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(iodConfig.doBrowser, {
method: "POST",
body: JSON.stringify(params),
signal: abortController.signal
})
.then((response) => response.json())
.then((res) => {
if (res.status !== "Success") {
console.log(res)
return []
}
const body = JSON.parse(res.result.body)
if (body.code !== 0) {
console.log(body)
return []
}
const results: IodRegistryEntry[] = body.data?.results || []
for (const r of results) {
r.url = r.url || r.pdf_url
}
for (const r of results) {
r.doId = r.doId || r.doid
}
return results
})
.catch((e) => {
console.log(e)
return []
})
})
)
).flat()
// results 根据 doId 去重
const map = new Map<string, IodRegistryEntry>()
for (const r of results) {
map.set(r.doId, r)
}
return Array.from(map.values())
*/
}
const ARXIV_URL_PATTERN = /^https?:\/\/arxiv\.org\//
const ARXIV_NO_HTM = "No HTML for"
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
}
const docs: Document<Record<string, any>>[] = []
const resMap = new Map<string, IodRegistryEntry>()
for (const result of searchResults) {
const url = result.url
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)) {
htmlUrl = url.replace("/pdf/", "/html/").replace(".pdf", "")
}
let noHtml = htmlUrl === ""
if (!noHtml) {
const loader = new PageAssistHtmlLoader({
html: "",
url: htmlUrl
})
try {
const documents = await loader.loadByURL()
for (const doc of documents) {
if (doc.pageContent.includes(ARXIV_NO_HTM)) {
noHtml = true
return
}
docs.push(doc)
}
} catch (e) {
console.log(e)
noHtml = true
}
}
if (noHtml) {
if (url.endsWith(".pdf")) {
const loader = new PageAssistPDFUrlLoader({
name: result.name,
url
})
try {
const documents = await loader.load()
for (const doc of documents) {
docs.push(doc)
}
} catch (e) {
console.log(e)
}
} else {
const loader = new PageAssistHtmlLoader({
html: "",
url
})
try {
const documents = await loader.loadByURL()
for (const doc of documents) {
docs.push(doc)
}
} catch (e) {
console.log(e)
}
}
}
}
return searchResults
/*
const ollamaUrl = await getOllamaURL()
const embeddingModle = await defaultEmbeddingModelForRag()
const ollamaEmbedding = await pageAssistEmbeddingModel({
model: embeddingModle || "",
baseUrl: cleanUrl(ollamaUrl)
})
const textSplitter = await getPageAssistTextSplitter()
const chunks = await textSplitter.splitDocuments(docs)
const store = new MemoryVectorStore(ollamaEmbedding)
await store.addDocuments(chunks)
const resultsWithEmbeddings = await store.similaritySearch(query, 3)
const searchResult = resultsWithEmbeddings.map((result) => {
// `source` for PDF type
const key = result.metadata.url || result.metadata.source
if (!key) return null
const fullRes = resMap[key]
return {
...fullRes,
content: result.pageContent
}
}).filter((r) => r)
return searchResult
*/
}
export const calculateTokenCount = function(str:string){
const byteArray = new TextEncoder().encode(str);
return byteArray.length;
}

View File

@@ -8,7 +8,10 @@ import { getWebsiteFromQuery, processSingleWebsite } from "./website"
import { searxngSearch } from "./search-engines/searxng"
import { braveAPISearch } from "./search-engines/brave-api"
import { webBaiduSearch } from "./search-engines/baidu"
import { LucideToggleRight } from "lucide-react"
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 {
@@ -19,110 +22,141 @@ const getHostName = (url: string) => {
}
}
const searchWeb = (provider: string, query: string) => {
async function searchWeb(
provider: string,
query: string
): Promise<WebSearchResult[]> {
let results = []
switch (provider) {
case "duckduckgo":
return webDuckDuckGoSearch(query)
results = await webDuckDuckGoSearch(query)
break
case "sogou":
return webSogouSearch(query)
results = await webSogouSearch(query)
break
case "brave":
return webBraveSearch(query)
results = await webBraveSearch(query)
break
case "searxng":
return searxngSearch(query)
results = await searxngSearch(query)
break
case "brave-api":
return braveAPISearch(query)
results = await braveAPISearch(query)
break
case "baidu":
return webBaiduSearch(query)
results = await webBaiduSearch(query)
break
default:
return webGoogleSearch(query)
results = await webGoogleSearch(query)
break
}
return results.map((r) => ({ ...r, name: getHostName(r.url) }))
}
export const getSystemPromptForWeb = async (query: string, promptMode) => {
export const getSystemPromptForWeb = async (
query: string,
keywords: string[] = [],
webSearch = true,
iodSearch = false
) => {
try {
if (!promptMode){
return {
prompt: "",
source: [],
iodSource:[]
}
}
let iodsearch = []
if (promptMode.indexOf("iod_search_results")!=-1){
iodsearch = [
{
url:"http://bdware.cn/resolve?id=CSTR:432421111.1233.53323",
content:"数联网Internet Of Data):数据作为互联网上可独立管理的资源,在“物理/机器”互联网之上形成一个“虚拟/数据”网络,实现全网一体化的数据互联互通互操作。",
id:"CSTR:432421111.1233.53323,数联网定义"
}, {
url:"http://bdware.cn/resolve?id=CSTR:1121311.3423.7754",
content:"数据空间:面向具体的领域和业务场景,按照数据所对应的物理实体的结构、关系来对数据进行管理和组织,构成物理世界的数字孪生。",
id:"CSTR:1121311.3423.7754,数据空间定义"
}
]
}
const websiteVisit = getWebsiteFromQuery(query)
let search: {
url: any;
content: string;
}[] = []
let webSearchResults: WebSearchResult[] = []
// let search_results_web = ""
const isVisitSpecificWebsite = await getIsVisitSpecificWebsite()
if (isVisitSpecificWebsite && websiteVisit.hasUrl) {
const url = websiteVisit.url
const queryWithoutUrl = websiteVisit.queryWithouUrls
search = await processSingleWebsite(url, queryWithoutUrl)
} else if (promptMode.indexOf("web_search_results")!=-1) {
const searchProvider = await getSearchProvider()
search = await searchWeb(searchProvider, query)
if (webSearch) {
const isVisitSpecificWebsite = await getIsVisitSpecificWebsite()
if (isVisitSpecificWebsite && websiteVisit.hasUrl) {
const url = websiteVisit.url
const queryWithoutUrl = websiteVisit.queryWithouUrls
webSearchResults = await processSingleWebsite(url, queryWithoutUrl)
} else {
const searchProvider = await getSearchProvider()
webSearchResults = await searchWeb(searchProvider, query)
}
// search_results_web = webSearchResults
// .map(
// (result, idx) =>
// `<result source="${result.url}" id="${idx}">${result.content}</result>`
// )
// .join("\n")
}
const search_results = search
let iodSearchResults: IodRegistryEntry[] = []
// let search_results_iod = ""
if (iodSearch) {
iodSearchResults = await searchIod(query, keywords)
// search_results_iod = iodSearchResults
// .map(
// (result, idx) =>
// `<result source="${result.url}" id="${idx}">${result.content}</result>`
// )
// .join("\n")
}
const _iodSearchResults = iodSearchResults
.map((res) => ({
doId: res.doId,
name: res.name,
url: res.url,
data_type: res.data_type,
data_space: res.data_space,
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) =>{
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")
const web_search_results = webSearchResults
.map(
(result, idx) =>
`<result source="${result.url}" id="${idx+1}">${result.content}</result>`
`<result source="${result.url}" name="${result.name}" id="${idx + 1}">${result.content}</result>`
)
.join("\n")
const current_date_time = new Date().toLocaleString()
const system = promptMode
const iod_search_results= iodsearch.map(
(result, idx) =>
`<result source="${result.url}" id="${idx+1}">${result.content}</result>`
)
.join("\n")
console.log("iod_search_xml in web.ts")
console.log(iod_search_results)
const system = await getWebSearchPrompt()
const prompt = system
.replace("{current_date_time}", current_date_time)
.replace("{web_search_results}", search_results)
.replace("{iod_search_results}",iod_search_results)
.replace("{iod_search_results}", iod_search_results)
.replace("{web_search_results}", web_search_results)
return {
prompt,
source: search.map((result) => {
webSources: webSearchResults.map((result) => {
return {
url: result.url,
name: getHostName(result.url),
name: result.name,
type: "url"
}
}),
iodSource: iodsearch.map((result) => {
return {
url: result.url,
name: result.id,
type: "url"
}
})
iodSources: iodSearchResults,
iodSearchResults: _iodSearchResults,
iodTokenCount: _iodSearchResults.reduce((acc, cur) => (acc + cur.content.length), 0)
}
} catch (e) {
console.error(e)
return {
prompt: "",
source: [],
iodSource:[]
webSources: [],
iodSources: [],
iodSearchResults: [],
iodTokenCount: 0,
}
}
}

View File

@@ -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)',

View File

@@ -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",