feat: add database support, conversation sidebar, and improve chat streaming

- Update .gitignore to exclude database files, extensions, and .cursor directory
- Enhance chat streaming API with safe enqueue and close functions to prevent errors
- Integrate ConversationSidebar component into ClientHome for better UI
- Add dependencies like better-sqlite3 and esbuild packages for database functionality

This commit introduces database persistence for conversations, improves error handling in streaming responses, and adds a sidebar for managing chats, enhancing overall app robustness and user experience.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-28 14:14:07 -03:00
1 parent a3f8226fda
commit 0cc7a81319
34 files changed
+2370 -198

No files matched your search

+51
View File
@@ -5,6 +5,7 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import rehypeHighlight from "rehype-highlight";
import { Copy } from "lucide-react";
type MarkdownRendererProps = {
content: string;
@@ -15,6 +16,56 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeHighlight]}
components={{
code({ inline, className, children, ...props }: any) {
const text = String(children || "");
if (inline) {
return (
<code className="rounded bg-[var(--surface)] px-1 py-0.5 text-[0.95em]">
{children}
</code>
);
}
return (
<div className="relative my-3 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"
onClick={() => navigator.clipboard.writeText(text)}
aria-label="Copiar código"
>
<Copy className="h-3 w-3" />
</button>
<pre className="overflow-auto p-3">
<code className={className} {...props}>
{children}
</code>
</pre>
</div>
);
},
h1: ({ children }: any) => (
<h1 className="text-xl font-bold mt-3 mb-1">{children}</h1>
),
h2: ({ children }: any) => (
<h2 className="text-lg font-semibold mt-2 mb-1">{children}</h2>
),
h3: ({ children }: any) => (
<h3 className="text-base font-semibold mt-2 mb-1">{children}</h3>
),
h4: ({ children }: any) => (
<h4 className="text-sm font-semibold mt-1 mb-1">{children}</h4>
),
h5: ({ children }: any) => (
<h5 className="text-sm font-medium mt-1 mb-1">{children}</h5>
),
h6: ({ children }: any) => (
<h6 className="text-xs font-medium mt-1 mb-1">{children}</h6>
),
p: ({ children }: any) => <p className="mb-1">{children}</p>,
}}
>
{content}
</ReactMarkdown>
+49 -51
View File
@@ -1,16 +1,15 @@
"use client";
import { useState } from "react";
import { Copy, Edit3, RefreshCw, Check } from "lucide-react";
import { Copy, Edit3, RefreshCw, Check, ChevronDown } from "lucide-react";
import { useCopyPrefs } from "@/hooks/useCopyPrefs";
import { stripMarkdown } from "@/hooks/useMarkdown";
type MessageActionsProps = {
content: string;
disabled?: boolean;
onEdit?: () => void;
onRegenerate?: () => void;
extractCodeBlocks?: (
text: string
) => Array<{ language: string; code: string }>;
};
export function MessageActions({
@@ -18,70 +17,69 @@ export function MessageActions({
disabled,
onEdit,
onRegenerate,
extractCodeBlocks,
}: MessageActionsProps) {
const [copied, setCopied] = useState<"none" | "markdown" | "text" | "code">(
"none"
);
const [copied, setCopied] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false);
const { copyMode, setCopyMode } = useCopyPrefs();
const copy = async (text: string, flag: MessageActionsProps["content"]) => {
const copy = async (text: string) => {
await navigator.clipboard.writeText(text);
setCopied(flag as any);
setTimeout(() => setCopied("none"), 1500);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
const stripMarkdown = (md: string) =>
md
.replace(/```[\s\S]*?```/g, (block) =>
block.replace(/```[\s\S]*?\n/, "").replace(/```$/, "")
)
.replace(/[#*_>`~-]/g, "");
const codeBlocks = extractCodeBlocks ? extractCodeBlocks(content) : [];
const handleCopyDefault = () => {
const textToCopy =
copyMode === "markdown" ? content : stripMarkdown(content);
copy(textToCopy);
};
const handleModeSelect = (mode: "markdown" | "text") => {
setCopyMode(mode);
const textToCopy = mode === "markdown" ? content : stripMarkdown(content);
copy(textToCopy);
setDropdownOpen(false);
};
return (
<div className="flex items-center gap-1 opacity-70">
<button
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"
onClick={() => copy(content, "markdown")}
onClick={handleCopyDefault}
disabled={disabled}
aria-label="Copy as markdown"
aria-label="Copiar"
>
{copied === "markdown" ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}{" "}
MD
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
</button>
<button
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"
onClick={() => copy(stripMarkdown(content), "text")}
disabled={disabled}
aria-label="Copy as plain text"
>
{copied === "text" ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}{" "}
TXT
</button>
{codeBlocks.map((b, i) => (
<div className="relative">
<button
key={`${b.language}-${i}`}
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"
onClick={() => copy(b.code, "code")}
className="rounded px-1 py-1 text-xs hover:bg-[var(--surface)]"
onClick={() => setDropdownOpen(!dropdownOpen)}
disabled={disabled}
aria-label={`Copy ${b.language} code`}
aria-label="Selecionar formato"
>
{copied === "code" ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}{" "}
{b.language}
<ChevronDown className="h-3 w-3" />
</button>
))}
{dropdownOpen && (
<div className="absolute right-0 z-10 mt-1 rounded border border-[var(--border)] bg-[var(--background)] shadow-lg">
<button
className="block w-full px-3 py-2 text-left text-xs hover:bg-[var(--surface)]"
onClick={() => handleModeSelect("markdown")}
>
Markdown
</button>
<button
className="block w-full px-3 py-2 text-left text-xs hover:bg-[var(--surface)]"
onClick={() => handleModeSelect("text")}
>
Texto
</button>
</div>
)}
</div>
{onEdit && (
<button
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"