From 184ccc81e9b61c706604ab8d1e5f1bb63f4dca1d Mon Sep 17 00:00:00 2001 From: aliang <1098486429@qq.com> Date: Wed, 15 Nov 2023 04:50:58 +0800 Subject: [PATCH] feat: adding conversation history / new chat in chat playground (#780) * feat: adding conversation history / new chat * fix: format * fix: use user's first question as title --- ee/tabby-ui/app/layout.tsx | 3 +- .../app/playground/components/chats.tsx | 32 ++++ ee/tabby-ui/app/playground/page.tsx | 7 +- ee/tabby-ui/components/chat-context.tsx | 38 +++++ ee/tabby-ui/components/chat-list.tsx | 9 +- .../components/chat-message-actions.tsx | 40 ++++- ee/tabby-ui/components/chat-message.tsx | 13 +- ee/tabby-ui/components/chat-panel.tsx | 36 +++-- ee/tabby-ui/components/chat-sessions.tsx | 125 +++++++++++++++ ee/tabby-ui/components/chat.tsx | 149 +++++++++++++++--- ee/tabby-ui/components/clear-chats-button.tsx | 59 +++++++ .../components/edit-chat-title-dialog.tsx | 92 +++++++++++ ee/tabby-ui/components/header.tsx | 2 +- ee/tabby-ui/components/prompt-form.tsx | 36 ++++- ee/tabby-ui/components/skeleton.tsx | 12 ++ ee/tabby-ui/lib/hooks/use-hydration.ts | 14 ++ ee/tabby-ui/lib/hooks/use-store.ts | 15 ++ ee/tabby-ui/lib/stores/chat-actions.ts | 76 +++++++++ ee/tabby-ui/lib/stores/chat-store.ts | 50 ++++++ ee/tabby-ui/lib/stores/utils.ts | 9 ++ ee/tabby-ui/lib/types/chat.ts | 29 ++++ ee/tabby-ui/lib/types/common.ts | 12 -- ee/tabby-ui/lib/types/index.ts | 2 +- ee/tabby-ui/lib/types/search.ts | 15 -- ee/tabby-ui/lib/utils.ts | 27 ++++ ee/tabby-ui/package.json | 3 +- ee/tabby-ui/yarn.lock | 9 +- 27 files changed, 826 insertions(+), 88 deletions(-) create mode 100644 ee/tabby-ui/app/playground/components/chats.tsx create mode 100644 ee/tabby-ui/components/chat-context.tsx create mode 100644 ee/tabby-ui/components/chat-sessions.tsx create mode 100644 ee/tabby-ui/components/clear-chats-button.tsx create mode 100644 ee/tabby-ui/components/edit-chat-title-dialog.tsx create mode 100644 ee/tabby-ui/components/skeleton.tsx create mode 100644 ee/tabby-ui/lib/hooks/use-hydration.ts create mode 100644 ee/tabby-ui/lib/hooks/use-store.ts create mode 100644 ee/tabby-ui/lib/stores/chat-actions.ts create mode 100644 ee/tabby-ui/lib/stores/chat-store.ts create mode 100644 ee/tabby-ui/lib/stores/utils.ts create mode 100644 ee/tabby-ui/lib/types/chat.ts delete mode 100644 ee/tabby-ui/lib/types/search.ts diff --git a/ee/tabby-ui/app/layout.tsx b/ee/tabby-ui/app/layout.tsx index e71160e..5e9d258 100644 --- a/ee/tabby-ui/app/layout.tsx +++ b/ee/tabby-ui/app/layout.tsx @@ -39,9 +39,8 @@ export default function RootLayout({ children }: RootLayoutProps) {
- {/* @ts-ignore */}
-
{children}
+
{children}
diff --git a/ee/tabby-ui/app/playground/components/chats.tsx b/ee/tabby-ui/app/playground/components/chats.tsx new file mode 100644 index 0000000..4f32f2a --- /dev/null +++ b/ee/tabby-ui/app/playground/components/chats.tsx @@ -0,0 +1,32 @@ +'use client' + +import React from 'react' +import { Chat } from '@/components/chat' +import { useChatStore } from '@/lib/stores/chat-store' +import { getChatById } from '@/lib/stores/utils' +import { ChatSessions } from '@/components/chat-sessions' +import { useStore } from '@/lib/hooks/use-store' +import type { Message } from 'ai' + +const emptyMessages: Message[] = [] + +export default function Chats() { + const _hasHydrated = useStore(useChatStore, state => state._hasHydrated) + const chats = useStore(useChatStore, state => state.chats) + const activeChatId = useStore(useChatStore, state => state.activeChatId) + + const chatId = activeChatId + const chat = getChatById(chats, chatId) + + return ( +
+ + +
+ ) +} diff --git a/ee/tabby-ui/app/playground/page.tsx b/ee/tabby-ui/app/playground/page.tsx index a05f342..afdde74 100644 --- a/ee/tabby-ui/app/playground/page.tsx +++ b/ee/tabby-ui/app/playground/page.tsx @@ -1,13 +1,10 @@ -import { nanoid } from '@/lib/utils' -import { Chat } from '@/components/chat' import { Metadata } from 'next' +import Chats from './components/chats' export const metadata: Metadata = { title: 'Playground' } export default function IndexPage() { - const id = nanoid() - - return + return } diff --git a/ee/tabby-ui/components/chat-context.tsx b/ee/tabby-ui/components/chat-context.tsx new file mode 100644 index 0000000..587208f --- /dev/null +++ b/ee/tabby-ui/components/chat-context.tsx @@ -0,0 +1,38 @@ +'use client' + +import React from 'react' +import { Message, UseChatHelpers, useChat } from 'ai/react' +import { toast } from 'react-hot-toast' + +export interface ChatContextValue extends UseChatHelpers { + id: string | undefined +} + +export const ChatContext = React.createContext({} as ChatContextValue) +export interface ChatContextProviderProps { + id: string | undefined + initialMessages?: Message[] +} + +export const ChatContextProvider: React.FC< + React.PropsWithChildren +> = ({ children, id, initialMessages }) => { + const chatHelpers = useChat({ + initialMessages, + id, + body: { + id + }, + onResponse(response) { + if (response.status === 401) { + toast.error(response.statusText) + } + } + }) + + return ( + + {children} + + ) +} diff --git a/ee/tabby-ui/components/chat-list.tsx b/ee/tabby-ui/components/chat-list.tsx index 0aa677e..01a2be0 100644 --- a/ee/tabby-ui/components/chat-list.tsx +++ b/ee/tabby-ui/components/chat-list.tsx @@ -2,12 +2,14 @@ import { type Message } from 'ai' import { Separator } from '@/components/ui/separator' import { ChatMessage } from '@/components/chat-message' +import { MessageActionType } from '@/lib/types' export interface ChatList { messages: Message[] + handleMessageAction: (messageId: string, action: MessageActionType) => void } -export function ChatList({ messages }: ChatList) { +export function ChatList({ messages, handleMessageAction }: ChatList) { if (!messages.length) { return null } @@ -16,7 +18,10 @@ export function ChatList({ messages }: ChatList) {
{messages.map((message, index) => (
- + {index < messages.length - 1 && ( )} diff --git a/ee/tabby-ui/components/chat-message-actions.tsx b/ee/tabby-ui/components/chat-message-actions.tsx index d4e4b40..8e345a8 100644 --- a/ee/tabby-ui/components/chat-message-actions.tsx +++ b/ee/tabby-ui/components/chat-message-actions.tsx @@ -3,17 +3,26 @@ import { type Message } from 'ai' import { Button } from '@/components/ui/button' -import { IconCheck, IconCopy } from '@/components/ui/icons' +import { + IconCheck, + IconCopy, + IconEdit, + IconRefresh, + IconTrash +} from '@/components/ui/icons' import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' import { cn } from '@/lib/utils' +import { MessageActionType } from '@/lib/types' interface ChatMessageActionsProps extends React.ComponentProps<'div'> { message: Message + handleMessageAction: (messageId: string, action: MessageActionType) => void } export function ChatMessageActions({ message, className, + handleMessageAction, ...props }: ChatMessageActionsProps) { const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) @@ -26,11 +35,38 @@ export function ChatMessageActions({ return (
+ {message.role === 'user' ? ( + + ) : ( + + )} +
) diff --git a/ee/tabby-ui/components/chat-panel.tsx b/ee/tabby-ui/components/chat-panel.tsx index 13e3c2f..35975b2 100644 --- a/ee/tabby-ui/components/chat-panel.tsx +++ b/ee/tabby-ui/components/chat-panel.tsx @@ -1,10 +1,11 @@ -import { type UseChatHelpers } from 'ai/react' - +import React from 'react' import { Button } from '@/components/ui/button' -import { PromptForm } from '@/components/prompt-form' +import { PromptForm, PromptFormRef } from '@/components/prompt-form' import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' import { IconRefresh, IconStop } from '@/components/ui/icons' import { FooterText } from '@/components/footer' +import { cn } from '@/lib/utils' +import type { UseChatHelpers } from 'ai/react' export interface ChatPanelProps extends Pick< @@ -18,6 +19,8 @@ export interface ChatPanelProps | 'setInput' > { id?: string + className?: string + onSubmit: (content: string) => Promise } export function ChatPanel({ @@ -28,10 +31,22 @@ export function ChatPanel({ reload, input, setInput, - messages + messages, + className, + onSubmit }: ChatPanelProps) { + const promptFormRef = React.useRef(null) + React.useEffect(() => { + promptFormRef?.current?.focus() + }, [id]) + return ( -
+
@@ -57,15 +72,10 @@ export function ChatPanel({ ) )}
-
+
{ - await append({ - id, - content: value, - role: 'user' - }) - }} + ref={promptFormRef} + onSubmit={onSubmit} input={input} setInput={setInput} isLoading={isLoading} diff --git a/ee/tabby-ui/components/chat-sessions.tsx b/ee/tabby-ui/components/chat-sessions.tsx new file mode 100644 index 0000000..28d2ec2 --- /dev/null +++ b/ee/tabby-ui/components/chat-sessions.tsx @@ -0,0 +1,125 @@ +'use client' + +import React from 'react' +import { cn, nanoid } from '@/lib/utils' +import { useChatStore } from '@/lib/stores/chat-store' +import { + clearChats, + deleteChat, + setActiveChatId +} from '@/lib/stores/chat-actions' +import { IconPlus, IconTrash } from '@/components/ui/icons' +import { + Tooltip, + TooltipContent, + TooltipTrigger +} from '@/components/ui/tooltip' +import { EditChatTitleDialog } from '@/components/edit-chat-title-dialog' +import { useStore } from '@/lib/hooks/use-store' +import { Button } from '@/components/ui/button' +import { ListSkeleton } from '@/components/skeleton' +import { Separator } from '@/components/ui/separator' +import { ClearChatsButton } from './clear-chats-button' + +interface ChatSessionsProps { + className?: string +} + +export const ChatSessions = ({ className }: ChatSessionsProps) => { + const _hasHydrated = useStore(useChatStore, state => state._hasHydrated) + const chats = useStore(useChatStore, state => state.chats) + const activeChatId = useStore(useChatStore, state => state.activeChatId) + + const onDeleteClick = ( + e: React.MouseEvent, + chatId: string + ) => { + deleteChat(chatId) + } + + const onNewChatClick = (e: React.MouseEvent) => { + setActiveChatId(nanoid()) + } + + const handleClearChats = () => { + clearChats() + } + + return ( + <> +
+
+
+ +
+ +
+ {!_hasHydrated ? ( + + ) : ( + <> + {chats?.map(chat => { + const isActive = activeChatId === chat.id + return ( +
setActiveChatId(chat.id)} + className={cn( + 'hover:bg-accent flex cursor-pointer items-center justify-between gap-3 rounded-lg px-3 py-2 text-zinc-900 transition-all hover:text-zinc-900 dark:text-zinc-50 hover:dark:bg-zinc-900 dark:hover:text-zinc-50', + isActive && '!bg-zinc-200 dark:!bg-zinc-800' + )} + > + + {chat.title || '(Untitled)'} + + {isActive && ( +
e.stopPropagation()} + > + + + + + + + Delete + + +
+ )} +
+ ) + })} + + )} +
+ +
+ +
+
+
+ + ) +} diff --git a/ee/tabby-ui/components/chat.tsx b/ee/tabby-ui/components/chat.tsx index 2405191..ef81831 100644 --- a/ee/tabby-ui/components/chat.tsx +++ b/ee/tabby-ui/components/chat.tsx @@ -1,22 +1,31 @@ 'use client' -import { useChat, type Message } from 'ai/react' - -import { cn } from '@/lib/utils' +import React from 'react' +import { useChat } from 'ai/react' +import { cn, nanoid, truncateText } from '@/lib/utils' import { ChatList } from '@/components/chat-list' import { ChatPanel } from '@/components/chat-panel' import { EmptyScreen } from '@/components/empty-screen' import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' import { toast } from 'react-hot-toast' import { usePatchFetch } from '@/lib/hooks/use-patch-fetch' +import { addChat, updateMessages } from '@/lib/stores/chat-actions' +import { find, findIndex } from 'lodash-es' +import { useStore } from '@/lib/hooks/use-store' +import { useChatStore } from '@/lib/stores/chat-store' +import { ListSkeleton } from '@/components/skeleton' +import type { MessageActionType } from '@/lib/types' +import type { Message } from 'ai/react' export interface ChatProps extends React.ComponentProps<'div'> { initialMessages?: Message[] id?: string + loading?: boolean } -export function Chat({ id, initialMessages, className }: ChatProps) { +export function Chat({ id, initialMessages, loading, className }: ChatProps) { usePatchFetch() + const chats = useStore(useChatStore, state => state.chats) const { messages, @@ -39,28 +48,118 @@ export function Chat({ id, initialMessages, className }: ChatProps) { } } }) + + const [selectedMessageId, setSelectedMessageId] = React.useState() + + const onRegenerateResponse = (messageId: string) => { + const messageIndex = findIndex(messages, { id: messageId }) + const prevMessage = messages?.[messageIndex - 1] + if (prevMessage?.role === 'user') { + setMessages(messages.slice(0, messageIndex - 1)) + append(prevMessage) + } + } + + const onDeleteMessage = (messageId: string) => { + const message = find(messages, { id: messageId }) + if (message) { + setMessages(messages.filter(m => m.id !== messageId)) + } + } + + const onEditMessage = (messageId: string) => { + const message = find(messages, { id: messageId }) + if (message) { + setInput(message.content) + setSelectedMessageId(messageId) + } + } + + const handleMessageAction = ( + messageId: string, + actionType: MessageActionType + ) => { + switch (actionType) { + case 'edit': + onEditMessage(messageId) + break + case 'delete': + onDeleteMessage(messageId) + break + case 'regenerate': + onRegenerateResponse(messageId) + break + default: + break + } + } + + const handleSubmit = async (value: string) => { + if (findIndex(chats, { id }) === -1) { + addChat(id, truncateText(value)) + } else if (selectedMessageId) { + let messageIdx = findIndex(messages, { id: selectedMessageId }) + setMessages(messages.slice(0, messageIdx)) + setSelectedMessageId(undefined) + } + await append({ + id: nanoid(), + content: value, + role: 'user' + }) + } + + React.useEffect(() => { + if (id) { + updateMessages(id, messages) + } + }, [messages]) + + React.useEffect(() => { + const scrollHeight = document.documentElement.scrollHeight + window.scrollTo(0, scrollHeight) + + return () => stop() + }, []) + return ( - <> -
- {messages.length ? ( - <> - - - - ) : ( - - )} +
+
+
+ {loading ? ( +
+
+ +
+
+ +
+
+ ) : messages.length ? ( + <> + + + + ) : ( + + )} +
+
- - +
) } diff --git a/ee/tabby-ui/components/clear-chats-button.tsx b/ee/tabby-ui/components/clear-chats-button.tsx new file mode 100644 index 0000000..606804a --- /dev/null +++ b/ee/tabby-ui/components/clear-chats-button.tsx @@ -0,0 +1,59 @@ +import React from 'react' +import { Button, ButtonProps } from '@/components/ui/button' +import { IconCheck, IconTrash } from '@/components/ui/icons' + +interface ClearChatsButtonProps extends ButtonProps { + onClear: () => void +} + +export const ClearChatsButton = ({ + onClear, + onClick, + onBlur, + ...rest +}: ClearChatsButtonProps) => { + const [waitingConfirmation, setWaitingConfirmation] = React.useState(false) + + const cancelConfirmation = () => { + setWaitingConfirmation(false) + } + + const handleBlur: React.FocusEventHandler = e => { + if (waitingConfirmation) { + cancelConfirmation() + } + onBlur?.(e) + } + + const handleClick: React.MouseEventHandler = e => { + if (!waitingConfirmation) { + setWaitingConfirmation(true) + } else { + onClear() + setWaitingConfirmation(false) + } + onClick?.(e) + } + + return ( + + ) +} diff --git a/ee/tabby-ui/components/edit-chat-title-dialog.tsx b/ee/tabby-ui/components/edit-chat-title-dialog.tsx new file mode 100644 index 0000000..4f3aeae --- /dev/null +++ b/ee/tabby-ui/components/edit-chat-title-dialog.tsx @@ -0,0 +1,92 @@ +'use client' + +import React from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription +} from '@/components/ui/dialog' +import { + Tooltip, + TooltipContent, + TooltipTrigger +} from '@/components/ui/tooltip' +import { IconArrowElbow, IconEdit, IconTrash } from '@/components/ui/icons' + +import { Input } from '@/components/ui/input' +import { updateChat } from '@/lib/stores/chat-actions' +import { Button } from './ui/button' + +interface EditChatTitleDialogProps { + initialValue: string | undefined + chatId: string + children?: React.ReactNode +} + +export const EditChatTitleDialog = ({ + children, + initialValue, + chatId +}: EditChatTitleDialogProps) => { + const [open, setOpen] = React.useState(false) + const formRef = React.useRef(null) + const [input, setInput] = React.useState(initialValue) + + const handleSubmit: React.FormEventHandler = async e => { + e.preventDefault() + if (!input?.trim()) { + return + } + updateChat(chatId, { title: input }) + setOpen(false) + } + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + formRef.current?.requestSubmit() + e.preventDefault() + } + } + + return ( + + + + + + Edit + + + + Edit Chat Title + +
+ setInput(e.target.value)} + onKeyDown={onKeyDown} + /> +
+ + + + + Edit Title + +
+
+
+
+
+
+ ) +} diff --git a/ee/tabby-ui/components/header.tsx b/ee/tabby-ui/components/header.tsx index d83584c..c46ffa9 100644 --- a/ee/tabby-ui/components/header.tsx +++ b/ee/tabby-ui/components/header.tsx @@ -24,7 +24,7 @@ export function Header() { const newVersionAvailable = isNewVersionAvailable(version, latestRelease) return ( -
+
diff --git a/ee/tabby-ui/components/prompt-form.tsx b/ee/tabby-ui/components/prompt-form.tsx index 165c4c9..dcbe38a 100644 --- a/ee/tabby-ui/components/prompt-form.tsx +++ b/ee/tabby-ui/components/prompt-form.tsx @@ -32,12 +32,14 @@ export interface PromptProps isLoading: boolean } -export function PromptForm({ - onSubmit, - input, - setInput, - isLoading -}: PromptProps) { +export interface PromptFormRef { + focus: () => void +} + +function PromptFormRenderer( + { onSubmit, input, setInput, isLoading }: PromptProps, + ref: React.ForwardedRef +) { const { formRef, onKeyDown } = useEnterSubmit() const [queryCompletionUrl, setQueryCompletionUrl] = React.useState< string | null @@ -62,6 +64,24 @@ export function PromptForm({ } }) + React.useImperativeHandle(ref, () => { + return { + focus: () => { + inputRef.current?.focus() + } + } + }) + + React.useEffect(() => { + if ( + input && + inputRef.current && + inputRef.current !== document.activeElement + ) { + inputRef.current.focus() + } + }, [input]) + React.useLayoutEffect(() => { if (nextInputSelectionRange.current?.length) { inputRef.current?.setSelectionRange?.( @@ -268,6 +288,10 @@ export function PromptForm({ ) } +export const PromptForm = React.forwardRef( + PromptFormRenderer +) + /** * Retrieves the name of the completion query from a given string@. * @param {string} val - The input string to search for the completion query name. diff --git a/ee/tabby-ui/components/skeleton.tsx b/ee/tabby-ui/components/skeleton.tsx new file mode 100644 index 0000000..60f0673 --- /dev/null +++ b/ee/tabby-ui/components/skeleton.tsx @@ -0,0 +1,12 @@ +'use client' + +export const ListSkeleton = () => { + return ( +
    +
  • +
  • +
  • +
  • +
+ ) +} diff --git a/ee/tabby-ui/lib/hooks/use-hydration.ts b/ee/tabby-ui/lib/hooks/use-hydration.ts new file mode 100644 index 0000000..3823487 --- /dev/null +++ b/ee/tabby-ui/lib/hooks/use-hydration.ts @@ -0,0 +1,14 @@ +import React from 'react' + +let hydrating = true + +export function useHydrated() { + let [hydrated, setHydrated] = React.useState(() => !hydrating) + + React.useEffect(function hydrate() { + hydrating = false + setHydrated(true) + }, []) + + return hydrated +} diff --git a/ee/tabby-ui/lib/hooks/use-store.ts b/ee/tabby-ui/lib/hooks/use-store.ts new file mode 100644 index 0000000..06bceaf --- /dev/null +++ b/ee/tabby-ui/lib/hooks/use-store.ts @@ -0,0 +1,15 @@ +import React from 'react' + +export const useStore = ( + store: (callback: (state: T) => unknown) => unknown, + callback: (state: T) => F +) => { + const result = store(callback) as F + const [data, setData] = React.useState() + + React.useEffect(() => { + setData(result) + }, [result]) + + return data +} diff --git a/ee/tabby-ui/lib/stores/chat-actions.ts b/ee/tabby-ui/lib/stores/chat-actions.ts new file mode 100644 index 0000000..b4cdb31 --- /dev/null +++ b/ee/tabby-ui/lib/stores/chat-actions.ts @@ -0,0 +1,76 @@ +import { Message } from 'ai' +import { nanoid } from '@/lib/utils' +import type { Chat } from '@/lib/types' +import { useChatStore } from './chat-store' + +const get = useChatStore.getState +const set = useChatStore.setState + +export const updateHybrated = (state: boolean) => { + set(() => ({ _hasHydrated: state })) +} +export const setActiveChatId = (id: string) => { + set(() => ({ activeChatId: id })) +} + +export const addChat = (_id?: string, title?: string) => { + const id = _id ?? nanoid() + set(state => ({ + activeChatId: id, + chats: [ + { + id, + title: title ?? '', + messages: [], + createdAt: new Date(), + userId: '', + path: '' + }, + ...(state.chats || []) + ] + })) +} + +export const deleteChat = (id: string) => { + set(state => { + return { + activeChatId: nanoid(), + chats: state.chats?.filter(chat => chat.id !== id) + } + }) +} + +export const clearChats = () => { + set(() => ({ + activeChatId: nanoid(), + chats: [] + })) +} + +export const updateMessages = (id: string, messages: Message[]) => { + set(state => ({ + chats: state.chats?.map(chat => { + if (chat.id === id) { + return { + ...chat, + messages + } + } + return chat + }) + })) +} + +export const updateChat = (id: string, chat: Partial) => { + set(state => ({ + chats: state.chats?.map(c => { + if (c.id === id) { + return { + ...c, + ...chat + } + } + return c + }) + })) +} diff --git a/ee/tabby-ui/lib/stores/chat-store.ts b/ee/tabby-ui/lib/stores/chat-store.ts new file mode 100644 index 0000000..052bc34 --- /dev/null +++ b/ee/tabby-ui/lib/stores/chat-store.ts @@ -0,0 +1,50 @@ +import { Chat } from '@/lib/types' +import { create } from 'zustand' +import { persist } from 'zustand/middleware' +import { nanoid } from '@/lib/utils' + +const excludeFromState = ['_hasHydrated', 'setHasHydrated'] + +export interface ChatState { + chats: Chat[] | undefined + activeChatId: string | undefined + _hasHydrated: boolean + setHasHydrated: (state: boolean) => void +} + +const initialState: Omit = { + _hasHydrated: false, + chats: undefined, + activeChatId: nanoid() +} + +export const useChatStore = create()( + persist( + set => { + return { + ...initialState, + setHasHydrated: (state: boolean) => { + set({ + _hasHydrated: state + }) + } + } + }, + { + name: 'tabby-chat-storage', + partialize: state => + Object.fromEntries( + Object.entries(state).filter( + ([key]) => !excludeFromState.includes(key) + ) + ), + onRehydrateStorage() { + return state => { + if (state) { + state.setHasHydrated(true) + } + } + } + } + ) +) diff --git a/ee/tabby-ui/lib/stores/utils.ts b/ee/tabby-ui/lib/stores/utils.ts new file mode 100644 index 0000000..9f14100 --- /dev/null +++ b/ee/tabby-ui/lib/stores/utils.ts @@ -0,0 +1,9 @@ +import { Chat } from '@/lib/types' + +export const getChatById = ( + chats: Chat[] | undefined, + chatId: string | undefined +): Chat | undefined => { + if (!Array.isArray(chats) || !chatId) return undefined + return chats.find(c => c.id === chatId) +} diff --git a/ee/tabby-ui/lib/types/chat.ts b/ee/tabby-ui/lib/types/chat.ts new file mode 100644 index 0000000..acb0972 --- /dev/null +++ b/ee/tabby-ui/lib/types/chat.ts @@ -0,0 +1,29 @@ +import { type Message } from 'ai' + +export interface Chat extends Record { + id: string + title: string + createdAt: Date + userId: string + path: string + messages: Message[] + sharePath?: string +} + +export type ISearchHit = { + id: number + doc?: { + body?: string + name?: string + filepath?: string + git_url?: string + kind?: string + language?: string + } +} +export type SearchReponse = { + hits?: Array + num_hits?: number +} + +export type MessageActionType = 'edit' | 'delete' | 'regenerate' diff --git a/ee/tabby-ui/lib/types/common.ts b/ee/tabby-ui/lib/types/common.ts index 6f86a9c..ca3e53a 100644 --- a/ee/tabby-ui/lib/types/common.ts +++ b/ee/tabby-ui/lib/types/common.ts @@ -1,15 +1,3 @@ -import { type Message } from 'ai' - -export interface Chat extends Record { - id: string - title: string - createdAt: Date - userId: string - path: string - messages: Message[] - sharePath?: string -} - export type ServerActionResult = Promise< | Result | { diff --git a/ee/tabby-ui/lib/types/index.ts b/ee/tabby-ui/lib/types/index.ts index a022027..fa2431a 100644 --- a/ee/tabby-ui/lib/types/index.ts +++ b/ee/tabby-ui/lib/types/index.ts @@ -1,2 +1,2 @@ export * from './common' -export * from './search' +export * from './chat' diff --git a/ee/tabby-ui/lib/types/search.ts b/ee/tabby-ui/lib/types/search.ts deleted file mode 100644 index b030134..0000000 --- a/ee/tabby-ui/lib/types/search.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type ISearchHit = { - id: number - doc?: { - body?: string - name?: string - filepath?: string - git_url?: string - kind?: string - language?: string - } -} -export type SearchReponse = { - hits?: Array - num_hits?: number -} diff --git a/ee/tabby-ui/lib/utils.ts b/ee/tabby-ui/lib/utils.ts index cea3d17..bfe780c 100644 --- a/ee/tabby-ui/lib/utils.ts +++ b/ee/tabby-ui/lib/utils.ts @@ -41,3 +41,30 @@ export function formatDate(input: string | number | Date): string { year: 'numeric' }) } + +export function truncateText( + text: string, + maxLength = 50, + delimiters = /[ ,.:;\n,。:;]/ +) { + if (!text) return '' + if (text.length <= maxLength) { + return text + } + + let truncatedText = text.slice(0, maxLength) + + let lastDelimiterIndex = -1 + for (let i = maxLength - 1; i >= 0; i--) { + if (delimiters.test(truncatedText[i])) { + lastDelimiterIndex = i + break + } + } + + if (lastDelimiterIndex !== -1) { + truncatedText = truncatedText.slice(0, lastDelimiterIndex) + } + + return truncatedText + '...' +} diff --git a/ee/tabby-ui/package.json b/ee/tabby-ui/package.json index 9410ddd..f8b81a2 100644 --- a/ee/tabby-ui/package.json +++ b/ee/tabby-ui/package.json @@ -48,7 +48,8 @@ "react-textarea-autosize": "^8.4.1", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", - "swr": "^2.2.4" + "swr": "^2.2.4", + "zustand": "^4.4.6" }, "devDependencies": { "@tailwindcss/typography": "^0.5.9", diff --git a/ee/tabby-ui/yarn.lock b/ee/tabby-ui/yarn.lock index 940998d..a2ba9be 100644 --- a/ee/tabby-ui/yarn.lock +++ b/ee/tabby-ui/yarn.lock @@ -4430,7 +4430,7 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.2.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== @@ -4591,6 +4591,13 @@ zod@3.21.4: resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== +zustand@^4.4.6: + version "4.4.6" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.6.tgz#03c78e3e2686c47095c93714c0c600b72a6512bd" + integrity sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg== + dependencies: + use-sync-external-store "1.2.0" + zwitch@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"