feat: collapsible long assistant messages with show more/less toggle

Long assistant messages (>3000 chars) are now collapsed to 400px with a
gradient fade-out and a 'Show more' button. Clicking it reveals the full
message with a 'Show less' button to re-collapse. Streaming messages are
never collapsed. Fully i18n'd (8 languages).
This commit is contained in:
Nicolas Varrot
2026-02-15 06:03:38 +00:00
parent 9bedaba289
commit 578c0d2a47
2 changed files with 78 additions and 7 deletions

View File

@@ -10,7 +10,7 @@ import { CodeBlock } from './CodeBlock';
import { ToolCall } from './ToolCall';
import { ImageBlock } from './ImageBlock';
import { buildImageSrc } from '../lib/image';
import { Bot, User, Wrench, Copy, Check, CheckCheck, RefreshCw, Zap, Info, Webhook, Braces, Clock, AlertCircle, Bookmark } from 'lucide-react';
import { Bot, User, Wrench, Copy, Check, CheckCheck, RefreshCw, Zap, Info, Webhook, Braces, Clock, AlertCircle, Bookmark, ChevronDown } from 'lucide-react';
import { t, getLocale } from '../lib/i18n';
import { useLocale } from '../hooks/useLocale';
import { stripWebhookScaffolding, hasWebhookScaffolding } from '../lib/systemEvent';
@@ -199,6 +199,49 @@ function MarkdownLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
const markdownComponents = { pre: CodeBlock, img: MarkdownImage, a: MarkdownLink };
/** Threshold (in characters) above which assistant messages are collapsed by default */
const COLLAPSE_THRESHOLD = 3000;
const COLLAPSED_MAX_HEIGHT = 400; // px
/** Wrapper that collapses long content with a gradient fade and "Show more" button */
function CollapsibleContent({ content, isStreaming, children }: { content: string; isStreaming?: boolean; children: React.ReactNode }) {
const [expanded, setExpanded] = useState(false);
const shouldCollapse = !isStreaming && content.length > COLLAPSE_THRESHOLD;
if (!shouldCollapse || expanded) {
return (
<>
{children}
{shouldCollapse && expanded && (
<button
onClick={() => setExpanded(false)}
className="mt-2 flex items-center gap-1 text-xs text-pc-accent-light hover:text-pc-accent transition-colors"
>
<ChevronDown size={12} className="rotate-180" />
<span>{t('message.showLess')}</span>
</button>
)}
</>
);
}
return (
<div className="relative">
<div style={{ maxHeight: `${COLLAPSED_MAX_HEIGHT}px`, overflow: 'hidden' }}>
{children}
</div>
<div className="absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-[var(--pc-bg-elevated)] to-transparent pointer-events-none" />
<button
onClick={() => setExpanded(true)}
className="relative mt-1 flex items-center gap-1 text-xs text-pc-accent-light hover:text-pc-accent transition-colors"
>
<ChevronDown size={12} />
<span>{t('message.showMore')}</span>
</button>
</div>
);
}
function renderTextBlocks(blocks: MessageBlock[]) {
return getTextBlocks(blocks).map((block, i) => (
<div key={`text-${i}`} className="markdown-body">
@@ -516,12 +559,24 @@ export const ChatMessageComponent = memo(function ChatMessageComponent({ message
</button>
)}
{/* User-visible text */}
{message.blocks.length > 0 ? renderTextBlocks(message.blocks) : (
<div className="markdown-body">
<LazyMarkdown components={markdownComponents}>
{autoFormatText(message.content)}
</LazyMarkdown>
</div>
{!isUser ? (
<CollapsibleContent content={message.content || ''} isStreaming={message.isStreaming}>
{message.blocks.length > 0 ? renderTextBlocks(message.blocks) : (
<div className="markdown-body">
<LazyMarkdown components={markdownComponents}>
{autoFormatText(message.content)}
</LazyMarkdown>
</div>
)}
</CollapsibleContent>
) : (
message.blocks.length > 0 ? renderTextBlocks(message.blocks) : (
<div className="markdown-body">
<LazyMarkdown components={markdownComponents}>
{autoFormatText(message.content)}
</LazyMarkdown>
</div>
)
)}
{/* Inline images */}