Merge branch 'feat/metering' of gitea.internetapi.cn:iod/page-assist into feat/page

This commit is contained in:
李芳 2025-02-22 17:00:58 +08:00
commit 970ffdac15
12 changed files with 463 additions and 79 deletions

View File

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

View File

@ -1,28 +1,29 @@
{ {
"newChat": "新聊天", "newChat": "新聊天",
"selectAPrompt": "选择一个提示词", "selectAPrompt": "选择一个提示词",
"githubRepository": "GitHub 仓库", "githubRepository": "GitHub 仓库",
"settings": "设置", "settings": "设置",
"sidebarTitle": "聊天历史", "metering": "计量",
"error": "错误", "sidebarTitle": "聊天历史",
"somethingWentWrong": "出现了错误", "error": "错误",
"validationSelectModel": "请选择一个模型以继续", "somethingWentWrong": "出现了错误",
"deleteHistoryConfirmation": "你确定要删除这个历史记录吗?", "validationSelectModel": "请选择一个模型以继续",
"editHistoryTitle": "输入一个新的标题", "deleteHistoryConfirmation": "你确定要删除这个历史记录吗?",
"temporaryChat": "临时聊天", "editHistoryTitle": "输入一个新的标题",
"more": { "temporaryChat": "临时聊天",
"copy": { "more": {
"group": "复制", "copy": {
"asText": "复制为文本", "group": "复制",
"asMarkdown": "复制为 Markdown", "asText": "复制为文本",
"success": "已复制到剪贴板!" "asMarkdown": "复制为 Markdown",
}, "success": "已复制到剪贴板!"
"download": { },
"group": "下载", "download": {
"text": "文本文件 (.txt)", "group": "下载",
"markdown": "Markdown 文件 (.md)", "text": "文本文件 (.txt)",
"json": "JSON 文件 (.json)" "markdown": "Markdown 文件 (.md)",
}, "json": "JSON 文件 (.json)"
"share": "分享" },
} "share": "分享"
} }
}

View File

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

View File

@ -0,0 +1,168 @@
import {
Card,
List,
Table,
Tag,
Space,
TableProps,
Divider,
Typography
} from "antd"
import { NavLink } from "react-router-dom"
const data = [
{
key: "输出token数",
value: 2
},
{
key: "输入token数",
value: 2
},
{
key: "模型",
value: "xxx"
}
]
const outputTokenData = [
{
key: "关键词提示",
value: "xxx"
},
{
key: "问题",
value: "xxx"
},
{
key: "数联网引用数据",
value: "xxx"
},
{
key: "提供方",
value: "xxx"
},
{
key: "token数量",
value: 2
},
{
key: "内容",
value: "xxx"
}
]
interface DataType {
key: string
name: string
age: number
address: string
tags: string[]
}
const columns: TableProps<DataType>["columns"] = [
{
title: "Name",
dataIndex: "name",
key: "name",
render: (text) => <a>{text}</a>
},
{
title: "Age",
dataIndex: "age",
key: "age"
},
{
title: "Address",
dataIndex: "address",
key: "address"
},
{
title: "Tags",
key: "tags",
dataIndex: "tags",
render: (_, { tags }) => (
<>
{tags.map((tag) => {
let color = tag.length > 5 ? "geekblue" : "green"
if (tag === "loser") {
color = "volcano"
}
return (
<Tag color={color} key={tag}>
{tag.toUpperCase()}
</Tag>
)
})}
</>
)
},
{
title: "Action",
key: "action",
render: (_, record) => (
<Space size="middle">
{/* <a>Invite {record.name}</a> */}
<NavLink to="/metering/list/123">
<a>Detail</a>
</NavLink>
</Space>
)
}
]
const data1: DataType[] = [
{
key: "1",
name: "John Brown",
age: 32,
address: "New York No. 1 Lake Park",
tags: ["nice", "developer"]
},
{
key: "2",
name: "Jim Green",
age: 42,
address: "London No. 1 Lake Park",
tags: ["loser"]
},
{
key: "3",
name: "Joe Black",
age: 32,
address: "Sydney No. 1 Lake Park",
tags: ["cool", "teacher"]
}
]
export const ListDetail = () => {
return (
<div className="p-[1rem] pt-[4rem]">
<List
grid={{ gutter: 16, column: 3 }}
dataSource={data}
renderItem={(item) => (
<List.Item>
<Card title={item.key}>{item.value}</Card>
</List.Item>
)}
/>
<div className="mb-[50px]">
<Divider orientation="left">token详情</Divider>
<List
bordered
dataSource={outputTokenData}
renderItem={(item) => (
<List.Item>
<Typography.Text mark>{item.key}</Typography.Text> {item.value}
</List.Item>
)}
/>
</div>
<Table<DataType> columns={columns} dataSource={data1} />
</div>
)
}

View File

@ -0,0 +1,137 @@
import React from "react"
import { ChatMessage, useStoreMessageOption } from "@/store/option"
import { Card, List, Table, Tag, Space, TableProps, Tooltip } from "antd"
import { NavLink } from "react-router-dom"
import { formatDate } from "@/utils/date"
const data = [
{
key: "对话数量",
value: 2
},
{
key: "输出token数",
value: 2
},
{
key: "输入token数",
value: 2
}
]
const columns: TableProps<ChatMessage>["columns"] = [
{
title: "id",
dataIndex: "id",
key: "id",
width: "13%"
},
{
title: "问题",
dataIndex: "query",
key: "query"
},
{
title: "提示词全文",
dataIndex: "prompt",
key: "prompt",
ellipsis: {
showTitle: false
},
render: (prompt) => (
<Tooltip placement="topLeft" title={prompt}>
{prompt}
</Tooltip>
),
width: "10%"
},
{
title: "思维链",
key: "thinkingChain",
dataIndex: "thinkingChain",
width: "10%"
},
{
title: "回答",
dataIndex: "answer",
key: "answer",
ellipsis: {
showTitle: false
},
render: (answer) => (
<Tooltip placement="topLeft" title={answer}>
{answer}
</Tooltip>
),
width: "10%"
},
{
title: "关联数据个数",
dataIndex: "relatedDataCount",
key: "relatedDataCount"
},
{
title: "数联网token",
dataIndex: "iodOutputToken",
key: "iodOutputToken",
render: (iodOutputToken) => <div>{iodOutputToken.length}</div>
},
{
title: "大模型token",
key: "largeModelToken",
dataIndex: "largeModelToken",
render: (_, record) => {
return (
<div>{record.iodInputToken.length + record.iodOutputToken.length}</div>
)
}
},
{
title: "日期",
dataIndex: "date",
key: "date",
render: (date) => {
return <div>{formatDate(date)}</div>
}
},
{
title: "耗时",
key: "timeTaken",
dataIndex: "timeTaken"
},
{
title: "操作",
key: "action",
render: (_, record) => (
<Space size="middle">
{/* <a>Invite {record.name}</a> */}
<NavLink to={`/metering/list/${record.id}`}>
<a>Detail</a>
</NavLink>
</Space>
)
}
]
export const MeteringDetail = () => {
const { chatMessages } = useStoreMessageOption()
console.log(chatMessages, "opppp")
return (
<div className="pt-[4rem]">
<List
grid={{ gutter: 16, column: 3 }}
dataSource={data}
renderItem={(item) => (
<List.Item>
<Card title={item.key}>{item.value}</Card>
</List.Item>
)}
/>
<Table<ChatMessage> columns={columns} dataSource={chatMessages} />
</div>
)
}

View File

@ -8,7 +8,7 @@ import {
promptForRag, promptForRag,
systemPromptForNonRagOption systemPromptForNonRagOption
} from "~/services/ollama" } from "~/services/ollama"
import { type ChatHistory, type Message } from "~/store/option" import { type ChatHistory, ChatMessage, type Message } from "~/store/option"
import { SystemMessage } from "@langchain/core/messages" import { SystemMessage } from "@langchain/core/messages"
import { useStoreMessageOption } from "~/store/option" import { useStoreMessageOption } from "~/store/option"
import { import {
@ -114,6 +114,26 @@ export const useMessageOption = () => {
setWebSearch(true) setWebSearch(true)
} }
} }
// 从最后的结果中解析出 思维链 和 结果
const responseResolver = (msg: string) => {
const thinkStart = msg.indexOf("<think>")
const thinkEnd = msg.indexOf("</think>")
let think = ""
let content = ""
if (thinkStart > -1 && thinkEnd > -1) {
think = msg.substring(thinkStart + 7, thinkEnd)
content = msg.substring(thinkEnd + 8)
} else {
content = msg
}
// 去掉换行符
think = think.replace(/\n/g, "")
content = content.replace(/\n/g, "")
return {
think,
content
}
}
const searchChatMode = async ( const searchChatMode = async (
webSearch: boolean, webSearch: boolean,
@ -170,6 +190,10 @@ export const useMessageOption = () => {
}) })
let newMessage: Message[] = [] let newMessage: Message[] = []
let generateMessageId = generateID() let generateMessageId = generateID()
const chatMessage: ChatMessage = {
id: generateMessageId,
queryContent: message
} as ChatMessage
if (!isRegenerate) { if (!isRegenerate) {
newMessage = [ newMessage = [
@ -300,6 +324,7 @@ export const useMessageOption = () => {
) )
console.log("prompt:\n" + prompt) console.log("prompt:\n" + prompt)
setIsSearchingInternet(false) setIsSearchingInternet(false)
chatMessage.prompt = prompt
// message = message.trim().replaceAll("\n", " ") // message = message.trim().replaceAll("\n", " ")
@ -460,23 +485,13 @@ export const useMessageOption = () => {
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
setChatMessages([ chatMessage.relatedDataCount = keywords.length
...chatMessages, chatMessage.timeTaken = timetaken
{ chatMessage.date = reasoningStartTime
id: generateMessageId, const { think, content } = responseResolver(fullText)
query: message, chatMessage.thinkingChain = think
prompt: prompt, chatMessage.responseContent = content
thinkingChain: "", setChatMessages([...chatMessages, chatMessage])
answer: fullText,
relatedDataCount: count,
iodInputToken: "",
iodOutputToken: "",
modelInputToken: "",
modelOutputToken: "",
date: reasoningStartTime,
timeTaken: timetaken
}
])
} catch (e) { } catch (e) {
const errorSave = await saveMessageOnError({ const errorSave = await saveMessageOnError({
e, e,

View File

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

View File

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

View File

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

View File

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

View File

@ -33,13 +33,13 @@ export type ChatHistory = {
export type ChatMessage = { export type ChatMessage = {
id: string id: string
// 问题 // 问题
query: string queryContent: string
// 提示词全文 // 提示词全文
prompt: string prompt: string
// 思维链(只有深度思考时有) // 思维链(只有深度思考时有)
thinkingChain?: string thinkingChain?: string
// 回答 // 回答
answer: string responseContent: string
// 关联数据个数 // 关联数据个数
relatedDataCount: number relatedDataCount: number
// 数联网输入token // 数联网输入token
@ -54,15 +54,15 @@ export type ChatMessage = {
date: Date date: Date
// 耗时 // 耗时
timeTaken: number timeTaken: number
}[] }
type State = { type State = {
messages: Message[] messages: Message[]
setMessages: (messages: Message[]) => void setMessages: (messages: Message[]) => void
history: ChatHistory history: ChatHistory
setHistory: (history: ChatHistory) => void setHistory: (history: ChatHistory) => void
chatMessages: ChatMessage chatMessages: ChatMessage[]
setChatMessages: (chatMessages: ChatMessage) => void setChatMessages: (chatMessages: ChatMessage[]) => void
streaming: boolean streaming: boolean
setStreaming: (streaming: boolean) => void setStreaming: (streaming: boolean) => void
isFirstMessage: boolean isFirstMessage: boolean

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

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