chore: token speed and edit message (#5031)

* chore: add token speed measurement

* chore: add edit message handler

* chore: add DialogClose wrapper around save button
This commit is contained in:
Louis 2025-05-20 14:09:25 +07:00 committed by GitHub
parent 76827d42f5
commit 46943a1cf7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 136 additions and 13 deletions

View File

@ -49,6 +49,7 @@ const ChatInput = ({
const { prompt, setPrompt } = usePrompt() const { prompt, setPrompt } = usePrompt()
const { t } = useTranslation() const { t } = useTranslation()
const { spellCheckChatInput } = useGeneralSetting() const { spellCheckChatInput } = useGeneralSetting()
const { tokenSpeed } = useAppState()
const maxRows = 10 const maxRows = 10
const { selectedModel } = useModelProvider() const { selectedModel } = useModelProvider()
@ -226,7 +227,7 @@ const ChatInput = ({
{showSpeedToken && ( {showSpeedToken && (
<div className="flex items-center gap-1 text-main-view-fg/60 text-xs"> <div className="flex items-center gap-1 text-main-view-fg/60 text-xs">
<IconBrandSpeedtest size={18} /> <IconBrandSpeedtest size={18} />
<span>42 tokens/sec</span> <span>{Math.round(tokenSpeed?.tokenSpeed ?? 0)} tokens/sec</span>
</div> </div>
)} )}
</div> </div>

View File

@ -14,6 +14,18 @@ import { useMessages } from '@/hooks/useMessages'
import ThinkingBlock from '@/containers/ThinkingBlock' import ThinkingBlock from '@/containers/ThinkingBlock'
import ToolCallBlock from '@/containers/ToolCallBlock' import ToolCallBlock from '@/containers/ToolCallBlock'
import { useChat } from '@/hooks/useChat' import { useChat } from '@/hooks/useChat'
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { toast } from 'sonner'
const CopyButton = ({ text }: { text: string }) => { const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@ -49,6 +61,8 @@ const CopyButton = ({ text }: { text: string }) => {
// Use memo to prevent unnecessary re-renders, but allow re-renders when props change // Use memo to prevent unnecessary re-renders, but allow re-renders when props change
export const ThreadContent = memo( export const ThreadContent = memo(
(item: ThreadMessage & { isLastMessage?: boolean; index?: number }) => { (item: ThreadMessage & { isLastMessage?: boolean; index?: number }) => {
const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
// Use useMemo to stabilize the components prop // Use useMemo to stabilize the components prop
const linkComponents = useMemo( const linkComponents = useMemo(
() => ({ () => ({
@ -94,6 +108,20 @@ export const ThreadContent = memo(
sendMessage(lastMessage.content?.[0]?.text?.value || '') sendMessage(lastMessage.content?.[0]?.text?.value || '')
}, [deleteMessage, getMessages, item, sendMessage]) }, [deleteMessage, getMessages, item, sendMessage])
const editMessage = useCallback(
(messageId: string) => {
const threadMessages = getMessages(item.thread_id)
const index = threadMessages.findIndex((msg) => msg.id === messageId)
if (index === -1) return
// Delete all messages after the edited message
for (let i = threadMessages.length - 1; i >= index; i--) {
deleteMessage(threadMessages[i].thread_id, threadMessages[i].id)
}
sendMessage(message)
},
[deleteMessage, getMessages, item.thread_id, message, sendMessage]
)
const isToolCalls = const isToolCalls =
item.metadata && item.metadata &&
'tool_calls' in item.metadata && 'tool_calls' in item.metadata &&
@ -110,6 +138,8 @@ export const ThreadContent = memo(
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-2 text-main-view-fg/60 text-xs mt-2"> <div className="flex items-center justify-end gap-2 text-main-view-fg/60 text-xs mt-2">
<Dialog>
<DialogTrigger asChild>
<button <button
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative" className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
onClick={() => { onClick={() => {
@ -121,6 +151,50 @@ export const ThreadContent = memo(
Edit Edit
</span> </span>
</button> </button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Message</DialogTitle>
<Textarea
value={message}
onChange={(e) => {
setMessage(e.target.value)
}}
className="mt-2 resize-none"
onKeyDown={(e) => {
// Prevent key from being captured by parent components
e.stopPropagation()
}}
/>
<DialogFooter className="mt-2 flex items-center">
<DialogClose asChild>
<Button
variant="link"
size="sm"
className="hover:no-underline"
>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
disabled={!message}
onClick={() => {
editMessage(item.id)
toast.success('Edit Message', {
id: 'edit-message',
description:
'Message edited successfully. Please wait for the model to respond.',
})
}}
>
Save
</Button>
</DialogClose>
</DialogFooter>
</DialogHeader>
</DialogContent>
</Dialog>
<button <button
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative" className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
onClick={() => { onClick={() => {

View File

@ -8,11 +8,14 @@ type AppState = {
tools: MCPTool[] tools: MCPTool[]
serverStatus: 'running' | 'stopped' | 'pending' serverStatus: 'running' | 'stopped' | 'pending'
abortControllers: Record<string, AbortController> abortControllers: Record<string, AbortController>
tokenSpeed?: TokenSpeed
setServerStatus: (value: 'running' | 'stopped' | 'pending') => void setServerStatus: (value: 'running' | 'stopped' | 'pending') => void
updateStreamingContent: (content: ThreadMessage | undefined) => void updateStreamingContent: (content: ThreadMessage | undefined) => void
updateLoadingModel: (loading: boolean) => void updateLoadingModel: (loading: boolean) => void
updateTools: (tools: MCPTool[]) => void updateTools: (tools: MCPTool[]) => void
setAbortController: (threadId: string, controller: AbortController) => void setAbortController: (threadId: string, controller: AbortController) => void
updateTokenSpeed: (message: ThreadMessage) => void
resetTokenSpeed: () => void
} }
export const useAppState = create<AppState>()((set) => ({ export const useAppState = create<AppState>()((set) => ({
@ -21,6 +24,7 @@ export const useAppState = create<AppState>()((set) => ({
tools: [], tools: [],
serverStatus: 'stopped', serverStatus: 'stopped',
abortControllers: {}, abortControllers: {},
tokenSpeed: undefined,
updateStreamingContent: (content) => { updateStreamingContent: (content) => {
set({ streamingContent: content }) set({ streamingContent: content })
}, },
@ -39,4 +43,37 @@ export const useAppState = create<AppState>()((set) => ({
}, },
})) }))
}, },
updateTokenSpeed: (message) =>
set((state) => {
const currentTimestamp = new Date().getTime() // Get current time in milliseconds
if (!state.tokenSpeed) {
// If this is the first update, just set the lastTimestamp and return
return {
tokenSpeed: {
lastTimestamp: currentTimestamp,
tokenSpeed: 0,
tokenCount: 1,
message: message.id,
},
}
}
const timeDiffInSeconds =
(currentTimestamp - state.tokenSpeed.lastTimestamp) / 1000 // Time difference in seconds
const totalTokenCount = state.tokenSpeed.tokenCount + 1
const averageTokenSpeed =
totalTokenCount / (timeDiffInSeconds > 0 ? timeDiffInSeconds : 1) // Calculate average token speed
return {
tokenSpeed: {
...state.tokenSpeed,
tokenSpeed: averageTokenSpeed,
tokenCount: totalTokenCount,
message: message.id,
},
}
}),
resetTokenSpeed: () =>
set({
tokenSpeed: undefined,
}),
})) }))

View File

@ -22,7 +22,7 @@ import { useAssistant } from './useAssistant'
export const useChat = () => { export const useChat = () => {
const { prompt, setPrompt } = usePrompt() const { prompt, setPrompt } = usePrompt()
const { tools } = useAppState() const { tools, updateTokenSpeed, resetTokenSpeed } = useAppState()
const { currentAssistant } = useAssistant() const { currentAssistant } = useAssistant()
const { getProviderByName, selectedModel, selectedProvider } = const { getProviderByName, selectedModel, selectedProvider } =
@ -68,6 +68,7 @@ export const useChat = () => {
async (message: string) => { async (message: string) => {
const activeThread = await getCurrentThread() const activeThread = await getCurrentThread()
resetTokenSpeed()
if (!activeThread || !provider) return if (!activeThread || !provider) return
updateStreamingContent(emptyThreadContent) updateStreamingContent(emptyThreadContent)
@ -119,6 +120,7 @@ export const useChat = () => {
accumulatedText accumulatedText
) )
updateStreamingContent(currentContent) updateStreamingContent(currentContent)
updateTokenSpeed(currentContent)
await new Promise((resolve) => setTimeout(resolve, 0)) await new Promise((resolve) => setTimeout(resolve, 0))
} }
} }
@ -144,6 +146,7 @@ export const useChat = () => {
}, },
[ [
getCurrentThread, getCurrentThread,
resetTokenSpeed,
provider, provider,
updateStreamingContent, updateStreamingContent,
addMessage, addMessage,
@ -153,6 +156,7 @@ export const useChat = () => {
setAbortController, setAbortController,
updateLoadingModel, updateLoadingModel,
tools, tools,
updateTokenSpeed,
] ]
) )

View File

@ -55,3 +55,10 @@ type Assistant = {
instructions: string instructions: string
parameters: Record<string, unknown> parameters: Record<string, unknown>
} }
type TokenSpeed = {
message: string
tokenSpeed: number
tokenCount: number
lastTimestamp: number
}