style: Align project menus and composer

This commit is contained in:
Danny Avila 2026-06-02 17:30:13 -04:00
parent c66b700a5b
commit 6741b70f26
8 changed files with 321 additions and 218 deletions

View file

@ -3,11 +3,18 @@ import { useRecoilValue } from 'recoil';
import { useForm } from 'react-hook-form';
import { Spinner } from '@librechat/client';
import { useParams } from 'react-router-dom';
import { Folder } from 'lucide-react';
import { Constants, buildTree } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
import type { TChatProject, TMessage } from 'librechat-data-provider';
import type { ChatFormValues } from '~/common';
import { ChatContext, AddedChatContext, ChatFormProvider, useFileMapContext } from '~/Providers';
import { useAddedResponse, useResumeOnLoad, useAdaptiveSSE, useChatHelpers } from '~/hooks';
import {
useAddedResponse,
useResumeOnLoad,
useAdaptiveSSE,
useChatHelpers,
useLocalize,
} from '~/hooks';
import ConversationStarters from './Input/ConversationStarters';
import { useGetMessagesByConvoId } from '~/data-provider';
import MessagesView from './Messages/MessagesView';
@ -29,8 +36,27 @@ function LoadingSpinner() {
);
}
function ChatView({ index = 0 }: { index?: number }) {
function ProjectLanding({ project }: { project: TChatProject }) {
return (
<div className="flex h-full max-h-full transform-gpu flex-col items-center justify-center pb-16 transition-all duration-200 sm:max-h-0">
<div className="flex max-w-2xl flex-col items-center gap-3 px-4 text-center">
<div className="flex items-center gap-3">
<Folder className="h-9 w-9 shrink-0 text-text-secondary" aria-hidden="true" />
<h1 className="min-w-0 truncate text-2xl font-medium text-text-primary sm:text-4xl">
{project.name}
</h1>
</div>
{project.description && (
<p className="max-w-md text-sm text-text-secondary">{project.description}</p>
)}
</div>
</div>
);
}
function ChatView({ index = 0, project }: { index?: number; project?: TChatProject }) {
const { conversationId } = useParams();
const localize = useLocalize();
const rootSubmission = useRecoilValue(store.submissionByIndex(index));
const isSubmitting = useRecoilValue(store.isSubmittingFamily(index));
const centerFormOnLanding = useRecoilValue(store.centerFormOnLanding);
@ -70,6 +96,7 @@ function ChatView({ index = 0 }: { index?: number }) {
(!messagesTree || messagesTree.length === 0) &&
(conversationId === Constants.NEW_CONVO || !conversationId);
const isNavigating = (!messagesTree || messagesTree.length === 0) && conversationId != null;
const isProjectLandingPage = isLandingPage && project != null;
if (isLoading && conversationId !== Constants.NEW_CONVO) {
content = <LoadingSpinner />;
@ -77,10 +104,17 @@ function ChatView({ index = 0 }: { index?: number }) {
content = <LoadingSpinner />;
} else if (!isLandingPage) {
content = <MessagesView messagesTree={messagesTree} />;
} else if (isProjectLandingPage && project) {
content = <ProjectLanding project={project} />;
} else {
content = <Landing centerFormOnLanding={centerFormOnLanding} />;
}
const chatFormPlaceholder =
isProjectLandingPage && project
? localize('com_ui_new_chat_in_project', { name: project.name })
: undefined;
return (
<ChatFormProvider {...methods}>
<ChatContext.Provider value={chatHelpers}>
@ -104,7 +138,7 @@ function ChatView({ index = 0 }: { index?: number }) {
isLandingPage && 'max-w-3xl transition-all duration-200 xl:max-w-4xl',
)}
>
<ChatForm index={index} />
<ChatForm index={index} placeholder={chatFormPlaceholder} />
{isLandingPage ? <ConversationStarters /> : <Footer />}
</div>
</div>

View file

@ -42,6 +42,7 @@ import store from '~/store';
interface ChatFormProps {
index: number;
placeholder?: string;
/** From ChatContext — individual values so memo can compare them */
files: Map<string, ExtendedFile>;
setFiles: FileSetter;
@ -55,6 +56,7 @@ interface ChatFormProps {
const ChatForm = memo(function ChatForm({
index,
placeholder,
files,
setFiles,
conversation,
@ -177,6 +179,7 @@ const ChatForm = memo(function ChatForm({
submitButtonRef,
setIsScrollable,
disabled: disableInputs,
placeholder,
});
useQueryParams({ textAreaRef });
@ -413,7 +416,7 @@ ChatForm.displayName = 'ChatForm';
* to the memo'd ChatForm. This prevents ChatForm from re-rendering on every
* streaming chunk it only re-renders when the specific values it uses change.
*/
function ChatFormWrapper({ index = 0 }: { index?: number }) {
function ChatFormWrapper({ index = 0, placeholder }: { index?: number; placeholder?: string }) {
const {
files,
setFiles,
@ -465,6 +468,7 @@ function ChatFormWrapper({ index = 0 }: { index?: number }) {
return (
<ChatForm
index={index}
placeholder={placeholder}
files={files}
setFiles={setFiles}
conversation={stableConversation}

View file

@ -1,15 +1,8 @@
import { memo, type FC, type ReactNode } from 'react';
import { memo, useId, useMemo, useState, type FC, type ReactNode } from 'react';
import * as Ariakit from '@ariakit/react';
import { Check, Clock3, Ellipsis, Folder, FolderPlus, SquarePen, ChevronDown } from 'lucide-react';
import {
TooltipAnchor,
DropdownMenu,
DropdownMenuItem,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from '@librechat/client';
import { DropdownPopup, TooltipAnchor } from '@librechat/client';
import type { MenuItemProps, RenderProp } from '~/common';
import type { SidebarChatSort, SidebarOrganizationMode } from './types';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -33,14 +26,50 @@ const headerIconButtonClassName =
const menuIconClassName = 'h-4 w-4 text-text-secondary';
const SelectionMark = memo(({ isSelected }: { isSelected: boolean }) => {
if (!isSelected) {
return <span className="ml-auto h-4 w-4" aria-hidden="true" />;
}
return <Check className="ml-auto h-4 w-4 text-text-primary" aria-hidden="true" />;
});
function renderSelectedMenuItem(label: string, icon: ReactNode, isSelected: boolean): RenderProp {
return function SelectedMenuItem({ className, ...props }) {
return (
<div {...props} className={cn(className, 'justify-between gap-5')}>
<span className="flex min-w-0 items-center gap-2">
<span className="mr-0 flex h-4 w-4 shrink-0 items-center justify-center" aria-hidden>
{icon}
</span>
<span className="truncate">{label}</span>
</span>
{isSelected ? (
<Check className="h-4 w-4 shrink-0 text-text-primary" aria-hidden="true" />
) : (
<span className="h-4 w-4 shrink-0" aria-hidden="true" />
)}
</div>
);
};
}
SelectionMark.displayName = 'ChatsHeaderSelectionMark';
function createSelectedMenuItem<T extends string>({
id,
value,
label,
icon,
selectedValue,
onSelect,
}: {
id: string;
value: T;
label: string;
icon: ReactNode;
selectedValue: T;
onSelect: (value: T) => void;
}): MenuItemProps {
const isSelected = selectedValue === value;
return {
id,
ariaLabel: label,
ariaChecked: isSelected,
onClick: () => onSelect(value),
render: renderSelectedMenuItem(label, icon, isSelected),
};
}
const ChatsHeader: FC<ChatsHeaderProps> = ({
isExpanded,
@ -53,45 +82,68 @@ const ChatsHeader: FC<ChatsHeaderProps> = ({
onNewChat,
}) => {
const localize = useLocalize();
const menuId = useId();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const organizationItems: Array<{
value: SidebarOrganizationMode;
label: string;
icon: ReactNode;
}> = [
{
value: 'byProject',
label: localize('com_ui_sidebar_mode_by_project'),
icon: <Folder className={menuIconClassName} aria-hidden="true" />,
},
{
value: 'recentProjects',
label: localize('com_ui_sidebar_mode_recent_projects'),
icon: <Folder className={menuIconClassName} aria-hidden="true" />,
},
{
value: 'chronological',
label: localize('com_ui_sidebar_mode_chronological_list'),
icon: <Clock3 className={menuIconClassName} aria-hidden="true" />,
},
];
const sortItems: Array<{
value: SidebarChatSort;
label: string;
icon: ReactNode;
}> = [
{
value: 'createdAt',
label: localize('com_ui_sort_created'),
icon: <Clock3 className={menuIconClassName} aria-hidden="true" />,
},
{
value: 'updatedAt',
label: localize('com_ui_sort_updated'),
icon: <SquarePen className={menuIconClassName} aria-hidden="true" />,
},
];
const dropdownItems = useMemo<MenuItemProps[]>(
() => [
{
id: 'organize-sidebar',
label: localize('com_ui_sidebar_organization_label'),
icon: <Folder className={menuIconClassName} aria-hidden="true" />,
subItems: [
createSelectedMenuItem<SidebarOrganizationMode>({
id: 'organize-by-project',
value: 'byProject',
label: localize('com_ui_sidebar_mode_by_project'),
icon: <Folder className={menuIconClassName} aria-hidden="true" />,
selectedValue: organizationMode,
onSelect: onOrganizationModeChange,
}),
createSelectedMenuItem<SidebarOrganizationMode>({
id: 'organize-recent-projects',
value: 'recentProjects',
label: localize('com_ui_sidebar_mode_recent_projects'),
icon: <Folder className={menuIconClassName} aria-hidden="true" />,
selectedValue: organizationMode,
onSelect: onOrganizationModeChange,
}),
createSelectedMenuItem<SidebarOrganizationMode>({
id: 'organize-chronological',
value: 'chronological',
label: localize('com_ui_sidebar_mode_chronological_list'),
icon: <Clock3 className={menuIconClassName} aria-hidden="true" />,
selectedValue: organizationMode,
onSelect: onOrganizationModeChange,
}),
],
},
{
id: 'sort-chats',
label: localize('com_ui_sort_by'),
icon: <Clock3 className={menuIconClassName} aria-hidden="true" />,
subItems: [
createSelectedMenuItem<SidebarChatSort>({
id: 'sort-created',
value: 'createdAt',
label: localize('com_ui_sort_created'),
icon: <Clock3 className={menuIconClassName} aria-hidden="true" />,
selectedValue: chatSortBy,
onSelect: onChatSortByChange,
}),
createSelectedMenuItem<SidebarChatSort>({
id: 'sort-updated',
value: 'updatedAt',
label: localize('com_ui_sort_updated'),
icon: <SquarePen className={menuIconClassName} aria-hidden="true" />,
selectedValue: chatSortBy,
onSelect: onChatSortByChange,
}),
],
},
],
[chatSortBy, localize, onChatSortByChange, onOrganizationModeChange, organizationMode],
);
return (
<div className="flex h-8 w-full items-center gap-0.5">
@ -111,60 +163,34 @@ const ChatsHeader: FC<ChatsHeaderProps> = ({
/>
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={localize('com_nav_convo_menu_options')}
title={localize('com_nav_convo_menu_options')}
className={headerIconButtonClassName}
>
<Ellipsis className="h-4 w-4" aria-hidden="true" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Folder className={menuIconClassName} aria-hidden="true" />
<span>{localize('com_ui_sidebar_organization_label')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56">
{organizationItems.map((item) => (
<DropdownMenuItem
key={item.value}
role="menuitemradio"
aria-checked={organizationMode === item.value}
onSelect={() => onOrganizationModeChange(item.value)}
>
{item.icon}
<span>{item.label}</span>
<SelectionMark isSelected={organizationMode === item.value} />
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Clock3 className={menuIconClassName} aria-hidden="true" />
<span>{localize('com_ui_sort_by')}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-44">
{sortItems.map((item) => (
<DropdownMenuItem
key={item.value}
role="menuitemradio"
aria-checked={chatSortBy === item.value}
onSelect={() => onChatSortByChange(item.value)}
>
{item.icon}
<span>{item.label}</span>
<SelectionMark isSelected={chatSortBy === item.value} />
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
<DropdownPopup
portal={true}
focusLoop={true}
unmountOnHide={true}
menuId={menuId}
isOpen={isMenuOpen}
setIsOpen={setIsMenuOpen}
className="z-[125] min-w-56"
iconClassName="mr-0 text-text-secondary"
trigger={
<TooltipAnchor
description={localize('com_nav_convo_menu_options')}
render={
<Ariakit.MenuButton
id="chats-header-menu-button"
aria-label={localize('com_nav_convo_menu_options')}
className={cn(
headerIconButtonClassName,
isMenuOpen && 'bg-surface-hover text-text-primary',
)}
>
<Ellipsis className="h-4 w-4" aria-hidden="true" />
</Ariakit.MenuButton>
}
/>
}
items={dropdownItems}
/>
<TooltipAnchor
description={localize('com_ui_new_project')}

View file

@ -1,27 +1,39 @@
import { useCallback, useMemo, useState, type FormEvent } from 'react';
import { ArrowLeft, ArrowUpDown, Check, Folder, Plus, Send } from 'lucide-react';
import { useCallback, useId, useMemo, useState } from 'react';
import * as Ariakit from '@ariakit/react';
import { ArrowLeft, ArrowUpDown, Check, Folder, Plus } from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom';
import {
Button,
Spinner,
DropdownMenu,
DropdownMenuItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@librechat/client';
import type { ConversationListResponse } from 'librechat-data-provider';
import { Button, Spinner, DropdownPopup } from '@librechat/client';
import type { MenuItemProps, RenderProp } from '~/common';
import { useConversationsInfiniteQuery, useProjectQuery } from '~/data-provider';
import { useLocalize, useNewConvo } from '~/hooks';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import ProjectChatList from './ProjectChatList';
type ChatSortField = 'updatedAt' | 'createdAt';
function renderSortMenuItem(label: string, isSelected: boolean): RenderProp {
return function SortMenuItem({ className, ...props }) {
return (
<div {...props} className={cn(className, 'justify-between gap-5')}>
<span className="truncate">{label}</span>
{isSelected ? (
<Check className="h-4 w-4 shrink-0 text-text-primary" aria-hidden="true" />
) : (
<span className="h-4 w-4 shrink-0" aria-hidden="true" />
)}
</div>
);
};
}
export default function ProjectWorkspace() {
const localize = useLocalize();
const navigate = useNavigate();
const { projectId = '' } = useParams();
const { newConversation } = useNewConvo();
const [sortBy, setSortBy] = useState<ChatSortField>('updatedAt');
const sortMenuId = useId();
const [isSortMenuOpen, setIsSortMenuOpen] = useState(false);
const { data: project, isLoading: isProjectLoading } = useProjectQuery(projectId);
const activeProjectId = project?._id;
const sortOptions = useMemo(
@ -33,6 +45,20 @@ export default function ProjectWorkspace() {
);
const selectedSortLabel =
sortOptions.find((option) => option.value === sortBy)?.label ?? localize('com_ui_sort_updated');
const sortMenuItems = useMemo<MenuItemProps[]>(
() =>
sortOptions.map((option) => {
const isSelected = sortBy === option.value;
return {
id: `project-chat-sort-${option.value}`,
ariaLabel: option.label,
ariaChecked: isSelected,
onClick: () => setSortBy(option.value),
render: renderSortMenuItem(option.label, isSelected),
};
}),
[sortBy, sortOptions],
);
const {
data,
@ -66,19 +92,14 @@ export default function ProjectWorkspace() {
return lastPage.nextCursor !== null;
}, [data?.pages]);
const startProjectChat = useCallback(
(event?: FormEvent<HTMLFormElement>) => {
event?.preventDefault();
if (!activeProjectId) {
return;
}
newConversation({ template: { chatProjectId: activeProjectId } });
navigate(`/c/new?projectId=${encodeURIComponent(activeProjectId)}`, {
state: { focusChat: true },
});
},
[activeProjectId, navigate, newConversation],
);
const startProjectChat = useCallback(() => {
if (!activeProjectId) {
return;
}
navigate(`/c/new?projectId=${encodeURIComponent(activeProjectId)}`, {
state: { focusChat: true },
});
}, [activeProjectId, navigate]);
if (isProjectLoading) {
return (
@ -126,30 +147,6 @@ export default function ProjectWorkspace() {
</Button>
</header>
<form
onSubmit={startProjectChat}
className="rounded-2xl border border-border-medium bg-surface-primary p-4 shadow-sm"
>
<div className="flex min-h-24 items-start gap-3">
<Plus className="mt-1 h-5 w-5 shrink-0 text-text-secondary" aria-hidden="true" />
<input
className="min-w-0 flex-1 bg-transparent text-lg outline-none placeholder:text-text-tertiary"
placeholder={localize('com_ui_new_chat_in_project', { name: project.name })}
readOnly
onFocus={() => startProjectChat()}
/>
<Button
type="submit"
variant="submit"
size="icon"
className="h-9 w-9 shrink-0"
aria-label={localize('com_ui_new_chat')}
>
<Send className="h-4 w-4" aria-hidden="true" />
</Button>
</div>
</form>
<section className="flex min-h-[360px] flex-1 flex-col gap-3">
<div className="flex items-center justify-between gap-3">
<div className="inline-flex rounded-lg border border-border-light p-1">
@ -160,30 +157,28 @@ export default function ProjectWorkspace() {
{localize('com_ui_chats')}
</button>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="border-border-medium bg-transparent"
<DropdownPopup
portal={true}
focusLoop={true}
unmountOnHide={true}
menuId={sortMenuId}
isOpen={isSortMenuOpen}
setIsOpen={setIsSortMenuOpen}
className="z-[125] min-w-44"
trigger={
<Ariakit.MenuButton
aria-label={localize('com_ui_sort_chats_by')}
className={cn(
'inline-flex h-9 items-center justify-center gap-2 whitespace-nowrap rounded-lg border border-border-medium bg-transparent px-3 text-sm font-medium text-text-primary ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
isSortMenuOpen && 'bg-surface-hover text-text-primary',
)}
>
<ArrowUpDown className="h-4 w-4 text-text-secondary" aria-hidden="true" />
{selectedSortLabel}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
{sortOptions.map((option) => (
<DropdownMenuItem key={option.value} onSelect={() => setSortBy(option.value)}>
<span>{option.label}</span>
{sortBy === option.value && (
<Check className="ml-auto h-4 w-4 text-text-primary" aria-hidden="true" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</Ariakit.MenuButton>
}
items={sortMenuItems}
/>
</div>
<ProjectChatList
conversations={conversations}

View file

@ -1,17 +1,10 @@
import { useDeferredValue, useMemo, useState, type FormEvent } from 'react';
import { useDeferredValue, useId, useMemo, useState, type FormEvent } from 'react';
import * as Ariakit from '@ariakit/react';
import { ArrowUpDown, Check, Folder, Plus, Search } from 'lucide-react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import {
Input,
Button,
Spinner,
DropdownMenu,
DropdownMenuItem,
DropdownMenuContent,
DropdownMenuTrigger,
useToastContext,
} from '@librechat/client';
import type { TChatProject } from 'librechat-data-provider';
import { Input, Button, Spinner, DropdownPopup, useToastContext } from '@librechat/client';
import type { MenuItemProps, RenderProp } from '~/common';
import { useCreateProjectMutation, useProjectsInfiniteQuery } from '~/data-provider';
import { useLocalize } from '~/hooks';
import { NotificationSeverity } from '~/common';
@ -19,6 +12,21 @@ import { cn } from '~/utils';
type ProjectSort = 'name' | 'createdAt' | 'lastConversationAt';
function renderSortMenuItem(label: string, isSelected: boolean): RenderProp {
return function SortMenuItem({ className, ...props }) {
return (
<div {...props} className={cn(className, 'justify-between gap-5')}>
<span className="truncate">{label}</span>
{isSelected ? (
<Check className="h-4 w-4 shrink-0 text-text-primary" aria-hidden="true" />
) : (
<span className="h-4 w-4 shrink-0" aria-hidden="true" />
)}
</div>
);
};
}
function formatActivity(project: TChatProject, fallback: string) {
const value = project.lastConversationAt ?? project.updatedAt ?? project.createdAt;
return value ? new Date(value).toLocaleString() : fallback;
@ -31,6 +39,8 @@ export default function ProjectsView() {
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState<ProjectSort>('lastConversationAt');
const [isCreating, setIsCreating] = useState(searchParams.get('new') === '1');
const sortMenuId = useId();
const [isSortMenuOpen, setIsSortMenuOpen] = useState(false);
const [name, setName] = useState('');
const deferredSearch = useDeferredValue(search);
const createProject = useCreateProjectMutation();
@ -55,6 +65,20 @@ export default function ProjectsView() {
const selectedSortLabel =
sortOptions.find((option) => option.value === sortBy)?.label ??
localize('com_ui_latest_activity');
const sortMenuItems = useMemo<MenuItemProps[]>(
() =>
sortOptions.map((option) => {
const isSelected = sortBy === option.value;
return {
id: `project-sort-${option.value}`,
ariaLabel: option.label,
ariaChecked: isSelected,
onClick: () => setSortBy(option.value),
render: renderSortMenuItem(option.label, isSelected),
};
}),
[sortBy, sortOptions],
);
const handleCreate = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
@ -103,13 +127,21 @@ export default function ProjectsView() {
className="border-border-medium bg-transparent pl-9 text-text-primary placeholder:text-text-secondary focus-visible:ring-2 focus-visible:ring-ring-primary"
/>
</label>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="outline"
className="h-10 justify-between border-border-medium bg-transparent px-3 sm:w-56"
<DropdownPopup
portal={true}
focusLoop={true}
unmountOnHide={true}
menuId={sortMenuId}
isOpen={isSortMenuOpen}
setIsOpen={setIsSortMenuOpen}
className="z-[125] min-w-56"
trigger={
<Ariakit.MenuButton
aria-label={localize('com_ui_sort_projects_by')}
className={cn(
'inline-flex h-10 items-center justify-between gap-2 whitespace-nowrap rounded-lg border border-border-medium bg-transparent px-3 text-sm font-medium text-text-primary ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 sm:w-56',
isSortMenuOpen && 'bg-surface-hover text-text-primary',
)}
>
<span className="flex min-w-0 items-center gap-2">
<ArrowUpDown
@ -118,19 +150,10 @@ export default function ProjectsView() {
/>
<span className="truncate">{selectedSortLabel}</span>
</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{sortOptions.map((option) => (
<DropdownMenuItem key={option.value} onSelect={() => setSortBy(option.value)}>
<span>{option.label}</span>
{sortBy === option.value && (
<Check className="ml-auto h-4 w-4 text-text-primary" aria-hidden="true" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</Ariakit.MenuButton>
}
items={sortMenuItems}
/>
</div>
{isCreating && (

View file

@ -193,7 +193,10 @@ export default function useChatFunctions({
parentMessageId = Constants.NO_PARENT;
currentMessages = [];
conversationId = null;
navigate('/c/new', { state: { focusChat: true } });
const projectSearch = conversation?.chatProjectId
? `?projectId=${encodeURIComponent(conversation.chatProjectId)}`
: '';
navigate(`/c/new${projectSearch}`, { state: { focusChat: true } });
}
const targetParentMessageId = isRegenerate ? messageId : latestMessage?.parentMessageId;

View file

@ -28,11 +28,13 @@ export default function useTextarea({
submitButtonRef,
setIsScrollable,
disabled = false,
placeholder,
}: {
textAreaRef: React.RefObject<HTMLTextAreaElement>;
submitButtonRef: React.RefObject<HTMLButtonElement>;
setIsScrollable: React.Dispatch<React.SetStateAction<boolean>>;
disabled?: boolean;
placeholder?: string;
}) {
const localize = useLocalize();
const getSender = useGetSender();
@ -95,6 +97,10 @@ export default function useTextarea({
return localize('com_endpoint_message_not_appendable');
}
if (placeholder) {
return placeholder;
}
const sender =
isAssistant || isAgent
? getEntityName({ name: entityName, isAgent, localize })
@ -137,6 +143,7 @@ export default function useTextarea({
conversation,
latestMessage,
isNotAppendable,
placeholder,
]);
const handleKeyDown = useCallback(

View file

@ -98,15 +98,24 @@ export default function ChatRoute() {
useEffect(() => {
// Wait for roles to load so hasAgentAccess has a definitive value in useNewConvo
const rolesLoaded = roles?.USER != null;
const isNewConvo = conversationId === Constants.NEW_CONVO;
const newConvoNeedsInit =
isNewConvo &&
(conversation?.conversationId !== Constants.NEW_CONVO ||
(verifiedChatProjectId
? conversation?.chatProjectId !== verifiedChatProjectId
: conversation?.chatProjectId != null));
const shouldSetConvo =
(startupConfig && rolesLoaded && !hasSetConversation.current && !modelsQuery.data?.initial) ??
(startupConfig &&
rolesLoaded &&
(!hasSetConversation.current || newConvoNeedsInit) &&
!modelsQuery.data?.initial) ??
false;
/* Early exit if startupConfig is not loaded and conversation is already set and only initial models have loaded */
if (!shouldSetConvo) {
return;
}
const isNewConvo = conversationId === Constants.NEW_CONVO;
if (isNewConvo && chatProjectId && projectQuery.isLoading) {
return;
}
@ -218,6 +227,8 @@ export default function ChatRoute() {
chatProjectId,
projectQuery.data?._id,
projectQuery.isLoading,
conversation?.chatProjectId,
conversation?.conversationId,
]);
if (endpointsQuery.isLoading || modelsQuery.isLoading) {
@ -247,7 +258,7 @@ export default function ChatRoute() {
return (
<ToolCallsMapProvider conversationId={conversation.conversationId ?? ''}>
<ChatView index={index} />
<ChatView index={index} project={verifiedChatProjectId ? projectQuery.data : undefined} />
</ToolCallsMapProvider>
);
}