Files
flynn/src/frontends/tui/components/MessageList.tsx
T

123 lines
3.7 KiB
TypeScript

import React, { memo } from 'react';
import { Box, Text, Static } from 'ink';
import type { Message } from '../../../models/types.js';
import { getMessageText } from '../../../models/media.js';
import { renderMarkdown } from '../markdown.js';
import { getBannerLines } from '../banner.js';
export interface MessageListProps {
messages: Message[];
scrollOffset?: number;
streamingContent?: string;
verbose?: boolean;
}
// Helper to format timestamp in human-readable way
function formatTimestamp(timestamp: number): string {
const date = new Date(timestamp);
const now = new Date();
const isSameDay = date.toDateString() === now.toDateString();
if (isSameDay) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
// Individual message component
const MessageItem = memo(function MessageItem({
message,
index,
}: {
message: Message;
index: number;
}): React.ReactElement {
const isUser = message.role === 'user';
const accentColor = isUser ? 'blue' : '#ff8c00';
const timestampText = message.timestamp ? formatTimestamp(message.timestamp) : 'unknown time';
return (
<Box
key={index}
marginBottom={1}
flexDirection="row"
>
<Box minWidth={1} backgroundColor={accentColor} />
<Box
flexDirection="column"
flexGrow={1}
paddingX={2}
paddingY={1}
>
{/* Author line */}
<Box marginBottom={1} justifyContent="space-between" flexDirection="row">
<Text color={accentColor} bold>
{isUser ? 'You' : 'Flynn'}
</Text>
<Text color="gray">| {timestampText}</Text>
</Box>
{/* Content */}
<Text wrap="wrap">
{message.role === 'assistant'
? renderMarkdown(getMessageText(message))
: getMessageText(message)}
</Text>
</Box>
</Box>
);
});
export const MessageList = memo(function MessageList({
messages,
scrollOffset = 0,
streamingContent,
verbose = false,
}: MessageListProps): React.ReactElement {
const visibleMessages = messages.slice(scrollOffset);
return (
<Box flexDirection="column" flexGrow={1} paddingX={1} overflowY="hidden">
{visibleMessages.length === 0 && !streamingContent ? (
<Box flexDirection="column" alignItems="center" justifyContent="center" flexGrow={1}>
{getBannerLines().map((line, i) => (
<Text key={i} color="#ff8c00">{line}</Text>
))}
<Text color="gray">{'\n'}Start typing to chat with Flynn.</Text>
</Box>
) : (
<>
<Static items={visibleMessages}>
{(message, index) => (
<MessageItem key={index} message={message} index={index} />
)}
</Static>
{streamingContent && (
<Box marginBottom={1} flexDirection="row">
<Box minWidth={1} backgroundColor="#ff8c00" />
<Box
flexDirection="column"
flexGrow={1}
paddingX={2}
paddingY={1}
>
<Box marginBottom={1}>
<Text color="#ff8c00" bold>Flynn</Text>
</Box>
<Text wrap="wrap">{verbose ? streamingContent : renderMarkdown(streamingContent)}</Text>
<Text color="yellow"></Text>
</Box>
</Box>
)}
</>
)}
{messages.length > 0 && scrollOffset > 0 && (
<Box position="absolute" marginTop={-1}>
<Text color="gray">{scrollOffset} more above</Text>
</Box>
)}
</Box>
);
});