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(() => {