Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CodeStyleProvider } from './context/CodeStyleProvider'
import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
import TodoProvider from './context/TodoProvider'
import Router from './Router'

const logger = loggerService.withContext('App.tsx')
Expand Down Expand Up @@ -42,7 +43,9 @@ function App(): React.ReactElement {
<CodeStyleProvider>
<PersistGate loading={null} persistor={persistor}>
<TopViewContainer>
<Router />
<TodoProvider>
<Router />
</TodoProvider>
</TopViewContainer>
</PersistGate>
</CodeStyleProvider>
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/components/QuickPanel/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
if (lastSymbolIndex !== -1) {
const newSearchText = textBeforeCursor.slice(lastSymbolIndex)
setSearchTextDebounced(newSearchText)
} else {
} else if (ctx.symbol !== 'todos') {
// 使用本地 handleClose,确保在删除触发符时同步受控输入值
handleClose('delete-symbol')
}
Expand Down Expand Up @@ -467,7 +467,7 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (target.closest('#inputbar')) return
if (bodyRef.current && !bodyRef.current.contains(target)) {
if (bodyRef.current && !bodyRef.current.contains(target) && ctx.symbol !== 'todos') {
handleClose('outsideclick')
}
}
Expand Down
30 changes: 30 additions & 0 deletions src/renderer/src/context/TodoProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import assistantTodoService from '@renderer/services/AssistantTodoService'
import { useAppSelector } from '@renderer/store'
import { selectActiveTodoExecutorSet } from '@renderer/store/runtime'
import { PropsWithChildren, useEffect } from 'react'

export default function TodoProvider({ children }: PropsWithChildren<{}>) {
const { assistants } = useAppSelector((state) => state.assistants)
const activeExecutors = useAppSelector((state) => selectActiveTodoExecutorSet(state))

// Drive queue on todos changes for active assistants only
useEffect(() => {
try {
assistants.forEach((assistant) => {
if (!activeExecutors.has(assistant.id)) return
const byTopic = assistant.todos || {}
Object.keys(byTopic).forEach((topicId) => {
const list = byTopic[topicId] || []
const hasPending = list.some((t) => t.status === 'pending')
if (hasPending) {
assistantTodoService.processNext(assistant.id, topicId)
}
})
})
} catch (e) {
// swallow errors to avoid breaking render tree
// use global logger if needed
}
}, [assistants, activeExecutors])
return children
}
8 changes: 7 additions & 1 deletion src/renderer/src/i18n/locales/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -3478,7 +3478,8 @@
"paste_long_text_threshold": "Paste long text length",
"send_shortcuts": "Send shortcuts",
"show_estimated_tokens": "Show estimated tokens",
"title": "Input Settings"
"title": "Input Settings",
"todos_auto_popped_panel": "Auto pop up todos panel"
},
"markdown_rendering_input_message": "Markdown render input message",
"metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
Expand Down Expand Up @@ -4194,6 +4195,11 @@
"settings": "Settings",
"translate": "Translate"
},
"todos": {
"clear_all": "Clear all todos",
"clear_ended": "Clear ended todos",
"title": "Todos"
},
"trace": {
"backList": "Back To List",
"edasSupport": "Powered by Alibaba Cloud EDAS",
Expand Down
8 changes: 7 additions & 1 deletion src/renderer/src/i18n/locales/zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -3479,7 +3479,8 @@
"paste_long_text_threshold": "长文本长度",
"send_shortcuts": "发送快捷键",
"show_estimated_tokens": "显示预估 Token 数",
"title": "输入设置"
"title": "输入设置",
"todos_auto_popped_panel": "自动弹出待办事项面板"
},
"markdown_rendering_input_message": "Markdown 渲染输入消息",
"metrics": "首字时延 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens",
Expand Down Expand Up @@ -4195,6 +4196,11 @@
"settings": "设置",
"translate": "翻译"
},
"todos": {
"clear_all": "清除所有待办事项",
"clear_ended": "清除已经结束的待办事项",
"title": "待办事项"
},
"trace": {
"backList": "返回列表",
"edasSupport": "Powered by Alibaba Cloud EDAS",
Expand Down
8 changes: 7 additions & 1 deletion src/renderer/src/i18n/locales/zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -3478,7 +3478,8 @@
"paste_long_text_threshold": "長文字長度",
"send_shortcuts": "傳送快捷鍵",
"show_estimated_tokens": "顯示預估 Token 數",
"title": "輸入設定"
"title": "輸入設定",
"todos_auto_popped_panel": "自動彈出待辦事項面板"
},
"markdown_rendering_input_message": "Markdown 渲染輸入訊息",
"metrics": "首字延遲 {{time_first_token_millsec}} ms | 每秒 {{token_speed}} tokens",
Expand Down Expand Up @@ -4194,6 +4195,11 @@
"settings": "設定",
"translate": "翻譯"
},
"todos": {
"clear_all": "清除所有待辦事項",
"clear_ended": "清除已經結束的待辦事項",
"title": "待辦事項"
},
"trace": {
"backList": "返回清單",
"edasSupport": "Powered by Alibaba Cloud EDAS",
Expand Down
118 changes: 78 additions & 40 deletions src/renderer/src/pages/home/Inputbar/Inputbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,18 @@ import { useTimer } from '@renderer/hooks/useTimer'
import useTranslate from '@renderer/hooks/useTranslate'
import { getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import FileManager from '@renderer/services/FileManager'
import { checkRateLimit, getUserMessage } from '@renderer/services/MessagesService'
import { checkRateLimit, sendUserMessage } from '@renderer/services/MessagesService'
import { getModelUniqId } from '@renderer/services/ModelService'
import PasteService from '@renderer/services/PasteService'
import { spanManagerService } from '@renderer/services/SpanManagerService'
import { estimateTextTokens as estimateTxtTokens, estimateUserPromptUsage } from '@renderer/services/TokenService'
import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService'
import { translateText } from '@renderer/services/TranslateService'
import WebSearchService from '@renderer/services/WebSearchService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setSearching } from '@renderer/store/runtime'
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
import { addAssistantTodo } from '@renderer/store/assistants'
import { addActiveTodoExecutor, setSearching } from '@renderer/store/runtime'
import { Assistant, FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types'
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
import { classNames, delay, filterSupportedFiles, formatFileSize } from '@renderer/utils'
import { PendingUserMessage, TodoAction, TodoStatus, UserMessageTodo } from '@renderer/types/todos'
import { classNames, delay, filterSupportedFiles, formatFileSize, uuid } from '@renderer/utils'
import { formatQuotedText } from '@renderer/utils/formats'
import {
getFilesFromDropEvent,
Expand Down Expand Up @@ -90,7 +88,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
showInputEstimatedTokens,
autoTranslateWithSpace,
enableQuickPanelTriggers,
enableSpellCheck
enableSpellCheck,
assistantTodos
} = useSettings()
const [expanded, setExpand] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
Expand Down Expand Up @@ -216,51 +215,89 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
if (inputEmpty) {
return
}
if (checkRateLimit(assistant)) {
return
}

logger.info('Starting to send message')

const parent = spanManagerService.startTrace(
{ topicId: topic.id, name: 'sendMessage', inputs: text },
mentionedModels && mentionedModels.length > 0 ? mentionedModels : [assistant.model]
)
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, { topicId: topic.id, traceId: parent?.spanContext().traceId })
logger.info('Preparing to send user message')

try {
// Dispatch the sendMessage action with all options
const uploadedFiles = await FileManager.uploadFiles(files)

const baseUserMessage: MessageInputBaseParams = { assistant, topic, content: text }

// getUserMessage()
if (uploadedFiles) {
baseUserMessage.files = uploadedFiles
}
if (!loading) {
if (checkRateLimit(assistant)) {
return
}
logger.info('Sending user message directly')
await sendUserMessage({
assistant,
topic,
content: text,
files,
mentions: mentionedModels,
messageId: uuid()
})
} else {
logger.info('Creating a user message todo')
const todoId = uuid()

// Create pending user message
const pendingMessage: PendingUserMessage = {
id: todoId,
content: text,
files: files.length > 0 ? files : undefined,
mentions: mentionedModels.length > 0 ? mentionedModels : undefined
}

if (mentionedModels) {
baseUserMessage.mentions = mentionedModels
}
// Create todo
const todo: UserMessageTodo = {
id: todoId,
title: `${text.slice(0, 50)}${text.length > 50 ? '...' : ''}`,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
action: TodoAction.SendMessage,
status: TodoStatus.Pending,
assistant,
context: {
topic,
message: pendingMessage
}
}

baseUserMessage.usage = await estimateUserPromptUsage(baseUserMessage)
// Register todo - the AssistantTodoService will handle the actual sending
dispatch(
addAssistantTodo({
assistantId: assistant.id,
topicId: topic.id,
todo
})
)

const { message, blocks } = getUserMessage(baseUserMessage)
message.traceId = parent?.spanContext().traceId
// Activate runtime gate if first time for this assistant in this session
dispatch(addActiveTodoExecutor(assistant.id))

dispatch(_sendMessage(message, blocks, assistant, topic.id))
// Open the todos panel
if (assistantTodos.autoPoppedPanel) {
inputbarToolsRef.current?.openTodosPanel()
}
}

// Clear input
// Clear input immediately for better UX
setText('')
setFiles([])
setTimeoutTimer('sendMessage_1', () => setText(''), 500)
setTimeoutTimer('sendMessage_2', () => resizeTextArea(true), 0)
setExpand(false)
} catch (error) {
logger.warn('Failed to send message:', error as Error)
parent?.recordException(error as Error)
logger.warn('Failed to create a todo for sending message:', error as Error)
}
}, [assistant, dispatch, files, inputEmpty, mentionedModels, resizeTextArea, setTimeoutTimer, text, topic])
}, [
assistant,
assistantTodos.autoPoppedPanel,
dispatch,
files,
inputEmpty,
loading,
mentionedModels,
resizeTextArea,
setTimeoutTimer,
text,
topic
])

const translate = useCallback(async () => {
if (isTranslating) {
Expand Down Expand Up @@ -920,6 +957,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
ref={inputbarToolsRef}
assistant={assistant}
model={model}
topic={topic}
files={files}
extensions={supportedExts}
setFiles={setFiles}
Expand Down
23 changes: 21 additions & 2 deletions src/renderer/src/pages/home/Inputbar/InputbarTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isSupportUrlContextProvider } from '@renderer/config/providers'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsCollapsed, setToolOrder } from '@renderer/store/inputTools'
import { Assistant, FileType, KnowledgeBase, Model } from '@renderer/types'
import { Assistant, FileType, KnowledgeBase, Model, Topic } from '@renderer/types'
import { classNames } from '@renderer/utils'
import { Divider, Dropdown, Tooltip } from 'antd'
import { ItemType } from 'antd/es/menu/interface'
Expand Down Expand Up @@ -39,6 +39,7 @@ import MentionModelsButton, { MentionModelsButtonRef } from './MentionModelsButt
import NewContextButton from './NewContextButton'
import QuickPhrasesButton, { QuickPhrasesButtonRef } from './QuickPhrasesButton'
import ThinkingButton, { ThinkingButtonRef } from './ThinkingButton'
import TodosButton, { TodosButtonRef } from './TodosButton'
import UrlContextButton, { UrlContextButtonRef } from './UrlContextbutton'
import WebSearchButton, { WebSearchButtonRef } from './WebSearchButton'

Expand All @@ -53,11 +54,13 @@ export interface InputbarToolsRef {
}) => QuickPanelListItem[]
openMentionModelsPanel: (triggerInfo?: { type: 'input' | 'button'; position?: number; originalText?: string }) => void
openAttachmentQuickPanel: () => void
openTodosPanel: () => void
}

export interface InputbarToolsProps {
assistant: Assistant
model: Model
topic: Topic
files: FileType[]
setFiles: (files: FileType[]) => void
extensions: string[]
Expand Down Expand Up @@ -102,6 +105,7 @@ const InputbarTools = ({
ref,
assistant,
model,
topic,
files,
setFiles,
showThinkingButton,
Expand Down Expand Up @@ -137,6 +141,7 @@ const InputbarTools = ({
const webSearchButtonRef = useRef<WebSearchButtonRef | null>(null)
const thinkingButtonRef = useRef<ThinkingButtonRef | null>(null)
const urlContextButtonRef = useRef<UrlContextButtonRef | null>(null)
const todosButtonRef = useRef<TodosButtonRef>(null)

const toolOrder = useAppSelector((state) => state.inputTools.toolOrder)
const isCollapse = useAppSelector((state) => state.inputTools.isCollapsed)
Expand Down Expand Up @@ -299,7 +304,8 @@ const InputbarTools = ({
useImperativeHandle(ref, () => ({
getQuickPanelMenu: getQuickPanelMenuImpl,
openMentionModelsPanel: (triggerInfo) => mentionModelsButtonRef.current?.openQuickPanel(triggerInfo),
openAttachmentQuickPanel: () => attachmentButtonRef.current?.openQuickPanel()
openAttachmentQuickPanel: () => attachmentButtonRef.current?.openQuickPanel(),
openTodosPanel: () => todosButtonRef.current?.openQuickPanel()
}))

const toolButtons = useMemo<ToolButtonConfig[]>(() => {
Expand Down Expand Up @@ -410,6 +416,18 @@ const InputbarTools = ({
/>
)
},
{
key: 'todos',
label: t('todos.title'),
component: (
<TodosButton
ref={todosButtonRef}
assistantId={assistant.id}
topicId={topic.id}
ToolbarButton={ToolbarButton}
/>
)
},
{
key: 'quick_phrases',
label: t('settings.quickPhrase.title'),
Expand Down Expand Up @@ -485,6 +503,7 @@ const InputbarTools = ({
showKnowledgeIcon,
showMcpTools,
showThinkingButton,
topic.id,
t
])

Expand Down
Loading