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.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-28 14:40:34 -03:00
1 parent 0cc7a81319
commit d5dd3a9f09
11 files changed
+54 -13

No files matched your search

+1
View File
@@ -62,6 +62,7 @@ export function ClientHome({ offline, models }: ClientHomeProps) {
models={models} models={models}
offline={offline} offline={offline}
onConversationCreated={handleConversationCreated} onConversationCreated={handleConversationCreated}
currentConversationId={currentConversationId}
/> />
</div> </div>
</main> </main>
+13
View File
@@ -15,12 +15,14 @@ interface ChatContainerProps {
models: ModelInfo[]; models: ModelInfo[];
offline: boolean; offline: boolean;
onConversationCreated?: () => void; onConversationCreated?: () => void;
currentConversationId?: string | null;
} }
export function ChatContainer({ export function ChatContainer({
models, models,
offline, offline,
onConversationCreated, onConversationCreated,
currentConversationId: propCurrentConversationId,
}: ChatContainerProps) { }: ChatContainerProps) {
const { prefs, ready, update } = useUserPrefs(); const { prefs, ready, update } = useUserPrefs();
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
@@ -42,6 +44,17 @@ export function ChatContainer({
} }
}, [ready, prefs.selectedModel]); }, [ready, prefs.selectedModel]);
// Carregar mensagens quando uma conversa é selecionada
useEffect(() => {
if (propCurrentConversationId) {
loadConversation(propCurrentConversationId);
} else {
// Limpar mensagens quando nenhuma conversa está selecionada
setMessages([]);
setConversationId(null);
}
}, [propCurrentConversationId]);
// Salvar modelo quando selecionado // Salvar modelo quando selecionado
const handleModelChange = (modelName: string) => { const handleModelChange = (modelName: string) => {
setSelectedModel(modelName); setSelectedModel(modelName);
+2 -2
View File
@@ -34,9 +34,9 @@ export function ChatMessage({
: "bg-[var(--surface)] text-[var(--foreground)]" : "bg-[var(--surface)] text-[var(--foreground)]"
}`} }`}
> >
<div className="whitespace-pre-wrap break-words"> <div className="break-words">
{isUser ? ( {isUser ? (
message.content <div className="whitespace-pre-wrap">{message.content}</div>
) : ( ) : (
<MarkdownRenderer content={message.content} /> <MarkdownRenderer content={message.content} />
)} )}
+33 -9
View File
@@ -12,6 +12,12 @@ type MarkdownRendererProps = {
}; };
export function MarkdownRenderer({ content }: MarkdownRendererProps) { export function MarkdownRenderer({ content }: MarkdownRendererProps) {
// Normalizar quebras de linha e espaços
const normalizedContent = content
.replace(/\n{3,}/g, "\n\n") // Máximo 2 quebras consecutivas
.replace(/[ \t]+$/gm, "") // Remover espaços no final das linhas
.trim();
return ( return (
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
@@ -29,7 +35,7 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
} }
return ( return (
<div className="relative my-3 rounded-lg border border-[var(--border)] bg-[var(--background)]"> <div className="relative my-4 rounded-lg border border-[var(--border)] bg-[var(--background)]">
<button <button
type="button" type="button"
className="absolute right-2 top-2 rounded px-2 py-1 text-xs bg-[var(--surface)]/70 hover:bg-[var(--surface)] transition-colors" className="absolute right-2 top-2 rounded px-2 py-1 text-xs bg-[var(--surface)]/70 hover:bg-[var(--surface)] transition-colors"
@@ -47,27 +53,45 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
); );
}, },
h1: ({ children }: any) => ( h1: ({ children }: any) => (
<h1 className="text-xl font-bold mt-3 mb-1">{children}</h1> <h1 className="text-xl font-bold mt-4 mb-2 first:mt-0">{children}</h1>
), ),
h2: ({ children }: any) => ( h2: ({ children }: any) => (
<h2 className="text-lg font-semibold mt-2 mb-1">{children}</h2> <h2 className="text-lg font-semibold mt-3 mb-2">{children}</h2>
), ),
h3: ({ children }: any) => ( h3: ({ children }: any) => (
<h3 className="text-base font-semibold mt-2 mb-1">{children}</h3> <h3 className="text-base font-semibold mt-3 mb-1">{children}</h3>
), ),
h4: ({ children }: any) => ( h4: ({ children }: any) => (
<h4 className="text-sm font-semibold mt-1 mb-1">{children}</h4> <h4 className="text-sm font-semibold mt-2 mb-1">{children}</h4>
), ),
h5: ({ children }: any) => ( h5: ({ children }: any) => (
<h5 className="text-sm font-medium mt-1 mb-1">{children}</h5> <h5 className="text-sm font-medium mt-2 mb-1">{children}</h5>
), ),
h6: ({ children }: any) => ( h6: ({ children }: any) => (
<h6 className="text-xs font-medium mt-1 mb-1">{children}</h6> <h6 className="text-xs font-medium mt-2 mb-1">{children}</h6>
), ),
p: ({ children }: any) => <p className="mb-1">{children}</p>, p: ({ children }: any) => (
<p className="mb-3 leading-relaxed">{children}</p>
),
ul: ({ children }: any) => (
<ul className="mb-3 ml-4 list-disc space-y-1">{children}</ul>
),
ol: ({ children }: any) => (
<ol className="mb-3 ml-4 list-decimal space-y-1">{children}</ol>
),
li: ({ children }: any) => (
<li className="leading-relaxed">{children}</li>
),
blockquote: ({ children }: any) => (
<blockquote className="border-l-4 border-[var(--border)] pl-4 my-3 italic text-[var(--foreground)]/80">
{children}
</blockquote>
),
hr: () => <hr className="my-4 border-[var(--border)]" />,
br: () => <br className="mb-1" />,
}} }}
> >
{content} {normalizedContent}
</ReactMarkdown> </ReactMarkdown>
); );
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -1
View File
@@ -91,7 +91,9 @@ export async function ensureModelExists(
} else if (chunk.status === "success") { } else if (chunk.status === "success") {
console.log(`\n✅ Modelo ${modelName} baixado com sucesso!`); console.log(`\n✅ Modelo ${modelName} baixado com sucesso!`);
if (description) { if (description) {
console.log(`🎯 ${description.replace("será usado", "agora funciona")}`); console.log(
`🎯 ${description.replace("será usado", "agora funciona")}`
);
} }
break; break;
} }
+1 -1
View File
@@ -131,7 +131,7 @@ export async function initializeEmbeddingModel(): Promise<void> {
try { try {
await ensureModelExists( await ensureModelExists(
"nomic-embed-text", "nomic-embed-text",
"Este modelo será usado para busca vetorial das mensagens" "Este modelo será usado para busca vetorial das mensagens (274MB - 768 dimensões)"
); );
console.log("✅ Modelo de embedding pronto!"); console.log("✅ Modelo de embedding pronto!");
} catch (error) { } catch (error) {
+1
View File
@@ -0,0 +1 @@