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
@@ -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
@@ -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`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user