Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 63 additions & 6 deletions frontend/app/[locale]/newchat/assistant-ui/thread-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
Expand Down Expand Up @@ -303,17 +305,47 @@ export const BatchSidebarFooter: FC<{ onSwitchToLegacy?: () => void }> = ({
);
};

type RenameSuccessHandler = (
threadId: string,
remoteId: string | undefined,
title: string
) => void;

interface ThreadListProps {
generatedTitles?: ReadonlyMap<string, string>;
serverConversationIds?: ReadonlyMap<string, string>;
onRenameSuccess?: RenameSuccessHandler;
}

export const ThreadList: FC<ThreadListProps> = ({
generatedTitles,
serverConversationIds,
onRenameSuccess,
}) => {
const { t } = useTranslation();
const completedConversations = useMemo(() => new Set<string>(), []);
const [completedConversations, setCompletedConversations] = useState(
() => new Set<string>()
);
const isThreadRunning = useAuiState((s) => s.thread.isRunning);
const mainThreadId = useAuiState((s) => s.threads.mainThreadId);
const runningThreadId = useRef<string | undefined>(undefined);

useEffect(() => {
if (isThreadRunning) {
runningThreadId.current = mainThreadId;
return;
}

if (runningThreadId.current) {
const completedThreadId = runningThreadId.current;
setCompletedConversations((previous) => {
const next = new Set(previous);
next.add(completedThreadId);
return next;
});
Comment thread
YehongPan marked this conversation as resolved.
runningThreadId.current = undefined;
}
}, [isThreadRunning, mainThreadId]);
const isLoading = useAuiState((s) => s.threads.isLoading);
const isLoadingMore = useAuiState((s) => s.threads.isLoadingMore);
const hasMore = useAuiState((s) => s.threads.hasMore);
Expand All @@ -335,6 +367,7 @@ export const ThreadList: FC<ThreadListProps> = ({
<ThreadListItems
completedConversations={completedConversations}
generatedTitles={generatedTitles}
onRenameSuccess={onRenameSuccess}
visibleThreadIds={visibleThreadIds}
/>
)}
Expand Down Expand Up @@ -373,12 +406,14 @@ const ThreadListEmpty: FC = () => {
interface ThreadListItemsProps {
completedConversations: Set<string>;
generatedTitles?: ReadonlyMap<string, string>;
onRenameSuccess?: RenameSuccessHandler;
visibleThreadIds: string[];
}

const ThreadListItems: FC<ThreadListItemsProps> = ({
completedConversations,
generatedTitles,
onRenameSuccess,
visibleThreadIds,
}) => {
const { t } = useTranslation();
Expand All @@ -392,10 +427,11 @@ const ThreadListItems: FC<ThreadListItemsProps> = ({
<ThreadListItem
completedConversations={completedConversations}
generatedTitles={generatedTitles}
onRenameSuccess={onRenameSuccess}
/>
);
},
[completedConversations, generatedTitles]
[completedConversations, generatedTitles, onRenameSuccess]
);

// Render each thread by index so we can interleave group labels between
Expand Down Expand Up @@ -529,17 +565,20 @@ const ThreadListSkeleton: FC = () => {
interface ThreadListItemProps {
completedConversations: Set<string>;
generatedTitles?: ReadonlyMap<string, string>;
onRenameSuccess?: RenameSuccessHandler;
}

const ThreadListItem: FC<ThreadListItemProps> = ({
completedConversations,
generatedTitles,
onRenameSuccess,
}) => {
return (
<ThreadListItemPrimitive.Root className="group/item flex h-9 items-center rounded-lg hover:bg-muted data-[active=true]:bg-muted">
<ThreadListItemContent
completedConversations={completedConversations}
generatedTitles={generatedTitles}
onRenameSuccess={onRenameSuccess}
/>
</ThreadListItemPrimitive.Root>
);
Expand All @@ -548,11 +587,13 @@ const ThreadListItem: FC<ThreadListItemProps> = ({
interface ThreadListItemContentProps {
completedConversations: Set<string>;
generatedTitles?: ReadonlyMap<string, string>;
onRenameSuccess?: RenameSuccessHandler;
}

const ThreadListItemContent: FC<ThreadListItemContentProps> = ({
completedConversations,
generatedTitles,
onRenameSuccess,
}) => {
const aui = useAui();
const { t } = useTranslation();
Expand All @@ -575,6 +616,7 @@ const ThreadListItemContent: FC<ThreadListItemContentProps> = ({
setPendingThreadOperationId(thread.id);
try {
await threadListItem.rename(newTitle);
onRenameSuccess?.(thread.id, thread.remoteId, newTitle);
log.log(`[ThreadList] Renamed thread to "${newTitle}"`);
setIsEditing(false);
} catch (error) {
Expand All @@ -584,7 +626,7 @@ const ThreadListItemContent: FC<ThreadListItemContentProps> = ({
setPendingThreadOperationId(undefined);
}
},
[thread.id, threadListItem, t]
[thread.id, thread.remoteId, threadListItem, onRenameSuccess, t]
);

const handleRenameClick = useCallback(() => {
Expand Down Expand Up @@ -697,11 +739,26 @@ const ConversationStatusIndicatorWrapper: FC<{
completedConversations: Set<string>;
}> = ({ completedConversations }) => {
const aui = useAui();
const status = aui.threadListItem().getState().status as string;
const isRunning = status === "running" || status === "streaming";
const threadId = aui.threadListItem().getState().id;

return (
<ConversationStatusIndicator isStreaming={isRunning} isCompleted={false} />
<>
<AuiIf
condition={(s) =>
s.thread.isRunning && s.threads.mainThreadId === threadId
}
>
<ConversationStatusIndicator isStreaming isCompleted={false} />
</AuiIf>
<AuiIf
condition={(s) =>
completedConversations.has(threadId) &&
!(s.thread.isRunning && s.threads.mainThreadId === threadId)
}
>
<ConversationStatusIndicator isStreaming={false} isCompleted />
</AuiIf>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,19 @@ import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";

type RenameSuccessHandler = (
threadId: string,
remoteId: string | undefined,
title: string
) => void;

interface ThreadListSidebarProps extends SidebarProps {
className?: string;
generatedTitles?: ReadonlyMap<string, string>;
serverConversationIds?: ReadonlyMap<string, string>;
onPrepareNewConversation?: () => void;
onNewConversation?: () => void | Promise<void>;
onRenameSuccess?: RenameSuccessHandler;
showLegacySwitch?: boolean;
}

Expand All @@ -38,6 +45,7 @@ export function ThreadListSidebar({
serverConversationIds,
onPrepareNewConversation,
onNewConversation,
onRenameSuccess,
showLegacySwitch = true,
...props
}: ThreadListSidebarProps) {
Expand Down Expand Up @@ -144,6 +152,7 @@ export function ThreadListSidebar({
<ThreadList
generatedTitles={generatedTitles}
serverConversationIds={serverConversationIds}
onRenameSuccess={onRenameSuccess}
/>
</SidebarContent>
<SidebarFooter>
Expand Down
13 changes: 13 additions & 0 deletions frontend/app/[locale]/newchat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,18 @@ const HomeContent: FC<{
[chatMode]
);

const handleConversationRenamed = useCallback(
(threadId: string, remoteId: string | undefined, title: string) => {
setGeneratedTitles((titles) => {
const next = new Map(titles);
next.set(threadId, title);
if (remoteId) next.set(remoteId, title);
return next;
Comment thread
YehongPan marked this conversation as resolved.
});
},
[]
);

const handleGenerationStopped = useCallback((conversationId: number) => {
// A user-initiated stop transitions assistant-ui to idle before the
// backend has persisted the terminal message. Do not mistake that short
Expand Down Expand Up @@ -781,6 +793,7 @@ const HomeContent: FC<{
<ThreadListSidebar
generatedTitles={generatedTitles}
serverConversationIds={serverConversationIds}
onRenameSuccess={handleConversationRenamed}
onPrepareNewConversation={handlePrepareNewConversation}
onNewConversation={handleNewConversation}
/>
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/interaction/user-message-bubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ReactNode } from "react";

export function UserMessageBubble({ children }: { children: ReactNode }) {
return (
<div className="aui-user-message-content peer bg-muted text-foreground rounded-xl px-4 py-2 wrap-break-word empty:hidden">
<div className="aui-user-message-content peer bg-muted text-foreground rounded-xl px-4 py-2 whitespace-pre-wrap wrap-break-word empty:hidden">
{children}
</div>
);
Expand Down
13 changes: 13 additions & 0 deletions frontend/features/workbench/WorkbenchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,18 @@ const HomeContent: FC = () => {
[chatMode, dispatchWorkbench, workbenchState.configVersion]
);

const handleConversationRenamed = useCallback(
(threadId: string, remoteId: string | undefined, title: string) => {
setGeneratedTitles((titles) => {
const next = new Map(titles);
next.set(threadId, title);
if (remoteId) next.set(remoteId, title);
return next;
Comment thread
YehongPan marked this conversation as resolved.
});
},
[]
);

const handleGenerationStopped = useCallback((conversationId: number) => {
// A user-initiated stop transitions assistant-ui to idle before the
// backend has persisted the terminal message. Do not mistake that short
Expand Down Expand Up @@ -1168,6 +1180,7 @@ const HomeContent: FC = () => {
showLegacySwitch={false}
generatedTitles={generatedTitles}
serverConversationIds={serverConversationIds}
onRenameSuccess={handleConversationRenamed}
onPrepareNewConversation={handlePrepareNewConversation}
onNewConversation={handleNewConversation}
/>
Expand Down
Loading