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', 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()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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