diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json
index fb9be262..1aa5059f 100644
--- a/src/renderer/src/i18n/locales/en-us.json
+++ b/src/renderer/src/i18n/locales/en-us.json
@@ -113,7 +113,9 @@
"topics.move_to": "Move to",
"topics.title": "Topics",
"translate": "Translate",
- "resend": "Resend"
+ "resend": "Resend",
+ "thinking": "Thinking",
+ "deeply_thought": "Deeply thought ({{secounds}} seconds)"
},
"common": {
"and": "and",
diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json
index 2129cb61..748d5079 100644
--- a/src/renderer/src/i18n/locales/ja-jp.json
+++ b/src/renderer/src/i18n/locales/ja-jp.json
@@ -113,7 +113,9 @@
"topics.move_to": "移動先",
"topics.title": "トピック",
"translate": "翻訳",
- "resend": "再送信"
+ "resend": "再送信",
+ "thinking": "思考中...",
+ "deeply_thought": "深く考えています({{secounds}} 秒)"
},
"common": {
"and": "と",
@@ -260,7 +262,8 @@
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
"upgrade.success.title": "アップグレードに成功しました",
"regenerate.confirm": "再生成すると現在のメッセージが置き換えられます",
- "copy.success": "コピーしました!"
+ "copy.success": "コピーしました!",
+ "error.get_embedding_dimensions": "埋込み次元を取得できませんでした"
},
"minapp": {
"title": "ミニアプリ",
diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json
index 8db41968..1b962735 100644
--- a/src/renderer/src/i18n/locales/ru-ru.json
+++ b/src/renderer/src/i18n/locales/ru-ru.json
@@ -113,7 +113,9 @@
"topics.move_to": "Переместить в",
"topics.title": "Топики",
"translate": "Перевести",
- "resend": "Переотправить"
+ "resend": "Переотправить",
+ "thinking": "Мыслим",
+ "deeply_thought": "Мыслим ({{secounds}} секунд)"
},
"common": {
"and": "и",
diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json
index 3b02384d..d9fe9f19 100644
--- a/src/renderer/src/i18n/locales/zh-cn.json
+++ b/src/renderer/src/i18n/locales/zh-cn.json
@@ -113,7 +113,9 @@
"topics.move_to": "移动到",
"topics.title": "话题",
"translate": "翻译",
- "resend": "重新发送"
+ "resend": "重新发送",
+ "thinking": "思考中",
+ "deeply_thought": "已深度思考(用时 {{secounds}} 秒)"
},
"common": {
"and": "和",
diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json
index 870136d9..249ce35d 100644
--- a/src/renderer/src/i18n/locales/zh-tw.json
+++ b/src/renderer/src/i18n/locales/zh-tw.json
@@ -113,7 +113,9 @@
"topics.move_to": "移動到",
"topics.title": "話題",
"translate": "翻譯",
- "resend": "重新發送"
+ "resend": "重新發送",
+ "thinking": "思考中",
+ "deeply_thought": "已深度思考(用時 {{secounds}} 秒)"
},
"common": {
"and": "與",
diff --git a/src/renderer/src/pages/agents/AgentsPage.tsx b/src/renderer/src/pages/agents/AgentsPage.tsx
index 138f02bb..a8defebe 100644
--- a/src/renderer/src/pages/agents/AgentsPage.tsx
+++ b/src/renderer/src/pages/agents/AgentsPage.tsx
@@ -292,13 +292,14 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')};
user-select: none;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
-
+ outline: none !important;
.ant-tabs-tab-btn {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100px;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+ outline: none !important;
}
&:hover {
color: var(--color-text) !important;
diff --git a/src/renderer/src/pages/home/Messages/MessageContent.tsx b/src/renderer/src/pages/home/Messages/MessageContent.tsx
index 769e6a41..b18ac698 100644
--- a/src/renderer/src/pages/home/Messages/MessageContent.tsx
+++ b/src/renderer/src/pages/home/Messages/MessageContent.tsx
@@ -11,6 +11,7 @@ import Markdown from '../Markdown/Markdown'
import MessageAttachments from './MessageAttachments'
import MessageError from './MessageError'
import MessageSearchResults from './MessageSearchResults'
+import MessageThought from './MessageThought'
const MessageContent: React.FC<{
message: Message
@@ -40,6 +41,7 @@ const MessageContent: React.FC<{
{message.mentions?.map((model) => {'@' + model.name})}
+
{message.translatedContent && (
<>
diff --git a/src/renderer/src/pages/home/Messages/MessageThought.tsx b/src/renderer/src/pages/home/Messages/MessageThought.tsx
new file mode 100644
index 00000000..524ea2c8
--- /dev/null
+++ b/src/renderer/src/pages/home/Messages/MessageThought.tsx
@@ -0,0 +1,60 @@
+import { Message } from '@renderer/types'
+import { Collapse } from 'antd'
+import { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import ReactMarkdown from 'react-markdown'
+import BarLoader from 'react-spinners/BarLoader'
+import styled from 'styled-components'
+
+interface Props {
+ message: Message
+}
+
+const MessageThought: FC = ({ message }) => {
+ const isThinking = !message.content
+ const { t } = useTranslation()
+
+ if (!message.reasoning_content) {
+ return null
+ }
+
+ const thinkingTime = message.metrics?.time_thinking_millsec || 0
+ const thinkingTimeSecounds = (thinkingTime / 1000).toFixed(1)
+
+ return (
+
+
+ {isThinking ? t('chat.thinking') : t('chat.deeply_thought', { secounds: thinkingTimeSecounds })}
+
+ {isThinking && }
+
+ ),
+ children: {message.reasoning_content}
+ }
+ ]}
+ />
+ )
+}
+
+const CollapseContainer = styled(Collapse)`
+ margin-bottom: 15px;
+`
+
+const MessageTitleLabel = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ height: 22px;
+ gap: 15px;
+`
+
+const TinkingText = styled.span`
+ color: var(--color-text-2);
+`
+
+export default MessageThought
diff --git a/src/renderer/src/providers/BaseProvider.ts b/src/renderer/src/providers/BaseProvider.ts
index 29cdb5c2..e359e376 100644
--- a/src/renderer/src/providers/BaseProvider.ts
+++ b/src/renderer/src/providers/BaseProvider.ts
@@ -3,7 +3,7 @@ import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
import { getKnowledgeReferences } from '@renderer/services/KnowledgeService'
import store from '@renderer/store'
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
-import { delay, isJSON } from '@renderer/utils'
+import { delay, isJSON, parseJSON } from '@renderer/utils'
import OpenAI from 'openai'
import { CompletionsParams } from '.'
@@ -98,9 +98,15 @@ export default abstract class BaseProvider {
}
if (param.type === 'json') {
const value = param.value as string
- return { ...acc, [param.name]: isJSON(value) ? JSON.parse(value) : value }
+ return {
+ ...acc,
+ [param.name]: isJSON(value) ? parseJSON(value) : value
+ }
+ }
+ return {
+ ...acc,
+ [param.name]: param.value
}
- return { ...acc, [param.name]: param.value }
}, {}) || {}
)
}
diff --git a/src/renderer/src/providers/OpenAIProvider.ts b/src/renderer/src/providers/OpenAIProvider.ts
index 84e0005e..2dd95c7c 100644
--- a/src/renderer/src/providers/OpenAIProvider.ts
+++ b/src/renderer/src/providers/OpenAIProvider.ts
@@ -117,6 +117,20 @@ export default class OpenAIProvider extends BaseProvider {
} as ChatCompletionMessageParam
}
+ private getTemperature(assistant: Assistant, model: Model) {
+ const isOpenAIo1 = model.id.startsWith('o1')
+
+ if (isOpenAIo1) {
+ return undefined
+ }
+
+ if (model.provider === 'deepseek' && model.id === 'deepseek-reasoner') {
+ return undefined
+ }
+
+ return assistant?.settings?.temperature
+ }
+
async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise {
const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel
@@ -128,6 +142,12 @@ export default class OpenAIProvider extends BaseProvider {
const _messages = filterContextMessages(takeRight(messages, contextCount + 1))
onFilterMessages(_messages)
+ if (model.id === 'deepseek-reasoner') {
+ if (_messages[0]?.role !== 'user') {
+ userMessages.push({ role: 'user', content: '' })
+ }
+ }
+
for (const message of _messages) {
userMessages.push(await this.getMessageParam(message, model))
}
@@ -142,6 +162,7 @@ export default class OpenAIProvider extends BaseProvider {
}
let time_first_token_millsec = 0
+ let time_first_content_millsec = 0
const start_time_millsec = new Date().getTime()
// @ts-ignore key is not typed
@@ -150,7 +171,7 @@ export default class OpenAIProvider extends BaseProvider {
messages: [isOpenAIo1 ? undefined : systemMessage, ...userMessages].filter(
Boolean
) as ChatCompletionMessageParam[],
- temperature: isOpenAIo1 ? 1 : assistant?.settings?.temperature,
+ temperature: this.getTemperature(assistant, model),
top_p: assistant?.settings?.topP,
max_tokens: maxTokens,
keep_alive: this.keepAliveTime,
@@ -176,17 +197,28 @@ export default class OpenAIProvider extends BaseProvider {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
break
}
+
if (time_first_token_millsec == 0) {
time_first_token_millsec = new Date().getTime() - start_time_millsec
}
+
+ if (time_first_content_millsec == 0 && chunk.choices[0]?.delta?.content) {
+ time_first_content_millsec = new Date().getTime()
+ }
+
const time_completion_millsec = new Date().getTime() - start_time_millsec
+ const time_thinking_millsec = time_first_content_millsec ? time_first_content_millsec - start_time_millsec : 0
+
onChunk({
text: chunk.choices[0]?.delta?.content || '',
+ // @ts-ignore key is not typed
+ reasoning_content: chunk.choices[0]?.delta?.reasoning_content || '',
usage: chunk.usage,
metrics: {
completion_tokens: chunk.usage?.completion_tokens,
time_completion_millsec,
- time_first_token_millsec
+ time_first_token_millsec,
+ time_thinking_millsec
}
})
}
diff --git a/src/renderer/src/providers/index.d.ts b/src/renderer/src/providers/index.d.ts
index 58d564b4..186a1002 100644
--- a/src/renderer/src/providers/index.d.ts
+++ b/src/renderer/src/providers/index.d.ts
@@ -1,8 +1,9 @@
import type { GroundingMetadata } from '@google/generative-ai'
-import type { Assistant, Metrics } from '@renderer/types'
+import type { Assistant, Message, Metrics } from '@renderer/types'
interface ChunkCallbackData {
text?: string
+ reasoning_content?: string
usage?: OpenAI.Completions.CompletionUsage
metrics?: Metrics
search?: GroundingMetadata
@@ -11,6 +12,6 @@ interface ChunkCallbackData {
interface CompletionsParams {
messages: Message[]
assistant: Assistant
- onChunk: ({ text, usage, metrics, search }: ChunkCallbackData) => void
+ onChunk: ({ text, reasoning_content, usage, metrics, search }: ChunkCallbackData) => void
onFilterMessages: (messages: Message[]) => void
}
diff --git a/src/renderer/src/services/ApiService.ts b/src/renderer/src/services/ApiService.ts
index 47ab6ffc..4e7a7dfc 100644
--- a/src/renderer/src/services/ApiService.ts
+++ b/src/renderer/src/services/ApiService.ts
@@ -56,11 +56,15 @@ export async function fetchChatCompletion({
messages,
assistant,
onFilterMessages: (messages) => (_messages = messages),
- onChunk: ({ text, usage, metrics, search }) => {
+ onChunk: ({ text, reasoning_content, usage, metrics, search }) => {
message.content = message.content + text || ''
message.usage = usage
message.metrics = metrics
+ if (reasoning_content) {
+ message.reasoning_content = (message.reasoning_content || '') + reasoning_content
+ }
+
if (search) {
message.metadata = { groundingMetadata: search }
}
diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts
index 0512452c..7e239938 100644
--- a/src/renderer/src/types/index.ts
+++ b/src/renderer/src/types/index.ts
@@ -47,6 +47,7 @@ export type Message = {
assistantId: string
role: 'user' | 'assistant'
content: string
+ reasoning_content?: string
translatedContent?: string
topicId: string
createdAt: string
@@ -71,6 +72,7 @@ export type Metrics = {
completion_tokens?: number
time_completion_millsec?: number
time_first_token_millsec?: number
+ time_thinking_millsec?: number
}
export type Topic = {
diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts
index 7e40ae3d..8719b8d5 100644
--- a/src/renderer/src/utils/index.ts
+++ b/src/renderer/src/utils/index.ts
@@ -26,6 +26,18 @@ export function isJSON(str: any): boolean {
}
}
+export function parseJSON(str: string) {
+ if (str === 'undefined') {
+ return undefined
+ }
+
+ try {
+ return JSON.parse(str)
+ } catch (e) {
+ return null
+ }
+}
+
export const delay = (seconds: number) => {
return new Promise((resolve) => {
setTimeout(() => {