Files
openchamber/packages/ui/src/components/chat/message/parts/UserTextPart.tsx
T

88 lines
2.8 KiB
TypeScript
Raw Normal View History

2025-12-07 19:32:53 +02:00
import React from 'react';
import { cn } from '@/lib/utils';
import { SimpleMarkdownRenderer } from '../../MarkdownRenderer';
2026-01-03 13:39:59 +02:00
import type { Part } from '@opencode-ai/sdk/v2';
2025-12-07 19:32:53 +02:00
import type { AgentMentionInfo } from '../types';
type PartWithText = Part & { text?: string; content?: string; value?: string };
type UserTextPartProps = {
part: Part;
messageId: string;
isMobile: boolean;
agentMention?: AgentMentionInfo;
};
const buildMentionUrl = (name: string): string => {
2025-12-07 19:32:53 +02:00
const encoded = encodeURIComponent(name);
return `https://opencode.ai/docs/agents/#${encoded}`;
2025-12-07 19:32:53 +02:00
};
const UserTextPart: React.FC<UserTextPartProps> = ({ part, messageId, agentMention }) => {
2025-12-07 19:32:53 +02:00
const partWithText = part as PartWithText;
const rawText = partWithText.text;
const textContent = typeof rawText === 'string' ? rawText : partWithText.content || partWithText.value || '';
const [isExpanded, setIsExpanded] = React.useState(false);
const [isTruncated, setIsTruncated] = React.useState(false);
const textRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const el = textRef.current;
if (!el || isExpanded) return;
const checkTruncation = () => {
2025-12-07 19:32:53 +02:00
setIsTruncated(el.scrollHeight > el.clientHeight);
};
checkTruncation();
const resizeObserver = new ResizeObserver(checkTruncation);
resizeObserver.observe(el);
return () => resizeObserver.disconnect();
}, [textContent, isExpanded]);
2025-12-07 19:32:53 +02:00
const handleClick = React.useCallback(() => {
if (isTruncated || isExpanded) {
setIsExpanded((prev) => !prev);
}
}, [isTruncated, isExpanded]);
if (!textContent || textContent.trim().length === 0) {
2025-12-07 19:32:53 +02:00
return null;
}
const renderContent = () => {
if (!agentMention?.token || !textContent.includes(agentMention.token)) {
return textContent;
}
const idx = textContent.indexOf(agentMention.token);
const before = textContent.slice(0, idx);
const after = textContent.slice(idx + agentMention.token.length);
const mentionLink = `[${agentMention.token}](${buildMentionUrl(agentMention.name)})`;
return `${before}${mentionLink}${after}`;
};
2025-12-07 19:32:53 +02:00
return (
<div
className={cn(
"font-sans typography-markdown",
2025-12-07 19:32:53 +02:00
!isExpanded && "line-clamp-3",
(isTruncated || isExpanded) && "cursor-pointer"
)}
ref={textRef}
onClick={handleClick}
key={part.id || `${messageId}-user-text`}
>
<SimpleMarkdownRenderer
content={renderContent()}
className="text-foreground/90"
/>
2025-12-07 19:32:53 +02:00
</div>
);
};
export default React.memo(UserTextPart);