mirror of https://github.com/buster-so/buster.git
add additional dropdown menus
This commit is contained in:
parent
0ad6eb3a00
commit
41dbd8aea8
|
@ -19,10 +19,6 @@ const meta: Meta<typeof EditableTitle> = {
|
|||
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<typeof EditableTitle>;
|
|||
// Helper component to control editable state in Storybook
|
||||
const EditableTitleContainer = (args: React.ComponentProps<typeof EditableTitle>) => {
|
||||
const [value, setValue] = React.useState(args.children);
|
||||
const [isEditing, setIsEditing] = React.useState(args.editing || false);
|
||||
|
||||
return (
|
||||
<EditableTitle
|
||||
{...args}
|
||||
children={value}
|
||||
editing={isEditing}
|
||||
onChange={(newValue) => {
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
|
@ -73,7 +73,6 @@ export const TermIndividualContent: React.FC<{
|
|||
<div className="mb-5 flex flex-col space-y-0.5">
|
||||
<div className={'overflow-hidden'}>
|
||||
<EditableTitle
|
||||
editing={editingTermName}
|
||||
onEdit={setEditingTermName}
|
||||
onChange={(v) => {
|
||||
onSetTermName(v);
|
||||
|
@ -94,7 +93,7 @@ export const TermIndividualContent: React.FC<{
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<MoreDropdown termId={termId} setEditingTermName={setEditingTermName} />
|
||||
<MoreDropdown termId={termId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -153,10 +152,7 @@ const SkeletonLoader: React.FC = () => {
|
|||
return <div className="p-4">{/* <Skeleton /> */}</div>;
|
||||
};
|
||||
|
||||
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: <EditSquare />,
|
||||
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 (
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<ChatHeaderTitle chatTitle={chatTitle || ''} />
|
||||
<ChatHeaderTitle
|
||||
chatTitle={chatTitle || ''}
|
||||
chatId={chatId || ''}
|
||||
isCompletedStream={isCompletedStream}
|
||||
/>
|
||||
<ChatHeaderOptions />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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: <Trash />,
|
||||
onClick: () => chatId && deleteChat([chatId])
|
||||
},
|
||||
{
|
||||
label: 'Duplicate chat',
|
||||
value: 'duplicate',
|
||||
icon: <Copy />,
|
||||
onClick: () =>
|
||||
chatId &&
|
||||
duplicateChat({ id: chatId, message_id: currentMessageId, share_with_same_people: false })
|
||||
},
|
||||
{
|
||||
label: 'Edit chat title',
|
||||
value: 'edit-chat-title',
|
||||
icon: <Pencil />,
|
||||
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 (
|
||||
<div>
|
||||
<Dropdown items={menuItem}>{chatId ? children : null}</Dropdown>
|
||||
</div>
|
||||
);
|
||||
return <Dropdown items={menuItem}>{chatId ? children : null}</Dropdown>;
|
||||
});
|
||||
|
||||
ChatContainerHeaderDropdown.displayName = 'ChatContainerHeaderDropdown';
|
||||
|
|
|
@ -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 <div></div>;
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div {...animation} key={chatTitle} className="flex items-center overflow-hidden">
|
||||
<Text truncate>{chatTitle}</Text>
|
||||
<motion.div
|
||||
{...animation}
|
||||
key={chatTitle}
|
||||
className="flex w-full items-center overflow-hidden">
|
||||
<EditableTitle
|
||||
className="w-full"
|
||||
id={CHAT_HEADER_TITLE_ID}
|
||||
onChange={(value) => updateChat({ id: chatId, title: value })}>
|
||||
{chatTitle}
|
||||
</EditableTitle>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
|
|
@ -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<Record<string, BusterChatMessage>>((acc, m) => {
|
||||
acc[m.id] = m;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue