feat: add IoD search

This commit is contained in:
Nex Zhu 2025-02-14 18:17:12 +08:00
parent 691575e449
commit e8471f1802
33 changed files with 524 additions and 104 deletions

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

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

@ -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 (
<a <div className="block items-center gap-1 text-xs text-gray-800 dark:text-gray-100 mb-1">
href={source?.url} <span className="text-xs font-medium"></span>{" "}
target="_blank" <a
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"> href={source?.url}
<span className="text-xs">{source.name}</span> target="_blank"
</a> onContextMenu={onContextMenu}
className="inline-block cursor-pointer transition-shadow duration-300 ease-in-out hover:shadow-lg items-center rounded-md bg-gray-100 p-1 text-xs text-gray-800 border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-100 opacity-80 hover:opacity-100">
{source.doId ? (
<>
<span className="text-xs">
[{index + 1}] doid: {source.doId}
</span>
<br />
<span className="text-xs">{source.name}</span>
{showContent && (
<div className="mt-2 p-2 border-t border-gray-200 dark:border-gray-700">
{source.content || source.pageContent || source.description}
</div>
)}
</>
) : (
<>
<span className="text-xs">
[{index + 1}] {source.name}
</span>
{showContent && (
<div className="mt-2 p-2 border-t border-gray-200 dark:border-gray-700">
{source.content || source.pageContent}
</div>
)}
</>
)}
</a>
</div>
) )
} }

View File

@ -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,6 +3,7 @@ import { cleanUrl } from "~/libs/clean-url"
import { import {
defaultEmbeddingModelForRag, defaultEmbeddingModelForRag,
geWebSearchFollowUpPrompt, geWebSearchFollowUpPrompt,
geWebSearchKeywordsPrompt,
getOllamaURL, getOllamaURL,
promptForRag, promptForRag,
systemPromptForNonRagOption systemPromptForNonRagOption
@ -67,6 +68,8 @@ export const useMessageOption = () => {
setChatMode, setChatMode,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet, isSearchingInternet,
setIsSearchingInternet, setIsSearchingInternet,
selectedQuickPrompt, selectedQuickPrompt,
@ -111,6 +114,8 @@ export const useMessageOption = () => {
} }
const searchChatMode = async ( const searchChatMode = async (
webSearch: boolean,
iodSearch: boolean,
message: string, message: string,
image: string, image: string,
isRegenerate: boolean, isRegenerate: boolean,
@ -172,14 +177,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 +197,8 @@ export const useMessageOption = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@ -204,6 +212,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,7 +277,23 @@ 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,
)
setIsSearchingInternet(false) setIsSearchingInternet(false)
// message = message.trim().replaceAll("\n", " ") // message = message.trim().replaceAll("\n", " ")
@ -390,7 +415,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,7 +446,8 @@ export const useMessageOption = () => {
message, message,
image, image,
fullText, fullText,
source, webSources,
iodSources,
generationInfo, generationInfo,
reasoning_time_taken: timetaken reasoning_time_taken: timetaken
}) })
@ -552,14 +579,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 +599,8 @@ export const useMessageOption = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@ -855,14 +885,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 +905,8 @@ export const useMessageOption = () => {
isBot: true, isBot: true,
name: selectedModel, name: selectedModel,
message: "▋", message: "▋",
sources: [], webSources: [],
iodSources: [],
id: generateMessageId id: generateMessageId
} }
] ]
@ -1076,7 +1109,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 +1208,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 +1346,8 @@ export const useMessageOption = () => {
regenerateLastMessage, regenerateLastMessage,
webSearch, webSearch,
setWebSearch, setWebSearch,
iodSearch,
setIodSearch,
isSearchingInternet, isSearchingInternet,
setIsSearchingInternet, setIsSearchingInternet,
selectedQuickPrompt, selectedQuickPrompt,

View File

@ -58,6 +58,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) {
@ -411,6 +435,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
@ -52,6 +53,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
@ -100,6 +103,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,

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

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

@ -0,0 +1,148 @@
import { cleanUrl } from "@/libs/clean-url"
import { PageAssistHtmlLoader } from "@/loader/html"
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"
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 const localIodSearch = async (query: string, keywords: string[]) => {
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 =
body.data?.results?.filter((r) => r.url || r.pdf_url) || []
results.forEach((r) => {
r.url = r.url || r.pdf_url
})
return results
})
.catch((e) => {
console.log(e)
return []
})
})
)
).flat()
return results
}
const ARXIV_URL = /^https:\/\/arxiv.org\//
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>>[] = []
for (const result of searchResults) {
let url = result.url
if (ARXIV_URL.test(result.url)) {
url = result.url.replace("/pdf/", "/abs/").replace(".pdf", "")
}
const loader = new PageAssistHtmlLoader({
html: "",
url
})
const documents = await loader.loadByURL()
documents.forEach((doc) => {
docs.push(doc)
})
}
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) => {
return {
url: result.metadata.url,
content: result.pageContent
}
})
return searchResult
}

View File

@ -8,6 +8,7 @@ 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"
const getHostName = (url: string) => { const getHostName = (url: string) => {
try { try {
@ -37,30 +38,66 @@ const searchWeb = (provider: string, query: string) => {
} }
} }
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: {
url: any; url: any
content: string; content: string
}[] = [] }[] = []
// let search_results_web = ""
const isVisitSpecificWebsite = await getIsVisitSpecificWebsite() if (webSearch) {
const isVisitSpecificWebsite = await getIsVisitSpecificWebsite()
if (isVisitSpecificWebsite && websiteVisit.hasUrl) { if (isVisitSpecificWebsite && websiteVisit.hasUrl) {
const url = websiteVisit.url
const queryWithoutUrl = websiteVisit.queryWithouUrls
webSearchResults = await processSingleWebsite(url, queryWithoutUrl)
} else {
const searchProvider = await getSearchProvider()
webSearchResults = await searchWeb(searchProvider, query)
}
const url = websiteVisit.url // search_results_web = webSearchResults
const queryWithoutUrl = websiteVisit.queryWithouUrls // .map(
search = await processSingleWebsite(url, queryWithoutUrl) // (result, idx) =>
// `<result source="${result.url}" id="${idx}">${result.content}</result>`
} else { // )
const searchProvider = await getSearchProvider() // .join("\n")
search = await searchWeb(searchProvider, query)
} }
let iodSearchResults: {
doId: string
name: string
url?: string
// pdf_url?: string
description: string
}[] = []
// let search_results_iod = ""
const search_results = search 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 search_results = iodSearchResults.map((res) => ({
url: `${res.doId}: ${res.name}`,
content: res.description
}))
.concat(
webSearchResults
)
.map( .map(
(result, idx) => (result, idx) =>
`<result source="${result.url}" id="${idx}">${result.content}</result>` `<result source="${result.url}" id="${idx}">${result.content}</result>`
@ -77,13 +114,14 @@ export const getSystemPromptForWeb = async (query: string) => {
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: getHostName(result.url),
type: "url" type: "url"
} }
}) }),
iodSources: iodSearchResults,
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)