feat(ui): enable drag-and-drop attachments and image previews in chat (#390)
* feat(BottomTerminalDock): add close button next to the fullscreen toggle in the dock * style: replace hardcoded gradient with theme value in shine text variant * fix(header): adapt instance button for desktop only * refactor(chat): polish sticky turn UX and message action rows for better readability Switch to stable sticky-only turn behavior and redesign user/assistant action controls (placement, hover rules, ordering, spacing, selection-safe clamp) to reduce visual noise and improve interaction flow. * feat(chat/message): refactor buttons in messages footer * feat: enhance image preview functionality in chat messages - Added a new ImagePreviewDialog component to handle image previews with navigation support. - Updated ToolOutputDialog to utilize the new ImagePreviewDialog for displaying images. - Modified the ToolPopupContent type to include a gallery of images and an index for the current image. - Removed the old inline image display logic from ToolOutputDialog. - Improved file handling in the file store, including better MIME type guessing and handling of server paths. - Introduced a new API endpoint for handling large session message payloads, allowing for better management of multi-file attachments. - Updated the VSCode bridge to support session message requests with appropriate headers and body handling. * feat(proxy): implement SSE forwarding and enhance generic API request handling * feat(chat): support submitting only queued messages * feat: add image preview transition state * fix: default VSCode view to draft and fixed sessions list regression
This commit is contained in:
committed by
GitHub
parent
39e625d8ec
commit
844562749d
@@ -8,6 +8,7 @@ import type { PermissionRequest } from '@/types/permission';
|
||||
import type { QuestionRequest } from '@/types/question';
|
||||
import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatScrollManager';
|
||||
import { filterSyntheticParts } from '@/lib/messages/synthetic';
|
||||
import { detectTurns, type Turn } from './hooks/useTurnGrouping';
|
||||
import { TurnGroupingProvider, useMessageNeighbors, useTurnGroupingContextForMessage, useTurnGroupingContextStatic, useLastTurnMessageIds } from './contexts/TurnGroupingContext';
|
||||
|
||||
interface ChatMessageEntry {
|
||||
@@ -87,31 +88,103 @@ const DynamicMessageRow = React.memo<MessageRowProps>(({
|
||||
|
||||
DynamicMessageRow.displayName = 'DynamicMessageRow';
|
||||
|
||||
// Inner component that renders messages with access to context hooks
|
||||
const MessageListContent: React.FC<{
|
||||
displayMessages: ChatMessageEntry[];
|
||||
interface TurnBlockProps {
|
||||
turn: Turn;
|
||||
onMessageContentChange: (reason?: ContentChangeReason) => void;
|
||||
getAnimationHandlers: (messageId: string) => AnimationHandlers;
|
||||
scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
|
||||
}> = ({ displayMessages, onMessageContentChange, getAnimationHandlers, scrollToBottom }) => {
|
||||
}
|
||||
|
||||
const TurnBlock: React.FC<TurnBlockProps> = ({
|
||||
turn,
|
||||
onMessageContentChange,
|
||||
getAnimationHandlers,
|
||||
scrollToBottom,
|
||||
}) => {
|
||||
const lastTurnMessageIds = useLastTurnMessageIds();
|
||||
|
||||
const renderMessage = React.useCallback(
|
||||
(message: ChatMessageEntry) => {
|
||||
const role = (message.info as { clientRole?: string | null | undefined }).clientRole ?? message.info.role;
|
||||
const isInLastTurn = role !== 'user' && lastTurnMessageIds.has(message.info.id);
|
||||
const RowComponent = isInLastTurn ? DynamicMessageRow : StaticMessageRow;
|
||||
|
||||
return (
|
||||
<RowComponent
|
||||
key={message.info.id}
|
||||
message={message}
|
||||
onContentChange={onMessageContentChange}
|
||||
animationHandlers={getAnimationHandlers(message.info.id)}
|
||||
scrollToBottom={scrollToBottom}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[getAnimationHandlers, lastTurnMessageIds, onMessageContentChange, scrollToBottom]
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="relative w-full" data-turn-id={turn.turnId}>
|
||||
<div className="sticky top-0 z-20 relative bg-[var(--surface-background)] [overflow-anchor:none]">
|
||||
<div className="relative z-10">
|
||||
{renderMessage(turn.userMessage)}
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 top-full z-0 h-8 bg-gradient-to-b from-[var(--surface-background)] to-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative z-0">
|
||||
{turn.assistantMessages.map((message) => renderMessage(message))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
TurnBlock.displayName = 'TurnBlock';
|
||||
|
||||
// Inner component that renders messages with access to context hooks
|
||||
const MessageListContent: React.FC<{
|
||||
turns: Turn[];
|
||||
ungroupedMessages: ChatMessageEntry[];
|
||||
onMessageContentChange: (reason?: ContentChangeReason) => void;
|
||||
getAnimationHandlers: (messageId: string) => AnimationHandlers;
|
||||
scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
|
||||
}> = ({ turns, ungroupedMessages, onMessageContentChange, getAnimationHandlers, scrollToBottom }) => {
|
||||
const lastTurnMessageIds = useLastTurnMessageIds();
|
||||
|
||||
const renderUngroupedMessage = React.useCallback(
|
||||
(message: ChatMessageEntry) => {
|
||||
const role = (message.info as { clientRole?: string | null | undefined }).clientRole ?? message.info.role;
|
||||
const isInLastTurn = role !== 'user' && lastTurnMessageIds.has(message.info.id);
|
||||
const RowComponent = isInLastTurn ? DynamicMessageRow : StaticMessageRow;
|
||||
|
||||
return (
|
||||
<RowComponent
|
||||
key={message.info.id}
|
||||
message={message}
|
||||
onContentChange={onMessageContentChange}
|
||||
animationHandlers={getAnimationHandlers(message.info.id)}
|
||||
scrollToBottom={scrollToBottom}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[getAnimationHandlers, lastTurnMessageIds, onMessageContentChange, scrollToBottom]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{displayMessages.map((message) => {
|
||||
const isInLastTurn = lastTurnMessageIds.has(message.info.id);
|
||||
const RowComponent = isInLastTurn ? DynamicMessageRow : StaticMessageRow;
|
||||
|
||||
return (
|
||||
<RowComponent
|
||||
key={message.info.id}
|
||||
message={message}
|
||||
onContentChange={onMessageContentChange}
|
||||
animationHandlers={getAnimationHandlers(message.info.id)}
|
||||
scrollToBottom={scrollToBottom}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{ungroupedMessages.map((message) => renderUngroupedMessage(message))}
|
||||
|
||||
{turns.map((turn) => (
|
||||
<TurnBlock
|
||||
key={turn.turnId}
|
||||
turn={turn}
|
||||
onMessageContentChange={onMessageContentChange}
|
||||
getAnimationHandlers={getAnimationHandlers}
|
||||
scrollToBottom={scrollToBottom}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -161,6 +234,25 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||
});
|
||||
}, [messages]);
|
||||
|
||||
const { turns, ungroupedMessages } = React.useMemo(() => {
|
||||
const groupedTurns = detectTurns(displayMessages);
|
||||
const groupedMessageIds = new Set<string>();
|
||||
|
||||
groupedTurns.forEach((turn) => {
|
||||
groupedMessageIds.add(turn.userMessage.info.id);
|
||||
turn.assistantMessages.forEach((message) => {
|
||||
groupedMessageIds.add(message.info.id);
|
||||
});
|
||||
});
|
||||
|
||||
const ungrouped = displayMessages.filter((message) => !groupedMessageIds.has(message.info.id));
|
||||
|
||||
return {
|
||||
turns: groupedTurns,
|
||||
ungroupedMessages: ungrouped,
|
||||
};
|
||||
}, [displayMessages]);
|
||||
|
||||
return (
|
||||
<TurnGroupingProvider messages={displayMessages}>
|
||||
<div>
|
||||
@@ -183,7 +275,8 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||
)}
|
||||
|
||||
<MessageListContent
|
||||
displayMessages={displayMessages}
|
||||
turns={turns}
|
||||
ungroupedMessages={ungroupedMessages}
|
||||
onMessageContentChange={onMessageContentChange}
|
||||
getAnimationHandlers={getAnimationHandlers}
|
||||
scrollToBottom={scrollToBottom}
|
||||
|
||||
Reference in New Issue
Block a user