Files
OllaHub/components/chat/ChatMessage.tsx
T
Evandro Fonseca Junior d5dd3a9f09 feat(chat): add conversation selection and enhance message rendering
- Add currentConversationId prop to ChatContainer for loading selected conversations
- Implement logic to load messages when conversation is selected and clear when deselected
- Improve user message rendering with proper whitespace handling
- Enhance MarkdownRenderer with content normalization and adjusted spacing for better readability
- Add paragraph styling to markdown output for improved layout

This enables users to switch between conversations seamlessly and provides cleaner message display.
2025-10-28 14:40:34 -03:00

158 lines
4.4 KiB
TypeScript

"use client";
import { useState, useEffect, useRef } from "react";
import { User, Bot, Send, Loader2, Square } from "lucide-react";
import { MarkdownRenderer } from "@/components/message/MarkdownRenderer";
import { MessageActions } from "@/components/message/MessageActions";
import type { Message } from "@/lib/chat";
import { formatTimestamp } from "@/lib/chat";
interface ChatMessageProps {
message: Message;
isStreaming?: boolean;
}
export function ChatMessage({
message,
isStreaming = false,
}: ChatMessageProps) {
const isUser = message.role === "user";
return (
<div
className={`flex gap-3 p-4 ${isUser ? "justify-end" : "justify-start"}`}
>
{!isUser && (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[var(--accent)] text-white">
<Bot className="h-4 w-4" />
</div>
)}
<div
className={`max-w-[80%] relative rounded-lg px-4 py-2 ${
isUser
? "bg-[var(--accent)] text-white"
: "bg-[var(--surface)] text-[var(--foreground)]"
}`}
>
<div className="break-words">
{isUser ? (
<div className="whitespace-pre-wrap">{message.content}</div>
) : (
<MarkdownRenderer content={message.content} />
)}
</div>
{isStreaming && <span className="ml-1 animate-pulse">▊</span>}
{!isUser && !isStreaming && (
<div className="absolute bottom-2 right-2">
<MessageActions content={message.content} />
</div>
)}
<div
className={`mt-1 text-xs opacity-70 ${
isUser ? "text-right" : "text-left"
}`}
>
{formatTimestamp(message.timestamp)}
</div>
</div>
{isUser && (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[var(--surface)]">
<User className="h-4 w-4" />
</div>
)}
</div>
);
}
interface ChatInputProps {
onSendMessage: (content: string) => void;
onStop?: () => void;
disabled?: boolean;
isStreaming?: boolean;
placeholder?: string;
}
export function ChatInput({
onSendMessage,
onStop,
disabled = false,
isStreaming = false,
placeholder = "Type a message...",
}: ChatInputProps) {
const [content, setContent] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (content.trim() && !disabled) {
onSendMessage(content.trim());
setContent("");
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
}
};
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = `${Math.min(
textareaRef.current.scrollHeight,
120
)}px`;
}
}, [content]);
return (
<form
onSubmit={handleSubmit}
className="flex gap-2 p-4 border-t border-[var(--border)]"
>
<textarea
ref={textareaRef}
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={disabled}
className="flex-1 resize-none rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)] disabled:opacity-50"
rows={1}
maxLength={2000}
/>
<button
type={isStreaming ? "button" : "submit"}
onClick={isStreaming ? onStop : undefined}
disabled={isStreaming ? false : !content.trim() || disabled}
className="flex h-10 w-10 items-center justify-center rounded-lg bg-[var(--accent)] text-white transition-colors hover:bg-[color-mix(in_oklab,var(--accent),black_10%)] disabled:opacity-50"
>
{isStreaming ? (
<Square className="h-4 w-4" />
) : disabled ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</button>
</form>
);
}
interface StreamingTextProps {
text: string;
isStreaming: boolean;
}
export function StreamingText({ text, isStreaming }: StreamingTextProps) {
return (
<span>
{text}
{isStreaming && <span className="ml-1 animate-pulse">▊</span>}
</span>
);
}