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:
1 parent
a3f8226fda
commit
0cc7a81319
34 files changed
+2370
-198
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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)]"
|
||||
|
||||
Reference in new issue
Block a user