10 Commits

Author SHA1 Message Date
李芳
c50bb49b37 feat: metring and detail pages 2025-02-22 18:20:11 +08:00
李芳
970ffdac15 Merge branch 'feat/metering' of gitea.internetapi.cn:iod/page-assist into feat/page 2025-02-22 17:00:58 +08:00
zhaoweijie
da162be01d feat: add metering data 2025-02-22 16:57:19 +08:00
zhaoweijie
6d79d42925 feat: add metering data 2025-02-22 14:09:57 +08:00
Nex Zhu
f617a05483 feat: improve DEFAULT_WEBSEARCH_PROMPT for IoD and 3W citations 2025-02-17 19:03:38 +08:00
Nex Zhu
4c5d5cfe99 feat: IoD search process HTML/PDF content 2025-02-17 16:44:33 +08:00
CaiHQ
51188b1428 update search result in prompt 2025-02-15 13:02:24 +08:00
Nex Zhu
a56e46a98d feat: IoD search process HTML/PDF content 2025-02-14 23:24:27 +08:00
Nex Zhu
e8471f1802 feat: add IoD search 2025-02-14 18:17:12 +08:00
Muhammed Nazeem
691575e449 Update README.md 2025-02-12 17:06:23 +05:30
46 changed files with 1177 additions and 183 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,8 @@
} }
}, },
"copyToClipboard": "复制到剪贴板", "copyToClipboard": "复制到剪贴板",
"webSearch": "搜索网", "webSearch": "搜索万维网",
"iodSearch": "搜索数联网",
"regenerate": "重新生成", "regenerate": "重新生成",
"edit": "编辑", "edit": "编辑",
"delete": "删除", "delete": "删除",
@@ -105,7 +106,8 @@
"rephrase": "重述", "rephrase": "重述",
"translate": "翻译" "translate": "翻译"
}, },
"citations": "引用", "webCitations": "万维网引用",
"iodCitations": "数联网引用",
"downloadCode": "下载代码", "downloadCode": "下载代码",
"date": { "date": {
"pinned": "已置顶", "pinned": "已置顶",

View File

@@ -3,6 +3,7 @@
"selectAPrompt": "选择一个提示词", "selectAPrompt": "选择一个提示词",
"githubRepository": "GitHub 仓库", "githubRepository": "GitHub 仓库",
"settings": "设置", "settings": "设置",
"metering": "计量",
"sidebarTitle": "聊天历史", "sidebarTitle": "聊天历史",
"error": "错误", "error": "错误",
"somethingWentWrong": "出现了错误", "somethingWentWrong": "出现了错误",

View File

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

View File

@@ -18,7 +18,7 @@ import { useTTS } from "@/hooks/useTTS"
import { tagColors } from "@/utils/color" import { tagColors } from "@/utils/color"
import { removeModelSuffix } from "@/db/models" import { removeModelSuffix } from "@/db/models"
import { GenerationInfo } from "./GenerationInfo" import { GenerationInfo } from "./GenerationInfo"
import { parseReasoning, } from "@/libs/reasoning" import { parseReasoning } from "@/libs/reasoning"
import { humanizeMilliseconds } from "@/utils/humanize-milliseconds" import { humanizeMilliseconds } from "@/utils/humanize-milliseconds"
type Props = { type Props = {
message: string message: string
@@ -36,7 +36,8 @@ type Props = {
isProcessing: boolean isProcessing: boolean
webSearch?: {} webSearch?: {}
isSearchingInternet?: boolean isSearchingInternet?: boolean
sources?: any[] webSources?: any[]
iodSources?: any[]
hideEditAndRegenerate?: boolean hideEditAndRegenerate?: boolean
onSourceClick?: (source: any) => void onSourceClick?: (source: any) => void
isTTSEnabled?: boolean isTTSEnabled?: boolean
@@ -166,7 +167,7 @@ export const PlaygroundMessage = (props: Props) => {
</div> </div>
)} )}
{props.isBot && props?.sources && props?.sources.length > 0 && ( {props.isBot && props?.webSources && props?.webSources.length > 0 && (
<Collapse <Collapse
className="mt-6" className="mt-6"
ghost ghost
@@ -175,15 +176,44 @@ export const PlaygroundMessage = (props: Props) => {
key: "1", key: "1",
label: ( label: (
<div className="italic text-gray-500 dark:text-gray-400"> <div className="italic text-gray-500 dark:text-gray-400">
{t("citations")} {t("webCitations")}
</div> </div>
), ),
children: ( children: (
<div className="mb-3 flex flex-wrap gap-2"> <div className="mb-3 flex flex-wrap gap-2">
{props?.sources?.map((source, index) => ( {props?.webSources?.map((source, index) => (
<MessageSource <MessageSource
onSourceClick={props.onSourceClick} onSourceClick={props.onSourceClick}
key={index} 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} source={source}
/> />
))} ))}

View File

@@ -1,6 +1,9 @@
import { useState } from "react"
import type React from "react"
import { KnowledgeIcon } from "@/components/Option/Knowledge/KnowledgeIcon" import { KnowledgeIcon } from "@/components/Option/Knowledge/KnowledgeIcon"
type Props = { type Props = {
index: number
source: { source: {
name?: string name?: string
url?: string url?: string
@@ -8,11 +11,20 @@ type Props = {
type?: string type?: string
pageContent?: string pageContent?: string
content?: string content?: string
doId?: string
description?: string
} }
onSourceClick?: (source: any) => void onSourceClick?: (source: any) => void
} }
export const MessageSource: React.FC<Props> = ({ source, onSourceClick }) => { export const MessageSource: React.FC<Props> = ({
index,
source,
onSourceClick
}) => {
// Add state for tracking and content visibility
const [showContent, setShowContent] = useState(false)
if (source?.mode === "rag" || source?.mode === "chat") { if (source?.mode === "rag" || source?.mode === "chat") {
return ( return (
<button <button
@@ -26,12 +38,46 @@ export const MessageSource: React.FC<Props> = ({ source, onSourceClick }) => {
) )
} }
const onContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation
setShowContent(true)
}
return ( return (
<div className="block items-center gap-1 text-xs text-gray-800 dark:text-gray-100 mb-1">
<span className="text-xs font-medium"></span>{" "}
<a <a
href={source?.url} href={source?.url}
target="_blank" target="_blank"
className="inline-flex cursor-pointer transition-shadow duration-300 ease-in-out hover:shadow-lg items-center rounded-md bg-gray-100 p-1 text-xs text-gray-800 border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-100 opacity-80 hover:opacity-100"> onContextMenu={onContextMenu}
className="inline-block cursor-pointer transition-shadow duration-300 ease-in-out hover:shadow-lg items-center rounded-md bg-gray-100 p-1 text-xs text-gray-800 border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-100 opacity-80 hover:opacity-100">
{source.doId ? (
<>
<span className="text-xs">
[{index + 1}] doid: {source.doId}
</span>
<br />
<span className="text-xs">{source.name}</span> <span className="text-xs">{source.name}</span>
{showContent && (
<div className="mt-2 p-2 border-t border-gray-200 dark:border-gray-700">
{source.content || source.pageContent || source.description}
</div>
)}
</>
) : (
<>
<span className="text-xs">
[{index + 1}] {source.name}
</span>
{showContent && (
<div className="mt-2 p-2 border-t border-gray-200 dark:border-gray-700">
{source.content || source.pageContent}
</div>
)}
</>
)}
</a> </a>
</div>
) )
} }

View File

@@ -5,6 +5,7 @@ import {
ChevronRight, ChevronRight,
CogIcon, CogIcon,
ComputerIcon, ComputerIcon,
Slice,
GithubIcon, GithubIcon,
PanelLeftIcon, PanelLeftIcon,
ZapIcon ZapIcon
@@ -240,6 +241,13 @@ export const Header: React.FC<Props> = ({
<CogIcon className="w-6 h-6" /> <CogIcon className="w-6 h-6" />
</NavLink> </NavLink>
</Tooltip> </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>
</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

@@ -0,0 +1,139 @@
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 data = [
{
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: "问题",
dataIndex: "queryContent",
key: "queryContent"
},
{
title: "提示词全文",
dataIndex: "prompt",
key: "prompt",
ellipsis: {
showTitle: false
},
render: (prompt) => (
<Tooltip placement="topLeft" title={prompt}>
{prompt}
</Tooltip>
),
width: "10%"
},
{
title: "思维链",
key: "thinkingChain",
dataIndex: "thinkingChain",
width: "10%"
},
{
title: "回答",
dataIndex: "responseContent",
key: "responseContent",
ellipsis: {
showTitle: false
},
render: (responseContent) => (
<Tooltip placement="topLeft" title={responseContent}>
{responseContent}
</Tooltip>
),
width: "10%"
},
{
title: "关联数据个数",
dataIndex: "relatedDataCount",
key: "relatedDataCount"
},
{
title: "数联网token",
dataIndex: "iodOutputToken",
key: "iodOutputToken",
render: (iodOutputToken) => <div>{iodOutputToken?.length}</div>
},
{
title: "大模型token",
key: "largeModelToken",
dataIndex: "largeModelToken",
render: (_, record) => {
return (
<div>
{record.iodInputToken?.length + record.iodOutputToken?.length}
</div>
)
}
},
{
title: "日期",
dataIndex: "date",
key: "date",
render: (date) => {
return <div>{formatDate(date)}</div>
}
},
{
title: "耗时",
key: "timeTaken",
dataIndex: "timeTaken"
},
{
title: "操作",
key: "action",
render: (_, record) => (
<Space size="middle">
{/* <a>Invite {record.name}</a> */}
<NavLink to={`/metering/list/${record.id}`}>
<a></a>
</NavLink>
</Space>
)
}
]
export const MeteringDetail = () => {
const { chatMessages } = useStoreMessageOption()
console.log(chatMessages, "opppp")
return (
<div className="pt-[4rem]">
<List
grid={{ gutter: 16, column: 3 }}
dataSource={data}
renderItem={(item) => (
<List.Item>
<Card title={item.key}>{item.value}</Card>
</List.Item>
)}
/>
<Table<ChatMessage> columns={columns} dataSource={chatMessages} />
</div>
)
}

View File

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

View File

@@ -13,7 +13,7 @@ import { getVariable } from "@/utils/select-variable"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect" import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect"
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition" import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"
import { PiGlobe } from "react-icons/pi" import { PiGlobe, PiNetwork } from "react-icons/pi"
import { handleChatInputKeyDown } from "@/utils/key-down" import { handleChatInputKeyDown } from "@/utils/key-down"
import { getIsSimpleInternetSearch } from "@/services/search" import { getIsSimpleInternetSearch } from "@/services/search"
@@ -34,6 +34,8 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
streaming: isSending, streaming: isSending,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
selectedQuickPrompt, selectedQuickPrompt,
textareaRef, textareaRef,
setSelectedQuickPrompt, setSelectedQuickPrompt,
@@ -301,6 +303,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
<div className="mt-2 flex justify-between items-center"> <div className="mt-2 flex justify-between items-center">
<div className="flex"> <div className="flex">
{!selectedKnowledge && ( {!selectedKnowledge && (
<div>
<Tooltip title={t("tooltip.searchInternet")}> <Tooltip title={t("tooltip.searchInternet")}>
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<PiGlobe <PiGlobe
@@ -314,6 +317,20 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
/> />
</div> </div>
</Tooltip> </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>
<div className="flex !justify-end gap-3"> <div className="flex !justify-end gap-3">

View File

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

View File

@@ -29,7 +29,8 @@ type Message = {
role: string role: string
content: string content: string
images?: string[] images?: string[]
sources?: string[] webSources?: string[]
iodSources?: string[]
search?: WebSearch search?: WebSearch
createdAt: number createdAt: number
reasoning_time_taken?: number reasoning_time_taken?: number
@@ -254,7 +255,8 @@ export const saveMessage = async (
role: string, role: string,
content: string, content: string,
images: string[], images: string[],
source?: any[], webSources?: any[],
iodSources?: any[],
time?: number, time?: number,
message_type?: string, message_type?: string,
generationInfo?: any, generationInfo?: any,
@@ -273,7 +275,8 @@ export const saveMessage = async (
content, content,
images, images,
createdAt, createdAt,
sources: source, webSources,
iodSources,
messageType: message_type, messageType: message_type,
generationInfo: generationInfo, generationInfo: generationInfo,
reasoning_time_taken reasoning_time_taken
@@ -303,7 +306,8 @@ export const formatToMessage = (messages: MessageHistory): MessageType[] => {
isBot: message.role === "assistant", isBot: message.role === "assistant",
message: message.content, message: message.content,
name: message.name, name: message.name,
sources: message?.sources || [], webSources: message?.webSources || [],
iodSources: message?.iodSources || [],
images: message.images || [], images: message.images || [],
generationInfo: message?.generationInfo, generationInfo: message?.generationInfo,
reasoning_time_taken: message?.reasoning_time_taken reasoning_time_taken: message?.reasoning_time_taken

View File

@@ -62,6 +62,7 @@ export const saveMessageOnError = async ({
userMessage, userMessage,
[image], [image],
[], [],
[],
1, 1,
message_type message_type
) )
@@ -73,6 +74,7 @@ export const saveMessageOnError = async ({
botMessage, botMessage,
[], [],
[], [],
[],
2, 2,
message_type message_type
) )
@@ -91,6 +93,7 @@ export const saveMessageOnError = async ({
userMessage, userMessage,
[image], [image],
[], [],
[],
1, 1,
message_type message_type
) )
@@ -102,6 +105,7 @@ export const saveMessageOnError = async ({
botMessage, botMessage,
[], [],
[], [],
[],
2, 2,
message_type message_type
) )
@@ -126,7 +130,8 @@ export const saveMessageOnSuccess = async ({
message, message,
image, image,
fullText, fullText,
source, webSources,
iodSources,
message_source = "web-ui", message_source = "web-ui",
message_type, generationInfo, message_type, generationInfo,
prompt_id, prompt_id,
@@ -140,7 +145,8 @@ export const saveMessageOnSuccess = async ({
message: string message: string
image: string image: string
fullText: string fullText: string
source: any[] webSources: any[]
iodSources: any[]
message_source?: "copilot" | "web-ui", message_source?: "copilot" | "web-ui",
message_type?: string message_type?: string
generationInfo?: any generationInfo?: any
@@ -157,6 +163,7 @@ export const saveMessageOnSuccess = async ({
message, message,
[image], [image],
[], [],
[],
1, 1,
message_type, message_type,
generationInfo, generationInfo,
@@ -169,7 +176,8 @@ export const saveMessageOnSuccess = async ({
"assistant", "assistant",
fullText, fullText,
[], [],
source, webSources,
iodSources,
2, 2,
message_type, message_type,
generationInfo, generationInfo,
@@ -189,6 +197,7 @@ export const saveMessageOnSuccess = async ({
message, message,
[image], [image],
[], [],
[],
1, 1,
message_type, message_type,
generationInfo, generationInfo,
@@ -200,7 +209,8 @@ export const saveMessageOnSuccess = async ({
"assistant", "assistant",
fullText, fullText,
[], [],
source, webSources,
iodSources,
2, 2,
message_type, message_type,
generationInfo, generationInfo,

View File

@@ -59,6 +59,8 @@ export const useMessage = () => {
setIsSearchingInternet, setIsSearchingInternet,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet isSearchingInternet
} = useStoreMessageOption() } = useStoreMessageOption()
const [defaultInternetSearchOn] = useStorage("defaultInternetSearchOn", false) const [defaultInternetSearchOn] = useStorage("defaultInternetSearchOn", false)
@@ -185,14 +187,16 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [],
images: [] images: []
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -203,7 +207,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -334,7 +339,16 @@ export const useMessage = () => {
} }
let context: string = "" let context: string = ""
let source: { let webSources: {
name: any
type: any
mode: string
url: string
pageContent: string
metadata: Record<string, any>
}[] = []
// TODO: update type
let iodSources: {
name: any name: any
type: any type: any
mode: string mode: string
@@ -346,7 +360,7 @@ export const useMessage = () => {
if (chatWithWebsiteEmbedding) { if (chatWithWebsiteEmbedding) {
const docs = await vectorstore.similaritySearch(query, 4) const docs = await vectorstore.similaritySearch(query, 4)
context = formatDocs(docs) context = formatDocs(docs)
source = docs.map((doc) => { webSources = docs.map((doc) => {
return { return {
...doc, ...doc,
name: doc?.metadata?.source || "untitled", name: doc?.metadata?.source || "untitled",
@@ -365,7 +379,7 @@ export const useMessage = () => {
.slice(0, maxWebsiteContext) .slice(0, maxWebsiteContext)
} }
source = [ webSources = [
{ {
name: embedURL, name: embedURL,
type: type, type: type,
@@ -476,7 +490,8 @@ export const useMessage = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source, webSources,
iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
} }
@@ -506,7 +521,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source, webSources,
iodSources,
message_source: "copilot", message_source: "copilot",
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
@@ -606,14 +622,16 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [],
images: [] images: []
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -624,7 +642,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -787,7 +806,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source: [], webSources: [],
iodSources: [],
message_source: "copilot", message_source: "copilot",
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
@@ -891,14 +911,16 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [],
images: [image] images: [image]
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -909,7 +931,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -1077,7 +1100,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source: [], webSources: [],
iodSources: [],
message_source: "copilot", message_source: "copilot",
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
@@ -1114,12 +1138,14 @@ export const useMessage = () => {
} }
const searchChatMode = async ( const searchChatMode = async (
webSearch: boolean,
iodSearch,
message: string, message: string,
image: string, image: string,
isRegenerate: boolean, isRegenerate: boolean,
messages: Message[], messages: Message[],
history: ChatHistory, history: ChatHistory,
signal: AbortSignal signal: AbortSignal,
) => { ) => {
const url = await getOllamaURL() const url = await getOllamaURL()
setStreaming(true) setStreaming(true)
@@ -1176,14 +1202,16 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [],
images: [image] images: [image]
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -1194,7 +1222,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -1271,7 +1300,8 @@ export const useMessage = () => {
query = removeReasoning(query) query = removeReasoning(query)
} }
const { prompt, source } = await getSystemPromptForWeb(query) const { prompt, webSources, iodSources } =
await getSystemPromptForWeb(query, [], webSearch, iodSearch)
setIsSearchingInternet(false) setIsSearchingInternet(false)
// message = message.trim().replaceAll("\n", " ") // message = message.trim().replaceAll("\n", " ")
@@ -1394,7 +1424,8 @@ export const useMessage = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source, webSources,
iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
} }
@@ -1424,7 +1455,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source, webSources,
iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
}) })
@@ -1523,7 +1555,8 @@ export const useMessage = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [],
images: [image], images: [image],
messageType: messageType messageType: messageType
}, },
@@ -1531,7 +1564,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -1542,7 +1576,8 @@ export const useMessage = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -1688,7 +1723,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source: [], webSources: [],
iodSources: [],
message_source: "copilot", message_source: "copilot",
message_type: messageType, message_type: messageType,
generationInfo, generationInfo,
@@ -1766,14 +1802,16 @@ export const useMessage = () => {
) )
} else { } else {
if (chatMode === "normal") { if (chatMode === "normal") {
if (webSearch) { if (webSearch || iodSearch) {
await searchChatMode( await searchChatMode(
webSearch,
iodSearch,
message, message,
image, image,
isRegenerate || false, isRegenerate || false,
messages, messages,
memory || history, memory || history,
signal signal,
) )
} else { } else {
await normalChatMode( await normalChatMode(
@@ -1906,6 +1944,8 @@ export const useMessage = () => {
regenerateLastMessage, regenerateLastMessage,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet, isSearchingInternet,
selectedQuickPrompt, selectedQuickPrompt,
setSelectedQuickPrompt, setSelectedQuickPrompt,

View File

@@ -3,11 +3,12 @@ import { cleanUrl } from "~/libs/clean-url"
import { import {
defaultEmbeddingModelForRag, defaultEmbeddingModelForRag,
geWebSearchFollowUpPrompt, geWebSearchFollowUpPrompt,
geWebSearchKeywordsPrompt,
getOllamaURL, getOllamaURL,
promptForRag, promptForRag,
systemPromptForNonRagOption systemPromptForNonRagOption
} from "~/services/ollama" } from "~/services/ollama"
import { type ChatHistory, type Message } from "~/store/option" import { type ChatHistory, ChatMessage, type Message } from "~/store/option"
import { SystemMessage } from "@langchain/core/messages" import { SystemMessage } from "@langchain/core/messages"
import { useStoreMessageOption } from "~/store/option" import { useStoreMessageOption } from "~/store/option"
import { import {
@@ -54,6 +55,8 @@ export const useMessageOption = () => {
const { const {
history, history,
setHistory, setHistory,
chatMessages,
setChatMessages,
setStreaming, setStreaming,
streaming, streaming,
setIsFirstMessage, setIsFirstMessage,
@@ -67,6 +70,8 @@ export const useMessageOption = () => {
setChatMode, setChatMode,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet, isSearchingInternet,
setIsSearchingInternet, setIsSearchingInternet,
selectedQuickPrompt, selectedQuickPrompt,
@@ -109,8 +114,30 @@ export const useMessageOption = () => {
setWebSearch(true) setWebSearch(true)
} }
} }
// 从最后的结果中解析出 思维链 和 结果
const responseResolver = (msg: string) => {
const thinkStart = msg.indexOf("<think>")
const thinkEnd = msg.indexOf("</think>")
let think = ""
let content = ""
if (thinkStart > -1 && thinkEnd > -1) {
think = msg.substring(thinkStart + 7, thinkEnd)
content = msg.substring(thinkEnd + 8)
} else {
content = msg
}
// 去掉换行符
think = think.replace(/\n/g, "")
content = content.replace(/\n/g, "")
return {
think,
content
}
}
const searchChatMode = async ( const searchChatMode = async (
webSearch: boolean,
iodSearch: boolean,
message: string, message: string,
image: string, image: string,
isRegenerate: boolean, isRegenerate: boolean,
@@ -161,9 +188,12 @@ export const useMessageOption = () => {
useMlock: useMlock:
currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock
}) })
let newMessage: Message[] = [] let newMessage: Message[] = []
let generateMessageId = generateID() let generateMessageId = generateID()
const chatMessage: ChatMessage = {
id: generateMessageId,
queryContent: message
} as ChatMessage
if (!isRegenerate) { if (!isRegenerate) {
newMessage = [ newMessage = [
@@ -172,14 +202,16 @@ export const useMessageOption = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [],
images: [image] images: [image]
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -190,7 +222,8 @@ export const useMessageOption = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -204,6 +237,7 @@ export const useMessageOption = () => {
setIsSearchingInternet(true) setIsSearchingInternet(true)
let query = message let query = message
let keywords: string[] = []
if (newMessage.length > 2) { if (newMessage.length > 2) {
let questionPrompt = await geWebSearchFollowUpPrompt() let questionPrompt = await geWebSearchFollowUpPrompt()
@@ -268,8 +302,29 @@ export const useMessageOption = () => {
query = removeReasoning(query) query = removeReasoning(query)
} }
const { prompt, source } = await getSystemPromptForWeb(query) // Currently only IoD search use keywords
if (iodSearch) {
// Extract keywords
const questionPrompt = await geWebSearchKeywordsPrompt()
const promptForQuestion = questionPrompt.replaceAll("{query}", query)
const response = await ollama.invoke(promptForQuestion)
let res = response.content.toString()
res = removeReasoning(res)
keywords = res
.replace(/^Keywords:/i, "")
.split(", ")
.map((k) => k.trim())
}
const { prompt, webSources, iodSources } = await getSystemPromptForWeb(
query,
keywords,
webSearch,
iodSearch
)
console.log("prompt:\n" + prompt)
setIsSearchingInternet(false) setIsSearchingInternet(false)
chatMessage.prompt = prompt
// message = message.trim().replaceAll("\n", " ") // message = message.trim().replaceAll("\n", " ")
@@ -390,7 +445,8 @@ export const useMessageOption = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source, webSources,
iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
} }
@@ -420,13 +476,22 @@ export const useMessageOption = () => {
message, message,
image, image,
fullText, fullText,
source, webSources,
iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
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) { } catch (e) {
const errorSave = await saveMessageOnError({ const errorSave = await saveMessageOnError({
e, e,
@@ -552,14 +617,16 @@ export const useMessageOption = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [],
images: [image] images: [image]
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -570,7 +637,8 @@ export const useMessageOption = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -855,14 +923,16 @@ export const useMessageOption = () => {
isBot: false, isBot: false,
name: "You", name: "You",
message, message,
sources: [], webSources: [],
iodSources: [],
images: [] images: []
}, },
{ {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -873,7 +943,8 @@ export const useMessageOption = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@@ -1076,7 +1147,7 @@ export const useMessageOption = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source, webSources: source,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
} }
@@ -1175,8 +1246,10 @@ export const useMessageOption = () => {
signal signal
) )
} else { } else {
if (webSearch) { if (webSearch || iodSearch) {
await searchChatMode( await searchChatMode(
webSearch,
iodSearch,
message, message,
image, image,
isRegenerate, isRegenerate,
@@ -1311,6 +1384,8 @@ export const useMessageOption = () => {
regenerateLastMessage, regenerateLastMessage,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet, isSearchingInternet,
setIsSearchingInternet, setIsSearchingInternet,
selectedQuickPrompt, selectedQuickPrompt,

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,15 +21,39 @@ const DEFAULT_RAG_QUESTION_PROMPT =
const DEFAUTL_RAG_SYSTEM_PROMPT = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say you don't know. DO NOT try to make up an answer. If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context. {context} Question: {question} Helpful answer:` const DEFAUTL_RAG_SYSTEM_PROMPT = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say you don't know. DO NOT try to make up an answer. If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context. {context} Question: {question} Helpful answer:`
const DEFAULT_WEBSEARCH_PROMP = `You are an AI model who is expert at searching the web and answering user's queries. const DEFAULT_WEBSEARCH_PROMPT = `You are an AI 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}. Generate a response that is informative and relevant to the user's query based on provided search results. the current date and time are {current_date_time}.
\`search-results\` block provides knowledge from the web search results. You can use this information to generate a meaningful response. \`iod-search-results\` block provides knowledge from the Internet of Data (数联网) search results. Each search result has a format of:
\`<result doId="{doId}" name="{name}" url="{url}" id="{id}">{content}</result>\`
Please show the \`doId\` and \`name\` of the search result when you cite the Internet of Data search result, in the following format, in English:
\`[IoD source [id] doId: {doId} "{name}"]({url})\`
Or in Chinese:
\`[数联网引用[id] doId: {doId} "{name}"]({url})\`
For example, in English:
\`[IoD source [1] doId: 10.48550/arXiv.1803.05591v2 "On the insufficiency of existing momentum schemes for Stochastic Optimization"](http://arxiv.org/pdf/1803.05591v2.pdf)\`
Or in Chinese:
\`[数联网引用[1] doId: 10.48550/arXiv.1803.05591v2 "On the insufficiency of existing momentum schemes for Stochastic Optimization"](http://arxiv.org/pdf/1803.05591v2.pdf)\`
<search-results> \`web-search-results\` block provides knowledge from the World Wide Web (万维网) search results.
{search_results} Please show the \`doId\` and \`name\` of the search result when you cite the search result, in the following format, in English:
</search-results> \`[3W source [id] "{name}"]({url})\`
Or in Chinese:
\`[万维网引用[id] "{name}"]({url})\`
For example, in English:
\`[3W source [1] On the insufficiency of existing momentum schemes for Stochastic Optimization](http://arxiv.org/pdf/1803.05591v2.pdf)\`
Or in Chinese:
\`[万维网引用[1] On the insufficiency of existing momentum schemes for Stochastic Optimization](http://arxiv.org/pdf/1803.05591v2.pdf)\`
You can use these information to generate a meaningful response.
<iod-search-results>
{iod_search_results}
</iod-search-results>
<web-search-results>
{web_search_results}
</web-search-results>
` `
const DEFAULT_WEBSEARCH_FOLLOWUP_PROMPT = `You will give a follow-up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the AI model to search the internet. const DEFAULT_WEBSEARCH_FOLLOWUP_PROMPT = `You will give a follow-up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the AI model to search the internet.
@@ -58,6 +82,30 @@ Follow-up question: {question}
Rephrased question: Rephrased question:
` `
const DEFAULT_WEBSEARCH_KEYWORDS_PROMPT = `Extract the most important keywords from the query (at most 3), and give me English and Chinese versions of the keywords.
The result format should be: keyword_1, keyword_2, ..., keyword_n
Example:
Query: What are the symptoms of a heart attack?
Keywords: symptoms, 症状, heart attack, 心臟病
Query: 什么是物联网?
Keywords: Internet of Things, IoT, 物联网
Query: 人工智能的发展趋势?
Keywords: Artificial Intelligence, AI, 人工智能, trend, 趋势
Query: {query}
Keywords:
`
export const getOllamaURL = async () => { export const getOllamaURL = async () => {
const ollamaURL = await storage.get("ollamaURL") const ollamaURL = await storage.get("ollamaURL")
if (!ollamaURL || ollamaURL.length === 0) { if (!ollamaURL || ollamaURL.length === 0) {
@@ -385,7 +433,7 @@ export const saveForRag = async (
export const getWebSearchPrompt = async () => { export const getWebSearchPrompt = async () => {
const prompt = await storage.get("webSearchPrompt") const prompt = await storage.get("webSearchPrompt")
if (!prompt || prompt.length === 0) { if (!prompt || prompt.length === 0) {
return DEFAULT_WEBSEARCH_PROMP return DEFAULT_WEBSEARCH_PROMPT
} }
return prompt return prompt
} }
@@ -411,6 +459,18 @@ export const setWebPrompts = async (prompt: string, followUpPrompt: string) => {
await setWebSearchFollowUpPrompt(followUpPrompt) await setWebSearchFollowUpPrompt(followUpPrompt)
} }
export const geWebSearchKeywordsPrompt = async () => {
const prompt = await storage.get("webSearchKeywordsPrompt")
if (!prompt || prompt.length === 0) {
return DEFAULT_WEBSEARCH_KEYWORDS_PROMPT
}
return prompt
}
export const setWebSearchKeywordsPrompt = async (prompt: string) => {
await storage.set("webSearchKeywordsPrompt", prompt)
}
export const getPageShareUrl = async () => { export const getPageShareUrl = async () => {
const pageShareUrl = await storage.get("pageShareUrl") const pageShareUrl = await storage.get("pageShareUrl")
if (!pageShareUrl || pageShareUrl.length === 0) { if (!pageShareUrl || pageShareUrl.length === 0) {

View File

@@ -14,7 +14,8 @@ export type Message = {
isBot: boolean isBot: boolean
name: string name: string
message: string message: string
sources: any[] webSources: any[]
iodSources: any[]
images?: string[] images?: string[]
search?: WebSearch search?: WebSearch
reasoning_time_taken?: number reasoning_time_taken?: number
@@ -25,15 +26,43 @@ export type Message = {
export type ChatHistory = { export type ChatHistory = {
role: "user" | "assistant" | "system" role: "user" | "assistant" | "system"
content: string content: string
image?: string, image?: string
messageType?: string 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 = { type State = {
messages: Message[] messages: Message[]
setMessages: (messages: Message[]) => void setMessages: (messages: Message[]) => void
history: ChatHistory history: ChatHistory
setHistory: (history: ChatHistory) => void setHistory: (history: ChatHistory) => void
chatMessages: ChatMessage[]
setChatMessages: (chatMessages: ChatMessage[]) => void
streaming: boolean streaming: boolean
setStreaming: (streaming: boolean) => void setStreaming: (streaming: boolean) => void
isFirstMessage: boolean isFirstMessage: boolean
@@ -52,6 +81,8 @@ type State = {
setIsEmbedding: (isEmbedding: boolean) => void setIsEmbedding: (isEmbedding: boolean) => void
webSearch: boolean webSearch: boolean
setWebSearch: (webSearch: boolean) => void setWebSearch: (webSearch: boolean) => void
iodSearch: boolean
setIodSearch: (iodSearch: boolean) => void
isSearchingInternet: boolean isSearchingInternet: boolean
setIsSearchingInternet: (isSearchingInternet: boolean) => void setIsSearchingInternet: (isSearchingInternet: boolean) => void
@@ -79,6 +110,8 @@ export const useStoreMessageOption = create<State>((set) => ({
setMessages: (messages) => set({ messages }), setMessages: (messages) => set({ messages }),
history: [], history: [],
setHistory: (history) => set({ history }), setHistory: (history) => set({ history }),
chatMessages: [],
setChatMessages: (chatMessages) => set({ chatMessages }),
streaming: false, streaming: false,
setStreaming: (streaming) => set({ streaming }), setStreaming: (streaming) => set({ streaming }),
isFirstMessage: true, isFirstMessage: true,
@@ -100,6 +133,8 @@ export const useStoreMessageOption = create<State>((set) => ({
setIsEmbedding: (isEmbedding) => set({ isEmbedding }), setIsEmbedding: (isEmbedding) => set({ isEmbedding }),
webSearch: false, webSearch: false,
setWebSearch: (webSearch) => set({ webSearch }), setWebSearch: (webSearch) => set({ webSearch }),
iodSearch: false,
setIodSearch: (iodSearch) => set({ iodSearch }),
isSearchingInternet: false, isSearchingInternet: false,
setIsSearchingInternet: (isSearchingInternet) => set({ isSearchingInternet }), setIsSearchingInternet: (isSearchingInternet) => set({ isSearchingInternet }),
selectedSystemPrompt: null, selectedSystemPrompt: null,
@@ -115,5 +150,5 @@ export const useStoreMessageOption = create<State>((set) => ({
setTemporaryChat: (temporaryChat) => set({ temporaryChat }), setTemporaryChat: (temporaryChat) => set({ temporaryChat }),
useOCR: false, useOCR: false,
setUseOCR: (useOCR) => set({ useOCR }), setUseOCR: (useOCR) => set({ useOCR })
})) }))

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

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

View File

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

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

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

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

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

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

@@ -0,0 +1,205 @@
import { cleanUrl } from "@/libs/clean-url"
import { PageAssistHtmlLoader } from "@/loader/html"
import { PageAssistPDFUrlLoader } from "@/loader/pdf-url"
import { pageAssistEmbeddingModel } from "@/models/embedding"
import { defaultEmbeddingModelForRag, getOllamaURL } from "@/services/ollama"
import {
getIsSimpleInternetSearch,
totalSearchResults
} from "@/services/search"
import { getPageAssistTextSplitter } from "@/utils/text-splitter"
import type { Document } from "@langchain/core/documents"
import { MemoryVectorStore } from "langchain/vectorstores/memory"
import type { IodRegistryEntry } from "~/types/iod"
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: "MUST",
value: keyword
}
]
},
body: ""
}
})
export async function localIodSearch(
query: string,
keywords: string[]
): Promise<IodRegistryEntry[]> {
const TOTAL_SEARCH_RESULTS = await totalSearchResults()
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 []
})
})
)
).flat()
return results
}
const ARXIV_URL_PATTERN = /^https?:\/\/arxiv\.org\//
const ARXIV_NO_HTM = "No HTML for"
export const searchIod = async (query: string, keywords: string[]) => {
const searchResults = await localIodSearch(query, keywords)
const isSimpleMode = await getIsSimpleInternetSearch()
if (isSimpleMode) {
await getOllamaURL()
return searchResults
}
const docs: Document<Record<string, any>>[] = []
const resMap = new Map<string, IodRegistryEntry>()
for (const result of searchResults) {
const url = result.url
if (!url) continue
let htmlUrl = ""
if (ARXIV_URL_PATTERN.test(url)) {
htmlUrl = url.replace("/pdf/", "/html/").replace(".pdf", "")
}
let noHtml = htmlUrl === ""
if (!noHtml) {
const loader = new PageAssistHtmlLoader({
html: "",
url: htmlUrl
})
try {
const documents = await loader.loadByURL()
for (const doc of documents) {
if (doc.pageContent.includes(ARXIV_NO_HTM)) {
noHtml = true
return
}
docs.push(doc)
}
} catch (e) {
console.log(e)
noHtml = true
}
}
if (noHtml) {
if (url.endsWith(".pdf")) {
const loader = new PageAssistPDFUrlLoader({
name: result.name,
url
})
try {
const documents = await loader.load()
for (const doc of documents) {
docs.push(doc)
}
} catch (e) {
console.log(e)
}
} else {
const loader = new PageAssistHtmlLoader({
html: "",
url
})
try {
const documents = await loader.loadByURL()
for (const doc of documents) {
docs.push(doc)
}
} catch (e) {
console.log(e)
}
}
}
}
const ollamaUrl = await getOllamaURL()
const embeddingModle = await defaultEmbeddingModelForRag()
const ollamaEmbedding = await pageAssistEmbeddingModel({
model: embeddingModle || "",
baseUrl: cleanUrl(ollamaUrl)
})
const textSplitter = await getPageAssistTextSplitter()
const chunks = await textSplitter.splitDocuments(docs)
const store = new MemoryVectorStore(ollamaEmbedding)
await store.addDocuments(chunks)
const resultsWithEmbeddings = await store.similaritySearch(query, 3)
const searchResult = resultsWithEmbeddings.map((result) => {
// `source` for PDF type
const key = result.metadata.url || result.metadata.source
if (!key) return null
const fullRes = resMap[key]
return {
...fullRes,
content: result.pageContent
}
}).filter((r) => r)
return searchResult
}

View File

@@ -8,6 +8,9 @@ import { getWebsiteFromQuery, processSingleWebsite } from "./website"
import { searxngSearch } from "./search-engines/searxng" import { searxngSearch } from "./search-engines/searxng"
import { braveAPISearch } from "./search-engines/brave-api" import { braveAPISearch } from "./search-engines/brave-api"
import { webBaiduSearch } from "./search-engines/baidu" import { webBaiduSearch } from "./search-engines/baidu"
import { searchIod } from "./iod"
import type { WebSearchResult } from "~/types/web"
import type { IodRegistryEntry } from "~/types/iod"
const getHostName = (url: string) => { const getHostName = (url: string) => {
try { try {
@@ -18,54 +21,101 @@ const getHostName = (url: string) => {
} }
} }
const searchWeb = (provider: string, query: string) => { async function searchWeb(
provider: string,
query: string
): Promise<WebSearchResult[]> {
let results = []
switch (provider) { switch (provider) {
case "duckduckgo": case "duckduckgo":
return webDuckDuckGoSearch(query) results = await webDuckDuckGoSearch(query)
break
case "sogou": case "sogou":
return webSogouSearch(query) results = await webSogouSearch(query)
break
case "brave": case "brave":
return webBraveSearch(query) results = await webBraveSearch(query)
break
case "searxng": case "searxng":
return searxngSearch(query) results = await searxngSearch(query)
break
case "brave-api": case "brave-api":
return braveAPISearch(query) results = await braveAPISearch(query)
break
case "baidu": case "baidu":
return webBaiduSearch(query) results = await webBaiduSearch(query)
break
default: default:
return webGoogleSearch(query) results = await webGoogleSearch(query)
break
} }
return results.map((r) => ({ ...r, name: getHostName(r.url) }))
} }
export const getSystemPromptForWeb = async (query: string) => { export const getSystemPromptForWeb = async (
query: string,
keywords: string[] = [],
webSearch = true,
iodSearch = false
) => {
try { try {
const websiteVisit = getWebsiteFromQuery(query) const websiteVisit = getWebsiteFromQuery(query)
let search: { let webSearchResults: WebSearchResult[] = []
url: any; // let search_results_web = ""
content: string;
}[] = []
if (webSearch) {
const isVisitSpecificWebsite = await getIsVisitSpecificWebsite() const isVisitSpecificWebsite = await getIsVisitSpecificWebsite()
if (isVisitSpecificWebsite && websiteVisit.hasUrl) { if (isVisitSpecificWebsite && websiteVisit.hasUrl) {
const url = websiteVisit.url const url = websiteVisit.url
const queryWithoutUrl = websiteVisit.queryWithouUrls const queryWithoutUrl = websiteVisit.queryWithouUrls
search = await processSingleWebsite(url, queryWithoutUrl) webSearchResults = await processSingleWebsite(url, queryWithoutUrl)
} else { } else {
const searchProvider = await getSearchProvider() const searchProvider = await getSearchProvider()
search = await searchWeb(searchProvider, query) webSearchResults = await searchWeb(searchProvider, query)
} }
// search_results_web = webSearchResults
// .map(
// (result, idx) =>
// `<result source="${result.url}" id="${idx}">${result.content}</result>`
// )
// .join("\n")
}
const search_results = search let iodSearchResults: IodRegistryEntry[] = []
// let search_results_iod = ""
if (iodSearch) {
iodSearchResults = await searchIod(query, keywords)
// search_results_iod = iodSearchResults
// .map(
// (result, idx) =>
// `<result source="${result.url}" id="${idx}">${result.content}</result>`
// )
// .join("\n")
}
const iod_search_results = iodSearchResults
.map((res) => ({
doId: res.doId,
name: res.name,
url: res.url,
content: res.content || res.description
}))
.map( .map(
(result, idx) => (result, idx) =>
`<result source="${result.url}" id="${idx}">${result.content}</result>` `<result doId="${result.doId}" name="${result.name}" source="${result.url}" id="${idx + 1}">${result.content}</result>`
) )
.join("\n") .join("\n")
console.log("iod_search_result: " + iod_search_results)
const web_search_results = webSearchResults
.map(
(result, idx) =>
`<result source="${result.url}" name="${result.name}" id="${idx + 1}">${result.content}</result>`
)
.join("\n")
console.log("web_search_result: " + web_search_results)
const current_date_time = new Date().toLocaleString() const current_date_time = new Date().toLocaleString()
@@ -73,23 +123,26 @@ export const getSystemPromptForWeb = async (query: string) => {
const prompt = system const prompt = system
.replace("{current_date_time}", current_date_time) .replace("{current_date_time}", current_date_time)
.replace("{search_results}", search_results) .replace("{iod_search_results}", iod_search_results)
.replace("{web_search_results}", web_search_results)
return { return {
prompt, prompt,
source: search.map((result) => { webSources: webSearchResults.map((result) => {
return { return {
url: result.url, url: result.url,
name: getHostName(result.url), name: result.name,
type: "url" type: "url"
} }
}) }),
iodSources: iodSearchResults
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
return { return {
prompt: "", prompt: "",
source: [] webSources: [],
iodSources: []
} }
} }
} }