瀏覽代碼

fix: typing delay (#2200)

zxhlyh 1 年之前
父節點
當前提交
f2b2effc4b

+ 9 - 7
web/app/components/app/chat/answer/index.tsx

@@ -43,6 +43,7 @@ const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) =
 }
 export type IAnswerProps = {
   item: IChatItem
+  index: number
   feedbackDisabled: boolean
   isHideFeedbackEdit: boolean
   onQueryChange: (query: string) => void
@@ -59,14 +60,15 @@ export type IAnswerProps = {
   supportAnnotation?: boolean
   appId?: string
   question: string
-  onAnnotationEdited?: (question: string, answer: string) => void
-  onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string) => void
-  onAnnotationRemoved?: () => void
+  onAnnotationEdited?: (question: string, answer: string, index: number) => void
+  onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
+  onAnnotationRemoved?: (index: number) => void
   allToolIcons?: Record<string, string | Emoji>
 }
 // The component needs to maintain its own state to control whether to display input component
 const Answer: FC<IAnswerProps> = ({
   item,
+  index,
   onQueryChange,
   feedbackDisabled = false,
   isHideFeedbackEdit = false,
@@ -340,9 +342,9 @@ const Answer: FC<IAnswerProps> = ({
                     cached={hasAnnotation}
                     query={question}
                     answer={content}
-                    onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content)}
+                    onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
                     onEdit={() => setIsShowReplyModal(true)}
-                    onRemoved={onAnnotationRemoved!}
+                    onRemoved={() => onAnnotationRemoved!(index)}
                   />
                 )}
 
@@ -351,8 +353,8 @@ const Answer: FC<IAnswerProps> = ({
                   onHide={() => setIsShowReplyModal(false)}
                   query={question}
                   answer={content}
-                  onEdited={onAnnotationEdited!}
-                  onAdded={onAnnotationAdded!}
+                  onEdited={(editedQuery, editedAnswer) => onAnnotationEdited!(editedQuery, editedAnswer, index)}
+                  onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded!(annotationId, authorName, editedQuery, editedAnswer, index)}
                   appId={appId!}
                   messageId={id}
                   annotationId={annotation?.id || ''}

+ 76 - 73
web/app/components/app/chat/index.tsx

@@ -1,6 +1,6 @@
 'use client'
 import type { FC, ReactNode } from 'react'
-import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
+import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
 import Textarea from 'rc-textarea'
 import { useContext } from 'use-context-selector'
 import cn from 'classnames'
@@ -197,6 +197,76 @@ const Chat: FC<IChatProps> = ({
       logError(t('common.voiceInput.notAllow'))
     })
   }
+  const handleQueryChangeFromAnswer = useCallback((val: string) => {
+    onQueryChange(val)
+    handleSend(val)
+  }, [])
+  const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
+    onChatListChange?.(chatList.map((item, i) => {
+      if (i === index - 1) {
+        return {
+          ...item,
+          content: query,
+        }
+      }
+      if (i === index) {
+        return {
+          ...item,
+          content: answer,
+          annotation: {
+            ...item.annotation,
+            logAnnotation: undefined,
+          } as any,
+        }
+      }
+      return item
+    }))
+  }, [])
+  const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
+    onChatListChange?.(chatList.map((item, i) => {
+      if (i === index - 1) {
+        return {
+          ...item,
+          content: query,
+        }
+      }
+      if (i === index) {
+        const answerItem = {
+          ...item,
+          content: item.content,
+          annotation: {
+            id: annotationId,
+            authorName,
+            logAnnotation: {
+              content: answer,
+              account: {
+                id: '',
+                name: authorName,
+                email: '',
+              },
+            },
+          } as Annotation,
+        }
+        return answerItem
+      }
+      return item
+    }))
+  }, [])
+  const handleAnnotationRemoved = useCallback((index: number) => {
+    onChatListChange?.(chatList.map((item, i) => {
+      if (i === index) {
+        return {
+          ...item,
+          content: item.content,
+          annotation: {
+            ...(item.annotation || {}),
+            id: '',
+          } as Annotation,
+        }
+      }
+      return item
+    }))
+  }, [])
 
   return (
     <div className={cn('px-3.5', 'h-full')}>
@@ -210,10 +280,8 @@ const Chat: FC<IChatProps> = ({
             return <Answer
               key={item.id}
               item={item}
-              onQueryChange={(val) => {
-                onQueryChange(val)
-                handleSend(val)
-              }}
+              index={index}
+              onQueryChange={handleQueryChangeFromAnswer}
               feedbackDisabled={feedbackDisabled}
               isHideFeedbackEdit={isHideFeedbackEdit}
               onFeedback={onFeedback}
@@ -228,72 +296,9 @@ const Chat: FC<IChatProps> = ({
               supportAnnotation={supportAnnotation}
               appId={appId}
               question={chatList[index - 1]?.content}
-              onAnnotationEdited={(query, answer) => {
-                onChatListChange?.(chatList.map((item, i) => {
-                  if (i === index - 1) {
-                    return {
-                      ...item,
-                      content: query,
-                    }
-                  }
-                  if (i === index) {
-                    return {
-                      ...item,
-                      content: answer,
-                      annotation: {
-                        ...item.annotation,
-                        logAnnotation: undefined,
-                      } as any,
-                    }
-                  }
-                  return item
-                }))
-              }}
-              onAnnotationAdded={(annotationId, authorName, query, answer) => {
-                onChatListChange?.(chatList.map((item, i) => {
-                  if (i === index - 1) {
-                    return {
-                      ...item,
-                      content: query,
-                    }
-                  }
-                  if (i === index) {
-                    const answerItem = {
-                      ...item,
-                      content: item.content,
-                      annotation: {
-                        id: annotationId,
-                        authorName,
-                        logAnnotation: {
-                          content: answer,
-                          account: {
-                            id: '',
-                            name: authorName,
-                            email: '',
-                          },
-                        },
-                      } as Annotation,
-                    }
-                    return answerItem
-                  }
-                  return item
-                }))
-              }}
-              onAnnotationRemoved={() => {
-                onChatListChange?.(chatList.map((item, i) => {
-                  if (i === index) {
-                    return {
-                      ...item,
-                      content: item.content,
-                      annotation: {
-                        ...(item.annotation || {}),
-                        id: '',
-                      } as Annotation,
-                    }
-                  }
-                  return item
-                }))
-              }}
+              onAnnotationEdited={handleAnnotationEdited}
+              onAnnotationAdded={handleAnnotationAdded}
+              onAnnotationRemoved={handleAnnotationRemoved}
               allToolIcons={allToolIcons}
             />
           }
@@ -307,8 +312,6 @@ const Chat: FC<IChatProps> = ({
               item={item}
               isShowPromptLog={isShowPromptLog}
               isResponsing={isResponsing}
-              // ['https://placekitten.com/360/360', 'https://placekitten.com/360/640']
-              imgSrcs={(item.message_files && item.message_files?.length > 0) ? item.message_files.map(item => item.url) : []}
             />
           )
         })}

+ 2 - 2
web/app/components/app/chat/question/index.tsx

@@ -13,14 +13,14 @@ import ImageGallery from '@/app/components/base/image-gallery'
 type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'> & {
   isShowPromptLog?: boolean
   item: IChatItem
-  imgSrcs?: string[]
   isResponsing?: boolean
 }
 
-const Question: FC<IQuestionProps> = ({ id, content, imgSrcs, more, useCurrentUserAvatar, isShowPromptLog, item, isResponsing }) => {
+const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar, isShowPromptLog, item, isResponsing }) => {
   const { userProfile } = useContext(AppContext)
   const userName = userProfile?.name
   const ref = useRef(null)
+  const imgSrcs = item.message_files?.map(item => item.url)
 
   return (
     <div className={`flex items-start justify-end ${isShowPromptLog && 'first-of-type:pt-[14px]'}`} key={id} ref={ref}>

+ 3 - 3
web/app/components/share/chat/hooks/use-conversation.ts

@@ -1,4 +1,4 @@
-import { useState } from 'react'
+import { useCallback, useState } from 'react'
 import produce from 'immer'
 import { useGetState } from 'ahooks'
 import type { ConversationItem } from '@/models/share'
@@ -11,7 +11,7 @@ function useConversation() {
   const [pinnedConversationList, setPinnedConversationList] = useState<ConversationItem[]>([])
   const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState<string>('-1')
   // when set conversation id, we do not have set appId
-  const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => {
+  const setCurrConversationId = useCallback((id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => {
     doSetCurrConversationId(id)
     if (isSetToLocalStroge && id !== '-1') {
       // conversationIdInfo: {[appId1]: conversationId1, [appId2]: conversationId2}
@@ -19,7 +19,7 @@ function useConversation() {
       conversationIdInfo[appId] = id
       globalThis.localStorage?.setItem(storageConversationIdKey, JSON.stringify(conversationIdInfo))
     }
-  }
+  }, [doSetCurrConversationId])
 
   const getConversationIdFromStorage = (appId: string) => {
     const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {}

+ 101 - 73
web/app/components/share/chat/index.tsx

@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-use-before-define */
 'use client'
 import type { FC } from 'react'
-import React, { useEffect, useRef, useState } from 'react'
+import React, { useCallback, useEffect, useRef, useState } from 'react'
 import cn from 'classnames'
 import useSWR from 'swr'
 import { useTranslation } from 'react-i18next'
@@ -65,6 +65,7 @@ const Main: FC<IMainProps> = ({
   installedAppInfo,
 }) => {
   const { t } = useTranslation()
+  const { notify } = useContext(ToastContext)
   const media = useBreakpoints()
   const isMobile = media === MediaType.mobile
 
@@ -123,7 +124,8 @@ const Main: FC<IMainProps> = ({
   const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
   const [hasMore, setHasMore] = useState<boolean>(true)
   const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true)
-  const onMoreLoaded = ({ data: conversations, has_more }: any) => {
+  const [isShowSuggestion, setIsShowSuggestion] = useState(false)
+  const onMoreLoaded = useCallback(({ data: conversations, has_more }: any) => {
     setHasMore(has_more)
     if (isClearConversationList) {
       setConversationList(conversations)
@@ -132,8 +134,8 @@ const Main: FC<IMainProps> = ({
     else {
       setConversationList([...conversationList, ...conversations])
     }
-  }
-  const onPinnedMoreLoaded = ({ data: conversations, has_more }: any) => {
+  }, [conversationList, setConversationList, isClearConversationList, clearConversationListFalse])
+  const onPinnedMoreLoaded = useCallback(({ data: conversations, has_more }: any) => {
     setHasPinnedMore(has_more)
     if (isClearPinnedConversationList) {
       setPinnedConversationList(conversations)
@@ -142,9 +144,9 @@ const Main: FC<IMainProps> = ({
     else {
       setPinnedConversationList([...pinnedConversationList, ...conversations])
     }
-  }
+  }, [pinnedConversationList, setPinnedConversationList, isClearPinnedConversationList, clearPinnedConversationListFalse])
   const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0)
-  const noticeUpdateList = () => {
+  const noticeUpdateList = useCallback(() => {
     setHasMore(true)
     clearConversationListTrue()
 
@@ -152,25 +154,25 @@ const Main: FC<IMainProps> = ({
     clearPinnedConversationListTrue()
 
     setControlUpdateConversationList(Date.now())
-  }
-  const handlePin = async (id: string) => {
+  }, [clearConversationListTrue, clearPinnedConversationListTrue])
+  const handlePin = useCallback(async (id: string) => {
     await pinConversation(isInstalledApp, installedAppInfo?.id, id)
     notify({ type: 'success', message: t('common.api.success') })
     noticeUpdateList()
-  }
+  }, [isInstalledApp, installedAppInfo?.id, t, notify, noticeUpdateList])
 
-  const handleUnpin = async (id: string) => {
+  const handleUnpin = useCallback(async (id: string) => {
     await unpinConversation(isInstalledApp, installedAppInfo?.id, id)
     notify({ type: 'success', message: t('common.api.success') })
     noticeUpdateList()
-  }
+  }, [isInstalledApp, installedAppInfo?.id, t, notify, noticeUpdateList])
   const [isShowConfirm, { setTrue: showConfirm, setFalse: hideConfirm }] = useBoolean(false)
   const [toDeleteConversationId, setToDeleteConversationId] = useState('')
-  const handleDelete = (id: string) => {
+  const handleDelete = useCallback((id: string) => {
     setToDeleteConversationId(id)
     hideSidebar() // mobile
     showConfirm()
-  }
+  }, [hideSidebar, showConfirm])
 
   const didDelete = async () => {
     await delConversation(isInstalledApp, installedAppInfo?.id, toDeleteConversationId)
@@ -186,17 +188,51 @@ const Main: FC<IMainProps> = ({
   const [speechToTextConfig, setSpeechToTextConfig] = useState<SpeechToTextConfig | null>(null)
   const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
   const [citationConfig, setCitationConfig] = useState<CitationConfig | null>(null)
-
+  const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
+  const chatListDomRef = useRef<HTMLDivElement>(null)
+  const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
+  const [abortController, setAbortController] = useState<AbortController | null>(null)
   const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
   const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false)
-  const handleStartChat = (inputs: Record<string, any>) => {
+  const conversationIntroduction = currConversationInfo?.introduction || ''
+  const createNewChat = useCallback(async () => {
+    // if new chat is already exist, do not create new chat
+    abortController?.abort()
+    setResponsingFalse()
+    if (conversationList.some(item => item.id === '-1'))
+      return
+
+    setConversationList(produce(conversationList, (draft) => {
+      draft.unshift({
+        id: '-1',
+        name: t('share.chat.newChatDefaultName'),
+        inputs: newConversationInputs,
+        introduction: conversationIntroduction,
+      })
+    }))
+  }, [
+    abortController,
+    setResponsingFalse,
+    setConversationList,
+    conversationList,
+    newConversationInputs,
+    conversationIntroduction,
+    t,
+  ])
+  const handleStartChat = useCallback((inputs: Record<string, any>) => {
     createNewChat()
     setConversationIdChangeBecauseOfNew(true)
     setCurrInputs(inputs)
     setChatStarted()
     // parse variables in introduction
     setChatList(generateNewChatListWithOpenstatement('', inputs))
-  }
+  }, [
+    createNewChat,
+    setConversationIdChangeBecauseOfNew,
+    setCurrInputs,
+    setChatStarted,
+    setChatList,
+  ])
   const hasSetInputs = (() => {
     if (!isNewConversation)
       return true
@@ -205,7 +241,6 @@ const Main: FC<IMainProps> = ({
   })()
 
   const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string
-  const conversationIntroduction = currConversationInfo?.introduction || ''
   const [controlChatUpdateAllConversation, setControlChatUpdateAllConversation] = useState(0)
 
   // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576
@@ -293,25 +328,9 @@ const Main: FC<IMainProps> = ({
   }
   useEffect(handleConversationSwitch, [currConversationId, inited])
 
-  const handleConversationIdChange = (id: string) => {
-    if (id === '-1') {
-      createNewChat()
-      setConversationIdChangeBecauseOfNew(true)
-    }
-    else {
-      setConversationIdChangeBecauseOfNew(false)
-    }
-    // trigger handleConversationSwitch
-    setCurrConversationId(id, appId)
-    setIsShowSuggestion(false)
-    hideSidebar()
-  }
-
   /*
   * chat info. chat is under conversation.
   */
-  const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
-  const chatListDomRef = useRef<HTMLDivElement>(null)
   useEffect(() => {
     // scroll to bottom
     if (chatListDomRef.current)
@@ -319,22 +338,27 @@ const Main: FC<IMainProps> = ({
   }, [chatList, currConversationId])
   // user can not edit inputs if user had send message
   const canEditInpus = !chatList.some(item => item.isAnswer === false) && isNewConversation
-  const createNewChat = async () => {
-    // if new chat is already exist, do not create new chat
-    abortController?.abort()
-    setResponsingFalse()
-    if (conversationList.some(item => item.id === '-1'))
-      return
 
-    setConversationList(produce(conversationList, (draft) => {
-      draft.unshift({
-        id: '-1',
-        name: t('share.chat.newChatDefaultName'),
-        inputs: newConversationInputs,
-        introduction: conversationIntroduction,
-      })
-    }))
-  }
+  const handleConversationIdChange = useCallback((id: string) => {
+    if (id === '-1') {
+      createNewChat()
+      setConversationIdChangeBecauseOfNew(true)
+    }
+    else {
+      setConversationIdChangeBecauseOfNew(false)
+    }
+    // trigger handleConversationSwitch
+    setCurrConversationId(id, appId)
+    setIsShowSuggestion(false)
+    hideSidebar()
+  }, [
+    appId,
+    createNewChat,
+    hideSidebar,
+    setCurrConversationId,
+    setIsShowSuggestion,
+    setConversationIdChangeBecauseOfNew,
+  ])
 
   // sometime introduction is not applied to state
   const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => {
@@ -446,14 +470,11 @@ const Main: FC<IMainProps> = ({
     })()
   }, [])
 
-  const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
-  const [abortController, setAbortController] = useState<AbortController | null>(null)
-  const { notify } = useContext(ToastContext)
-  const logError = (message: string) => {
+  const logError = useCallback((message: string) => {
     notify({ type: 'error', message })
-  }
+  }, [notify])
 
-  const checkCanSend = () => {
+  const checkCanSend = useCallback(() => {
     if (currConversationId !== '-1')
       return true
 
@@ -480,10 +501,9 @@ const Main: FC<IMainProps> = ({
       return false
     }
     return !hasEmptyInput
-  }
+  }, [currConversationId, currInputs, promptConfig, t, logError])
 
   const [controlFocus, setControlFocus] = useState(0)
-  const [isShowSuggestion, setIsShowSuggestion] = useState(false)
   const doShowSuggestion = isShowSuggestion && !isResponsing
   const [openingSuggestedQuestions, setOpeningSuggestedQuestions] = useState<string[]>([])
   const [messageTaskId, setMessageTaskId] = useState('')
@@ -755,7 +775,7 @@ const Main: FC<IMainProps> = ({
     }, isInstalledApp, installedAppInfo?.id)
   }
 
-  const handleFeedback = async (messageId: string, feedback: Feedbacktype) => {
+  const handleFeedback = useCallback(async (messageId: string, feedback: Feedbacktype) => {
     await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id)
     const newChatList = chatList.map((item) => {
       if (item.id === messageId) {
@@ -768,7 +788,19 @@ const Main: FC<IMainProps> = ({
     })
     setChatList(newChatList)
     notify({ type: 'success', message: t('common.api.success') })
-  }
+  }, [isInstalledApp, installedAppInfo?.id, chatList, t, notify, setChatList])
+
+  const handleListChanged = useCallback((list: ConversationItem[]) => {
+    setConversationList(list)
+    setControlChatUpdateAllConversation(Date.now())
+  }, [setConversationList, setControlChatUpdateAllConversation])
+  const handlePinnedListChanged = useCallback((list: ConversationItem[]) => {
+    setPinnedConversationList(list)
+    setControlChatUpdateAllConversation(Date.now())
+  }, [setPinnedConversationList, setControlChatUpdateAllConversation])
+  const handleStartChatOnSidebar = useCallback(() => {
+    handleConversationIdChange('-1')
+  }, [handleConversationIdChange])
 
   const renderSidebar = () => {
     if (!appId || !siteInfo || !promptConfig)
@@ -776,16 +808,10 @@ const Main: FC<IMainProps> = ({
     return (
       <Sidebar
         list={conversationList}
-        onListChanged={(list) => {
-          setConversationList(list)
-          setControlChatUpdateAllConversation(Date.now())
-        }}
+        onListChanged={handleListChanged}
         isClearConversationList={isClearConversationList}
         pinnedList={pinnedConversationList}
-        onPinnedListChanged={(list) => {
-          setPinnedConversationList(list)
-          setControlChatUpdateAllConversation(Date.now())
-        }}
+        onPinnedListChanged={handlePinnedListChanged}
         isClearPinnedConversationList={isClearPinnedConversationList}
         onMoreLoaded={onMoreLoaded}
         onPinnedMoreLoaded={onPinnedMoreLoaded}
@@ -801,11 +827,17 @@ const Main: FC<IMainProps> = ({
         onUnpin={handleUnpin}
         controlUpdateList={controlUpdateConversationList}
         onDelete={handleDelete}
-        onStartChat={() => handleConversationIdChange('-1')}
+        onStartChat={handleStartChatOnSidebar}
       />
     )
   }
 
+  const handleAbortResponsing = useCallback(async () => {
+    await stopChatMessageResponding(appId, messageTaskId, isInstalledApp, installedAppInfo?.id)
+    setHasStopResponded(true)
+    setResponsingFalse()
+  }, [appId, messageTaskId, isInstalledApp, installedAppInfo?.id])
+
   if (appUnavailable)
     return <AppUnavailable isUnknwonReason={isUnknwonReason} />
 
@@ -824,7 +856,7 @@ const Main: FC<IMainProps> = ({
           icon_background={siteInfo.icon_background}
           isMobile={isMobile}
           onShowSideBar={showSidebar}
-          onCreateNewChat={() => handleConversationIdChange('-1')}
+          onCreateNewChat={handleStartChatOnSidebar}
         />
       )}
 
@@ -884,11 +916,7 @@ const Main: FC<IMainProps> = ({
                     onFeedback={handleFeedback}
                     isResponsing={isResponsing}
                     canStopResponsing={!!messageTaskId && isResponsingConIsCurrCon}
-                    abortResponsing={async () => {
-                      await stopChatMessageResponding(appId, messageTaskId, isInstalledApp, installedAppInfo?.id)
-                      setHasStopResponded(true)
-                      setResponsingFalse()
-                    }}
+                    abortResponsing={handleAbortResponsing}
                     checkCanSend={checkCanSend}
                     controlFocus={controlFocus}
                     isShowSuggestion={doShowSuggestion}

+ 10 - 3
web/app/components/share/chat/sidebar/index.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react'
+import React, { useCallback, useEffect, useState } from 'react'
 import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
 import {
@@ -76,6 +76,13 @@ const Sidebar: FC<ISidebarProps> = ({
       checkHasPinned()
   }, [controlUpdateList])
 
+  const handleUnpin = useCallback((id: string) => {
+    onUnpin(id)
+  }, [onUnpin])
+  const handlePin = useCallback((id: string) => {
+    onPin(id)
+  }, [onPin])
+
   const maxListHeight = (isInstalledApp) ? 'max-h-[30vh]' : 'max-h-[40vh]'
 
   return (
@@ -119,7 +126,7 @@ const Sidebar: FC<ISidebarProps> = ({
               onMoreLoaded={onPinnedMoreLoaded}
               isNoMore={isPinnedNoMore}
               isPinned={true}
-              onPinChanged={id => onUnpin(id)}
+              onPinChanged={handleUnpin}
               controlUpdate={controlUpdateList + 1}
               onDelete={onDelete}
             />
@@ -142,7 +149,7 @@ const Sidebar: FC<ISidebarProps> = ({
             onMoreLoaded={onMoreLoaded}
             isNoMore={isNoMore}
             isPinned={false}
-            onPinChanged={id => onPin(id)}
+            onPinChanged={handlePin}
             controlUpdate={controlUpdateList + 1}
             onDelete={onDelete}
           />