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}
offline={offline}
onConversationCreated={handleConversationCreated}
currentConversationId={currentConversationId}
/>
</div>
</main>
+13
View File
@@ -15,12 +15,14 @@ interface ChatContainerProps {
models: ModelInfo[];
offline: boolean;
onConversationCreated?: () => void;
currentConversationId?: string | null;
}
export function ChatContainer({
models,
offline,
onConversationCreated,
currentConversationId: propCurrentConversationId,
}: ChatContainerProps) {
const { prefs, ready, update } = useUserPrefs();
const [messages, setMessages] = useState<Message[]>([]);
@@ -42,6 +44,17 @@ export function ChatContainer({
}
}, [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
const handleModelChange = (modelName: string) => {
setSelectedModel(modelName);
+2 -2
View File
@@ -34,9 +34,9 @@ export function ChatMessage({
: "bg-[var(--surface)] text-[var(--foreground)]"
}`}
>
<div className="whitespace-pre-wrap break-words">
<div className="break-words">
{isUser ? (
message.content
<div className="whitespace-pre-wrap">{message.content}</div>
) : (
<MarkdownRenderer content={message.content} />
)}
+33 -9
View File
@@ -12,6 +12,12 @@ type 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 (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
@@ -29,7 +35,7 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
}
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
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"
@@ -47,27 +53,45 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
);
},
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 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 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 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 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 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>
);
}
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") {
console.log(`\n✅ Modelo ${modelName} baixado com sucesso!`);
if (description) {
console.log(`🎯 ${description.replace("será usado", "agora funciona")}`);
console.log(
`🎯 ${description.replace("será usado", "agora funciona")}`
);
}
break;
}
+1 -1
View File
@@ -131,7 +131,7 @@ export async function initializeEmbeddingModel(): Promise<void> {
try {
await ensureModelExists(
"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!");
} catch (error) {
+1
View File
@@ -0,0 +1 @@