From 4d553beb85cdccb651d5e7dfe0c1aa156a4e3154 Mon Sep 17 00:00:00 2001
From: fullex <0xfullex@gmail.com>
Date: Wed, 18 Jun 2025 17:08:43 +0800
Subject: [PATCH] fix: series bugs of quick assistant
---
src/renderer/src/i18n/locales/en-us.json | 3 +-
src/renderer/src/i18n/locales/ja-jp.json | 3 +-
src/renderer/src/i18n/locales/ru-ru.json | 3 +-
src/renderer/src/i18n/locales/zh-cn.json | 3 +-
src/renderer/src/i18n/locales/zh-tw.json | 3 +-
.../settings/ModelSettings/ModelSettings.tsx | 33 +-
.../src/windows/mini/chat/ChatWindow.tsx | 12 +-
.../windows/mini/chat/components/Messages.tsx | 37 +-
.../src/windows/mini/home/HomeWindow.tsx | 542 +++++++++++-------
.../windows/mini/home/components/Footer.tsx | 32 +-
.../windows/mini/home/components/InputBar.tsx | 8 +-
11 files changed, 409 insertions(+), 270 deletions(-)
diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json
index 95d679149c..ad6e134fc5 100644
--- a/src/renderer/src/i18n/locales/en-us.json
+++ b/src/renderer/src/i18n/locales/en-us.json
@@ -755,7 +755,8 @@
"backspace_clear": "Backspace to clear",
"esc": "ESC to {{action}}",
"esc_back": "return",
- "esc_close": "close"
+ "esc_close": "close",
+ "esc_pause": "pause"
},
"input": {
"placeholder": {
diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json
index 96c68e03cd..a19e9e7e3d 100644
--- a/src/renderer/src/i18n/locales/ja-jp.json
+++ b/src/renderer/src/i18n/locales/ja-jp.json
@@ -752,10 +752,11 @@
},
"footer": {
"copy_last_message": "C キーを押してコピー",
+ "backspace_clear": "バックスペースを押してクリアします",
"esc": "ESC キーを押して{{action}}",
"esc_back": "戻る",
"esc_close": "ウィンドウを閉じる",
- "backspace_clear": "バックスペースを押してクリアします"
+ "esc_pause": "一時停止"
},
"input": {
"placeholder": {
diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json
index 4928b69a57..07b6f9afda 100644
--- a/src/renderer/src/i18n/locales/ru-ru.json
+++ b/src/renderer/src/i18n/locales/ru-ru.json
@@ -752,10 +752,11 @@
},
"footer": {
"copy_last_message": "Нажмите C для копирования",
+ "backspace_clear": "Нажмите Backspace, чтобы очистить",
"esc": "Нажмите ESC {{action}}",
"esc_back": "возвращения",
"esc_close": "закрытия окна",
- "backspace_clear": "Нажмите Backspace, чтобы очистить"
+ "esc_pause": "пауза"
},
"input": {
"placeholder": {
diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json
index d85730409c..b815f18aca 100644
--- a/src/renderer/src/i18n/locales/zh-cn.json
+++ b/src/renderer/src/i18n/locales/zh-cn.json
@@ -755,7 +755,8 @@
"backspace_clear": "按 Backspace 清空",
"esc": "按 ESC {{action}}",
"esc_back": "返回",
- "esc_close": "关闭"
+ "esc_close": "关闭",
+ "esc_pause": "暂停"
},
"input": {
"placeholder": {
diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json
index 48d429635e..0cd910e523 100644
--- a/src/renderer/src/i18n/locales/zh-tw.json
+++ b/src/renderer/src/i18n/locales/zh-tw.json
@@ -752,10 +752,11 @@
},
"footer": {
"copy_last_message": "按 C 鍵複製",
+ "backspace_clear": "按 Backspace 清空",
"esc": "按 ESC {{action}}",
"esc_back": "返回",
"esc_close": "關閉視窗",
- "backspace_clear": "按 Backspace 清空"
+ "esc_pause": "暫停"
},
"input": {
"placeholder": {
diff --git a/src/renderer/src/pages/settings/ModelSettings/ModelSettings.tsx b/src/renderer/src/pages/settings/ModelSettings/ModelSettings.tsx
index d61767b3af..b221e713c7 100644
--- a/src/renderer/src/pages/settings/ModelSettings/ModelSettings.tsx
+++ b/src/renderer/src/pages/settings/ModelSettings/ModelSettings.tsx
@@ -188,22 +188,29 @@ const ModelSettings: FC = () => {
{!quickAssistantId ? null : (
)}
diff --git a/src/renderer/src/windows/mini/chat/ChatWindow.tsx b/src/renderer/src/windows/mini/chat/ChatWindow.tsx
index d59169ba6f..ff57494428 100644
--- a/src/renderer/src/windows/mini/chat/ChatWindow.tsx
+++ b/src/renderer/src/windows/mini/chat/ChatWindow.tsx
@@ -1,18 +1,22 @@
import Scrollbar from '@renderer/components/Scrollbar'
-import { Assistant } from '@renderer/types'
+import { Assistant, Topic } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
import Messages from './components/Messages'
interface Props {
route: string
- assistant: Assistant
+ assistant: Assistant | null
+ topic: Topic | null
+ isOutputted: boolean
}
-const ChatWindow: FC = ({ route, assistant }) => {
+const ChatWindow: FC = ({ route, assistant, topic, isOutputted }) => {
+ if (!assistant || !topic) return null
+
return (
-
+
)
}
diff --git a/src/renderer/src/windows/mini/chat/components/Messages.tsx b/src/renderer/src/windows/mini/chat/components/Messages.tsx
index 932446312f..1a0c414aef 100644
--- a/src/renderer/src/windows/mini/chat/components/Messages.tsx
+++ b/src/renderer/src/windows/mini/chat/components/Messages.tsx
@@ -1,9 +1,10 @@
+import { LoadingOutlined } from '@ant-design/icons'
import Scrollbar from '@renderer/components/Scrollbar'
import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
-import { Assistant } from '@renderer/types'
+import { Assistant, Topic } from '@renderer/types'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { last } from 'lodash'
-import { FC, useRef } from 'react'
+import { FC } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -12,40 +13,19 @@ import MessageItem from './Message'
interface Props {
assistant: Assistant
+ topic: Topic
route: string
+ isOutputted: boolean
}
interface ContainerProps {
right?: boolean
}
-const Messages: FC = ({ assistant, route }) => {
- // const [messages, setMessages] = useState([])
- const messages = useTopicMessages(assistant.topics[0].id)
- const containerRef = useRef(null)
- const messagesRef = useRef(messages)
-
+const Messages: FC = ({ assistant, topic, route, isOutputted }) => {
+ const messages = useTopicMessages(topic.id)
const { t } = useTranslation()
- messagesRef.current = messages
-
- // const onSendMessage = useCallback(
- // async (message: Message) => {
- // setMessages((prev) => {
- // const assistantMessage = getAssistantMessage({ assistant, topic: assistant.topics[0] })
- // store.dispatch(newMessagesActions.addMessage({ topicId: assistant.topics[0].id, message: assistantMessage }))
- // const messages = prev.concat([message, assistantMessage])
- // return messages
- // })
- // },
- // [assistant]
- // )
-
- // useEffect(() => {
- // const unsubscribes = [EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)]
- // return () => unsubscribes.forEach((unsub) => unsub())
- // }, [assistant.id])
-
useHotkeys('c', () => {
const lastMessage = last(messages)
if (lastMessage) {
@@ -55,7 +35,8 @@ const Messages: FC = ({ assistant, route }) => {
}
})
return (
-
+
+ {!isOutputted && }
{[...messages].reverse().map((message, index) => (
))}
diff --git a/src/renderer/src/windows/mini/home/HomeWindow.tsx b/src/renderer/src/windows/mini/home/HomeWindow.tsx
index 7ea0fd6959..0339217cbe 100644
--- a/src/renderer/src/windows/mini/home/HomeWindow.tsx
+++ b/src/renderer/src/windows/mini/home/HomeWindow.tsx
@@ -1,27 +1,32 @@
import { isMac } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
-import { useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { fetchChatCompletion } from '@renderer/services/ApiService'
-import { getAssistantById } from '@renderer/services/AssistantService'
+import {
+ getAssistantById,
+ getDefaultAssistant,
+ getDefaultModel,
+ getDefaultTopic
+} from '@renderer/services/AssistantService'
import { getAssistantMessage, getUserMessage } from '@renderer/services/MessagesService'
import store, { useAppSelector } from '@renderer/store'
import { upsertManyBlocks } from '@renderer/store/messageBlock'
import { updateOneBlock, upsertOneBlock } from '@renderer/store/messageBlock'
import { newMessagesActions } from '@renderer/store/newMessage'
-import { Assistant, ThemeMode } from '@renderer/types'
+import { selectMessagesForTopic } from '@renderer/store/newMessage'
+import { Assistant, ThemeMode, Topic } from '@renderer/types'
import { Chunk, ChunkType } from '@renderer/types/chunk'
import { AssistantMessageStatus } from '@renderer/types/newMessage'
import { MessageBlockStatus } from '@renderer/types/newMessage'
-import { createMainTextBlock } from '@renderer/utils/messageUtils/create'
+import { abortCompletion } from '@renderer/utils/abortController'
+import { isAbortError } from '@renderer/utils/error'
+import { createMainTextBlock, createThinkingBlock } from '@renderer/utils/messageUtils/create'
import { defaultLanguage } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { Divider } from 'antd'
-import dayjs from 'dayjs'
import { isEmpty } from 'lodash'
import React, { FC, useCallback, useEffect, useRef, useState } from 'react'
-import { useHotkeys } from 'react-hotkeys-hook'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -33,28 +38,61 @@ import Footer from './components/Footer'
import InputBar from './components/InputBar'
const HomeWindow: FC = () => {
+ const { language, readClipboardAtStartup, windowStyle } = useSettings()
+ const { theme } = useTheme()
+ const { t } = useTranslation()
+
const [route, setRoute] = useState<'home' | 'chat' | 'translate' | 'summary' | 'explanation'>('home')
const [isFirstMessage, setIsFirstMessage] = useState(true)
const [clipboardText, setClipboardText] = useState('')
const [selectedText, setSelectedText] = useState('')
- const [currentAssistant, setCurrentAssistant] = useState({} as Assistant)
- const [text, setText] = useState('')
+
+ const [userInputText, setUserInputText] = useState('')
const [lastClipboardText, setLastClipboardText] = useState(null)
const textChange = useState(() => {})[1]
- const { defaultAssistant } = useDefaultAssistant()
- const topic = defaultAssistant.topics[0]
- const { defaultModel } = useDefaultModel()
- const model = currentAssistant.model || defaultModel
- const { language, readClipboardAtStartup, windowStyle } = useSettings()
- const { theme } = useTheme()
- const { t } = useTranslation()
- const inputBarRef = useRef(null)
- const featureMenusRef = useRef(null)
- const referenceText = selectedText || clipboardText || text
- const content = isFirstMessage ? (referenceText === text ? text : `${referenceText}\n\n${text}`).trim() : text.trim()
+ //indicator for loading(thinking/streaming)
+ const [isLoading, setIsLoading] = useState(false)
+ //indicator for wether the first message is outputted
+ const [isOutputted, setIsOutputted] = useState(false)
const { quickAssistantId } = useAppSelector((state) => state.llm)
+ const currentAssistant = useRef(null)
+ const currentTopic = useRef(null)
+ const currentAskId = useRef('')
+
+ const inputBarRef = useRef(null)
+ const featureMenusRef = useRef(null)
+ const referenceText = selectedText || clipboardText || userInputText
+
+ const content = isFirstMessage
+ ? (referenceText === userInputText ? userInputText : `${referenceText}\n\n${userInputText}`).trim()
+ : userInputText.trim()
+
+ //init the assistant and topic
+ useEffect(() => {
+ if (quickAssistantId) {
+ currentAssistant.current = getAssistantById(quickAssistantId) || getDefaultAssistant()
+ } else {
+ currentAssistant.current = getDefaultAssistant()
+ }
+
+ if (!currentAssistant.current?.model) {
+ currentAssistant.current.model = getDefaultModel()
+ }
+ currentTopic.current = getDefaultTopic(currentAssistant.current?.id)
+ }, [quickAssistantId])
+
+ useEffect(() => {
+ i18n.changeLanguage(language || navigator.language || defaultLanguage)
+ }, [language])
+
+ // 当路由为home时,初始化isFirstMessage为true
+ useEffect(() => {
+ if (route === 'home') {
+ setIsFirstMessage(true)
+ }
+ }, [route])
const readClipboard = useCallback(async () => {
if (!readClipboardAtStartup) return
@@ -66,6 +104,12 @@ const HomeWindow: FC = () => {
}
}, [readClipboardAtStartup, lastClipboardText])
+ const clearClipboard = () => {
+ setClipboardText('')
+ setSelectedText('')
+ focusInput()
+ }
+
const focusInput = () => {
if (inputBarRef.current) {
const input = inputBarRef.current.querySelector('input')
@@ -81,15 +125,19 @@ const HomeWindow: FC = () => {
focusInput()
}, [readClipboard])
+ useEffect(() => {
+ window.electron.ipcRenderer.on(IpcChannel.ShowMiniWindow, onWindowShow)
+
+ return () => {
+ window.electron.ipcRenderer.removeAllListeners(IpcChannel.ShowMiniWindow)
+ }
+ }, [onWindowShow])
+
useEffect(() => {
readClipboard()
}, [readClipboard])
- useEffect(() => {
- i18n.changeLanguage(language || navigator.language || defaultLanguage)
- }, [language])
-
- const onCloseWindow = () => window.api.miniWindow.hide()
+ const handleCloseWindow = () => window.api.miniWindow.hide()
const handleKeyDown = (e: React.KeyboardEvent) => {
// 使用非直接输入法时(例如中文、日文输入法),存在输入法键入过程
@@ -115,7 +163,7 @@ const HomeWindow: FC = () => {
} else {
// 目前文本框只在'chat'时可以继续输入,这里相当于 route === 'chat'
setRoute('chat')
- onSendMessage().then()
+ handleSendMessage().then()
focusInput()
}
}
@@ -124,7 +172,7 @@ const HomeWindow: FC = () => {
case 'Backspace':
{
textChange(() => {
- if (text.length === 0) {
+ if (userInputText.length === 0) {
clearClipboard()
}
})
@@ -148,137 +196,209 @@ const HomeWindow: FC = () => {
break
case 'Escape':
{
- setText('')
- setRoute('home')
- route === 'home' && onCloseWindow()
+ handleEsc()
}
break
}
}
const handleChange = (e: React.ChangeEvent) => {
- setText(e.target.value)
+ setUserInputText(e.target.value)
}
- useEffect(() => {
- const defaultCurrentAssistant = {
- ...defaultAssistant,
- model: defaultModel
- }
-
- if (quickAssistantId) {
- // 獲取指定助手,如果不存在則使用默認助手
- const assistantFromId = getAssistantById(quickAssistantId)
- const currentAssistant = assistantFromId || defaultCurrentAssistant
- // 如果助手本身沒有設定模型,則使用預設模型
- if (!currentAssistant.model) {
- currentAssistant.model = defaultModel
- }
- setCurrentAssistant(currentAssistant)
- } else {
- setCurrentAssistant(defaultCurrentAssistant)
- }
- }, [quickAssistantId, defaultAssistant, defaultModel])
-
- const onSendMessage = useCallback(
+ const handleSendMessage = useCallback(
async (prompt?: string) => {
- if (isEmpty(content)) {
+ if (isEmpty(content) || !currentAssistant.current || !currentTopic.current) {
return
}
- const topic = currentAssistant.topics[0]
- const messageParams = {
- role: 'user',
- content: [prompt, content].filter(Boolean).join('\n\n'),
- assistant: currentAssistant,
- topic,
- createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
- status: 'success'
- }
- const topicId = topic.id
- const { message: userMessage, blocks } = getUserMessage(messageParams)
- store.dispatch(newMessagesActions.addMessage({ topicId, message: userMessage }))
- store.dispatch(upsertManyBlocks(blocks))
+ try {
+ const topicId = currentTopic.current.id
- const assistant = currentAssistant
- let blockId: string | null = null
- let blockContent: string = ''
+ const { message: userMessage, blocks } = getUserMessage({
+ content: [prompt, content].filter(Boolean).join('\n\n'),
+ assistant: currentAssistant.current,
+ topic: currentTopic.current
+ })
- const assistantMessage = getAssistantMessage({ assistant, topic: assistant.topics[0] })
- store.dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
+ store.dispatch(newMessagesActions.addMessage({ topicId, message: userMessage }))
+ store.dispatch(upsertManyBlocks(blocks))
- fetchChatCompletion({
- messages: [userMessage],
- assistant: { ...assistant, settings: { streamOutput: true } },
- onChunkReceived: (chunk: Chunk) => {
- if (chunk.type === ChunkType.TEXT_DELTA) {
- blockContent += chunk.text
- if (!blockId) {
- const block = createMainTextBlock(assistantMessage.id, chunk.text, {
- status: MessageBlockStatus.STREAMING
- })
- blockId = block.id
- store.dispatch(
- newMessagesActions.updateMessage({
- topicId,
- messageId: assistantMessage.id,
- updates: { blockInstruction: { id: block.id } }
- })
- )
- store.dispatch(upsertOneBlock(block))
- } else {
- store.dispatch(updateOneBlock({ id: blockId, changes: { content: blockContent } }))
+ const assistantMessage = getAssistantMessage({
+ assistant: currentAssistant.current,
+ topic: currentTopic.current
+ })
+ assistantMessage.askId = userMessage.id
+ currentAskId.current = userMessage.id
+
+ store.dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
+
+ const allMessagesForTopic = selectMessagesForTopic(store.getState(), topicId)
+ const userMessageIndex = allMessagesForTopic.findIndex((m) => m?.id === userMessage.id)
+
+ const messagesForContext = allMessagesForTopic
+ .slice(0, userMessageIndex + 1)
+ .filter((m) => m && !m.status?.includes('ing'))
+
+ let blockId: string | null = null
+ let blockContent: string = ''
+ let thinkingBlockId: string | null = null
+ let thinkingBlockContent: string = ''
+
+ setIsLoading(true)
+ setIsOutputted(false)
+
+ setIsFirstMessage(false)
+ setUserInputText('')
+
+ await fetchChatCompletion({
+ messages: messagesForContext,
+ assistant: { ...currentAssistant.current, settings: { streamOutput: true } },
+ onChunkReceived: (chunk: Chunk) => {
+ switch (chunk.type) {
+ case ChunkType.THINKING_DELTA:
+ {
+ thinkingBlockContent += chunk.text
+ setIsOutputted(true)
+ if (!thinkingBlockId) {
+ const block = createThinkingBlock(assistantMessage.id, chunk.text, {
+ status: MessageBlockStatus.STREAMING,
+ thinking_millsec: chunk.thinking_millsec
+ })
+ thinkingBlockId = block.id
+ store.dispatch(
+ newMessagesActions.updateMessage({
+ topicId,
+ messageId: assistantMessage.id,
+ updates: { blockInstruction: { id: block.id } }
+ })
+ )
+ store.dispatch(upsertOneBlock(block))
+ } else {
+ store.dispatch(
+ updateOneBlock({
+ id: thinkingBlockId,
+ changes: { content: thinkingBlockContent, thinking_millsec: chunk.thinking_millsec }
+ })
+ )
+ }
+ }
+ break
+ case ChunkType.THINKING_COMPLETE:
+ {
+ if (thinkingBlockId) {
+ store.dispatch(
+ updateOneBlock({
+ id: thinkingBlockId,
+ changes: { status: MessageBlockStatus.SUCCESS, thinking_millsec: chunk.thinking_millsec }
+ })
+ )
+ }
+ }
+ break
+ case ChunkType.TEXT_DELTA:
+ {
+ blockContent += chunk.text
+ setIsOutputted(true)
+ if (!blockId) {
+ const block = createMainTextBlock(assistantMessage.id, chunk.text, {
+ status: MessageBlockStatus.STREAMING
+ })
+ blockId = block.id
+ store.dispatch(
+ newMessagesActions.updateMessage({
+ topicId,
+ messageId: assistantMessage.id,
+ updates: { blockInstruction: { id: block.id } }
+ })
+ )
+ store.dispatch(upsertOneBlock(block))
+ } else {
+ store.dispatch(updateOneBlock({ id: blockId, changes: { content: blockContent } }))
+ }
+ }
+ break
+
+ case ChunkType.TEXT_COMPLETE:
+ {
+ blockId &&
+ store.dispatch(updateOneBlock({ id: blockId, changes: { status: MessageBlockStatus.SUCCESS } }))
+ store.dispatch(
+ newMessagesActions.updateMessage({
+ topicId,
+ messageId: assistantMessage.id,
+ updates: { status: AssistantMessageStatus.SUCCESS }
+ })
+ )
+ }
+ break
+ case ChunkType.BLOCK_COMPLETE:
+ case ChunkType.ERROR:
+ setIsLoading(false)
+ setIsOutputted(true)
+ currentAskId.current = ''
+ break
}
}
- if (chunk.type === ChunkType.TEXT_COMPLETE) {
- blockId && store.dispatch(updateOneBlock({ id: blockId, changes: { status: MessageBlockStatus.SUCCESS } }))
- store.dispatch(
- newMessagesActions.updateMessage({
- topicId,
- messageId: assistantMessage.id,
- updates: { status: AssistantMessageStatus.SUCCESS }
- })
- )
- }
- }
- })
-
- setIsFirstMessage(false)
- setText('') // ✅ 清除输入框内容
+ })
+ } catch (err) {
+ if (isAbortError(err)) return
+ // onError(err instanceof Error ? err : new Error('An error occurred'))
+ console.error('Error fetching result:', err)
+ } finally {
+ setIsLoading(false)
+ setIsOutputted(true)
+ currentAskId.current = ''
+ }
},
- [content, currentAssistant, topic]
+ [content, currentAssistant]
)
- const clearClipboard = () => {
- setClipboardText('')
- setSelectedText('')
- focusInput()
+ const handleEsc = () => {
+ if (isLoading) {
+ handlePause()
+ } else {
+ if (route === 'home') {
+ handleCloseWindow()
+ } else {
+ //if we go back to home, we should clear the topic
+
+ //clear the topic messages in order to reduce memory usage
+ store.dispatch(newMessagesActions.clearTopicMessages(currentTopic.current!.id))
+
+ //reset the topic
+ if (currentAssistant.current?.id) {
+ currentTopic.current = getDefaultTopic(currentAssistant.current.id)
+ }
+
+ setRoute('home')
+ setUserInputText('')
+ }
+ }
}
- // If the input is focused, the `Esc` callback will not be triggered here.
- useHotkeys('esc', () => {
- if (route === 'home') {
- onCloseWindow()
- } else {
- setRoute('home')
- setText('')
- }
- })
+ const handlePause = () => {
+ if (currentAskId.current) {
+ // const topicId = currentTopic.current!.id
- useEffect(() => {
- window.electron.ipcRenderer.on(IpcChannel.ShowMiniWindow, onWindowShow)
+ // const topicMessages = selectMessagesForTopic(store.getState(), topicId)
+ // if (!topicMessages) return
- return () => {
- window.electron.ipcRenderer.removeAllListeners(IpcChannel.ShowMiniWindow)
- }
- }, [onWindowShow, onSendMessage, setRoute])
+ // const streamingMessages = topicMessages.filter((m) => m.status === 'processing' || m.status === 'pending')
+ // const askIds = [...new Set(streamingMessages?.map((m) => m.askId).filter((id) => !!id) as string[])]
- // 当路由为home时,初始化isFirstMessage为true
- useEffect(() => {
- if (route === 'home') {
- setIsFirstMessage(true)
+ // for (const askId of askIds) {
+ // abortCompletion(askId)
+ // }
+ // store.dispatch(newMessagesActions.setTopicLoading({ topicId: topicId, loading: false }))
+
+ abortCompletion(currentAskId.current)
+ // store.dispatch(newMessagesActions.setTopicLoading({ topicId: currentTopic.current!.id, loading: false }))
+ setIsLoading(false)
+ setIsOutputted(true)
+ currentAskId.current = ''
}
- }, [route])
+ }
const backgroundColor = () => {
// ONLY MAC: when transparent style + light theme: use vibrancy effect
@@ -286,88 +406,94 @@ const HomeWindow: FC = () => {
if (isMac && windowStyle === 'transparent' && theme === ThemeMode.light) {
return 'transparent'
}
-
return 'var(--color-background)'
}
- if (['chat', 'summary', 'explanation'].includes(route)) {
- return (
-
- {route === 'chat' && (
- <>
-
-
- >
- )}
- {['summary', 'explanation'].includes(route) && (
-
-
-
- )}
-
-
-
- )
- }
+ switch (route) {
+ case 'chat':
+ case 'summary':
+ case 'explanation':
+ return (
+
+ {route === 'chat' && (
+ <>
+
+
+ >
+ )}
+ {['summary', 'explanation'].includes(route) && (
+
+
+
+ )}
+
+
+
+
+ )
- if (route === 'translate') {
- return (
-
-
-
-
- )
- }
+ case 'translate':
+ return (
+
+
+
+
+
+ )
- return (
-
-
-
-
-
-
-
-
-
- )
+ //Home
+ default:
+ return (
+
+
+
+
+
+
+
+
+
+ )
+ }
}
const Container = styled.div`
diff --git a/src/renderer/src/windows/mini/home/components/Footer.tsx b/src/renderer/src/windows/mini/home/components/Footer.tsx
index fc01fdcc50..1202fe4661 100644
--- a/src/renderer/src/windows/mini/home/components/Footer.tsx
+++ b/src/renderer/src/windows/mini/home/components/Footer.tsx
@@ -1,37 +1,53 @@
-import { ArrowLeftOutlined } from '@ant-design/icons'
+import { ArrowLeftOutlined, LoadingOutlined } from '@ant-design/icons'
import { Tag as AntdTag, Tooltip } from 'antd'
import { CircleArrowLeft, Copy, Pin } from 'lucide-react'
import { FC, useState } from 'react'
+import { useHotkeys } from 'react-hotkeys-hook'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface FooterProps {
route: string
canUseBackspace?: boolean
+ loading?: boolean
clearClipboard?: () => void
- onExit: () => void
+ onEsc: () => void
}
-const Footer: FC = ({ route, canUseBackspace, clearClipboard, onExit }) => {
+const Footer: FC = ({ route, canUseBackspace, loading, clearClipboard, onEsc }) => {
const { t } = useTranslation()
const [isPinned, setIsPinned] = useState(false)
- const onClickPin = () => {
+ const handlePin = () => {
window.api.miniWindow.setPin(!isPinned).then(() => {
setIsPinned(!isPinned)
})
}
+ useHotkeys('esc', () => {
+ onEsc()
+ })
+
return (
}
+ icon={
+ loading ? (
+
+ ) : (
+
+ )
+ }
className="nodrag"
- onClick={() => onExit()}>
+ onClick={onEsc}>
{t('miniwindow.footer.esc', {
- action: route === 'home' ? t('miniwindow.footer.esc_close') : t('miniwindow.footer.esc_back')
+ action: loading
+ ? t('miniwindow.footer.esc_pause')
+ : route === 'home'
+ ? t('miniwindow.footer.esc_close')
+ : t('miniwindow.footer.esc_back')
})}
{route === 'home' && !canUseBackspace && (
@@ -54,7 +70,7 @@ const Footer: FC = ({ route, canUseBackspace, clearClipboard, onExi
)}
- onClickPin()} className="nodrag">
+ handlePin()} className="nodrag">
diff --git a/src/renderer/src/windows/mini/home/components/InputBar.tsx b/src/renderer/src/windows/mini/home/components/InputBar.tsx
index 3a8006160c..083b1c7f62 100644
--- a/src/renderer/src/windows/mini/home/components/InputBar.tsx
+++ b/src/renderer/src/windows/mini/home/components/InputBar.tsx
@@ -1,5 +1,4 @@
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
-import { useRuntime } from '@renderer/hooks/useRuntime'
import { Input as AntdInput } from 'antd'
import { InputRef } from 'rc-input/lib/interface'
import React, { useRef } from 'react'
@@ -10,6 +9,7 @@ interface InputBarProps {
model: any
referenceText: string
placeholder: string
+ loading: boolean
handleKeyDown: (e: React.KeyboardEvent) => void
handleChange: (e: React.ChangeEvent) => void
}
@@ -19,12 +19,12 @@ const InputBar = ({
text,
model,
placeholder,
+ loading,
handleKeyDown,
handleChange
}: InputBarProps & { ref?: React.RefObject }) => {
- const { generating } = useRuntime()
const inputRef = useRef(null)
- if (!generating) {
+ if (!loading) {
setTimeout(() => inputRef.current?.input?.focus(), 0)
}
return (
@@ -37,7 +37,7 @@ const InputBar = ({
autoFocus
onKeyDown={handleKeyDown}
onChange={handleChange}
- disabled={generating}
+ disabled={loading}
ref={inputRef}
/>