add additional dropdown menus

This commit is contained in:
Nate Kelley 2025-03-17 17:50:46 -06:00
parent 0ad6eb3a00
commit 41dbd8aea8
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
8 changed files with 65 additions and 46 deletions

View File

@ -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()
}

View File

@ -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}

View File

@ -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 (

View File

@ -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 />
</>
);

View File

@ -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';

View File

@ -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>
);

View File

@ -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;

View File

@ -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 = {