Compare commits
19 Commits
chq
...
feat/meter
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5fa739a95 | ||
|
|
70d1f40333 | ||
|
|
2866bcc7af | ||
|
|
2a57034c9d | ||
|
|
79a03ab6fc | ||
|
|
50f9e4354f | ||
|
|
8f27ca2e4e | ||
|
|
ce333714b7 | ||
|
|
7b8879a7a8 | ||
|
|
c50bb49b37 | ||
|
|
970ffdac15 | ||
|
|
da162be01d | ||
|
|
6d79d42925 | ||
|
|
f617a05483 | ||
|
|
4c5d5cfe99 | ||
|
|
51188b1428 | ||
|
|
a56e46a98d | ||
|
|
e8471f1802 | ||
|
|
691575e449 |
@@ -10,6 +10,8 @@ Page Assist supports Chromium-based browsers like Chrome, Brave, and Edge, as we
|
||||
|
||||
[](https://chrome.google.com/webstore/detail/page-assist/jfgfiigpkhlkbnfnbobbkinehhfdhndo)
|
||||
[](https://addons.mozilla.org/en-US/firefox/addon/page-assist/)
|
||||
[](https://microsoftedge.microsoft.com/addons/detail/page-assist-a-web-ui-fo/ogkogooadflifpmmidmhjedogicnhooa)
|
||||
|
||||
|
||||
Checkout the Demo (v1.0.0):
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"translate": "ترجمة",
|
||||
"custom": "مخصص"
|
||||
},
|
||||
"citations": "الاقتباسات",
|
||||
"webCitations": "الاقتباسات",
|
||||
"segmented": {
|
||||
"ollama": "نماذج Ollama",
|
||||
"custom": "نماذج مخصصة"
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"translate": "Oversæt",
|
||||
"custom": "Brugerdefineret"
|
||||
},
|
||||
"citations": "Citater",
|
||||
"webCitations": "Citater",
|
||||
"downloadCode": "Download Kode",
|
||||
"date": {
|
||||
"pinned": "Fastgjort",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"translate": "Übersetzen",
|
||||
"custom": "Benutzerdefiniert"
|
||||
},
|
||||
"citations": "Zitate",
|
||||
"webCitations": "Zitate",
|
||||
"downloadCode": "Code herunterladen",
|
||||
"date": {
|
||||
"pinned": "Angepinnt",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
},
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"webSearch": "Searching the web",
|
||||
"iodSearch": "Searching the Internet of Data",
|
||||
"regenerate": "Regenerate",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
@@ -136,7 +137,8 @@
|
||||
"translate": "Translate",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"citations": "Citations",
|
||||
"webCitations": "Web Citations",
|
||||
"iodCitations": "Internet of Data Citations",
|
||||
"segmented": {
|
||||
"ollama": "Ollama Models",
|
||||
"custom": "Custom Models"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"selectAPrompt": "Select a Prompt",
|
||||
"githubRepository": "GitHub Repository",
|
||||
"settings": "Settings",
|
||||
"metering": "Metering",
|
||||
"sidebarTitle": "Chat History",
|
||||
"error": "Error",
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
},
|
||||
"tooltip": {
|
||||
"searchInternet": "Search Internet",
|
||||
"searchIod": "Search Internet of Data",
|
||||
"speechToText": "Speech to Text",
|
||||
"uploadImage": "Upload Image",
|
||||
"stopStreaming": "Stop Streaming",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"rephrase": "Reformular",
|
||||
"translate": "Traducir"
|
||||
},
|
||||
"citations": "Citas",
|
||||
"webCitations": "Citas",
|
||||
"downloadCode": "Descargar Código",
|
||||
"date": {
|
||||
"pinned": "Fijado",
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
},
|
||||
"advanced": "تنظیمات بیشتر مدل"
|
||||
},
|
||||
"citations": "منابع",
|
||||
"webCitations": "منابع",
|
||||
"downloadCode": "دانلود کد",
|
||||
"date": {
|
||||
"pinned": "پین شده",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"rephrase": "Reformuler",
|
||||
"translate": "Traduire"
|
||||
},
|
||||
"citations": "Citations",
|
||||
"webCitations": "Citations",
|
||||
"downloadCode": "Télécharger le code",
|
||||
"date": {
|
||||
"pinned": "Épinglé",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"rephrase": "Riformulare",
|
||||
"translate": "Tradurre"
|
||||
},
|
||||
"citations": "Citazioni",
|
||||
"webCitations": "Citazioni",
|
||||
"downloadCode": "Scarica Codice",
|
||||
"date": {
|
||||
"pinned": "Fissato",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"rephrase": "言い換え",
|
||||
"translate": "翻訳"
|
||||
},
|
||||
"citations": "引用",
|
||||
"webCitations": "引用",
|
||||
"downloadCode": "コードをダウンロード",
|
||||
"date": {
|
||||
"pinned": "固定",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"rephrase": "다르게 표현",
|
||||
"translate": "번역"
|
||||
},
|
||||
"citations": "인용",
|
||||
"webCitations": "인용",
|
||||
"downloadCode": "코드 다운로드",
|
||||
"date": {
|
||||
"pinned": "고정됨",
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
"rephrase": "പുനഃരൂപീകരിക്കുക",
|
||||
"translate": "വിവർത്തനം ചെയ്യുക"
|
||||
},
|
||||
"citations": "ഉദ്ധരണികൾ",
|
||||
"webCitations": "ഉദ്ധരണികൾ",
|
||||
"downloadCode": "കോഡ് ഡൗൺലോഡ് ചെയ്യുക",
|
||||
"date": {
|
||||
"pinned": "പിൻ ചെയ്തത്",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"translate": "Oversett",
|
||||
"custom": "Egendefinert"
|
||||
},
|
||||
"citations": "Sitater",
|
||||
"webCitations": "Sitater",
|
||||
"downloadCode": "Last ned kode",
|
||||
"date": {
|
||||
"pinned": "Festet",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"rephrase": "Reformular",
|
||||
"translate": "Traduzir"
|
||||
},
|
||||
"citations": "Citações",
|
||||
"webCitations": "Citações",
|
||||
"downloadCode": "Baixar Código",
|
||||
"date": {
|
||||
"pinned": "Fixado",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"rephrase": "Перефразировать",
|
||||
"translate": "Перевести"
|
||||
},
|
||||
"citations": "Цитаты",
|
||||
"webCitations": "Цитаты",
|
||||
"downloadCode": "Скачать код",
|
||||
"date": {
|
||||
"pinned": "Закреплено",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"translate": "Översätt",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"citations": "Citat",
|
||||
"webCitations": "Citat",
|
||||
"segmented": {
|
||||
"ollama": "Ollama-modeller",
|
||||
"custom": "Custom modeller"
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"translate": "Перекласти",
|
||||
"custom": "Власне"
|
||||
},
|
||||
"citations": "Цитати",
|
||||
"webCitations": "Цитати",
|
||||
"segmented": {
|
||||
"ollama": "Моделі Ollama",
|
||||
"custom": "Власні моделі"
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
}
|
||||
},
|
||||
"copyToClipboard": "复制到剪贴板",
|
||||
"webSearch": "搜索网络",
|
||||
"webSearch": "搜索万维网",
|
||||
"iodSearch": "搜索数联网",
|
||||
"regenerate": "重新生成",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
@@ -105,7 +106,8 @@
|
||||
"rephrase": "重述",
|
||||
"translate": "翻译"
|
||||
},
|
||||
"citations": "引用",
|
||||
"webCitations": "万维网引用",
|
||||
"iodCitations": "数联网引用",
|
||||
"downloadCode": "下载代码",
|
||||
"date": {
|
||||
"pinned": "已置顶",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"selectAPrompt": "选择一个提示词",
|
||||
"githubRepository": "GitHub 仓库",
|
||||
"settings": "设置",
|
||||
"metering": "计量",
|
||||
"sidebarTitle": "聊天历史",
|
||||
"error": "错误",
|
||||
"somethingWentWrong": "出现了错误",
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"searchInternet": "搜索互联网",
|
||||
"searchInternet": "搜索万维网",
|
||||
"searchIod": "搜索数联网",
|
||||
"speechToText": "语音到文本",
|
||||
"uploadImage": "上传图片",
|
||||
"stopStreaming": "停止流媒体",
|
||||
|
||||
@@ -9,7 +9,12 @@ import {
|
||||
Pen,
|
||||
PlayIcon,
|
||||
RotateCcw,
|
||||
Square
|
||||
Square,
|
||||
Star,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
MessageSquareShare,
|
||||
ArrowUpSquare
|
||||
} from "lucide-react"
|
||||
import { EditMessageForm } from "./EditMessageForm"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@@ -18,7 +23,7 @@ import { useTTS } from "@/hooks/useTTS"
|
||||
import { tagColors } from "@/utils/color"
|
||||
import { removeModelSuffix } from "@/db/models"
|
||||
import { GenerationInfo } from "./GenerationInfo"
|
||||
import { parseReasoning, } from "@/libs/reasoning"
|
||||
import { parseReasoning } from "@/libs/reasoning"
|
||||
import { humanizeMilliseconds } from "@/utils/humanize-milliseconds"
|
||||
type Props = {
|
||||
message: string
|
||||
@@ -36,7 +41,8 @@ type Props = {
|
||||
isProcessing: boolean
|
||||
webSearch?: {}
|
||||
isSearchingInternet?: boolean
|
||||
sources?: any[]
|
||||
webSources?: any[]
|
||||
iodSources?: any[]
|
||||
hideEditAndRegenerate?: boolean
|
||||
onSourceClick?: (source: any) => void
|
||||
isTTSEnabled?: boolean
|
||||
@@ -166,7 +172,7 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.isBot && props?.sources && props?.sources.length > 0 && (
|
||||
{props.isBot && props?.webSources && props?.webSources.length > 0 && (
|
||||
<Collapse
|
||||
className="mt-6"
|
||||
ghost
|
||||
@@ -175,15 +181,44 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
key: "1",
|
||||
label: (
|
||||
<div className="italic text-gray-500 dark:text-gray-400">
|
||||
{t("citations")}
|
||||
{t("webCitations")}
|
||||
</div>
|
||||
),
|
||||
children: (
|
||||
<div className="mb-3 flex flex-wrap gap-2">
|
||||
{props?.sources?.map((source, index) => (
|
||||
{props?.webSources?.map((source, index) => (
|
||||
<MessageSource
|
||||
onSourceClick={props.onSourceClick}
|
||||
key={index}
|
||||
index={index}
|
||||
source={source}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{props.isBot && props?.iodSources && props?.iodSources.length > 0 && (
|
||||
<Collapse
|
||||
className="mt-6"
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
@@ -286,6 +321,51 @@ export const PlaygroundMessage = (props: Props) => {
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="收藏">
|
||||
<button
|
||||
aria-label="收藏"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<Star className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="发布语用">
|
||||
<button
|
||||
aria-label="发布语用"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<ArrowUpSquare className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="发布对话">
|
||||
<button
|
||||
aria-label="发布对话"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<MessageSquareShare className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="点赞">
|
||||
<button
|
||||
aria-label="点赞"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<ThumbsUp className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{ (
|
||||
<Tooltip title="点踩">
|
||||
<button
|
||||
aria-label="点踩"
|
||||
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
<ThumbsDown className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// add invisible div to prevent layout shift
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useState } from "react"
|
||||
import type React from "react"
|
||||
import { KnowledgeIcon } from "@/components/Option/Knowledge/KnowledgeIcon"
|
||||
|
||||
type Props = {
|
||||
index: number
|
||||
source: {
|
||||
name?: string
|
||||
url?: string
|
||||
@@ -8,11 +11,20 @@ type Props = {
|
||||
type?: string
|
||||
pageContent?: string
|
||||
content?: string
|
||||
doId?: string
|
||||
description?: string
|
||||
}
|
||||
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") {
|
||||
return (
|
||||
<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 (
|
||||
<a
|
||||
href={source?.url}
|
||||
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">
|
||||
<span className="text-xs">{source.name}</span>
|
||||
</a>
|
||||
<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
|
||||
href={source?.url}
|
||||
target="_blank"
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ChevronRight,
|
||||
CogIcon,
|
||||
ComputerIcon,
|
||||
GaugeCircle,
|
||||
GithubIcon,
|
||||
PanelLeftIcon,
|
||||
ZapIcon
|
||||
@@ -240,7 +241,14 @@ export const Header: React.FC<Props> = ({
|
||||
<CogIcon className="w-6 h-6" />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Tooltip title={t("metering")}>
|
||||
<NavLink
|
||||
to="/metering"
|
||||
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<GaugeCircle className="w-6 h-6" />
|
||||
</NavLink>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
165
src/components/Option/Metering/detail.tsx
Normal file
165
src/components/Option/Metering/detail.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { MeteringEntry, useStoreMessageOption } from "@/store/option"
|
||||
import { Card, List, Table, Tag, Space, TableProps, Tooltip } from "antd"
|
||||
import { NavLink } from "react-router-dom"
|
||||
import { formatDate } from "@/utils/date"
|
||||
|
||||
const columns: TableProps<MeteringEntry>["columns"] = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 100,
|
||||
render: (_text, _record, index) => index + 1, // 索引从0开始,+1后从1显示
|
||||
},
|
||||
{
|
||||
title: "问题",
|
||||
dataIndex: "queryContent",
|
||||
key: "queryContent"
|
||||
},
|
||||
{
|
||||
title: "提示词全文",
|
||||
dataIndex: "prompt",
|
||||
key: "prompt",
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
render: (prompt) => (
|
||||
<Tooltip placement="topLeft" title={prompt}>
|
||||
{prompt}
|
||||
</Tooltip>
|
||||
),
|
||||
width: "10%"
|
||||
},
|
||||
{
|
||||
title: "思维链",
|
||||
key: "cot",
|
||||
dataIndex: "cot",
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
render: (responseContent) => (
|
||||
<Tooltip placement="topLeft" title={responseContent}>
|
||||
{responseContent}
|
||||
</Tooltip>
|
||||
),
|
||||
width: "10%"
|
||||
},
|
||||
|
||||
{
|
||||
title: "回答",
|
||||
dataIndex: "responseContent",
|
||||
key: "responseContent",
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
render: (responseContent) => (
|
||||
<Tooltip placement="topLeft" title={responseContent}>
|
||||
{responseContent}
|
||||
</Tooltip>
|
||||
),
|
||||
width: "10%"
|
||||
},
|
||||
{
|
||||
title: "关联数据个数",
|
||||
dataIndex: "relatedDataCount",
|
||||
key: "relatedDataCount"
|
||||
},
|
||||
{
|
||||
title: "数联网token",
|
||||
dataIndex: "iodTokenCount",
|
||||
key: "iodTokenCount"
|
||||
},
|
||||
{
|
||||
title: "大模型token",
|
||||
key: "largeModelToken",
|
||||
dataIndex: "largeModelToken",
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<div>{record.modelInputTokenCount + record.modelOutputTokenCount}</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "日期",
|
||||
dataIndex: "date",
|
||||
key: "date",
|
||||
render: (date) => {
|
||||
return <div>{formatDate(date ?? new Date())}</div>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "耗时",
|
||||
key: "timeTaken",
|
||||
dataIndex: "timeTaken"
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
render: (_, record) => (
|
||||
<Space size="middle">
|
||||
{/* <a>Invite {record.name}</a> */}
|
||||
|
||||
<NavLink to={`/metering/list/${record.id}`}>
|
||||
<a>详情</a>
|
||||
</NavLink>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
export const MeteringDetail = () => {
|
||||
const { meteringEntries } = useStoreMessageOption()
|
||||
|
||||
const data = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "对话数量",
|
||||
value: meteringEntries.length
|
||||
},
|
||||
{
|
||||
key: "数联网输入token数",
|
||||
value: meteringEntries.reduce((acc, cur) => {
|
||||
for (const item of cur.iodKeywords) {
|
||||
acc += item.length
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
},
|
||||
{
|
||||
key: "数联网输出token数",
|
||||
value: meteringEntries.reduce((acc, cur) => acc + cur.iodTokenCount, 0)
|
||||
},
|
||||
{
|
||||
key: "大模型输入token数",
|
||||
value: meteringEntries.reduce(
|
||||
(acc, cur) => acc + cur.modelInputTokenCount,
|
||||
0
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "大模型输出token数",
|
||||
value: meteringEntries.reduce(
|
||||
(acc, cur) => acc + cur.modelOutputTokenCount,
|
||||
0
|
||||
)
|
||||
}
|
||||
],
|
||||
[meteringEntries]
|
||||
)
|
||||
return (
|
||||
<div className="p-4 pt-[4rem]">
|
||||
<List
|
||||
grid={{ gutter: 16, column: 5 }}
|
||||
dataSource={data}
|
||||
split={false}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<Card title={item.key}>{item.value}</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Table<MeteringEntry> columns={columns} dataSource={meteringEntries} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
203
src/components/Option/Metering/listDetail.tsx
Normal file
203
src/components/Option/Metering/listDetail.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import {
|
||||
Card,
|
||||
List,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
TableProps,
|
||||
Divider,
|
||||
Typography,
|
||||
Tooltip
|
||||
} from "antd"
|
||||
import { NavLink, useParams } from "react-router-dom"
|
||||
import { useStoreMessageOption } from "@/store/option.tsx"
|
||||
import { useMemo } from "react"
|
||||
|
||||
interface DataType {
|
||||
key: string
|
||||
name: string
|
||||
doId: number
|
||||
data_space: string
|
||||
content: string
|
||||
tokenCount: number
|
||||
}
|
||||
|
||||
const columns: TableProps<DataType>["columns"] = [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'index',
|
||||
width: 100,
|
||||
render: (_text, _record, index) => index + 1, // 索引从0开始,+1后从1显示
|
||||
},
|
||||
{
|
||||
title: "标识",
|
||||
dataIndex: "doId",
|
||||
key: "doId"
|
||||
},
|
||||
{
|
||||
title: "提供方",
|
||||
dataIndex: "data_space",
|
||||
key: "data_space"
|
||||
},
|
||||
{
|
||||
title: "token数量",
|
||||
key: "tokenCount",
|
||||
dataIndex: "tokenCount",
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: "内容",
|
||||
key: "content",
|
||||
dataIndex: "content",
|
||||
ellipsis: {
|
||||
showTitle: false
|
||||
},
|
||||
render: (content) => (
|
||||
<Tooltip placement="topLeft" title={content}>
|
||||
{content}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
export const ListDetail = () => {
|
||||
const { meteringEntries } = useStoreMessageOption()
|
||||
const { id } = useParams()
|
||||
const record = useMemo(
|
||||
() => meteringEntries.find((item) => item.id === id),
|
||||
[meteringEntries]
|
||||
)
|
||||
|
||||
const modelData = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "大模型输入token数",
|
||||
value: record.modelInputTokenCount
|
||||
},
|
||||
{
|
||||
key: "大模型输出token数",
|
||||
value: record.modelOutputTokenCount
|
||||
},
|
||||
{
|
||||
key: "模型",
|
||||
value: record.model
|
||||
}
|
||||
],
|
||||
[record]
|
||||
)
|
||||
|
||||
const inputTokenData = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "内容:",
|
||||
value: record.queryContent
|
||||
},
|
||||
{
|
||||
key: "token数量:",
|
||||
value: record.queryContent.length
|
||||
}
|
||||
],
|
||||
[record]
|
||||
)
|
||||
const keywordsData = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "token数量:",
|
||||
value: record.iodKeywords.reduce((acc, cur) => acc + cur.length, 0)
|
||||
},
|
||||
{
|
||||
key: "内容:",
|
||||
value: record.iodKeywords.join(", ")
|
||||
}
|
||||
],
|
||||
[record]
|
||||
)
|
||||
const responseContent = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: "token数量:",
|
||||
value: record.modelResponseContent.length
|
||||
},
|
||||
{
|
||||
key: "内容:",
|
||||
value: record.modelResponseContent
|
||||
}
|
||||
],
|
||||
[record]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="p-[1rem] pt-[4rem]">
|
||||
<List
|
||||
grid={{ gutter: 16, column: 3 }}
|
||||
dataSource={modelData}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<Card title={item.key}>{item.value}</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
style={{ marginBottom: "2rem" }}
|
||||
/>
|
||||
|
||||
<Space direction="vertical" size={10}>
|
||||
<Divider orientation="left">输入token详情</Divider>
|
||||
<List
|
||||
bordered
|
||||
header={<div>问题</div>}
|
||||
dataSource={inputTokenData}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ justifyContent: "flex-start" }}>
|
||||
<Typography.Paragraph style={{ marginBottom: 0 }} className="mr-1">
|
||||
{item.key}
|
||||
</Typography.Paragraph>
|
||||
<Tooltip placement="topLeft" style={{ marginBottom: 0 }} title={item.value}>
|
||||
{item.value}
|
||||
</Tooltip>
|
||||
</List.Item>
|
||||
)}
|
||||
style={{ marginBottom: "1rem" }}
|
||||
/>
|
||||
<Card title="数联网引用数据">
|
||||
<Table<DataType> columns={columns} dataSource={record.iodData} />
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
<Space direction="vertical" size={10}>
|
||||
<Divider orientation="left">输出token详情</Divider>
|
||||
<List
|
||||
bordered
|
||||
dataSource={keywordsData}
|
||||
header={<div>数联网搜索关键词</div>}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ justifyContent: "flex-start" }}>
|
||||
<Typography.Text className="mr-1" style={{ marginBottom: 0 }}>{item.key}</Typography.Text>
|
||||
<Tooltip style={{ marginBottom: 0 }} placement="topLeft" title={item.value}>
|
||||
{item.value}
|
||||
</Tooltip>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
<List
|
||||
bordered
|
||||
dataSource={responseContent}
|
||||
header={<div>回答</div>}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
style={{ justifyContent: "flex-start", alignItems: "center" }}>
|
||||
<Typography.Text
|
||||
className="mt-0 mr-1 w-20"
|
||||
style={{ marginBottom: 0 }}>
|
||||
{item.key}
|
||||
</Typography.Text>
|
||||
<Typography.Paragraph
|
||||
style={{ marginBottom: 0 }}
|
||||
ellipsis={{ tooltip: item.value, rows: 2, expandable: true }}>
|
||||
{item.value}
|
||||
</Typography.Paragraph>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -36,7 +36,8 @@ export const PlaygroundChat = () => {
|
||||
onRengerate={regenerateLastMessage}
|
||||
isProcessing={streaming}
|
||||
isSearchingInternet={isSearchingInternet}
|
||||
sources={message.sources}
|
||||
webSources={message.webSources}
|
||||
iodSources={message.iodSources}
|
||||
onEditFormSubmit={(value, isSend) => {
|
||||
editMessage(index, value, !message.isBot, isSend)
|
||||
}}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getVariable } from "@/utils/select-variable"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { KnowledgeSelect } from "../Knowledge/KnowledgeSelect"
|
||||
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"
|
||||
import { PiGlobe } from "react-icons/pi"
|
||||
import { PiGlobe, PiNetwork } from "react-icons/pi"
|
||||
import { handleChatInputKeyDown } from "@/utils/key-down"
|
||||
import { getIsSimpleInternetSearch } from "@/services/search"
|
||||
|
||||
@@ -34,6 +34,8 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
||||
streaming: isSending,
|
||||
webSearch,
|
||||
setWebSearch,
|
||||
iodSearch,
|
||||
setIodSearch,
|
||||
selectedQuickPrompt,
|
||||
textareaRef,
|
||||
setSelectedQuickPrompt,
|
||||
@@ -301,6 +303,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
||||
<div className="mt-2 flex justify-between items-center">
|
||||
<div className="flex">
|
||||
{!selectedKnowledge && (
|
||||
<div>
|
||||
<Tooltip title={t("tooltip.searchInternet")}>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<PiGlobe
|
||||
@@ -314,6 +317,20 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("tooltip.searchIod")} className="ml-3">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<PiNetwork
|
||||
className={`h-5 w-5 dark:text-gray-300 `}
|
||||
/>
|
||||
<Switch
|
||||
value={iodSearch}
|
||||
onChange={(e) => setIodSearch(e)}
|
||||
checkedChildren={t("form.webSearch.on")}
|
||||
unCheckedChildren={t("form.webSearch.off")}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex !justify-end gap-3">
|
||||
|
||||
@@ -39,7 +39,8 @@ export const SidePanelBody = () => {
|
||||
message_type={message.messageType}
|
||||
isProcessing={streaming}
|
||||
isSearchingInternet={isSearchingInternet}
|
||||
sources={message.sources}
|
||||
webSources={message.webSources}
|
||||
iodSources={message.iodSources}
|
||||
onEditFormSubmit={(value) => {
|
||||
editMessage(index, value, !message.isBot)
|
||||
}}
|
||||
|
||||
@@ -29,7 +29,8 @@ type Message = {
|
||||
role: string
|
||||
content: string
|
||||
images?: string[]
|
||||
sources?: string[]
|
||||
webSources?: string[]
|
||||
iodSources?: string[]
|
||||
search?: WebSearch
|
||||
createdAt: number
|
||||
reasoning_time_taken?: number
|
||||
@@ -254,7 +255,8 @@ export const saveMessage = async (
|
||||
role: string,
|
||||
content: string,
|
||||
images: string[],
|
||||
source?: any[],
|
||||
webSources?: any[],
|
||||
iodSources?: any[],
|
||||
time?: number,
|
||||
message_type?: string,
|
||||
generationInfo?: any,
|
||||
@@ -273,7 +275,8 @@ export const saveMessage = async (
|
||||
content,
|
||||
images,
|
||||
createdAt,
|
||||
sources: source,
|
||||
webSources,
|
||||
iodSources,
|
||||
messageType: message_type,
|
||||
generationInfo: generationInfo,
|
||||
reasoning_time_taken
|
||||
@@ -303,7 +306,8 @@ export const formatToMessage = (messages: MessageHistory): MessageType[] => {
|
||||
isBot: message.role === "assistant",
|
||||
message: message.content,
|
||||
name: message.name,
|
||||
sources: message?.sources || [],
|
||||
webSources: message?.webSources || [],
|
||||
iodSources: message?.iodSources || [],
|
||||
images: message.images || [],
|
||||
generationInfo: message?.generationInfo,
|
||||
reasoning_time_taken: message?.reasoning_time_taken
|
||||
|
||||
@@ -62,6 +62,7 @@ export const saveMessageOnError = async ({
|
||||
userMessage,
|
||||
[image],
|
||||
[],
|
||||
[],
|
||||
1,
|
||||
message_type
|
||||
)
|
||||
@@ -73,6 +74,7 @@ export const saveMessageOnError = async ({
|
||||
botMessage,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
2,
|
||||
message_type
|
||||
)
|
||||
@@ -91,6 +93,7 @@ export const saveMessageOnError = async ({
|
||||
userMessage,
|
||||
[image],
|
||||
[],
|
||||
[],
|
||||
1,
|
||||
message_type
|
||||
)
|
||||
@@ -102,6 +105,7 @@ export const saveMessageOnError = async ({
|
||||
botMessage,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
2,
|
||||
message_type
|
||||
)
|
||||
@@ -126,7 +130,8 @@ export const saveMessageOnSuccess = async ({
|
||||
message,
|
||||
image,
|
||||
fullText,
|
||||
source,
|
||||
webSources,
|
||||
iodSources,
|
||||
message_source = "web-ui",
|
||||
message_type, generationInfo,
|
||||
prompt_id,
|
||||
@@ -140,7 +145,8 @@ export const saveMessageOnSuccess = async ({
|
||||
message: string
|
||||
image: string
|
||||
fullText: string
|
||||
source: any[]
|
||||
webSources: any[]
|
||||
iodSources: any[]
|
||||
message_source?: "copilot" | "web-ui",
|
||||
message_type?: string
|
||||
generationInfo?: any
|
||||
@@ -157,6 +163,7 @@ export const saveMessageOnSuccess = async ({
|
||||
message,
|
||||
[image],
|
||||
[],
|
||||
[],
|
||||
1,
|
||||
message_type,
|
||||
generationInfo,
|
||||
@@ -169,7 +176,8 @@ export const saveMessageOnSuccess = async ({
|
||||
"assistant",
|
||||
fullText,
|
||||
[],
|
||||
source,
|
||||
webSources,
|
||||
iodSources,
|
||||
2,
|
||||
message_type,
|
||||
generationInfo,
|
||||
@@ -189,6 +197,7 @@ export const saveMessageOnSuccess = async ({
|
||||
message,
|
||||
[image],
|
||||
[],
|
||||
[],
|
||||
1,
|
||||
message_type,
|
||||
generationInfo,
|
||||
@@ -200,7 +209,8 @@ export const saveMessageOnSuccess = async ({
|
||||
"assistant",
|
||||
fullText,
|
||||
[],
|
||||
source,
|
||||
webSources,
|
||||
iodSources,
|
||||
2,
|
||||
message_type,
|
||||
generationInfo,
|
||||
|
||||
@@ -59,6 +59,8 @@ export const useMessage = () => {
|
||||
setIsSearchingInternet,
|
||||
webSearch,
|
||||
setWebSearch,
|
||||
iodSearch,
|
||||
setIodSearch,
|
||||
isSearchingInternet
|
||||
} = useStoreMessageOption()
|
||||
const [defaultInternetSearchOn] = useStorage("defaultInternetSearchOn", false)
|
||||
@@ -185,14 +187,16 @@ export const useMessage = () => {
|
||||
isBot: false,
|
||||
name: "You",
|
||||
message,
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
images: []
|
||||
},
|
||||
{
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -203,7 +207,8 @@ export const useMessage = () => {
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -334,7 +339,16 @@ export const useMessage = () => {
|
||||
}
|
||||
|
||||
let context: string = ""
|
||||
let source: {
|
||||
let webSources: {
|
||||
name: any
|
||||
type: any
|
||||
mode: string
|
||||
url: string
|
||||
pageContent: string
|
||||
metadata: Record<string, any>
|
||||
}[] = []
|
||||
// TODO: update type
|
||||
let iodSources: {
|
||||
name: any
|
||||
type: any
|
||||
mode: string
|
||||
@@ -346,7 +360,7 @@ export const useMessage = () => {
|
||||
if (chatWithWebsiteEmbedding) {
|
||||
const docs = await vectorstore.similaritySearch(query, 4)
|
||||
context = formatDocs(docs)
|
||||
source = docs.map((doc) => {
|
||||
webSources = docs.map((doc) => {
|
||||
return {
|
||||
...doc,
|
||||
name: doc?.metadata?.source || "untitled",
|
||||
@@ -365,7 +379,7 @@ export const useMessage = () => {
|
||||
.slice(0, maxWebsiteContext)
|
||||
}
|
||||
|
||||
source = [
|
||||
webSources = [
|
||||
{
|
||||
name: embedURL,
|
||||
type: type,
|
||||
@@ -476,7 +490,8 @@ export const useMessage = () => {
|
||||
return {
|
||||
...message,
|
||||
message: fullText,
|
||||
sources: source,
|
||||
webSources,
|
||||
iodSources,
|
||||
generationInfo,
|
||||
reasoning_time_taken: timetaken
|
||||
}
|
||||
@@ -506,7 +521,8 @@ export const useMessage = () => {
|
||||
message,
|
||||
image,
|
||||
fullText,
|
||||
source,
|
||||
webSources,
|
||||
iodSources,
|
||||
message_source: "copilot",
|
||||
generationInfo,
|
||||
reasoning_time_taken: timetaken
|
||||
@@ -606,14 +622,16 @@ export const useMessage = () => {
|
||||
isBot: false,
|
||||
name: "You",
|
||||
message,
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
images: []
|
||||
},
|
||||
{
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -624,7 +642,8 @@ export const useMessage = () => {
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -787,7 +806,8 @@ export const useMessage = () => {
|
||||
message,
|
||||
image,
|
||||
fullText,
|
||||
source: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
message_source: "copilot",
|
||||
generationInfo,
|
||||
reasoning_time_taken: timetaken
|
||||
@@ -891,14 +911,16 @@ export const useMessage = () => {
|
||||
isBot: false,
|
||||
name: "You",
|
||||
message,
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
images: [image]
|
||||
},
|
||||
{
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -909,7 +931,8 @@ export const useMessage = () => {
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -1077,7 +1100,8 @@ export const useMessage = () => {
|
||||
message,
|
||||
image,
|
||||
fullText,
|
||||
source: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
message_source: "copilot",
|
||||
generationInfo,
|
||||
reasoning_time_taken: timetaken
|
||||
@@ -1114,12 +1138,14 @@ export const useMessage = () => {
|
||||
}
|
||||
|
||||
const searchChatMode = async (
|
||||
webSearch: boolean,
|
||||
iodSearch,
|
||||
message: string,
|
||||
image: string,
|
||||
isRegenerate: boolean,
|
||||
messages: Message[],
|
||||
history: ChatHistory,
|
||||
signal: AbortSignal
|
||||
signal: AbortSignal,
|
||||
) => {
|
||||
const url = await getOllamaURL()
|
||||
setStreaming(true)
|
||||
@@ -1176,14 +1202,16 @@ export const useMessage = () => {
|
||||
isBot: false,
|
||||
name: "You",
|
||||
message,
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
images: [image]
|
||||
},
|
||||
{
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -1194,7 +1222,8 @@ export const useMessage = () => {
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -1271,7 +1300,8 @@ export const useMessage = () => {
|
||||
query = removeReasoning(query)
|
||||
}
|
||||
|
||||
const { prompt, source } = await getSystemPromptForWeb(query)
|
||||
const { prompt, webSources, iodSources } =
|
||||
await getSystemPromptForWeb(query, [], webSearch, iodSearch)
|
||||
setIsSearchingInternet(false)
|
||||
|
||||
// message = message.trim().replaceAll("\n", " ")
|
||||
@@ -1394,7 +1424,8 @@ export const useMessage = () => {
|
||||
return {
|
||||
...message,
|
||||
message: fullText,
|
||||
sources: source,
|
||||
webSources,
|
||||
iodSources,
|
||||
generationInfo,
|
||||
reasoning_time_taken: timetaken
|
||||
}
|
||||
@@ -1424,7 +1455,8 @@ export const useMessage = () => {
|
||||
message,
|
||||
image,
|
||||
fullText,
|
||||
source,
|
||||
webSources,
|
||||
iodSources,
|
||||
generationInfo,
|
||||
reasoning_time_taken: timetaken
|
||||
})
|
||||
@@ -1523,7 +1555,8 @@ export const useMessage = () => {
|
||||
isBot: false,
|
||||
name: "You",
|
||||
message,
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
images: [image],
|
||||
messageType: messageType
|
||||
},
|
||||
@@ -1531,7 +1564,8 @@ export const useMessage = () => {
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -1542,7 +1576,8 @@ export const useMessage = () => {
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -1688,7 +1723,8 @@ export const useMessage = () => {
|
||||
message,
|
||||
image,
|
||||
fullText,
|
||||
source: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
message_source: "copilot",
|
||||
message_type: messageType,
|
||||
generationInfo,
|
||||
@@ -1766,14 +1802,16 @@ export const useMessage = () => {
|
||||
)
|
||||
} else {
|
||||
if (chatMode === "normal") {
|
||||
if (webSearch) {
|
||||
if (webSearch || iodSearch) {
|
||||
await searchChatMode(
|
||||
webSearch,
|
||||
iodSearch,
|
||||
message,
|
||||
image,
|
||||
isRegenerate || false,
|
||||
messages,
|
||||
memory || history,
|
||||
signal
|
||||
signal,
|
||||
)
|
||||
} else {
|
||||
await normalChatMode(
|
||||
@@ -1906,6 +1944,8 @@ export const useMessage = () => {
|
||||
regenerateLastMessage,
|
||||
webSearch,
|
||||
setWebSearch,
|
||||
iodSearch,
|
||||
setIodSearch,
|
||||
isSearchingInternet,
|
||||
selectedQuickPrompt,
|
||||
setSelectedQuickPrompt,
|
||||
|
||||
@@ -3,11 +3,12 @@ import { cleanUrl } from "~/libs/clean-url"
|
||||
import {
|
||||
defaultEmbeddingModelForRag,
|
||||
geWebSearchFollowUpPrompt,
|
||||
geWebSearchKeywordsPrompt,
|
||||
getOllamaURL,
|
||||
promptForRag,
|
||||
systemPromptForNonRagOption
|
||||
} from "~/services/ollama"
|
||||
import { type ChatHistory, type Message } from "~/store/option"
|
||||
import type { ChatHistory, Message, MeteringEntry } from "~/store/option"
|
||||
import { SystemMessage } from "@langchain/core/messages"
|
||||
import { useStoreMessageOption } from "~/store/option"
|
||||
import {
|
||||
@@ -54,6 +55,8 @@ export const useMessageOption = () => {
|
||||
const {
|
||||
history,
|
||||
setHistory,
|
||||
meteringEntries,
|
||||
setMeteringEntries,
|
||||
setStreaming,
|
||||
streaming,
|
||||
setIsFirstMessage,
|
||||
@@ -67,6 +70,8 @@ export const useMessageOption = () => {
|
||||
setChatMode,
|
||||
webSearch,
|
||||
setWebSearch,
|
||||
iodSearch,
|
||||
setIodSearch,
|
||||
isSearchingInternet,
|
||||
setIsSearchingInternet,
|
||||
selectedQuickPrompt,
|
||||
@@ -110,7 +115,30 @@ export const useMessageOption = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 从最后的结果中解析出 思维链 (Chain-of-Thought) 和 结果
|
||||
const responseResolver = (msg: string) => {
|
||||
const cotStart = msg.indexOf("<think>")
|
||||
const cotEnd = msg.indexOf("</think>")
|
||||
let cot = ""
|
||||
let content = ""
|
||||
if (cotStart > -1 && cotEnd > -1) {
|
||||
cot = msg.substring(cotStart + 7, cotEnd)
|
||||
content = msg.substring(cotEnd + 8)
|
||||
} else {
|
||||
content = msg
|
||||
}
|
||||
// 去掉换行符
|
||||
cot = cot.replace(/\n/g, "")
|
||||
content = content.replace(/\n/g, "")
|
||||
return {
|
||||
cot: cot,
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
const searchChatMode = async (
|
||||
webSearch: boolean,
|
||||
iodSearch: boolean,
|
||||
message: string,
|
||||
image: string,
|
||||
isRegenerate: boolean,
|
||||
@@ -161,9 +189,13 @@ export const useMessageOption = () => {
|
||||
useMlock:
|
||||
currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock
|
||||
})
|
||||
|
||||
let newMessage: Message[] = []
|
||||
let generateMessageId = generateID()
|
||||
const meter: MeteringEntry = {
|
||||
id: generateMessageId,
|
||||
queryContent: message,
|
||||
date: new Date()
|
||||
} as MeteringEntry
|
||||
|
||||
if (!isRegenerate) {
|
||||
newMessage = [
|
||||
@@ -172,14 +204,16 @@ export const useMessageOption = () => {
|
||||
isBot: false,
|
||||
name: "You",
|
||||
message,
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
images: [image]
|
||||
},
|
||||
{
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -190,7 +224,8 @@ export const useMessageOption = () => {
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -204,6 +239,7 @@ export const useMessageOption = () => {
|
||||
setIsSearchingInternet(true)
|
||||
|
||||
let query = message
|
||||
let keywords: string[] = []
|
||||
|
||||
if (newMessage.length > 2) {
|
||||
let questionPrompt = await geWebSearchFollowUpPrompt()
|
||||
@@ -268,8 +304,28 @@ export const useMessageOption = () => {
|
||||
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, iodSearchResults: iodData, iodTokenCount } =
|
||||
await getSystemPromptForWeb(query, keywords, webSearch, iodSearch)
|
||||
console.log("prompt:\n" + prompt)
|
||||
setIsSearchingInternet(false)
|
||||
meter.prompt = prompt
|
||||
meter.iodKeywords = keywords
|
||||
meter.iodData = iodData
|
||||
meter.iodTokenCount = iodTokenCount
|
||||
|
||||
// message = message.trim().replaceAll("\n", " ")
|
||||
|
||||
@@ -330,6 +386,7 @@ export const useMessageOption = () => {
|
||||
}
|
||||
)
|
||||
let count = 0
|
||||
const chatStartTime = new Date()
|
||||
let reasoningStartTime: Date | undefined = undefined
|
||||
let reasoningEndTime: Date | undefined = undefined
|
||||
let apiReasoning = false
|
||||
@@ -390,7 +447,8 @@ export const useMessageOption = () => {
|
||||
return {
|
||||
...message,
|
||||
message: fullText,
|
||||
sources: source,
|
||||
webSources,
|
||||
iodSources,
|
||||
generationInfo,
|
||||
reasoning_time_taken: timetaken
|
||||
}
|
||||
@@ -420,13 +478,31 @@ export const useMessageOption = () => {
|
||||
message,
|
||||
image,
|
||||
fullText,
|
||||
source,
|
||||
webSources,
|
||||
iodSources,
|
||||
generationInfo,
|
||||
reasoning_time_taken: timetaken
|
||||
})
|
||||
|
||||
setIsProcessing(false)
|
||||
setStreaming(false)
|
||||
|
||||
// Save metering entry
|
||||
const { cot, content } = responseResolver(fullText)
|
||||
setMeteringEntries([ {
|
||||
...meter,
|
||||
modelInputTokenCount: prompt.length,
|
||||
modelOutputTokenCount: fullText.length,
|
||||
model: ollama.modelName,
|
||||
relatedDataCount: iodData?.length ?? 0,
|
||||
timeTaken: new Date().getTime() - meter.date.getTime(),
|
||||
date: chatStartTime,
|
||||
cot,
|
||||
responseContent: content,
|
||||
modelResponseContent: fullText,
|
||||
},
|
||||
...meteringEntries,
|
||||
])
|
||||
} catch (e) {
|
||||
const errorSave = await saveMessageOnError({
|
||||
e,
|
||||
@@ -552,14 +628,16 @@ export const useMessageOption = () => {
|
||||
isBot: false,
|
||||
name: "You",
|
||||
message,
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
images: [image]
|
||||
},
|
||||
{
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -570,7 +648,8 @@ export const useMessageOption = () => {
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -855,14 +934,16 @@ export const useMessageOption = () => {
|
||||
isBot: false,
|
||||
name: "You",
|
||||
message,
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
images: []
|
||||
},
|
||||
{
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -873,7 +954,8 @@ export const useMessageOption = () => {
|
||||
isBot: true,
|
||||
name: selectedModel,
|
||||
message: "▋",
|
||||
sources: [],
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
id: generateMessageId
|
||||
}
|
||||
]
|
||||
@@ -1076,7 +1158,7 @@ export const useMessageOption = () => {
|
||||
return {
|
||||
...message,
|
||||
message: fullText,
|
||||
sources: source,
|
||||
webSources: source,
|
||||
generationInfo,
|
||||
reasoning_time_taken: timetaken
|
||||
}
|
||||
@@ -1175,8 +1257,10 @@ export const useMessageOption = () => {
|
||||
signal
|
||||
)
|
||||
} else {
|
||||
if (webSearch) {
|
||||
if (webSearch || iodSearch) {
|
||||
await searchChatMode(
|
||||
webSearch,
|
||||
iodSearch,
|
||||
message,
|
||||
image,
|
||||
isRegenerate,
|
||||
@@ -1311,6 +1395,8 @@ export const useMessageOption = () => {
|
||||
regenerateLastMessage,
|
||||
webSearch,
|
||||
setWebSearch,
|
||||
iodSearch,
|
||||
setIodSearch,
|
||||
isSearchingInternet,
|
||||
setIsSearchingInternet,
|
||||
selectedQuickPrompt,
|
||||
|
||||
@@ -72,7 +72,7 @@ export const pageAssistModel = async ({
|
||||
configuration: {
|
||||
apiKey: providerInfo.apiKey || "temp",
|
||||
baseURL: providerInfo.baseUrl || ""
|
||||
}
|
||||
},
|
||||
}) as any
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export const pageAssistModel = async ({
|
||||
configuration: {
|
||||
apiKey: providerInfo.apiKey || "temp",
|
||||
baseURL: providerInfo.baseUrl || ""
|
||||
}
|
||||
},
|
||||
}) as any
|
||||
}
|
||||
return new ChatOllama({
|
||||
|
||||
@@ -12,6 +12,8 @@ import SidepanelSettings from "./sidepanel-settings"
|
||||
import OptionRagSettings from "./option-rag"
|
||||
import OptionChrome from "./option-settings-chrome"
|
||||
import OptionOpenAI from "./option-settings-openai"
|
||||
import OptionMetering from "./option-metering"
|
||||
import MeteringListDetail from "./metering-list-detail"
|
||||
|
||||
export const OptionRoutingChrome = () => {
|
||||
return (
|
||||
@@ -27,6 +29,8 @@ export const OptionRoutingChrome = () => {
|
||||
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
|
||||
<Route path="/settings/rag" element={<OptionRagSettings />} />
|
||||
<Route path="/settings/about" element={<OptionAbout />} />
|
||||
<Route path="/metering" element={<OptionMetering />} />
|
||||
<Route path="/metering/list/:id" element={<MeteringListDetail />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ const OptionModal = lazy(() => import("./option-settings-model"))
|
||||
const OptionPrompt = lazy(() => import("./option-settings-prompt"))
|
||||
const OptionOllamaSettings = lazy(() => import("./options-settings-ollama"))
|
||||
const OptionSettings = lazy(() => import("./option-settings"))
|
||||
const OptionMetering = lazy(() => import("./option-metering"))
|
||||
const MeteringListDetail = lazy(() => import("./metering-list-detail"))
|
||||
const OptionShare = lazy(() => import("./option-settings-share"))
|
||||
const OptionKnowledgeBase = lazy(() => import("./option-settings-knowledge"))
|
||||
const OptionAbout = lazy(() => import("./option-settings-about"))
|
||||
@@ -29,6 +31,8 @@ export const OptionRoutingFirefox = () => {
|
||||
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
|
||||
<Route path="/settings/about" element={<OptionAbout />} />
|
||||
<Route path="/settings/rag" element={<OptionRagSettings />} />
|
||||
<Route path="/metering" element={<OptionMetering />} />
|
||||
<Route path="/metering/list/:id" element={<MeteringListDetail />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
12
src/routes/metering-list-detail.tsx
Normal file
12
src/routes/metering-list-detail.tsx
Normal 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
|
||||
12
src/routes/option-metering.tsx
Normal file
12
src/routes/option-metering.tsx
Normal 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
|
||||
@@ -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 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}.
|
||||
|
||||
\`search-results\` block provides knowledge from the web search results. You can use this information to generate a meaningful response.
|
||||
\`iod-search-results\` block provides knowledge from the Internet of Data (数联网) search results. Each search result has a format of:
|
||||
\`<result doId="{doId}" name="{name}" url="{url}" id="{id}">{content}</result>\`
|
||||
Please show the \`doId\` and \`name\` of the search result when you cite the Internet of Data search result, in the following format, in English:
|
||||
\`[IoD source [id] doId: {doId} "{name}"]({url})\`
|
||||
Or in Chinese:
|
||||
\`[数联网引用[id] doId: {doId} "{name}"]({url})\`
|
||||
For example, in English:
|
||||
\`[IoD source [1] doId: 10.48550/arXiv.1803.05591v2 "On the insufficiency of existing momentum schemes for Stochastic Optimization"](http://arxiv.org/pdf/1803.05591v2.pdf)\`
|
||||
Or in Chinese:
|
||||
\`[数联网引用[1] doId: 10.48550/arXiv.1803.05591v2 "On the insufficiency of existing momentum schemes for Stochastic Optimization"](http://arxiv.org/pdf/1803.05591v2.pdf)\`
|
||||
|
||||
<search-results>
|
||||
{search_results}
|
||||
</search-results>
|
||||
\`web-search-results\` block provides knowledge from the World Wide Web (万维网) search results.
|
||||
Please show the \`doId\` and \`name\` of the search result when you cite the search result, in the following format, in English:
|
||||
\`[3W source [id] "{name}"]({url})\`
|
||||
Or in Chinese:
|
||||
\`[万维网引用[id] "{name}"]({url})\`
|
||||
For example, in English:
|
||||
\`[3W source [1] On the insufficiency of existing momentum schemes for Stochastic Optimization](http://arxiv.org/pdf/1803.05591v2.pdf)\`
|
||||
Or in Chinese:
|
||||
\`[万维网引用[1] On the insufficiency of existing momentum schemes for Stochastic Optimization](http://arxiv.org/pdf/1803.05591v2.pdf)\`
|
||||
|
||||
You can use these information to generate a meaningful response.
|
||||
|
||||
<iod-search-results>
|
||||
{iod_search_results}
|
||||
</iod-search-results>
|
||||
<web-search-results>
|
||||
{web_search_results}
|
||||
</web-search-results>
|
||||
`
|
||||
|
||||
const DEFAULT_WEBSEARCH_FOLLOWUP_PROMPT = `You will give a follow-up question. You need to rephrase the follow-up question if needed so it is a standalone question that can be used by the AI model to search the internet.
|
||||
@@ -58,6 +82,30 @@ Follow-up question: {question}
|
||||
Rephrased question:
|
||||
`
|
||||
|
||||
const DEFAULT_WEBSEARCH_KEYWORDS_PROMPT = `Extract the most important keywords from the query (at most 3), and give me English and Chinese versions of the keywords.
|
||||
|
||||
The result format should be: keyword_1, keyword_2, ..., keyword_n
|
||||
|
||||
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 () => {
|
||||
const ollamaURL = await storage.get("ollamaURL")
|
||||
if (!ollamaURL || ollamaURL.length === 0) {
|
||||
@@ -385,7 +433,7 @@ export const saveForRag = async (
|
||||
export const getWebSearchPrompt = async () => {
|
||||
const prompt = await storage.get("webSearchPrompt")
|
||||
if (!prompt || prompt.length === 0) {
|
||||
return DEFAULT_WEBSEARCH_PROMP
|
||||
return DEFAULT_WEBSEARCH_PROMPT
|
||||
}
|
||||
return prompt
|
||||
}
|
||||
@@ -411,6 +459,18 @@ export const setWebPrompts = async (prompt: string, followUpPrompt: string) => {
|
||||
await setWebSearchFollowUpPrompt(followUpPrompt)
|
||||
}
|
||||
|
||||
export const geWebSearchKeywordsPrompt = async () => {
|
||||
const prompt = await storage.get("webSearchKeywordsPrompt")
|
||||
if (!prompt || prompt.length === 0) {
|
||||
return DEFAULT_WEBSEARCH_KEYWORDS_PROMPT
|
||||
}
|
||||
return prompt
|
||||
}
|
||||
|
||||
export const setWebSearchKeywordsPrompt = async (prompt: string) => {
|
||||
await storage.set("webSearchKeywordsPrompt", prompt)
|
||||
}
|
||||
|
||||
export const getPageShareUrl = async () => {
|
||||
const pageShareUrl = await storage.get("pageShareUrl")
|
||||
if (!pageShareUrl || pageShareUrl.length === 0) {
|
||||
|
||||
@@ -14,7 +14,8 @@ export type Message = {
|
||||
isBot: boolean
|
||||
name: string
|
||||
message: string
|
||||
sources: any[]
|
||||
webSources: any[]
|
||||
iodSources: any[]
|
||||
images?: string[]
|
||||
search?: WebSearch
|
||||
reasoning_time_taken?: number
|
||||
@@ -25,7 +26,7 @@ export type Message = {
|
||||
export type ChatHistory = {
|
||||
role: "user" | "assistant" | "system"
|
||||
content: string
|
||||
image?: string,
|
||||
image?: string
|
||||
messageType?: string
|
||||
}[]
|
||||
|
||||
@@ -34,6 +35,8 @@ type State = {
|
||||
setMessages: (messages: Message[]) => void
|
||||
history: ChatHistory
|
||||
setHistory: (history: ChatHistory) => void
|
||||
meteringEntries: MeteringEntry[]
|
||||
setMeteringEntries: (meteringEntries: MeteringEntry[]) => void
|
||||
streaming: boolean
|
||||
setStreaming: (streaming: boolean) => void
|
||||
isFirstMessage: boolean
|
||||
@@ -52,6 +55,8 @@ type State = {
|
||||
setIsEmbedding: (isEmbedding: boolean) => void
|
||||
webSearch: boolean
|
||||
setWebSearch: (webSearch: boolean) => void
|
||||
iodSearch: boolean
|
||||
setIodSearch: (iodSearch: boolean) => void
|
||||
isSearchingInternet: boolean
|
||||
setIsSearchingInternet: (isSearchingInternet: boolean) => void
|
||||
|
||||
@@ -74,11 +79,49 @@ type State = {
|
||||
setUseOCR: (useOCR: boolean) => void
|
||||
}
|
||||
|
||||
export type MeteringEntry = {
|
||||
id: string
|
||||
// 问题
|
||||
queryContent: string
|
||||
// 提示词全文
|
||||
prompt: string
|
||||
// 思维链(只有深度思考时有)
|
||||
cot?: string
|
||||
// 回答
|
||||
responseContent: string
|
||||
// 关联数据个数
|
||||
relatedDataCount: number
|
||||
// 数联网输入token
|
||||
iodInputToken: string
|
||||
// 数联网输出token
|
||||
iodOutputToken: string
|
||||
// 大模型输入token数量
|
||||
modelInputTokenCount: number
|
||||
// 大模型输出token数量
|
||||
modelOutputTokenCount: number
|
||||
// 日期
|
||||
date: Date
|
||||
// 耗时
|
||||
timeTaken: number
|
||||
// 大模型回答的全部内容
|
||||
modelResponseContent: string
|
||||
// iod的全部内容的token数量
|
||||
iodTokenCount: number
|
||||
// iod返回的数据
|
||||
iodData: any[]
|
||||
// iod keywords
|
||||
iodKeywords: string[]
|
||||
// 模型
|
||||
model: string
|
||||
}
|
||||
|
||||
export const useStoreMessageOption = create<State>((set) => ({
|
||||
messages: [],
|
||||
setMessages: (messages) => set({ messages }),
|
||||
history: [],
|
||||
setHistory: (history) => set({ history }),
|
||||
meteringEntries: [],
|
||||
setMeteringEntries: (meteringEntries) => set({ meteringEntries }),
|
||||
streaming: false,
|
||||
setStreaming: (streaming) => set({ streaming }),
|
||||
isFirstMessage: true,
|
||||
@@ -100,6 +143,8 @@ export const useStoreMessageOption = create<State>((set) => ({
|
||||
setIsEmbedding: (isEmbedding) => set({ isEmbedding }),
|
||||
webSearch: false,
|
||||
setWebSearch: (webSearch) => set({ webSearch }),
|
||||
iodSearch: false,
|
||||
setIodSearch: (iodSearch) => set({ iodSearch }),
|
||||
isSearchingInternet: false,
|
||||
setIsSearchingInternet: (isSearchingInternet) => set({ isSearchingInternet }),
|
||||
selectedSystemPrompt: null,
|
||||
@@ -115,5 +160,5 @@ export const useStoreMessageOption = create<State>((set) => ({
|
||||
setTemporaryChat: (temporaryChat) => set({ temporaryChat }),
|
||||
|
||||
useOCR: false,
|
||||
setUseOCR: (useOCR) => set({ useOCR }),
|
||||
setUseOCR: (useOCR) => set({ useOCR })
|
||||
}))
|
||||
|
||||
9
src/types/iod.ts
Normal file
9
src/types/iod.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type IodRegistryEntry = {
|
||||
doId: string
|
||||
name: string
|
||||
url?: string
|
||||
pdf_url?: string
|
||||
description: string
|
||||
content?: string
|
||||
data_space?: string
|
||||
}
|
||||
@@ -11,7 +11,8 @@ export type Message = {
|
||||
isBot: boolean
|
||||
name: string
|
||||
message: string
|
||||
sources: any[]
|
||||
webSources: any[]
|
||||
iodSources: any[]
|
||||
images?: string[]
|
||||
search?: WebSearch
|
||||
messageType?: string
|
||||
|
||||
5
src/types/web.ts
Normal file
5
src/types/web.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type WebSearchResult = {
|
||||
url: string
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
22
src/utils/date.ts
Normal file
22
src/utils/date.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export function formatDate(date) {
|
||||
// 获取年份
|
||||
const year = date.getFullYear()
|
||||
|
||||
// 获取月份,注意月份是从0开始计数的,所以需要加1,并且确保月份是两位数
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0")
|
||||
|
||||
// 获取日期,确保日期是两位数
|
||||
const day = String(date.getDate()).padStart(2, "0")
|
||||
|
||||
// 获取小时,24小时制,并确保小时是两位数
|
||||
const hours = String(date.getHours()).padStart(2, "0")
|
||||
|
||||
// 获取分钟,并确保分钟是两位数
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0")
|
||||
|
||||
// 组合成所需的格式
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 示例使用
|
||||
const now = new Date()
|
||||
21
src/web/1.json
Normal file
21
src/web/1.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"action": "executeContract",
|
||||
"contractID": "BDBrowser",
|
||||
"operation": "sendRequestDirectly",
|
||||
"arg": {
|
||||
"id": "670E241C9937B3537047C87053E3AA36",
|
||||
"doipUrl": "tcp://reg01.public.internetofdata.cn:21037",
|
||||
"op": "Search",
|
||||
"attributes": {
|
||||
"offset": 2100,
|
||||
"count": 5,
|
||||
"bodyBase64Encoded": false,
|
||||
"searchMode": [
|
||||
{ "key": "data_type", "type": "MUST", "value": "paper" },
|
||||
{ "key": "title", "type": "MUST", "value": "Number_1" },
|
||||
{ "key": "description", "type": "MUST", "value": "Number_1" }
|
||||
]
|
||||
},
|
||||
"body": ""
|
||||
}
|
||||
}
|
||||
33
src/web/2.ts
Normal file
33
src/web/2.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
const ollama = await pageAssistModel({
|
||||
model: selectedModel!,
|
||||
baseUrl: cleanUrl(url),
|
||||
keepAlive:
|
||||
currentChatModelSettings?.keepAlive ?? userDefaultModelSettings?.keepAlive,
|
||||
temperature:
|
||||
currentChatModelSettings?.temperature ??
|
||||
userDefaultModelSettings?.temperature,
|
||||
topK: currentChatModelSettings?.topK ?? userDefaultModelSettings?.topK,
|
||||
topP: currentChatModelSettings?.topP ?? userDefaultModelSettings?.topP,
|
||||
numCtx: currentChatModelSettings?.numCtx ?? userDefaultModelSettings?.numCtx,
|
||||
seed: currentChatModelSettings?.seed,
|
||||
numGpu: currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
||||
numPredict:
|
||||
currentChatModelSettings?.numPredict ??
|
||||
userDefaultModelSettings?.numPredict,
|
||||
useMMap:
|
||||
currentChatModelSettings?.useMMap ?? userDefaultModelSettings?.useMMap,
|
||||
minP: currentChatModelSettings?.minP ?? userDefaultModelSettings?.minP,
|
||||
repeatLastN:
|
||||
currentChatModelSettings?.repeatLastN ??
|
||||
userDefaultModelSettings?.repeatLastN,
|
||||
repeatPenalty:
|
||||
currentChatModelSettings?.repeatPenalty ??
|
||||
userDefaultModelSettings?.repeatPenalty,
|
||||
tfsZ: currentChatModelSettings?.tfsZ ?? userDefaultModelSettings?.tfsZ,
|
||||
numKeep:
|
||||
currentChatModelSettings?.numKeep ?? userDefaultModelSettings?.numKeep,
|
||||
numThread:
|
||||
currentChatModelSettings?.numThread ?? userDefaultModelSettings?.numThread,
|
||||
useMlock:
|
||||
currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock
|
||||
})
|
||||
211
src/web/iod.ts
Normal file
211
src/web/iod.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
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()
|
||||
|
||||
// results 根据 doId 去重
|
||||
const map = new Map<string, IodRegistryEntry>()
|
||||
for (const r of results) {
|
||||
map.set(r.doId, r)
|
||||
}
|
||||
|
||||
return Array.from(map.values())
|
||||
}
|
||||
|
||||
const ARXIV_URL_PATTERN = /^https?:\/\/arxiv\.org\//
|
||||
const ARXIV_NO_HTM = "No HTML for"
|
||||
|
||||
export const searchIod = async (query: string, keywords: string[]) => {
|
||||
const searchResults = await localIodSearch(query, keywords)
|
||||
|
||||
const isSimpleMode = await getIsSimpleInternetSearch()
|
||||
|
||||
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
|
||||
}
|
||||
121
src/web/web.ts
121
src/web/web.ts
@@ -8,6 +8,9 @@ import { getWebsiteFromQuery, processSingleWebsite } from "./website"
|
||||
import { searxngSearch } from "./search-engines/searxng"
|
||||
import { braveAPISearch } from "./search-engines/brave-api"
|
||||
import { webBaiduSearch } from "./search-engines/baidu"
|
||||
import { searchIod } from "./iod"
|
||||
import type { WebSearchResult } from "~/types/web"
|
||||
import type { IodRegistryEntry } from "~/types/iod"
|
||||
|
||||
const getHostName = (url: string) => {
|
||||
try {
|
||||
@@ -18,54 +21,105 @@ const getHostName = (url: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const searchWeb = (provider: string, query: string) => {
|
||||
async function searchWeb(
|
||||
provider: string,
|
||||
query: string
|
||||
): Promise<WebSearchResult[]> {
|
||||
let results = []
|
||||
switch (provider) {
|
||||
case "duckduckgo":
|
||||
return webDuckDuckGoSearch(query)
|
||||
results = await webDuckDuckGoSearch(query)
|
||||
break
|
||||
case "sogou":
|
||||
return webSogouSearch(query)
|
||||
results = await webSogouSearch(query)
|
||||
break
|
||||
case "brave":
|
||||
return webBraveSearch(query)
|
||||
results = await webBraveSearch(query)
|
||||
break
|
||||
case "searxng":
|
||||
return searxngSearch(query)
|
||||
results = await searxngSearch(query)
|
||||
break
|
||||
case "brave-api":
|
||||
return braveAPISearch(query)
|
||||
results = await braveAPISearch(query)
|
||||
break
|
||||
case "baidu":
|
||||
return webBaiduSearch(query)
|
||||
results = await webBaiduSearch(query)
|
||||
break
|
||||
default:
|
||||
return webGoogleSearch(query)
|
||||
results = await webGoogleSearch(query)
|
||||
break
|
||||
}
|
||||
return results.map((r) => ({ ...r, name: getHostName(r.url) }))
|
||||
}
|
||||
|
||||
export const getSystemPromptForWeb = async (query: string) => {
|
||||
export const getSystemPromptForWeb = async (
|
||||
query: string,
|
||||
keywords: string[] = [],
|
||||
webSearch = true,
|
||||
iodSearch = false
|
||||
) => {
|
||||
try {
|
||||
|
||||
const websiteVisit = getWebsiteFromQuery(query)
|
||||
let search: {
|
||||
url: any;
|
||||
content: string;
|
||||
}[] = []
|
||||
let webSearchResults: WebSearchResult[] = []
|
||||
// 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
|
||||
const queryWithoutUrl = websiteVisit.queryWithouUrls
|
||||
search = await processSingleWebsite(url, queryWithoutUrl)
|
||||
|
||||
} else {
|
||||
const searchProvider = await getSearchProvider()
|
||||
search = await searchWeb(searchProvider, query)
|
||||
// search_results_web = webSearchResults
|
||||
// .map(
|
||||
// (result, idx) =>
|
||||
// `<result source="${result.url}" id="${idx}">${result.content}</result>`
|
||||
// )
|
||||
// .join("\n")
|
||||
}
|
||||
|
||||
let iodSearchResults: IodRegistryEntry[] = []
|
||||
// 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 _iodSearchResults = iodSearchResults
|
||||
.map((res) => ({
|
||||
doId: res.doId,
|
||||
name: res.name,
|
||||
url: res.url,
|
||||
data_space: res.data_space,
|
||||
content: res.content || res.description,
|
||||
tokenCount: (res.content || res.description)?.length ?? 0,
|
||||
}))
|
||||
|
||||
const iod_search_results = _iodSearchResults
|
||||
.map(
|
||||
(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")
|
||||
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()
|
||||
|
||||
@@ -73,23 +127,30 @@ export const getSystemPromptForWeb = async (query: string) => {
|
||||
|
||||
const prompt = system
|
||||
.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 {
|
||||
prompt,
|
||||
source: search.map((result) => {
|
||||
webSources: webSearchResults.map((result) => {
|
||||
return {
|
||||
url: result.url,
|
||||
name: getHostName(result.url),
|
||||
name: result.name,
|
||||
type: "url"
|
||||
}
|
||||
})
|
||||
}),
|
||||
iodSources: iodSearchResults,
|
||||
iodSearchResults: _iodSearchResults,
|
||||
iodTokenCount: _iodSearchResults.reduce((acc, cur) => (acc + cur.content.length), 0)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return {
|
||||
prompt: "",
|
||||
source: []
|
||||
webSources: [],
|
||||
iodSources: [],
|
||||
iodSearchResults: [],
|
||||
iodTokenCount: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user