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

@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from "next/server";
import { getDatabase } from "@/database/client";
import { MessageRepository } from "@/database/repositories/message";
import { ConversationRepository } from "@/database/repositories/conversation";
import { addMessageEmbedding } from "@/lib/vector-search";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const db = getDatabase();
const messageRepo = new MessageRepository(db);
const messages = messageRepo.findByConversation(id);
return NextResponse.json(messages);
} catch (error) {
console.error("Erro ao buscar mensagens:", error);
return NextResponse.json(
{ error: "Erro interno do servidor" },
{ status: 500 }
);
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const { role, content } = await request.json();
if (!role || !content) {
return NextResponse.json(
{ error: "Role e content são obrigatórios" },
{ status: 400 }
);
}
const db = getDatabase();
const messageRepo = new MessageRepository(db);
const conversationRepo = new ConversationRepository(db);
// Criar mensagem
const messageId = messageRepo.create(id, role, content);
// Atualizar timestamp da conversa
conversationRepo.updateTimestamp(id);
// Gerar embedding em background (apenas para mensagens do usuário e assistente)
if (role === "user" || role === "assistant") {
addMessageEmbedding(messageId, content).catch((error) => {
console.error("Erro ao gerar embedding:", error);
});
}
return NextResponse.json({ id: messageId });
} catch (error) {
console.error("Erro ao criar mensagem:", error);
return NextResponse.json(
{ error: "Erro interno do servidor" },
{ status: 500 }
);
}
}
+58
View File
@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from "next/server";
import { getDatabase } from "@/database/client";
import { ConversationRepository } from "@/database/repositories/conversation";
import { MessageRepository } from "@/database/repositories/message";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const db = getDatabase();
const conversationRepo = new ConversationRepository(db);
const messageRepo = new MessageRepository(db);
const conversation = conversationRepo.findById(id);
if (!conversation) {
return NextResponse.json(
{ error: "Conversa não encontrada" },
{ status: 404 }
);
}
const messages = messageRepo.findByConversation(id);
return NextResponse.json({
conversation,
messages,
});
} catch (error) {
console.error("Erro ao buscar conversa:", error);
return NextResponse.json(
{ error: "Erro interno do servidor" },
{ status: 500 }
);
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const db = getDatabase();
const conversationRepo = new ConversationRepository(db);
conversationRepo.delete(id);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Erro ao deletar conversa:", error);
return NextResponse.json(
{ error: "Erro interno do servidor" },
{ status: 500 }
);
}
}
+54
View File
@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from "next/server";
import { getDatabase } from "@/database/client";
import { ConversationRepository } from "@/database/repositories/conversation";
import { MessageRepository } from "@/database/repositories/message";
import { generateChatTitle } from "@/lib/embeddings";
export async function GET() {
try {
const db = getDatabase();
const conversationRepo = new ConversationRepository(db);
const conversations = conversationRepo.findAll();
return NextResponse.json(conversations);
} catch (error) {
console.error("Erro ao buscar conversas:", error);
return NextResponse.json(
{ error: "Erro interno do servidor" },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const { model, firstMessage } = await request.json();
if (!model) {
return NextResponse.json(
{ error: "Modelo é obrigatório" },
{ status: 400 }
);
}
const db = getDatabase();
const conversationRepo = new ConversationRepository(db);
// Gerar título se primeira mensagem fornecida
let title = "Nova conversa";
if (firstMessage) {
title = await generateChatTitle(firstMessage);
}
const conversationId = conversationRepo.create(model, title);
return NextResponse.json({ id: conversationId, title });
} catch (error) {
console.error("Erro ao criar conversa:", error);
return NextResponse.json(
{ error: "Erro interno do servidor" },
{ status: 500 }
);
}
}
+30 -12
View File
@@ -14,6 +14,30 @@ export async function POST(req: NextRequest) {
const stream = new ReadableStream({
async start(controller) {
let isClosed = false;
const safeEnqueue = (data: string) => {
if (!isClosed) {
try {
controller.enqueue(new TextEncoder().encode(data));
} catch (error) {
console.error("Error enqueuing data:", error);
isClosed = true;
}
}
};
const safeClose = () => {
if (!isClosed) {
try {
controller.close();
isClosed = true;
} catch (error) {
console.error("Error closing controller:", error);
}
}
};
try {
const ollamaMessages = convertToOllamaMessages(messages);
const response = await chatWithStream(
@@ -26,24 +50,18 @@ export async function POST(req: NextRequest) {
for await (const chunk of response) {
const token = chunk.message?.content || "";
if (token) {
controller.enqueue(
new TextEncoder().encode(JSON.stringify({ token }) + "\n")
);
safeEnqueue(JSON.stringify({ token }) + "\n");
}
}
controller.enqueue(
new TextEncoder().encode(JSON.stringify({ done: true }) + "\n")
);
controller.close();
safeEnqueue(JSON.stringify({ done: true }) + "\n");
safeClose();
} catch (error) {
console.error("Chat streaming error:", error);
controller.enqueue(
new TextEncoder().encode(
JSON.stringify({ error: "Failed to generate response" }) + "\n"
)
safeEnqueue(
JSON.stringify({ error: "Failed to generate response" }) + "\n"
);
controller.close();
safeClose();
}
},
});
+42
View File
@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from "next/server";
import { getPreferences, setPreferences } from "@/lib/preferences";
// Estrutura de preferências
type UserPreferences = {
selectedModel: string | null;
systemPrompt: string;
device: "auto" | "cpu" | "gpu";
gpuIndex: number;
numGpu: number;
};
// GET - Buscar preferências
export async function GET(request: NextRequest) {
try {
const userId = "default"; // Pode ser expandido para multi-usuário
const prefs = await getPreferences(userId);
return NextResponse.json(prefs);
} catch (error) {
console.error("Erro ao buscar preferências:", error);
return NextResponse.json(
{ error: "Erro interno do servidor" },
{ status: 500 }
);
}
}
// POST - Salvar preferências
export async function POST(request: NextRequest) {
try {
const userId = "default";
const prefs = await request.json();
await setPreferences(userId, prefs);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Erro ao salvar preferências:", error);
return NextResponse.json(
{ error: "Erro interno do servidor" },
{ status: 500 }
);
}
}
+25
View File
@@ -0,0 +1,25 @@
import { NextRequest, NextResponse } from "next/server";
import { semanticSearch } from "@/lib/vector-search";
export async function POST(request: NextRequest) {
try {
const { query, limit = 5 } = await request.json();
if (!query) {
return NextResponse.json(
{ error: "Query é obrigatória" },
{ status: 400 }
);
}
const results = await semanticSearch(query, limit);
return NextResponse.json(results);
} catch (error) {
console.error("Erro na busca semântica:", error);
return NextResponse.json(
{ error: "Erro interno do servidor" },
{ status: 500 }
);
}
}