Compare commits

..

No commits in common. "main" and "feat/page" have entirely different histories.

96 changed files with 1435 additions and 8112 deletions

2
.gitignore vendored
View File

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

View File

@ -49,9 +49,7 @@ cd page-assist
2. Install the dependencies
```bash
export PATH="/Users/huaqiancai/.bun/bin/:$PATH"
bun install
```
3. Build the extension (by default it will build for Chrome)

2552
bun.lock

File diff suppressed because one or more lines are too long

BIN
bun.lockb Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 KiB

View File

@ -1,6 +1,6 @@
{
"name": "pageassist",
"displayName": "IoD Bot - A Web UI for Local AI Models",
"displayName": "Page Assist - A Web UI for Local AI Models",
"version": "1.0.9",
"description": "Use your locally running AI models to assist you in your web browsing.",
"author": "n4ze3m",
@ -35,7 +35,6 @@
"cheerio": "^1.0.0-rc.12",
"d3-dsv": "2",
"dayjs": "^1.11.10",
"framer-motion": "^12.23.12",
"html-to-text": "^9.0.5",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.0",
@ -48,7 +47,6 @@
"property-information": "^6.4.1",
"pubsub-js": "^1.9.4",
"react": "18.2.0",
"react-countup": "^6.5.3",
"react-dom": "18.2.0",
"react-i18next": "^14.1.0",
"react-icons": "^5.2.1",
@ -60,8 +58,6 @@
"rehype-mathjax": "4.0.3",
"remark-gfm": "3.0.1",
"remark-math": "5.1.1",
"segmentit": "^2.0.3",
"styled-components": "^6.1.19",
"tesseract.js": "^5.1.1",
"turndown": "^7.1.3",
"unist-util-visit": "^5.0.0",
@ -79,7 +75,6 @@
"@types/react-dom": "18.2.18",
"@types/react-speech-recognition": "^3.9.5",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/styled-components": "^5.1.34",
"@types/turndown": "^5.0.4",
"autoprefixer": "^10.4.17",
"cross-env": "^7.0.3",
@ -93,4 +88,4 @@
"resolutions": {
"@langchain/core": "0.1.45"
}
}
}

View File

@ -1,30 +1,30 @@
{
"newChat": "New Chat",
"selectAPrompt": "Select a Prompt",
"githubRepository": "GitHub Repository",
"settings": "Settings",
"metering": "Metering",
"sidebarTitle": "Chat History",
"error": "Error",
"somethingWentWrong": "Something went wrong",
"validationSelectModel": "Please select a model to continue",
"deleteHistoryConfirmation": "Are you sure you want to delete this history?",
"editHistoryTitle": "Enter a new title",
"temporaryChat": "Temporary Chat",
"more": {
"copy": {
"group": "Copy",
"asText": "Copy as Text",
"asMarkdown": "Copy as Markdown",
"success": "Copied to clipboard!"
},
"download": {
"group": "Download",
"text": "Text File (.txt)",
"markdown": "Markdown (.md)",
"json": "JSON File (.json)",
"image": "Image (.png)"
},
"share": "Share"
}
}
"newChat": "New Chat",
"selectAPrompt": "Select a Prompt",
"githubRepository": "GitHub Repository",
"settings": "Settings",
"metering": "Metering",
"sidebarTitle": "Chat History",
"error": "Error",
"somethingWentWrong": "Something went wrong",
"validationSelectModel": "Please select a model to continue",
"deleteHistoryConfirmation": "Are you sure you want to delete this history?",
"editHistoryTitle": "Enter a new title",
"temporaryChat": "Temporary Chat",
"more": {
"copy": {
"group": "Copy",
"asText": "Copy as Text",
"asMarkdown": "Copy as Markdown",
"success": "Copied to clipboard!"
},
"download": {
"group": "Download",
"text": "Text File (.txt)",
"markdown": "Markdown (.md)",
"json": "JSON File (.json)",
"image": "Image (.png)"
},
"share": "Share"
}
}

View File

@ -1,5 +1,5 @@
{
"pageAssist": "IoD Bot",
"pageAssist": "Page Assist",
"selectAModel": "选择一个模型",
"save": "保存",
"saved": "已保存",
@ -38,7 +38,7 @@
}
},
"copyToClipboard": "复制到剪贴板",
"webSearch": "搜索中...",
"webSearch": "搜索万维网",
"iodSearch": "搜索数联网",
"regenerate": "重新生成",
"edit": "编辑",

View File

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

View File

@ -19,7 +19,7 @@
}
},
"tooltip": {
"searchInternet": "深度搜索",
"searchInternet": "搜索万维网",
"searchIod": "搜索数联网",
"speechToText": "语音到文本",
"uploadImage": "上传图片",
@ -29,4 +29,4 @@
"sendWhenEnter": "按Enter发送",
"welcome": "你好!今天我能帮你什么?",
"useOCR": "从图片中提取文字OCR"
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

View File

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

View File

@ -1,41 +0,0 @@
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({
message,
className = "prose-lg break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
className = "prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
}: {
message: string
className?: string

View File

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

View File

@ -1,139 +0,0 @@
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

@ -1,91 +0,0 @@
// 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,8 +1,7 @@
import { useForm } from "@mantine/form"
import React, { useEffect, useState } from "react"
import React from "react"
import { useTranslation } from "react-i18next"
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
import TextArea from "antd/es/input/TextArea"
type Props = {
value: string
@ -15,14 +14,6 @@ export const EditMessageForm = (props: Props) => {
const [isComposing, setIsComposing] = React.useState(false)
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
const { t } = useTranslation("common")
const [value, setValue] = useState(props.value);
useEffect(
() => {
setValue(props.value)
},
[props.value]
);
const form = useForm({
initialValues: {
@ -38,27 +29,46 @@ export const EditMessageForm = (props: Props) => {
return (
<form
onSubmit={form.onSubmit((data) => {
if (isComposing) return
props.onClose()
props.onSumbit(value, true)
props.onSumbit(data.message, true)
})}
className="flex flex-col gap-2 w-96 ml-auto">
<TextArea
className="flex flex-col gap-2">
<textarea
{...form.getInputProps("message")}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
required
rows={2}
value={value}
rows={1}
style={{ minHeight: "60px" }}
tabIndex={0}
onChange={(e) => {
setValue(e.target.value)
}}
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={`w-full flex ${
!props.isBot ? "justify-end" : "justify-end"
!props.isBot ? "justify-between" : "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">
<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
onClick={props.onClose}
@ -66,12 +76,6 @@ 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">
{t("cancel")}
</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>{" "}

View File

@ -1,482 +0,0 @@
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,34 +1,26 @@
import Markdown from "../../Common/Markdown"
import React from "react"
import { Collapse, Image, Popover, Tag, Tooltip } from "antd"
import { Tag, Image, Tooltip, Collapse, Popover } from "antd"
import { WebSearch } from "./WebSearch"
import {
ArrowUpSquare,
CheckIcon,
ClipboardIcon,
InfoIcon,
MessageSquareShare,
Pen,
PlayIcon,
RotateCcw,
Square,
Star,
ThumbsDown,
ThumbsUp
Square
} from "lucide-react"
import { EditMessageForm } from "./EditMessageForm"
import { useTranslation } from "react-i18next"
import { MessageSource } from "./MessageSource"
import { useTTS } from "@/hooks/useTTS"
import { tagColors } from "@/utils/color"
import { removeModelSuffix } from "@/db/models"
import { GenerationInfo } from "./GenerationInfo"
import { parseReasoning } from "@/libs/reasoning"
import { humanizeMilliseconds } from "@/utils/humanize-milliseconds"
import { AllIodRegistryEntry } from "@/types/iod.ts"
import { PiNetwork } from "react-icons/pi"
type Props = {
id?: string
message: string
message_type?: string
hideCopy?: boolean
@ -45,30 +37,50 @@ type Props = {
webSearch?: {}
isSearchingInternet?: boolean
webSources?: any[]
iodSources?: AllIodRegistryEntry
iodSources?: any[]
hideEditAndRegenerate?: boolean
onSourceClick?: (source: any) => void
isTTSEnabled?: boolean
generationInfo?: any
isStreaming: boolean
reasoningTimeTaken?: number
iodSearch?: boolean
setCurrentMessageId: (id: string) => void
}
export const PlaygroundMessage: React.FC<Props> = (props) => {
export const PlaygroundMessage = (props: Props) => {
const [isBtnPressed, setIsBtnPressed] = React.useState(false)
const [editMode, setEditMode] = React.useState(false)
const { t } = useTranslation("common")
const { cancel, isSpeaking, speak } = useTTS()
return (
<div className="group relative flex w-full flex-col items-end justify-center pb-2 md:px-4 text-gray-800 dark:text-gray-100">
<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="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-1 md:gap-1 my-2 m-auto w-full ${props.isBot ? "" : "flex-row-reverse"}`}>
<div className="flex flex-col gap-2">
<span className="text-xs font-bold text-gray-800 dark:text-white"></span>
<div className="flex flex-row gap-4 md:gap-6 my-2 m-auto w-full">
<div className="w-8 flex flex-col relative items-end">
<div className="relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center text-opacity-100r">
{props.isBot ? (
!props.botAvatar ? (
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400"></div>
) : (
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.isSearchingInternet &&
@ -82,7 +94,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
</Tag>
)}
</div>
<div className={`flex flex-grow flex-col w-full`}>
<div className="flex flex-grow flex-col">
{!editMode ? (
props.isBot ? (
<>
@ -91,7 +103,6 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
return (
<Collapse
key={i}
defaultActiveKey={["reasoning"]}
className="border-none !mb-3"
items={[
{
@ -121,17 +132,11 @@ export const PlaygroundMessage: React.FC<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
className={`bg-[#2563eb] font-normal rounded-tr-none
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 ${
props.message_type && "italic dark:text-gray-400"
} flex flex-row-reverse`}>
className={`prose dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${
props.message_type &&
"italic text-gray-500 dark:text-gray-400 text-sm"
}`}>
{props.message}
</p>
)
@ -166,10 +171,9 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
<Collapse
className="mt-6"
ghost
// defaultActiveKey={['webSources']}
items={[
{
key: "webSources",
key: "1",
label: (
<div className="italic text-gray-500 dark:text-gray-400">
{t("webCitations")}
@ -191,42 +195,34 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
]}
/>
)}
{props.isBot &&
props?.iodSources &&
Object.values(props?.iodSources)
.map((item) => item.data)
.flat().length > 0 && (
<Collapse
className="mt-6"
ghost
// defaultActiveKey={['iod']}
items={[
{
key: "iod",
label: (
<div className="italic text-gray-500 dark:text-gray-400">
{t("iodCitations")}
</div>
),
children: (
<div className="mb-3 flex flex-wrap gap-2">
{Object.values(props?.iodSources)
.map((item) => item.data)
.flat()
?.map((source, index) => (
<MessageSource
onSourceClick={props.onSourceClick}
key={index}
index={index}
source={source}
/>
))}
</div>
)
}
]}
/>
)}
{props.isBot && props?.iodSources && props?.iodSources.length > 0 && (
<Collapse
className="mt-6"
ghost
items={[
{
key: "1",
label: (
<div className="italic text-gray-500 dark:text-gray-400">
{t("iodCitations")}
</div>
),
children: (
<div className="mb-3 flex flex-wrap gap-2">
{props?.iodSources?.map((source, index) => (
<MessageSource
onSourceClick={props.onSourceClick}
key={index}
index={index}
source={source}
/>
))}
</div>
)
}
]}
/>
)}
{!props.isProcessing && !editMode ? (
<div
className={`space-x-2 gap-2 flex ${
@ -239,7 +235,7 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
// : "flex"
}`}>
{props.isTTSEnabled && (
<Tooltip title={t("tts")} className="hidden">
<Tooltip title={t("tts")}>
<button
aria-label={t("tts")}
onClick={() => {
@ -262,18 +258,6 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
)}
{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 && (
<Tooltip title={t("copyToClipboard")}>
<button
@ -297,7 +281,6 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
{props.generationInfo && (
<Popover
className="hidden"
content={
<GenerationInfo generationInfo={props.generationInfo} />
}
@ -323,8 +306,8 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
)}
</>
)}
{(!props.hideEditAndRegenerate && !props.isBot) && (
<Tooltip title={t("edit")} className="hidden">
{!props.hideEditAndRegenerate && (
<Tooltip title={t("edit")}>
<button
onClick={() => setEditMode(true)}
aria-label={t("edit")}
@ -333,51 +316,6 @@ export const PlaygroundMessage: React.FC<Props> = (props) => {
</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">
<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>
) : (
// add invisible div to prevent layout shift

View File

@ -1,150 +0,0 @@
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

@ -1,158 +0,0 @@
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

@ -1,46 +0,0 @@
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,13 +1,15 @@
import { Form, Image, Input, message, Modal } from "antd"
import React from "react"
import { Form, Image, Input, Modal, Tooltip, message } 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 { useMutation } from "@tanstack/react-query"
import { getPageShareUrl } from "~/services/ollama"
import { cleanUrl } from "~/libs/clean-url"
import { getTitleById, getUserId, saveWebshare } from "@/db"
import { useTranslation } from "react-i18next"
import fetcher from "@/libs/fetcher"
import { Message } from "@/types/message.ts"
type Props = {
messages: Message[]

View File

@ -1,21 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,33 +0,0 @@
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

@ -1,22 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,22 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,23 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,22 +0,0 @@
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,156 +1,255 @@
import React, { useMemo, useState } from "react"
import { useOptionLayoutContext } from "@/components/Layouts/Layout.tsx"
import { PanelLeftIcon } from "lucide-react"
import { Button, Tooltip } from "antd"
import { PlusOutlined } from "@ant-design/icons"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
import { useStorage } from "@plasmohq/storage/hook"
import {
BrainCog,
ChevronLeft,
ChevronRight,
CogIcon,
ComputerIcon,
Slice,
GithubIcon,
PanelLeftIcon,
ZapIcon
} from "lucide-react"
import { useTranslation } from "react-i18next"
import { NavLink, useLocation } from "react-router-dom"
import logo from "@/assets/logo.png"
import { BellIcon } from "@/components/Icons/Bell.tsx"
import { ShareIcon } from "@/components/Icons/Share.tsx"
import { NotCollectIcon } from "@/components/Icons/NotCollect.tsx"
import { CollectIcon } from "@/components/Icons/Collect.tsx"
import { SettingIcon } from "@/components/Icons/Setting.tsx"
import { useLocation, NavLink } from "react-router-dom"
import { SelectedKnowledge } from "../Option/Knowledge/SelectedKnowledge"
import { ModelSelect } from "../Common/ModelSelect"
import { PromptSelect } from "../Common/PromptSelect"
import { useQuery } from "@tanstack/react-query"
import { fetchChatModels } from "~/services/ollama"
import { useMessageOption } from "~/hooks/useMessageOption"
import { Select, Tooltip } from "antd"
import { getAllPrompts } from "@/db"
import { ProviderIcons } from "../Common/ProviderIcon"
import { NewChat } from "./NewChat"
import { PageAssistSelect } from "../Select"
import { MoreOptions } from "./MoreOptions"
type Props = {
setSidebarOpen: (open: boolean) => void
setOpenModelSettings: (open: boolean) => void
}
type Props = {}
export const Header: React.FC<Props> = ({
setOpenModelSettings,
setSidebarOpen
}) => {
const { t, i18n } = useTranslation(["option", "common"])
const isRTL = i18n?.dir() === "rtl"
export const Header: React.FC<Props> = ({}) => {
const location = useLocation()
const [shareModeEnabled] = useStorage("shareMode", false)
const [hideCurrentChatModelSettings] = useStorage(
"hideCurrentChatModelSettings",
false
)
const {
selectedModel,
setSelectedModel,
clearChat,
selectedSystemPrompt,
setSelectedQuickPrompt,
setSelectedSystemPrompt,
messages,
streaming,
historyId,
temporaryChat
} = useMessageOption()
const {
data: models,
isLoading: isModelsLoading,
refetch
} = useQuery({
queryKey: ["fetchModel"],
queryFn: () => fetchChatModels({ returnEmpty: true }),
refetchIntervalInBackground: false,
placeholderData: (prev) => prev
})
const { showOptionSidebar, setShowOptionSidebar } = useOptionLayoutContext()
const { data: prompts, isLoading: isPromptLoading } = useQuery({
queryKey: ["fetchAllPromptsLayout"],
queryFn: getAllPrompts
})
const showLeft = useMemo<boolean>(() => {
console.log(location.pathname)
if (location.pathname.includes("/settings")) {
return true
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
}
return showOptionSidebar
}, [location.pathname, showOptionSidebar])
const prompt = getPromptInfoById(value)
if (prompt?.is_system) {
setSelectedSystemPrompt(prompt.id)
} else {
setSelectedSystemPrompt(undefined)
setSelectedQuickPrompt(prompt!.content)
}
}
const { t } = useTranslation(["option", "common", "settings"])
const { clearChat } = useMessageOption()
// 是否隐藏logo
const hideLogo = useMemo(() => {
return localStorage.getItem("hideLogo") === "true"
}, [])
const [collect, setCollect] = useState<boolean>(false)
return (
<div
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]" : ""}`}>
{/*控制侧边栏显示隐藏与新建对话*/}
{!showLeft && (
<div className="flex items-center gap-3">
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>
<button
className="text-gray-500 dark:text-gray-400"
onClick={() => {
setShowOptionSidebar(!showOptionSidebar)
}}>
onClick={() => setSidebarOpen(true)}>
<PanelLeftIcon className="w-6 h-6" />
</button>
<Button
color="cyan"
variant="filled"
shape="round"
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>{t("newChat")}</span>
</div>
</div>
</Button>
</div>
)}
{location.pathname.includes("/settings") && (
<h2 className="text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3">
<NavLink
to="/"
className="!text-gray-500 dark:text-gray-400 flex items-center gap-2 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
{!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>
<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 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>
</Tooltip>
<div className="flex flex-1 justify-end px-4">
<div className="ml-4 flex items-center md:ml-6">
<div className="flex gap-4 items-center">
{messages.length > 0 && !streaming && (
<MoreOptions
shareModeEnabled={shareModeEnabled}
historyId={historyId}
messages={messages}
/>
)}
{!hideCurrentChatModelSettings && (
<Tooltip title={t("common:currentChatModelSettings")}>
<button
onClick={() => setOpenModelSettings(true)}
className="!text-gray-500 dark:text-gray-300 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<BrainCog className="w-6 h-6" />
</button>
</Tooltip>
)}
<Tooltip title={t("githubRepository")}>
<a
href="https://github.com/n4ze3m/page-assist"
target="_blank"
className="!text-gray-500 hidden lg:block dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<GithubIcon className="w-6 h-6" />
</a>
</Tooltip>
<Tooltip title={t("settings")}>
<NavLink
to="/settings"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<CogIcon className="w-6 h-6" />
</NavLink>
</Tooltip>
<Tooltip title={t("metering")}>
<NavLink
to="/metering"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<Slice className="w-6 h-6" />
</NavLink>
</Tooltip>
</div>
</div>
</div>
</div>
)

View File

@ -1,86 +1,104 @@
import React, { useContext, useState } from "react"
import React, { 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 { Header } from "./Header.tsx"
import IodVideo from "@/components/Option/VideoPlayer"
interface OptionLayoutContextType {
showOptionSidebar: boolean
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}
</>
)
}
import { Header } from "./Header"
import { EraserIcon } from "lucide-react"
import { PageAssitDatabase } from "@/db"
import { useMessageOption } from "@/hooks/useMessageOption"
import { useQueryClient } from "@tanstack/react-query"
import { useStoreChatModelSettings } from "@/store/model"
export default function OptionLayout({
children
}: {
children: React.ReactNode
}) {
const [sidebarOpen, setSidebarOpen] = useState(false)
const { t } = useTranslation(["option", "common", "settings"])
const [openModelSettings, setOpenModelSettings] = useState(false)
const {
setMessages,
setHistory,
setHistoryId,
historyId,
clearChat,
setSelectedModel,
temporaryChat,
setSelectedSystemPrompt
} = useMessageOption()
const queryClient = useQueryClient()
const { setSystemPrompt } = useStoreChatModelSettings()
return (
<div className="flex h-full w-full">
<main className="relative h-dvh w-full">
{/*<div className="relative z-10 w-full">*/}
{/*</div>*/}
<div className="relative z-10 w-full">
<Header
setSidebarOpen={setSidebarOpen}
setOpenModelSettings={setOpenModelSettings}
/>
</div>
{/* <div className="relative flex h-full flex-col items-center"> */}
<OptionLayoutProvider>
<OptionLayoutMain>{children}</OptionLayoutMain>
</OptionLayoutProvider>
{children}
{/* </div> */}
<Drawer
title={
<div className="flex items-center justify-between">
{t("sidebarTitle")}
<Tooltip
title={t(
"settings:generalSettings.system.deleteChatHistory.label"
)}
placement="right">
<button
onClick={async () => {
const confirm = window.confirm(
t(
"settings:generalSettings.system.deleteChatHistory.confirm"
)
)
if (confirm) {
const db = new PageAssitDatabase()
await db.deleteAllChatHistory()
await queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
clearChat()
}
}}
className="text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100">
<EraserIcon className="size-5" />
</button>
</Tooltip>
</div>
}
placement="left"
closeIcon={null}
onClose={() => setSidebarOpen(false)}
open={sidebarOpen}>
<Sidebar
onClose={() => setSidebarOpen(false)}
setMessages={setMessages}
setHistory={setHistory}
setHistoryId={setHistoryId}
setSelectedModel={setSelectedModel}
setSelectedSystemPrompt={setSelectedSystemPrompt}
clearChat={clearChat}
historyId={historyId}
setSystemPrompt={setSystemPrompt}
temporaryChat={temporaryChat}
history={history}
/>
</Drawer>
<CurrentChatModelSettings
open={openModelSettings}
setOpen={setOpenModelSettings}

View File

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

View File

@ -1,264 +0,0 @@
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,192 @@
import {
Card,
List,
Table,
Tag,
Space,
TableProps,
Divider,
Typography,
Tooltip
} from "antd"
import { NavLink } from "react-router-dom"
const data = [
{
key: "输出token数",
value: 2
},
{
key: "输入token数",
value: 2
},
{
key: "模型",
value: "xxx"
}
]
const inputTokenData = [
{
key: "关键词提示",
value: "xxx"
},
{
key: "问题",
value: "xxx"
},
{
key: "数联网引用数据",
value: "xxx"
},
{
key: "提供方",
value: "xxx"
},
{
key: "token数量",
value: 2
},
{
key: "内容",
value: "xxx"
}
]
const outputTokenData = [
{
key: "类型",
value: "xxx"
},
{
key: "来源",
value: "xxx"
},
{
key: "token数量",
value: 2
},
{
key: "内容",
value: "xxx"
}
]
interface DataType {
key: string
name: string
age: number
address: string
tags: number
content: string
}
const columns: TableProps<DataType>["columns"] = [
{
title: "序号",
dataIndex: "key",
key: "name",
render: (text) => <a>{text}</a>
},
{
title: "标识",
dataIndex: "age",
key: "age"
},
{
title: "提供方",
dataIndex: "address",
key: "address"
},
{
title: "token数量",
key: "tags",
dataIndex: "tags"
},
{
title: "内容",
key: "content",
dataIndex: "content"
}
]
const data1: DataType[] = [
{
key: "1",
name: "John Brown",
age: 32,
address: "New York No. 1 Lake Park",
tags: 2,
content: "内容"
},
{
key: "2",
name: "Jim Green",
age: 42,
address: "London No. 1 Lake Park",
tags: 3,
content: "内容"
},
{
key: "3",
name: "Joe Black",
age: 32,
address: "Sydney No. 1 Lake Park",
tags: 3,
content: "内容"
}
]
export const ListDetail = () => {
return (
<div className="p-[1rem] pt-[4rem]">
<List
grid={{ gutter: 16, column: 3 }}
dataSource={data}
renderItem={(item) => (
<List.Item>
<Card title={item.key}>{item.value}</Card>
</List.Item>
)}
style={{ marginBottom: "2rem" }}
/>
<div>
<Divider orientation="left">token详情</Divider>
<List
bordered
dataSource={inputTokenData}
renderItem={(item) => (
<List.Item style={{ justifyContent: "flex-start" }}>
<Typography.Text mark className="mr-1">
{item.key}
</Typography.Text>
<Tooltip placement="topLeft" title={item.value}>
{item.value}
</Tooltip>
</List.Item>
)}
style={{ marginBottom: "1rem" }}
/>
<Table<DataType> columns={columns} dataSource={data1} />
</div>
<div>
<Divider orientation="left">token详情</Divider>
<List
bordered
dataSource={outputTokenData}
renderItem={(item) => (
<List.Item style={{ justifyContent: "flex-start" }}>
<Typography.Text mark className="mr-1">
{item.key}
</Typography.Text>
<Tooltip placement="topLeft" title={item.value}>
{item.value}
</Tooltip>
</List.Item>
)}
/>
</div>
</div>
)
}

View File

@ -1,15 +1,30 @@
import React, { useMemo } from "react"
import { MeteringEntry, useStoreMessageOption } from "@/store/option"
import React from "react"
import { ChatMessage, 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"] = [
const data = [
{
title: '序号',
key: 'index',
width: 100,
render: (_text, _record, index) => index + 1, // 索引从0开始+1后从1显示
key: "对话数量",
value: 2
},
{
key: "输出token数",
value: 2
},
{
key: "输入token数",
value: 2
}
]
const columns: TableProps<ChatMessage>["columns"] = [
{
title: "id",
dataIndex: "id",
key: "id",
width: "13%"
},
{
title: "问题",
@ -32,16 +47,8 @@ const columns: TableProps<MeteringEntry>["columns"] = [
},
{
title: "思维链",
key: "cot",
dataIndex: "cot",
ellipsis: {
showTitle: false
},
render: (responseContent) => (
<Tooltip placement="topLeft" title={responseContent}>
{responseContent}
</Tooltip>
),
key: "thinkingChain",
dataIndex: "thinkingChain",
width: "10%"
},
@ -66,8 +73,9 @@ const columns: TableProps<MeteringEntry>["columns"] = [
},
{
title: "数联网token",
dataIndex: "iodTokenCount",
key: "iodTokenCount"
dataIndex: "iodOutputToken",
key: "iodOutputToken",
render: (iodOutputToken) => <div>{iodOutputToken?.length}</div>
},
{
title: "大模型token",
@ -75,7 +83,9 @@ const columns: TableProps<MeteringEntry>["columns"] = [
dataIndex: "largeModelToken",
render: (_, record) => {
return (
<div>{record.modelInputTokenCount + record.modelOutputTokenCount}</div>
<div>
{record.iodInputToken?.length + record.iodOutputToken?.length}
</div>
)
}
},
@ -84,7 +94,7 @@ const columns: TableProps<MeteringEntry>["columns"] = [
dataIndex: "date",
key: "date",
render: (date) => {
return <div>{formatDate(new Date(date))}</div>
return <div>{formatDate(date)}</div>
}
},
{
@ -108,50 +118,14 @@ const columns: TableProps<MeteringEntry>["columns"] = [
]
export const MeteringDetail = () => {
const { meteringEntries } = useStoreMessageOption()
const { chatMessages } = useStoreMessageOption()
console.log(chatMessages, "opppp")
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]">
<div className="pt-[4rem]">
<List
grid={{ gutter: 16, column: 5 }}
grid={{ gutter: 16, column: 3 }}
dataSource={data}
split={false}
renderItem={(item) => (
<List.Item>
<Card title={item.key}>{item.value}</Card>
@ -159,7 +133,7 @@ export const MeteringDetail = () => {
)}
/>
<Table<MeteringEntry> columns={columns} dataSource={meteringEntries} />
<Table<ChatMessage> columns={columns} dataSource={chatMessages} />
</div>
)
}

View File

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

View File

@ -11,24 +11,22 @@ export const PlaygroundChat = () => {
regenerateLastMessage,
isSearchingInternet,
editMessage,
ttsEnabled,
setCurrentMessageId,
ttsEnabled
} = useMessageOption()
const [isSourceOpen, setIsSourceOpen] = React.useState(false)
const [source, setSource] = React.useState<any>(null)
return (
<>
<div className="relative flex w-full flex-col items-center pb-4">
<div className="relative flex w-full flex-col items-center pt-16 pb-4">
{messages.length === 0 && (
<div className="mt-3 w-full">
<div className="mt-32 w-full">
<PlaygroundEmpty />
</div>
)}
{messages.map((message, index) => (
<PlaygroundMessage
key={index}
id={message.id}
isBot={message.isBot}
message={message.message}
name={message.name}
@ -51,12 +49,11 @@ export const PlaygroundChat = () => {
generationInfo={message?.generationInfo}
isStreaming={streaming}
reasoningTimeTaken={message?.reasoning_time_taken}
setCurrentMessageId={setCurrentMessageId}
iodSearch={message.iodSearch}
/>
))}
</div>
{messages.length !== 0 && <div className="w-full pb-[157px]"></div>}
<div className="w-full pb-[157px]"></div>
<MessageSourcePopup
open={isSourceOpen}
setOpen={setIsSourceOpen}

View File

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

View File

@ -1,24 +1,17 @@
import { useForm } from "@mantine/form"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import React, { useMemo } from "react"
import React from "react"
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
import { toBase64 } from "~/libs/to-base64"
import { useMessageOption } from "~/hooks/useMessageOption"
import {
Button,
Checkbox,
Dropdown,
Image,
MenuProps,
Switch,
Tooltip
} from "antd"
import { Checkbox, Dropdown, Switch, Tooltip } from "antd"
import { Image } from "antd"
import { useWebUI } from "~/store/webui"
import { defaultEmbeddingModelForRag } from "~/services/ollama"
import { ImageIcon, MicIcon, StopCircleIcon, X } from "lucide-react"
import { getVariable } from "@/utils/select-variable"
import { useTranslation } from "react-i18next"
// import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect"
import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect"
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"
import { PiGlobe, PiNetwork } from "react-icons/pi"
import { handleChatInputKeyDown } from "@/utils/key-down"
@ -213,47 +206,14 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
}
}
const iodSearchItems = useMemo<MenuProps["items"]>(() => {
return [
{
key: 0,
label: (
<div
onClick={() => {
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="flex w-full flex-col items-center p-2 pt-1 pb-4">
<div className="relative z-10 flex w-full flex-col items-center justify-center gap-2 text-base">
<div className="relative flex w-full flex-row justify-center gap-2 lg:w-5/5">
<div className="relative flex w-full flex-row justify-center gap-2 lg:w-4/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`}>
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
${temporaryChat ? "!bg-gray-200 dark:!bg-black " : ""}
`}>
<div
className={`border-b border-gray-200 dark:border-gray-600 relative ${
form.values.image.length === 0 ? "hidden" : "block"
@ -274,7 +234,8 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
/>
</div>
<div>
<div className={`flex bg-transparent `}>
<div
className={`flex bg-transparent `}>
<form
onSubmit={form.onSubmit(async (value) => {
stopListening()
@ -343,70 +304,55 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
<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>
<Tooltip title={t("tooltip.searchInternet")}>
<div className="inline-flex items-center gap-2">
<PiGlobe
className={`h-5 w-5 dark:text-gray-300 `}
/>
<Switch
value={webSearch}
onChange={(e) => setWebSearch(e)}
checkedChildren={t("form.webSearch.on")}
unCheckedChildren={t("form.webSearch.off")}
/>
</div>
</Tooltip>
<Tooltip title={t("tooltip.searchIod")} className="ml-3">
<div className="inline-flex items-center gap-2">
<PiNetwork
className={`h-5 w-5 dark:text-gray-300 `}
/>
<Switch
value={iodSearch}
onChange={(e) => setIodSearch(e)}
checkedChildren={t("form.webSearch.on")}
unCheckedChildren={t("form.webSearch.off")}
/>
</div>
</Tooltip>
</div>
)}
</div>
<div className="flex !justify-end gap-1">
<div className="flex !justify-end gap-3">
{!selectedKnowledge && (
<Tooltip title={t("tooltip.uploadImage")}>
<Button
color="default"
variant="text"
<button
type="button"
onClick={() => {
inputRef.current?.click()
}}
className={`!px-[5px] flex items-center justify-center dark:text-gray-300 ${
className={`flex items-center justify-center dark:text-gray-300 ${
chatMode === "rag" ? "hidden" : "block"
}`}>
<ImageIcon strokeWidth={1} className="h-5 w-5" />
</Button>
<ImageIcon className="h-5 w-5" />
</button>
</Tooltip>
)}
{browserSupportsSpeechRecognition && (
<Tooltip title={t("tooltip.speechToText")}>
<Button
color="default"
variant="text"
<button
type="button"
onClick={async () => {
if (isListening) {
stopSpeechRecognition()
@ -418,43 +364,40 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
})
}
}}
className={`flex items-center justify-center dark:text-gray-300 !px-[5px]`}>
className={`flex items-center justify-center dark:text-gray-300`}>
{!isListening ? (
<MicIcon strokeWidth={1} className="h-5 w-5" />
<MicIcon className="h-5 w-5" />
) : (
<div className="relative">
<span className="animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75"></span>
<MicIcon
strokeWidth={1}
className="h-5 w-5"
/>
<MicIcon className="h-5 w-5" />
</div>
)}
</Button>
</button>
</Tooltip>
)}
{/*<KnowledgeSelect />*/}
<KnowledgeSelect />
{!isSending ? (
<Dropdown.Button
type="default"
htmlType="submit"
disabled={isSending}
// icon={
// <svg
// xmlns="http://www.w3.org/2000/svg"
// fill="none"
// viewBox="0 0 24 24"
// strokeWidth={1.5}
// stroke="currentColor"
// className="w-5 h-5">
// <path
// strokeLinecap="round"
// strokeLinejoin="round"
// d="m19.5 8.25-7.5 7.5-7.5-7.5"
// />
// </svg>
// }
className="!justify-end !w-auto"
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-5 h-5">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m19.5 8.25-7.5 7.5-7.5-7.5"
/>
</svg>
}
menu={{
items: [
{
@ -484,6 +427,20 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
]
}}>
<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")}
</div>
</Dropdown.Button>

View File

@ -1,160 +0,0 @@
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

@ -0,0 +1,25 @@
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

@ -1,277 +0,0 @@
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,33 +0,0 @@
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 {
deleteByHistoryId,
PageAssitDatabase,
formatToChatHistory,
formatToMessage,
getPromptById,
PageAssitDatabase,
deleteByHistoryId,
updateHistory,
pinHistory,
updateHistory
getPromptById
} from "@/db"
import { Dropdown, Empty, Menu, Skeleton, Tooltip } from "antd"
import { Empty, Skeleton, Dropdown, Menu, Tooltip } from "antd"
import {
BotIcon,
MoreVertical,
PencilIcon,
Trash2,
MoreVertical,
PinIcon,
PinOffIcon,
Trash2
BotIcon
} from "lucide-react"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
@ -24,7 +24,6 @@ import {
getLastUsedChatSystemPrompt,
lastUsedChatModelEnabled
} from "@/services/model-settings"
import { useMessageOption } from "@/hooks/useMessageOption.tsx"
type Props = {
onClose: () => void
@ -34,7 +33,6 @@ type Props = {
setSelectedModel: (model: string) => void
setSelectedSystemPrompt: (prompt: string) => void
setSystemPrompt: (prompt: string) => void
stopStreamingRequest: () => void
clearChat: () => void
temporaryChat: boolean
historyId: string
@ -48,7 +46,6 @@ export const Sidebar = ({
setHistoryId,
setSelectedModel,
setSelectedSystemPrompt,
stopStreamingRequest,
clearChat,
historyId,
setSystemPrompt,
@ -58,8 +55,6 @@ export const Sidebar = ({
const client = useQueryClient()
const navigate = useNavigate()
const { setCurrentMessageId } = useMessageOption()
const { data: chatHistories, status } = useQuery({
queryKey: ["fetchChatHistory"],
queryFn: async () => {
@ -145,41 +140,6 @@ 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 (
<div
className={`overflow-y-auto z-99 ${temporaryChat ? "pointer-events-none opacity-50" : ""}`}>
@ -209,11 +169,7 @@ export const Sidebar = ({
{group.items.map((chat, index) => (
<div
key={index}
className={`
flex py-2 px-2 items-center gap-3 relative rounded-md truncate 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]"}
`}>
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">
{chat?.message_source === "copilot" && (
<Tooltip title={t("common:sidebarChat")} placement="top">
<BotIcon className="size-3 text-green-500" />
@ -221,7 +177,38 @@ export const Sidebar = ({
)}
<button
className="flex-1 overflow-hidden break-all text-start truncate w-full"
onClick={() => handleHistoryClick(chat)}>
onClick={async () => {
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>
</button>
<div className="flex items-center gap-2">
@ -278,9 +265,7 @@ export const Sidebar = ({
trigger={["click"]}
placement="bottomRight">
<button className="text-gray-500 dark:text-gray-400 opacity-80 hover:opacity-100">
<MoreVertical
className={`w-4 h-4 group-hover:text-white ${historyId === chat.id ? "text-white" : ""}`}
/>
<MoreVertical className="w-4 h-4" />
</button>
</Dropdown>
</div>

View File

@ -1,419 +0,0 @@
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

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

View File

View File

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

View File

@ -1,75 +0,0 @@
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

@ -1,507 +0,0 @@
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,30 +5,24 @@ import { clearBadge, streamDownload } from "@/utils/pull-ollama"
export default defineBackground({
main() {
let isCopilotRunning: boolean = false
browser.runtime.onMessage.addListener(async (message,sender,sendResponse) => {
switch(message.type){
case "sidepanel":
await browser.sidebarAction.open()
break;
case "pull_model":
const ollamaURL = await getOllamaURL()
browser.runtime.onMessage.addListener(async (message) => {
if (message.type === "sidepanel") {
await browser.sidebarAction.open()
} else if (message.type === "pull_model") {
const ollamaURL = await getOllamaURL()
const isRunning = await isOllamaRunning()
if (!isRunning) {
setBadgeText({ text: "E" })
setBadgeBackgroundColor({ color: "#FF0000" })
setTitle({ title: "Ollama is not running" })
setTimeout(() => {
clearBadge()
}, 5000)
}
await streamDownload(ollamaURL, message.modelName)
break;
case "retrieveDeepScript":
return retrieveDeepScript(message);
default:
break;
const isRunning = await isOllamaRunning()
if (!isRunning) {
setBadgeText({ text: "E" })
setBadgeBackgroundColor({ color: "#FF0000" })
setTitle({ title: "Ollama is not running" })
setTimeout(() => {
clearBadge()
}, 5000)
}
await streamDownload(ollamaURL, message.modelName)
}
})
@ -186,52 +180,3 @@ export default defineBackground({
},
persistent: true
})
const iodConfig = {
"gatewayUrl": "tcp://127.0.0.1:21051",
"registry":"bdware/Registry",
"localRepository":"bdtest.local/myrepo1",
"doBrowser":"http://127.0.0.1:21030/SCIDE/SCManager"
}
const makeDOIPParams = (doId:string, op:string, attributes:Object, requestBody: string) => ({
action: "executeContract",
contractID: "BDBrowser",
operation: "sendRequestDirectly",
arg: {
id: doId,
doipUrl: iodConfig.gatewayUrl,
op: op,
attributes: attributes,
body: requestBody
}
})
const retrieveDeepScript = async function(message) {
console.log(message);
const doId = message.doId;
console.log("retriveDoc:"+doId)
const params = makeDOIPParams(doId,"Retrieve",{
bodyBase64Encoded: false
}, "");
const abortController = new AbortController()
setTimeout(() => abortController.abort(), 10000)
return await fetch(iodConfig.doBrowser, {
method: "POST",
body: JSON.stringify(params),
signal: abortController.signal
}).then((response) => {
console.log("responseIn retrieveDoc:");
console.log(response);
return response.json()})
.then((res) => {
console.log("res:");
console.log(res.result.body);
//TODO
return {
metadata:{traceId:res.result.header.attributes?.traceId},
pageContent:res.result.body
}
})
}

View File

@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
<title>IoD Bot - A Web UI for Local AI Models</title>
<title>Page Assist - A Web UI for Local AI Models</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="manifest.type" content="browser_action" />
<meta name="manifest.browser_style" content="false" />

View File

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

View File

@ -21,15 +21,13 @@ const useDynamicTextareaSize = (
const contentHeight = currentTextarea.scrollHeight;
if (maxHeight) {
// Set max-height and adjust overflow behavior if maxHeight is provided
currentTextarea.style.maxHeight = `${maxHeight}px`;
currentTextarea.style.overflowY = contentHeight > maxHeight ? "scroll" : "hidden";
currentTextarea.style.height = `${Math.min(contentHeight, maxHeight) < 60 ? 60 : Math.min(contentHeight, maxHeight)}px`;
currentTextarea.style.fontWeight = "normal";
currentTextarea.style.color = "#374151";
currentTextarea.style.height = `${Math.min(contentHeight, maxHeight)}px`;
} else {
// Adjust height without max height constraint
currentTextarea.style.height = `${contentHeight}px`;
}
@ -37,4 +35,4 @@ const useDynamicTextareaSize = (
}, [textareaRef, textContent, maxHeight]);
};
export default useDynamicTextareaSize;
export default useDynamicTextareaSize;

View File

@ -2,15 +2,16 @@ import React from "react"
import { cleanUrl } from "~/libs/clean-url"
import {
defaultEmbeddingModelForRag,
getOllamaURL,
geWebSearchFollowUpPrompt,
getOllamaURL,
promptForRag,
systemPromptForNonRag
} from "~/services/ollama"
import { useStoreMessageOption } from "~/store/option"
import { useStoreMessageOption, type Message } from "~/store/option"
import { useStoreMessage } from "~/store"
import { SystemMessage } from "@langchain/core/messages"
import { getDataFromCurrentTab } from "~/libs/get-html"
import { MemoryVectorStore } from "langchain/vectorstores/memory"
import { memoryEmbedding } from "@/utils/memory-embeddings"
import { ChatHistory } from "@/store/option"
import {
@ -41,9 +42,6 @@ import {
mergeReasoningContent,
removeReasoning
} from "@/libs/reasoning"
import { AllIodRegistryEntry } from "@/types/iod.ts"
import { getDefaultIodSources } from "@/libs/iod.ts"
import { Message } from "@/types/message.ts"
export const useMessage = () => {
const {
@ -190,15 +188,15 @@ export const useMessage = () => {
name: "You",
message,
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
images: []
},
{
isBot: true,
name: selectedModel,
message: "",
message: "",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -210,7 +208,7 @@ export const useMessage = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -244,7 +242,6 @@ export const useMessage = () => {
}
isAlreadyExistEmbedding = keepTrackOfEmbedding[websiteUrl]
}
setMessages(newMessage)
const ollamaUrl = await getOllamaURL()
const embeddingModle = await defaultEmbeddingModelForRag()
@ -351,7 +348,14 @@ export const useMessage = () => {
metadata: Record<string, any>
}[] = []
// TODO: update type
let iodSources: AllIodRegistryEntry = getDefaultIodSources()
let iodSources: {
name: any
type: any
mode: string
url: string
pageContent: string
metadata: Record<string, any>
}[] = []
if (chatWithWebsiteEmbedding) {
const docs = await vectorstore.similaritySearch(query, 4)
@ -508,7 +512,7 @@ export const useMessage = () => {
content: fullText
}
])
debugger
await saveMessageOnSuccess({
historyId,
setHistoryId,
@ -619,7 +623,7 @@ export const useMessage = () => {
name: "You",
message,
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
images: []
},
{
@ -627,7 +631,7 @@ export const useMessage = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -639,7 +643,7 @@ export const useMessage = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -803,7 +807,7 @@ export const useMessage = () => {
image,
fullText,
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
message_source: "copilot",
generationInfo,
reasoning_time_taken: timetaken
@ -908,7 +912,7 @@ export const useMessage = () => {
name: "You",
message,
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
images: [image]
},
{
@ -916,7 +920,7 @@ export const useMessage = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -928,7 +932,7 @@ export const useMessage = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -1097,7 +1101,7 @@ export const useMessage = () => {
image,
fullText,
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
message_source: "copilot",
generationInfo,
reasoning_time_taken: timetaken
@ -1141,7 +1145,7 @@ export const useMessage = () => {
isRegenerate: boolean,
messages: Message[],
history: ChatHistory,
signal: AbortSignal
signal: AbortSignal,
) => {
const url = await getOllamaURL()
setStreaming(true)
@ -1199,7 +1203,7 @@ export const useMessage = () => {
name: "You",
message,
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
images: [image]
},
{
@ -1207,7 +1211,7 @@ export const useMessage = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -1219,7 +1223,7 @@ export const useMessage = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -1296,12 +1300,8 @@ export const useMessage = () => {
query = removeReasoning(query)
}
const { prompt, webSources, iodSources } = await getSystemPromptForWeb(
query,
[],
webSearch,
iodSearch
)
const { prompt, webSources, iodSources } =
await getSystemPromptForWeb(query, [], webSearch, iodSearch)
setIsSearchingInternet(false)
// message = message.trim().replaceAll("\n", " ")
@ -1556,7 +1556,7 @@ export const useMessage = () => {
name: "You",
message,
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
images: [image],
messageType: messageType
},
@ -1565,7 +1565,7 @@ export const useMessage = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -1577,7 +1577,7 @@ export const useMessage = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -1724,7 +1724,7 @@ export const useMessage = () => {
image,
fullText,
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
message_source: "copilot",
message_type: messageType,
generationInfo,
@ -1811,7 +1811,7 @@ export const useMessage = () => {
isRegenerate || false,
messages,
memory || history,
signal
signal,
)
} else {
await normalChatMode(

View File

@ -2,14 +2,15 @@ import React from "react"
import { cleanUrl } from "~/libs/clean-url"
import {
defaultEmbeddingModelForRag,
getOllamaURL,
geWebSearchFollowUpPrompt,
geWebSearchKeywordsPrompt,
getOllamaURL,
promptForRag,
systemPromptForNonRagOption
} from "~/services/ollama"
import type { ChatHistory, MeteringEntry } from "~/store/option"
import { useStoreMessageOption } from "~/store/option"
import { type ChatHistory, ChatMessage, type Message } from "~/store/option"
import { SystemMessage } from "@langchain/core/messages"
import { useStoreMessageOption } from "~/store/option"
import {
deleteChatForEdit,
generateID,
@ -20,8 +21,6 @@ import {
import { useNavigate } from "react-router-dom"
import { notification } from "antd"
import { getSystemPromptForWeb } from "~/web/web"
import { tokenizeInput } from "~/web/iod"
import { generateHistory } from "@/utils/generate-history"
import { useTranslation } from "react-i18next"
import {
@ -39,34 +38,25 @@ import { pageAssistModel } from "@/models"
import { getNoOfRetrievedDocs } from "@/services/app"
import { humanMessageFormatter } from "@/utils/human-message"
import { pageAssistEmbeddingModel } from "@/models/embedding"
import {
isReasoningEnded,
isReasoningStarted,
mergeReasoningContent,
removeReasoning
} from "@/libs/reasoning"
import { getDefaultIodSources } from "@/libs/iod.ts"
import type { Message } from "@/types/message.ts"
export const useMessageOption = () => {
const {
controller: abortController,
setController: setAbortController,
iodLoading,
setIodLoading,
currentMessageId,
setCurrentMessageId,
messages,
setMessages,
setMessages
} = usePageAssist()
const {
history,
setHistory,
meteringEntries,
setMeteringEntries,
setCurrentMeteringEntry,
chatMessages,
setChatMessages,
setStreaming,
streaming,
setIsFirstMessage,
@ -119,31 +109,28 @@ export const useMessageOption = () => {
setIsProcessing(false)
setStreaming(false)
currentChatModelSettings.reset()
setIodLoading(false)
setCurrentMessageId("")
textareaRef?.current?.focus()
if (defaultInternetSearchOn) {
setWebSearch(true)
}
}
// 从最后的结果中解析出 思维链 (Chain-of-Thought) 和 结果
// 从最后的结果中解析出 思维链 和 结果
const responseResolver = (msg: string) => {
const cotStart = msg.indexOf("<think>")
const cotEnd = msg.indexOf("</think>")
let cot = ""
const thinkStart = msg.indexOf("<think>")
const thinkEnd = msg.indexOf("</think>")
let think = ""
let content = ""
if (cotStart > -1 && cotEnd > -1) {
cot = msg.substring(cotStart + 7, cotEnd)
content = msg.substring(cotEnd + 8)
if (thinkStart > -1 && thinkEnd > -1) {
think = msg.substring(thinkStart + 7, thinkEnd)
content = msg.substring(thinkEnd + 8)
} else {
content = msg
}
// 去掉换行符
cot = cot.replace(/\n/g, "")
think = think.replace(/\n/g, "")
content = content.replace(/\n/g, "")
return {
cot: cot,
think,
content
}
}
@ -203,51 +190,41 @@ export const useMessageOption = () => {
})
let newMessage: Message[] = []
let generateMessageId = generateID()
setCurrentMessageId(generateMessageId)
const meter: MeteringEntry = {
const chatMessage: ChatMessage = {
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]
}
queryContent: message
} as ChatMessage
if (!isRegenerate) {
newMessage = [
...messages,
{
...JSON.parse(JSON.stringify(defaultMessage)),
id: generateID(),
isBot: false,
name: "You",
message,
webSources: [],
iodSources: [],
images: [image]
},
{
...JSON.parse(JSON.stringify(defaultMessage)),
id: generateMessageId,
message: "",
isBot: true,
name: selectedModel,
message: "▋",
webSources: [],
iodSources: [],
id: generateMessageId
}
]
} else {
newMessage = [
...messages,
{
...JSON.parse(JSON.stringify(defaultMessage)),
id: generateMessageId,
message: " ",
isBot: true,
name: selectedModel,
message: "▋",
webSources: [],
iodSources: [],
id: generateMessageId
}
]
}
@ -328,11 +305,6 @@ export const useMessageOption = () => {
// Currently only IoD search use keywords
if (iodSearch) {
// Extract keywords
console.log(
"query:" + query + " --> " + JSON.stringify(tokenizeInput(query))
)
keywords = tokenizeInput(query)
/*
const questionPrompt = await geWebSearchKeywordsPrompt()
const promptForQuestion = questionPrompt.replaceAll("{query}", query)
const response = await ollama.invoke(promptForQuestion)
@ -340,43 +312,19 @@ export const useMessageOption = () => {
res = removeReasoning(res)
keywords = res
.replace(/^Keywords:/i, "")
.replace(/^关键词:/i, "")
.replace(/^/i, "")
.replace(/^:/i, "")
.replaceAll(" ", "")
.split(",")
.split(", ")
.map((k) => k.trim())
*/
}
const {
prompt,
webSources,
iodSources,
iodSearchResults: iodData,
iodTokenCount
} = await getSystemPromptForWeb(query, keywords, webSearch, iodSearch)
setIodLoading(false)
const { prompt, webSources, iodSources } = await getSystemPromptForWeb(
query,
keywords,
webSearch,
iodSearch
)
console.log("prompt:\n" + prompt)
setIsSearchingInternet(false)
meter.prompt = prompt
meter.iodKeywords = keywords
meter.iodData = Object.values(iodData).flat()
meter.iodTokenCount = iodTokenCount
setMessages((prev) => {
return prev.map((message) => {
if (message.id === generateMessageId) {
return {
...message,
webSources,
iodSources,
}
}
return message
})
})
chatMessage.prompt = prompt
// message = message.trim().replaceAll("\n", " ")
@ -437,7 +385,6 @@ export const useMessageOption = () => {
}
)
let count = 0
const chatStartTime = new Date()
let reasoningStartTime: Date | undefined = undefined
let reasoningEndTime: Date | undefined = undefined
let apiReasoning = false
@ -529,8 +476,6 @@ export const useMessageOption = () => {
message,
image,
fullText,
iodSearch,
webSearch,
webSources,
iodSources,
generationInfo,
@ -540,27 +485,13 @@ export const useMessageOption = () => {
setIsProcessing(false)
setStreaming(false)
// Save metering entry
const { cot, content } = responseResolver(fullText)
const currentMeteringEntry = {
...meter,
modelInputTokenCount: prompt.length,
modelOutputTokenCount: fullText.length,
model: ollama.modelName ?? ollama.model,
relatedDataCount: 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))
chatMessage.relatedDataCount = keywords.length
chatMessage.timeTaken = timetaken
chatMessage.date = reasoningStartTime
const { think, content } = responseResolver(fullText)
chatMessage.thinkingChain = think
chatMessage.responseContent = content
setChatMessages([...chatMessages, chatMessage])
} catch (e) {
const errorSave = await saveMessageOnError({
e,
@ -572,9 +503,7 @@ export const useMessageOption = () => {
setHistory,
setHistoryId,
userMessage: message,
isRegenerating: isRegenerate,
iodSearch,
webSearch,
isRegenerating: isRegenerate
})
if (!errorSave) {
@ -680,17 +609,7 @@ export const useMessageOption = () => {
let newMessage: Message[] = []
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) {
newMessage = [
...messages,
@ -698,9 +617,8 @@ export const useMessageOption = () => {
isBot: false,
name: "You",
message,
id: generateID(),
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
images: [image]
},
{
@ -708,7 +626,7 @@ export const useMessageOption = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -720,7 +638,7 @@ export const useMessageOption = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -817,7 +735,6 @@ export const useMessageOption = () => {
let reasoningStartTime: Date | null = null
let reasoningEndTime: Date | null = null
let apiReasoning: boolean = false
const chatStartTime = new Date()
for await (const chunk of chunks) {
if (chunk?.additional_kwargs?.reasoning_content) {
@ -898,6 +815,7 @@ export const useMessageOption = () => {
content: fullText
}
])
await saveMessageOnSuccess({
historyId,
setHistoryId,
@ -906,8 +824,6 @@ export const useMessageOption = () => {
message,
image,
fullText,
iodSearch,
webSearch,
source: [],
generationInfo,
prompt_content: promptContent,
@ -919,27 +835,6 @@ export const useMessageOption = () => {
setStreaming(false)
setIsProcessing(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) {
const errorSave = await saveMessageOnError({
e,
@ -953,9 +848,7 @@ export const useMessageOption = () => {
userMessage: message,
isRegenerating: isRegenerate,
prompt_content: promptContent,
prompt_id: promptId,
iodSearch,
webSearch,
prompt_id: promptId
})
if (!errorSave) {
@ -1031,7 +924,7 @@ export const useMessageOption = () => {
name: "You",
message,
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
images: []
},
{
@ -1039,7 +932,7 @@ export const useMessageOption = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -1051,7 +944,7 @@ export const useMessageOption = () => {
name: selectedModel,
message: "▋",
webSources: [],
iodSources: getDefaultIodSources(),
iodSources: [],
id: generateMessageId
}
]
@ -1286,9 +1179,7 @@ export const useMessageOption = () => {
fullText,
source,
generationInfo,
reasoning_time_taken: timetaken,
iodSearch,
webSearch,
reasoning_time_taken: timetaken
})
setIsProcessing(false)
@ -1304,9 +1195,7 @@ export const useMessageOption = () => {
setHistory,
setHistoryId,
userMessage: message,
isRegenerating: isRegenerate,
iodSearch,
webSearch,
isRegenerating: isRegenerate
})
if (!errorSave) {
@ -1358,7 +1247,6 @@ export const useMessageOption = () => {
)
} else {
if (webSearch || iodSearch) {
setIodLoading(iodSearch)
await searchChatMode(
webSearch,
iodSearch,
@ -1475,10 +1363,6 @@ export const useMessageOption = () => {
editMessage,
messages,
setMessages,
iodLoading,
currentMessageId,
setIodLoading,
setCurrentMessageId,
onSubmit,
setStreaming,
streaming,

View File

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

View File

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

View File

@ -1,91 +0,0 @@
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: {
apiKey: providerInfo.apiKey || "temp",
baseURL: providerInfo.baseUrl || ""
},
}
}) as any
}
@ -85,7 +85,7 @@ export const pageAssistModel = async ({
configuration: {
apiKey: providerInfo.apiKey || "temp",
baseURL: providerInfo.baseUrl || ""
},
}
}) as any
}
return new ChatOllama({

View File

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

View File

@ -7,7 +7,6 @@ import OptionOllamaSettings from "./options-settings-ollama"
import OptionShare from "./option-settings-share"
import OptionKnowledgeBase from "./option-settings-knowledge"
import OptionAbout from "./option-settings-about"
import OptionIodSettings from "./option-settings-iod"
import SidepanelChat from "./sidepanel-chat"
import SidepanelSettings from "./sidepanel-settings"
import OptionRagSettings from "./option-rag"
@ -29,7 +28,6 @@ export const OptionRoutingChrome = () => {
<Route path="/settings/share" element={<OptionShare />} />
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
<Route path="/settings/rag" element={<OptionRagSettings />} />
<Route path="/settings/iod" element={<OptionIodSettings />} />
<Route path="/settings/about" element={<OptionAbout />} />
<Route path="/metering" element={<OptionMetering />} />
<Route path="/metering/list/:id" element={<MeteringListDetail />} />

View File

@ -15,7 +15,6 @@ const MeteringListDetail = lazy(() => import("./metering-list-detail"))
const OptionShare = lazy(() => import("./option-settings-share"))
const OptionKnowledgeBase = lazy(() => import("./option-settings-knowledge"))
const OptionAbout = lazy(() => import("./option-settings-about"))
const OptionIodSettings = lazy(() => import("./option-settings-iod"))
const OptionRagSettings = lazy(() => import("./option-rag"))
const OptionOpenAI = lazy(() => import("./option-settings-openai"))
@ -32,7 +31,6 @@ export const OptionRoutingFirefox = () => {
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
<Route path="/settings/about" element={<OptionAbout />} />
<Route path="/settings/rag" element={<OptionRagSettings />} />
<Route path="/settings/iod" element={<OptionIodSettings />} />
<Route path="/metering" element={<OptionMetering />} />
<Route path="/metering/list/:id" element={<MeteringListDetail />} />
</Routes>

View File

@ -1,11 +1,10 @@
import OptionLayout from "~/components/Layouts/Layout"
import IodVideo from "@/components/Option/VideoPlayer/index.tsx"
import { Playground } from "~/components/Option/Playground/Playground"
const OptionIndex = () => {
return (
<OptionLayout>
<Playground />
<Playground />
</OptionLayout>
)
}

View File

@ -1,15 +0,0 @@
import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~/components/Layouts/Layout"
import { IodApp } from "@/components/Option/Settings/iod"
const OptionAbout = () => {
return (
<OptionLayout>
<SettingsLayout>
<IodApp />
</SettingsLayout>
</OptionLayout>
)
}
export default OptionAbout

View File

@ -21,54 +21,7 @@ const DEFAULT_RAG_QUESTION_PROMPT =
const DEFAUTL_RAG_SYSTEM_PROMPT = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say you don't know. DO NOT try to make up an answer. If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context. {context} Question: {question} Helpful answer:`
const DEFAULT_WEBSEARCH_PROMPT = `你是一个中文AI助手当前日期和时间是 {current_date_time}。在<数联网搜索结果> 中提供了来自数联网Internet of Data)的搜索结果。
\`<result doId="{doId}" name="{title}" authors="{authors}" dataType="{paper,dataset or algorithm}" year="{year}" url="{url}" id="{id}">{abstract}</result>\`
\`doId\`\`name\` ,如果没有url则空着 :
\`[数联网引用[id] doId: {doId} "{name}"]({url})\`
:
\`[数联网引用[1] doId: 10.48550/arXiv.1803.05591v2 "On the insufficiency of existing momentum schemes for Stochastic Optimization"](http://arxiv.org/pdf/1803.05591v2.pdf)\`
<数联网搜索结果>
{iod_search_results}
</数联网搜索结果>
<数联网搜索结果><数联网搜索结果>
<数联网搜索结果>
<think>使
4;
4
`
const DEFAULT_WEBSEARCH_PROMPT2 = `You are an AI assistant specialized in retrieving and analyzing academic papers from Neo4j graph database.
Generate a response that how can user achieve his request based on provided search results. The current date and time are {current_date_time}.
The \`iod-search-results\` block provides information retrieved from Internet of Data. Each search result has a format of:
\`<result doId="{doId}" name="{title}" authors="{authors}" dataType="{paper,dataset or algorithm}" year="{year}" url="{url}" id="{id}">{abstract}</result>\`
Please show the \`doId\` and \`name\` of the search result when you refer to search result in your response and chain of thought, in the following format, in English:
\`[IoD source [id] doId: {doId} "{name}"]({url})\`
Or in Chinese:
\`[数联网引用[id] doId: {doId} "{name}"]({url})\`
For example, in English:
\`[IoD source [1] doId: 10.48550/arXiv.1803.05591v2 "On the insufficiency of existing momentum schemes for Stochastic Optimization"](http://arxiv.org/pdf/1803.05591v2.pdf)\`
Or in Chinese:
\`[数联网引用[1] doId: 10.48550/arXiv.1803.05591v2 "On the insufficiency of existing momentum schemes for Stochastic Optimization"](http://arxiv.org/pdf/1803.05591v2.pdf)\`
Use this information to generate a meaningful response that includes:
0.
1.
2. 使(dataset)
3.
4.
<iod-search-results>
{iod_search_results}
</iod-search-results>
`
const DEFAULT_WEBSEARCH2_PROMPT = `You are an AI model who is expert at searching the web and answering user's queries.
const DEFAULT_WEBSEARCH_PROMPT = `You are an AI model who is expert at searching the web and answering user's queries.
Generate a response that is informative and relevant to the user's query based on provided search results. the current date and time are {current_date_time}.
@ -133,25 +86,24 @@ const DEFAULT_WEBSEARCH_KEYWORDS_PROMPT = `Extract the most important keywords f
The result format should be: keyword_1, keyword_2, ..., keyword_n
"research", "研究", "data analysis", "data", "数据"
Keywords部分Query部分不用输出
Example:
Query: What are the symptoms of a heart attack?
你的输出: symptoms, , heart attack,
Keywords: symptoms, , heart attack,
Query: 什么是物联网?
你的输出: Internet of Things, IoT,
Keywords: Internet of Things, IoT,
Query: 人工智能的发展趋势
你的输出: Artificial Intelligence, AI, , trend,
Keywords: Artificial Intelligence, AI, , trend,
Query: {query}
Keywords:
`
export const getOllamaURL = async () => {

View File

@ -18,10 +18,6 @@ export type ChatHistory = {
type State = {
messages: Message[]
setMessages: (messages: Message[]) => void
currentMessageId: string
setCurrentMessageId: (messageId: string) => void
iodLoading: boolean
setIodLoading: (iodLoading: boolean) => void
history: ChatHistory
setHistory: (history: ChatHistory) => void
streaming: boolean
@ -57,10 +53,6 @@ type State = {
export const useStoreMessage = create<State>((set) => ({
messages: [],
setMessages: (messages) => set({ messages }),
currentMessageId: "",
setCurrentMessageId: (currentMessageId) => set({ currentMessageId }),
iodLoading: false,
setIodLoading: (iodLoading) => set({ iodLoading }),
history: [],
setHistory: (history) => set({ history }),
streaming: false,

View File

@ -1,6 +1,27 @@
import { Knowledge } from "@/db/knowledge"
import { create } from "zustand"
import { Message } from "esbuild"
type WebSearch = {
search_engine: string
search_url: string
search_query: string
search_results: {
title: string
link: string
}[]
}
export type Message = {
isBot: boolean
name: string
message: string
webSources: any[]
iodSources: any[]
images?: string[]
search?: WebSearch
reasoning_time_taken?: number
id?: string
messageType?: string
}
export type ChatHistory = {
role: "user" | "assistant" | "system"
@ -9,18 +30,39 @@ export type ChatHistory = {
messageType?: string
}[]
export type ChatMessage = {
id: string
// 问题
queryContent: string
// 提示词全文
prompt: string
// 思维链(只有深度思考时有)
thinkingChain?: string
// 回答
responseContent: string
// 关联数据个数
relatedDataCount: number
// 数联网输入token
iodInputToken: string
// 数联网输出token
iodOutputToken: string
// 大模型输入token
modelInputToken: string
// 大模型输出token
modelOutputToken: string
// 日期
date: Date
// 耗时
timeTaken: number
}
type State = {
messages: Message[]
setMessages: (messages: Message[]) => void
history: ChatHistory
setHistory: (history: ChatHistory) => void
currentMeteringEntry: { data: MeteringEntry; loading: boolean }
setCurrentMeteringEntry: (meteringEntry: {
data: MeteringEntry
loading: boolean
}) => void
meteringEntries: MeteringEntry[]
setMeteringEntries: (meteringEntries: MeteringEntry[]) => void
chatMessages: ChatMessage[]
setChatMessages: (chatMessages: ChatMessage[]) => void
streaming: boolean
setStreaming: (streaming: boolean) => void
isFirstMessage: boolean
@ -63,54 +105,13 @@ type State = {
setUseOCR: (useOCR: boolean) => void
}
export type MeteringEntry = {
id: string
// 问题
queryContent: string
// 提示词全文
prompt: string
// 思维链(只有深度思考时有)
cot?: string
// 回答
responseContent: string
// 关联数据个数
relatedDataCount: number
// 数联网输入token
iodInputToken: string
// 数联网输出token
iodOutputToken: string
// 大模型输入token数量
modelInputTokenCount: number
// 大模型输出token数量
modelOutputTokenCount: number
// 日期
date: number
// 耗时
timeTaken: number
// 大模型回答的全部内容
modelResponseContent: string
// iod的全部内容的token数量
iodTokenCount: number
// iod返回的数据
iodData: any[]
// iod keywords
iodKeywords: string[]
// 模型
model: string
}
export const useStoreMessageOption = create<State>((set) => ({
messages: [],
setMessages: (messages) => set({ messages }),
history: [],
setHistory: (history) => set({ history }),
currentMeteringEntry: { data: {} as MeteringEntry, loading: false },
setCurrentMeteringEntry: (currentMeteringEntry) =>
set({ currentMeteringEntry }),
meteringEntries: JSON.parse(
localStorage.getItem("meteringEntries") || JSON.stringify([])
),
setMeteringEntries: (meteringEntries) => set({ meteringEntries }),
chatMessages: [],
setChatMessages: (chatMessages) => set({ chatMessages }),
streaming: false,
setStreaming: (streaming) => set({ streaming }),
isFirstMessage: true,
@ -132,7 +133,7 @@ export const useStoreMessageOption = create<State>((set) => ({
setIsEmbedding: (isEmbedding) => set({ isEmbedding }),
webSearch: false,
setWebSearch: (webSearch) => set({ webSearch }),
iodSearch: true,
iodSearch: false,
setIodSearch: (iodSearch) => set({ iodSearch }),
isSearchingInternet: false,
setIsSearchingInternet: (isSearchingInternet) => set({ isSearchingInternet }),

View File

@ -5,23 +5,4 @@ export type IodRegistryEntry = {
pdf_url?: string
description: string
content?: string
data_space?: string
data_type?:string
traceId?:string
fromRepo?: string
}
export type AllIodRegistryEntry = {
data: {
data: IodRegistryEntry[]
total: number
}
scenario: {
data: IodRegistryEntry[]
total: number
}
organization: {
data: IodRegistryEntry[]
total: number
}
}

View File

@ -1,5 +1,3 @@
import { AllIodRegistryEntry } from "@/types/iod.ts"
type WebSearch = {
search_engine: string
search_url: string
@ -13,16 +11,12 @@ export type Message = {
isBot: boolean
name: string
message: string
webSearch?: boolean
webSources: any[]
iodSearch?: boolean
iodSources: AllIodRegistryEntry
iodSources: any[]
images?: string[]
search?: WebSearch
messageType?: string
id?: string
generationInfo?: any
reasoning_time_taken?: number
}
export type Messages = Message[]
}

View File

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

View File

@ -1,21 +0,0 @@
{
"action": "executeContract",
"contractID": "BDBrowser",
"operation": "sendRequestDirectly",
"arg": {
"id": "670E241C9937B3537047C87053E3AA36",
"doipUrl": "tcp://reg01.public.internetofdata.cn:21037",
"op": "Search",
"attributes": {
"offset": 2100,
"count": 5,
"bodyBase64Encoded": false,
"searchMode": [
{ "key": "data_type", "type": "MUST", "value": "paper" },
{ "key": "title", "type": "MUST", "value": "Number_1" },
{ "key": "description", "type": "MUST", "value": "Number_1" }
]
},
"body": ""
}
}

View File

@ -1,33 +0,0 @@
const ollama = await pageAssistModel({
model: selectedModel!,
baseUrl: cleanUrl(url),
keepAlive:
currentChatModelSettings?.keepAlive ?? userDefaultModelSettings?.keepAlive,
temperature:
currentChatModelSettings?.temperature ??
userDefaultModelSettings?.temperature,
topK: currentChatModelSettings?.topK ?? userDefaultModelSettings?.topK,
topP: currentChatModelSettings?.topP ?? userDefaultModelSettings?.topP,
numCtx: currentChatModelSettings?.numCtx ?? userDefaultModelSettings?.numCtx,
seed: currentChatModelSettings?.seed,
numGpu: currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
numPredict:
currentChatModelSettings?.numPredict ??
userDefaultModelSettings?.numPredict,
useMMap:
currentChatModelSettings?.useMMap ?? userDefaultModelSettings?.useMMap,
minP: currentChatModelSettings?.minP ?? userDefaultModelSettings?.minP,
repeatLastN:
currentChatModelSettings?.repeatLastN ??
userDefaultModelSettings?.repeatLastN,
repeatPenalty:
currentChatModelSettings?.repeatPenalty ??
userDefaultModelSettings?.repeatPenalty,
tfsZ: currentChatModelSettings?.tfsZ ?? userDefaultModelSettings?.tfsZ,
numKeep:
currentChatModelSettings?.numKeep ?? userDefaultModelSettings?.numKeep,
numThread:
currentChatModelSettings?.numThread ?? userDefaultModelSettings?.numThread,
useMlock:
currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock
})

View File

@ -1,386 +1,97 @@
import { cleanUrl } from "@/libs/clean-url"
import { PageAssistHtmlLoader } from "@/loader/html"
import { PageAssistPDFUrlLoader } from "@/loader/pdf-url"
import { getOllamaURL } from "@/services/ollama"
import { pageAssistEmbeddingModel } from "@/models/embedding"
import { defaultEmbeddingModelForRag, getOllamaURL } from "@/services/ollama"
import {
getIsSimpleInternetSearch,
totalSearchResults
} from "@/services/search"
import { Document } from "@langchain/core/documents"
import { AllIodRegistryEntry, IodRegistryEntry } from "~/types/iod"
import { getPageAssistTextSplitter } from "@/utils/text-splitter"
import type { Document } from "@langchain/core/documents"
import { MemoryVectorStore } from "langchain/vectorstores/memory"
import type { IodRegistryEntry } from "~/types/iod"
import { PageAssitDatabase } from "@/db"
import { enPOSTag, Segment, useDefault } from "segmentit"
import { getDefaultIodSources } from "@/libs/iod.ts"
import { IodDb } from "@/db/iod.ts"
const segment = useDefault(new Segment())
export const tokenizeInput = function (input: string): string[] {
const words = segment.doSegment(input, { simple: false })
console.log(
words.map(function (word) {
return { w: word.w, p: enPOSTag(word.p) }
})
)
return words.filter((word) => word.w.length > 1).map((word) => word.w)
}
function getIodConfig() {
return IodDb.getInstance().getIodConnection()
}
export const iodConfigLocal = {
gatewayUrl: "tcp://127.0.0.1:21036",
registry: "bdware/Registry",
localRepository: "bdtest.local/myrepo1",
doBrowser: "http://127.0.0.1:21030/SCIDE/SCManager"
}
function inGrepList(str: string){
return "什么|问题|需要|合适|设计|考虑|合作|精度|传感器|最新|研究|药物|如何|解决|中的|难题|成本|提升".indexOf(
str
) != -1
}
export const makeSearchParamsWithDataType = function (
count: number,
keyword: string | string[],
dataType: string
) {
const iodConfig = getIodConfig()
const searchMode = []
searchMode.push({ key: "data_type", type: "MUST", value: dataType })
if (typeof keyword === "string") {
// 如果 keyword 是字符串,则直接添加一个 searchMode 条目
searchMode.push({
key: "description",
type: "MUST",
value: keyword
})
} else if (Array.isArray(keyword)) {
// 如果 keyword 是数组,则为每个元素添加一个 searchMode 条目
keyword.forEach((str) => {
if (!inGrepList(str))
searchMode.push({
const makeRegSearchParams = (count: number, keyword: string) => ({
action: "executeContract",
contractID: "BDBrowser",
operation: "sendRequestDirectly",
arg: {
id: "670E241C9937B3537047C87053E3AA36",
doipUrl: "tcp://reg01.public.internetofdata.cn:21037",
op: "Search",
attributes: {
offset: 0,
count,
bodyBase64Encoded: false,
searchMode: [
{
key: "data_type",
type: "MUST",
value: "paper"
},
// {
// key: "title",
// type: "MUST",
// value: keyword,
// },
{
key: "description",
type: "SHOULD",
value: str
})
})
}
return {
action: "executeContract",
contractID: "BDBrowser",
operation: "sendRequestDirectly",
arg: {
id: iodConfig.registry,
//doipUrl:"tcp://127.0.0.1:21039",
doipUrl: iodConfig.gatewayUrl,
op: "Search",
vars: {
timeout: 15000
},
attributes: {
offset: 0,
count,
bodyBase64Encoded: false,
searchMode: searchMode
},
body: ""
}
}
}
export const makeRegSearchParams = function (
count: number,
keyword: string | string[]
) {
const searchMode = []
const iodConfig = getIodConfig()
if (typeof keyword === "string") {
// 如果 keyword 是字符串,则直接添加一个 searchMode 条目
searchMode.push({
key: "description",
type: "MUST",
value: keyword
})
} else if (Array.isArray(keyword)) {
// 如果 keyword 是数组,则为每个元素添加一个 searchMode 条目
keyword.forEach((str) => {
if (!inGrepList(str))
searchMode.push({
key: "description",
type: "SHOULD",
value: str
})
})
}
return {
action: "executeContract",
contractID: "BDBrowser",
operation: "sendRequestDirectly",
arg: {
id: iodConfig.registry,
//doipUrl:"tcp://127.0.0.1:21039",
doipUrl: iodConfig.gatewayUrl,
op: "Search",
vars: {
timeout: 15000
},
attributes: {
offset: 0,
count,
bodyBase64Encoded: false,
searchMode: searchMode
},
body: ""
}
}
}
export const makeDOIPParams = (
doId: string,
op: string,
attributes: Object,
requestBody: string
) => {
const iodConfig = getIodConfig()
return {
action: "executeContract",
contractID: "BDBrowser",
operation: "sendRequestDirectly",
arg: {
id: doId,
doipUrl: iodConfig.gatewayUrl,
op: op,
attributes: attributes,
body: requestBody
}
}
}
export const retrieveDoc = function (doId: string): Promise<Document> {
const iodConfig = getIodConfig()
console.log("retriveDoc:" + doId)
const params = makeDOIPParams(
doId,
"Retrieve",
{
bodyBase64Encoded: false
type: "MUST",
value: keyword
}
]
},
""
)
const abortController = new AbortController()
setTimeout(() => abortController.abort(), 10000)
return fetch(iodConfig.doBrowser, {
method: "POST",
body: JSON.stringify(params),
signal: abortController.signal
})
.then((response) => {
console.log("responseIn retrieveDoc:")
console.log(response)
return response.json()
})
.then((res) => {
console.log("res:")
console.log(res.result.body)
//TODO
return {
metadata: { traceId: res.result.header.attributes?.traceId },
pageContent: res.result.body
}
})
}
export const updateInLocalRepo = function (
historyId: string,
requestBody: Object
): Promise<string> {
const iodConfig = getIodConfig()
const params = makeDOIPParams(
iodConfig.localRepository,
"Update",
{
aiDialogID: historyId,
bodyBase64Encoded: false
},
JSON.stringify(requestBody)
)
const abortController = new AbortController()
setTimeout(() => abortController.abort(), 10000)
return fetch(iodConfig.doBrowser, {
method: "POST",
body: JSON.stringify(params),
signal: abortController.signal
})
.then((response) => response.json())
.then((res) => {
console.log("update dialog:" + JSON.stringify(res))
return res.body
})
}
export const updateDialog = async function (
histroyId: string,
botMessage: any
): Promise<string> {
//TODO @Nex confused by Message/MessageType in ./db/index.ts!
const db = new PageAssitDatabase()
const chatHistory = await db.getChatHistory(histroyId)
var userMessage = null
for (var i = 0; i < chatHistory.length; i++) {
userMessage = chatHistory[i]
if (userMessage.role == "user") break
body: ""
}
let updateBody: any = {}
// !!!IMPORTANT!!! traceId = histroyId+"/"+userMessage.id;
// Update traceId in retrieveDoc!
updateBody.traceId = histroyId + "/" + userMessage.id
updateBody.question = {
id: histroyId + "/" + userMessage.id,
content: userMessage.content,
tokenCount: userMessage.content.length
}
updateBody.answer = {
id: histroyId + "/" + botMessage.id,
content: botMessage.content,
tokenCount: botMessage.content.length
}
//TODO set a correct model ID
updateBody.model = { id: "bdware.ollama/" + userMessage.name }
//TODO incorrect tokenCount calculated!!
updateBody.webSources =
botMessage.webSources?.map((r) => ({
url: r.url,
tokenCount: r.url.length,
content: r.url,
traceId: r?.traceId
})) ?? []
updateBody.IoDSources =
Object.values((botMessage?.iodSources ?? {}) as AllIodRegistryEntry).flatMap(iod => iod.data)?.map((r) => ({
id: r.doId,
tokenCount:
r.content || r.description
? calculateTokenCount(r.content || r.description)
: 0,
content: r.content || r.description,
traceId: r?.traceId
})) ?? []
console.log("updateBody:")
console.log(updateBody)
return updateInLocalRepo(histroyId, updateBody)
}
})
export async function localIodSearch(
query: string,
keywords: string[]
): Promise<AllIodRegistryEntry> {
const iodConfig = getIodConfig()
): Promise<IodRegistryEntry[]> {
const TOTAL_SEARCH_RESULTS = await totalSearchResults()
const abortController = new AbortController()
setTimeout(() => abortController.abort(), 10000)
const params = makeRegSearchParams(TOTAL_SEARCH_RESULTS, keywords)
const dataParams = makeSearchParamsWithDataType(
TOTAL_SEARCH_RESULTS,
keywords,
"data"
)
const scenarioParams = makeSearchParamsWithDataType(
TOTAL_SEARCH_RESULTS,
keywords,
"scenario"
)
const orgParams = makeSearchParamsWithDataType(
TOTAL_SEARCH_RESULTS,
keywords,
"organization"
)
try {
console.log("params------->", params)
const requests = [
fetch(iodConfig.doBrowser, {
method: "POST",
body: JSON.stringify(dataParams),
signal: abortController.signal
}),
fetch(iodConfig.doBrowser, {
method: "POST",
body: JSON.stringify(scenarioParams),
signal: abortController.signal
}),
fetch(iodConfig.doBrowser, {
method: "POST",
body: JSON.stringify(orgParams),
signal: abortController.signal
const results = (
await Promise.all(
keywords.map(async (keyword) => {
const abortController = new AbortController()
setTimeout(() => abortController.abort(), 10000)
const params = makeRegSearchParams(TOTAL_SEARCH_RESULTS, keyword)
return fetch("http://47.93.156.31:21033/SCIDE/SCManager", {
method: "POST",
body: JSON.stringify(params),
signal: abortController.signal
})
.then((response) => response.json())
.then((res) => {
if (res.status !== "Success") {
console.log(res)
return []
}
const body = JSON.parse(res.result.body)
if (body.code !== 0) {
console.log(body)
return []
}
const results: IodRegistryEntry[] =
body.data?.results?.filter((r) => r.url || r.pdf_url) || []
for (const r of results) {
r.url = r.url || r.pdf_url
}
return results
})
.catch((e) => {
console.log(e)
return []
})
})
]
//TODO @Zhaoweijie 这三类分别是数据、场景、团队的搜索请求。
const responses = await Promise.all(requests)
const results = await Promise.all(responses.map((res) => res.json()))
)
).flat()
const allResults: AllIodRegistryEntry = getDefaultIodSources()
let i = 0
for (const res of results) {
// 检查顶层状态
if (res.status !== "Success") {
continue // 跳过失败的请求
}
let body
try {
body = JSON.parse(res.result.body)
} catch (e) {
console.warn("Failed to parse result.body as JSON", e)
continue
}
if (body.code !== 0) {
continue
}
const entries: IodRegistryEntry[] = body.data?.results || []
const prunedEntries: IodRegistryEntry[] = []
const seenDoIds = new Set<string>()
// 数据清洗:补全 url 和 doId
for (const r of entries) {
r.url = r.url || r.pdf_url
// @ts-ignore
r.doId = r.doId || r.doid
if (seenDoIds.has(r.doId)) {
continue
}
prunedEntries.push(r)
}
// 数据
if (i === 0) {
allResults.data = {
data: prunedEntries,
total: body.data?.total ?? 0
}
}
// 场景
if (i === 1) {
allResults.scenario = {
data: prunedEntries,
total: body.data?.total ?? 0
}
}
// 团队
if (i === 2) {
allResults.organization = {
data: prunedEntries,
total: body.data?.total ?? 0
}
}
i++
}
return allResults
} catch (e) {
console.log(e)
return getDefaultIodSources()
}
return results
}
const ARXIV_URL_PATTERN = /^https?:\/\/arxiv\.org\//
@ -390,42 +101,17 @@ export const searchIod = async (query: string, keywords: string[]) => {
const searchResults = await localIodSearch(query, keywords)
const isSimpleMode = await getIsSimpleInternetSearch()
console.log(
"searchMode:" +
isSimpleMode +
"\n kw:" +
JSON.stringify(keywords) +
"\n" +
" ->searchResult:\n" +
JSON.stringify(searchResults)
)
if (isSimpleMode) {
await getOllamaURL()
return searchResults
}
const docs: Document<Record<string, any>>[] = []
for (const result of Object.values(searchResults)
.map((item) => item.data)
.flat()) {
const resMap = new Map<string, IodRegistryEntry>()
for (const result of searchResults) {
const url = result.url
if (result.doId) {
//TODO !!!!@Nex traceId should be the id of history/question!
let docFromRetrieve = await retrieveDoc(result.doId)
console.log(
"doc from Retrieve:" +
result.doId +
" -->" +
JSON.stringify(docFromRetrieve)
)
docs.push(docFromRetrieve)
result.description = docFromRetrieve.pageContent
result.traceId = docFromRetrieve.metadata?.traceId
continue
}
if (!url) {
continue
}
if (!url) continue
let htmlUrl = ""
if (ARXIV_URL_PATTERN.test(url)) {
@ -486,9 +172,6 @@ export const searchIod = async (query: string, keywords: string[]) => {
}
}
}
return searchResults
/*
const ollamaUrl = await getOllamaURL()
const embeddingModle = await defaultEmbeddingModelForRag()
@ -519,10 +202,4 @@ export const searchIod = async (query: string, keywords: string[]) => {
}).filter((r) => r)
return searchResult
*/
}
export const calculateTokenCount = function (str: string) {
const byteArray = new TextEncoder().encode(str)
return byteArray.length
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,5 +1,4 @@
import { defineConfig } from "wxt"
import { defineRunnerConfig } from "wxt"
import react from "@vitejs/plugin-react"
import topLevelAwait from "vite-plugin-top-level-await"
@ -9,6 +8,7 @@ const chromeMV3Permissions = [
"activeTab",
"scripting",
"declarativeNetRequest",
"action",
"unlimitedStorage",
"contextMenus",
"tts",
@ -49,11 +49,12 @@ export default defineConfig({
process.env.TARGET === "firefox" ? "entries-firefox" : "entries",
srcDir: "src",
outDir: "build",
manifest: {
version: "1.5.0",
name:
process.env.TARGET === "firefox"
? "IoD Bot - A Web UI for Local AI Models"
? "Page Assist - A Web UI for Local AI Models"
: "__MSG_extName__",
description: "__MSG_extDescription__",
default_locale: "en",
@ -96,4 +97,4 @@ export default defineConfig({
? firefoxMV2Permissions
: chromeMV3Permissions
}
}) as any
}) as any