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

+102
View File
@@ -0,0 +1,102 @@
import { Ollama } from "ollama";
const ollama = new Ollama({
host: process.env.OLLAMA_HOST || "http://localhost:11434",
});
export async function generateEmbedding(
text: string,
model: string = "nomic-embed-text"
): Promise<Float32Array> {
try {
const response = await ollama.embeddings({
model,
prompt: text,
});
return new Float32Array(response.embedding);
} catch (error) {
console.error("Erro ao gerar embedding:", error);
// Fallback: retornar vetor zero se modelo não estiver disponível
console.warn(
`Modelo ${model} não disponível, usando vetor zero como fallback`
);
return new Float32Array(384); // 384 dimensões padrão
}
}
const EMOJI_CATEGORIES = {
code: ["💻", "🖥️", "⌨️", "🔧", "🛠️"],
idea: ["💡", "🧠", "💭", "🤔", "✨"],
question: ["❓", "🤷", "🔍", "🧐", "📚"],
creative: ["🎨", "🎭", "🎪", "🌈", "🎬"],
business: ["💼", "📊", "💰", "📈", "🏢"],
science: ["🔬", "🧪", "🔭", "⚗️", "🧬"],
education: ["📖", "📝", "🎓", "✏️", "📚"],
communication: ["💬", "📱", "📧", "🗣️", "💌"],
general: ["🌟", "🎯", "🚀", "⚡", "🔥"],
};
export async function generateChatTitle(firstMessage: string): Promise<string> {
try {
const response = await ollama.generate({
model: "qwen2.5:0.5b",
prompt: `Analise esta mensagem e crie um título curto e descritivo (MAX 30 caracteres, sem emoji):
Mensagem: "${firstMessage}"
Título (direto, sem explicação):`,
options: {
temperature: 0.7,
num_predict: 15,
},
});
let title = response.response
.trim()
.replace(/['"]/g, "")
.replace(/^Título:\s*/i, "")
.substring(0, 30);
// Selecionar emoji baseado no conteúdo
const emoji = selectEmoji(firstMessage);
return `${emoji} ${title}`;
} catch (error) {
console.error("Erro ao gerar título:", error);
// Fallback: usar título simples baseado no conteúdo
const emoji = selectEmoji(firstMessage);
const fallbackTitle = firstMessage.substring(0, 20).trim();
return `${emoji} ${fallbackTitle}`;
}
}
function selectEmoji(content: string): string {
const lower = content.toLowerCase();
if (/code|program|debug|function|class/.test(lower))
return EMOJI_CATEGORIES.code[
Math.floor(Math.random() * EMOJI_CATEGORIES.code.length)
];
if (/why|how|what|when|where/.test(lower))
return EMOJI_CATEGORIES.question[
Math.floor(Math.random() * EMOJI_CATEGORIES.question.length)
];
if (/idea|think|create|design/.test(lower))
return EMOJI_CATEGORIES.idea[
Math.floor(Math.random() * EMOJI_CATEGORIES.idea.length)
];
if (/business|money|sale|market/.test(lower))
return EMOJI_CATEGORIES.business[
Math.floor(Math.random() * EMOJI_CATEGORIES.business.length)
];
if (/science|research|experiment/.test(lower))
return EMOJI_CATEGORIES.science[
Math.floor(Math.random() * EMOJI_CATEGORIES.science.length)
];
return EMOJI_CATEGORIES.general[
Math.floor(Math.random() * EMOJI_CATEGORIES.general.length)
];
}
+62 -2
View File
@@ -29,10 +29,11 @@ export async function chatWithStream(
const client = new Ollama({ host: OLLAMA_HOST });
return client.chat({
model,
messages,
messages: system
? [...messages, { role: "system", content: system }]
: messages,
stream: true,
options,
system,
});
}
@@ -40,3 +41,62 @@ export async function pullModel(model: string) {
const client = new Ollama({ host: OLLAMA_HOST });
return client.pull({ model, stream: true });
}
export async function checkModelExists(modelName: string): Promise<boolean> {
try {
const models = await listModelsViaSdk();
return models.some((model) => model.name === modelName);
} catch (error) {
console.error("Erro ao verificar modelos:", error);
return false;
}
}
export async function ensureModelExists(
modelName: string,
description?: string
): Promise<void> {
const exists = await checkModelExists(modelName);
if (!exists) {
console.log(`📥 Baixando modelo ${modelName}...`);
if (description) {
console.log(`ℹ️ ${description}`);
}
const stream = await pullModel(modelName);
let downloadedBytes = 0;
let totalBytes = 0;
let progress = 0;
// Processar stream para mostrar progresso
for await (const chunk of stream) {
if (chunk.status === "downloading" && chunk.completed && chunk.total) {
downloadedBytes = chunk.completed;
totalBytes = chunk.total;
progress = Math.round((downloadedBytes / totalBytes) * 100);
// Criar barra de progresso visual
const barLength = 20;
const filledLength = Math.round((progress / 100) * barLength);
const bar =
"█".repeat(filledLength) + "░".repeat(barLength - filledLength);
// Mostrar progresso com informações
const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(1);
const totalMB = (totalBytes / 1024 / 1024).toFixed(1);
process.stdout.write(
`\r📥 ${modelName}: [${bar}] ${progress}% (${downloadedMB}MB/${totalMB}MB)`
);
} else if (chunk.status === "success") {
console.log(`\n✅ Modelo ${modelName} baixado com sucesso!`);
if (description) {
console.log(`🎯 ${description.replace("será usado", "agora funciona")}`);
}
break;
}
}
} else {
console.log(`✅ Modelo ${modelName} já está disponível`);
}
}
+99
View File
@@ -0,0 +1,99 @@
import Database from "better-sqlite3";
import path from "path";
import fs from "fs";
let db: Database.Database | null = null;
export async function getPreferencesDb(): Promise<Database.Database> {
if (!db) {
try {
const dbPath = path.join(
process.cwd(),
"database",
"ollahub-preferences.db"
);
// Garantir que o diretório existe
const dbDir = path.dirname(dbPath);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
db = new Database(dbPath);
// Configurações de performance
db.pragma("journal_mode = WAL");
db.pragma("synchronous = NORMAL");
db.pragma("foreign_keys = ON");
db.pragma("cache_size = 10000");
db.pragma("temp_store = memory");
// Criar tabela de preferências se não existir
db.exec(`
CREATE TABLE IF NOT EXISTS user_preferences (
user_id TEXT PRIMARY KEY,
preferences TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
console.log("✅ Banco de preferências SQLite conectado com sucesso");
} catch (error) {
console.warn("⚠️ Erro ao conectar banco de preferências:", error);
throw error;
}
}
return db;
}
export async function getPreferences(userId: string = "default"): Promise<any> {
try {
const database = await getPreferencesDb();
const stmt = database.prepare(
"SELECT preferences FROM user_preferences WHERE user_id = ?"
);
const result = stmt.get(userId) as { preferences: string } | undefined;
if (result) {
return JSON.parse(result.preferences);
}
// Retornar preferências padrão
return {
selectedModel: null,
systemPrompt: "",
device: "auto",
gpuIndex: 0,
numGpu: 1,
};
} catch (error) {
console.error("Erro ao buscar preferências:", error);
throw error;
}
}
export async function setPreferences(
userId: string = "default",
preferences: any
): Promise<void> {
try {
const database = await getPreferencesDb();
const stmt = database.prepare(`
INSERT OR REPLACE INTO user_preferences (user_id, preferences, updated_at)
VALUES (?, ?, ?)
`);
stmt.run(userId, JSON.stringify(preferences), Date.now());
console.log("✅ Preferências salvas no SQLite");
} catch (error) {
console.error("Erro ao salvar preferências:", error);
throw error;
}
}
export function closePreferencesDb(): void {
if (db) {
db.close();
db = null;
}
}
+32
View File
@@ -0,0 +1,32 @@
import { generateEmbedding } from "./embeddings";
import { getDatabase } from "@/database/client";
import { EmbeddingRepository } from "@/database/repositories/embedding";
export async function semanticSearch(query: string, limit: number = 5) {
try {
const queryVector = await generateEmbedding(query);
const db = getDatabase();
const embeddingRepo = new EmbeddingRepository(db);
return await embeddingRepo.searchSimilar(queryVector, limit);
} catch (error) {
console.error("Erro na busca semântica:", error);
return [];
}
}
export async function addMessageEmbedding(
messageId: string,
content: string
): Promise<void> {
try {
const vector = await generateEmbedding(content);
const db = getDatabase();
const embeddingRepo = new EmbeddingRepository(db);
await embeddingRepo.create(messageId, vector);
} catch (error) {
console.error("Erro ao adicionar embedding:", error);
// Não falhar silenciosamente - embedding é opcional
}
}