index.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useRef, useState } from 'react'
  4. import {
  5. ChatBubbleOvalLeftEllipsisIcon,
  6. } from '@heroicons/react/24/outline'
  7. import { useBoolean, useInfiniteScroll } from 'ahooks'
  8. import { ChatBubbleOvalLeftEllipsisIcon as ChatBubbleOvalLeftEllipsisSolidIcon } from '@heroicons/react/24/solid'
  9. import cn from 'classnames'
  10. import { useTranslation } from 'react-i18next'
  11. import RenameModal from '../rename-modal'
  12. import s from './style.module.css'
  13. import type { ConversationItem } from '@/models/share'
  14. import { fetchConversations, renameConversation } from '@/service/share'
  15. import { fetchConversations as fetchUniversalConversations, renameConversation as renameUniversalConversation } from '@/service/universal-chat'
  16. import ItemOperation from '@/app/components/explore/item-operation'
  17. import Toast from '@/app/components/base/toast'
  18. export type IListProps = {
  19. className: string
  20. currentId: string
  21. onCurrentIdChange: (id: string) => void
  22. list: ConversationItem[]
  23. onListChanged?: (newList: ConversationItem[]) => void
  24. isClearConversationList: boolean
  25. isInstalledApp: boolean
  26. isUniversalChat?: boolean
  27. installedAppId?: string
  28. onMoreLoaded: (res: { data: ConversationItem[]; has_more: boolean }) => void
  29. isNoMore: boolean
  30. isPinned: boolean
  31. onPinChanged: (id: string) => void
  32. controlUpdate: number
  33. onDelete: (id: string) => void
  34. }
  35. const List: FC<IListProps> = ({
  36. className,
  37. currentId,
  38. onCurrentIdChange,
  39. list,
  40. onListChanged,
  41. isClearConversationList,
  42. isInstalledApp,
  43. isUniversalChat,
  44. installedAppId,
  45. onMoreLoaded,
  46. isNoMore,
  47. isPinned,
  48. onPinChanged,
  49. controlUpdate,
  50. onDelete,
  51. }) => {
  52. const { t } = useTranslation()
  53. const listRef = useRef<HTMLDivElement>(null)
  54. useInfiniteScroll(
  55. async () => {
  56. if (!isNoMore) {
  57. let lastId = !isClearConversationList ? list[list.length - 1]?.id : undefined
  58. if (lastId === '-1')
  59. lastId = undefined
  60. let res: any
  61. if (isUniversalChat)
  62. res = await fetchUniversalConversations(lastId, isPinned)
  63. else
  64. res = await fetchConversations(isInstalledApp, installedAppId, lastId, isPinned)
  65. const { data: conversations, has_more }: any = res
  66. onMoreLoaded({ data: conversations, has_more })
  67. }
  68. return { list: [] }
  69. },
  70. {
  71. target: listRef,
  72. isNoMore: () => {
  73. return isNoMore
  74. },
  75. reloadDeps: [isNoMore, controlUpdate],
  76. },
  77. )
  78. const [isShowRename, { setTrue: setShowRename, setFalse: setHideRename }] = useBoolean(false)
  79. const [isSaving, { setTrue: setIsSaving, setFalse: setNotSaving }] = useBoolean(false)
  80. const [currentConversation, setCurrentConversation] = useState<ConversationItem | null>(null)
  81. const showRename = (item: ConversationItem) => {
  82. setCurrentConversation(item)
  83. setShowRename()
  84. }
  85. const handleRename = async (newName: string) => {
  86. if (!newName.trim() || !currentConversation) {
  87. Toast.notify({
  88. type: 'error',
  89. message: t('common.chat.conversationNameCanNotEmpty'),
  90. })
  91. return
  92. }
  93. setIsSaving()
  94. const currId = currentConversation.id
  95. try {
  96. if (isUniversalChat)
  97. await renameUniversalConversation(currId, newName)
  98. else
  99. await renameConversation(isInstalledApp, installedAppId, currId, newName)
  100. Toast.notify({
  101. type: 'success',
  102. message: t('common.actionMsg.modifiedSuccessfully'),
  103. })
  104. onListChanged?.(list.map((item) => {
  105. if (item.id === currId) {
  106. return {
  107. ...item,
  108. name: newName,
  109. }
  110. }
  111. return item
  112. }))
  113. setHideRename()
  114. }
  115. finally {
  116. setNotSaving()
  117. }
  118. }
  119. return (
  120. <nav
  121. ref={listRef}
  122. className={cn(className, 'shrink-0 space-y-1 bg-white overflow-y-auto overflow-x-hidden')}
  123. >
  124. {list.map((item) => {
  125. const isCurrent = item.id === currentId
  126. const ItemIcon
  127. = isCurrent ? ChatBubbleOvalLeftEllipsisSolidIcon : ChatBubbleOvalLeftEllipsisIcon
  128. return (
  129. <div
  130. onClick={() => onCurrentIdChange(item.id)}
  131. key={item.id}
  132. className={cn(s.item,
  133. isCurrent
  134. ? 'bg-primary-50 text-primary-600'
  135. : 'text-gray-700 hover:bg-gray-200 hover:text-gray-700',
  136. 'group flex justify-between items-center rounded-md px-2 py-2 text-sm font-medium cursor-pointer',
  137. )}
  138. >
  139. <div className='flex items-center w-0 grow'>
  140. <ItemIcon
  141. className={cn(
  142. isCurrent
  143. ? 'text-primary-600'
  144. : 'text-gray-400 group-hover:text-gray-500',
  145. 'mr-3 h-5 w-5 flex-shrink-0',
  146. )}
  147. aria-hidden="true"
  148. />
  149. <span>{item.name}</span>
  150. </div>
  151. {item.id !== '-1' && (
  152. <div className={cn(s.opBtn, 'shrink-0')} onClick={e => e.stopPropagation()}>
  153. <ItemOperation
  154. isPinned={isPinned}
  155. togglePin={() => onPinChanged(item.id)}
  156. isShowRenameConversation
  157. onRenameConversation={() => showRename(item)}
  158. isShowDelete
  159. onDelete={() => onDelete(item.id)}
  160. />
  161. </div>
  162. )}
  163. </div>
  164. )
  165. })}
  166. {isShowRename && (
  167. <RenameModal
  168. isShow={isShowRename}
  169. onClose={setHideRename}
  170. saveLoading={isSaving}
  171. name={currentConversation?.name || ''}
  172. onSave={handleRename}
  173. />
  174. )}
  175. </nav>
  176. )
  177. }
  178. export default React.memo(List)