feat: Add reasoning UI

This commit is contained in:
n4ze3m 2025-01-24 22:29:18 +05:30
parent b39d60fc3c
commit 97daaf9dc2
31 changed files with 461 additions and 72 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -58,6 +58,7 @@
"remark-math": "5.1.1", "remark-math": "5.1.1",
"tesseract.js": "^5.1.1", "tesseract.js": "^5.1.1",
"turndown": "^7.1.3", "turndown": "^7.1.3",
"unist-util-visit": "^5.0.0",
"yt-transcript": "^0.0.2", "yt-transcript": "^0.0.2",
"zustand": "^4.5.0" "zustand": "^4.5.0"
}, },

View File

@ -125,5 +125,9 @@
"pin": "تثبيت", "pin": "تثبيت",
"unpin": "إلغاء التثبيت", "unpin": "إلغاء التثبيت",
"generationInfo": "معلومات التوليد", "generationInfo": "معلومات التوليد",
"sidebarChat": "دردشة الشريط الجانبي" "sidebarChat": "دردشة الشريط الجانبي",
"reasoning": {
"thinking": "جاري التفكير....",
"thought": "فكر لمدة {{time}}"
}
} }

View File

@ -118,5 +118,9 @@
"pin": "Fastgør", "pin": "Fastgør",
"unpin": "Frigør", "unpin": "Frigør",
"generationInfo": "Genererings Info", "generationInfo": "Genererings Info",
"sidebarChat": "Sidepanel Chat" "sidebarChat": "Sidepanel Chat",
"reasoning": {
"thinking": "Tænker....",
"thought": "Tænkte i {{time}}"
}
} }

View File

@ -118,5 +118,9 @@
"pin": "Anheften", "pin": "Anheften",
"unpin": "Losheften", "unpin": "Losheften",
"generationInfo": "Generierungsinformationen", "generationInfo": "Generierungsinformationen",
"sidebarChat": "Seitenleisten-Chat" "sidebarChat": "Seitenleisten-Chat",
"reasoning": {
"thinking": "Denke nach....",
"thought": "Gedanke für {{time}}"
}
} }

View File

@ -152,5 +152,9 @@
"pin": "Pin", "pin": "Pin",
"unpin": "Unpin", "unpin": "Unpin",
"generationInfo": "Generation Info", "generationInfo": "Generation Info",
"sidebarChat": "Sidebar Chat" "sidebarChat": "Sidebar Chat",
"reasoning": {
"thinking": "Thinking....",
"thought": "Thought for {{time}}"
}
} }

View File

@ -117,5 +117,9 @@
"pin": "Fijar", "pin": "Fijar",
"unpin": "Desfijar", "unpin": "Desfijar",
"generationInfo": "Información de Generación", "generationInfo": "Información de Generación",
"sidebarChat": "Chat lateral" "sidebarChat": "Chat lateral",
"reasoning": {
"thinking": "Pensando....",
"thought": "Pensamiento por {{time}}"
}
} }

View File

@ -111,5 +111,9 @@
"pin": "پین کردن", "pin": "پین کردن",
"unpin": "حذف پین", "unpin": "حذف پین",
"generationInfo": "اطلاعات تولید", "generationInfo": "اطلاعات تولید",
"sidebarChat": "چت کناری" "sidebarChat": "چت کناری",
"reasoning": {
"thinking": "در حال فکر کردن....",
"thought": "فکر کردن برای {{time}}"
}
} }

View File

@ -117,5 +117,9 @@
"pin": "Épingler", "pin": "Épingler",
"unpin": "Désépingler", "unpin": "Désépingler",
"generationInfo": "Informations de génération", "generationInfo": "Informations de génération",
"sidebarChat": "Chat latéral" "sidebarChat": "Chat latéral",
"reasoning": {
"thinking": "Réflexion....",
"thought": "Réflexion pendant {{time}}"
}
} }

View File

@ -117,5 +117,9 @@
"pin": "Fissa", "pin": "Fissa",
"unpin": "Rimuovi", "unpin": "Rimuovi",
"generationInfo": "Informazioni sulla Generazione", "generationInfo": "Informazioni sulla Generazione",
"sidebarChat": "Chat Laterale" "sidebarChat": "Chat Laterale",
"reasoning": {
"thinking": "Pensando....",
"thought": "Pensato per {{time}}"
}
} }

View File

@ -117,5 +117,9 @@
"pin": "固定", "pin": "固定",
"unpin": "固定解除", "unpin": "固定解除",
"generationInfo": "生成情報", "generationInfo": "生成情報",
"sidebarChat": "サイドバーチャット" "sidebarChat": "サイドバーチャット",
"reasoning": {
"thinking": "考え中....",
"thought": "{{time}}の思考"
}
} }

View File

@ -117,5 +117,9 @@
"pin": "고정", "pin": "고정",
"unpin": "고정 해제", "unpin": "고정 해제",
"generationInfo": "생성 정보", "generationInfo": "생성 정보",
"sidebarChat": "사이드바 채팅" "sidebarChat": "사이드바 채팅",
"reasoning": {
"thinking": "생각 중....",
"thought": "{{time}} 동안 생각함"
}
} }

View File

@ -116,6 +116,9 @@
"pin": "പിൻ ചെയ്യുക", "pin": "പിൻ ചെയ്യുക",
"unpin": "അൺപിൻ ചെയ്യുക", "unpin": "അൺപിൻ ചെയ്യുക",
"generationInfo": "ജനറേഷൻ വിവരങ്ങൾ", "generationInfo": "ജനറേഷൻ വിവരങ്ങൾ",
"sidebarChat": "സൈഡ്ബാർ ചാറ്റ്" "sidebarChat": "സൈഡ്ബാർ ചാറ്റ്",
"reasoning": {
"thinking": "ചിന്തിക്കുന്നു....",
"thought": "{{time}} നേരത്തെ ചിന്ത"
}
} }

View File

@ -118,5 +118,9 @@
"pin": "Fest", "pin": "Fest",
"unpin": "Løsne", "unpin": "Løsne",
"generationInfo": "Generasjonsinformasjon", "generationInfo": "Generasjonsinformasjon",
"sidebarChat": "Sidepanel-chat" "sidebarChat": "Sidepanel-chat",
"reasoning": {
"thinking": "Tenker....",
"thought": "Tenkte i {{time}}"
}
} }

View File

@ -117,5 +117,9 @@
"pin": "Fixar", "pin": "Fixar",
"unpin": "Desafixar", "unpin": "Desafixar",
"generationInfo": "Informações de Geração", "generationInfo": "Informações de Geração",
"sidebarChat": "Chat Lateral" "sidebarChat": "Chat Lateral",
"reasoning": {
"thinking": "Pensando....",
"thought": "Pensou por {{time}}"
}
} }

View File

@ -117,5 +117,9 @@
"pin": "Закрепить", "pin": "Закрепить",
"unpin": "Открепить", "unpin": "Открепить",
"generationInfo": "Информация о генерации", "generationInfo": "Информация о генерации",
"sidebarChat": "Боковой чат" "sidebarChat": "Боковой чат",
"reasoning": {
"thinking": "Размышляю...",
"thought": "Размышлял {{time}}"
}
} }

View File

@ -122,5 +122,9 @@
"pin": "Fäst", "pin": "Fäst",
"unpin": "Ta bort fäst", "unpin": "Ta bort fäst",
"generationInfo": "Generationsinformation", "generationInfo": "Generationsinformation",
"sidebarChat": "Sidofältschatt" "sidebarChat": "Sidofältschatt",
"reasoning": {
"thinking": "Tänker....",
"thought": "Tänkte i {{time}}"
}
} }

View File

@ -122,5 +122,9 @@
"pin": "Прикріпити", "pin": "Прикріпити",
"unpin": "Відкріпити", "unpin": "Відкріпити",
"generationInfo": "Інформація про генерацію", "generationInfo": "Інформація про генерацію",
"sidebarChat": "Бічний чат" "sidebarChat": "Бічний чат",
"reasoning": {
"thinking": "Думаю....",
"thought": "Думав протягом {{time}}"
}
} }

View File

@ -117,5 +117,9 @@
"pin": "置顶", "pin": "置顶",
"unpin": "取消置顶", "unpin": "取消置顶",
"generationInfo": "生成信息", "generationInfo": "生成信息",
"sidebarChat": "侧边栏聊天" "sidebarChat": "侧边栏聊天",
"reasoning": {
"thinking": "思考中....",
"thought": "思考了 {{time}}"
}
} }

View File

@ -18,7 +18,8 @@ import { useTTS } from "@/hooks/useTTS"
import { tagColors } from "@/utils/color" import { tagColors } from "@/utils/color"
import { removeModelSuffix } from "@/db/models" import { removeModelSuffix } from "@/db/models"
import { GenerationInfo } from "./GenerationInfo" import { GenerationInfo } from "./GenerationInfo"
import { parseReasoning } from "@/libs/reasoning"
import { humanizeMilliseconds } from "@/utils/humanize-miliseconds"
type Props = { type Props = {
message: string message: string
message_type?: string message_type?: string
@ -40,6 +41,8 @@ type Props = {
onSourceClick?: (source: any) => void onSourceClick?: (source: any) => void
isTTSEnabled?: boolean isTTSEnabled?: boolean
generationInfo?: any generationInfo?: any
isStreaming: boolean
reasoningTimeTaken?: number
} }
export const PlaygroundMessage = (props: Props) => { export const PlaygroundMessage = (props: Props) => {
@ -48,7 +51,6 @@ export const PlaygroundMessage = (props: Props) => {
const { t } = useTranslation("common") const { t } = useTranslation("common")
const { cancel, isSpeaking, speak } = useTTS() const { cancel, isSpeaking, speak } = useTTS()
return ( return (
<div className="group w-full text-gray-800 dark:text-gray-100"> <div className="group w-full text-gray-800 dark:text-gray-100">
<div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> <div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full">
@ -94,7 +96,40 @@ export const PlaygroundMessage = (props: Props) => {
<div className="flex flex-grow flex-col"> <div className="flex flex-grow flex-col">
{!editMode ? ( {!editMode ? (
props.isBot ? ( props.isBot ? (
<Markdown message={props.message} /> <>
{parseReasoning(props.message).map((e, i) => {
if (e.type === "reasoning") {
return (
<Collapse
key={i}
className="border-none !mb-3"
items={[
{
key: "reasoning",
label:
props.isStreaming && e?.reasoning_running ? (
<div className="flex items-center gap-2">
<span className="italic">
{t("reasoning.thinking")}
</span>
</div>
) : (
t("reasoning.thought", {
time: humanizeMilliseconds(
props.reasoningTimeTaken
)
})
),
children: <Markdown message={e.content} />
}
]}
/>
)
}
return <Markdown key={i} message={e.content} />
})}
</>
) : ( ) : (
<p <p
className={`prose dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${ className={`prose dark:prose-invert whitespace-pre-line prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark ${
@ -220,8 +255,8 @@ export const PlaygroundMessage = (props: Props) => {
} }
title={t("generationInfo")}> title={t("generationInfo")}>
<button <button
aria-label={t("generationInfo")} aria-label={t("generationInfo")}
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"> 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" /> <InfoIcon className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />
</button> </button>
</Popover> </Popover>
@ -231,7 +266,7 @@ export const PlaygroundMessage = (props: Props) => {
props.currentMessageIndex === props.totalMessages - 1 && ( props.currentMessageIndex === props.totalMessages - 1 && (
<Tooltip title={t("regenerate")}> <Tooltip title={t("regenerate")}>
<button <button
aria-label={t("regenerate")} aria-label={t("regenerate")}
onClick={props.onRengerate} onClick={props.onRengerate}
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"> 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">
<RotateCcw className="w-3 h-3 text-gray-400 group-hover:text-gray-500" /> <RotateCcw className="w-3 h-3 text-gray-400 group-hover:text-gray-500" />

View File

@ -55,6 +55,8 @@ export const PlaygroundChat = () => {
}} }}
isTTSEnabled={ttsEnabled} isTTSEnabled={ttsEnabled}
generationInfo={message?.generationInfo} generationInfo={message?.generationInfo}
isStreaming={streaming}
reasoningTimeTaken={message?.reasoning_time_taken}
/> />
))} ))}
{messages.length > 0 && ( {messages.length > 0 && (

View File

@ -48,6 +48,8 @@ export const SidePanelBody = () => {
}} }}
isTTSEnabled={ttsEnabled} isTTSEnabled={ttsEnabled}
generationInfo={message?.generationInfo} generationInfo={message?.generationInfo}
isStreaming={streaming}
reasoningTimeTaken={message?.reasoning_time_taken}
/> />
))} ))}
<div className="w-full h-48 flex-shrink-0"></div> <div className="w-full h-48 flex-shrink-0"></div>

View File

@ -32,6 +32,7 @@ type Message = {
sources?: string[] sources?: string[]
search?: WebSearch search?: WebSearch
createdAt: number createdAt: number
reasoning_time_taken?: number
messageType?: string messageType?: string
generationInfo?: any generationInfo?: any
} }
@ -256,7 +257,8 @@ export const saveMessage = async (
source?: any[], source?: any[],
time?: number, time?: number,
message_type?: string, message_type?: string,
generationInfo?: any generationInfo?: any,
reasoning_time_taken?: number
) => { ) => {
const id = generateID() const id = generateID()
let createdAt = Date.now() let createdAt = Date.now()
@ -273,7 +275,8 @@ export const saveMessage = async (
createdAt, createdAt,
sources: source, sources: source,
messageType: message_type, messageType: message_type,
generationInfo: generationInfo generationInfo: generationInfo,
reasoning_time_taken
} }
const db = new PageAssitDatabase() const db = new PageAssitDatabase()
await db.addMessage(message) await db.addMessage(message)
@ -303,6 +306,7 @@ export const formatToMessage = (messages: MessageHistory): MessageType[] => {
sources: message?.sources || [], sources: message?.sources || [],
images: message.images || [], images: message.images || [],
generationInfo: message?.generationInfo, generationInfo: message?.generationInfo,
reasoning_time_taken: message?.reasoning_time_taken
} }
}) })
} }

View File

@ -130,7 +130,8 @@ export const saveMessageOnSuccess = async ({
message_source = "web-ui", message_source = "web-ui",
message_type, generationInfo, message_type, generationInfo,
prompt_id, prompt_id,
prompt_content prompt_content,
reasoning_time_taken = 0
}: { }: {
historyId: string | null historyId: string | null
setHistoryId: (historyId: string) => void setHistoryId: (historyId: string) => void
@ -145,6 +146,7 @@ export const saveMessageOnSuccess = async ({
generationInfo?: any generationInfo?: any
prompt_id?: string prompt_id?: string
prompt_content?: string prompt_content?: string
reasoning_time_taken?: number
}) => { }) => {
if (historyId) { if (historyId) {
if (!isRegenerate) { if (!isRegenerate) {
@ -157,7 +159,8 @@ export const saveMessageOnSuccess = async ({
[], [],
1, 1,
message_type, message_type,
generationInfo generationInfo,
reasoning_time_taken
) )
} }
await saveMessage( await saveMessage(
@ -169,7 +172,8 @@ export const saveMessageOnSuccess = async ({
source, source,
2, 2,
message_type, message_type,
generationInfo generationInfo,
reasoning_time_taken
) )
await setLastUsedChatModel(historyId, selectedModel!) await setLastUsedChatModel(historyId, selectedModel!)
if (prompt_id || prompt_content) { if (prompt_id || prompt_content) {
@ -187,7 +191,8 @@ export const saveMessageOnSuccess = async ({
[], [],
1, 1,
message_type, message_type,
generationInfo generationInfo,
reasoning_time_taken
) )
await saveMessage( await saveMessage(
newHistoryId.id, newHistoryId.id,
@ -198,7 +203,8 @@ export const saveMessageOnSuccess = async ({
source, source,
2, 2,
message_type, message_type,
generationInfo generationInfo,
reasoning_time_taken
) )
setHistoryId(newHistoryId.id) setHistoryId(newHistoryId.id)
await setLastUsedChatModel(newHistoryId.id, selectedModel!) await setLastUsedChatModel(newHistoryId.id, selectedModel!)

View File

@ -36,6 +36,7 @@ import { humanMessageFormatter } from "@/utils/human-message"
import { pageAssistEmbeddingModel } from "@/models/embedding" import { pageAssistEmbeddingModel } from "@/models/embedding"
import { PAMemoryVectorStore } from "@/libs/PAMemoryVectorStore" import { PAMemoryVectorStore } from "@/libs/PAMemoryVectorStore"
import { getScreenshotFromCurrentTab } from "@/libs/get-screenshot" import { getScreenshotFromCurrentTab } from "@/libs/get-screenshot"
import { isReasoningEnded, isReasoningStarted, removeReasoning } from "@/libs/reasoning"
export const useMessage = () => { export const useMessage = () => {
const { const {
@ -55,14 +56,9 @@ export const useMessage = () => {
setWebSearch, setWebSearch,
isSearchingInternet isSearchingInternet
} = useStoreMessageOption() } = useStoreMessageOption()
const [defaultInternetSearchOn, ] = useStorage( const [defaultInternetSearchOn] = useStorage("defaultInternetSearchOn", false)
"defaultInternetSearchOn",
false
)
const [ const [defaultChatWithWebsite] = useStorage("defaultChatWithWebsite", false)
defaultChatWithWebsite,
] = useStorage("defaultChatWithWebsite", false)
const [chatWithWebsiteEmbedding] = useStorage( const [chatWithWebsiteEmbedding] = useStorage(
"chatWithWebsiteEmbedding", "chatWithWebsiteEmbedding",
@ -115,10 +111,10 @@ export const useMessage = () => {
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
currentChatModelSettings.reset() currentChatModelSettings.reset()
if(defaultInternetSearchOn) { if (defaultInternetSearchOn) {
setWebSearch(true) setWebSearch(true)
} }
if(defaultChatWithWebsite) { if (defaultChatWithWebsite) {
setChatMode("rag") setChatMode("rag")
} }
} }
@ -329,6 +325,7 @@ export const useMessage = () => {
}) })
const response = await questionOllama.invoke(promptForQuestion) const response = await questionOllama.invoke(promptForQuestion)
query = response.content.toString() query = response.content.toString()
query = removeReasoning(query)
} }
let context: string = "" let context: string = ""
@ -413,18 +410,36 @@ export const useMessage = () => {
} }
) )
let count = 0 let count = 0
let reasoningStartTime: Date | null = null
let reasoningEndTime: Date | null = null
let timetaken = 0
for await (const chunk of chunks) { for await (const chunk of chunks) {
contentToSave += chunk?.content contentToSave += chunk?.content
fullText += chunk?.content fullText += chunk?.content
if (count === 0) { if (count === 0) {
setIsProcessing(true) setIsProcessing(true)
} }
if (isReasoningStarted(fullText) && !reasoningStartTime) {
reasoningStartTime = new Date()
}
if (
reasoningStartTime &&
!reasoningEndTime &&
isReasoningEnded(fullText)
) {
reasoningEndTime = new Date()
const reasoningTime =
reasoningEndTime.getTime() - reasoningStartTime.getTime()
timetaken = reasoningTime
}
setMessages((prev) => { setMessages((prev) => {
return prev.map((message) => { return prev.map((message) => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText + "▋" message: fullText + "▋",
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -440,7 +455,8 @@ export const useMessage = () => {
...message, ...message,
message: fullText, message: fullText,
sources: source, sources: source,
generationInfo generationInfo,
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -470,7 +486,8 @@ export const useMessage = () => {
fullText, fullText,
source, source,
message_source: "copilot", message_source: "copilot",
generationInfo generationInfo,
reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)
@ -664,18 +681,36 @@ export const useMessage = () => {
} }
) )
let count = 0 let count = 0
let reasoningStartTime: Date | undefined = undefined
let reasoningEndTime: Date | undefined = undefined
let timetaken = 0
for await (const chunk of chunks) { for await (const chunk of chunks) {
contentToSave += chunk?.content contentToSave += chunk?.content
fullText += chunk?.content fullText += chunk?.content
if (count === 0) { if (count === 0) {
setIsProcessing(true) setIsProcessing(true)
} }
if (isReasoningStarted(fullText) && !reasoningStartTime) {
reasoningStartTime = new Date()
}
if (
reasoningStartTime &&
!reasoningEndTime &&
isReasoningEnded(fullText)
) {
reasoningEndTime = new Date()
const reasoningTime =
reasoningEndTime.getTime() - reasoningStartTime.getTime()
timetaken = reasoningTime
}
setMessages((prev) => { setMessages((prev) => {
return prev.map((message) => { return prev.map((message) => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText + "▋" message: fullText + "▋",
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -689,7 +724,8 @@ export const useMessage = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
generationInfo generationInfo,
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -718,7 +754,8 @@ export const useMessage = () => {
fullText, fullText,
source: [], source: [],
message_source: "copilot", message_source: "copilot",
generationInfo generationInfo,
reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)
@ -914,18 +951,37 @@ export const useMessage = () => {
} }
) )
let count = 0 let count = 0
let reasoningStartTime: Date | null = null
let reasoningEndTime: Date | null = null
let timetaken = 0
for await (const chunk of chunks) { for await (const chunk of chunks) {
contentToSave += chunk?.content contentToSave += chunk?.content
fullText += chunk?.content fullText += chunk?.content
if (count === 0) { if (count === 0) {
setIsProcessing(true) setIsProcessing(true)
} }
if (isReasoningStarted(fullText) && !reasoningStartTime) {
reasoningStartTime = new Date()
}
if (
reasoningStartTime &&
!reasoningEndTime &&
isReasoningEnded(fullText)
) {
reasoningEndTime = new Date()
const reasoningTime =
reasoningEndTime.getTime() - reasoningStartTime.getTime()
timetaken = reasoningTime
}
setMessages((prev) => { setMessages((prev) => {
return prev.map((message) => { return prev.map((message) => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText + "▋" message: fullText + "▋",
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -940,7 +996,8 @@ export const useMessage = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
generationInfo generationInfo,
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -970,7 +1027,8 @@ export const useMessage = () => {
fullText, fullText,
source: [], source: [],
message_source: "copilot", message_source: "copilot",
generationInfo generationInfo,
reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)
@ -1158,6 +1216,7 @@ export const useMessage = () => {
}) })
const response = await questionOllama.invoke(promptForQuestion) const response = await questionOllama.invoke(promptForQuestion)
query = response.content.toString() query = response.content.toString()
query = removeReasoning(query)
} }
const { prompt, source } = await getSystemPromptForWeb(query) const { prompt, source } = await getSystemPromptForWeb(query)
@ -1221,18 +1280,37 @@ export const useMessage = () => {
} }
) )
let count = 0 let count = 0
let timetaken = 0
let reasoningStartTime: Date | undefined = undefined
let reasoningEndTime: Date | undefined = undefined
for await (const chunk of chunks) { for await (const chunk of chunks) {
contentToSave += chunk?.content contentToSave += chunk?.content
fullText += chunk?.content fullText += chunk?.content
if (count === 0) { if (count === 0) {
setIsProcessing(true) setIsProcessing(true)
} }
if (isReasoningStarted(fullText) && !reasoningStartTime) {
reasoningStartTime = new Date()
}
if (
reasoningStartTime &&
!reasoningEndTime &&
isReasoningEnded(fullText)
) {
reasoningEndTime = new Date()
const reasoningTime =
reasoningEndTime.getTime() - reasoningStartTime.getTime()
timetaken = reasoningTime
}
setMessages((prev) => { setMessages((prev) => {
return prev.map((message) => { return prev.map((message) => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText + "▋" message: fullText + "▋",
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -1248,7 +1326,8 @@ export const useMessage = () => {
...message, ...message,
message: fullText, message: fullText,
sources: source, sources: source,
generationInfo generationInfo,
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -1277,7 +1356,8 @@ export const useMessage = () => {
image, image,
fullText, fullText,
source, source,
generationInfo generationInfo,
reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)
@ -1448,18 +1528,36 @@ export const useMessage = () => {
] ]
}) })
let count = 0 let count = 0
let reasoningStartTime: Date | null = null
let reasoningEndTime: Date | null = null
let timetaken = 0
for await (const chunk of chunks) { for await (const chunk of chunks) {
contentToSave += chunk?.content contentToSave += chunk?.content
fullText += chunk?.content fullText += chunk?.content
if (count === 0) { if (count === 0) {
setIsProcessing(true) setIsProcessing(true)
} }
if (isReasoningStarted(fullText) && !reasoningStartTime) {
reasoningStartTime = new Date()
}
if (
reasoningStartTime &&
!reasoningEndTime &&
isReasoningEnded(fullText)
) {
reasoningEndTime = new Date()
const reasoningTime =
reasoningEndTime.getTime() - reasoningStartTime.getTime()
timetaken = reasoningTime
}
setMessages((prev) => { setMessages((prev) => {
return prev.map((message) => { return prev.map((message) => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText + "▋" message: fullText + "▋",
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -1474,7 +1572,8 @@ export const useMessage = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
generationInfo generationInfo,
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -1506,7 +1605,8 @@ export const useMessage = () => {
source: [], source: [],
message_source: "copilot", message_source: "copilot",
message_type: messageType, message_type: messageType,
generationInfo generationInfo,
reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)

View File

@ -37,6 +37,11 @@ import { pageAssistModel } from "@/models"
import { getNoOfRetrievedDocs } from "@/services/app" import { getNoOfRetrievedDocs } from "@/services/app"
import { humanMessageFormatter } from "@/utils/human-message" import { humanMessageFormatter } from "@/utils/human-message"
import { pageAssistEmbeddingModel } from "@/models/embedding" import { pageAssistEmbeddingModel } from "@/models/embedding"
import {
isReasoningEnded,
isReasoningStarted,
removeReasoning
} from "@/libs/reasoning"
export const useMessageOption = () => { export const useMessageOption = () => {
const { const {
@ -76,10 +81,7 @@ export const useMessageOption = () => {
} = useStoreMessageOption() } = useStoreMessageOption()
const currentChatModelSettings = useStoreChatModelSettings() const currentChatModelSettings = useStoreChatModelSettings()
const [selectedModel, setSelectedModel] = useStorage("selectedModel") const [selectedModel, setSelectedModel] = useStorage("selectedModel")
const [defaultInternetSearchOn, ] = useStorage( const [defaultInternetSearchOn] = useStorage("defaultInternetSearchOn", false)
"defaultInternetSearchOn",
false
)
const [speechToTextLanguage, setSpeechToTextLanguage] = useStorage( const [speechToTextLanguage, setSpeechToTextLanguage] = useStorage(
"speechToTextLanguage", "speechToTextLanguage",
"en-US" "en-US"
@ -102,7 +104,7 @@ export const useMessageOption = () => {
setStreaming(false) setStreaming(false)
currentChatModelSettings.reset() currentChatModelSettings.reset()
textareaRef?.current?.focus() textareaRef?.current?.focus()
if(defaultInternetSearchOn) { if (defaultInternetSearchOn) {
setWebSearch(true) setWebSearch(true)
} }
} }
@ -195,6 +197,7 @@ export const useMessageOption = () => {
setMessages(newMessage) setMessages(newMessage)
let fullText = "" let fullText = ""
let contentToSave = "" let contentToSave = ""
let timetaken = 0
try { try {
setIsSearchingInternet(true) setIsSearchingInternet(true)
@ -261,6 +264,7 @@ export const useMessageOption = () => {
}) })
const response = await questionOllama.invoke(promptForQuestion) const response = await questionOllama.invoke(promptForQuestion)
query = response.content.toString() query = response.content.toString()
query = removeReasoning(query)
} }
const { prompt, source } = await getSystemPromptForWeb(query) const { prompt, source } = await getSystemPromptForWeb(query)
@ -325,18 +329,35 @@ export const useMessageOption = () => {
} }
) )
let count = 0 let count = 0
let reasoningStartTime: Date | undefined = undefined
let reasoningEndTime: Date | undefined = undefined
for await (const chunk of chunks) { for await (const chunk of chunks) {
contentToSave += chunk?.content contentToSave += chunk?.content
fullText += chunk?.content fullText += chunk?.content
if (count === 0) { if (count === 0) {
setIsProcessing(true) setIsProcessing(true)
} }
if (isReasoningStarted(fullText) && !reasoningStartTime) {
reasoningStartTime = new Date()
}
if (
reasoningStartTime &&
!reasoningEndTime &&
isReasoningEnded(fullText)
) {
reasoningEndTime = new Date()
const reasoningTime =
reasoningEndTime.getTime() - reasoningStartTime.getTime()
timetaken = reasoningTime
}
setMessages((prev) => { setMessages((prev) => {
return prev.map((message) => { return prev.map((message) => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText + "▋" message: fullText + "▋",
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -352,7 +373,8 @@ export const useMessageOption = () => {
...message, ...message,
message: fullText, message: fullText,
sources: source, sources: source,
generationInfo generationInfo,
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -381,7 +403,8 @@ export const useMessageOption = () => {
image, image,
fullText, fullText,
source, source,
generationInfo generationInfo,
reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)
@ -537,6 +560,7 @@ export const useMessageOption = () => {
setMessages(newMessage) setMessages(newMessage)
let fullText = "" let fullText = ""
let contentToSave = "" let contentToSave = ""
let timetaken = 0
try { try {
const prompt = await systemPromptForNonRagOption() const prompt = await systemPromptForNonRagOption()
@ -622,9 +646,28 @@ export const useMessageOption = () => {
) )
let count = 0 let count = 0
let reasoningStartTime: Date | null = null
let reasoningEndTime: Date | null = null
for await (const chunk of chunks) { for await (const chunk of chunks) {
contentToSave += chunk?.content contentToSave += chunk?.content
fullText += chunk?.content fullText += chunk?.content
if (isReasoningStarted(fullText) && !reasoningStartTime) {
reasoningStartTime = new Date()
}
if (
reasoningStartTime &&
!reasoningEndTime &&
isReasoningEnded(fullText)
) {
reasoningEndTime = new Date()
const reasoningTime =
reasoningEndTime.getTime() - reasoningStartTime.getTime()
timetaken = reasoningTime
}
if (count === 0) { if (count === 0) {
setIsProcessing(true) setIsProcessing(true)
} }
@ -633,7 +676,8 @@ export const useMessageOption = () => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText + "▋" message: fullText + "▋",
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -648,7 +692,8 @@ export const useMessageOption = () => {
return { return {
...message, ...message,
message: fullText, message: fullText,
generationInfo generationInfo,
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -679,7 +724,8 @@ export const useMessageOption = () => {
source: [], source: [],
generationInfo, generationInfo,
prompt_content: promptContent, prompt_content: promptContent,
prompt_id: promptId prompt_id: promptId,
reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)
@ -818,7 +864,7 @@ export const useMessageOption = () => {
knownledge_id: selectedKnowledge.id knownledge_id: selectedKnowledge.id
} }
) )
let timetaken = 0
try { try {
let query = message let query = message
const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } = const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =
@ -882,6 +928,7 @@ export const useMessageOption = () => {
}) })
const response = await questionOllama.invoke(promptForQuestion) const response = await questionOllama.invoke(promptForQuestion)
query = response.content.toString() query = response.content.toString()
query = removeReasoning(query)
} }
const docSize = await getNoOfRetrievedDocs() const docSize = await getNoOfRetrievedDocs()
@ -933,18 +980,36 @@ export const useMessageOption = () => {
} }
) )
let count = 0 let count = 0
let reasoningStartTime: Date | undefined = undefined
let reasoningEndTime: Date | undefined = undefined
for await (const chunk of chunks) { for await (const chunk of chunks) {
contentToSave += chunk?.content contentToSave += chunk?.content
fullText += chunk?.content fullText += chunk?.content
if (count === 0) { if (count === 0) {
setIsProcessing(true) setIsProcessing(true)
} }
if (isReasoningStarted(fullText) && !reasoningStartTime) {
reasoningStartTime = new Date()
}
if (
reasoningStartTime &&
!reasoningEndTime &&
isReasoningEnded(fullText)
) {
reasoningEndTime = new Date()
const reasoningTime =
reasoningEndTime.getTime() - reasoningStartTime.getTime()
timetaken = reasoningTime
}
setMessages((prev) => { setMessages((prev) => {
return prev.map((message) => { return prev.map((message) => {
if (message.id === generateMessageId) { if (message.id === generateMessageId) {
return { return {
...message, ...message,
message: fullText + "▋" message: fullText + "▋",
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -960,7 +1025,8 @@ export const useMessageOption = () => {
...message, ...message,
message: fullText, message: fullText,
sources: source, sources: source,
generationInfo generationInfo,
reasoning_time_taken: timetaken
} }
} }
return message return message
@ -989,7 +1055,8 @@ export const useMessageOption = () => {
image, image,
fullText, fullText,
source, source,
generationInfo generationInfo,
reasoning_time_taken: timetaken
}) })
setIsProcessing(false) setIsProcessing(false)
@ -1206,6 +1273,6 @@ export const useMessageOption = () => {
setTemporaryChat, setTemporaryChat,
useOCR, useOCR,
setUseOCR, setUseOCR,
defaultInternetSearchOn, defaultInternetSearchOn
} }
} }

73
src/libs/reasoning.ts Normal file
View File

@ -0,0 +1,73 @@
const tags = ["think", "reason", "reasoning", "thought"];
export function parseReasoning(text: string): { type: 'reasoning' | 'text', content: string, reasoning_running?: boolean }[] {
try {
const result: { type: 'reasoning' | 'text', content: string, reasoning_running?: boolean }[] = []
const tagPattern = new RegExp(`<(${tags.join('|')})>`, 'i')
const closeTagPattern = new RegExp(`</(${tags.join('|')})>`, 'i')
let currentIndex = 0
let isReasoning = false
while (currentIndex < text.length) {
const openTagMatch = text.slice(currentIndex).match(tagPattern)
const closeTagMatch = text.slice(currentIndex).match(closeTagPattern)
if (!isReasoning && openTagMatch) {
const beforeText = text.slice(currentIndex, currentIndex + openTagMatch.index)
if (beforeText.trim()) {
result.push({ type: 'text', content: beforeText.trim() })
}
isReasoning = true
currentIndex += openTagMatch.index! + openTagMatch[0].length
continue
}
if (isReasoning && closeTagMatch) {
const reasoningContent = text.slice(currentIndex, currentIndex + closeTagMatch.index)
if (reasoningContent.trim()) {
result.push({ type: 'reasoning', content: reasoningContent.trim() })
}
isReasoning = false
currentIndex += closeTagMatch.index! + closeTagMatch[0].length
continue
}
if (currentIndex < text.length) {
const remainingText = text.slice(currentIndex)
result.push({
type: isReasoning ? 'reasoning' : 'text',
content: remainingText.trim(),
reasoning_running: isReasoning
})
break
}
}
return result
} catch (e) {
console.log(`Error parsing reasoning: ${e}`)
return [
{
type: 'text',
content: text
}
]
}
}
export function isReasoningStarted(text: string): boolean {
const tagPattern = new RegExp(`<(${tags.join('|')})>`, 'i')
return tagPattern.test(text)
}
export function isReasoningEnded(text: string): boolean {
const closeTagPattern = new RegExp(`</(${tags.join('|')})>`, 'i')
return closeTagPattern.test(text)
}
export function removeReasoning(text: string): string {
const tagPattern = new RegExp(`<(${tags.join('|')})>.*?</(${tags.join('|')})>`, 'gis')
return text.replace(tagPattern, '').trim()
}

View File

@ -17,6 +17,7 @@ export type Message = {
sources: any[] sources: any[]
images?: string[] images?: string[]
search?: WebSearch search?: WebSearch
reasoning_time_taken?: number
id?: string id?: string
messageType?: string messageType?: string
} }

View File

@ -17,4 +17,5 @@ export type Message = {
messageType?: string messageType?: string
id?: string id?: string
generationInfo?: any generationInfo?: any
reasoning_time_taken?: number
} }

View File

@ -0,0 +1,30 @@
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
dayjs.extend(duration)
export const humanizeMilliseconds = (milliseconds: number): string => {
try {
const duration = dayjs.duration(milliseconds)
if (milliseconds < 1000) {
return `${milliseconds}ms`
}
if (milliseconds < 60000) {
return `${Math.floor(duration.asSeconds())}s`
}
if (milliseconds < 3600000) {
return `${Math.floor(duration.asMinutes())}m`
}
if (milliseconds < 86400000) {
return `${Math.floor(duration.asHours())}h`
}
return `${Math.floor(duration.asDays())}d`
} catch (e) {
return `${milliseconds}ms`
}
}

View File

@ -51,7 +51,7 @@ export default defineConfig({
outDir: "build", outDir: "build",
manifest: { manifest: {
version: "1.4.3", version: "1.4.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"