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',
|
control: 'boolean',
|
||||||
description: 'Whether the title is editable or not'
|
description: 'Whether the title is editable or not'
|
||||||
},
|
},
|
||||||
editing: {
|
|
||||||
control: 'boolean',
|
|
||||||
description: 'Whether the title is currently in edit mode'
|
|
||||||
},
|
|
||||||
placeholder: {
|
placeholder: {
|
||||||
control: 'text',
|
control: 'text',
|
||||||
description: 'Placeholder text when the title is empty'
|
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
|
// Helper component to control editable state in Storybook
|
||||||
const EditableTitleContainer = (args: React.ComponentProps<typeof EditableTitle>) => {
|
const EditableTitleContainer = (args: React.ComponentProps<typeof EditableTitle>) => {
|
||||||
const [value, setValue] = React.useState(args.children);
|
const [value, setValue] = React.useState(args.children);
|
||||||
const [isEditing, setIsEditing] = React.useState(args.editing || false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
{...args}
|
{...args}
|
||||||
children={value}
|
children={value}
|
||||||
editing={isEditing}
|
|
||||||
onChange={(newValue) => {
|
onChange={(newValue) => {
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
args.onChange?.(newValue);
|
args.onChange?.(newValue);
|
||||||
}}
|
}}
|
||||||
onEdit={(editing) => {
|
|
||||||
setIsEditing(editing);
|
|
||||||
args.onEdit?.(editing);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -131,7 +121,6 @@ export const InitiallyEditing: Story = {
|
||||||
args: {
|
args: {
|
||||||
children: 'Initially in Edit Mode',
|
children: 'Initially in Edit Mode',
|
||||||
level: 4,
|
level: 4,
|
||||||
editing: true,
|
|
||||||
onChange: fn(),
|
onChange: fn(),
|
||||||
onEdit: fn()
|
onEdit: fn()
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,6 @@ export const EditableTitle = React.memo(
|
||||||
onSetValue?: (value: string) => void;
|
onSetValue?: (value: string) => void;
|
||||||
onPressEnter?: () => void;
|
onPressEnter?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
editing?: boolean;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
@ -47,7 +46,6 @@ export const EditableTitle = React.memo(
|
||||||
inputClassName = '',
|
inputClassName = '',
|
||||||
placeholder,
|
placeholder,
|
||||||
onPressEnter,
|
onPressEnter,
|
||||||
editing,
|
|
||||||
children,
|
children,
|
||||||
level = 4,
|
level = 4,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
@ -63,13 +61,6 @@ export const EditableTitle = React.memo(
|
||||||
setValue(children);
|
setValue(children);
|
||||||
}, [children]);
|
}, [children]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (editing) {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
inputRef.current?.select();
|
|
||||||
}
|
|
||||||
}, [editing]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
@ -73,7 +73,6 @@ export const TermIndividualContent: React.FC<{
|
||||||
<div className="mb-5 flex flex-col space-y-0.5">
|
<div className="mb-5 flex flex-col space-y-0.5">
|
||||||
<div className={'overflow-hidden'}>
|
<div className={'overflow-hidden'}>
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
editing={editingTermName}
|
|
||||||
onEdit={setEditingTermName}
|
onEdit={setEditingTermName}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
onSetTermName(v);
|
onSetTermName(v);
|
||||||
|
@ -94,7 +93,7 @@ export const TermIndividualContent: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<MoreDropdown termId={termId} setEditingTermName={setEditingTermName} />
|
<MoreDropdown termId={termId} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -153,10 +152,7 @@ const SkeletonLoader: React.FC = () => {
|
||||||
return <div className="p-4">{/* <Skeleton /> */}</div>;
|
return <div className="p-4">{/* <Skeleton /> */}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MoreDropdown: React.FC<{ termId: string; setEditingTermName: (value: boolean) => void }> = ({
|
const MoreDropdown: React.FC<{ termId: string }> = ({ termId }) => {
|
||||||
termId,
|
|
||||||
setEditingTermName
|
|
||||||
}) => {
|
|
||||||
const { mutateAsync: deleteTerm, isPending: isPendingDeleteTerm } = useDeleteTerm();
|
const { mutateAsync: deleteTerm, isPending: isPendingDeleteTerm } = useDeleteTerm();
|
||||||
const onChangePage = useAppLayoutContextSelector((s) => s.onChangePage);
|
const onChangePage = useAppLayoutContextSelector((s) => s.onChangePage);
|
||||||
|
|
||||||
|
@ -177,10 +173,7 @@ const MoreDropdown: React.FC<{ termId: string; setEditingTermName: (value: boole
|
||||||
{
|
{
|
||||||
value: 'edit',
|
value: 'edit',
|
||||||
icon: <EditSquare />,
|
icon: <EditSquare />,
|
||||||
label: 'Edit term title',
|
label: 'Edit term title'
|
||||||
onClick: () => {
|
|
||||||
setEditingTermName(true);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
|
@ -190,7 +183,7 @@ const MoreDropdown: React.FC<{ termId: string; setEditingTermName: (value: boole
|
||||||
onClick: onDeleteTermsPreflight
|
onClick: onDeleteTermsPreflight
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[setEditingTermName, onDeleteTermsPreflight]
|
[onDeleteTermsPreflight]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -6,13 +6,19 @@ import { ChatHeaderTitle } from './ChatHeaderTitle';
|
||||||
import { useChatIndividualContextSelector } from '../../ChatContext';
|
import { useChatIndividualContextSelector } from '../../ChatContext';
|
||||||
|
|
||||||
export const ChatHeader: React.FC<{}> = React.memo(({}) => {
|
export const ChatHeader: React.FC<{}> = React.memo(({}) => {
|
||||||
|
const chatId = useChatIndividualContextSelector((state) => state.chatId);
|
||||||
const chatTitle = useChatIndividualContextSelector((state) => state.chatTitle);
|
const chatTitle = useChatIndividualContextSelector((state) => state.chatTitle);
|
||||||
|
const isCompletedStream = useChatIndividualContextSelector((state) => state.isStreamingMessage);
|
||||||
|
|
||||||
if (!chatTitle) return null;
|
if (!chatTitle) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ChatHeaderTitle chatTitle={chatTitle || ''} />
|
<ChatHeaderTitle
|
||||||
|
chatTitle={chatTitle || ''}
|
||||||
|
chatId={chatId || ''}
|
||||||
|
isCompletedStream={isCompletedStream}
|
||||||
|
/>
|
||||||
<ChatHeaderOptions />
|
<ChatHeaderOptions />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import { Dropdown, DropdownItems } from '@/components/ui/dropdown';
|
import { Dropdown, DropdownItems } from '@/components/ui/dropdown';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useChatIndividualContextSelector } from '../../../ChatContext';
|
import { useChatIndividualContextSelector } from '../../../ChatContext';
|
||||||
import { Trash } from '@/components/ui/icons';
|
import { Copy, Trash, TextA, Pencil } from '@/components/ui/icons';
|
||||||
import { useDeleteChat } from '@/api/buster_rest/chats';
|
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<{
|
export const ChatContainerHeaderDropdown: React.FC<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}> = React.memo(({ children }) => {
|
}> = React.memo(({ children }) => {
|
||||||
const chatId = useChatIndividualContextSelector((state) => state.chatId);
|
const chatId = useChatIndividualContextSelector((state) => state.chatId);
|
||||||
const { mutate: deleteChat } = useDeleteChat();
|
const { mutate: deleteChat } = useDeleteChat();
|
||||||
|
const { mutate: duplicateChat } = useDuplicateChat();
|
||||||
|
const currentMessageId = useChatIndividualContextSelector((state) => state.currentMessageId);
|
||||||
|
|
||||||
const menuItem: DropdownItems = useMemo(() => {
|
const menuItem: DropdownItems = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
@ -16,15 +21,33 @@ export const ChatContainerHeaderDropdown: React.FC<{
|
||||||
value: 'delete',
|
value: 'delete',
|
||||||
icon: <Trash />,
|
icon: <Trash />,
|
||||||
onClick: () => chatId && deleteChat([chatId])
|
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 (
|
return <Dropdown items={menuItem}>{chatId ? children : null}</Dropdown>;
|
||||||
<div>
|
|
||||||
<Dropdown items={menuItem}>{chatId ? children : null}</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ChatContainerHeaderDropdown.displayName = 'ChatContainerHeaderDropdown';
|
ChatContainerHeaderDropdown.displayName = 'ChatContainerHeaderDropdown';
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Text } from '@/components/ui/typography';
|
import React, { useState } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import { useUpdateChat } from '@/api/buster_rest/chats';
|
||||||
|
import { EditableTitle } from '@/components/ui/typography/EditableTitle';
|
||||||
|
|
||||||
const animation = {
|
const animation = {
|
||||||
initial: { opacity: 0 },
|
initial: { opacity: 0 },
|
||||||
|
@ -11,15 +12,29 @@ const animation = {
|
||||||
transition: { duration: 0.25 }
|
transition: { duration: 0.25 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CHAT_HEADER_TITLE_ID = 'chat-header-title';
|
||||||
|
|
||||||
export const ChatHeaderTitle: React.FC<{
|
export const ChatHeaderTitle: React.FC<{
|
||||||
chatTitle: string;
|
chatTitle: string;
|
||||||
}> = React.memo(({ chatTitle }) => {
|
chatId: string;
|
||||||
|
isCompletedStream: boolean;
|
||||||
|
}> = React.memo(({ chatTitle, chatId }) => {
|
||||||
|
const { mutateAsync: updateChat } = useUpdateChat();
|
||||||
|
|
||||||
if (!chatTitle) return <div></div>;
|
if (!chatTitle) return <div></div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
<motion.div {...animation} key={chatTitle} className="flex items-center overflow-hidden">
|
<motion.div
|
||||||
<Text truncate>{chatTitle}</Text>
|
{...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>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
|
|
|
@ -153,6 +153,7 @@ export const MOCK_CHAT = (): BusterChat => {
|
||||||
id: faker.string.uuid(),
|
id: faker.string.uuid(),
|
||||||
title: faker.lorem.sentence(),
|
title: faker.lorem.sentence(),
|
||||||
is_favorited: faker.datatype.boolean(),
|
is_favorited: faker.datatype.boolean(),
|
||||||
|
feedback: null,
|
||||||
message_ids: messageIds,
|
message_ids: messageIds,
|
||||||
messages: messages.reduce<Record<string, BusterChatMessage>>((acc, m) => {
|
messages: messages.reduce<Record<string, BusterChatMessage>>((acc, m) => {
|
||||||
acc[m.id] = m;
|
acc[m.id] = m;
|
||||||
|
|
|
@ -107,7 +107,8 @@ refresh_interval: 300`,
|
||||||
public_password: null,
|
public_password: null,
|
||||||
public_expiry_date: null,
|
public_expiry_date: null,
|
||||||
public_enabled_by: null,
|
public_enabled_by: null,
|
||||||
password_secret_id: null
|
password_secret_id: null,
|
||||||
|
versions: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const response: BusterDashboardResponse = {
|
const response: BusterDashboardResponse = {
|
||||||
|
|
Loading…
Reference in New Issue