feat: Add generation info to messages

This commit introduces a new feature that displays generation information for each message in the chat.

The generation info is displayed in a popover and includes details about the model used, the prompt, and other relevant information. This helps users understand how their messages were generated and troubleshoot any issues that may arise.

The generation info is retrieved from the LLM response and is stored in the database alongside other message details.

This commit also includes translations for the generation info label in all supported languages.
This commit is contained in:
n4ze3m 2024-11-09 15:17:59 +05:30
parent 9ecc8c4e75
commit 9f383a81b6
26 changed files with 283 additions and 64 deletions

View File

@ -112,5 +112,6 @@
"older": "Ældre" "older": "Ældre"
}, },
"pin": "Fastgør", "pin": "Fastgør",
"unpin": "Frigør" "unpin": "Frigør",
"generationInfo": "Genererings Info"
} }

View File

@ -112,5 +112,6 @@
"older": "Älter" "older": "Älter"
}, },
"pin": "Anheften", "pin": "Anheften",
"unpin": "Losheften" "unpin": "Losheften",
"generationInfo": "Generierungsinformationen"
} }

View File

@ -116,5 +116,6 @@
"older": "Older" "older": "Older"
}, },
"pin": "Pin", "pin": "Pin",
"unpin": "Unpin" "unpin": "Unpin",
"generationInfo": "Generation Info"
} }

View File

@ -111,5 +111,6 @@
"older": "Más antiguo" "older": "Más antiguo"
}, },
"pin": "Fijar", "pin": "Fijar",
"unpin": "Desfijar" "unpin": "Desfijar",
"generationInfo": "Información de Generación"
} }

View File

@ -105,5 +105,6 @@
"older": "قدیمی‌تر" "older": "قدیمی‌تر"
}, },
"pin": "پین کردن", "pin": "پین کردن",
"unpin": "حذف پین" "unpin": "حذف پین",
"generationInfo": "اطلاعات تولید"
} }

View File

@ -111,5 +111,6 @@
"older": "Plus ancien" "older": "Plus ancien"
}, },
"pin": "Épingler", "pin": "Épingler",
"unpin": "Désépingler" "unpin": "Désépingler",
"generationInfo": "Informations de génération"
} }

View File

@ -111,5 +111,6 @@
"older": "Più Vecchi" "older": "Più Vecchi"
}, },
"pin": "Fissa", "pin": "Fissa",
"unpin": "Rimuovi" "unpin": "Rimuovi",
"generationInfo": "Informazioni sulla Generazione"
} }

View File

@ -111,5 +111,6 @@
"older": "それ以前" "older": "それ以前"
}, },
"pin": "固定", "pin": "固定",
"unpin": "固定解除" "unpin": "固定解除",
"generationInfo": "生成情報"
} }

View File

@ -111,5 +111,6 @@
"older": "그 이전" "older": "그 이전"
}, },
"pin": "고정", "pin": "고정",
"unpin": "고정 해제" "unpin": "고정 해제",
"generationInfo": "생성 정보"
} }

View File

@ -110,5 +110,7 @@
"older": "പഴയത്" "older": "പഴയത്"
}, },
"pin": "പിൻ ചെയ്യുക", "pin": "പിൻ ചെയ്യുക",
"unpin": "അൺപിൻ ചെയ്യുക" "unpin": "അൺപിൻ ചെയ്യുക",
"generationInfo": "ജനറേഷൻ വിവരങ്ങൾ"
} }

View File

@ -112,5 +112,6 @@
"older": "Eldre" "older": "Eldre"
}, },
"pin": "Fest", "pin": "Fest",
"unpin": "Løsne" "unpin": "Løsne",
"generationInfo": "Generasjonsinformasjon"
} }

View File

@ -111,5 +111,6 @@
"older": "Mais Antigos" "older": "Mais Antigos"
}, },
"pin": "Fixar", "pin": "Fixar",
"unpin": "Desafixar" "unpin": "Desafixar",
"generationInfo": "Informações de Geração"
} }

View File

@ -111,5 +111,6 @@
"older": "Ранее" "older": "Ранее"
}, },
"pin": "Закрепить", "pin": "Закрепить",
"unpin": "Открепить" "unpin": "Открепить",
"generationInfo": "Информация о генерации"
} }

View File

@ -116,5 +116,6 @@
"older": "Äldre" "older": "Äldre"
}, },
"pin": "Fäst", "pin": "Fäst",
"unpin": "Ta bort fäst" "unpin": "Ta bort fäst",
"generationInfo": "Generationsinformation"
} }

View File

@ -111,5 +111,6 @@
"older": "更早" "older": "更早"
}, },
"pin": "置顶", "pin": "置顶",
"unpin": "取消置顶" "unpin": "取消置顶",
"generationInfo": "生成信息"
} }

View File

@ -0,0 +1,65 @@
type GenerationMetrics = {
total_duration?: number
load_duration?: number
prompt_eval_count?: number
prompt_eval_duration?: number
eval_count?: number
eval_duration?: number
context?: string
response?: string
}
type Props = {
generationInfo: GenerationMetrics
}
export const GenerationInfo = ({ generationInfo }: Props) => {
if (!generationInfo) return null
const calculateTokensPerSecond = (
evalCount?: number,
evalDuration?: number
) => {
if (!evalCount || !evalDuration) return 0
return (evalCount / evalDuration) * 1e9
}
const formatDuration = (nanoseconds?: number) => {
if (!nanoseconds) return "0ms"
const ms = nanoseconds / 1e6
if (ms < 1) return `${ms.toFixed(3)}ms`
if (ms < 1000) return `${Math.round(ms)}ms`
return `${(ms / 1000).toFixed(2)}s`
}
const metricsToDisplay = {
...generationInfo,
...(generationInfo?.eval_count && generationInfo?.eval_duration
? {
tokens_per_second: calculateTokensPerSecond(
generationInfo.eval_count,
generationInfo.eval_duration
).toFixed(2)
}
: {})
}
return (
<div className="p-2 w-full">
<div className="flex flex-col gap-2">
{Object.entries(metricsToDisplay)
.filter(([key]) => key !== "model")
.map(([key, value]) => (
<div key={key} className="flex flex-wrap justify-between">
<div className="font-medium text-xs">{key}</div>
<div className="font-medium text-xs break-all">
{key.includes("duration")
? formatDuration(value as number)
: String(value)}
</div>
</div>
))}
</div>
</div>
)
}

View File

@ -1,10 +1,11 @@
import Markdown from "../../Common/Markdown" import Markdown from "../../Common/Markdown"
import React from "react" import React from "react"
import { Tag, Image, Tooltip, Collapse } from "antd" import { Tag, Image, Tooltip, Collapse, Popover } from "antd"
import { WebSearch } from "./WebSearch" import { WebSearch } from "./WebSearch"
import { import {
CheckIcon, CheckIcon,
ClipboardIcon, ClipboardIcon,
InfoIcon,
Pen, Pen,
PlayIcon, PlayIcon,
RotateCcw, RotateCcw,
@ -16,6 +17,7 @@ import { MessageSource } from "./MessageSource"
import { useTTS } from "@/hooks/useTTS" 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"
type Props = { type Props = {
message: string message: string
@ -37,6 +39,7 @@ type Props = {
hideEditAndRegenerate?: boolean hideEditAndRegenerate?: boolean
onSourceClick?: (source: any) => void onSourceClick?: (source: any) => void
isTTSEnabled?: boolean isTTSEnabled?: boolean
generationInfo?: any
} }
export const PlaygroundMessage = (props: Props) => { export const PlaygroundMessage = (props: Props) => {
@ -206,6 +209,18 @@ export const PlaygroundMessage = (props: Props) => {
</Tooltip> </Tooltip>
)} )}
{props.generationInfo && (
<Popover
content={
<GenerationInfo generationInfo={props.generationInfo} />
}
title={t("generationInfo")}>
<button 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">
<InfoIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
</button>
</Popover>
)}
{!props.hideEditAndRegenerate && {!props.hideEditAndRegenerate &&
props.currentMessageIndex === props.totalMessages - 1 && ( props.currentMessageIndex === props.totalMessages - 1 && (
<Tooltip title={t("regenerate")}> <Tooltip title={t("regenerate")}>

View File

@ -54,6 +54,7 @@ export const PlaygroundChat = () => {
setIsSourceOpen(true) setIsSourceOpen(true)
}} }}
isTTSEnabled={ttsEnabled} isTTSEnabled={ttsEnabled}
generationInfo={message?.generationInfo}
/> />
))} ))}
{messages.length > 0 && ( {messages.length > 0 && (

View File

@ -47,6 +47,7 @@ export const SidePanelBody = () => {
setIsSourceOpen(true) setIsSourceOpen(true)
}} }}
isTTSEnabled={ttsEnabled} isTTSEnabled={ttsEnabled}
generationInfo={message?.generationInfo}
/> />
))} ))}
<div className="w-full h-48 flex-shrink-0"></div> <div className="w-full h-48 flex-shrink-0"></div>

View File

@ -33,6 +33,7 @@ type Message = {
search?: WebSearch search?: WebSearch
createdAt: number createdAt: number
messageType?: string messageType?: string
generationInfo?: any
} }
type Webshare = { type Webshare = {
@ -254,7 +255,8 @@ export const saveMessage = async (
images: string[], images: string[],
source?: any[], source?: any[],
time?: number, time?: number,
message_type?: string message_type?: string,
generationInfo?: any
) => { ) => {
const id = generateID() const id = generateID()
let createdAt = Date.now() let createdAt = Date.now()
@ -270,7 +272,8 @@ export const saveMessage = async (
images, images,
createdAt, createdAt,
sources: source, sources: source,
messageType: message_type messageType: message_type,
generationInfo: generationInfo
} }
const db = new PageAssitDatabase() const db = new PageAssitDatabase()
await db.addMessage(message) await db.addMessage(message)

View File

@ -118,7 +118,7 @@ export const saveMessageOnSuccess = async ({
fullText, fullText,
source, source,
message_source = "web-ui", message_source = "web-ui",
message_type message_type, generationInfo
}: { }: {
historyId: string | null historyId: string | null
setHistoryId: (historyId: string) => void setHistoryId: (historyId: string) => void
@ -130,6 +130,7 @@ export const saveMessageOnSuccess = async ({
source: any[] source: any[]
message_source?: "copilot" | "web-ui", message_source?: "copilot" | "web-ui",
message_type?: string message_type?: string
generationInfo?: any
}) => { }) => {
if (historyId) { if (historyId) {
if (!isRegenerate) { if (!isRegenerate) {
@ -141,7 +142,8 @@ export const saveMessageOnSuccess = async ({
[image], [image],
[], [],
1, 1,
message_type message_type,
generationInfo
) )
} }
await saveMessage( await saveMessage(
@ -152,7 +154,8 @@ export const saveMessageOnSuccess = async ({
[], [],
source, source,
2, 2,
message_type message_type,
generationInfo
) )
await setLastUsedChatModel(historyId, selectedModel!) await setLastUsedChatModel(historyId, selectedModel!)
} else { } else {
@ -166,7 +169,8 @@ export const saveMessageOnSuccess = async ({
[image], [image],
[], [],
1, 1,
message_type message_type,
generationInfo
) )
await saveMessage( await saveMessage(
newHistoryId.id, newHistoryId.id,
@ -176,7 +180,8 @@ export const saveMessageOnSuccess = async ({
[], [],
source, source,
2, 2,
message_type message_type,
generationInfo
) )
setHistoryId(newHistoryId.id) setHistoryId(newHistoryId.id)
await setLastUsedChatModel(newHistoryId.id, selectedModel!) await setLastUsedChatModel(newHistoryId.id, selectedModel!)

View File

@ -328,10 +328,25 @@ export const useMessage = () => {
const applicationChatHistory = generateHistory(history, selectedModel) const applicationChatHistory = generateHistory(history, selectedModel)
let generationInfo: any | undefined = undefined
const chunks = await ollama.stream( const chunks = await ollama.stream(
[...applicationChatHistory, humanMessage], [...applicationChatHistory, humanMessage],
{ {
signal: signal signal: signal,
callbacks: [
{
handleLLMEnd(
output: any,
): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
console.log("handleLLMEnd error", e)
}
}
}
]
} }
) )
let count = 0 let count = 0
@ -361,7 +376,8 @@ export const useMessage = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source sources: source,
generationInfo
} }
} }
return message return message
@ -390,7 +406,8 @@ export const useMessage = () => {
image, image,
fullText, fullText,
source, source,
message_source: "copilot" message_source: "copilot",
generationInfo
}) })
setIsProcessing(false) setIsProcessing(false)
@ -544,10 +561,25 @@ export const useMessage = () => {
) )
} }
let generationInfo: any | undefined = undefined
const chunks = await ollama.stream( const chunks = await ollama.stream(
[...applicationChatHistory, humanMessage], [...applicationChatHistory, humanMessage],
{ {
signal: signal signal: signal,
callbacks: [
{
handleLLMEnd(
output: any,
): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
console.log("handleLLMEnd error", e)
}
}
}
]
} }
) )
let count = 0 let count = 0
@ -576,7 +608,8 @@ export const useMessage = () => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText message: fullText,
generationInfo
} }
} }
return message return message
@ -605,7 +638,8 @@ export const useMessage = () => {
image, image,
fullText, fullText,
source: [], source: [],
message_source: "copilot" message_source: "copilot",
generationInfo
}) })
setIsProcessing(false) setIsProcessing(false)
@ -789,10 +823,24 @@ export const useMessage = () => {
) )
} }
let generationInfo: any | undefined = undefined
const chunks = await ollama.stream( const chunks = await ollama.stream(
[...applicationChatHistory, humanMessage], [...applicationChatHistory, humanMessage],
{ {
signal: signal signal: signal,
callbacks: [
{
handleLLMEnd(
output: any,
): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
console.log("handleLLMEnd error", e)
}
}
}
]
} }
) )
let count = 0 let count = 0
@ -822,7 +870,8 @@ export const useMessage = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source sources: source,
generationInfo
} }
} }
return message return message
@ -850,7 +899,8 @@ export const useMessage = () => {
message, message,
image, image,
fullText, fullText,
source source,
generationInfo
}) })
setIsProcessing(false) setIsProcessing(false)
@ -982,8 +1032,23 @@ export const useMessage = () => {
}) })
} }
let generationInfo: any | undefined = undefined
const chunks = await ollama.stream([humanMessage], { const chunks = await ollama.stream([humanMessage], {
signal: signal signal: signal,
callbacks: [
{
handleLLMEnd(
output: any,
): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
console.log("handleLLMEnd error", e)
}
}
}
]
}) })
let count = 0 let count = 0
for await (const chunk of chunks) { for await (const chunk of chunks) {
@ -1011,7 +1076,8 @@ export const useMessage = () => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText message: fullText,
generationInfo
} }
} }
return message return message
@ -1042,7 +1108,8 @@ export const useMessage = () => {
fullText, fullText,
source: [], source: [],
message_source: "copilot", message_source: "copilot",
message_type: messageType message_type: messageType,
generationInfo
}) })
setIsProcessing(false) setIsProcessing(false)

View File

@ -243,10 +243,23 @@ export const useMessageOption = () => {
) )
} }
let generationInfo: any | undefined = undefined
const chunks = await ollama.stream( const chunks = await ollama.stream(
[...applicationChatHistory, humanMessage], [...applicationChatHistory, humanMessage],
{ {
signal: signal signal: signal,
callbacks: [
{
handleLLMEnd(output: any): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
console.log("handleLLMEnd error", e)
}
}
}
]
} }
) )
let count = 0 let count = 0
@ -276,7 +289,8 @@ export const useMessageOption = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source sources: source,
generationInfo
} }
} }
return message return message
@ -304,7 +318,8 @@ export const useMessageOption = () => {
message, message,
image, image,
fullText, fullText,
source source,
generationInfo
}) })
setIsProcessing(false) setIsProcessing(false)
@ -465,10 +480,23 @@ export const useMessageOption = () => {
) )
} }
let generationInfo: any | undefined = undefined
const chunks = await ollama.stream( const chunks = await ollama.stream(
[...applicationChatHistory, humanMessage], [...applicationChatHistory, humanMessage],
{ {
signal: signal signal: signal,
callbacks: [
{
handleLLMEnd(output: any): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
console.log("handleLLMEnd error", e)
}
}
}
],
} }
) )
@ -498,7 +526,8 @@ export const useMessageOption = () => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText message: fullText,
generationInfo
} }
} }
return message return message
@ -526,7 +555,8 @@ export const useMessageOption = () => {
message, message,
image, image,
fullText, fullText,
source: [] source: [],
generationInfo
}) })
setIsProcessing(false) setIsProcessing(false)
@ -711,10 +741,23 @@ export const useMessageOption = () => {
const applicationChatHistory = generateHistory(history, selectedModel) const applicationChatHistory = generateHistory(history, selectedModel)
let generationInfo: any | undefined = undefined
const chunks = await ollama.stream( const chunks = await ollama.stream(
[...applicationChatHistory, humanMessage], [...applicationChatHistory, humanMessage],
{ {
signal: signal signal: signal,
callbacks: [
{
handleLLMEnd(output: any): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
console.log("handleLLMEnd error", e)
}
}
}
]
} }
) )
let count = 0 let count = 0
@ -744,7 +787,8 @@ export const useMessageOption = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
sources: source sources: source,
generationInfo
} }
} }
return message return message
@ -772,7 +816,8 @@ export const useMessageOption = () => {
message, message,
image, image,
fullText, fullText,
source source,
generationInfo
}) })
setIsProcessing(false) setIsProcessing(false)

View File

@ -49,7 +49,7 @@ export const pageAssistModel = async ({
configuration: { configuration: {
apiKey: providerInfo.apiKey || "temp", apiKey: providerInfo.apiKey || "temp",
baseURL: providerInfo.baseUrl || "", baseURL: providerInfo.baseUrl || "",
} },
}) as any }) as any
} }

View File

@ -16,4 +16,5 @@ type WebSearch = {
search?: WebSearch search?: WebSearch
messageType?: string messageType?: string
id?: string id?: string
generationInfo?: any
} }

View File

@ -50,7 +50,7 @@ export default defineConfig({
outDir: "build", outDir: "build",
manifest: { manifest: {
version: "1.3.3", version: "1.3.4",
name: name:
process.env.TARGET === "firefox" process.env.TARGET === "firefox"
? "Page Assist - A Web UI for Local AI Models" ? "Page Assist - A Web UI for Local AI Models"