AIAssistantChatSelector.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import { Check, ChevronDown, Edit, Plus, Trash, X } from 'lucide-react'
  2. import { useState } from 'react'
  3. import {
  4. Button,
  5. cn,
  6. Command,
  7. CommandEmpty,
  8. CommandGroup,
  9. CommandInput,
  10. CommandItem,
  11. CommandList,
  12. CommandSeparator,
  13. Input,
  14. Popover,
  15. PopoverContent,
  16. PopoverTrigger,
  17. ScrollArea,
  18. } from 'ui'
  19. import { useAiAssistantStateSnapshot } from '@/state/ai-assistant-state'
  20. interface AIAssistantChatSelectorProps {
  21. disabled?: boolean
  22. }
  23. export const AIAssistantChatSelector = ({ disabled = false }: AIAssistantChatSelectorProps) => {
  24. const snap = useAiAssistantStateSnapshot()
  25. const currentChat = snap.activeChat?.name
  26. const [chatSelectorOpen, setChatSelectorOpen] = useState(false)
  27. const [editingChatId, setEditingChatId] = useState<string | null>(null)
  28. const [editingChatName, setEditingChatName] = useState('')
  29. const chats = Object.entries(snap.chats)
  30. const handleSelectChat = (id: string) => {
  31. snap.selectChat(id)
  32. setChatSelectorOpen(false)
  33. }
  34. const handleDeleteChat = (id: string, e?: React.MouseEvent) => {
  35. if (e) {
  36. e.stopPropagation()
  37. }
  38. snap.deleteChat(id)
  39. }
  40. const handleStartEditChat = (id: string, name: string, e?: React.MouseEvent) => {
  41. if (e) {
  42. e.stopPropagation()
  43. }
  44. setEditingChatId(id)
  45. setEditingChatName(name)
  46. }
  47. const handleSaveEditChat = (e?: React.MouseEvent) => {
  48. if (e) {
  49. e.stopPropagation()
  50. }
  51. if (editingChatId && editingChatName.trim()) {
  52. snap.renameChat(editingChatId, editingChatName.trim())
  53. setEditingChatId(null)
  54. setEditingChatName('')
  55. }
  56. }
  57. const handleCancelEditChat = (e?: React.MouseEvent | React.FocusEvent) => {
  58. if (e) {
  59. e.stopPropagation()
  60. }
  61. setEditingChatId(null)
  62. setEditingChatName('')
  63. }
  64. const handleInputBlur = (e: React.FocusEvent) => {
  65. e.stopPropagation()
  66. const relatedTarget = e.relatedTarget as HTMLElement | null
  67. const isSaveOrCancelButton = relatedTarget?.closest('button')
  68. if (!isSaveOrCancelButton && editingChatId && editingChatName.trim()) {
  69. handleSaveEditChat()
  70. } else if (!isSaveOrCancelButton) {
  71. handleCancelEditChat(e)
  72. }
  73. }
  74. return (
  75. <Popover open={chatSelectorOpen} onOpenChange={setChatSelectorOpen}>
  76. <PopoverTrigger asChild>
  77. <Button
  78. type="text"
  79. size="tiny"
  80. iconRight={<ChevronDown size={14} />}
  81. className="max-w-64 truncate"
  82. >
  83. {currentChat}
  84. </Button>
  85. </PopoverTrigger>
  86. <PopoverContent className="w-[250px] p-0" align="start">
  87. <Command>
  88. <CommandInput className="text-xs" placeholder="Search chats..." />
  89. <CommandList>
  90. <CommandEmpty>No chats found.</CommandEmpty>
  91. <CommandGroup>
  92. <ScrollArea className={chats.length > 4 ? 'h-40' : ''}>
  93. {/* @ts-ignore */}
  94. {chats.map(([id, chat]) => (
  95. <CommandItem
  96. key={id}
  97. value={id}
  98. onSelect={() => handleSelectChat(id)}
  99. className="flex items-center justify-between gap-2 py-1 w-full overflow-hidden group"
  100. keywords={!!chat.name ? [chat.name] : undefined}
  101. disabled={disabled}
  102. >
  103. <div className="flex items-center w-full flex-1 min-w-0">
  104. {editingChatId === id ? (
  105. <div className="flex items-center gap-2 w-full">
  106. <Input
  107. value={editingChatName}
  108. onChange={(e) => setEditingChatName(e.target.value)}
  109. autoFocus
  110. size="tiny"
  111. className="flex-1 w-full"
  112. onClick={(e) => e.stopPropagation()}
  113. onBlur={handleInputBlur}
  114. onKeyDown={(e) => {
  115. if (e.key === 'Enter') {
  116. e.preventDefault()
  117. e.stopPropagation()
  118. handleSaveEditChat()
  119. } else if (e.key === 'Escape') {
  120. e.preventDefault()
  121. e.stopPropagation()
  122. handleCancelEditChat()
  123. }
  124. }}
  125. />
  126. <div className="flex items-center gap-0">
  127. <Button
  128. type="text"
  129. size="tiny"
  130. icon={<Check size={14} />}
  131. onClick={(e) => handleSaveEditChat(e)}
  132. className="h-7 w-7"
  133. />
  134. <Button
  135. type="text"
  136. size="tiny"
  137. icon={<X size={14} />}
  138. onClick={(e) => handleCancelEditChat(e)}
  139. className="h-7 w-7"
  140. />
  141. </div>
  142. </div>
  143. ) : (
  144. <>
  145. <Check
  146. className={cn(
  147. 'mr-2 h-4 w-4 shrink-0',
  148. snap.activeChatId === id ? 'opacity-100' : 'opacity-0'
  149. )}
  150. />
  151. <span className="truncate flex-1 w-0">{chat.name}</span>
  152. </>
  153. )}
  154. </div>
  155. {editingChatId !== id && (
  156. <div className="flex items-center gap-x-0 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
  157. <Button
  158. type="text"
  159. size="tiny"
  160. icon={<Edit size={14} />}
  161. onClick={(e) => handleStartEditChat(id, chat.name, e)}
  162. className="h-6 w-6"
  163. />
  164. {chats.length > 1 && (
  165. <Button
  166. type="text"
  167. size="tiny"
  168. icon={<Trash size={14} />}
  169. onClick={(e) => handleDeleteChat(id, e)}
  170. className="h-6 w-6"
  171. />
  172. )}
  173. </div>
  174. )}
  175. </CommandItem>
  176. ))}
  177. </ScrollArea>
  178. </CommandGroup>
  179. <CommandSeparator />
  180. <CommandGroup>
  181. <CommandItem
  182. className="cursor-pointer w-full gap-x-2"
  183. onSelect={() => {
  184. snap.newChat()
  185. setChatSelectorOpen(false)
  186. }}
  187. onClick={() => {
  188. snap.newChat()
  189. setChatSelectorOpen(false)
  190. }}
  191. disabled={disabled}
  192. >
  193. <Plus size={14} strokeWidth={1.5} />
  194. <span>Start a new chat</span>
  195. </CommandItem>
  196. </CommandGroup>
  197. </CommandList>
  198. </Command>
  199. </PopoverContent>
  200. </Popover>
  201. )
  202. }