Compare commits

..

59 Commits
chq ... main

Author SHA1 Message Date
CaiHQ
629fd0d586 fix: react dom version 2025-08-26 21:42:47 +08:00
CaiHQ
9adffd981f ignore video blob 2025-08-26 19:21:42 +08:00
zhaoweijie
dd0e01a114 Merge branch 'main' of gitea.internetapi.cn:iod/page-assist 2025-08-26 19:15:56 +08:00
zhaoweijie
9c0da9915c feat: delete video 2025-08-26 19:15:49 +08:00
zhaoweijie
26837559a5 refactor(components): 重构 VideoPlayer 组件
- 使用 createPortal将控制栏渲染到 document.body- 隐藏视频默认控件
- 更新 react-dom 依赖版本
2025-08-26 19:13:56 +08:00
CaiHQ
74ba466141 update prompt 2025-08-26 09:41:03 +08:00
zhaoweijie
d3a0b05910 feat(VideoPlayer): 隐藏 logo 功能
- 添加 hideLogo 的 useMemo 计算,根据 localStorage 的值决定是否隐藏 logo
- 修改 logo 渲染逻辑,根据 hideLogo 的值确定是否显示- 更新视频资源路径,从 assets 改为 public
2025-08-26 09:15:27 +08:00
zhaoweijie
f6bd08da49 style(VideoPlayer): 调整控制栏样式
- 移除了 control bar 类名中的 `bottom-0`,以修复控制栏在隐藏时仍然占据空间的问题- 优化了 control bar 的显示和隐藏动画,使其更加流畅
2025-08-25 20:52:34 +08:00
zhaoweijie
901bc13526 refactor(layout): 重构布局组件并添加视频播放功能
-重写 Header 组件,使用新的 OptionLayoutContext 替代 HistoryContext
- 新增 VideoPlayer 组件,用于播放视频
- 更新 Playground 组件,集成新的侧边栏和视频播放功能
- 重构 Layout 组件,支持新的选项布局
- 更新相关路由和导出导入逻辑,以支持上述更改
2025-08-25 20:43:27 +08:00
zhaoweijie
c937694d8b Merge branch 'main' of gitea.internetapi.cn:iod/page-assist 2025-08-25 19:40:20 +08:00
zhaoweijie
635e792e22 refactor(layout): 重构布局组件并添加视频播放功能
-重写 Header 组件,使用新的 OptionLayoutContext 替代 HistoryContext
- 新增 VideoPlayer 组件,用于播放视频
- 更新 Playground 组件,集成新的侧边栏和视频播放功能
- 重构 Layout 组件,支持新的选项布局
- 更新相关路由和导出导入逻辑,以支持上述更改
2025-08-25 19:40:02 +08:00
CaiHQ
121dfabbd1 fix: fix first search failed bugs 2025-08-25 19:23:19 +08:00
CaiHQ
b47669f3e4 update playground 2025-08-25 15:36:13 +08:00
zhaoweijie
6f386709e2 Merge remote-tracking branch 'origin/main' 2025-08-24 19:00:58 +08:00
zhaoweijie
2b4885ae2d refactor(iod): 重构数联网相关组件和逻辑
-优化了 Data、Scene 和 Team组件的逻辑,使用 currentIodMessage 替代复杂的条件判断- 改进了 IodRelevant 组件的动画和数据处理方式
- 调整了 Message 组件以支持数联网搜索功能
- 重构了 PlaygroundIodProvider,简化了上下文类型和数据处理
- 更新了数据库相关操作,使用新的 HistoryMessage 类型
- 新增了 IodDb 类来管理数联网连接配置
2025-08-24 19:00:49 +08:00
CaiHQ
6bb9247c6c update playground 2025-08-24 13:53:31 +08:00
zhaoweijie
f9763778fa refactor(components): 重构碘搜索相关组件
- 优化 IodRelevant 组件中的加载状态和计数器动画
- 重构 PlaygroundIod 组件,使用新的 PlaygroundIodProvider 组件
- 改进 Context 提供的数据结构,使用更准确的命名
2025-08-24 13:04:43 +08:00
zhaoweijie
cb6c3c225b refactor(web): 优化 IOD 相关组件和 hooks
- 修改 IodRelevant 组件,根据搜索状态动态显示文本
- 更新 useMessage hook,添加调试断点
- 重构 iod.ts 中的 IoDSources 处理逻辑
2025-08-24 12:44:02 +08:00
CaiHQ
ecb90d9035 update search hint 2025-08-23 23:20:08 +08:00
CaiHQ
e21c52cb22 update data and team 2025-08-23 20:47:04 +08:00
zhaoweijie
a9d1f1a94f feat(i18n): 优化国际化文案并调整 UI 样式
- 修改系统重置文案为清除最近对话
- 优化历史记录列表样式,增加截断显示
- 调整卡片阴影样式
- 在头部增加收藏、分享和消息的 Tooltip 提示
2025-08-23 20:39:40 +08:00
zhaoweijie
50af75d347 refactor(components): 重构 Playground组件中的数据展示逻辑
-移除了 useEffect 钩用,改用函数式组件的按需渲染
- 优化了 Header组件的点击事件处理,提高代码复用性
- 统一了数据加载和展示的逻辑,提升组件可维护性
2025-08-23 20:22:01 +08:00
zhaoweijie
e0e41d7e21 feat(components): 新增图标组件并优化历史记录功能
- 新增 Bell、Collect 和 NotCollect 图标组件
- 优化 History 组件,添加隐藏 logo 功能
- 调整 Message 组件样式,移除不必要的代码
- 更新 Scene 组件 Header 颜色
- 注释掉 tailwind.css 中的 arimo 字体权重
2025-08-23 20:11:11 +08:00
zhaoweijie
e640b1254d refactor(components): 重构 playground组件
- 移除 Data、Scene 和 IodRelevant 组件中的 Drawer
- 优化 Data、Scene 和 IodRelevant 组件的结构
- 添加 Header 组件用于展示标题和关闭按钮
- 使用 Main 组件替代原来的 ShowCard 组件
- 调整样式和布局,提高组件的可复用性和可维护性
2025-08-23 17:03:14 +08:00
zhaoweijie
6fb71b01f0 feat(components): 新增 Drawer 组件并优化 Playground 页面
- 新增 Drawer 组件用于创建可滑动的抽屉式界面
-优化 Playground 页面布局和样式,增加 logo 和 frosted glass 效果
- 添加统计卡片组件和动画效果,提升用户体验
- 新增数据项目图标组件
2025-08-22 21:28:40 +08:00
CaiHQ
37a11fbb8b feat: update faqs 2025-08-22 18:31:19 +08:00
CaiHQ
165140162a feat: update prompt 2025-08-22 18:29:15 +08:00
CaiHQ
e9fcbd36d4 update playground 2025-08-22 18:03:54 +08:00
zhaoweijie
17020e8755 feat(iod): 重构数联网搜索功能
- 新增数联网设置页面
- 优化数联网搜索结果展示
- 添加数据集、科创场景和科技企业等不同类型的搜索结果
- 重构搜索结果卡片组件,支持加载状态和不同展示模式
- 更新数联网搜索相关的国际化文案
2025-08-22 17:15:19 +08:00
zhaoweijie
efbf2a3eff feat/playground: 重构 playground组件
- 更新 Data 和 History组件的样式和布局
- 添加新的功能和交互,如热门搜索和智能体选择
- 优化组件性能和可维护性
2025-08-21 14:12:29 +08:00
zhaoweijie
df0bf51bdf refactor(layout): 优化团队页面布局和滚动
- 在 Team组件中添加 overflow-y-auto 以启用垂直滚动
- 在 Playground组件中调整网格布局,移除不必要的导入
- 优化消息列表布局,确保内容可以滚动
2025-08-21 14:08:40 +08:00
zhaoweijie
48404fb316 refactor(components): 重构历史记录组件和 playground 布局- 更新 History 组件样式和动画效果
- 调整 Playground 布局结构
-优化 Sidebar 聊天记录样式
2025-08-21 14:08:40 +08:00
CaiHQ
30aa0faaa1 update configs 2025-08-20 18:36:48 +08:00
CaiHQ
8d6a9b39bb update prompt 2025-08-19 17:39:17 +08:00
zhaoweijie
1104fb2733 refactor(layout): 重构布局组件并添加新功能
- 更新 Header 组件,增加项目标题和历史记录切换按钮
- 新增 DataNavigation 组件用于数据导航
- 添加 Playground 相关新组件,包括数据、场景、团队等信息展示
- 重构 Layout 组件,使用 Context 管理历史记录状态
- 更新 zh/option.json 文件,添加新的项目标题和对话相关翻译
2025-08-19 16:20:37 +08:00
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
119 changed files with 8710 additions and 1177 deletions

2
.gitignore vendored
View File

@ -2,7 +2,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# settings # settings
.vscode .vscode
video.mp4
# dependencies # dependencies
/node_modules /node_modules
/.pnp /.pnp

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) [![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/) [![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): Checkout the Demo (v1.0.0):
@ -47,7 +49,9 @@ cd page-assist
2. Install the dependencies 2. Install the dependencies
```bash ```bash
export PATH="/Users/huaqiancai/.bun/bin/:$PATH"
bun install bun install
``` ```
3. Build the extension (by default it will build for Chrome) 3. Build the extension (by default it will build for Chrome)

2552
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", "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", "version": "1.0.9",
"description": "Use your locally running AI models to assist you in your web browsing.", "description": "Use your locally running AI models to assist you in your web browsing.",
"author": "n4ze3m", "author": "n4ze3m",
@ -35,6 +35,7 @@
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"d3-dsv": "2", "d3-dsv": "2",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"framer-motion": "^12.23.12",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"i18next": "^23.10.1", "i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.0", "i18next-browser-languagedetector": "^7.2.0",
@ -47,6 +48,7 @@
"property-information": "^6.4.1", "property-information": "^6.4.1",
"pubsub-js": "^1.9.4", "pubsub-js": "^1.9.4",
"react": "18.2.0", "react": "18.2.0",
"react-countup": "^6.5.3",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",
"react-icons": "^5.2.1", "react-icons": "^5.2.1",
@ -58,6 +60,8 @@
"rehype-mathjax": "4.0.3", "rehype-mathjax": "4.0.3",
"remark-gfm": "3.0.1", "remark-gfm": "3.0.1",
"remark-math": "5.1.1", "remark-math": "5.1.1",
"segmentit": "^2.0.3",
"styled-components": "^6.1.19",
"tesseract.js": "^5.1.1", "tesseract.js": "^5.1.1",
"turndown": "^7.1.3", "turndown": "^7.1.3",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
@ -75,6 +79,7 @@
"@types/react-dom": "18.2.18", "@types/react-dom": "18.2.18",
"@types/react-speech-recognition": "^3.9.5", "@types/react-speech-recognition": "^3.9.5",
"@types/react-syntax-highlighter": "^15.5.11", "@types/react-syntax-highlighter": "^15.5.11",
"@types/styled-components": "^5.1.34",
"@types/turndown": "^5.0.4", "@types/turndown": "^5.0.4",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,17 @@
{ {
"newChat": "新聊天", "projectTitle": "数联网科创智能体",
"selectAPrompt": "本地回答", "newChat": "新对话",
"selectAPrompt": "选择一个提示词",
"githubRepository": "GitHub 仓库", "githubRepository": "GitHub 仓库",
"settings": "设置", "settings": "设置",
"sidebarTitle": "聊天历史", "metering": "计量",
"sidebarTitle": "对话历史",
"error": "错误", "error": "错误",
"somethingWentWrong": "出现了错误", "somethingWentWrong": "出现了错误",
"validationSelectModel": "请选择一个模型以继续", "validationSelectModel": "请选择一个模型以继续",
"deleteHistoryConfirmation": "你确定要删除这个历史记录吗?", "deleteHistoryConfirmation": "你确定要删除这个历史记录吗?",
"editHistoryTitle": "输入一个新的标题", "editHistoryTitle": "输入一个新的标题",
"temporaryChat": "临时聊天", "temporaryChat": "临时对话",
"more": { "more": {
"copy": { "copy": {
"group": "复制", "group": "复制",

View File

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

View File

@ -88,7 +88,7 @@
"system": { "system": {
"heading": "系统设置", "heading": "系统设置",
"deleteChatHistory": { "deleteChatHistory": {
"label": "系统重置", "label": "清除最近对话",
"button": "全部重置", "button": "全部重置",
"confirm": "您确定要执行系统重置吗?这将清除所有数据且无法撤消。" "confirm": "您确定要执行系统重置吗?这将清除所有数据且无法撤消。"
}, },
@ -316,6 +316,10 @@
"title": "管理知识", "title": "管理知识",
"heading": "配置知识库" "heading": "配置知识库"
}, },
"iodSettings": {
"title": "数联网 设置",
"heading": "配置数联网"
},
"rag": { "rag": {
"title": "RAG 设置", "title": "RAG 设置",
"ragSettings": { "ragSettings": {

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

View File

@ -6,7 +6,7 @@
.arimo { .arimo {
font-family: "Arimo", sans-serif; font-family: "Arimo", sans-serif;
font-weight: 500; /*font-weight: 500;*/
font-style: normal; font-style: normal;
} }

View File

@ -0,0 +1,41 @@
import React from "react"
import { Typography } from "antd"
import { 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">
{/* 左侧部分 */}
<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-[#3581e3] transition-colors duration-200"
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({ function Markdown({
message, message,
className = "prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark" className = "prose-lg break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
}: { }: {
message: string message: string
className?: string className?: string

View File

@ -11,6 +11,11 @@ export const PageAssistProvider = ({
const [controller, setController] = React.useState<AbortController | null>( const [controller, setController] = React.useState<AbortController | null>(
null null
) )
const [iodLoading, setIodLoading] = React.useState<boolean>(false)
const [currentMessageId, setCurrentMessageId] = React.useState<string>('')
const [embeddingController, setEmbeddingController] = const [embeddingController, setEmbeddingController] =
React.useState<AbortController | null>(null) React.useState<AbortController | null>(null)
@ -20,6 +25,12 @@ export const PageAssistProvider = ({
messages, messages,
setMessages, setMessages,
iodLoading,
setIodLoading,
currentMessageId,
setCurrentMessageId,
controller, controller,
setController, setController,

View File

@ -0,0 +1,139 @@
import React, { useMemo } from "react"
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Skeleton } from "antd"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { IodRegistryEntry } from "@/types/iod.ts"
import { useIodPlaygroundContext } from "@/components/Option/Playground/PlaygroundIod.tsx"
// import { Drawer } from './Drawer.tsx'
const defaultData: IodRegistryEntry[] = [
{
name: "固态电池固体电解质材料数据集",
doId: "CSTR:16666.11.nbsdc.9bjqrscd",
description: "国家基础学科公共科学数据中心"
},
{
name: "固体颗粒物与流体耦合",
doId: "CSTR:16666.11.nbsdc.xyzbycl7",
description: "清华大学"
}
]
type HeaderProps = {
title: string
showButton?: boolean
onClick?: () => void
}
const Header: React.FC<HeaderProps> = ({
title,
showButton = true,
onClick
}) => (
<DataNavigation
Header={
<div className="flex items-center gap-0.5 text-[#3581e3]">
<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="#3581e3"></path>
</svg>
{title}
</div>
}
showButton={showButton}
onClick={onClick}
/>
)
type MainProps = {
loading: boolean
data: IodRegistryEntry[]
truncate?: boolean
}
const Main: React.FC<MainProps> = ({ data, loading, truncate = true }) => (
<div className="space-y-1.5 flex-1 overflow-y-auto">
{data.map((item, index) => {
return (
<Card
className="[&_.ant-card-body]:!p-2 !bg-[gb(248, 248, 248)] border !border-[#e9e9e9]"
key={item.doId}>
{loading ? (
<Skeleton title={false} active />
) : (
<div className="flex flex-col gap-0.5">
<h3
className={`text-base font-medium mb-1 text-[#222222] break-all ${truncate ? "line-clamp-2" : ""}`}
title={item.name}>
{item.name}
</h3>
<p
className={`text-sm text-[#383838] break-all ${truncate ? "line-clamp-2" : ""}`}
title={item.doId}>
{item.doId}
</p>
<p
className={`text-[#828282] text-xs break-all ${truncate ? "truncate" : ""}`}
title={item.description}>
{item.description}
</p>
</div>
)}
</Card>
)
})}
</div>
)
type Props = {
className?: string
}
export const PlaygroundData: React.FC<Props> = ({ className }) => {
const { iodLoading } = useMessageOption()
const {
setShowPlayground,
setDetailHeader,
setDetailMain,
currentIodMessage
} = useIodPlaygroundContext()
const data = useMemo<IodRegistryEntry[]>(() => {
return currentIodMessage ? currentIodMessage.data?.data ?? [] : defaultData
}, [currentIodMessage])
const title = useMemo(() => {
return currentIodMessage ? "推荐数据" : "热点数据"
}, [currentIodMessage])
const showMore = () => {
setShowPlayground(false)
setDetailHeader(
<Header
title={title}
showButton={false}
onClick={() => setShowPlayground(false)}
/>
)
setDetailMain(<Main loading={iodLoading && Boolean(currentIodMessage)} data={data} truncate={false} />)
}
return (
<Card className={`${className}`} hoverable>
<div className="h-full flex flex-col gap-2 relative">
{/* 数据导航 */}
<Header title={title} onClick={showMore} />
{/* 数据列表 */}
<Main loading={iodLoading && Boolean(currentIodMessage)} data={data.slice(0, 3)} />
</div>
</Card>
)
}

View File

@ -0,0 +1,91 @@
// Drawer.tsx
import React, { useEffect } from "react"
import styled from "styled-components"
import { shadow } from "pdfjs-dist"
interface DrawerProps {
open: boolean
onClose: () => void
children: React.ReactNode
width?: string | number
overlay?: boolean
keydown?: boolean
shadow?: boolean
}
const DrawerOverlay = styled.div<{ isOpen: boolean }>`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
visibility: ${({ isOpen }) => (isOpen ? "visible" : "hidden")};
transition:
opacity 0.3s ease,
visibility 0.3s ease;
z-index: 1000;
`
const DrawerContainer = styled.div<{
isOpen: boolean
width: string | number
shadow: boolean
}>`
position: fixed;
top: 0;
right: 0;
height: 100%;
width: ${({ width }) => (typeof width === "number" ? `${width}px` : width)};
background: #ffffff;
box-shadow: ${shadow ? "-2px 0 8px rgba(0, 0, 0, 0.15)" : ""};
transform: translateX(${({ isOpen }) => (isOpen ? "0" : "100%")});
transition: transform 0.3s ease;
z-index: 9999;
overflow-y: auto;
`
export const Drawer: React.FC<DrawerProps> = ({
open,
onClose,
children,
overlay = true,
keydown = true,
shadow = true,
width = "300px"
}) => {
// 处理 Escape 键关闭抽屉
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
onClose()
}
}
if (keydown) {
document.addEventListener("keydown", handleEscape)
}
return () => {
if (keydown) {
document.removeEventListener("keydown", handleEscape)
}
}
}, [open, onClose, keydown])
// 处理点击遮罩层关闭抽屉
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose()
}
}
return (
<>
{overlay && <DrawerOverlay isOpen={open} onClick={handleOverlayClick} />}
<DrawerContainer isOpen={open} width={width}>
{children}
</DrawerContainer>
</>
)
}

View File

@ -1,7 +1,8 @@
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import React from "react" import React, { useEffect, useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize" import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
import TextArea from "antd/es/input/TextArea"
type Props = { type Props = {
value: string value: string
@ -14,6 +15,14 @@ export const EditMessageForm = (props: Props) => {
const [isComposing, setIsComposing] = React.useState(false) const [isComposing, setIsComposing] = React.useState(false)
const textareaRef = React.useRef<HTMLTextAreaElement>(null) const textareaRef = React.useRef<HTMLTextAreaElement>(null)
const { t } = useTranslation("common") const { t } = useTranslation("common")
const [value, setValue] = useState(props.value);
useEffect(
() => {
setValue(props.value)
},
[props.value]
);
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
@ -29,46 +38,27 @@ export const EditMessageForm = (props: Props) => {
return ( return (
<form <form
onSubmit={form.onSubmit((data) => { onSubmit={form.onSubmit((data) => {
if (isComposing) return
props.onClose() props.onClose()
props.onSumbit(data.message, true) props.onSumbit(value, true)
})} })}
className="flex flex-col gap-2"> className="flex flex-col gap-2 w-96 ml-auto">
<textarea <TextArea
{...form.getInputProps("message")}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
required required
rows={1} rows={2}
value={value}
style={{ minHeight: "60px" }} style={{ minHeight: "60px" }}
tabIndex={0} tabIndex={0}
onChange={(e) => {
setValue(e.target.value)
}}
placeholder={t("editMessage.placeholder")} placeholder={t("editMessage.placeholder")}
ref={textareaRef}
className="w-full bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
/> />
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
<div <div
className={`w-full flex ${ className={`w-full flex ${
!props.isBot ? "justify-between" : "justify-end" !props.isBot ? "justify-end" : "justify-end"
}`}> }`}>
{!props.isBot && (
<button
type="button"
onClick={() => {
props.onSumbit(form.values.message, false)
props.onClose()
}}
aria-label={t("save")}
className="border border-gray-600 px-2 py-1.5 rounded-lg text-gray-700 dark:text-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900 text-sm">
{t("save")}
</button>
)}
<div className="flex space-x-2"> <div className="flex space-x-2">
<button
aria-label={t("save")}
className="bg-black px-2 py-1.5 rounded-lg text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-900 text-sm">
{props.isBot ? t("save") : t("saveAndSubmit")}
</button>
<button <button
onClick={props.onClose} onClick={props.onClose}
@ -76,6 +66,12 @@ export const EditMessageForm = (props: Props) => {
className="border dark:border-gray-600 px-2 py-1.5 rounded-lg text-gray-700 dark:text-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900 text-sm"> className="border dark:border-gray-600 px-2 py-1.5 rounded-lg text-gray-700 dark:text-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900 text-sm">
{t("cancel")} {t("cancel")}
</button> </button>
<button
aria-label={t("save")}
className="bg-[#0057ff] px-2 py-1.5 rounded-lg text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 text-sm">
{props.isBot ? t("save") : t("saveAndSubmit")}
</button>
</div> </div>
</div> </div>
</div>{" "} </div>{" "}

View File

@ -0,0 +1,482 @@
import React, { useEffect, useMemo, useState } from "react"
import { Avatar, Card } from "antd"
import { AnimatePresence, motion } from "framer-motion" // 使用 CSS-in-JS 方式
import styled, { keyframes } from "styled-components"
import CountUp from "react-countup"
import { TalentPoolIcon } from "@/components/Icons/TalentPool .tsx"
import { ResearchPaperIcon } from "@/components/Icons/ResearchPaper.tsx"
import { DataProjectIcon } from "@/components/Icons/DataProject.tsx"
import { DatasetIcon } from "@/components/Icons/Dataset.tsx"
import { TechCompanyIcon } from "@/components/Icons/TechCompany.tsx"
import { ResearchInstitutesIcon } from "@/components/Icons/ResearchInstitutes.tsx"
import { NSDCIcon } from "@/components/Icons/NSDC.tsx"
import { useIodPlaygroundContext } from "@/components/Option/Playground/PlaygroundIod.tsx"
import { totalSearchResults } from "@/services/search.ts"
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);
}
`
// 花瓣 /* ${(props) => (props.playing ? "running" : "paused")}; */
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;
animation-play-state: running;
animation-duration: 3s; /* 添加动画总持续时间 */
animation-fill-mode: forwards; /* 保持动画结束时的状态 */
`
const FrostedGlassCard = styled(Card)`
background: rgba(255, 255, 255, 0.25) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
`
const SuccessIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-green-500"
ref={ref}
{...props}>
<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 = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon animate-spin"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="29588"
ref={ref}
{...props}>
<path
d="M483.712 888.064a52.437333 52.437333 0 1 1 52.48 52.352 52.394667 52.394667 0 0 1-52.48-52.352z m-235.434667-53.76a65.578667 65.578667 0 1 1 46.421334 19.242667 65.962667 65.962667 0 0 1-46.378667-19.242667z m499.584-16.597333a41.984 41.984 0 0 1 59.264-59.434667 42.282667 42.282667 0 0 1 0 59.434667 41.941333 41.941333 0 0 1-59.264 0zM112.853333 546.602667a81.92 81.92 0 1 1 81.92 81.92 81.834667 81.834667 0 0 1-81.92-81.877334z m731.008 0a33.536 33.536 0 1 1 33.493334 33.578666 33.578667 33.578667 0 0 1-33.450667-33.536zM222.208 377.6a102.4 102.4 0 1 1 72.533333 29.866667 102.869333 102.869333 0 0 1-72.533333-29.824z m536.32-53.504a26.666667 26.666667 0 1 1 18.816 7.936 26.368 26.368 0 0 1-18.773333-7.893333zM414.378667 205.184a121.642667 121.642667 0 1 1 121.813333 121.6A121.728 121.728 0 0 1 414.378667 205.226667z"
p-id="29589"
fill="#4284f6"></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>
)
}
// 自定义统计卡片组件
const StatCard: React.FC<{
number: number
unit?: string
label: string
decimals?: number
icon: React.ReactNode
}> = ({ number, unit, label, decimals, icon }) => {
return (
<div
className="flex flex-col items-center justify-center p-3 rounded-xl shadow-sm bg-[rgba(240,245,255,0.3)] backdrop-blur-sm border border-[rgba(200,220,255,0.25)]
">
<Avatar size={40} className="!bg-[#3581e3b3]" icon={icon} />
<div className="text-lg font-bold text-[#f00000]">
<CountUp
end={number}
duration={2.5}
separator=","
decimals={decimals}
/>
{unit}
</div>
<div className="text-sm text-[#3581e3] mt-1 flex items-center gap-2">
{" "}
{label}
</div>
</div>
)
}
export const StatisticGrid: React.FC = () => {
return (
<div className="p-6">
{/* 第一行3 个卡片 */}
<div className="grid grid-cols-3 gap-6 mb-6">
<StatCard
icon={<NSDCIcon className="w-6 h-6 text-white" color="#3581e3" />}
number={11}
unit="家"
label="国家科学数据中心"
/>
<StatCard
icon={
<ResearchInstitutesIcon
className="w-6 h-6 text-white"
color="#3581e3"
/>
}
number={763}
unit="家"
label="高等院校和科研机构"
/>
<StatCard
icon={
<TechCompanyIcon className="w-6 h-6 text-white" color="#3581e3" />
}
number={2.1}
decimals={1}
unit="万"
label="科技型企业"
/>
</div>
{/* 第二行4 个卡片 */}
<div className="grid grid-cols-4 gap-6">
<StatCard
icon={<DatasetIcon className="w-6 h-6 text-white" color="#3581e3" />}
number={537163}
label="数据集"
/>
<StatCard
icon={
<DataProjectIcon className="w-6 h-6 text-white" color="#3581e3" />
}
number={183729}
label="数据项目"
/>
<StatCard
icon={
<ResearchPaperIcon className="w-6 h-6 text-white" color="#3581e3" />
}
number={1380026}
label="数据论文"
/>
<StatCard
icon={
<TalentPoolIcon className="w-6 h-6 text-white" color="#3581e3" />
}
number={2}
unit="万"
label="科创人才"
/>
</div>
</div>
)
}
type Props = {
className?: string
}
export const PlaygroundIodRelevant: React.FC<Props> = ({ className }) => {
const { iodLoading, iodSearch } = useMessageOption()
const { currentIodMessage } = useIodPlaygroundContext()
const showSearchData = useMemo(() => {
return currentIodMessage && !iodLoading
}, [currentIodMessage, iodLoading])
const [count, setCount] = useState<number>(0)
useEffect(() => {
totalSearchResults().then((res) => {
setCount(res)
})
}, [])
const getMinNum = (n1: number) => {
return Math.min(n1, count)
}
const data = useMemo(() => {
const loading = iodSearch && iodLoading
const text = loading ? "正" : "已"
const text2 = loading ? "进行" : "完成"
const text3 = loading ? "……" : ""
const duration = loading ? 2.5 : 0
return [
{
title: (
<p className="font-extrabold">
{text}
<span className="text-[#f00000]">
<CountUp end={29} duration={duration} separator="," />
</span>
<span className="text-[#f00000]">
{" "}
<CountUp end={55} duration={duration} separator="," />
</span>
<span className="text-[#f00000]">
<CountUp
decimals={1}
end={53.7}
duration={duration}
separator=","
/>
</span>
{text2}{text3}
</p>
),
description: showSearchData ? (
<p>
<span className="text-green-700">
{" "}
<CountUp
end={currentIodMessage?.data.total ?? 0}
duration={2.5}
separator=","
/>
{" "}
</span>
{getMinNum(currentIodMessage?.data.total ?? 0)}
</p>
) : (
""
)
},
{
title: (
<p className="font-extrabold">
{text}
<span className="text-[#f00000]">
<CountUp end={138} duration={duration} separator="," />
</span>
<span className="text-[#f00000]">
<CountUp
end={18.3}
decimals={1}
duration={duration}
separator=","
/>
</span>
{text2}{text3}
</p>
),
description: showSearchData ? (
<p>
<span className="text-green-700">
{" "}
<CountUp
end={currentIodMessage?.scenario.total ?? 0}
duration={2.5}
separator=","
/>
{" "}
</span>
{getMinNum(currentIodMessage?.scenario.total ?? 0)}
</p>
) : (
""
)
},
{
title: (
<p className="font-extrabold">
{text}
<span className="text-[#f00000]">
<CountUp end={763} duration={duration} separator="," />
</span>
<span className="text-[#f00000]">
{" "}
<CountUp
end={2.1}
decimals={1}
duration={duration}
separator=","
/>
</span>
<span className="text-[#f00000]">
{" "}
<CountUp end={2} duration={duration} separator="," />
</span>
{text2}{text3}
</p>
),
description: showSearchData ? (
<p>
<span className="text-green-700">
{" "}
<CountUp
end={currentIodMessage?.organization.total ?? 0}
duration={2.5}
separator=","
/>
{" "}
</span>
{getMinNum(currentIodMessage?.organization.total ?? 0)}
</p>
) : (
""
)
}
]
}, [showSearchData, iodLoading, count])
return (
<Card
hoverable
variant="outlined"
className={`${className} translate-y-[-2px] !bg-[#d0e6ff] shadow-md`}>
<div className="h-full flex flex-col relative">
{/* 花瓣效果 */}
<div
className={`absolute inset-0 pointer-events-none z-0 overflow-hidden ${showSearchData ? "" : ""}`}>
<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-[#1a3c87] flex justify-center items-center">
<div className="flex items-center gap-2">
<SearchIcon />
{currentIodMessage ? "科创数联网深度搜索" : "科创数联网连接资源"}
</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-[#1a3c87] mt-1 text-center">
{currentIodMessage
? "下面是在科创数联网上进行深度搜索得到的相关数据、场景和团队"
: "下面是科创数联网连接的数据、场景和团队"}
</p>
</div>
{/* Content */}
<div className="space-y-2 flex-1 overflow-y-auto">
{currentIodMessage ? (
<AnimatePresence mode="wait">
<motion.div
key="search-results"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="space-y-2 flex-1 overflow-y-auto">
{data.map((item, index) => (
<FrostedGlassCard
className="[&_.ant-card-body]:!p-3 [&_.ant-card-body]:h-full shadow-md min-h-[88px]"
key={index}>
<div
className={`flex flex-col gap-2 h-full items-start ${showSearchData ? "justify-start" : "justify-center"}`}>
<div className="flex items-center gap-2">
<div>
{iodSearch && iodLoading ? (
<LoadingIcon
width={showSearchData ? 16 : 22}
height={showSearchData ? 16 : 22}
/>
) : (
<SuccessIcon
width={showSearchData ? 16 : 22}
height={showSearchData ? 16 : 22}
/>
)}
</div>
<div
className={`text-gray-700 ${showSearchData ? "text-sm" : "text-lg"}`}>
{item.title}
</div>
</div>
{item.description && (
<div className="flex-1">
<div className="text-xs text-gray-500 mt-1 pl-7">
{item.description}
</div>
</div>
)}
</div>
</FrostedGlassCard>
))}
</motion.div>
</AnimatePresence>
) : (
<AnimatePresence mode="wait">
<motion.div
key="statistic-grid"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="flex-1 overflow-y-auto">
<StatisticGrid />
</motion.div>
</AnimatePresence>
)}
</div>
</div>
</Card>
)
}

View File

@ -1,26 +1,34 @@
import Markdown from "../../Common/Markdown" import Markdown from "../../Common/Markdown"
import React from "react" import React from "react"
import { Tag, Image, Tooltip, Collapse, Popover } from "antd" import { Collapse, Image, Popover, Tag, Tooltip } from "antd"
import { WebSearch } from "./WebSearch" import { WebSearch } from "./WebSearch"
import { import {
ArrowUpSquare,
CheckIcon, CheckIcon,
ClipboardIcon, ClipboardIcon,
InfoIcon, InfoIcon,
MessageSquareShare,
Pen, Pen,
PlayIcon, PlayIcon,
RotateCcw, RotateCcw,
Square Square,
Star,
ThumbsDown,
ThumbsUp
} from "lucide-react" } from "lucide-react"
import { EditMessageForm } from "./EditMessageForm" import { EditMessageForm } from "./EditMessageForm"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { MessageSource } from "./MessageSource" import { MessageSource } from "./MessageSource"
import { useTTS } from "@/hooks/useTTS" import { useTTS } from "@/hooks/useTTS"
import { tagColors } from "@/utils/color" import { tagColors } from "@/utils/color"
import { removeModelSuffix } from "@/db/models"
import { GenerationInfo } from "./GenerationInfo" import { GenerationInfo } from "./GenerationInfo"
import { parseReasoning, } from "@/libs/reasoning" import { parseReasoning } from "@/libs/reasoning"
import { humanizeMilliseconds } from "@/utils/humanize-milliseconds" import { humanizeMilliseconds } from "@/utils/humanize-milliseconds"
import { AllIodRegistryEntry } from "@/types/iod.ts"
import { PiNetwork } from "react-icons/pi"
type Props = { type Props = {
id?: string
message: string message: string
message_type?: string message_type?: string
hideCopy?: boolean hideCopy?: boolean
@ -36,50 +44,31 @@ type Props = {
isProcessing: boolean isProcessing: boolean
webSearch?: {} webSearch?: {}
isSearchingInternet?: boolean isSearchingInternet?: boolean
sources?: any[] webSources?: any[]
iodSources?:any[] iodSources?: AllIodRegistryEntry
hideEditAndRegenerate?: boolean hideEditAndRegenerate?: boolean
onSourceClick?: (source: any) => void onSourceClick?: (source: any) => void
isTTSEnabled?: boolean isTTSEnabled?: boolean
generationInfo?: any generationInfo?: any
isStreaming: boolean isStreaming: boolean
reasoningTimeTaken?: number reasoningTimeTaken?: number
iodSearch?: boolean
setCurrentMessageId: (id: string) => void
} }
export const PlaygroundMessage = (props: Props) => { export const PlaygroundMessage: React.FC<Props> = (props) => {
const [isBtnPressed, setIsBtnPressed] = React.useState(false) const [isBtnPressed, setIsBtnPressed] = React.useState(false)
const [editMode, setEditMode] = React.useState(false) const [editMode, setEditMode] = React.useState(false)
const { t } = useTranslation("common") const { t } = useTranslation("common")
const { cancel, isSpeaking, speak } = useTTS() const { cancel, isSpeaking, speak } = useTTS()
return ( return (
<div className="group relative flex w-full max-w-3xl flex-col items-end justify-center pb-2 md:px-4 lg:w-4/5 text-gray-800 dark:text-gray-100"> <div className="group relative flex w-full flex-col items-end justify-center pb-2 md:px-4 text-gray-800 dark:text-gray-100">
{/* <div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> */} {/* <div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> */}
<div className="flex flex-row gap-4 md:gap-6 my-2 m-auto w-full"> <div
<div className="w-8 flex flex-col relative items-end"> className={`flex flex-row gap-1 md:gap-1 my-2 m-auto w-full ${props.isBot ? "" : "flex-row-reverse"}`}>
<div className="relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center text-opacity-100r"> <div className="flex flex-col gap-2">
{props.isBot ? ( <span className="text-xs font-bold text-gray-800 dark:text-white"></span>
!props.botAvatar ? (
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400"></div>
) : (
props.botAvatar
)
) : !props.userAvatar ? (
<div className="absolute h-8 w-8 rounded-full from-blue-400 to-blue-600 bg-gradient-to-r"></div>
) : (
props.userAvatar
)}
</div>
</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 && {props.isBot &&
props.isSearchingInternet && props.isSearchingInternet &&
@ -93,7 +82,7 @@ export const PlaygroundMessage = (props: Props) => {
</Tag> </Tag>
)} )}
</div> </div>
<div className="flex flex-grow flex-col"> <div className={`flex flex-grow flex-col w-full`}>
{!editMode ? ( {!editMode ? (
props.isBot ? ( props.isBot ? (
<> <>
@ -102,6 +91,7 @@ export const PlaygroundMessage = (props: Props) => {
return ( return (
<Collapse <Collapse
key={i} key={i}
defaultActiveKey={["reasoning"]}
className="border-none !mb-3" className="border-none !mb-3"
items={[ items={[
{ {
@ -131,11 +121,17 @@ export const PlaygroundMessage = (props: Props) => {
})} })}
</> </>
) : ( ) : (
// <p
// className={`bg-[#f1f3f4] font-normal text-[#000000d9] px-4 py-2.5 rounded-2xl prose-lg dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${
// props.message_type && "italic dark:text-gray-400"
// } flex flex-row-reverse`}>
// {props.message}
// </p>
<p <p
className={`prose dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${ className={`bg-[#2563eb] font-normal rounded-tr-none
props.message_type && text-white px-4 py-2.5 rounded-2xl prose-lg dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${
"italic text-gray-500 dark:text-gray-400 text-sm" props.message_type && "italic dark:text-gray-400"
}`}> } flex flex-row-reverse`}>
{props.message} {props.message}
</p> </p>
) )
@ -166,26 +162,27 @@ export const PlaygroundMessage = (props: Props) => {
</div> </div>
)} )}
{props.isBot && props?.iodSources && props?.iodSources.length > 0 && ( {props.isBot && props?.webSources && props?.webSources.length > 0 && (
<Collapse <Collapse
className="mt-6" className="mt-6"
ghost ghost
// defaultActiveKey={['webSources']}
items={[ items={[
{ {
key: "1", key: "webSources",
label: ( label: (
<div className="italic text-gray-500 dark:text-gray-400"> <div className="italic text-gray-500 dark:text-gray-400">
{t("iodcitations")} {t("webCitations")}
</div> </div>
), ),
children: ( children: (
<div className="block"> <div className="mb-3 flex flex-wrap gap-2">
{props?.iodSources?.map((source, index) => ( {props?.webSources?.map((source, index) => (
<MessageSource <MessageSource
onSourceClick={props.onSourceClick} onSourceClick={props.onSourceClick}
key={index} key={index}
index={index}
source={source} source={source}
index = {index}
/> />
))} ))}
</div> </div>
@ -194,26 +191,34 @@ export const PlaygroundMessage = (props: Props) => {
]} ]}
/> />
)} )}
{props.isBot && props?.sources && props?.sources.length > 0 && ( {props.isBot &&
props?.iodSources &&
Object.values(props?.iodSources)
.map((item) => item.data)
.flat().length > 0 && (
<Collapse <Collapse
className="mt-6" className="mt-6"
ghost ghost
// defaultActiveKey={['iod']}
items={[ items={[
{ {
key: "1", key: "iod",
label: ( label: (
<div className="italic text-gray-500 dark:text-gray-400"> <div className="italic text-gray-500 dark:text-gray-400">
{t("citations")} {t("iodCitations")}
</div> </div>
), ),
children: ( children: (
<div className="block"> <div className="mb-3 flex flex-wrap gap-2">
{props?.sources?.map((source, index) => ( {Object.values(props?.iodSources)
.map((item) => item.data)
.flat()
?.map((source, index) => (
<MessageSource <MessageSource
onSourceClick={props.onSourceClick} onSourceClick={props.onSourceClick}
key={index} key={index}
index={index}
source={source} source={source}
index = {index}
/> />
))} ))}
</div> </div>
@ -234,7 +239,7 @@ export const PlaygroundMessage = (props: Props) => {
// : "flex" // : "flex"
}`}> }`}>
{props.isTTSEnabled && ( {props.isTTSEnabled && (
<Tooltip title={t("tts")}> <Tooltip title={t("tts")} className="hidden">
<button <button
aria-label={t("tts")} aria-label={t("tts")}
onClick={() => { onClick={() => {
@ -257,6 +262,18 @@ export const PlaygroundMessage = (props: Props) => {
)} )}
{props.isBot && ( {props.isBot && (
<> <>
{/*数联网搜索*/}
{props.iodSearch && (
<Tooltip title="数联网信息">
<button
onClick={() => props.setCurrentMessageId(props.id)}
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">
<PiNetwork className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
</button>
</Tooltip>
)}
{!props.hideCopy && ( {!props.hideCopy && (
<Tooltip title={t("copyToClipboard")}> <Tooltip title={t("copyToClipboard")}>
<button <button
@ -280,6 +297,7 @@ export const PlaygroundMessage = (props: Props) => {
{props.generationInfo && ( {props.generationInfo && (
<Popover <Popover
className="hidden"
content={ content={
<GenerationInfo generationInfo={props.generationInfo} /> <GenerationInfo generationInfo={props.generationInfo} />
} }
@ -305,8 +323,8 @@ export const PlaygroundMessage = (props: Props) => {
)} )}
</> </>
)} )}
{!props.hideEditAndRegenerate && ( {(!props.hideEditAndRegenerate && !props.isBot) && (
<Tooltip title={t("edit")}> <Tooltip title={t("edit")} className="hidden">
<button <button
onClick={() => setEditMode(true)} onClick={() => setEditMode(true)}
aria-label={t("edit")} aria-label={t("edit")}
@ -315,6 +333,51 @@ export const PlaygroundMessage = (props: Props) => {
</button> </button>
</Tooltip> </Tooltip>
)} )}
{
<Tooltip title="收藏" className="hidden">
<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="发布语用" className="hidden">
<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="发布对话" className="hidden">
<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="点赞" className="hidden">
<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="点踩" className="hidden">
<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> </div>
) : ( ) : (
// add invisible div to prevent layout shift // 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" import { KnowledgeIcon } from "@/components/Option/Knowledge/KnowledgeIcon"
type Props = { type Props = {
index: number
source: { source: {
name?: string name?: string
url?: string url?: string
@ -8,42 +11,72 @@ type Props = {
type?: string type?: string
pageContent?: string pageContent?: string
content?: string content?: string
doId?: string
description?: string
} }
key: number
onSourceClick?: (source: any) => void 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") { if (source?.mode === "rag" || source?.mode === "chat") {
return ( return (
<div className="block items-center gap-1 text-xs text-gray-800 dark:text-gray-100 mb-1"> <button
<span className="text-xs font-medium">[{index + 1}]</span> {/* 显示序号 */} onClick={() => {
<span className="text-xs">{source.name}</span> onSourceClick && onSourceClick(source)
<a
href={source?.url}
target="_blank"
className="text-xs text-blue-500 hover:underline"
onClick={(e) => {
e.preventDefault(); // 阻止默认的链接行为
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">
{source.url} <KnowledgeIcon type={source.type} className="h-3 w-3" />
</a> <span className="text-xs">{source.name}</span>
</div> </button>
); )
}
const onContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation
setShowContent(true)
} }
return ( return (
<div className="block items-center gap-1 text-xs text-gray-800 dark:text-gray-100 mb-1"> <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 <a
href={source?.url} href={source?.url}
target="_blank" target="_blank"
className="text-xs text-blue-500 hover:underline" 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.name} {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> </a>
</div> </div>
) )

View File

@ -0,0 +1,150 @@
import React, { useMemo } from "react"
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Skeleton } from "antd"
import { IodRegistryEntry } from "@/types/iod.ts"
import { useIodPlaygroundContext } from "@/components/Option/Playground/PlaygroundIod.tsx"
const defaultData: IodRegistryEntry[] = [
{
name: "绿色化工工艺项目",
description:
"基于生物基原料采用repeal2.0可降解材料技术,开发新型环保材料。",
doId: "CSTR:13552.11.01.61.2021.742"
},
{
name: "智能农业解决方案",
description: "利用物联网技术,实现精准农业管理,提高农作物产量。",
doId: "CSTR:14542.11.01.61.2031.528"
},
{
name: "新能源汽车电池技术",
description: "研发高能量密度、长寿命的新型电池材料,推动电动汽车发展。",
doId: "CSTR:147842.11.04.91.2031.680"
},
{
name: "碳捕集与封存技术",
description: "开发高效的碳捕集技术,减少工业排放,助力碳中和目标。",
doId: "CSTR:14242.19.11.61.2131.428"
}
]
type HeaderProps = {
title: string
showButton?: boolean
onClick?: () => void
}
const Header: React.FC<HeaderProps> = ({
title,
showButton = true,
onClick
}) => (
<DataNavigation
Header={
<div className="flex items-center text-[#4ab01a] 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="#4ab01a"
p-id="6236"></path>
</svg>
{title}
</div>
}
showButton={showButton}
onClick={onClick}
/>
)
type MainProps = {
loading: boolean
data: IodRegistryEntry[]
truncate?: boolean
}
const Main: React.FC<MainProps> = ({ data, loading, truncate = true }) => (
<div className="space-y-1.5 flex-1 overflow-y-auto">
{data.map((item, index) => {
return (
<Card
className="[&_.ant-card-body]:!p-2 !bg-[gb(248, 248, 248)] border !border-[#e9e9e9]"
key={item.doId}>
{loading ? (
<Skeleton title={false} active />
) : (
<div className="flex flex-col gap-0.5">
<h3
className={`text-base font-medium mb-1 text-[#222222] break-all ${truncate ? "line-clamp-2" : ""}`}
title={item.name}>
{item.name}
</h3>
<p
className={`text-sm text-[#383838] break-all ${truncate ? "line-clamp-2" : ""}`}
title={item.doId}>
{item.doId}
</p>
<p
className={`text-[#828282] text-xs break-all ${truncate ? "truncate" : ""}`}
title={item.description}>
{item.description}
</p>
</div>
)}
</Card>
)
})}
</div>
)
type Props = {
className?: string
}
export const PlaygroundScene: React.FC<Props> = ({ className }) => {
const { iodLoading } = useMessageOption()
const {
setShowPlayground,
setDetailHeader,
setDetailMain,
currentIodMessage
} = useIodPlaygroundContext()
const data = useMemo<IodRegistryEntry[]>(() => {
return currentIodMessage
? currentIodMessage.scenario?.data ?? []
: defaultData
}, [currentIodMessage])
const title = useMemo(() => {
return currentIodMessage ? "推荐场景" : "热点场景"
}, [currentIodMessage])
const showMore = () => {
setShowPlayground(false)
setDetailHeader(
<Header
title={title}
showButton={false}
onClick={() => setShowPlayground(false)}
/>
)
setDetailMain(<Main loading={iodLoading && Boolean(currentIodMessage)} data={data} truncate={false} />)
}
return (
<Card className={`${className}`} hoverable>
<div className="h-full flex flex-col gap-2 relative">
{/* 数据导航 */}
<Header title={title} onClick={showMore} />
{/* 数据列表 */}
<Main loading={iodLoading && Boolean(currentIodMessage)} data={data.slice(0, 3)} />
</div>
</Card>
)
}

View File

@ -0,0 +1,158 @@
import React, { useMemo } from "react"
import { DataNavigation } from "@/components/Common/DataNavigation.tsx"
import { Card, Skeleton } from "antd"
import { IodRegistryEntry } from "@/types/iod.ts"
import { useIodPlaygroundContext } from "@/components/Option/Playground/PlaygroundIod.tsx"
const defaultData: IodRegistryEntry[] = [
{
name: "北京大学",
description:
"北大是常为新的,改进的运动的先锋,要使中国向着好的,往上的道路走。",
doId: "12100000400002259P"
},
{
name: "长三角先进材料研究院",
description: "由江苏省人民政府联合中国科学院、中国钢研科技集团和中国",
doId: "91320507MAEKWL5Y2L"
},
{
name: "伊利诺伊大学香槟分校UIUC",
description: "创建于1867年坐落于伊利诺伊州双子城厄巴纳香槟市",
doId: "bdware.org/uiuc"
}
]
type HeaderProps = {
title: string
showButton?: boolean
onClick?: () => void
}
const Header: React.FC<HeaderProps> = ({
title,
showButton = true,
onClick
}) => (
<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>
{title}
</div>
}
showButton={showButton}
onClick={onClick}
/>
)
type MainProps = {
loading: boolean
data: IodRegistryEntry[]
truncate?: boolean
// 水平展示三个还是按列展示(页面和详情展示不一样)
flat?: boolean
}
const Main: React.FC<MainProps> = ({
data,
loading,
truncate = true,
flat = true
}) => (
<div
className={`${flat ? "grid grid-cols-3 gap-3" : "space-y-1.5"} flex-1 overflow-y-auto`}>
{data.map((item) => {
return (
<Card
className="[&_.ant-card-body]:!p-2 !bg-[gb(248, 248, 248)] border !border-[#e9e9e9]"
key={item.doId}>
{loading ? (
<Skeleton title={false} active />
) : (
<div className="flex flex-col gap-0.5">
<h3
className={`text-base font-medium mb-1 text-[#222222] break-all ${truncate ? "line-clamp-2" : ""}`}
title={item.name}>
{item.name}
</h3>
<p
className={`text-sm text-[#383838] break-all ${truncate ? "line-clamp-2" : ""}`}
title={item.doId}>
{item.doId}
</p>
<p
className={`text-[#828282] text-xs break-all ${truncate ? "truncate" : ""}`}
title={item.description}>
{item.description}
</p>
</div>
)}
</Card>
)
})}
</div>
)
type Props = {
className?: string
}
export const PlaygroundTeam: React.FC<Props> = ({ className }) => {
const { iodLoading } = useMessageOption()
const {
setShowPlayground,
setDetailHeader,
setDetailMain,
currentIodMessage
} = useIodPlaygroundContext()
const data = useMemo<IodRegistryEntry[]>(() => {
return currentIodMessage
? currentIodMessage.organization?.data ?? []
: defaultData
}, [currentIodMessage])
const title = useMemo(() => {
return currentIodMessage ? "推荐团队" : "热点团队"
}, [currentIodMessage])
const showMore = () => {
setShowPlayground(false)
setDetailHeader(
<Header
title={title}
showButton={false}
onClick={() => setShowPlayground(false)}
/>
)
setDetailMain(
<Main loading={iodLoading && Boolean(currentIodMessage)} data={data} truncate={false} flat={false} />
)
}
return (
<Card className={`${className}`} hoverable>
<div className="h-full flex flex-col gap-2 relative">
{/* 数据导航 */}
<Header title={title} onClick={showMore} />
{/* 数据列表 */}
<Main loading={iodLoading && Boolean(currentIodMessage)} data={data.slice(0, 3)} />
</div>
</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,15 +1,13 @@
import { Form, Image, Input, Modal, Tooltip, message } from "antd" import { Form, Image, Input, message, Modal } from "antd"
import { Share } from "lucide-react"
import { useState } from "react"
import type { Message } from "~/store/option"
import Markdown from "./Markdown"
import React from "react" import React from "react"
import Markdown from "./Markdown"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
import { getPageShareUrl } from "~/services/ollama" import { getPageShareUrl } from "~/services/ollama"
import { cleanUrl } from "~/libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { getTitleById, getUserId, saveWebshare } from "@/db" import { getTitleById, getUserId, saveWebshare } from "@/db"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import fetcher from "@/libs/fetcher" import fetcher from "@/libs/fetcher"
import { Message } from "@/types/message.ts"
type Props = { type Props = {
messages: Message[] messages: Message[]

View File

@ -0,0 +1,21 @@
import React from "react"
export const BatteryIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M604.16 112.64c13.568-35.0208-5.9904-57.7024-24.1152-80.8448H452.7616C436.2752 56.32 419.84 80.4864 434.176 112.64zM194.56 888.3712a213.4528 213.4528 0 0 0 21.504 2.304c195.7888 0 391.5776 0 587.3152 0.6656 25.1904 0 29.4912-10.24 29.3888-32.1024-0.6144-202.0864-3.1232-633.7024-3.1232-633.7024H194.56zM597.3504 307.712l7.1168 4.2496c-25.6 73.1136-50.8928 146.2272-78.4384 225.28h139.0592L437.9648 824.32l-8.6016-2.7648c17.92-71.0144 35.84-142.0288 54.9888-217.7536l-134.656-5.7856zM192.6656 926.72c-4.096 41.8304 14.7456 64.4096 54.3744 64.8192 66.56 0.6656 133.12 0 200.0384 0 105.8304 0 211.6608 0.3072 317.44 0 44.6464 0 69.6832-25.856 64.512-64.8704zM777.4208 141.0048c-20.992-1.6896-42.24-0.4096-63.3856-0.4096H257.4848c-41.3696 0-57.9584 12.3904-66.0992 49.3568h641.2288c-6.5024-32.5632-26.368-46.592-55.1936-48.9472z"
p-id="50919"></path>
</svg>
)
})

View File

@ -0,0 +1,26 @@
import React from "react"
export const BellIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="76534"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M593.861938 788.582269 424.670537 788.582269c-9.444093 0-18.437931 3.931542-24.695448 10.902304-6.313799 6.970762-9.441023 16.32378-8.547677 25.675776 2.860141 29.191856 16.32378 56.238862 38.009685 76.018348 21.772886 20.016893 50.161447 31.0379 79.889515 31.0379 29.696346 0 58.084906-11.022031 79.830163-30.977525 21.714558-19.839861 35.178197-46.885843 38.068014-76.255755 0.595564-9.473769-2.534729-18.707061-8.638751-25.498744C612.299869 792.513812 603.306031 788.582269 593.861938 788.582269zM555.020304 863.825974c-25.082258 22.877033-66.604954 22.817682-91.567485 0.060375-7.596002-6.970762-13.404288-15.429411-17.157775-24.723078l125.82266 0C568.394916 848.51629 562.643935 856.914564 555.020304 863.825974z"
p-id="76535"></path>
<path
d="M818.608631 648.343271l-62.763462-82.927711 0-36.22197 0-13.046131L755.845169 410.432767c0-70.745251-24.215518-136.337131-68.182892-184.682209-26.003234-28.625968-57.310264-49.715285-93.055372-62.821791-3.306302-18.944468-12.720719-36.251645-26.926256-49.207725-32.050973-29.251208-85.104283-29.251208-117.095905 0-14.356986 13.046131-23.77038 30.382984-26.986631 49.2681-35.71441 13.046131-67.022463 34.135448-93.025697 62.791092-43.937698 48.434106-68.183915 114.025986-68.183915 184.652534l0.179079 154.686035-62.315254 82.45085c-8.757454 9.353019-13.582343 21.506826-13.582343 34.256198l0 40.331567c0 27.643594 22.460548 50.042743 50.042743 50.042743l544.812313 0c27.610848 0 50.011021-22.400173 50.011021-50.042743l0-40.331567C831.535035 669.075455 826.739822 656.921647 818.608631 648.343271zM535.776008 149.881612c-7.387247-0.655939-19.301602-1.906419-26.569122-1.906419-7.29822 0-19.689435 1.251503-27.048029 1.906419C494.578724 129.627313 526.542716 133.379777 535.776008 149.881612zM237.426992 722.156394l-0.119727-40.034808 62.315254-82.449827c8.698103-9.354042 13.524015-21.447475 13.524015-34.256198L313.146535 410.432767c0-58.056254 19.540032-111.553679 54.986335-150.634766 17.574261-19.361977 38.307468-34.374902 61.540611-44.681642 48.851615-21.745257 110.302175-21.745257 159.096485 0 23.321148 10.425444 43.99398 25.438369 61.538565 44.681642 35.449373 39.081087 54.958706 92.578512 54.958706 150.634766l0 105.715717 0 13.046131 0 36.22197c0 12.867052 4.825912 25.081235 12.95608 33.539884l62.791092 82.868359 0.508583 39.795355L237.426992 722.156394z"
p-id="76536"></path>
</svg>
)
})

View File

@ -0,0 +1,26 @@
import React from "react"
export const CheckIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="41530"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M334.935114 642.334328l-185.247485-227.447917c-16.445757-20.169324-13.342784-49.957864 6.826541-66.713918L571.071355 10.569036c20.169324-16.445757 49.957864-13.342784 66.713919 6.826541l185.247484 227.447916c16.445757 20.169324 13.342784 49.957864-6.82654 66.713919L401.649033 649.160868c-20.479621 16.445757-50.268162 13.342784-66.713919-6.82654zM189.71598 693.843679L39.53209 509.216788c-14.273676-17.376648-11.481-43.131324 5.895648-57.404999l5.585352-4.654459c17.376648-14.273676 43.131324-11.481 57.404999 5.895648l150.494188 184.62689c14.273676 17.376648 11.481 43.131324-5.895649 57.405l-5.585351 4.654459c-17.686946 14.273676-43.441621 11.481-57.715297-5.895648zM877.024488 1024H275.668331a44.372513 44.372513 0 1 1 0-88.745026h601.356157a44.372513 44.372513 0 1 1 0 88.745026z"
p-id="41531"></path>
<path
d="M564.555112 345.06952l-77.264026-94.950972c-16.445757-20.169324-13.342784-49.957864 6.82654-66.713919l199.521161-162.595782c20.169324-16.445757 49.957864-13.342784 66.713918 6.82654l77.264026 94.950972c16.445757 20.169324 13.342784 49.957864-6.82654 66.713919l-199.521161 162.595782c-20.169324 16.445757-50.268162 13.342784-66.713918-6.82654zM646.163301 1020.58673l-94.950973-79.746405c51.509351-61.438864 77.574324-137.771999 72.919865-215.346322-4.344162-76.022837-37.85627-143.977945-94.330378-191.143134l79.746405-94.950972c82.849378 69.506594 131.87635 168.491431 138.392593 278.957268 6.205946 109.224648-29.78854 216.587512-101.777512 302.229565z"
p-id="41532"></path>
</svg>
)
})

View File

@ -0,0 +1,24 @@
import React from "react"
export const CollectIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="73631"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}
>
<path
d="M536.934 860.314c-26.828-14.797-70.502-14.695-97.177 0L251.238 964.096c-53.606 29.542-88.78 2.56-78.54-59.802l35.993-219.801c5.12-31.335-8.448-74.752-30.054-96.768L26.163 431.975c-43.417-44.289-29.696-87.655 30.003-96.769l210.74-32c30.003-4.608 65.28-31.539 78.643-59.852l94.259-199.936c26.829-56.884 70.451-56.679 97.126 0l94.208 199.987c13.466 28.467 48.896 55.296 78.695 59.853l210.739 32.05c60.006 9.114 73.216 52.583 30.054 96.718L798.106 587.674c-21.71 22.17-35.124 65.69-30.055 96.819l35.994 219.801c10.24 62.567-25.14 89.19-78.541 59.802l-188.57-103.782z"
p-id="73632"></path>
</svg>
)
})

View File

@ -0,0 +1,24 @@
import React from "react"
export const DataProjectIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M669.538462 315.076923a185.659077 185.659077 0 0 1 122.171076 325.474462 354.500923 354.500923 0 0 1 232.093539 321.378461l0.196923 11.421539c0 27.963077-22.685538 50.648615-50.688 50.648615H365.764923a50.688 50.688 0 0 1-50.412308-45.489231L315.076923 973.312a354.579692 354.579692 0 0 1 232.290462-332.8A185.659077 185.659077 0 0 1 669.538462 315.076923z m-263.404308 161.910154a267.815385 267.815385 0 0 0-1.024 23.748923l0.196923 11.027692c1.378462 32.846769 8.782769 64.630154 21.464615 93.971693l2.087385 4.489846v2.756923l-1.220923 0.866461A433.742769 433.742769 0 0 0 249.619692 866.461538H126.936615A87.512615 87.512615 0 0 1 39.384615 778.948923v-214.449231c0-48.324923 39.187692-87.512615 87.512616-87.512615h279.236923zM341.346462 0c48.324923 0 87.512615 39.187692 87.512615 87.512615V389.513846H126.897231A87.512615 87.512615 0 0 1 39.384615 301.961846V87.512615C39.384615 39.187692 78.572308 0 126.897231 0h214.449231z m476.947692 0C866.697846 0 905.846154 39.187692 905.846154 87.512615v294.4A264.428308 264.428308 0 0 0 516.332308 285.144615V87.512615C516.371692 39.187692 555.559385 0 603.884308 0h214.44923z"
p-id="11293"></path>
<path
d="M24.024615 24.615385L23.630769 22.646154l0.393846 1.969231zM24.024615 1001.353846l-0.393846-1.969231 0.393846 1.969231zM1000.763077 24.615385l-0.393846-1.969231 0.393846 1.969231zM1000.763077 1001.353846l-0.393846-1.969231 0.393846 1.969231z"
p-id="11294"></path>
</svg>
)
})

View File

@ -0,0 +1,27 @@
import React from "react"
export const DatasetIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1153 1024"
version="1.1"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M849.992624 307.054752c-56.549976 0-109.613995 0.871489-162.581182 0a39.701182 39.701182 0 0 0-40.282175 23.239716c-15.29948 27.403499-31.857778 54.226005-49.190733 80.370686A36.118392 36.118392 0 0 1 576.054468 426.061466C416.378251 441.167281 257.670355 439.908463 104.966052 380.35669a323.419385 323.419385 0 0 1-45.123782-22.852387C19.850591 334.167754-2.420804 300.857494 0.871489 252.344586a735.92435 735.92435 0 0 0 0-77.465721A98.478298 98.478298 0 0 1 46.285768 87.148936c51.030544-36.312057 109.80766-53.257683 169.940426-65.071206C384.810969-11.329362 552.427423-8.811726 716.55792 44.252293a419.767376 419.767376 0 0 1 93.927186 48.416076c28.274988 18.01078 39.894846 46.963593 39.313854 81.04851-0.387329 41.444161 0.193664 83.178818 0.193664 133.337873zM724.594988 957.766809c-52.967187 10.554704-105.740709 22.755556-159.095224 31.373617a981.587518 981.587518 0 0 1-293.595083 0 625.535697 625.535697 0 0 1-193.664303-55.581655c-52.289362-25.951017-82.791489-63.618723-77.465721-125.881797 3.195461-40.088511 0.580993-80.56435 0.580993-122.976832 58.099291 78.724539 144.279905 96.832151 229.879527 113.487281 111.16331 21.399905 223.391773 28.274988 335.717069 5.713097 14.137494-2.808132 21.496738 2.517636 28.178156 15.202648 16.267801 30.59896 35.634232 59.454941 51.417872 90.150733a58.099291 58.099291 0 0 0 50.159055 34.762742 189.50052 189.50052 0 0 1 27.113002 6.29409zM1.161986 397.689645c25.66052 44.54279 61.972577 65.071206 101.092766 81.242175A723.820331 723.820331 0 0 0 358.27896 531.027518c65.749031 1.936643 131.691726-5.616265 197.537588-7.456076a35.5374 35.5374 0 0 1 26.338346 13.556501 751.514326 751.514326 0 0 1 45.414279 79.692861c3.58279 7.262411 0 18.301277-1.258818 27.500331 0 2.808132-3.970118 5.035272-5.422601 7.843404-19.36643 38.732861-49.481229 56.646809-94.701844 60.32643-142.343262 11.426194-281.491064 1.936643-416.37825-47.350922a320.804917 320.804917 0 0 1-43.574469-20.818912C21.980898 619.725768-2.808132 584.575697 0.580993 530.737021c2.7113-41.734657 0.580993-83.856643 0.580993-133.047376zM1017.899574 477.57617c0-32.148274-0.774657-59.551773 0.580993-87.148936 0-6.100426 7.746572-12.104019 12.685012-17.429787 11.135697-11.910355 13.846998-26.822506 1.258818-35.731064-7.64974-5.4226-30.59896-4.454279-33.213428 0.580993-15.202648 29.049645-45.607943 34.375414-68.653995 50.546383a312.864681 312.864681 0 0 1-30.017967 17.139291c-9.683215-15.29948-19.36643-29.72747-27.306667-45.123783a21.399905 21.399905 0 0 1 1.646147-17.429787c18.591773-33.794421 37.474043-67.782506 58.099291-100.511773a32.729267 32.729267 0 0 1 22.561891-13.556501c37.086714-1.35565 74.27026-1.452482 111.260142 0a35.731064 35.731064 0 0 1 24.789031 14.137494c20.818913 31.567281 40.572671 64.006052 58.873948 96.832151a35.150071 35.150071 0 0 1 0 27.984492c-15.008983 29.049645-33.213428 57.227801-48.416076 86.567943-8.908558 17.429787-20.334752 25.176359-39.894846 23.046052a405.242553 405.242553 0 0 0-44.252294 0.096832zM900.539007 842.439716c28.37182 16.267801 55.000662 30.986288 80.951679 46.866762 4.551111 2.808132 5.906761 10.457872 9.683215 14.912151a76.594232 76.594232 0 0 0 24.014373 22.271395 25.370024 25.370024 0 0 0 20.915745-12.781844 72.430449 72.430449 0 0 0-8.811726-31.179953c-2.323972-6.197258-9.102222-11.523026-9.683215-17.429787-0.968322-29.049645 0-58.099291 0-90.44123 19.36643 0 38.055035-0.677825 56.162648 0a22.271395 22.271395 0 0 1 14.331158 10.264208c20.141087 33.891253 39.991678 67.782506 58.099291 102.738913a29.049645 29.049645 0 0 1-0.580993 23.917541c-17.720284 33.31026-36.312057 66.330024-56.25948 98.478298a32.148274 32.148274 0 0 1-22.658723 12.491348q-55.484823 2.033475-111.16331 0a32.148274 32.148274 0 0 1-22.368227-12.685012q-30.405296-47.931915-57.324633-97.994137a34.084917 34.084917 0 0 1 0-25.854185A401.272435 401.272435 0 0 1 900.539007 842.439716z"
p-id="6804"></path>
<path
d="M875.362648 419.96104c-26.628842 15.493144-51.32104 30.308463-76.691064 43.961797-4.551111 2.420804-12.200851-1.646147-17.817116 0-10.748369 2.517636-24.692199 4.06695-30.308463 11.329362-4.357447 5.713097 1.258818 19.36643 3.776454 29.630638 1.742979 7.165579 8.037069 13.750165 8.327565 20.72208 0.968322 29.049645 0 58.099291 0 90.92539-19.36643 0-37.667707 0.968322-55.678487-0.580993a25.176359 25.176359 0 0 1-15.008984-12.781844c-19.36643-34.278582-38.732861-68.653995-56.549976-103.900898a36.118392 36.118392 0 0 1 0.774657-28.37182c16.267801-31.373617 33.600757-62.359905 52.870355-91.990544a40.669504 40.669504 0 0 1 27.790827-16.267801c32.051442-2.033475 64.393381 0 96.832151-1.258818 20.334752-1.065154 33.794421 4.551111 41.928322 23.820709a381.22818 381.22818 0 0 0 19.753759 34.762742zM763.037352 639.092199c0 33.503924 0.774657 62.55357 0 91.40955 0 7.64974-8.424397 14.621655-10.36104 22.561892s-6.003593 22.658723-1.452482 27.984491 18.591773 7.262411 29.049645 9.00539a242.080378 242.080378 0 0 1 32.826099 3.292294 134.790355 134.790355 0 0 1 31.664114 17.332955c10.845201 7.359243 30.695792 18.785437 29.049645 23.530212a193.664303 193.664303 0 0 1-27.113002 50.352719c-2.130307 3.195461-10.264208 3.292293-15.589976 3.292293-38.732861 0-77.465721 1.065154-116.198582 0a34.375414 34.375414 0 0 1-23.723877-14.524822c-19.36643-30.695792-37.474043-62.069409-53.838676-94.314516a38.732861 38.732861 0 0 1 0-30.405295c14.427991-29.049645 32.632435-55.581655 46.866761-84.340804 9.683215-19.36643 21.303073-28.274988 43.090307-25.079527A246.050496 246.050496 0 0 0 763.037352 639.092199zM1017.899574 752.095319c0-31.373617-0.580993-58.680284 0-85.890118a30.017967 30.017967 0 0 1 9.683216-17.139291c15.589976-14.912151 15.686809-28.178156 1.452482-42.993475a40.088511 40.088511 0 0 1-11.038865-23.433381c-1.452482-25.466856-0.580993-51.127376-0.580993-79.983357 20.72208 0 38.732861-1.161986 57.130969 0.677825a29.630638 29.630638 0 0 1 17.236123 14.137495c19.36643 31.276785 39.410686 62.747234 56.937305 95.282836a38.055035 38.055035 0 0 1 0 30.211632c-15.202648 30.114799-33.891253 58.099291-49.384397 88.504586-8.037069 15.589976-17.623452 22.94922-35.5374 21.012577a450.075839 450.075839 0 0 0-45.89844-0.387329z"
p-id="6805"></path>
<path
d="M875.45948 551.943262c-32.535603 68.84766-35.924728 70.881135-103.51357 62.650402 0-31.276785-1.161986-63.328227 1.258818-95.089172 0-4.744775 20.334752-14.234326 26.435177-11.329362 25.951017 11.813522 50.159054 27.984492 75.819575 43.768132zM879.816927 697.966147c-27.693995 16.461466-54.03234 33.213428-81.532672 47.641418-14.427991 7.64974-25.66052 0.580993-26.338345-16.074137-1.161986-24.885863 0-49.771726 0-74.754421a19.947423 19.947423 0 0 1 4.066951-13.266005c10.554704-10.264208 71.074799 0 78.724539 12.58818s15.783641 27.693995 25.079527 43.864965zM907.510922 834.693144c39.410686-73.398771 16.945626-60.035934 102.15792-58.099291 0 31.470449 0.580993 63.134563-0.968322 94.701844 0 3.292293-15.493144 10.457872-20.044255 8.230733-27.209835-13.750165-53.354515-29.340142-81.145343-44.833286zM909.060236 416.378251c25.079527-14.912151 48.997069-29.533806 73.398771-43.283972 15.686809-8.908558 25.757352 0 26.725674 14.427991 1.936643 29.049645 0.580993 57.518298 0.580993 85.599621-56.356312 14.331158-71.55896 5.809929-100.705438-56.74364zM1009.765674 504.882837v87.148936c0 14.234326-9.683215 18.688605-21.399906 12.200851-25.854184-14.234326-51.127376-29.824303-76.206903-44.542789 14.331158-54.613333 29.921135-63.618723 97.606809-54.806998zM904.218629 568.114232c26.628842 15.880473 52.095697 31.083121 77.465721 46.479432 11.329362 6.778251 12.394515 14.718487 0.677825 21.884066-25.273191 15.589976-50.836879 30.695792-75.819575 45.704776-42.025154-43.090307-42.509314-58.680284-2.323971-114.068274zM1009.765674 749.965012c-64.780709 7.359243-66.233191 6.487754-99.252955-55.097494 26.628842-15.29948 53.160851-31.276785 80.56435-45.704776 10.845201-5.616265 18.688605-0.580993 18.688605 12.975509zM883.883877 709.973333c39.798014 60.616927 38.732861 46.479433-1.258818 110.098156L788.213712 764.973995zM791.31234 488.421371c7.359243-5.809929 10.264208-8.618061 13.556502-10.651536 24.789031-14.718487 49.674894-29.049645 74.27026-43.671301 36.796217 38.151868 37.086714 54.322837 0.580993 106.999527z"
p-id="6806"></path>
</svg>
)
})

View File

@ -0,0 +1,33 @@
import React from "react"
export const IodIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1088 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
fillRule="evenodd"
p-id="32289"
{...props}
ref={ref}
>
<path
d="M853.333333 458.666667h21.333334v21.333333h-21.333334zM680.533333 217.6h21.333334v21.333333h-21.333334zM740.266667 264.533333h21.333333v21.333334h-21.333333zM629.333333 177.066667h21.333334v21.333333h-21.333334zM398.933333 177.066667h21.333334v21.333333h-21.333334zM343.466667 217.6h21.333333v21.333333h-21.333333zM285.866667 264.533333h21.333333v21.333334h-21.333333zM174.933333 458.666667h21.333334v21.333333h-21.333334zM174.933333 520.533333h21.333334v21.333334h-21.333334zM174.933333 576h21.333334v21.333333h-21.333334zM292.266667 759.466667h21.333333v21.333333h-21.333333zM347.733333 800h21.333334v21.333333h-21.333334zM403.2 846.933333h21.333333v21.333334h-21.333333zM629.333333 846.933333h21.333334v21.333334h-21.333334zM454.4 514.133333h21.333333v21.333334h-21.333333zM512 514.133333h21.333333v21.333334h-21.333333zM567.466667 514.133333h21.333333v21.333334h-21.333333zM680.533333 800h21.333334v21.333333h-21.333334zM740.266667 759.466667h21.333333v21.333333h-21.333333zM853.333333 520.533333h21.333334v21.333334h-21.333334zM853.333333 576h21.333334v21.333333h-21.333334zM509.866667 189.866667h21.333333v147.2h-21.333333zM509.866667 708.266667h21.333333v147.2h-21.333333zM225.92 672.170667l127.488-73.6 10.666667 18.474666-127.488 73.6zM223.509333 375.125333l10.666667-18.474666 127.466667 73.6-10.666667 18.474666zM677.312 422.186667l132.309333-64.554667 9.344 19.2-132.309333 64.512zM684.544 618.517333l10.986667-18.282666 126.186666 75.797333-10.986666 18.304z"
p-id="32290"></path>
<path
d="M520.533333 590.933333c-93.866667 0-189.866667-23.466667-189.866666-66.133333s96-66.133333 189.866666-66.133333 189.866667 23.466667 189.866667 66.133333-93.866667 66.133333-189.866667 66.133333z m0-110.933333c-102.4 0-168.533333 27.733333-168.533333 44.8s66.133333 44.8 168.533333 44.8c102.4 0 168.533333-27.733333 168.533334-44.8s-64-44.8-168.533334-44.8z"
p-id="32291"></path>
<path
d="M520.533333 714.666667c-36.266667 0-57.6-68.266667-64-130.133334l21.333334-2.133333c8.533333 76.8 29.866667 110.933333 42.666666 110.933333 12.8 0 34.133333-34.133333 42.666667-113.066666l21.333333 2.133333c-6.4 64-25.6 132.266667-64 132.266667z m44.8-243.2c-8.533333-78.933333-29.866667-115.2-42.666666-115.2-12.8 0-34.133333 36.266667-42.666667 113.066666l-21.333333-2.133333c6.4-64 27.733333-132.266667 64-132.266667 38.4 0 57.6 70.4 64 134.4l-21.333334 2.133334zM520.533333 209.066667c-36.266667 0-66.133333-29.866667-66.133333-66.133334 0-36.266667 29.866667-66.133333 66.133333-66.133333 36.266667 0 66.133333 29.866667 66.133334 66.133333 2.133333 36.266667-27.733333 66.133333-66.133334 66.133334z m0-113.066667c-25.6 0-44.8 21.333333-44.8 44.8 0 25.6 21.333333 44.8 44.8 44.8 25.6 0 44.8-21.333333 44.8-44.8 2.133333-23.466667-19.2-44.8-44.8-44.8zM857.6 407.466667c-36.266667 0-66.133333-29.866667-66.133333-66.133334 0-36.266667 29.866667-66.133333 66.133333-66.133333s66.133333 29.866667 66.133333 66.133333c2.133333 36.266667-27.733333 66.133333-66.133333 66.133334z m0-113.066667c-25.6 0-44.8 21.333333-44.8 44.8 0 25.6 21.333333 44.8 44.8 44.8s44.8-21.333333 44.8-44.8c2.133333-23.466667-19.2-44.8-44.8-44.8zM857.6 776.533333c-36.266667 0-66.133333-29.866667-66.133333-66.133333 0-36.266667 29.866667-66.133333 66.133333-66.133333s66.133333 29.866667 66.133333 66.133333c2.133333 34.133333-27.733333 66.133333-66.133333 66.133333z m0-113.066666c-25.6 0-44.8 21.333333-44.8 44.8s21.333333 44.8 44.8 44.8 44.8-21.333333 44.8-44.8-19.2-44.8-44.8-44.8zM520.533333 974.933333c-36.266667 0-66.133333-29.866667-66.133333-66.133333 0-36.266667 29.866667-66.133333 66.133333-66.133333 36.266667 0 66.133333 29.866667 66.133334 66.133333 2.133333 36.266667-27.733333 66.133333-66.133334 66.133333z m0-113.066666c-25.6 0-44.8 21.333333-44.8 44.8s21.333333 44.8 44.8 44.8c25.6 0 44.8-21.333333 44.8-44.8s-19.2-44.8-44.8-44.8zM183.466667 407.466667c-36.266667 0-66.133333-29.866667-66.133334-66.133334 0-36.266667 29.866667-66.133333 66.133334-66.133333 36.266667 0 66.133333 29.866667 66.133333 66.133333 2.133333 36.266667-27.733333 66.133333-66.133333 66.133334z m0-113.066667c-25.6 0-44.8 21.333333-44.8 44.8 0 25.6 21.333333 44.8 44.8 44.8 25.6 0 44.8-21.333333 44.8-44.8 2.133333-23.466667-19.2-44.8-44.8-44.8zM183.466667 776.533333c-36.266667 0-66.133333-29.866667-66.133334-66.133333 0-36.266667 29.866667-66.133333 66.133334-66.133333 36.266667 0 66.133333 29.866667 66.133333 66.133333 2.133333 34.133333-27.733333 66.133333-66.133333 66.133333z m0-113.066666c-25.6 0-44.8 21.333333-44.8 44.8s21.333333 44.8 44.8 44.8c25.6 0 44.8-21.333333 44.8-44.8s-19.2-44.8-44.8-44.8z"
p-id="32292"></path>
<path
d="M514.133333 731.733333c-117.333333 0-215.466667-96-215.466666-215.466666 0-117.333333 96-215.466667 215.466666-215.466667 117.333333 0 215.466667 96 215.466667 215.466667s-96 215.466667-215.466667 215.466666z m0-386.133333c-93.866667 0-172.8 76.8-172.8 172.8s76.8 172.8 172.8 172.8c93.866667 0 172.8-76.8 172.8-172.8s-76.8-172.8-172.8-172.8z"
p-id="32293"></path>
</svg>
)
})

View File

@ -0,0 +1,22 @@
import React from "react"
export const MedicineBottleFillIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
p-id="25925"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M725.333333 213.333333v85.333334a128 128 0 0 1 128 128v469.333333a42.666667 42.666667 0 0 1-42.666666 42.666667H213.333333a42.666667 42.666667 0 0 1-42.666666-42.666667V426.666667a128 128 0 0 1 128-128V213.333333h426.666666z m-170.666666 256h-85.333334v85.333334H384v85.333333h85.290667L469.333333 725.333333h85.333334l-0.042667-85.333333H640v-85.333333h-85.333333v-85.333334z m256-384v85.333334H213.333333V85.333333h597.333334z"
p-id="25926"></path>
</svg>
)
})

View File

@ -0,0 +1,26 @@
import React from "react"
export const NSDCIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M482.233219 569.858314a74.216 74.216 0 1 0 56.802467-137.133287 74.216 74.216 0 1 0-56.802467 137.133287Z"
fill=""
p-id="7898"></path>
<path
d="M952.472 447.816c-25.08-25.224-59-45.952-99.736-63.784-81.616-35.808-191.784-59.872-317.32-66.536a1387.864 1387.864 0 0 0-49-1.16c28.56-38.272 56.824-72.624 83.496-101.04 52.328-55.376 100.896-84.224 123.216-85.088 9.712-0.288 15.224 1.448 19.856 4.352 9.568 6.376 22.176 21.456 21.456 47.256-0.584 19.856-8.984 58.128-19.568 81.76l67.696 30.152c15.656-34.648 25.08-75.528 26.096-109.88 1.304-50.88-23.776-91.032-55.232-111.472-18.992-12.176-41.312-17.104-63.056-16.384-62.912 2.464-116.984 47.544-174.384 108.432-40.592 42.912-81.904 96.112-122.64 154.24-28.264 2.176-55.952 5.072-82.336 9.28-13.48-45.52-22.472-86.544-23.048-115.096-0.432-25.656 2.176-46.824 6.52-60.304 4.496-13.624 8.984-17.688 12.464-19.424 2.752-1.448 5.656-2.32 10-2.608 20.008-1.592 60.88 6.96 100.312 43.776l50.592-54.216C421.32 76.584 370.88 57.88 328.984 56a233.76 233.76 0 0 0-17.544 0.288l-0.144 0.144a103.392 103.392 0 0 0-38.128 10.44v0.144c-25.512 12.904-41.024 37.256-49.288 62.624-8.264 25.512-10.584 53.632-10.144 84.368 0.728 38.128 9.568 82.624 22.616 129.016-22.32 5.656-43.488 12.032-63.056 19.136-39.576 14.496-73.208 31.744-99.008 53.632-25.952 21.888-46.096 50.88-46.096 85.528 0 39.864 21.6 72.912 47.544 95.672 26.096 22.904 57.256 38.416 87.408 50.304l26.96-69.288c-25.08-9.712-49.432-22.76-65.376-36.672-15.8-13.92-22.328-25.8-22.328-40.008 0-5.8 3.624-15.368 19.568-28.848 15.944-13.336 42.76-28.12 76.832-40.592 17.976-6.52 38.272-12.464 59.872-17.688 9.424 25.8 19.856 51.464 30.584 76.688-57.84 123.216-76.832 241.216-75.528 316.736 0.872 50.736 19.424 95.96 56.968 120.32 24.352 15.656 55.232 23.48 87.408 19.136 32.04-4.352 64.656-19.424 99.736-44.504l-42.912-60.304c-28.704 20.44-51.168 29.136-66.976 31.312-15.8 2.032-25.656-0.576-36.816-7.976-11.888-7.68-22.616-24.936-23.192-59.288-0.872-50.448 10.584-133.512 43.632-223.968 6.088 11.888 12.032 23.632 17.976 34.648h-0.144c39.576 73.352 99.008 161.2 161.2 228.168 31.024 33.488 62.48 61.752 95.096 80.6 31.6 18.12 69.144 27.832 103.792 12.176 30.584-10.584 49.576-37.832 58.856-67.264 9.568-30.728 12.032-66.68 9.424-107.416-5.512-81.32-33.048-181.344-86.104-279.488l-65.232 35.08c47.984 89.152 72.624 180.912 77.264 249.336 2.32 34.352-0.728 62.768-6.232 80.16-5.36 17.392-10.872 20.152-11.6 20.44l-3.48 0.872-3.04 1.592c-4.208 2.176-14.784 2.752-36.528-9.856-21.888-12.608-49.72-36.528-77.696-66.68C509.04 734.256 451.2 649.312 414.96 581.904c-14.352-26.672-29.136-57.552-43.488-90.312a694.256 694.256 0 0 1 33.632-57.84c9.28-14.496 18.848-28.416 28.416-42.472 10.872-0.288 21.6-1.304 32.616-1.304 21.312 0 43.056 0.584 65.376 1.736 118.288 6.088 221.496 29.568 291.512 60.304 34.936 15.224 61.32 32.472 76.832 48.128 15.656 15.656 19.568 27.104 18.992 36.384 0 2.464-4.352 13.048-22.032 24.936-17.832 11.888-46.384 23.336-80.888 29.568l12.904 73.064c42.76-7.68 80.016-21.456 109.152-41.024 29.28-19.568 53.2-46.68 55.088-82.776 1.728-35.224-15.52-67.408-40.6-92.48z m-615.936-43.784a34.84 34.84 0 0 1-1.592-4.2c1.448-0.144 2.896-0.288 4.208-0.432-0.88 1.592-1.752 3.04-2.616 4.632z"
fill=""
p-id="7899"></path>
</svg>
)
})

View File

@ -0,0 +1,23 @@
import React from "react"
export const NewBottleIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="40222"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M848.818342 511.548501l-319.661376 308.373898c-14.899471 16.705467-35.216931 27.541446-56.888888 30.70194 16.705467-17.608466 29.798942-38.828924 37.474426-61.40388l175.633157-164.345679-105.199294-105.650794L717.883598 397.770723c4.96649-4.514991 8.126984-10.835979 8.126984-17.608466s-3.160494-13.093474-8.126984-17.608465c-9.029982-10.38448-24.832451-11.738977-35.216931-2.708995L542.250441 478.589065l-30.70194-30.70194v-30.70194L632.098765 295.731922s92.557319-92.557319 199.562611 13.544974c107.45679 106.102293 16.253968 201.820106 16.253968 201.820106h0.902998z m-339.075838 74.948853v-74.948853l30.70194-30.70194 38.828925 38.828924-69.530865 66.821869z m-200.465608 294.828925C216.719577 881.326279 139.964727 819.470899 139.964727 758.067019v-492.134038c0-61.40388 80.818342-123.259259 169.312169-123.25926S478.589065 204.077601 478.589065 265.932981v492.134038c0 61.40388-76.75485 123.259259-169.312169 123.25926zM447.887125 263.223986C425.763668 206.335097 370.229277 169.312169 309.276896 170.666667c-60.952381-1.354497-116.938272 35.216931-139.061728 92.557319v246.067019h61.40388v215.816579c-1.354497 11.738977 4.514991 23.026455 14.447971 29.347442 9.932981 6.320988 23.026455 6.320988 32.959436 0s15.802469-17.608466 14.447972-29.347442v-213.559083h153.961199l0.451499-248.324515z m-184.663139 30.70194c8.126984 0 16.253968 3.160494 22.123457 9.029982 5.869489 5.869489 9.029982 13.996473 9.029982 22.123457v184.663139H232.522046V326.433862c0-8.126984 2.708995-16.253968 9.029982-22.123456 5.869489-5.869489 13.996473-9.029982 22.123457-9.029983l-0.451499-1.354497z m0 0"
p-id="40223"></path>
</svg>
)
})

View File

@ -0,0 +1,23 @@
import React from "react"
export const NotCollectIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1059 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="73488"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M253.488042 1024c-16.9 0-33.2875-5.1125-47.6125-15.3625-26.625-18.425-39.425-49.6625-34.3125-81.925l40.9625-251.9c1.5375-10.2375-1.5375-20.475-8.7-27.65L28.213042 466.4375c-22.0125-22.525-29.1875-55.3-19.45-84.9875 9.725-29.7 35.325-51.2 66.05-55.8125l237.575-36.35c10.75-1.5375 19.4625-8.1875 24.0625-17.925L441.388042 48.125c13.825-29.7 42.5-48.125 75.2625-48.125s61.4375 18.4375 75.2625 48.125l104.45 223.2375c4.6125 9.725 13.825 16.375 24.0625 17.925L958.000542 325.625a82.355 82.355 0 0 1 66.05 55.8125c10.2375 29.7 2.5625 62.4625-19.45 84.9875l-175.625 180.7375c-7.1625 7.175-10.2375 17.925-8.7 27.65l40.9625 251.9c5.125 31.75-8.1875 63.4875-34.3 81.925-26.1125 18.4375-59.9 20.4875-88.0625 4.6125l-206.85-114.6875c-9.725-5.1125-20.9875-5.1125-30.7125 0l-207.3625 115.2c-12.8125 6.65-26.6375 10.2375-40.4625 10.2375zM516.650542 51.2c-12.8 0-23.55 7.1625-29.1875 18.4375L383.525542 292.875c-11.775 25.0875-35.325 43.0125-62.975 47.1l-237.575 36.35c-12.2875 2.05-21.5 9.7375-25.6 21.5-4.1 11.775-1.025 24.0625 7.675 32.775L240.688042 611.325c18.4375 18.95 26.625 45.5625 22.525 71.675L222.250542 934.9125c-2.05 12.8 3.075 24.575 13.3125 31.7375 10.2375 7.175 23.0375 7.6875 33.7875 1.5375l207.3625-115.2c25.0875-13.825 55.3-13.825 80.3875 0l207.3625 115.2c10.75 6.1375 23.55 5.625 33.8-1.5375 10.2375-7.1625 15.3625-18.95 13.3125-31.7375L770.625542 683.0125c-4.1-26.1125 4.1-52.7375 22.525-71.675l175.625-180.7375c8.7-8.7 11.2625-20.9875 7.675-32.775-4.0875-11.775-13.3125-19.9625-25.6-21.5l-237.5625-36.35c-27.65-4.0875-51.2-22.0125-62.975-47.1L545.838042 69.6375c-5.625-11.2625-16.375-18.4375-29.1875-18.4375z m0 0"
p-id="73489"></path>
</svg>
)
})

View File

@ -0,0 +1,24 @@
import React from "react"
export const ResearchInstitutesIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M977.997713 356.640616l-433.994529-149.255836c-27.084774-9.776045-21.395041-9.833246-48.245647-0.11619L59.169291 355.289237c-26.854181 9.717056-26.682578 25.531403 0.402196 35.336048l103.741547 35.223434c-45.948661 44.509693-48.937424 90.77296-49.513011 144.425909-17.825328 6.844482-30.363118 24.094223-30.363118 44.2505 0 18.515317 10.580437 34.531656 26.051577 42.325321-7.30388 54.571743-28.409339 116.85135-90.284962 190.658788 30.650912 23.692027 46.408058 31.600094 70.100085 39.479561 86.488232-37.150399 75.964996-135.85824 69.234916-234.076295 11.905002-8.626658 19.611078-22.601629 19.611078-38.387376 0-16.933346-8.912664-31.744885-22.195858-40.139163 1.494382-52.587576 12.938199-99.657023 52.27297-130.107731 0.344995-0.832993 1.206588-1.52477 2.93335-2.24336l298.340069-120.53189c11.098823-4.458119 23.634826 0.920582 28.062557 12.019405l0.402196 0.949183c4.431306 11.070222-0.918794 23.634826-12.017617 28.062557l-252.164391 100.808198 225.655204 76.540584c27.027572 9.802858 21.389678 9.861846 48.188446 0.141215L978.34092 392.007052c26.857756-9.747444 26.684365-25.561791-0.400408-35.337836L977.997713 356.640616zM977.997713 356.640616"
p-id="5109"></path>
<path
d="M498.801714 597.128809l-273.092884-92.610549 0 69.665713c14.260977 13.056177 22.140444 31.802086 22.140444 52.676953 0 18.74591-6.554901 35.797233-17.653724 48.563829 3.621552 10.925431 9.890447 21.622058 18.976502 24.959391 158.946079 87.866423 378.616606 86.88864 555.332599-8.885851 13.109803-10.898618 23.288043-24.412405 23.288043-37.493607l0-153.370748-280.570155 96.611059c-26.798768 9.690243-21.334264 9.659855-48.363624-0.11619L498.801714 597.128809zM498.801714 597.128809"
p-id="5110"></path>
</svg>
)
})

View File

@ -0,0 +1,21 @@
import React from "react"
export const ResearchPaperIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M715.648 196.928V0H308.352v118.144h220.544l11.904 11.52 203.648 196.992 11.904 11.52v528.32H960V236.288h-244.352v-39.36zM756.352 0v196.928H960L756.352 0zM471.296 157.568v236.288h244.352V1024H64V157.568h407.296zM512 354.496h203.648L512 157.568v196.928z m-136.064 47.424H163.328c-6.4 0-11.52 8-11.52 17.792 0 9.792 5.12 17.792 11.52 17.792h212.608c6.4 0 11.52-8 11.52-17.792 0-9.792-5.12-17.792-11.52-17.792z m100.352 189.888H168.512c-9.216 0-16.704 7.936-16.704 17.728 0 9.856 7.488 17.792 16.704 17.792h307.776c9.216 0 16.64-7.936 16.64-17.792 0-9.792-7.424-17.728-16.64-17.728z m-302.336 225.344H581.76c12.224 0 22.144-7.936 22.144-17.728 0-9.856-9.92-17.792-22.144-17.792H173.952c-12.224 0-22.144 7.936-22.144 17.792 0 9.792 9.92 17.728 22.144 17.728z"
p-id="10242"></path>
</svg>
)
})

View File

@ -0,0 +1,22 @@
import React from "react"
export const SettingIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1084 1024"
version="1.1"
p-id="10420"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M1072.147851 406.226367c-6.331285-33.456782-26.762037-55.073399-52.047135-55.073399-0.323417 0-0.651455 0.003081-0.830105 0.009241l-4.655674 0c-73.124722 0-132.618162-59.491899-132.618162-132.618162 0-23.731152 11.447443-50.336101 11.546009-50.565574 13.104573-29.498767 3.023185-65.672257-23.427755-84.127081l-1.601687-1.127342-134.400039-74.661726-1.700252-0.745401c-8.753836-3.805547-18.334698-5.735272-28.479231-5.735272-20.789593 0-41.235746 8.344174-54.683758 22.306575-14.741683 15.216028-65.622973 58.649474-104.721083 58.649474-39.450789 0-90.633935-44.286652-105.438762-59.784516-13.518857-14.247316-34.128258-22.753199-55.127302-22.753199-9.945862 0-19.354234 1.861961-27.958682 5.531982l-1.746455 0.74078-139.141957 76.431283-1.643269 1.139662c-26.537186 18.437884-36.675557 54.579032-23.584845 84.062398 0.115506 0.264895 11.579891 26.725075 11.579891 50.634877 0 73.126262-59.491899 132.618162-132.618162 132.618162l-4.581749 0c-0.318797-0.00616-0.636055-0.01078-0.951772-0.01078-25.260456 0-45.672728 21.618157-52.002472 55.0811-0.462025 2.453354-11.313456 60.622322-11.313456 106.117939 0 45.494078 10.85143 103.659965 11.314996 106.119479 6.334365 33.458322 26.758957 55.076479 52.036353 55.076479 0.320337 0 0.651455-0.00616 0.842426-0.012321l4.655674 0c73.126262 0 132.618162 59.491899 132.618162 132.616622 0 23.760413-11.444363 50.333021-11.546009 50.565574-13.093793 29.474125-3.041666 65.646075 23.395414 84.151722l1.569346 1.093459 131.838879 73.726895 1.675611 0.7377c8.750757 3.84251 18.305437 5.790715 28.397607 5.790715 21.082208 0 41.676209-8.706094 55.0888-23.290689 18.724339-20.347588 69.527086-62.362616 107.04815-62.362616 40.625872 0 92.72537 47.100385 107.759669 63.583903 13.441852 14.831008 34.176001 23.689571 55.470741 23.695731l0.00616 0c9.895039 0 19.27877-1.883523 27.893999-5.598205l1.711034-0.73924 136.659342-75.531873 1.617088-1.128882c26.492523-18.456365 36.601633-54.600594 23.538642-84.016195-0.115506-0.267974-11.595291-27.082374-11.595291-50.67646 0-73.124722 59.49344-132.616622 132.618162-132.616622l4.517066-0.00154c0.300316 0.00616 0.599092 0.009241 0.899409 0.009241 25.331299-0.00154 45.785153-21.619697 52.107197-55.054918 0.112426-0.589852 11.325776-59.507301 11.325776-106.14104C1083.464388 466.640776 1072.609877 408.67356 1072.147851 406.226367zM377.486862 945.656142l-115.32764-64.487932c5.082277-13.052211 15.437801-43.51815 15.437801-75.017486 0-109.382917-84.176364-199.816642-192.587488-208.134635-2.647404-15.427021-8.873963-54.967133-8.873963-85.667166 0-30.65691 6.223479-70.232445 8.869343-85.671786 108.415744-8.311832 192.592108-98.745557 192.592108-208.134635 0-31.416171-10.300081-61.797405-15.371577-74.854236l122.721583-67.40331c0.003081 0 0.00462 0.00154 0.007701 0.00154 4.423121 4.518606 22.121764 22.080182 46.558275 39.493911 39.929754 28.46229 77.952885 42.894416 113.014434 42.894416 34.716571 0 72.437845-14.151831 112.115025-42.06431 24.282503-17.07953 41.896442-34.302288 46.308782-38.74543 0.009241-0.00154 0.018481-0.00462 0.026182-0.00616l118.301542 65.726159c-5.077657 13.055291-15.416239 43.499669-15.416239 74.958962 0 109.389077 84.174824 199.822802 192.590568 208.134635 2.645865 15.462442 8.872423 55.107281 8.872423 85.671786 0 30.687711-6.223479 70.241685-8.869343 85.673326C890.042174 606.334084 805.86427 696.767809 805.86427 806.158426c0 31.450053 10.317022 61.851309 15.393138 74.903519l-119.783103 66.198965c-5.168521-5.490399-22.603811-23.363073-46.740005-41.288109-40.701336-30.224145-79.662378-45.549521-115.800446-45.549521-35.79155 0-74.458435 15.038919-114.927219 44.694774C400.22004 922.554885 382.666163 940.255068 377.486862 945.656142zM731.271848 511.646647c0-105.803762-86.081448-191.88059-191.888289-191.88059-105.803762 0-191.88059 86.076827-191.88059 191.88059 0 105.803762 86.076827 191.882129 191.88059 191.882129C645.19194 703.528777 731.271848 617.450409 731.271848 511.646647zM539.383558 395.903184c63.825696 0 115.751164 51.922387 115.751164 115.743463 0 63.825696-51.925468 115.751164-115.751164 115.751164-63.821076 0-115.743463-51.925468-115.743463-115.751164C423.640095 447.824031 475.562482 395.903184 539.383558 395.903184z"
p-id="10421"></path>
</svg>
)
})

View File

@ -0,0 +1,23 @@
import React from "react"
export const ShareIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="75461"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M1009.777778 503.466667l-443.733334-455.111111c-5.688889-5.688889-11.377778 0-11.377777 5.688888v267.377778C8.533333 409.6 2.844444 918.755556 17.066667 932.977778c0 0 45.511111-48.355556 164.977777-113.777778 85.333333-48.355556 224.711111-85.333333 369.777778-102.4v261.688889c0 8.533333 11.377778 11.377778 14.222222 5.688889l443.733334-480.711111z m-398.222222 358.4v-199.111111l-36.977778-2.844445c-221.866667 8.533333-378.311111 73.955556-497.777778 156.444445 76.8-275.911111 267.377778-403.911111 466.488889-438.044445l68.266667-2.844444v-199.111111l312.888888 312.888888s8.533333 5.688889 8.533334 14.222223-8.533333 14.222222-8.533334 14.222222l-312.888888 344.177778z"
p-id="75462"></path>
</svg>
)
})

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
import React from "react"
export const Ship1Icon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="42511"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M532.48 241.777778h-28.444444a151.608889 151.608889 0 0 0-48.924445 8.533333l-142.222222 76.231111V199.111111a42.382222 42.382222 0 0 1 42.382222-42.097778H455.111111V102.115556a42.666667 42.666667 0 0 1 43.52-42.382223H540.444444a42.666667 42.666667 0 0 1 42.382223 42.382223v59.164444h101.546666a42.666667 42.666667 0 0 1 42.382223 42.382222v131.413334l-156.728889-85.333334a209.351111 209.351111 0 0 0-38.115556-8.533333z m309.191111 461.653333a768 768 0 0 0-220.16 85.333333c-113.777778 63.715556-118.613333 50.915556-207.644444 8.533334a961.422222 961.422222 0 0 0-203.093334-76.231111L151.608889 631.466667c-63.715556-80.497778-76.231111-119.466667-42.382222-139.946667L455.111111 309.475556a127.431111 127.431111 0 0 1 122.88 0l347.022222 182.044444c21.333333 17.066667 38.115556 55.182222-25.315555 139.946667z m122.595556 186.311111a42.097778 42.097778 0 0 1-42.382223 42.097778c-8.248889 0-12.515556 0-16.782222-3.982222a199.111111 199.111111 0 0 0-105.813333-34.133334c-63.431111 0-143.928889 59.448889-143.928889 59.448889S612.977778 995.555556 512 995.555556a220.728889 220.728889 0 0 1-143.928889-42.382223s-89.031111-63.431111-143.928889-59.448889a129.706667 129.706667 0 0 0-106.097778 34.133334 25.884444 25.884444 0 0 1-16.782222 3.982222 42.097778 42.097778 0 0 1-42.382222-42.097778 46.933333 46.933333 0 0 1 21.048889-42.382222s119.182222-136.248889 304.355555 0c0 0 59.448889 42.382222 101.546667 42.382222h42.382222a216.462222 216.462222 0 0 0 101.546667-42.382222c50.915556-33.848889 178.062222-118.613333 304.924444 0a47.502222 47.502222 0 0 1 28.444445 42.382222z m0 0"
p-id="42512"></path>
</svg>
)
})

View File

@ -0,0 +1,24 @@
import React from "react"
export const TalentPoolIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}>
<path
d="M194.7 296.1H571v50.6H194.7zM679.7 294.1c-15 1.9-26.2 13.1-26.2 28.1v1.9c1.8 15 13.1 26.2 28.1 26.2 15-1.9 26.2-13.2 26.2-28.1v-1.9c-1.9-14.9-13.2-26.2-28.1-26.2z"
p-id="19241"></path>
<path
d="M932.4 86.4c-18.7-15-43.1-22.5-67.4-22.5H279c-26.2 0-50.5 7.5-67.4 22.5-18.7 16.9-29.9 39.4-29.9 61.9v41H163c-26.2 0-50.5 7.5-67.4 22.4C75 230.5 63.8 253 63.8 277.4v598c0 24.3 11.2 46.8 29.9 61.8s43.1 22.5 67.4 22.5h584.2c26.2 0 50.6-7.5 67.4-22.5 18.7-16.8 31.8-39.3 31.8-61.8v-30h16.9c26.2 0 50.5-7.5 67.4-22.5 18.7-16.8 30-39.4 30-61.8v-611c3.6-24.4-7.6-46.8-26.4-63.7z m-345.5 711c-2.3 48.8-57.3 48.8-138.8 49-81.5-0.2-136.5-0.2-138.8-49-2.3-48.8 30-90.6 54.9-111 24.9-20.4 43-19.4 43-19.4l40.9-0.1 40.9 0.1s18.2-0.9 43 19.4c24.9 20.4 57.2 62.2 54.9 111zM368.5 578.5c0-43.9 35.6-79.6 79.6-79.6s79.6 35.6 79.6 79.6S492 658 448.1 658s-79.6-35.6-79.6-79.5z m412.3-197.9H128.2v-86.3c0-18.8 2.5-40.8 40.7-40.8h575.2c14.2 0 36.7 15 36.7 33.8v93.3z m117.1 363.6c0 3.8-6.4 22.5-18.6 31-12.2 8.4-30.1 6.5-32 6.5h-2.9V277.3c0-34.3-12.2-86.2-102.8-86.2H242.3v-30c0-3.8 0-7.5 1.9-11.3 1.9-3.8 3.8-5.6 7.5-9.4 9.4-9.4 22.5-15 39.3-15h548.2c26.2 0 58.7 15 58.7 33.7v585.1z"
p-id="19242"></path>
</svg>
)
})

View File

@ -0,0 +1,22 @@
import React from "react"
export const TechCompanyIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
className="icon"
viewBox="0 0 1024 1024"
version="1.1"
fill="currentColor"
fillRule="evenodd"
ref={ref}
{...props}
>
<path
d="M968 905.6h-48.64V357.376c0-36.992-28.608-66.816-63.808-66.816h-128v615.168h-48V135.68c0-36.864-28.544-66.816-63.872-66.816H168.448c-35.328 0-63.872 29.952-63.872 66.816v769.92H56a24.32 24.32 0 0 0-24 24.64 24.32 24.32 0 0 0 24 24.64h576.128v0.128h287.168v-0.128h48.64a24.32 24.32 0 0 0 24.064-24.64 24.32 24.32 0 0 0-24-24.64zM440.192 265.92h95.808v73.856H440.192V265.856z m0 196.864h95.808v73.856H440.192V462.72z m0 196.928h95.808v73.856H440.192v-73.856z m-192-393.792h96v73.856h-96V265.856z m0 196.864h96v73.856h-96V462.72z m0 196.928h96v73.856h-96v-73.856z"
p-id="9056"></path>
</svg>
)
})

View File

@ -1,248 +1,157 @@
import { useStorage } from "@plasmohq/storage/hook" import React, { useMemo, useState } from "react"
import { import { useOptionLayoutContext } from "@/components/Layouts/Layout.tsx"
BrainCog, import { PanelLeftIcon } from "lucide-react"
ChevronLeft, import { Button, Tooltip } from "antd"
ChevronRight, import { PlusOutlined } from "@ant-design/icons"
CogIcon, import { useMessageOption } from "@/hooks/useMessageOption.tsx"
ComputerIcon,
GithubIcon,
PanelLeftIcon,
ZapIcon
} from "lucide-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useLocation, NavLink } from "react-router-dom" import { NavLink, useLocation } from "react-router-dom"
import { SelectedKnowledge } from "../Option/Knowledge/SelectedKnowledge" import logo from "@/assets/logo.png"
import { ModelSelect } from "../Common/ModelSelect" import { BellIcon } from "@/components/Icons/Bell.tsx"
import { PromptSelect } from "../Common/PromptSelect" import { ShareIcon } from "@/components/Icons/Share.tsx"
import { useQuery } from "@tanstack/react-query" import { NotCollectIcon } from "@/components/Icons/NotCollect.tsx"
import { fetchChatModels } from "~/services/ollama" import { CollectIcon } from "@/components/Icons/Collect.tsx"
import { useMessageOption } from "~/hooks/useMessageOption" import { SettingIcon } from "@/components/Icons/Setting.tsx"
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> = ({ type Props = {}
setOpenModelSettings,
setSidebarOpen
}) => {
const { t, i18n } = useTranslation(["option", "common"])
const isRTL = i18n?.dir() === "rtl"
const [shareModeEnabled] = useStorage("shareMode", false) export const Header: React.FC<Props> = ({}) => {
const [hideCurrentChatModelSettings] = useStorage( const location = useLocation()
"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({ const { showOptionSidebar, setShowOptionSidebar } = useOptionLayoutContext()
queryKey: ["fetchAllPromptsLayout"],
queryFn: getAllPrompts
})
const { pathname } = useLocation() const showLeft = useMemo<boolean>(() => {
console.log(location.pathname)
const getPromptInfoById = (id: string) => { if (location.pathname.includes("/settings")) {
return prompts?.find((prompt) => prompt.id === id) return true
} }
return showOptionSidebar
}, [location.pathname, showOptionSidebar])
const handlePromptChange = (value?: string) => { const { t } = useTranslation(["option", "common", "settings"])
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 { clearChat } = useMessageOption()
// 是否隐藏logo
const hideLogo = useMemo(() => {
return localStorage.getItem("hideLogo") === "true"
}, [])
const [collect, setCollect] = useState<boolean>(false)
return ( return (
<div <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 ${ className={`h-[60px] absolute inset-0 pl-5 z-10 flex items-center transition-all duration-300 ease-in-out ${showOptionSidebar && !location.pathname.includes("/settings") ? "left-[300px]" : ""}`}>
temporaryChat && "!bg-gray-200 dark:!bg-black" {/*控制侧边栏显示隐藏与新建对话*/}
}`}> {!showLeft && (
<div className="flex gap-2 items-center"> <div className="flex items-center gap-3">
{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>
<button <button
className="text-gray-500 dark:text-gray-400" className="text-gray-500 dark:text-gray-400"
onClick={() => setSidebarOpen(true)}> onClick={() => {
setShowOptionSidebar(!showOptionSidebar)
}}>
<PanelLeftIcon className="w-6 h-6" /> <PanelLeftIcon className="w-6 h-6" />
</button> </button>
</div> <Button
<NewChat clearChat={clearChat} /> color="cyan"
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600"> variant="filled"
{"/"} shape="round"
</span> style={{
<div className="hidden lg:block"> color: "#0057ff",
<Select background: "#0057ff0f",
className="w-80" border: "1px solid #0066ff26"
placeholder={t("common:selectAModel")}
// loadingText={t("common:selectAModel")}
value={selectedModel}
onChange={(e) => {
setSelectedModel(e)
localStorage.setItem("selectedModel", e)
}} }}
filterOption={(input, option) => { onClick={clearChat}>
//@ts-ignore <div className="flex items-center justify-between w-full">
return ( <div className="flex items-center">
option?.label?.props["data-title"] <PlusOutlined
?.toLowerCase() className="text-sm"
?.indexOf(input.toLowerCase()) >= 0 style={{ fontSize: "16px", fontWeight: 500 }}
)
}}
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()
// }}
/> />
<span>{t("newChat")}</span>
</div> </div>
<div className="lg:hidden">
<ModelSelect />
</div> </div>
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600"> </Button>
{"/"} </div>
</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} {location.pathname.includes("/settings") && (
</span> <h2 className="text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3">
),
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 <NavLink
to="/settings" to="/"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> className="!text-gray-500 dark:text-gray-400 flex items-center gap-2 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<CogIcon className="w-6 h-6" /> {!hideLogo && <img src={logo} alt="logo" className="w-8" />}
<p>
<span className="text-[#d30100]"></span>
</p>
</NavLink>
</h2>
)}
{/* 项目标题 */}
<div
className={`
absolute left-1/2 transform -translate-x-1/2
w-[600px] h-[60px] dark:bg-black
flex items-center justify-center
transition-[top] drop-shadow
${showOptionSidebar ? "-top-[60px]" : "-top-[2px] delay-200"}
`}>
<svg
className="icon"
viewBox="0 0 8960 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="9634"
width="100%"
height="55">
<path
d="M8960 0c-451.52 181.184-171.2 1024-992 1024H992C171.232 1024 451.392 181.184 0 0h8960z"
fill="#ffffff"
p-id="9635"></path>
</svg>
<h2 className="flex items-center gap-3 text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3 absolute left-1/2 transform -translate-x-1/2">
{!hideLogo && <img src={logo} alt="logo" className="w-8" />}
<p>
<span className="text-[#d30100]"></span>
</p>
</h2>
</div>
{/*设置框*/}
<div className="flex items-center gap-1 ml-auto pr-5">
<Tooltip title="收藏">
{collect ? (
<Button
color="default"
variant="text"
className="!px-[5px]"
onClick={() => setCollect(false)}>
<CollectIcon className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors w-5 h-5 cursor-pointer" />
</Button>
) : (
<Button
color="default"
variant="text"
className="!px-[5px]"
onClick={() => setCollect(true)}>
<NotCollectIcon className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors w-5 h-5 cursor-pointer" />
</Button>
)}
</Tooltip>
<Tooltip title="分享">
<Button color="default" variant="text" className="!px-[5px]">
<ShareIcon className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors w-5 h-5 cursor-pointer" />
</Button>
</Tooltip>
<Tooltip title="消息">
<Button color="default" variant="text" className="!px-[5px]">
<BellIcon className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors w-5 h-5 cursor-pointer" />
</Button>
</Tooltip>
<Tooltip title={t("settings")}>
<NavLink to="/settings">
<SettingIcon className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors w-5 h-5 cursor-pointer" />
</NavLink> </NavLink>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
</div>
</div>
) )
} }

View File

@ -1,104 +1,86 @@
import React, { useState } from "react" import React, { useContext, useState } from "react"
import { Sidebar } from "../Option/Sidebar"
import { Drawer, Tooltip } from "antd"
import { useTranslation } from "react-i18next"
import { CurrentChatModelSettings } from "../Common/Settings/CurrentChatModelSettings" import { CurrentChatModelSettings } from "../Common/Settings/CurrentChatModelSettings"
import { Header } from "./Header" import { Header } from "./Header.tsx"
import { EraserIcon } from "lucide-react" import IodVideo from "@/components/Option/VideoPlayer"
import { PageAssitDatabase } from "@/db"
import { useMessageOption } from "@/hooks/useMessageOption" interface OptionLayoutContextType {
import { useQueryClient } from "@tanstack/react-query" showOptionSidebar: boolean
import { useStoreChatModelSettings } from "@/store/model" setShowOptionSidebar: (show: boolean) => void
showVideo: boolean
setShowVideo: (show: boolean) => void
}
const OptionLayoutContext = React.createContext<OptionLayoutContextType>({
showOptionSidebar: true,
setShowOptionSidebar: () => {},
showVideo: true,
setShowVideo: () => {}
})
// 创建自定义 hook 以便子组件使用
export const useOptionLayoutContext = () => {
const context = useContext(OptionLayoutContext)
if (context === undefined) {
throw new Error(
"useOptionLayoutContext must be used within a OptionLayoutProvider"
)
}
return context
}
const OptionLayoutProvider = ({ children }: { children: React.ReactNode }) => {
const [showHistory, setShowHistory] = useState(true)
const [showVideo, setShowVideo] = useState<boolean>(false)
return (
<OptionLayoutContext.Provider
value={{
showOptionSidebar: showHistory,
setShowOptionSidebar: setShowHistory,
showVideo,
setShowVideo
}}>
{children}
</OptionLayoutContext.Provider>
)
}
const OptionLayoutMain: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
const { showVideo } = useOptionLayoutContext()
if (showVideo) {
return <IodVideo />
}
return (
<>
<Header />
{children}
</>
)
}
export default function OptionLayout({ export default function OptionLayout({
children children
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
const [sidebarOpen, setSidebarOpen] = useState(false)
const { t } = useTranslation(["option", "common", "settings"])
const [openModelSettings, setOpenModelSettings] = useState(false) const [openModelSettings, setOpenModelSettings] = useState(false)
const {
setMessages,
setHistory,
setHistoryId,
historyId,
clearChat,
setSelectedModel,
temporaryChat,
setSelectedSystemPrompt
} = useMessageOption()
const queryClient = useQueryClient()
const { setSystemPrompt } = useStoreChatModelSettings()
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<main className="relative h-dvh w-full"> <main className="relative h-dvh w-full">
<div className="relative z-10 w-full"> {/*<div className="relative z-10 w-full">*/}
<Header {/*</div>*/}
setSidebarOpen={setSidebarOpen}
setOpenModelSettings={setOpenModelSettings}
/>
</div>
{/* <div className="relative flex h-full flex-col items-center"> */} {/* <div className="relative flex h-full flex-col items-center"> */}
{children} <OptionLayoutProvider>
<OptionLayoutMain>{children}</OptionLayoutMain>
</OptionLayoutProvider>
{/* </div> */} {/* </div> */}
<Drawer
title={
<div className="flex items-center justify-between">
{t("sidebarTitle")}
<Tooltip
title={t(
"settings:generalSettings.system.deleteChatHistory.label"
)}
placement="right">
<button
onClick={async () => {
const confirm = window.confirm(
t(
"settings:generalSettings.system.deleteChatHistory.confirm"
)
)
if (confirm) {
const db = new PageAssitDatabase()
await db.deleteAllChatHistory()
await queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
clearChat()
}
}}
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100">
<EraserIcon className="size-5" />
</button>
</Tooltip>
</div>
}
placement="left"
closeIcon={null}
onClose={() => setSidebarOpen(false)}
open={sidebarOpen}>
<Sidebar
onClose={() => setSidebarOpen(false)}
setMessages={setMessages}
setHistory={setHistory}
setHistoryId={setHistoryId}
setSelectedModel={setSelectedModel}
setSelectedSystemPrompt={setSelectedSystemPrompt}
clearChat={clearChat}
historyId={historyId}
setSystemPrompt={setSystemPrompt}
temporaryChat={temporaryChat}
history={history}
/>
</Drawer>
<CurrentChatModelSettings <CurrentChatModelSettings
open={openModelSettings} open={openModelSettings}
setOpen={setOpenModelSettings} setOpen={setOpenModelSettings}

View File

@ -1,17 +1,18 @@
import { import {
BlocksIcon,
BookIcon, BookIcon,
BrainCircuitIcon, BrainCircuitIcon,
OrbitIcon,
ShareIcon,
BlocksIcon,
InfoIcon,
CombineIcon,
ChromeIcon, ChromeIcon,
CpuIcon CombineIcon,
CpuIcon,
InfoIcon,
OrbitIcon,
ShareIcon
} from "lucide-react" } from "lucide-react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { Link, useLocation } from "react-router-dom" import { Link, useLocation } from "react-router-dom"
import { OllamaIcon } from "../Icons/Ollama" import { OllamaIcon } from "../Icons/Ollama"
import { IodIcon } from "../Icons/Iod.tsx"
import { BetaTag } from "../Common/Beta" import { BetaTag } from "../Common/Beta"
function classNames(...classes: string[]) { function classNames(...classes: string[]) {
@ -82,6 +83,12 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
icon={OllamaIcon} icon={OllamaIcon}
current={location.pathname} current={location.pathname}
/> />
<LinkComponent
href="/settings/iod"
name={t("iodSettings.title")}
icon={IodIcon}
current={location.pathname}
/>
{import.meta.env.BROWSER === "chrome" && ( {import.meta.env.BROWSER === "chrome" && (
<LinkComponent <LinkComponent
href="/settings/chrome" href="/settings/chrome"

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,11 @@
import React from "react" import React from "react"
import { PlaygroundForm } from "./PlaygroundForm" import { PlaygroundForm } from "./PlaygroundForm"
import { PlaygroundChat } from "./PlaygroundChat" import { PlaygroundChat } from "./PlaygroundChat"
import { PlaygroundSidebar } from "./PlaygroundSidebar.tsx"
import { useMessageOption } from "@/hooks/useMessageOption" import { useMessageOption } from "@/hooks/useMessageOption"
import { webUIResumeLastChat } from "@/services/app" import { webUIResumeLastChat } from "@/services/app"
import { import {
formatToChatHistory, formatToChatHistory,
formatToMessage, formatToMessage,
@ -13,6 +16,7 @@ import { getLastUsedChatSystemPrompt } from "@/services/model-settings"
import { useStoreChatModelSettings } from "@/store/model" import { useStoreChatModelSettings } from "@/store/model"
import { useSmartScroll } from "@/hooks/useSmartScroll" import { useSmartScroll } from "@/hooks/useSmartScroll"
import { ChevronDown } from "lucide-react" import { ChevronDown } from "lucide-react"
import { PlaygroundIod } from "@/components/Option/Playground/PlaygroundIod.tsx"
export const Playground = () => { export const Playground = () => {
const drop = React.useRef<HTMLDivElement>(null) const drop = React.useRef<HTMLDivElement>(null)
@ -132,17 +136,20 @@ export const Playground = () => {
return ( return (
<div <div
ref={drop} ref={drop}
className={`relative flex h-full flex-col items-center ${ className={`relative flex gap-3 h-full items-center ${
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800" : "" dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800" : ""
} bg-white dark:bg-[#171717]`}> } bg-white dark:bg-[#171717]`}>
<PlaygroundSidebar />
<div className="h-full flex-1 overflow-x-hidden prose-lg flex flex-col items-center [&>*]:max-w-[848px] pt-[60px]">
<div <div
ref={containerRef} 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"> className="custom-scrollbar flex h-auto w-full flex-col items-center px-5">
<PlaygroundChat /> <PlaygroundChat />
</div> </div>
<div className="absolute bottom-0 w-full"> <div
className={`${messages.length ? "absolute" : "relative"} bottom-0 w-full`}>
{!isAtBottom && ( {!isAtBottom && (
<div className="fixed bottom-36 z-20 left-0 right-0 flex justify-center"> <div className="absolute bottom-36 z-20 left-0 right-0 flex justify-center">
<button <button
onClick={scrollToBottom} 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"> className="bg-gray-50 shadow border border-gray-200 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto">
@ -153,5 +160,7 @@ export const Playground = () => {
<PlaygroundForm dropedFile={dropedFile} /> <PlaygroundForm dropedFile={dropedFile} />
</div> </div>
</div> </div>
<PlaygroundIod />
</div>
) )
} }

View File

@ -11,22 +11,24 @@ export const PlaygroundChat = () => {
regenerateLastMessage, regenerateLastMessage,
isSearchingInternet, isSearchingInternet,
editMessage, editMessage,
ttsEnabled ttsEnabled,
setCurrentMessageId,
} = useMessageOption() } = useMessageOption()
const [isSourceOpen, setIsSourceOpen] = React.useState(false) const [isSourceOpen, setIsSourceOpen] = React.useState(false)
const [source, setSource] = React.useState<any>(null) const [source, setSource] = React.useState<any>(null)
return ( 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 && ( {messages.length === 0 && (
<div className="mt-32 w-full"> <div className="mt-3 w-full">
<PlaygroundEmpty /> <PlaygroundEmpty />
</div> </div>
)} )}
{messages.map((message, index) => ( {messages.map((message, index) => (
<PlaygroundMessage <PlaygroundMessage
key={index} key={index}
id={message.id}
isBot={message.isBot} isBot={message.isBot}
message={message.message} message={message.message}
name={message.name} name={message.name}
@ -36,7 +38,7 @@ export const PlaygroundChat = () => {
onRengerate={regenerateLastMessage} onRengerate={regenerateLastMessage}
isProcessing={streaming} isProcessing={streaming}
isSearchingInternet={isSearchingInternet} isSearchingInternet={isSearchingInternet}
sources={message.sources} webSources={message.webSources}
iodSources={message.iodSources} iodSources={message.iodSources}
onEditFormSubmit={(value, isSend) => { onEditFormSubmit={(value, isSend) => {
editMessage(index, value, !message.isBot, isSend) editMessage(index, value, !message.isBot, isSend)
@ -49,11 +51,12 @@ export const PlaygroundChat = () => {
generationInfo={message?.generationInfo} generationInfo={message?.generationInfo}
isStreaming={streaming} isStreaming={streaming}
reasoningTimeTaken={message?.reasoning_time_taken} reasoningTimeTaken={message?.reasoning_time_taken}
setCurrentMessageId={setCurrentMessageId}
iodSearch={message.iodSearch}
/> />
))} ))}
</div> </div>
<div className="w-full pb-[157px]"></div> {messages.length !== 0 && <div className="w-full pb-[157px]"></div>}
<MessageSourcePopup <MessageSourcePopup
open={isSourceOpen} open={isSourceOpen}
setOpen={setIsSourceOpen} setOpen={setIsSourceOpen}

View File

@ -1,130 +1,40 @@
import { cleanUrl } from "@/libs/clean-url" import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useStorage } from "@plasmohq/storage/hook" import { useMutation, useQueryClient } from "@tanstack/react-query"
import { useQuery } from "@tanstack/react-query" import { qaPrompt } from "@/libs/playground.tsx"
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"
export const PlaygroundEmpty = () => { export const PlaygroundEmpty = () => {
const [ollamaURL, setOllamaURL] = useState<string>("") const { onSubmit } = useMessageOption()
const { t } = useTranslation(["playground", "common"])
const [checkOllamaStatus] = useStorage("checkOllamaStatus", true) const queryClient = useQueryClient()
const { const { mutateAsync: sendMessage } = useMutation({
data: ollamaInfo, mutationFn: onSubmit,
status: ollamaStatus, onSuccess: () => {
refetch, queryClient.invalidateQueries({
isRefetching queryKey: ["fetchChatHistory"]
} = useQuery({ })
queryKey: ["ollamaStatus"],
queryFn: async () => {
const ollamaURL = await getOllamaURL()
const isOk = await isOllamaRunning()
if (ollamaURL) {
saveOllamaURL(ollamaURL)
} }
return {
isOk,
ollamaURL
}
},
enabled: checkOllamaStatus
}) })
useEffect(() => { function handleQuestion(message: string) {
if (ollamaInfo?.ollamaURL) { void sendMessage({ message, image: "" })
setOllamaURL(ollamaInfo.ollamaURL)
} }
}, [ollamaInfo])
if (!checkOllamaStatus) {
return ( return (
<div className="mx-auto sm:max-w-xl px-4 mt-10"> <div className="w-full pb-4 pt-[20%] grid grid-cols-3 gap-3">
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-gray-50 dark:bg-[#262626] dark:border-gray-600"> {qaPrompt.map((item, index) => (
<h1 className="text-sm font-medium text-center text-gray-500 dark:text-gray-400 flex gap-3 items-center justify-center"> <div
<span >👋</span> key={item.id}
<span className="text-gray-700 dark:text-gray-300"> className="p-6 bg-gradient-to-br from-blue-50/90 via-indigo-50/90 to-purple-50/90 backdrop-blur-xl border border-white/60 shadow-xl rounded-2xl cursor-pointer hover:shadow-blue-200/40 hover:from-blue-100/90 hover:to-indigo-100/90 transition-all duration-500 hover:-translate-y-1"
{t("welcome")} onClick={() => handleQuestion(item.title)}>
</span> <div className="flex items-center">
</h1> <div className="text-blue-500 mr-2 w-10">{item.icon}</div>
<div className="text-sm text-gray-800">
{item.title}
</div> </div>
</div> </div>
)
}
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> </div>
))}
</div> </div>
) )
} }

View File

@ -1,19 +1,26 @@
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { useMutation, useQueryClient } from "@tanstack/react-query" import { useMutation, useQueryClient } from "@tanstack/react-query"
import React from "react" import React, { useMemo } from "react"
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize" import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
import { toBase64 } from "~/libs/to-base64" import { toBase64 } from "~/libs/to-base64"
import { useMessageOption } from "~/hooks/useMessageOption" import { useMessageOption } from "~/hooks/useMessageOption"
import { Checkbox, Dropdown, Switch, Tooltip } from "antd" import {
import { Image } from "antd" Button,
Checkbox,
Dropdown,
Image,
MenuProps,
Switch,
Tooltip
} from "antd"
import { useWebUI } from "~/store/webui" import { useWebUI } from "~/store/webui"
import { defaultEmbeddingModelForRag } from "~/services/ollama" import { defaultEmbeddingModelForRag } from "~/services/ollama"
import { ImageIcon, MicIcon, StopCircleIcon, X } from "lucide-react" import { ImageIcon, MicIcon, StopCircleIcon, X } from "lucide-react"
import { getVariable } from "@/utils/select-variable" import { getVariable } from "@/utils/select-variable"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect" // import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect"
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition" 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 { handleChatInputKeyDown } from "@/utils/key-down"
import { getIsSimpleInternetSearch } from "@/services/search" import { getIsSimpleInternetSearch } from "@/services/search"
@ -34,6 +41,8 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
streaming: isSending, streaming: isSending,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
selectedQuickPrompt, selectedQuickPrompt,
textareaRef, textareaRef,
setSelectedQuickPrompt, setSelectedQuickPrompt,
@ -126,7 +135,6 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
} }
}, [transcript]) }, [transcript])
/*
React.useEffect(() => { React.useEffect(() => {
if (selectedQuickPrompt) { if (selectedQuickPrompt) {
const word = getVariable(selectedQuickPrompt) const word = getVariable(selectedQuickPrompt)
@ -143,7 +151,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
} }
} }
}, [selectedQuickPrompt]) }, [selectedQuickPrompt])
*/
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { mutateAsync: sendMessage } = useMutation({ const { mutateAsync: sendMessage } = useMutation({
@ -205,14 +213,47 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
} }
} }
return ( const iodSearchItems = useMemo<MenuProps["items"]>(() => {
<div className="flex w-full flex-col items-center p-2 pt-1 pb-4"> return [
<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"> key: 0,
label: (
<div <div
className={` bg-neutral-50 dark:bg-[#262626] relative w-full max-w-[48rem] p-1 backdrop-blur-lg duration-100 border border-gray-300 rounded-xl dark:border-gray-600 onClick={() => {
${temporaryChat ? "!bg-gray-200 dark:!bg-black " : ""} setIodSearch(true)
`}> }}>
<p
className={`${iodSearch ? "text-[#0057ff]" : "text-[#000000d9]"} flex items-center gap-1 mb-1`}>
<PiNetwork className="h-5 w-5" />
</p>
<p className="text-[#00000080]"></p>
</div>
)
},
{
key: 1,
label: (
<div
onClick={() => {
setIodSearch(false)
}}>
<p
className={`${!iodSearch ? "text-[#0057ff]" : "text-[#000000d9]"} flex items-center gap-1 mb-1`}>
<PiNetwork className="h-5 w-5" />
</p>
<p className="text-[#00000080]"></p>
</div>
)
}
]
}, [iodSearch])
return (
<div className="flex w-full flex-col items-center pt-1 px-5 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-5/5">
<div
className={`shadow-xl relative w-full max-w-[65rem] p-1 rounded-xl bg-gradient-to-br from-white/90 via-blue-50/90 to-cyan-50/90 backdrop-blur-lg border border-blue-100/70 cursor-pointer hover:shadow-blue-100/60 transition-all duration-500`}>
<div <div
className={`border-b border-gray-200 dark:border-gray-600 relative ${ className={`border-b border-gray-200 dark:border-gray-600 relative ${
form.values.image.length === 0 ? "hidden" : "block" form.values.image.length === 0 ? "hidden" : "block"
@ -233,8 +274,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
/> />
</div> </div>
<div> <div>
<div <div className={`flex bg-transparent `}>
className={`flex bg-transparent `}>
<form <form
onSubmit={form.onSubmit(async (value) => { onSubmit={form.onSubmit(async (value) => {
stopListening() stopListening()
@ -300,26 +340,73 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
{...form.getInputProps("message")} {...form.getInputProps("message")}
/> />
<div className="mt-2 flex justify-between items-center"> <div className="mt-2 flex justify-between items-center">
<div className="flex !justify-end gap-3"> <div className="flex">
{!selectedKnowledge && (
<div>
{/* 展示隐藏深度搜索*/}
<Tooltip
title={t("tooltip.searchInternet")}
className="hidden">
<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>
<Dropdown
menu={{ items: iodSearchItems }}
placement="bottom"
trigger={["click"]}
arrow>
<Button
color="default"
variant="filled"
size="large"
className="w-full mt-4 hover:!bg-[#0057ff1a]"
style={
iodSearch
? {
color: "#0057ff",
background: "#0057ff0f",
border: "1px solid #0066ff26"
}
: {}
}>
<PiNetwork className="h-5 w-5" />
{iodSearch ? ":开" : ""}
</Button>
</Dropdown>
</div>
)}
</div>
<div className="flex !justify-end gap-1">
{!selectedKnowledge && ( {!selectedKnowledge && (
<Tooltip title={t("tooltip.uploadImage")}> <Tooltip title={t("tooltip.uploadImage")}>
<button <Button
type="button" color="default"
variant="text"
onClick={() => { onClick={() => {
inputRef.current?.click() inputRef.current?.click()
}} }}
className={`flex items-center justify-center dark:text-gray-300 ${ className={`!px-[5px] flex items-center justify-center dark:text-gray-300 ${
chatMode === "rag" ? "hidden" : "block" chatMode === "rag" ? "hidden" : "block"
}`}> }`}>
<ImageIcon className="h-5 w-5" /> <ImageIcon strokeWidth={1} className="h-5 w-5" />
</button> </Button>
</Tooltip> </Tooltip>
)} )}
{browserSupportsSpeechRecognition && ( {browserSupportsSpeechRecognition && (
<Tooltip title={t("tooltip.speechToText")}> <Tooltip title={t("tooltip.speechToText")}>
<button <Button
type="button" color="default"
variant="text"
onClick={async () => { onClick={async () => {
if (isListening) { if (isListening) {
stopSpeechRecognition() stopSpeechRecognition()
@ -331,40 +418,43 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
}) })
} }
}} }}
className={`flex items-center justify-center dark:text-gray-300`}> className={`flex items-center justify-center dark:text-gray-300 !px-[5px]`}>
{!isListening ? ( {!isListening ? (
<MicIcon className="h-5 w-5" /> <MicIcon strokeWidth={1} className="h-5 w-5" />
) : ( ) : (
<div className="relative"> <div className="relative">
<span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"></span> <span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"></span>
<MicIcon className="h-5 w-5" /> <MicIcon
strokeWidth={1}
className="h-5 w-5"
/>
</div> </div>
)} )}
</button> </Button>
</Tooltip> </Tooltip>
)} )}
<KnowledgeSelect /> {/*<KnowledgeSelect />*/}
{!isSending ? ( {!isSending ? (
<Dropdown.Button <Dropdown.Button
type="default"
htmlType="submit" htmlType="submit"
disabled={isSending} disabled={isSending}
className="!justify-end !w-auto" // icon={
icon={ // <svg
<svg // xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" // fill="none"
fill="none" // viewBox="0 0 24 24"
viewBox="0 0 24 24" // strokeWidth={1.5}
strokeWidth={1.5} // stroke="currentColor"
stroke="currentColor" // className="w-5 h-5">
className="w-5 h-5"> // <path
<path // strokeLinecap="round"
strokeLinecap="round" // strokeLinejoin="round"
strokeLinejoin="round" // d="m19.5 8.25-7.5 7.5-7.5-7.5"
d="m19.5 8.25-7.5 7.5-7.5-7.5" // />
/> // </svg>
</svg> // }
}
menu={{ menu={{
items: [ items: [
{ {
@ -394,20 +484,6 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
] ]
}}> }}>
<div className="inline-flex gap-2"> <div className="inline-flex gap-2">
{sendWhenEnter ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-5 w-5"
viewBox="0 0 24 24">
<path d="M9 10L4 15 9 20"></path>
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
</svg>
) : null}
{t("common:submit")} {t("common:submit")}
</div> </div>
</Dropdown.Button> </Dropdown.Button>

View File

@ -0,0 +1,160 @@
import React, { createContext, useContext, useMemo, useState } from "react"
import { AnimatePresence, motion } from "framer-motion"
import { PlaygroundIodRelevant } from "@/components/Common/Playground/IodRelevant.tsx"
import { PlaygroundData } from "@/components/Common/Playground/Data.tsx"
import { PlaygroundScene } from "@/components/Common/Playground/Scene.tsx"
import { PlaygroundTeam } from "@/components/Common/Playground/Team.tsx"
import { Card } from "antd"
import { CloseOutlined } from "@ant-design/icons"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { AllIodRegistryEntry } from "@/types/iod.ts"
// 定义 Context 类型
interface IodPlaygroundContextType {
showPlayground: boolean
setShowPlayground: React.Dispatch<React.SetStateAction<boolean>>
detailHeader: React.ReactNode
setDetailHeader: React.Dispatch<React.SetStateAction<React.ReactNode>>
detailMain: React.ReactNode
setDetailMain: React.Dispatch<React.SetStateAction<React.ReactNode>>
currentIodMessage?: AllIodRegistryEntry
}
// 创建 Context
const PlaygroundContext = createContext<IodPlaygroundContextType | undefined>(
undefined
)
// 创建自定义 hook 以便子组件使用
export const useIodPlaygroundContext = () => {
const context = useContext(PlaygroundContext)
if (context === undefined) {
throw new Error(
"usePlaygroundContext must be used within a PlaygroundProvider"
)
}
return context
}
const PlaygroundIodProvider: React.FC<{ children: React.ReactNode }> = ({
children
}) => {
const { messages, iodLoading, currentMessageId } = useMessageOption()
const [showPlayground, setShowPlayground] = useState<boolean>(true)
const [detailHeader, setDetailHeader] = useState(<></>)
const [detailMain, setDetailMain] = useState(<></>)
const currentIodMessage = useMemo<AllIodRegistryEntry | undefined>(() => {
// loading 返回 undefined是为了避免数据不足三个的情况
if (iodLoading || !messages.length) {
return undefined
}
console.log(messages)
console.log(currentMessageId)
// 如果不存在currentMessageId默认返回最后一个message
if (!currentMessageId) {
const lastMessage = messages.at(-1)
// 如果最后一次message没有开启数联网搜索则返回undefined
return lastMessage?.iodSearch ? lastMessage.iodSources : undefined
}
const currentMessage = messages?.find(
(message) => message.id === currentMessageId
)
return currentMessage?.iodSearch ? currentMessage.iodSources : undefined
}, [currentMessageId, messages, iodLoading])
return (
<PlaygroundContext.Provider
value={{
currentIodMessage,
showPlayground,
setShowPlayground,
detailMain,
setDetailMain,
detailHeader,
setDetailHeader
}}>
{children}
</PlaygroundContext.Provider>
)
}
// 子组件使用修改card的默认样式
const classNames =
"h-full [&_.ant-card-body]:h-full [&_.ant-card-body]:!p-[20px] overflow-y-hidden !bg-[rgba(240,245,255,0.3)] backdrop-blur-sm border border-white/30 shadow-xl rounded-2xl"
// 将原来的返回内容移到这个组件中
const PlaygroundContent = () => {
const { showPlayground, detailMain, detailHeader, setShowPlayground } =
useIodPlaygroundContext()
return (
<AnimatePresence mode="popLayout">
{showPlayground ? (
<motion.div
key="playground"
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{
duration: 0.6,
ease: "easeInOut"
}}
className="h-full grid grid-rows-12 gap-3">
<div className="w-full row-span-5">
<PlaygroundIodRelevant
className={classNames
.replace("!bg-[rgba(240,245,255,0.3)]", "")
.replace("shadow-xl", "")}
/>
</div>
<div className="w-full row-span-4 grid grid-cols-2 gap-3 custom-scrollbar">
<PlaygroundData className={classNames} />
<PlaygroundScene className={classNames} />
</div>
<div className="w-full row-span-3 pb-3">
<PlaygroundTeam className={classNames} />
</div>
</motion.div>
) : (
<motion.div
key="alternative"
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{
duration: 0.6,
ease: "easeInOut"
}}
className="h-full pb-5">
<Card className="h-full shadow-xl shadow-gray-500/20 [&_.ant-card-body]:h-full">
<div className="flex flex-col h-full">
<div className="pb-6 flex items-center justify-between">
<div>{detailHeader}</div>
<CloseOutlined
size={30}
className="hover:text-red-500 cursor-pointer transition-colors duration-200 text-xl"
onClick={() => setShowPlayground(true)}
/>
</div>
{detailMain}
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
)
}
export const PlaygroundIod = () => {
return (
<div className="w-[36%] h-full pt-16 pr-5 pb-0">
<PlaygroundIodProvider>
<PlaygroundContent />
</PlaygroundIodProvider>
</div>
)
}

View File

@ -1,25 +0,0 @@
import { PencilIcon } from "lucide-react"
import { useMessage } from "../../../hooks/useMessage"
import { useTranslation } from 'react-i18next';
export const PlaygroundNewChat = () => {
const { setHistory, setMessages, setHistoryId } = useMessage()
const { t } = useTranslation('optionChat')
const handleClick = () => {
setHistoryId(null)
setMessages([])
setHistory([])
}
return (
<button
onClick={handleClick}
className="flex w-full border bg-transparent hover:bg-gray-200 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100 rounded-md p-2 dark:border-gray-800">
<PencilIcon className="mx-3 h-5 w-5" aria-hidden="true" />
<span className="inline-flex font-semibol text-white text-sm">
{t('newChat')}
</span>
</button>
)
}

View File

@ -0,0 +1,277 @@
import { Sidebar } from "@/components/Option/Sidebar.tsx"
import React, { useMemo } from "react"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useStoreChatModelSettings } from "@/store/model.tsx"
import {
Button,
Card,
Divider,
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 { useOptionLayoutContext } 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"
import logo from "@/assets/logo.png"
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 PlaygroundSidebar = () => {
const { setSystemPrompt } = useStoreChatModelSettings()
const { showOptionSidebar, setShowOptionSidebar, setShowVideo } = useOptionLayoutContext()
const {
setMessages,
setHistory,
setHistoryId,
historyId,
clearChat,
selectedModel,
setSelectedModel,
temporaryChat,
setSelectedSystemPrompt,
stopStreamingRequest
} = 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: (
<div className="flex items-center gap-2 truncate w-full">
<p className="w-5 h-5 [&_.ant-avatar]:!w-full [&_.ant-avatar]:!h-full [&_.ant-avatar]:relative [&_.ant-avatar]:-top-3">
{item.icon}
</p>
<span className="flex-1 truncate" title={item.title}>
{item.title}
</span>
</div>
)
}
})
}
]
}, [])
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 } = useQuery({
queryKey: ["fetchModel"],
queryFn: () => fetchChatModels({ returnEmpty: true }),
refetchIntervalInBackground: false,
placeholderData: (prev) => prev
})
// 是否隐藏logo
const hideLogo = useMemo(() => {
return localStorage.getItem("hideLogo") === "true"
}, [])
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 backdrop-blur-lg !bg-[#f3f4f6]`}
style={{ width: showOptionSidebar ? "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]">
<div className="flex items-center gap-2 cursor-pointer" onClick={() => setShowVideo(true)}>
{!hideLogo && <img src={logo} alt="logo" className="w-8" />}
<h2 className="text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3">
<span className="text-[#d30100]"></span>
</h2>
</div>
<button
className="text-gray-500 dark:text-gray-400"
onClick={() => {
setShowOptionSidebar(!showOptionSidebar)
}}>
<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={() => setShowOptionSidebar(true)}
setMessages={setMessages}
setHistory={setHistory}
setHistoryId={setHistoryId}
setSelectedModel={setSelectedModel}
setSelectedSystemPrompt={setSelectedSystemPrompt}
clearChat={clearChat}
historyId={historyId}
setSystemPrompt={setSystemPrompt}
temporaryChat={temporaryChat}
stopStreamingRequest={stopStreamingRequest}
history={history}
/>
</div>
</div>
</Card>
)
}

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

@ -0,0 +1,33 @@
import { useTranslation } from "react-i18next"
import TextArea from "antd/es/input/TextArea"
import { IodDb } from "@/db/iod.ts"
import { useState } from "react"
export const IodApp = () => {
const { t } = useTranslation("settings")
const db = IodDb.getInstance()
const [connection, setConnection] = useState(JSON.stringify(db.getIodConnection(), null, 2))
const setConnectValWrap = (val: string) => {
db.insertIodConnection(JSON.parse(val))
setConnection(val)
}
return (
<dl className="flex flex-col space-y-6 text-sm">
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
{t("iodSettings.heading")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
</div>
<div className="flex flex-col gap-3">
<span className="text-gray-700 dark:text-neutral-50"></span>
<TextArea rows={6} placeholder="请输入数联网连接配置" value={connection} onChange={(e) => setConnectValWrap(e.target.value)} />
</div>
</dl>
)
}

View File

@ -1,21 +1,21 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { import {
PageAssitDatabase, deleteByHistoryId,
formatToChatHistory, formatToChatHistory,
formatToMessage, formatToMessage,
deleteByHistoryId, getPromptById,
updateHistory, PageAssitDatabase,
pinHistory, pinHistory,
getPromptById updateHistory
} from "@/db" } from "@/db"
import { Empty, Skeleton, Dropdown, Menu, Tooltip } from "antd" import { Dropdown, Empty, Menu, Skeleton, Tooltip } from "antd"
import { import {
PencilIcon, BotIcon,
Trash2,
MoreVertical, MoreVertical,
PencilIcon,
PinIcon, PinIcon,
PinOffIcon, PinOffIcon,
BotIcon Trash2
} from "lucide-react" } from "lucide-react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
@ -24,6 +24,7 @@ import {
getLastUsedChatSystemPrompt, getLastUsedChatSystemPrompt,
lastUsedChatModelEnabled lastUsedChatModelEnabled
} from "@/services/model-settings" } from "@/services/model-settings"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
type Props = { type Props = {
onClose: () => void onClose: () => void
@ -32,8 +33,8 @@ type Props = {
setHistoryId: (historyId: string) => void setHistoryId: (historyId: string) => void
setSelectedModel: (model: string) => void setSelectedModel: (model: string) => void
setSelectedSystemPrompt: (prompt: string) => void setSelectedSystemPrompt: (prompt: string) => void
setSelectedQuickPrompt: (prompt: string | undefined) => void
setSystemPrompt: (prompt: string) => void setSystemPrompt: (prompt: string) => void
stopStreamingRequest: () => void
clearChat: () => void clearChat: () => void
temporaryChat: boolean temporaryChat: boolean
historyId: string historyId: string
@ -47,7 +48,7 @@ export const Sidebar = ({
setHistoryId, setHistoryId,
setSelectedModel, setSelectedModel,
setSelectedSystemPrompt, setSelectedSystemPrompt,
setSelectedQuickPrompt, stopStreamingRequest,
clearChat, clearChat,
historyId, historyId,
setSystemPrompt, setSystemPrompt,
@ -57,6 +58,8 @@ export const Sidebar = ({
const client = useQueryClient() const client = useQueryClient()
const navigate = useNavigate() const navigate = useNavigate()
const { setCurrentMessageId } = useMessageOption()
const { data: chatHistories, status } = useQuery({ const { data: chatHistories, status } = useQuery({
queryKey: ["fetchChatHistory"], queryKey: ["fetchChatHistory"],
queryFn: async () => { queryFn: async () => {
@ -142,6 +145,41 @@ export const Sidebar = ({
} }
}) })
const handleHistoryClick = async (chat: any) => {
const db = new PageAssitDatabase()
const history = await db.getChatHistory(chat.id)
setHistoryId(chat.id)
setCurrentMessageId("")
setHistory(formatToChatHistory(history))
setMessages(formatToMessage(history))
stopStreamingRequest()
const isLastUsedChatModel =
await lastUsedChatModelEnabled()
if (isLastUsedChatModel) {
const currentChatModel = await getLastUsedChatModel(
chat.id
)
if (currentChatModel) {
setSelectedModel(currentChatModel)
}
}
const lastUsedPrompt =
await getLastUsedChatSystemPrompt(chat.id)
if (lastUsedPrompt) {
if (lastUsedPrompt.prompt_id) {
const prompt = await getPromptById(
lastUsedPrompt.prompt_id
)
if (prompt) {
setSelectedSystemPrompt(lastUsedPrompt.prompt_id)
}
}
setSystemPrompt(lastUsedPrompt.prompt_content)
}
navigate("/")
onClose()
}
return ( return (
<div <div
className={`overflow-y-auto z-99 ${temporaryChat ? "pointer-events-none opacity-50" : ""}`}> className={`overflow-y-auto z-99 ${temporaryChat ? "pointer-events-none opacity-50" : ""}`}>
@ -171,7 +209,11 @@ export const Sidebar = ({
{group.items.map((chat, index) => ( {group.items.map((chat, index) => (
<div <div
key={index} 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 group ease-in-out
dark:hover:bg-[#2d2d2d] transition-colors duration-300
${historyId === chat.id ? "text-white bg-[#2563eb] hover:bg-[#1d4ed8]" : "dark:text-gray-100 text-gray-800 hover:text-white hover:bg-[#2563eb]"}
`}>
{chat?.message_source === "copilot" && ( {chat?.message_source === "copilot" && (
<Tooltip title={t("common:sidebarChat")} placement="top"> <Tooltip title={t("common:sidebarChat")} placement="top">
<BotIcon className="size-3 text-green-500" /> <BotIcon className="size-3 text-green-500" />
@ -179,38 +221,7 @@ export const Sidebar = ({
)} )}
<button <button
className="flex-1 overflow-hidden break-all text-start truncate w-full" className="flex-1 overflow-hidden break-all text-start truncate w-full"
onClick={async () => { onClick={() => handleHistoryClick(chat)}>
const db = new PageAssitDatabase()
const history = await db.getChatHistory(chat.id)
setHistoryId(chat.id)
setHistory(formatToChatHistory(history))
setMessages(formatToMessage(history))
const isLastUsedChatModel =
await lastUsedChatModelEnabled()
if (isLastUsedChatModel) {
const currentChatModel = await getLastUsedChatModel(
chat.id
)
if (currentChatModel) {
setSelectedModel(currentChatModel)
}
}
const lastUsedPrompt =
await getLastUsedChatSystemPrompt(chat.id)
if (lastUsedPrompt) {
if (lastUsedPrompt.prompt_id) {
const prompt = await getPromptById(
lastUsedPrompt.prompt_id
)
if (prompt) {
setSelectedSystemPrompt(lastUsedPrompt.prompt_id)
}
}
setSystemPrompt(lastUsedPrompt.prompt_content)
}
navigate("/")
onClose()
}}>
<span className="flex-grow truncate">{chat.title}</span> <span className="flex-grow truncate">{chat.title}</span>
</button> </button>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -267,7 +278,9 @@ export const Sidebar = ({
trigger={["click"]} trigger={["click"]}
placement="bottomRight"> placement="bottomRight">
<button className="text-gray-500 dark:text-gray-400 opacity-80 hover:opacity-100"> <button className="text-gray-500 dark:text-gray-400 opacity-80 hover:opacity-100">
<MoreVertical className="w-4 h-4" /> <MoreVertical
className={`w-4 h-4 group-hover:text-white ${historyId === chat.id ? "text-white" : ""}`}
/>
</button> </button>
</Dropdown> </Dropdown>
</div> </div>

View File

@ -0,0 +1,419 @@
import React, { useEffect, useMemo, useRef, useState } from "react"
import iodVideo from "@/public/video.mp4"
import { useOptionLayoutContext } from "@/components/Layouts/Layout.tsx"
import { createPortal } from "react-dom"
import {
ExpandOutlined,
PauseCircleOutlined,
PlayCircleOutlined
} from "@ant-design/icons"
import logo from "@/assets/logo.png"
const VideoPlayer = () => {
const { setShowVideo } = useOptionLayoutContext()
const videoRef = useRef<HTMLVideoElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const controlsTimerRef = useRef<NodeJS.Timeout | null>(null)
const mouseMoveTimerRef = useRef<NodeJS.Timeout | null>(null)
const isPlayingRef = useRef(false)
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0)
const [volume, setVolume] = useState(1)
const [isMuted, setIsMuted] = useState(false)
const [showControls, setShowControls] = useState(false)
const [isBuffering, setIsBuffering] = useState(false)
// 更新 isPlayingRef 当状态变化时
useEffect(() => {
isPlayingRef.current = isPlaying
}, [isPlaying])
// 格式化时间
const formatTime = (seconds: number) => {
if (isNaN(seconds)) return "00:00"
const minutes = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`
}
// 处理播放/暂停
const togglePlayPause = () => {
const video = videoRef.current
if (!video) return
if (isPlaying) {
video.pause()
setIsPlaying(false)
} else {
video.play().catch((error) => {
console.error("播放失败:", error)
})
setIsPlaying(true)
}
}
// 处理音量变化
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(e.target.value)
setVolume(newVolume)
if (videoRef.current) {
videoRef.current.volume = newVolume
setIsMuted(newVolume === 0)
}
}
// 切换静音
const toggleMute = () => {
const newMuted = !isMuted
setIsMuted(newMuted)
if (videoRef.current) {
videoRef.current.muted = newMuted
}
}
// 进度条点击处理
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
const progressBar = e.currentTarget
const clickPosition = e.nativeEvent.offsetX
const progressBarWidth = progressBar.offsetWidth
if (duration > 0 && videoRef.current) {
const newTime = (clickPosition / progressBarWidth) * duration
videoRef.current.currentTime = newTime
}
}
// 全屏切换
const toggleFullscreen = () => {
const videoContainer = containerRef.current
if (!videoContainer) return
if (!document.fullscreenElement) {
if (videoContainer.requestFullscreen) {
videoContainer.requestFullscreen().catch((err) => {
console.error("全屏切换失败:", err)
})
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
}
}
}
const handleEnded = () => {
setIsPlaying(false)
setShowVideo(false)
}
// 控制栏显示/隐藏 - 与原始HTML版本行为完全一致添加防抖功能
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
// 清除之前的防抖定时器
if (mouseMoveTimerRef.current) {
clearTimeout(mouseMoveTimerRef.current)
}
// 设置新的防抖定时器
mouseMoveTimerRef.current = setTimeout(() => {
const container = containerRef.current
if (!container) return
const containerHeight = container.offsetHeight
const mouseY = e.clientY - container.getBoundingClientRect().top
console.log(mouseY > containerHeight - 150)
// 如果鼠标在底部150px区域内
if (mouseY > containerHeight - 150) {
// 清除之前的隐藏定时器
if (controlsTimerRef.current) {
clearTimeout(controlsTimerRef.current)
}
// 立即显示控制器
setShowControls(true)
} else {
// 鼠标离开底部区域,设置定时器隐藏控制器
if (controlsTimerRef.current) {
clearTimeout(controlsTimerRef.current)
}
controlsTimerRef.current = setTimeout(() => {
setShowControls(false)
}, 300) // 300ms后隐藏
}
}, 10) // 10ms 防抖延迟
}
// 鼠标离开整个视频容器时立即隐藏控制器
const handleMouseLeave = () => {
// 清除防抖定时器
if (mouseMoveTimerRef.current) {
clearTimeout(mouseMoveTimerRef.current)
}
// 清除控制栏隐藏定时器
if (controlsTimerRef.current) {
clearTimeout(controlsTimerRef.current)
}
// 立即隐藏控制栏
setShowControls(false)
}
// 视频事件处理
useEffect(() => {
const video = videoRef.current
if (!video) return
const handleLoadedMetadata = () => {
setDuration(video.duration)
}
const handleTimeUpdate = () => {
setCurrentTime(video.currentTime)
}
const handleWaiting = () => {
setIsBuffering(true)
}
const handlePlaying = () => {
setIsBuffering(false)
setIsPlaying(true)
}
const handlePause = () => {
if (!isBuffering) {
setIsPlaying(false)
}
}
video.addEventListener("loadedmetadata", handleLoadedMetadata)
video.addEventListener("timeupdate", handleTimeUpdate)
video.addEventListener("waiting", handleWaiting)
video.addEventListener("playing", handlePlaying)
video.addEventListener("pause", handlePause)
video.addEventListener("ended", handleEnded)
// 组件挂载时尝试播放视频
const playVideo = async () => {
try {
await video.play()
setIsPlaying(true)
} catch (error) {
console.error("自动播放失败:", error)
}
}
const timer = setTimeout(playVideo, 100)
return () => {
video.removeEventListener("loadedmetadata", handleLoadedMetadata)
video.removeEventListener("timeupdate", handleTimeUpdate)
video.removeEventListener("waiting", handleWaiting)
video.removeEventListener("playing", handlePlaying)
video.removeEventListener("pause", handlePause)
video.removeEventListener("ended", handleEnded)
// 清除所有定时器
if (controlsTimerRef.current) {
clearTimeout(controlsTimerRef.current)
}
if (mouseMoveTimerRef.current) {
clearTimeout(mouseMoveTimerRef.current)
}
clearTimeout(timer)
}
}, [])
// 处理键盘事件
const handleKeyDown = (e: KeyboardEvent) => {
if (!videoRef.current) {
return
}
switch (e.code) {
case "Space":
e.preventDefault()
const video = videoRef.current
if (!video) return
if (isPlayingRef.current) {
video.pause()
setIsPlaying(false)
} else {
video.play().catch((error) => {
console.error("播放失败:", error)
})
setIsPlaying(true)
}
break
case "ArrowLeft":
e.preventDefault()
if (videoRef.current) {
videoRef.current.currentTime = Math.max(
0,
videoRef.current.currentTime - 10
)
}
break
case "ArrowRight":
e.preventDefault()
if (videoRef.current && duration) {
videoRef.current.currentTime = Math.min(
duration,
videoRef.current.currentTime + 10
)
}
break
case "ArrowUp":
e.preventDefault()
setVolume((prev) => {
const newVolume = Math.min(1, prev + 0.1)
if (videoRef.current) {
videoRef.current.volume = newVolume
setIsMuted(newVolume === 0)
}
return newVolume
})
break
case "ArrowDown":
e.preventDefault()
setVolume((prev) => {
const newVolume = Math.max(0, prev - 0.1)
if (videoRef.current) {
videoRef.current.volume = newVolume
setIsMuted(newVolume === 0)
}
return newVolume
})
break
case "KeyM":
e.preventDefault()
toggleMute()
break
case "KeyF":
e.preventDefault()
toggleFullscreen()
break
}
}
// 键盘事件监听
useEffect(() => {
document.addEventListener("keydown", handleKeyDown)
if (containerRef.current) {
containerRef.current.tabIndex = 0
}
return () => {
document.removeEventListener("keydown", handleKeyDown)
}
}, [duration])
// 计算进度条百分比
const progressPercent = duration ? (currentTime / duration) * 100 : 0
// 是否隐藏logo
const hideLogo = useMemo(() => {
return localStorage.getItem("hideLogo") === "true"
}, [])
return (
<div
ref={containerRef}
className="relative w-full h-screen bg-black flex justify-center items-center"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}>
<video
ref={videoRef}
className="w-full h-full bg-black [&::-webkit-media-controls]:hidden [&::-webkit-media-controls-start-playback-button]:hidden"
onClick={togglePlayPause}
playsInline
preload="auto">
<source src={iodVideo} type="video/mp4" />
HTML5视频播放
</video>
{/* 暂停时的遮罩层 */}
{!isPlaying && !isBuffering && (
<div
className="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center cursor-pointer"
onClick={togglePlayPause}>
<PlayCircleOutlined className="text-white text-6xl opacity-80" />
</div>
)}
{/* 缓冲提示 */}
{isBuffering && (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white text-sm bg-black bg-opacity-50 px-4 py-2 rounded">
...
</div>
)}
{/* 控制栏 - 使用与原始HTML相同的类名和行为 */}
{createPortal(
<div
className={`fixed left-0 w-full bg-gradient-to-t from-black to-transparent p-4 transition-all duration-300 ease-in-out flex flex-col gap-2.5 z-50 ${showControls ? "bottom-0" : "-bottom-40"}`}>
<div
className="flex items-center justify-end gap-2 cursor-pointer"
onClick={handleEnded}>
{!hideLogo && <img src={logo} alt="logo" className="w-8" />}
<h2 className="text-xl font-bold text-white dark:text-zinc-300 mr-3">
<span className="text-[#d30100]"></span>
</h2>
</div>
<div
className="w-full h-1.5 bg-white bg-opacity-20 rounded cursor-pointer mb-2.5"
onClick={handleProgressClick}>
<div
className="h-full bg-gradient-to-r from-orange-500 to-pink-600 rounded transition-all duration-100"
style={{ width: `${progressPercent}%` }}></div>
</div>
<div className="flex items-center gap-4">
<button
className="bg-transparent border-none text-white text-lg cursor-pointer p-1 rounded-full w-12 h-12 flex items-center justify-center hover:bg-white hover:bg-opacity-20 transition-colors"
onClick={togglePlayPause}>
{isPlaying ? (
<PauseCircleOutlined className="text-2xl" />
) : (
<PlayCircleOutlined className="text-2xl" />
)}
</button>
<span className="text-white text-sm min-w-[100px] text-center">
<span>{formatTime(currentTime)}</span> /
<span>{formatTime(duration)}</span>
</span>
<div className="flex items-center ml-auto">
<button
className="bg-transparent border-none text-white text-2xl cursor-pointer p-1 rounded-full w-12 h-12 flex items-center justify-center hover:bg-white hover:bg-opacity-20 transition-colors"
onClick={toggleMute}>
{isMuted ? "🔇" : "🔊"}
</button>
<input
type="range"
min="0"
max="1"
step="0.1"
value={isMuted ? 0 : volume}
onChange={handleVolumeChange}
className="w-20 h-1.5 ml-2.5"
/>
</div>
<button
className="bg-transparent border-none text-white text-lg cursor-pointer p-1 rounded-full w-12 h-12 flex items-center justify-center hover:bg-white hover:bg-opacity-20 transition-colors"
onClick={toggleFullscreen}>
<ExpandOutlined className="text-2xl" />
</button>
</div>
</div>,
document.body
)}
</div>
)
}
export default VideoPlayer

View File

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

View File

@ -5,6 +5,12 @@ interface PageAssistContext {
messages: Message[] messages: Message[]
setMessages: Dispatch<SetStateAction<Message[]>> setMessages: Dispatch<SetStateAction<Message[]>>
currentMessageId: string
setCurrentMessageId: Dispatch<SetStateAction<string>>
iodLoading: boolean
setIodLoading: Dispatch<SetStateAction<boolean>>
controller: AbortController | null controller: AbortController | null
setController: Dispatch<SetStateAction<AbortController>> setController: Dispatch<SetStateAction<AbortController>>
@ -16,6 +22,12 @@ export const PageAssistContext = createContext<PageAssistContext>({
messages: [], messages: [],
setMessages: () => {}, setMessages: () => {},
currentMessageId: "",
setCurrentMessageId: () => {},
iodLoading: false,
setIodLoading: () => {},
controller: null, controller: null,
setController: () => {}, setController: () => {},

0
src/cs.js Normal file
View File

View File

@ -1,7 +1,7 @@
import { import { type ChatHistory as ChatHistoryType } from "~/store/option"
type ChatHistory as ChatHistoryType, import { AllIodRegistryEntry } from "@/types/iod.ts"
type Message as MessageType import { type Message as MessageType } from "@/types/message.ts"
} from "~/store/option" import { getDefaultIodSources } from "@/libs/iod.ts"
type HistoryInfo = { type HistoryInfo = {
id: string id: string
@ -29,8 +29,8 @@ type Message = {
role: string role: string
content: string content: string
images?: string[] images?: string[]
sources?: string[] webSources?: string[]
iodSources?:string[] iodSources?: AllIodRegistryEntry
search?: WebSearch search?: WebSearch
createdAt: number createdAt: number
reasoning_time_taken?: number reasoning_time_taken?: number
@ -239,7 +239,7 @@ export const generateID = () => {
export const saveHistory = async ( export const saveHistory = async (
title: string, title: string,
is_rag?: boolean, is_rag?: boolean,
message_source?: "copilot" | "web-ui", message_source?: "copilot" | "web-ui"
) => { ) => {
const id = generateID() const id = generateID()
const createdAt = Date.now() const createdAt = Date.now()
@ -248,38 +248,31 @@ export const saveHistory = async (
await db.addChatHistory(history) await db.addChatHistory(history)
return history return history
} }
export type HistoryMessage = {
export const saveMessage = async ( history_id: string
history_id: string, name: string
name: string, role: string
role: string, content: string
content: string, images: string[]
images: string[], iodSearch?: boolean
source?: any[], webSearch?: boolean
iodSource?:any[], webSources?: any[]
time?: number, iodSources?: AllIodRegistryEntry
message_type?: string, createdAt?: number
generationInfo?: any, messageType?: string
generationInfo?: any
reasoning_time_taken?: number reasoning_time_taken?: number
) => { }
export const saveMessage = async (msg: HistoryMessage): Promise<Message> => {
const id = generateID() const id = generateID()
let createdAt = Date.now() let createdAt = Date.now()
if (time) { if (msg.createdAt) {
createdAt += time createdAt += msg.createdAt
} }
const message = { const message = {
...msg,
id, id,
history_id,
name,
role,
content,
images,
createdAt, createdAt,
sources: source,
iodSources:iodSource,
messageType: message_type,
generationInfo: generationInfo,
reasoning_time_taken
} }
const db = new PageAssitDatabase() const db = new PageAssitDatabase()
await db.addMessage(message) await db.addMessage(message)
@ -303,11 +296,12 @@ export const formatToMessage = (messages: MessageHistory): MessageType[] => {
messages.sort((a, b) => a.createdAt - b.createdAt) messages.sort((a, b) => a.createdAt - b.createdAt)
return messages.map((message) => { return messages.map((message) => {
return { return {
...message,
isBot: message.role === "assistant", isBot: message.role === "assistant",
message: message.content, message: message.content,
name: message.name, name: message.name,
sources: message?.sources || [], webSources: message?.webSources || [],
iodSources: message?.iodSources || [], iodSources: message?.iodSources || getDefaultIodSources(),
images: message.images || [], images: message.images || [],
generationInfo: message?.generationInfo, generationInfo: message?.generationInfo,
reasoning_time_taken: message?.reasoning_time_taken reasoning_time_taken: message?.reasoning_time_taken

75
src/db/iod.ts Normal file
View File

@ -0,0 +1,75 @@
const iodConnection = "iodConnection-g3"
export const defaultIodConnectionConfig = {
gatewayUrl: "tcp://reg01.public.internetofdata.cn:21037",
registry: "data/Registry",
localRepository: "data/Repository",
doBrowser: "http://021.node.internetapi.cn:21030/SCIDE/SCManager"
} as const
export type IodConnectionConfig = {
gatewayUrl: string
registry: string
localRepository: string
doBrowser: string
}
export class IodDb {
private static instance: IodDb
private static iodConnectionConfig: IodConnectionConfig | null = null
// 单例模式
static getInstance(): IodDb {
if (!IodDb.instance) {
IodDb.instance = new IodDb()
}
return IodDb.instance
}
insertIodConnection(config: IodConnectionConfig): void {
try {
localStorage.setItem(iodConnection, JSON.stringify(config))
IodDb.iodConnectionConfig = config
} catch (error) {
console.error('Failed to save IOD connection config:', error)
throw new Error('Failed to save IOD connection configuration')
}
}
getIodConnection(): IodConnectionConfig {
// 如果已经有缓存,直接返回
if (IodDb.iodConnectionConfig) {
return IodDb.iodConnectionConfig
}
try {
const val = localStorage.getItem(iodConnection)
if (!val) {
return defaultIodConnectionConfig
}
IodDb.iodConnectionConfig = JSON.parse(val)
return IodDb.iodConnectionConfig
} catch (error) {
console.warn('Failed to parse IOD connection config, using default:', error)
return defaultIodConnectionConfig
}
}
// 添加清除配置的方法
clearIodConnection(): void {
try {
localStorage.removeItem(iodConnection)
IodDb.iodConnectionConfig = null
} catch (error) {
console.error('Failed to clear IOD connection config:', error)
throw new Error('Failed to clear IOD connection configuration')
}
}
getIodConfig() {
return {
connection: this.getIodConnection(),
}
}
}

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,10 +5,12 @@ import { clearBadge, streamDownload } from "@/utils/pull-ollama"
export default defineBackground({ export default defineBackground({
main() { main() {
let isCopilotRunning: boolean = false let isCopilotRunning: boolean = false
browser.runtime.onMessage.addListener(async (message) => { browser.runtime.onMessage.addListener(async (message,sender,sendResponse) => {
if (message.type === "sidepanel") { switch(message.type){
case "sidepanel":
await browser.sidebarAction.open() await browser.sidebarAction.open()
} else if (message.type === "pull_model") { break;
case "pull_model":
const ollamaURL = await getOllamaURL() const ollamaURL = await getOllamaURL()
const isRunning = await isOllamaRunning() const isRunning = await isOllamaRunning()
@ -21,8 +23,12 @@ export default defineBackground({
clearBadge() clearBadge()
}, 5000) }, 5000)
} }
await streamDownload(ollamaURL, message.modelName) await streamDownload(ollamaURL, message.modelName)
break;
case "retrieveDeepScript":
return retrieveDeepScript(message);
default:
break;
} }
}) })
@ -180,3 +186,52 @@ export default defineBackground({
}, },
persistent: true 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> <!doctype html>
<html> <html>
<head> <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="viewport" content="width=device-width, initial-scale=1">
<meta name="manifest.type" content="browser_action" /> <meta name="manifest.type" content="browser_action" />
<meta name="manifest.browser_style" content="false" /> <meta name="manifest.browser_style" content="false" />

View File

@ -1,7 +1,13 @@
import { saveHistory, saveMessage } from "@/db" import { HistoryMessage, saveHistory, saveMessage } from "@/db"
import { setLastUsedChatModel, setLastUsedChatSystemPrompt } from "@/services/model-settings" import {
setLastUsedChatModel,
setLastUsedChatSystemPrompt
} from "@/services/model-settings"
import { generateTitle } from "@/services/title" import { generateTitle } from "@/services/title"
import { ChatHistory } from "@/store/option" import { ChatHistory } from "@/store/option"
import { updateDialog } from "@/web/iod"
import { AllIodRegistryEntry } from "@/types/iod.ts"
import { getDefaultIodSources } from "@/libs/iod.ts"
export const saveMessageOnError = async ({ export const saveMessageOnError = async ({
e, e,
@ -17,7 +23,9 @@ export const saveMessageOnError = async ({
message_source = "web-ui", message_source = "web-ui",
message_type, message_type,
prompt_content, prompt_content,
prompt_id prompt_id,
iodSearch,
webSearch,
}: { }: {
e: any e: any
setHistory: (history: ChatHistory) => void setHistory: (history: ChatHistory) => void
@ -32,7 +40,9 @@ export const saveMessageOnError = async ({
message_source?: "copilot" | "web-ui" message_source?: "copilot" | "web-ui"
message_type?: string message_type?: string
prompt_id?: string prompt_id?: string
prompt_content?: string prompt_content?: string,
iodSearch?: boolean,
webSearch?: boolean,
}) => { }) => {
if ( if (
e?.name === "AbortError" || e?.name === "AbortError" ||
@ -53,66 +63,63 @@ export const saveMessageOnError = async ({
} }
]) ])
const defaultMessage: HistoryMessage = {
history_id: historyId,
name: selectedModel,
role: "assistant",
content: botMessage,
webSources: [],
iodSources: getDefaultIodSources(),
messageType: message_type,
iodSearch,
webSearch,
images: []
}
if (historyId) { if (historyId) {
if (!isRegenerating) { if (!isRegenerating) {
await saveMessage( await saveMessage({
historyId, ...JSON.parse(JSON.stringify(defaultMessage)),
selectedModel, role: "user",
"user", content: userMessage,
userMessage, images: [image]
[image], })
[],
[],
1,
message_type
)
} }
await saveMessage( await saveMessage({
historyId, ...JSON.parse(JSON.stringify(defaultMessage))
selectedModel, })
"assistant",
botMessage,
[],
[],
[],
2,
message_type
)
await setLastUsedChatModel(historyId, selectedModel) await setLastUsedChatModel(historyId, selectedModel)
if (prompt_id || prompt_content) { if (prompt_id || prompt_content) {
await setLastUsedChatSystemPrompt(historyId, { prompt_content, prompt_id }) await setLastUsedChatSystemPrompt(historyId, {
prompt_content,
prompt_id
})
} }
} else { } else {
const title = await generateTitle(selectedModel, userMessage, userMessage) const title = await generateTitle(selectedModel, userMessage, userMessage)
const newHistoryId = await saveHistory(title, false, message_source) const newHistoryId = await saveHistory(title, false, message_source)
if (!isRegenerating) { if (!isRegenerating) {
await saveMessage( await saveMessage({
newHistoryId.id, ...JSON.parse(JSON.stringify(defaultMessage)),
selectedModel, history_id: newHistoryId.id,
"user", content: userMessage,
userMessage, role: "user",
[image], images: [image]
[], })
[],
1,
message_type
)
} }
await saveMessage( await saveMessage(
newHistoryId.id, {
selectedModel, ...JSON.parse(JSON.stringify(defaultMessage)),
"assistant", history_id: newHistoryId.id,
botMessage, },
[],
[],
[],
2,
message_type
) )
setHistoryId(newHistoryId.id) setHistoryId(newHistoryId.id)
await setLastUsedChatModel(newHistoryId.id, selectedModel) await setLastUsedChatModel(newHistoryId.id, selectedModel)
if (prompt_id || prompt_content) { if (prompt_id || prompt_content) {
await setLastUsedChatSystemPrompt(newHistoryId.id, { prompt_content, prompt_id }) await setLastUsedChatSystemPrompt(newHistoryId.id, {
prompt_content,
prompt_id
})
} }
} }
@ -130,10 +137,13 @@ export const saveMessageOnSuccess = async ({
message, message,
image, image,
fullText, fullText,
source, iodSearch,
iodSource, webSearch,
webSources,
iodSources,
message_source = "web-ui", message_source = "web-ui",
message_type, generationInfo, message_type,
generationInfo,
prompt_id, prompt_id,
prompt_content, prompt_content,
reasoning_time_taken = 0 reasoning_time_taken = 0
@ -145,81 +155,87 @@ export const saveMessageOnSuccess = async ({
message: string message: string
image: string image: string
fullText: string fullText: string
source: any[] iodSearch?: boolean
iodSource: any[] webSearch?: boolean
message_source?: "copilot" | "web-ui", webSources: any[]
iodSources: AllIodRegistryEntry
message_source?: "copilot" | "web-ui"
message_type?: string message_type?: string
generationInfo?: any generationInfo?: any
prompt_id?: string prompt_id?: string
prompt_content?: string prompt_content?: string
reasoning_time_taken?: number reasoning_time_taken?: number
}) => { }) => {
var botMessage
const defaultMessage: HistoryMessage = {
history_id: historyId,
name: selectedModel,
role: "assistant",
content: fullText,
webSources: webSources,
iodSources: iodSources,
messageType: message_type,
images: [],
iodSearch,
webSearch,
generationInfo,
reasoning_time_taken,
}
if (historyId) { if (historyId) {
if (!isRegenerate) { if (!isRegenerate) {
await saveMessage( await saveMessage(
historyId, {
selectedModel, ...JSON.parse(JSON.stringify(defaultMessage)),
"user", role: "user",
message, content: message,
[image], images: [image],
[], webSources: [],
[], iodSources: getDefaultIodSources(),
1, },
message_type,
generationInfo,
reasoning_time_taken
) )
} }
await saveMessage( botMessage = await saveMessage(
historyId, {
selectedModel!, ...JSON.parse(JSON.stringify(defaultMessage)),
"assistant", }
fullText,
[],
source,
iodSource,
2,
message_type,
generationInfo,
reasoning_time_taken
) )
updateDialog(historyId, botMessage)
await setLastUsedChatModel(historyId, selectedModel!) await setLastUsedChatModel(historyId, selectedModel!)
if (prompt_id || prompt_content) { if (prompt_id || prompt_content) {
await setLastUsedChatSystemPrompt(historyId, { prompt_content, prompt_id }) await setLastUsedChatSystemPrompt(historyId, {
prompt_content,
prompt_id
})
} }
} else { } else {
const title = await generateTitle(selectedModel, message, message) const title = await generateTitle(selectedModel, message, message)
const newHistoryId = await saveHistory(title, false, message_source) const newHistoryId = await saveHistory(title, false, message_source)
await saveMessage( await saveMessage(
newHistoryId.id, {
selectedModel, ...JSON.parse(JSON.stringify(defaultMessage)),
"user", history_id: newHistoryId.id,
message, role: "user",
[image], content: message,
[], images: [image],
[], webSources: [],
1, iodSources: getDefaultIodSources(),
message_type, },
generationInfo,
reasoning_time_taken
) )
await saveMessage( botMessage = await saveMessage(
newHistoryId.id, {
selectedModel!, ...JSON.parse(JSON.stringify(defaultMessage)),
"assistant", history_id: newHistoryId.id,
fullText, }
[],
source,
iodSource,
2,
message_type,
generationInfo,
reasoning_time_taken
) )
updateDialog(newHistoryId.id, botMessage)
setHistoryId(newHistoryId.id) setHistoryId(newHistoryId.id)
await setLastUsedChatModel(newHistoryId.id, selectedModel!) await setLastUsedChatModel(newHistoryId.id, selectedModel!)
if (prompt_id || prompt_content) { if (prompt_id || prompt_content) {
await setLastUsedChatSystemPrompt(newHistoryId.id, { prompt_content, prompt_id }) await setLastUsedChatSystemPrompt(newHistoryId.id, {
prompt_content,
prompt_id
})
} }
} }
} }

View File

@ -25,7 +25,9 @@ const useDynamicTextareaSize = (
// Set max-height and adjust overflow behavior if maxHeight is provided // Set max-height and adjust overflow behavior if maxHeight is provided
currentTextarea.style.maxHeight = `${maxHeight}px`; currentTextarea.style.maxHeight = `${maxHeight}px`;
currentTextarea.style.overflowY = contentHeight > maxHeight ? "scroll" : "hidden"; currentTextarea.style.overflowY = contentHeight > maxHeight ? "scroll" : "hidden";
currentTextarea.style.height = `${Math.min(contentHeight, maxHeight)}px`; currentTextarea.style.height = `${Math.min(contentHeight, maxHeight) < 60 ? 60 : Math.min(contentHeight, maxHeight)}px`;
currentTextarea.style.fontWeight = "normal";
currentTextarea.style.color = "#374151";
} else { } else {
// Adjust height without max height constraint // Adjust height without max height constraint

View File

@ -2,16 +2,15 @@ import React from "react"
import { cleanUrl } from "~/libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { import {
defaultEmbeddingModelForRag, defaultEmbeddingModelForRag,
geWebSearchFollowUpPrompt,
getOllamaURL, getOllamaURL,
geWebSearchFollowUpPrompt,
promptForRag, promptForRag,
systemPromptForNonRag systemPromptForNonRag
} from "~/services/ollama" } from "~/services/ollama"
import { useStoreMessageOption, type Message } from "~/store/option" import { useStoreMessageOption } from "~/store/option"
import { useStoreMessage } from "~/store" import { useStoreMessage } from "~/store"
import { SystemMessage } from "@langchain/core/messages" import { SystemMessage } from "@langchain/core/messages"
import { getDataFromCurrentTab } from "~/libs/get-html" import { getDataFromCurrentTab } from "~/libs/get-html"
import { MemoryVectorStore } from "langchain/vectorstores/memory"
import { memoryEmbedding } from "@/utils/memory-embeddings" import { memoryEmbedding } from "@/utils/memory-embeddings"
import { ChatHistory } from "@/store/option" import { ChatHistory } from "@/store/option"
import { import {
@ -42,6 +41,9 @@ import {
mergeReasoningContent, mergeReasoningContent,
removeReasoning removeReasoning
} from "@/libs/reasoning" } from "@/libs/reasoning"
import { AllIodRegistryEntry } from "@/types/iod.ts"
import { getDefaultIodSources } from "@/libs/iod.ts"
import { Message } from "@/types/message.ts"
export const useMessage = () => { export const useMessage = () => {
const { const {
@ -59,6 +61,8 @@ export const useMessage = () => {
setIsSearchingInternet, setIsSearchingInternet,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet isSearchingInternet
} = useStoreMessageOption() } = useStoreMessageOption()
const [defaultInternetSearchOn] = useStorage("defaultInternetSearchOn", false) const [defaultInternetSearchOn] = useStorage("defaultInternetSearchOn", false)
@ -185,16 +189,16 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources:[], iodSources: getDefaultIodSources(),
images: [] images: []
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "", message: "",
sources: [], webSources: [],
iodSources:[], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -205,8 +209,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources:[], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -240,6 +244,7 @@ export const useMessage = () => {
} }
isAlreadyExistEmbedding = keepTrackOfEmbedding[websiteUrl] isAlreadyExistEmbedding = keepTrackOfEmbedding[websiteUrl]
} }
setMessages(newMessage) setMessages(newMessage)
const ollamaUrl = await getOllamaURL() const ollamaUrl = await getOllamaURL()
const embeddingModle = await defaultEmbeddingModelForRag() const embeddingModle = await defaultEmbeddingModelForRag()
@ -337,7 +342,7 @@ export const useMessage = () => {
} }
let context: string = "" let context: string = ""
let source: { let webSources: {
name: any name: any
type: any type: any
mode: string mode: string
@ -345,11 +350,13 @@ export const useMessage = () => {
pageContent: string pageContent: string
metadata: Record<string, any> metadata: Record<string, any>
}[] = [] }[] = []
// TODO: update type
let iodSources: AllIodRegistryEntry = getDefaultIodSources()
if (chatWithWebsiteEmbedding) { if (chatWithWebsiteEmbedding) {
const docs = await vectorstore.similaritySearch(query, 4) const docs = await vectorstore.similaritySearch(query, 4)
context = formatDocs(docs) context = formatDocs(docs)
source = docs.map((doc) => { webSources = docs.map((doc) => {
return { return {
...doc, ...doc,
name: doc?.metadata?.source || "untitled", name: doc?.metadata?.source || "untitled",
@ -368,7 +375,7 @@ export const useMessage = () => {
.slice(0, maxWebsiteContext) .slice(0, maxWebsiteContext)
} }
source = [ webSources = [
{ {
name: embedURL, name: embedURL,
type: type, type: type,
@ -479,7 +486,8 @@ export const useMessage = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source, webSources,
iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
} }
@ -500,7 +508,7 @@ export const useMessage = () => {
content: fullText content: fullText
} }
]) ])
const iodSource = [] debugger
await saveMessageOnSuccess({ await saveMessageOnSuccess({
historyId, historyId,
setHistoryId, setHistoryId,
@ -509,8 +517,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source, webSources,
iodSource, iodSources,
message_source: "copilot", message_source: "copilot",
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
@ -610,16 +618,16 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources:[], iodSources: getDefaultIodSources(),
images: [] images: []
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -630,8 +638,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -794,8 +802,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source: [], webSources: [],
iodSource:[], iodSources: getDefaultIodSources(),
message_source: "copilot", message_source: "copilot",
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
@ -899,16 +907,16 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
images: [image] images: [image]
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -919,8 +927,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -1088,8 +1096,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source: [], webSources: [],
iodSource:[], iodSources: getDefaultIodSources(),
message_source: "copilot", message_source: "copilot",
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
@ -1126,6 +1134,8 @@ export const useMessage = () => {
} }
const searchChatMode = async ( const searchChatMode = async (
webSearch: boolean,
iodSearch,
message: string, message: string,
image: string, image: string,
isRegenerate: boolean, isRegenerate: boolean,
@ -1188,16 +1198,16 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
images: [image] images: [image]
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -1208,8 +1218,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -1286,10 +1296,14 @@ export const useMessage = () => {
query = removeReasoning(query) query = removeReasoning(query)
} }
const { prompt, source, iodSource } = await getSystemPromptForWeb(query, selectedQuickPrompt) const { prompt, webSources, iodSources } = await getSystemPromptForWeb(
query,
[],
webSearch,
iodSearch
)
setIsSearchingInternet(false) setIsSearchingInternet(false)
console.log("iodSource:")
console.log(iodSource)
// message = message.trim().replaceAll("\n", " ") // message = message.trim().replaceAll("\n", " ")
let humanMessage = await humanMessageFormatter({ let humanMessage = await humanMessageFormatter({
@ -1410,8 +1424,8 @@ export const useMessage = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source, webSources,
iodSources: iodSource, iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
} }
@ -1441,8 +1455,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source, webSources,
iodSource, iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
}) })
@ -1541,8 +1555,8 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
images: [image], images: [image],
messageType: messageType messageType: messageType
}, },
@ -1550,8 +1564,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -1562,8 +1576,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -1709,8 +1723,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source: [], webSources: [],
iodSource:[], iodSources: getDefaultIodSources(),
message_source: "copilot", message_source: "copilot",
message_type: messageType, message_type: messageType,
generationInfo, generationInfo,
@ -1788,8 +1802,10 @@ export const useMessage = () => {
) )
} else { } else {
if (chatMode === "normal") { if (chatMode === "normal") {
if (webSearch) { if (webSearch || iodSearch) {
await searchChatMode( await searchChatMode(
webSearch,
iodSearch,
message, message,
image, image,
isRegenerate || false, isRegenerate || false,
@ -1928,6 +1944,8 @@ export const useMessage = () => {
regenerateLastMessage, regenerateLastMessage,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet, isSearchingInternet,
selectedQuickPrompt, selectedQuickPrompt,
setSelectedQuickPrompt, setSelectedQuickPrompt,

View File

@ -2,14 +2,14 @@ import React from "react"
import { cleanUrl } from "~/libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { import {
defaultEmbeddingModelForRag, defaultEmbeddingModelForRag,
geWebSearchFollowUpPrompt,
getOllamaURL, getOllamaURL,
geWebSearchFollowUpPrompt,
promptForRag, promptForRag,
systemPromptForNonRagOption systemPromptForNonRagOption
} from "~/services/ollama" } from "~/services/ollama"
import { type ChatHistory, type Message } from "~/store/option" import type { ChatHistory, MeteringEntry } from "~/store/option"
import { SystemMessage } from "@langchain/core/messages"
import { useStoreMessageOption } from "~/store/option" import { useStoreMessageOption } from "~/store/option"
import { SystemMessage } from "@langchain/core/messages"
import { import {
deleteChatForEdit, deleteChatForEdit,
generateID, generateID,
@ -20,6 +20,8 @@ import {
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { notification } from "antd" import { notification } from "antd"
import { getSystemPromptForWeb } from "~/web/web" import { getSystemPromptForWeb } from "~/web/web"
import { tokenizeInput } from "~/web/iod"
import { generateHistory } from "@/utils/generate-history" import { generateHistory } from "@/utils/generate-history"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { import {
@ -37,23 +39,34 @@ import { pageAssistModel } from "@/models"
import { getNoOfRetrievedDocs } from "@/services/app" import { getNoOfRetrievedDocs } from "@/services/app"
import { humanMessageFormatter } from "@/utils/human-message" import { humanMessageFormatter } from "@/utils/human-message"
import { pageAssistEmbeddingModel } from "@/models/embedding" import { pageAssistEmbeddingModel } from "@/models/embedding"
import { import {
isReasoningEnded, isReasoningEnded,
isReasoningStarted, isReasoningStarted,
mergeReasoningContent, mergeReasoningContent,
removeReasoning removeReasoning
} from "@/libs/reasoning" } from "@/libs/reasoning"
import { getDefaultIodSources } from "@/libs/iod.ts"
import type { Message } from "@/types/message.ts"
export const useMessageOption = () => { export const useMessageOption = () => {
const { const {
controller: abortController, controller: abortController,
setController: setAbortController, setController: setAbortController,
iodLoading,
setIodLoading,
currentMessageId,
setCurrentMessageId,
messages, messages,
setMessages setMessages,
} = usePageAssist() } = usePageAssist()
const { const {
history, history,
setHistory, setHistory,
meteringEntries,
setMeteringEntries,
setCurrentMeteringEntry,
setStreaming, setStreaming,
streaming, streaming,
setIsFirstMessage, setIsFirstMessage,
@ -67,6 +80,8 @@ export const useMessageOption = () => {
setChatMode, setChatMode,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet, isSearchingInternet,
setIsSearchingInternet, setIsSearchingInternet,
selectedQuickPrompt, selectedQuickPrompt,
@ -104,13 +119,38 @@ export const useMessageOption = () => {
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
currentChatModelSettings.reset() currentChatModelSettings.reset()
setIodLoading(false)
setCurrentMessageId("")
textareaRef?.current?.focus() textareaRef?.current?.focus()
if (defaultInternetSearchOn) { if (defaultInternetSearchOn) {
setWebSearch(true) setWebSearch(true)
} }
} }
// 从最后的结果中解析出 思维链 (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 ( const searchChatMode = async (
webSearch: boolean,
iodSearch: boolean,
message: string, message: string,
image: string, image: string,
isRegenerate: boolean, isRegenerate: boolean,
@ -161,40 +201,53 @@ export const useMessageOption = () => {
useMlock: useMlock:
currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock
}) })
let newMessage: Message[] = [] let newMessage: Message[] = []
let generateMessageId = generateID() let generateMessageId = generateID()
setCurrentMessageId(generateMessageId)
const meter: MeteringEntry = {
id: generateMessageId,
queryContent: message,
date: new Date().getTime()
} as MeteringEntry
setCurrentMeteringEntry({
loading: true,
data: meter
})
let defaultMessage: Message = {
isBot: true,
name: selectedModel,
message,
iodSearch,
webSearch,
webSources: [],
iodSources: getDefaultIodSources(),
images: [image]
}
if (!isRegenerate) { if (!isRegenerate) {
newMessage = [ newMessage = [
...messages, ...messages,
{ {
...JSON.parse(JSON.stringify(defaultMessage)),
id: generateID(),
isBot: false, isBot: false,
name: "You", name: "You",
message,
sources: [],
iodSources: [],
images: [image]
}, },
{ {
isBot: true, ...JSON.parse(JSON.stringify(defaultMessage)),
name: selectedModel, id: generateMessageId,
message: "▋", message: "",
sources: [],
iodSources: [],
id: generateMessageId
} }
] ]
} else { } else {
newMessage = [ newMessage = [
...messages, ...messages,
{ {
isBot: true, ...JSON.parse(JSON.stringify(defaultMessage)),
name: selectedModel, id: generateMessageId,
message: "▋", message: " ",
sources: [],
iodSources: [],
id: generateMessageId
} }
] ]
} }
@ -207,7 +260,8 @@ export const useMessageOption = () => {
setIsSearchingInternet(true) setIsSearchingInternet(true)
let query = message let query = message
/* let keywords: string[] = []
if (newMessage.length > 2) { if (newMessage.length > 2) {
let questionPrompt = await geWebSearchFollowUpPrompt() let questionPrompt = await geWebSearchFollowUpPrompt()
const lastTenMessages = newMessage.slice(-10) const lastTenMessages = newMessage.slice(-10)
@ -270,17 +324,60 @@ export const useMessageOption = () => {
query = response.content.toString() query = response.content.toString()
query = removeReasoning(query) query = removeReasoning(query)
} }
// 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 quickPrompt = selectedQuickPrompt; }
console.log("quick prompt:"+quickPrompt)
const { prompt, source, iodSource } = await getSystemPromptForWeb(query, quickPrompt) const {
prompt,
webSources,
iodSources,
iodSearchResults: iodData,
iodTokenCount
} = await getSystemPromptForWeb(query, keywords, webSearch, iodSearch)
setIodLoading(false)
console.log("prompt:\n" + prompt)
setIsSearchingInternet(false) setIsSearchingInternet(false)
console.log("iodSource from useMessageOption:") meter.prompt = prompt
console.log(iodSource) meter.iodKeywords = keywords
console.log("prompt") meter.iodData = Object.values(iodData).flat()
console.log(prompt) meter.iodTokenCount = iodTokenCount
console.log("query")
console.log(query)
setMessages((prev) => {
return prev.map((message) => {
if (message.id === generateMessageId) {
return {
...message,
webSources,
iodSources,
}
}
return message
})
})
// message = message.trim().replaceAll("\n", " ") // message = message.trim().replaceAll("\n", " ")
let humanMessage = await humanMessageFormatter({ let humanMessage = await humanMessageFormatter({
@ -340,6 +437,7 @@ export const useMessageOption = () => {
} }
) )
let count = 0 let count = 0
const chatStartTime = new Date()
let reasoningStartTime: Date | undefined = undefined let reasoningStartTime: Date | undefined = undefined
let reasoningEndTime: Date | undefined = undefined let reasoningEndTime: Date | undefined = undefined
let apiReasoning = false let apiReasoning = false
@ -400,8 +498,8 @@ export const useMessageOption = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source, webSources,
iodSources:iodSource, iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
} }
@ -431,14 +529,38 @@ export const useMessageOption = () => {
message, message,
image, image,
fullText, fullText,
source, iodSearch,
iodSource, webSearch,
webSources,
iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
// Save metering entry
const { cot, content } = responseResolver(fullText)
const currentMeteringEntry = {
...meter,
modelInputTokenCount: prompt.length,
modelOutputTokenCount: fullText.length,
model: ollama.modelName ?? ollama.model,
relatedDataCount: Object.values(iodData).flat()?.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) { } catch (e) {
const errorSave = await saveMessageOnError({ const errorSave = await saveMessageOnError({
e, e,
@ -450,7 +572,9 @@ export const useMessageOption = () => {
setHistory, setHistory,
setHistoryId, setHistoryId,
userMessage: message, userMessage: message,
isRegenerating: isRegenerate isRegenerating: isRegenerate,
iodSearch,
webSearch,
}) })
if (!errorSave) { if (!errorSave) {
@ -556,7 +680,17 @@ export const useMessageOption = () => {
let newMessage: Message[] = [] let newMessage: Message[] = []
let generateMessageId = generateID() let generateMessageId = generateID()
setCurrentMessageId(generateMessageId)
const meter: MeteringEntry = {
id: generateMessageId,
queryContent: message,
date: new Date().getTime()
} as MeteringEntry
setCurrentMeteringEntry({
loading: true,
data: meter
})
if (!isRegenerate) { if (!isRegenerate) {
newMessage = [ newMessage = [
...messages, ...messages,
@ -564,16 +698,17 @@ export const useMessageOption = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], id: generateID(),
iodSources: [], webSources: [],
iodSources: getDefaultIodSources(),
images: [image] images: [image]
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -584,8 +719,8 @@ export const useMessageOption = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -682,6 +817,7 @@ export const useMessageOption = () => {
let reasoningStartTime: Date | null = null let reasoningStartTime: Date | null = null
let reasoningEndTime: Date | null = null let reasoningEndTime: Date | null = null
let apiReasoning: boolean = false let apiReasoning: boolean = false
const chatStartTime = new Date()
for await (const chunk of chunks) { for await (const chunk of chunks) {
if (chunk?.additional_kwargs?.reasoning_content) { if (chunk?.additional_kwargs?.reasoning_content) {
@ -762,7 +898,6 @@ export const useMessageOption = () => {
content: fullText content: fullText
} }
]) ])
await saveMessageOnSuccess({ await saveMessageOnSuccess({
historyId, historyId,
setHistoryId, setHistoryId,
@ -771,8 +906,9 @@ export const useMessageOption = () => {
message, message,
image, image,
fullText, fullText,
iodSearch,
webSearch,
source: [], source: [],
iodSource:[],
generationInfo, generationInfo,
prompt_content: promptContent, prompt_content: promptContent,
prompt_id: promptId, prompt_id: promptId,
@ -783,6 +919,27 @@ export const useMessageOption = () => {
setStreaming(false) setStreaming(false)
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
// Save metering entry
const { cot, content } = responseResolver(fullText)
const currentMeteringEntry = {
...meter,
modelInputTokenCount: prompt? prompt.length : 0,
modelOutputTokenCount: fullText? fullText.length : 0,
model: ollama.modelName ?? ollama.model,
relatedDataCount: 0,
timeTaken: new Date().getTime() - chatStartTime.getTime(),
date: chatStartTime.getTime(),
cot,
responseContent: content,
modelResponseContent: fullText
}
const _meteringEntries = [currentMeteringEntry, ...meteringEntries]
setCurrentMeteringEntry({
loading: false,
data: currentMeteringEntry
})
setMeteringEntries(_meteringEntries)
} catch (e) { } catch (e) {
const errorSave = await saveMessageOnError({ const errorSave = await saveMessageOnError({
e, e,
@ -796,7 +953,9 @@ export const useMessageOption = () => {
userMessage: message, userMessage: message,
isRegenerating: isRegenerate, isRegenerating: isRegenerate,
prompt_content: promptContent, prompt_content: promptContent,
prompt_id: promptId prompt_id: promptId,
iodSearch,
webSearch,
}) })
if (!errorSave) { if (!errorSave) {
@ -871,16 +1030,16 @@ export const useMessageOption = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
images: [] images: []
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -891,8 +1050,8 @@ export const useMessageOption = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [], iodSources: getDefaultIodSources(),
id: generateMessageId id: generateMessageId
} }
] ]
@ -998,8 +1157,7 @@ export const useMessageOption = () => {
} }
}) })
// message = message.trim().replaceAll("\n", " ") // message = message.trim().replaceAll("\n", " ")
const iodSource = []
//TODO not support iodSource in RAG
let humanMessage = await humanMessageFormatter({ let humanMessage = await humanMessageFormatter({
content: [ content: [
{ {
@ -1096,8 +1254,7 @@ export const useMessageOption = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source, webSources: source,
iodSources: iodSource,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
} }
@ -1128,9 +1285,10 @@ export const useMessageOption = () => {
image, image,
fullText, fullText,
source, source,
iodSource,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken,
iodSearch,
webSearch,
}) })
setIsProcessing(false) setIsProcessing(false)
@ -1146,7 +1304,9 @@ export const useMessageOption = () => {
setHistory, setHistory,
setHistoryId, setHistoryId,
userMessage: message, userMessage: message,
isRegenerating: isRegenerate isRegenerating: isRegenerate,
iodSearch,
webSearch,
}) })
if (!errorSave) { if (!errorSave) {
@ -1197,8 +1357,11 @@ export const useMessageOption = () => {
signal signal
) )
} else { } else {
if (webSearch) { if (webSearch || iodSearch) {
setIodLoading(iodSearch)
await searchChatMode( await searchChatMode(
webSearch,
iodSearch,
message, message,
image, image,
isRegenerate, isRegenerate,
@ -1312,6 +1475,10 @@ export const useMessageOption = () => {
editMessage, editMessage,
messages, messages,
setMessages, setMessages,
iodLoading,
currentMessageId,
setIodLoading,
setCurrentMessageId,
onSubmit, onSubmit,
setStreaming, setStreaming,
streaming, streaming,
@ -1333,6 +1500,8 @@ export const useMessageOption = () => {
regenerateLastMessage, regenerateLastMessage,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet, isSearchingInternet,
setIsSearchingInternet, setIsSearchingInternet,
selectedQuickPrompt, selectedQuickPrompt,

View File

@ -6,6 +6,7 @@ import {
} from "@/db" } from "@/db"
import { exportKnowledge, importKnowledge } from "@/db/knowledge" import { exportKnowledge, importKnowledge } from "@/db/knowledge"
import { exportVectors, importVectors } from "@/db/vector" import { exportVectors, importVectors } from "@/db/vector"
import { IodDb } from "@/db/iod"
import { message } from "antd" import { message } from "antd"
export const exportPageAssistData = async () => { export const exportPageAssistData = async () => {
@ -13,12 +14,14 @@ export const exportPageAssistData = async () => {
const chat = await exportChatHistory() const chat = await exportChatHistory()
const vector = await exportVectors() const vector = await exportVectors()
const prompts = await exportPrompts() const prompts = await exportPrompts()
const iod = IodDb.getInstance().getIodConfig()
const data = { const data = {
knowledge, knowledge,
chat, chat,
vector, vector,
prompts prompts,
iod
} }
const dataStr = JSON.stringify(data) const dataStr = JSON.stringify(data)
@ -34,6 +37,7 @@ export const exportPageAssistData = async () => {
} }
export const importPageAssistData = async (file: File) => { export const importPageAssistData = async (file: File) => {
debugger
const reader = new FileReader() const reader = new FileReader()
reader.onload = async () => { reader.onload = async () => {
try { try {
@ -55,6 +59,10 @@ export const importPageAssistData = async (file: File) => {
await importPrompts(data.prompts) await importPrompts(data.prompts)
} }
if(data?.iod) {
IodDb.getInstance().insertIodConnection(data.iod.connection)
}
message.success("Data imported successfully") message.success("Data imported successfully")
} catch (e) { } catch (e) {
console.error(e) console.error(e)

18
src/libs/iod.ts Normal file
View File

@ -0,0 +1,18 @@
import { AllIodRegistryEntry } from "@/types/iod.ts"
export const getDefaultIodSources = (): AllIodRegistryEntry => {
return {
data: {
data: [],
total: 0
},
scenario: {
data: [],
total: 0
},
organization: {
data: [],
total: 0
}
}
}

91
src/libs/playground.tsx Normal file
View File

@ -0,0 +1,91 @@
import { Avatar } from "antd"
import { MedicineBottleFillIcon } from "@/components/Icons/MedicineBottleFill.tsx"
import { CheckIcon } from "@/components/Icons/Check.tsx"
import { NewBottleIcon } from "@/components/Icons/NewBottle.tsx"
import { BatteryIcon } from "@/components/Icons/Battery.tsx"
import { ShipIcon } from "@/components/Icons/Ship.tsx"
import { Ship1Icon } from "@/components/Icons/Ship1.tsx"
export const qaPrompt = [
// {
// title: "如何开发一个适合超大型城市的碳普惠方法学?",
// icon: <img src={RocketSvg} alt="Rocket" className="w-10 my-0" />,
// },
// {
// title: "如何开发一个零碳园区的数字化评价系统?",
// icon: <img src={BulbSvg} alt="Rocket" className="w-10 my-0" />,
// },
// {
// title: "如何开发一个碳定价预测系统?",
// icon: <img src={EyeSvg} alt="Rocket" className="w-10 my-0" />,
// },
{
title: "如何解决固态电池的成本和寿命难题?",
icon: (
<Avatar
className="!bg-[#3581e3b3]"
shape="square"
size={40}
icon={<BatteryIcon className="w-7" />}
/>
)
},
{
title: "如何解决船舶制造中的材料腐蚀难题?",
icon: (
<Avatar
className="!bg-[#3581e3b3]"
shape="square"
size={40}
icon={<ShipIcon className="w-7" />}
/>
)
},
{
title: "如何解决船舶制造中流体模拟和建模优化难题?",
icon: (
<Avatar
className="!bg-[#3581e3b3]"
shape="square"
size={40}
icon={<Ship1Icon className="w-7" />}
/>
)
},
{
title: "新药临床研究如何提升实验安全性?",
icon: (
<Avatar
className="!bg-[#3581e3b3]"
shape="square"
size={40}
icon={<MedicineBottleFillIcon className="w-7" />}
/>
)
},
{
title: "人工智能技术如何加速新药申报和审批?",
icon: (
<Avatar
className="!bg-[#3581e3b3]"
shape="square"
size={40}
icon={<CheckIcon className="w-7" />}
/>
)
},
{
title: "如何研制与利妥昔单抗相似的新药?",
icon: (
<Avatar
className="!bg-[#3581e3b3]"
shape="square"
size={40}
icon={<NewBottleIcon className="w-7" />}
/>
)
}
].map((item, index) => ({
...item,
id: index.toString()
}))

View File

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

View File

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

View File

@ -7,11 +7,14 @@ import OptionOllamaSettings from "./options-settings-ollama"
import OptionShare from "./option-settings-share" import OptionShare from "./option-settings-share"
import OptionKnowledgeBase from "./option-settings-knowledge" import OptionKnowledgeBase from "./option-settings-knowledge"
import OptionAbout from "./option-settings-about" import OptionAbout from "./option-settings-about"
import OptionIodSettings from "./option-settings-iod"
import SidepanelChat from "./sidepanel-chat" import SidepanelChat from "./sidepanel-chat"
import SidepanelSettings from "./sidepanel-settings" import SidepanelSettings from "./sidepanel-settings"
import OptionRagSettings from "./option-rag" import OptionRagSettings from "./option-rag"
import OptionChrome from "./option-settings-chrome" import OptionChrome from "./option-settings-chrome"
import OptionOpenAI from "./option-settings-openai" import OptionOpenAI from "./option-settings-openai"
import OptionMetering from "./option-metering"
import MeteringListDetail from "./metering-list-detail"
export const OptionRoutingChrome = () => { export const OptionRoutingChrome = () => {
return ( return (
@ -26,7 +29,10 @@ export const OptionRoutingChrome = () => {
<Route path="/settings/share" element={<OptionShare />} /> <Route path="/settings/share" element={<OptionShare />} />
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} /> <Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
<Route path="/settings/rag" element={<OptionRagSettings />} /> <Route path="/settings/rag" element={<OptionRagSettings />} />
<Route path="/settings/iod" element={<OptionIodSettings />} />
<Route path="/settings/about" element={<OptionAbout />} /> <Route path="/settings/about" element={<OptionAbout />} />
<Route path="/metering" element={<OptionMetering />} />
<Route path="/metering/list/:id" element={<MeteringListDetail />} />
</Routes> </Routes>
) )
} }

Some files were not shown because too many files have changed in this diff Show More