diff --git a/web/src/components/ui/typography/EditableTitle.stories.tsx b/web/src/components/ui/typography/EditableTitle.stories.tsx index 7c5c70b5b..569ae84f5 100644 --- a/web/src/components/ui/typography/EditableTitle.stories.tsx +++ b/web/src/components/ui/typography/EditableTitle.stories.tsx @@ -19,10 +19,6 @@ const meta: Meta = { control: 'boolean', description: 'Whether the title is editable or not' }, - editing: { - control: 'boolean', - description: 'Whether the title is currently in edit mode' - }, placeholder: { control: 'text', description: 'Placeholder text when the title is empty' @@ -44,21 +40,15 @@ type Story = StoryObj; // Helper component to control editable state in Storybook const EditableTitleContainer = (args: React.ComponentProps) => { const [value, setValue] = React.useState(args.children); - const [isEditing, setIsEditing] = React.useState(args.editing || false); return ( { setValue(newValue); args.onChange?.(newValue); }} - onEdit={(editing) => { - setIsEditing(editing); - args.onEdit?.(editing); - }} /> ); }; @@ -131,7 +121,6 @@ export const InitiallyEditing: Story = { args: { children: 'Initially in Edit Mode', level: 4, - editing: true, onChange: fn(), onEdit: fn() } diff --git a/web/src/components/ui/typography/EditableTitle.tsx b/web/src/components/ui/typography/EditableTitle.tsx index 7d6a05a31..bcbe38e11 100644 --- a/web/src/components/ui/typography/EditableTitle.tsx +++ b/web/src/components/ui/typography/EditableTitle.tsx @@ -30,7 +30,6 @@ export const EditableTitle = React.memo( onSetValue?: (value: string) => void; onPressEnter?: () => void; disabled?: boolean; - editing?: boolean; className?: string; placeholder?: string; style?: React.CSSProperties; @@ -47,7 +46,6 @@ export const EditableTitle = React.memo( inputClassName = '', placeholder, onPressEnter, - editing, children, level = 4, onEdit, @@ -63,13 +61,6 @@ export const EditableTitle = React.memo( setValue(children); }, [children]); - useEffect(() => { - if (editing) { - inputRef.current?.focus(); - inputRef.current?.select(); - } - }, [editing]); - return (
{ onSetTermName(v); @@ -94,7 +93,7 @@ export const TermIndividualContent: React.FC<{
- +
@@ -153,10 +152,7 @@ const SkeletonLoader: React.FC = () => { return
{/* */}
; }; -const MoreDropdown: React.FC<{ termId: string; setEditingTermName: (value: boolean) => void }> = ({ - termId, - setEditingTermName -}) => { +const MoreDropdown: React.FC<{ termId: string }> = ({ termId }) => { const { mutateAsync: deleteTerm, isPending: isPendingDeleteTerm } = useDeleteTerm(); const onChangePage = useAppLayoutContextSelector((s) => s.onChangePage); @@ -177,10 +173,7 @@ const MoreDropdown: React.FC<{ termId: string; setEditingTermName: (value: boole { value: 'edit', icon: , - label: 'Edit term title', - onClick: () => { - setEditingTermName(true); - } + label: 'Edit term title' }, { value: 'delete', @@ -190,7 +183,7 @@ const MoreDropdown: React.FC<{ termId: string; setEditingTermName: (value: boole onClick: onDeleteTermsPreflight } ], - [setEditingTermName, onDeleteTermsPreflight] + [onDeleteTermsPreflight] ); return ( diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx index 10a98591b..e2323cf91 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeader.tsx @@ -6,13 +6,19 @@ import { ChatHeaderTitle } from './ChatHeaderTitle'; import { useChatIndividualContextSelector } from '../../ChatContext'; export const ChatHeader: React.FC<{}> = React.memo(({}) => { + const chatId = useChatIndividualContextSelector((state) => state.chatId); const chatTitle = useChatIndividualContextSelector((state) => state.chatTitle); + const isCompletedStream = useChatIndividualContextSelector((state) => state.isStreamingMessage); if (!chatTitle) return null; return ( <> - + ); diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderOptions/ChatHeaderDropdown.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderOptions/ChatHeaderDropdown.tsx index 7dd239483..4f699c934 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderOptions/ChatHeaderDropdown.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderOptions/ChatHeaderDropdown.tsx @@ -1,14 +1,19 @@ import { Dropdown, DropdownItems } from '@/components/ui/dropdown'; import React, { useMemo } from 'react'; import { useChatIndividualContextSelector } from '../../../ChatContext'; -import { Trash } from '@/components/ui/icons'; -import { useDeleteChat } from '@/api/buster_rest/chats'; +import { Copy, Trash, TextA, Pencil } from '@/components/ui/icons'; +import { duplicateChat, useDeleteChat, useDuplicateChat } from '@/api/buster_rest/chats'; +import { CHAT_HEADER_TITLE_ID } from '../ChatHeaderTitle'; +import { timeout } from '@/lib'; export const ChatContainerHeaderDropdown: React.FC<{ children: React.ReactNode; }> = React.memo(({ children }) => { const chatId = useChatIndividualContextSelector((state) => state.chatId); const { mutate: deleteChat } = useDeleteChat(); + const { mutate: duplicateChat } = useDuplicateChat(); + const currentMessageId = useChatIndividualContextSelector((state) => state.currentMessageId); + const menuItem: DropdownItems = useMemo(() => { return [ { @@ -16,15 +21,33 @@ export const ChatContainerHeaderDropdown: React.FC<{ value: 'delete', icon: , onClick: () => chatId && deleteChat([chatId]) + }, + { + label: 'Duplicate chat', + value: 'duplicate', + icon: , + onClick: () => + chatId && + duplicateChat({ id: chatId, message_id: currentMessageId, share_with_same_people: false }) + }, + { + label: 'Edit chat title', + value: 'edit-chat-title', + icon: , + onClick: async () => { + const input = document.getElementById(CHAT_HEADER_TITLE_ID) as HTMLInputElement; + if (input) { + await timeout(25); + input.focus(); + input.select(); + console.log('input', input.select); + } + } } ]; - }, []); + }, [chatId, currentMessageId, deleteChat, duplicateChat]); - return ( -
- {chatId ? children : null} -
- ); + return {chatId ? children : null}; }); ChatContainerHeaderDropdown.displayName = 'ChatContainerHeaderDropdown'; diff --git a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx index dffb539b6..41439ef98 100644 --- a/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx +++ b/web/src/layouts/ChatLayout/ChatContainer/ChatHeader/ChatHeaderTitle.tsx @@ -1,8 +1,9 @@ 'use client'; -import { Text } from '@/components/ui/typography'; -import React from 'react'; +import React, { useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; +import { useUpdateChat } from '@/api/buster_rest/chats'; +import { EditableTitle } from '@/components/ui/typography/EditableTitle'; const animation = { initial: { opacity: 0 }, @@ -11,15 +12,29 @@ const animation = { transition: { duration: 0.25 } }; +export const CHAT_HEADER_TITLE_ID = 'chat-header-title'; + export const ChatHeaderTitle: React.FC<{ chatTitle: string; -}> = React.memo(({ chatTitle }) => { + chatId: string; + isCompletedStream: boolean; +}> = React.memo(({ chatTitle, chatId }) => { + const { mutateAsync: updateChat } = useUpdateChat(); + if (!chatTitle) return
; return ( - - {chatTitle} + + updateChat({ id: chatId, title: value })}> + {chatTitle} + ); diff --git a/web/src/mocks/MOCK_CHAT.ts b/web/src/mocks/MOCK_CHAT.ts index 92d663212..9875cd350 100644 --- a/web/src/mocks/MOCK_CHAT.ts +++ b/web/src/mocks/MOCK_CHAT.ts @@ -153,6 +153,7 @@ export const MOCK_CHAT = (): BusterChat => { id: faker.string.uuid(), title: faker.lorem.sentence(), is_favorited: faker.datatype.boolean(), + feedback: null, message_ids: messageIds, messages: messages.reduce>((acc, m) => { acc[m.id] = m; diff --git a/web/src/mocks/MOCK_DASHBOARD.ts b/web/src/mocks/MOCK_DASHBOARD.ts index 61bdb4764..9be05d8dd 100644 --- a/web/src/mocks/MOCK_DASHBOARD.ts +++ b/web/src/mocks/MOCK_DASHBOARD.ts @@ -107,7 +107,8 @@ refresh_interval: 300`, public_password: null, public_expiry_date: null, public_enabled_by: null, - password_secret_id: null + password_secret_id: null, + versions: [] }; const response: BusterDashboardResponse = {