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
+2369
-197
No files matched your search
@@ -39,3 +39,12 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# database
|
||||||
|
database/*.db
|
||||||
|
database/extensions/*.so
|
||||||
|
database/extensions/*.dll
|
||||||
|
database/extensions/*.dylib
|
||||||
|
|
||||||
|
|
||||||
|
/.cursor
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,30 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
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 {
|
try {
|
||||||
const ollamaMessages = convertToOllamaMessages(messages);
|
const ollamaMessages = convertToOllamaMessages(messages);
|
||||||
const response = await chatWithStream(
|
const response = await chatWithStream(
|
||||||
@@ -26,24 +50,18 @@ export async function POST(req: NextRequest) {
|
|||||||
for await (const chunk of response) {
|
for await (const chunk of response) {
|
||||||
const token = chunk.message?.content || "";
|
const token = chunk.message?.content || "";
|
||||||
if (token) {
|
if (token) {
|
||||||
controller.enqueue(
|
safeEnqueue(JSON.stringify({ token }) + "\n");
|
||||||
new TextEncoder().encode(JSON.stringify({ token }) + "\n")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.enqueue(
|
safeEnqueue(JSON.stringify({ done: true }) + "\n");
|
||||||
new TextEncoder().encode(JSON.stringify({ done: true }) + "\n")
|
safeClose();
|
||||||
);
|
|
||||||
controller.close();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Chat streaming error:", error);
|
console.error("Chat streaming error:", error);
|
||||||
controller.enqueue(
|
safeEnqueue(
|
||||||
new TextEncoder().encode(
|
JSON.stringify({ error: "Failed to generate response" }) + "\n"
|
||||||
JSON.stringify({ error: "Failed to generate response" }) + "\n"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
controller.close();
|
safeClose();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ModeToggle } from "@/components/mode-toggle";
|
import { ModeToggle } from "@/components/mode-toggle";
|
||||||
import { ChatContainer } from "@/components/chat/ChatContainer";
|
import { ChatContainer } from "@/components/chat/ChatContainer";
|
||||||
|
import { ConversationSidebar } from "@/components/chat/ConversationSidebar";
|
||||||
import {
|
import {
|
||||||
SettingsButton,
|
SettingsButton,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
@@ -15,6 +16,23 @@ interface ClientHomeProps {
|
|||||||
|
|
||||||
export function ClientHome({ offline, models }: ClientHomeProps) {
|
export function ClientHome({ offline, models }: ClientHomeProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||||
|
const [currentConversationId, setCurrentConversationId] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const handleConversationCreated = () => {
|
||||||
|
// Incrementar trigger para forçar atualização da sidebar
|
||||||
|
setRefreshTrigger((prev) => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectConversation = (id: string) => {
|
||||||
|
setCurrentConversationId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewConversation = () => {
|
||||||
|
setCurrentConversationId(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-[var(--background)]">
|
<div className="h-screen flex flex-col bg-[var(--background)]">
|
||||||
@@ -33,7 +51,19 @@ export function ClientHome({ offline, models }: ClientHomeProps) {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1 overflow-hidden">
|
<main className="flex-1 overflow-hidden">
|
||||||
<ChatContainer models={models} offline={offline} />
|
<div className="flex h-full">
|
||||||
|
<ConversationSidebar
|
||||||
|
currentConversationId={currentConversationId}
|
||||||
|
onSelectConversation={handleSelectConversation}
|
||||||
|
onNewConversation={handleNewConversation}
|
||||||
|
refreshTrigger={refreshTrigger}
|
||||||
|
/>
|
||||||
|
<ChatContainer
|
||||||
|
models={models}
|
||||||
|
offline={offline}
|
||||||
|
onConversationCreated={handleConversationCreated}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<SettingsModal isOpen={open} onClose={() => setOpen(false)} />
|
<SettingsModal isOpen={open} onClose={() => setOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { ModelDropdown } from "@/components/model/ModelDropdown";
|
import { ModelDropdown } from "@/components/model/ModelDropdown";
|
||||||
import { ModelPullDialog } from "@/components/model/ModelPullDialog";
|
import { ModelPullDialog } from "@/components/model/ModelPullDialog";
|
||||||
import { ChatMessage, ChatInput } from "@/components/chat/ChatMessage";
|
import { ChatMessage } from "@/components/chat/ChatMessage";
|
||||||
import { SystemPrompt } from "@/components/chat/SystemPrompt";
|
import { SettingsModal } from "@/components/chat/SettingsModal";
|
||||||
|
import { Send, Square } from "lucide-react";
|
||||||
import type { ModelInfo } from "@/lib/models";
|
import type { ModelInfo } from "@/lib/models";
|
||||||
import type { Message } from "@/lib/chat";
|
import type { Message } from "@/lib/chat";
|
||||||
import { createMessage } from "@/lib/chat";
|
import { createMessage } from "@/lib/chat";
|
||||||
@@ -13,20 +14,40 @@ import { useUserPrefs, buildOllamaOptions } from "@/hooks/useUserPrefs";
|
|||||||
interface ChatContainerProps {
|
interface ChatContainerProps {
|
||||||
models: ModelInfo[];
|
models: ModelInfo[];
|
||||||
offline: boolean;
|
offline: boolean;
|
||||||
|
onConversationCreated?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatContainer({ models, offline }: ChatContainerProps) {
|
export function ChatContainer({
|
||||||
const { prefs, ready } = useUserPrefs();
|
models,
|
||||||
|
offline,
|
||||||
|
onConversationCreated,
|
||||||
|
}: ChatContainerProps) {
|
||||||
|
const { prefs, ready, update } = useUserPrefs();
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
const [selectedModel, setSelectedModel] = useState<string | null>(null);
|
||||||
|
const [conversationId, setConversationId] = useState<string | null>(null);
|
||||||
const [isStreaming, setIsStreaming] = useState(false);
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
const [streamingContent, setStreamingContent] = useState("");
|
const [streamingContent, setStreamingContent] = useState("");
|
||||||
|
const [inputContent, setInputContent] = useState("");
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
const [showPullDialog, setShowPullDialog] = useState(false);
|
const [showPullDialog, setShowPullDialog] = useState(false);
|
||||||
const [modelToPull, setModelToPull] = useState<string>("");
|
const [modelToPull, setModelToPull] = useState<string>("");
|
||||||
const [systemPrompt, setSystemPrompt] = useState<string>("");
|
const [systemPrompt, setSystemPrompt] = useState<string>("");
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Carregar modelo salvo ao iniciar
|
||||||
|
useEffect(() => {
|
||||||
|
if (ready && prefs.selectedModel && !selectedModel) {
|
||||||
|
setSelectedModel(prefs.selectedModel);
|
||||||
|
}
|
||||||
|
}, [ready, prefs.selectedModel]);
|
||||||
|
|
||||||
|
// Salvar modelo quando selecionado
|
||||||
|
const handleModelChange = (modelName: string) => {
|
||||||
|
setSelectedModel(modelName);
|
||||||
|
update({ selectedModel: modelName }); // Salvar no Redis
|
||||||
|
};
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
};
|
};
|
||||||
@@ -35,9 +56,55 @@ export function ChatContainer({ models, offline }: ChatContainerProps) {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages, streamingContent]);
|
}, [messages, streamingContent]);
|
||||||
|
|
||||||
|
const loadConversation = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/conversations/${id}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.conversation && data.messages) {
|
||||||
|
setConversationId(id);
|
||||||
|
setMessages(data.messages);
|
||||||
|
setSelectedModel(data.conversation.model);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar conversa:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startNewConversation = () => {
|
||||||
|
setConversationId(null);
|
||||||
|
setMessages([]);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSendMessage = async (content: string) => {
|
const handleSendMessage = async (content: string) => {
|
||||||
if (!selectedModel || isStreaming) return;
|
if (!selectedModel || isStreaming) return;
|
||||||
|
|
||||||
|
// Criar nova conversa se não existir (COM TÍTULO)
|
||||||
|
let currentConversationId = conversationId;
|
||||||
|
if (!currentConversationId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/conversations", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: selectedModel,
|
||||||
|
firstMessage: content, // Passar primeira mensagem
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const { id, title } = await response.json();
|
||||||
|
currentConversationId = id;
|
||||||
|
setConversationId(id);
|
||||||
|
console.log("✨ Chat criado:", title);
|
||||||
|
|
||||||
|
// Notificar sidebar para atualizar lista de conversas
|
||||||
|
if (onConversationCreated) {
|
||||||
|
onConversationCreated();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao criar conversa:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const userMessage = createMessage("user", content);
|
const userMessage = createMessage("user", content);
|
||||||
const newMessages = [...messages, userMessage];
|
const newMessages = [...messages, userMessage];
|
||||||
|
|
||||||
@@ -51,6 +118,19 @@ export function ChatContainer({ models, offline }: ChatContainerProps) {
|
|||||||
setIsStreaming(true);
|
setIsStreaming(true);
|
||||||
setStreamingContent("");
|
setStreamingContent("");
|
||||||
|
|
||||||
|
// Persistir mensagem do usuário
|
||||||
|
if (currentConversationId) {
|
||||||
|
try {
|
||||||
|
await fetch(`/api/conversations/${currentConversationId}/messages`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ role: "user", content }),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao persistir mensagem:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const options = buildOllamaOptions(
|
const options = buildOllamaOptions(
|
||||||
prefs.device,
|
prefs.device,
|
||||||
@@ -105,6 +185,28 @@ export function ChatContainer({ models, offline }: ChatContainerProps) {
|
|||||||
);
|
);
|
||||||
setMessages((prev) => [...prev, assistantMessage]);
|
setMessages((prev) => [...prev, assistantMessage]);
|
||||||
setStreamingContent("");
|
setStreamingContent("");
|
||||||
|
|
||||||
|
// Persistir mensagem do assistente
|
||||||
|
if (currentConversationId) {
|
||||||
|
try {
|
||||||
|
await fetch(
|
||||||
|
`/api/conversations/${currentConversationId}/messages`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
role: "assistant",
|
||||||
|
content: assistantContent,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Erro ao persistir mensagem do assistente:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
throw new Error(data.error);
|
throw new Error(data.error);
|
||||||
@@ -128,16 +230,36 @@ export function ChatContainer({ models, offline }: ChatContainerProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (inputContent.trim() && !isStreaming) {
|
||||||
|
handleSendMessage(inputContent.trim());
|
||||||
|
setInputContent("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleStop = () => {
|
const handleStop = () => {
|
||||||
if (abortRef.current && isStreaming) {
|
if (abortRef.current && isStreaming) {
|
||||||
abortRef.current.abort();
|
abortRef.current.abort();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSystemPromptChange = (prompt: string) => {
|
||||||
|
setSystemPrompt(prompt);
|
||||||
|
update({ systemPrompt: prompt }); // Salvar no SQLite
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectModel = (modelId: string) => {
|
const handleSelectModel = (modelId: string) => {
|
||||||
const model = models.find((m) => m.id === modelId);
|
const model = models.find((m) => m.id === modelId);
|
||||||
if (model) {
|
if (model) {
|
||||||
setSelectedModel(modelId);
|
handleModelChange(modelId);
|
||||||
// Optional: clear chat when switching models
|
// Optional: clear chat when switching models
|
||||||
// setMessages([]);
|
// setMessages([]);
|
||||||
}
|
}
|
||||||
@@ -191,97 +313,98 @@ export function ChatContainer({ models, offline }: ChatContainerProps) {
|
|||||||
const selectedModelData = models.find((m) => m.id === selectedModel);
|
const selectedModelData = models.find((m) => m.id === selectedModel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full bg-[var(--background)]">
|
<div className="flex-1 flex flex-col bg-[var(--background)] min-h-0">
|
||||||
{/* Sidebar */}
|
{/* Chat Header */}
|
||||||
<div className="w-80 border-r border-[var(--border)] bg-[var(--surface)] p-4 overflow-y-auto">
|
<div className="border-b border-[var(--border)] p-4 flex items-center justify-between gap-3 flex-shrink-0">
|
||||||
<div className="mb-4">
|
<div className="flex items-center gap-3">
|
||||||
<h2 className="text-sm font-medium text-[var(--foreground)]/80 mb-2">
|
|
||||||
Models
|
|
||||||
</h2>
|
|
||||||
<ModelDropdown
|
|
||||||
models={models}
|
|
||||||
selectedModel={selectedModel}
|
|
||||||
onSelectModel={handleSelectModel}
|
|
||||||
disabled={isStreaming}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{offline && (
|
|
||||||
<div className="rounded-lg border border-orange-200 bg-orange-50 p-3">
|
|
||||||
<p className="text-sm text-orange-800">
|
|
||||||
Ollama is offline. Using mock data.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedModelData && (
|
|
||||||
<div className="rounded-lg border border-[var(--border)] bg-[var(--background)] p-3">
|
|
||||||
<h3 className="font-medium text-sm mb-2">Selected Model</h3>
|
|
||||||
<div className="space-y-1 text-xs text-[var(--foreground)]/80">
|
|
||||||
<div>Name: {selectedModelData.name}</div>
|
|
||||||
<div>Size: {selectedModelData.sizeGB} GB</div>
|
|
||||||
<div>Quantization: {selectedModelData.quantization}</div>
|
|
||||||
<div>Device: {selectedModelData.device}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SystemPrompt onSystemPromptChange={setSystemPrompt} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Chat Area */}
|
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
|
||||||
{/* Chat Header */}
|
|
||||||
<div className="border-b border-[var(--border)] p-4 flex items-center justify-between gap-3">
|
|
||||||
<h1 className="text-lg font-semibold">
|
<h1 className="text-lg font-semibold">
|
||||||
{selectedModelData
|
{selectedModelData
|
||||||
? `Chat with ${selectedModelData.name}`
|
? "Chat with"
|
||||||
: "Select a model to start chatting"}
|
: "Select a model to start chatting"}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center gap-2">
|
{selectedModelData && (
|
||||||
<button
|
<ModelDropdown
|
||||||
onClick={handleStop}
|
models={models}
|
||||||
disabled={!isStreaming}
|
selectedModel={selectedModel}
|
||||||
className="rounded-md border border-[var(--border)] px-3 py-1 text-sm hover:bg-[var(--surface)] disabled:opacity-50"
|
onSelectModel={handleSelectModel}
|
||||||
>
|
disabled={isStreaming}
|
||||||
Stop
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Messages */}
|
|
||||||
<div className="flex-1 overflow-y-auto min-h-0">
|
|
||||||
{messages.length === 0 ? (
|
|
||||||
<div className="flex h-full items-center justify-center">
|
|
||||||
<div className="text-center text-[var(--foreground)]/60">
|
|
||||||
<p className="text-lg mb-2">Welcome to Ollahub</p>
|
|
||||||
<p className="text-sm">Select a model and start chatting!</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{messages.map((message) => (
|
|
||||||
<ChatMessage key={message.id} message={message} />
|
|
||||||
))}
|
|
||||||
{isStreaming && streamingContent && (
|
|
||||||
<ChatMessage
|
|
||||||
message={createMessage("assistant", streamingContent)}
|
|
||||||
isStreaming={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div ref={messagesEndRef} />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SettingsModal
|
||||||
|
systemPrompt={systemPrompt}
|
||||||
|
onSystemPromptChange={handleSystemPromptChange}
|
||||||
|
selectedModel={selectedModel}
|
||||||
|
models={models}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleStop}
|
||||||
|
disabled={!isStreaming}
|
||||||
|
className="rounded-md border border-[var(--border)] px-3 py-1 text-sm hover:bg-[var(--surface)] disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Stop
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Input */}
|
{/* Messages */}
|
||||||
<ChatInput
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
onSendMessage={handleSendMessage}
|
{messages.length === 0 ? (
|
||||||
disabled={!selectedModel || isStreaming}
|
<div className="flex h-full items-center justify-center">
|
||||||
placeholder={
|
<div className="text-center text-[var(--foreground)]/60">
|
||||||
selectedModel ? "Type a message..." : "Select a model first..."
|
<p className="text-lg mb-2">Welcome to Ollahub</p>
|
||||||
}
|
<p className="text-sm">Select a model and start chatting!</p>
|
||||||
/>
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{messages.map((message) => (
|
||||||
|
<ChatMessage key={message.id} message={message} />
|
||||||
|
))}
|
||||||
|
{isStreaming && streamingContent && (
|
||||||
|
<ChatMessage
|
||||||
|
message={createMessage("assistant", streamingContent)}
|
||||||
|
isStreaming={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div className="flex-shrink-0 border-t border-[var(--border)] bg-[var(--background)]">
|
||||||
|
<form onSubmit={handleSubmit} className="p-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<textarea
|
||||||
|
value={inputContent}
|
||||||
|
onChange={(e) => setInputContent(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={
|
||||||
|
selectedModel ? "Type a message..." : "Select a model first..."
|
||||||
|
}
|
||||||
|
disabled={!selectedModel || isStreaming}
|
||||||
|
className="flex-1 resize-none rounded-lg border border-[var(--border)] bg-[var(--background)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[var(--accent)] disabled:opacity-50"
|
||||||
|
rows={1}
|
||||||
|
maxLength={2000}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type={isStreaming ? "button" : "submit"}
|
||||||
|
onClick={isStreaming ? handleStop : undefined}
|
||||||
|
disabled={
|
||||||
|
isStreaming ? false : !inputContent.trim() || !selectedModel
|
||||||
|
}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-lg bg-[var(--accent)] text-white transition-colors hover:bg-[color-mix(in_oklab,var(--accent),black_10%)] disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isStreaming ? (
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Send className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pull Dialog */}
|
{/* Pull Dialog */}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { User, Bot, Send, Loader2 } from "lucide-react";
|
import { User, Bot, Send, Loader2, Square } from "lucide-react";
|
||||||
import { MarkdownRenderer } from "@/components/message/MarkdownRenderer";
|
import { MarkdownRenderer } from "@/components/message/MarkdownRenderer";
|
||||||
import { MessageActions } from "@/components/message/MessageActions";
|
import { MessageActions } from "@/components/message/MessageActions";
|
||||||
import { extractCodeBlocks } from "@/hooks/useMarkdown";
|
|
||||||
import type { Message } from "@/lib/chat";
|
import type { Message } from "@/lib/chat";
|
||||||
import { formatTimestamp } from "@/lib/chat";
|
import { formatTimestamp } from "@/lib/chat";
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ export function ChatMessage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`max-w-[80%] rounded-lg px-4 py-2 ${
|
className={`max-w-[80%] relative rounded-lg px-4 py-2 ${
|
||||||
isUser
|
isUser
|
||||||
? "bg-[var(--accent)] text-white"
|
? "bg-[var(--accent)] text-white"
|
||||||
: "bg-[var(--surface)] text-[var(--foreground)]"
|
: "bg-[var(--surface)] text-[var(--foreground)]"
|
||||||
@@ -44,11 +43,8 @@ export function ChatMessage({
|
|||||||
</div>
|
</div>
|
||||||
{isStreaming && <span className="ml-1 animate-pulse">▊</span>}
|
{isStreaming && <span className="ml-1 animate-pulse">▊</span>}
|
||||||
{!isUser && !isStreaming && (
|
{!isUser && !isStreaming && (
|
||||||
<div className="mt-2">
|
<div className="absolute bottom-2 right-2">
|
||||||
<MessageActions
|
<MessageActions content={message.content} />
|
||||||
content={message.content}
|
|
||||||
extractCodeBlocks={extractCodeBlocks}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
@@ -71,13 +67,17 @@ export function ChatMessage({
|
|||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
onSendMessage: (content: string) => void;
|
onSendMessage: (content: string) => void;
|
||||||
|
onStop?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
isStreaming?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
|
onStop,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
isStreaming = false,
|
||||||
placeholder = "Type a message...",
|
placeholder = "Type a message...",
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
@@ -125,11 +125,14 @@ export function ChatInput({
|
|||||||
maxLength={2000}
|
maxLength={2000}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type={isStreaming ? "button" : "submit"}
|
||||||
disabled={!content.trim() || disabled}
|
onClick={isStreaming ? onStop : undefined}
|
||||||
|
disabled={isStreaming ? false : !content.trim() || disabled}
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-lg bg-[var(--accent)] text-white transition-colors hover:bg-[color-mix(in_oklab,var(--accent),black_10%)] disabled:opacity-50"
|
className="flex h-10 w-10 items-center justify-center rounded-lg bg-[var(--accent)] text-white transition-colors hover:bg-[color-mix(in_oklab,var(--accent),black_10%)] disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{disabled ? (
|
{isStreaming ? (
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
) : disabled ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Send className="h-4 w-4" />
|
<Send className="h-4 w-4" />
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { MessageSquare, Plus, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
type Conversation = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
model: string;
|
||||||
|
updated_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConversationSidebarProps = {
|
||||||
|
currentConversationId: string | null;
|
||||||
|
onSelectConversation: (id: string) => void;
|
||||||
|
onNewConversation: () => void;
|
||||||
|
refreshTrigger?: number; // Para forçar atualização
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ConversationSidebar({
|
||||||
|
currentConversationId,
|
||||||
|
onSelectConversation,
|
||||||
|
onNewConversation,
|
||||||
|
refreshTrigger,
|
||||||
|
}: ConversationSidebarProps) {
|
||||||
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadConversations();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Recarregar conversas quando refreshTrigger mudar
|
||||||
|
useEffect(() => {
|
||||||
|
if (refreshTrigger !== undefined) {
|
||||||
|
loadConversations();
|
||||||
|
}
|
||||||
|
}, [refreshTrigger]);
|
||||||
|
|
||||||
|
const loadConversations = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/conversations");
|
||||||
|
const data = await response.json();
|
||||||
|
setConversations(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar conversas:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (!confirm("Deletar esta conversa?")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`/api/conversations/${id}`, { method: "DELETE" });
|
||||||
|
setConversations((prev) => prev.filter((c) => c.id !== id));
|
||||||
|
|
||||||
|
if (currentConversationId === id) {
|
||||||
|
onNewConversation();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao deletar conversa:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-64 h-full bg-[var(--surface)] border-r border-[var(--border)] flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b border-[var(--border)]">
|
||||||
|
<button
|
||||||
|
onClick={onNewConversation}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-[var(--accent)] text-white rounded-lg hover:opacity-90 transition-opacity"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Nova Conversa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lista de Conversas */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-2">
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center p-4 text-sm text-[var(--foreground)]/60">
|
||||||
|
Carregando...
|
||||||
|
</div>
|
||||||
|
) : conversations.length === 0 ? (
|
||||||
|
<div className="text-center p-4 text-sm text-[var(--foreground)]/60">
|
||||||
|
Nenhuma conversa ainda
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
conversations.map((conv) => (
|
||||||
|
<div
|
||||||
|
key={conv.id}
|
||||||
|
onClick={() => onSelectConversation(conv.id)}
|
||||||
|
className={`w-full flex items-center gap-2 p-3 rounded-lg mb-1 text-left transition-colors group cursor-pointer ${
|
||||||
|
currentConversationId === conv.id
|
||||||
|
? "bg-[var(--accent)]/10 text-[var(--accent)]"
|
||||||
|
: "hover:bg-[var(--background)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<MessageSquare className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<span className="flex-1 text-sm truncate">{conv.title}</span>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleDelete(conv.id, e)}
|
||||||
|
className="p-1 hover:bg-[var(--background)] rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Settings, X } from "lucide-react";
|
||||||
|
|
||||||
|
type SettingsModalProps = {
|
||||||
|
systemPrompt: string;
|
||||||
|
onSystemPromptChange: (prompt: string) => void;
|
||||||
|
selectedModel: string | null;
|
||||||
|
models: Array<{ id: string; name: string; device: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SettingsModal({
|
||||||
|
systemPrompt,
|
||||||
|
onSystemPromptChange,
|
||||||
|
selectedModel,
|
||||||
|
models,
|
||||||
|
}: SettingsModalProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [tempPrompt, setTempPrompt] = useState(systemPrompt);
|
||||||
|
|
||||||
|
const selectedModelData = models.find((m) => m.id === selectedModel);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSystemPromptChange(tempPrompt);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setTempPrompt(systemPrompt);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Botão de Configurações */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="p-2 rounded-lg hover:bg-[var(--surface)] transition-colors"
|
||||||
|
aria-label="Configurações"
|
||||||
|
>
|
||||||
|
<Settings className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-[var(--background)] rounded-lg border border-[var(--border)] w-full max-w-2xl mx-4 max-h-[80vh] overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-[var(--border)]">
|
||||||
|
<h2 className="text-lg font-semibold">Configurações</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="p-1 rounded hover:bg-[var(--surface)] transition-colors"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-4 space-y-6 overflow-y-auto max-h-[60vh]">
|
||||||
|
{/* Model Info */}
|
||||||
|
{selectedModelData && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="font-medium">Modelo Selecionado</h3>
|
||||||
|
<div className="rounded-lg border border-[var(--border)] bg-[var(--surface)] p-3">
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-[var(--foreground)]/60">Nome:</span>
|
||||||
|
<span>{selectedModelData.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-[var(--foreground)]/60">Dispositivo:</span>
|
||||||
|
<span className="capitalize">{selectedModelData.device}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* System Prompt */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="font-medium">System Prompt</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-[var(--foreground)]/80">
|
||||||
|
Instruções para o modelo (opcional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={tempPrompt}
|
||||||
|
onChange={(e) => setTempPrompt(e.target.value)}
|
||||||
|
placeholder="Ex: Você é um assistente útil e prestativo..."
|
||||||
|
className="w-full h-32 p-3 rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)] resize-none focus:outline-none focus:ring-2 focus:ring-[var(--accent)]"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-[var(--foreground)]/60">
|
||||||
|
Este prompt será usado como contexto para todas as mensagens desta conversa.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-end gap-2 p-4 border-t border-[var(--border)]">
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="px-4 py-2 rounded-lg border border-[var(--border)] hover:bg-[var(--surface)] transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="px-4 py-2 rounded-lg bg-[var(--accent)] text-white hover:opacity-90 transition-opacity"
|
||||||
|
>
|
||||||
|
Salvar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import ReactMarkdown from "react-markdown";
|
|||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import rehypeHighlight from "rehype-highlight";
|
import rehypeHighlight from "rehype-highlight";
|
||||||
|
import { Copy } from "lucide-react";
|
||||||
|
|
||||||
type MarkdownRendererProps = {
|
type MarkdownRendererProps = {
|
||||||
content: string;
|
content: string;
|
||||||
@@ -15,6 +16,56 @@ export function MarkdownRenderer({ content }: MarkdownRendererProps) {
|
|||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[rehypeRaw, rehypeHighlight]}
|
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}
|
{content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
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 = {
|
type MessageActionsProps = {
|
||||||
content: string;
|
content: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
onRegenerate?: () => void;
|
onRegenerate?: () => void;
|
||||||
extractCodeBlocks?: (
|
|
||||||
text: string
|
|
||||||
) => Array<{ language: string; code: string }>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MessageActions({
|
export function MessageActions({
|
||||||
@@ -18,70 +17,69 @@ export function MessageActions({
|
|||||||
disabled,
|
disabled,
|
||||||
onEdit,
|
onEdit,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
extractCodeBlocks,
|
|
||||||
}: MessageActionsProps) {
|
}: MessageActionsProps) {
|
||||||
const [copied, setCopied] = useState<"none" | "markdown" | "text" | "code">(
|
const [copied, setCopied] = useState(false);
|
||||||
"none"
|
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);
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(flag as any);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied("none"), 1500);
|
setTimeout(() => setCopied(false), 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stripMarkdown = (md: string) =>
|
const handleCopyDefault = () => {
|
||||||
md
|
const textToCopy =
|
||||||
.replace(/```[\s\S]*?```/g, (block) =>
|
copyMode === "markdown" ? content : stripMarkdown(content);
|
||||||
block.replace(/```[\s\S]*?\n/, "").replace(/```$/, "")
|
copy(textToCopy);
|
||||||
)
|
};
|
||||||
.replace(/[#*_>`~-]/g, "");
|
|
||||||
const codeBlocks = extractCodeBlocks ? extractCodeBlocks(content) : [];
|
const handleModeSelect = (mode: "markdown" | "text") => {
|
||||||
|
setCopyMode(mode);
|
||||||
|
const textToCopy = mode === "markdown" ? content : stripMarkdown(content);
|
||||||
|
copy(textToCopy);
|
||||||
|
setDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1 opacity-70">
|
<div className="flex items-center gap-1 opacity-70">
|
||||||
<button
|
<button
|
||||||
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"
|
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"
|
||||||
onClick={() => copy(content, "markdown")}
|
onClick={handleCopyDefault}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-label="Copy as markdown"
|
aria-label="Copiar"
|
||||||
>
|
>
|
||||||
{copied === "markdown" ? (
|
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
)}{" "}
|
|
||||||
MD
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"
|
<div className="relative">
|
||||||
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) => (
|
|
||||||
<button
|
<button
|
||||||
key={`${b.language}-${i}`}
|
className="rounded px-1 py-1 text-xs hover:bg-[var(--surface)]"
|
||||||
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||||
onClick={() => copy(b.code, "code")}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-label={`Copy ${b.language} code`}
|
aria-label="Selecionar formato"
|
||||||
>
|
>
|
||||||
{copied === "code" ? (
|
<ChevronDown className="h-3 w-3" />
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
)}{" "}
|
|
||||||
{b.language}
|
|
||||||
</button>
|
</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 && (
|
{onEdit && (
|
||||||
<button
|
<button
|
||||||
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"
|
className="rounded px-2 py-1 text-xs hover:bg-[var(--surface)]"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
let db: Database.Database | null = null;
|
||||||
|
|
||||||
|
export function getDatabase(): Database.Database {
|
||||||
|
if (!db) {
|
||||||
|
const dbPath = path.join(process.cwd(), "database", "ollahub.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");
|
||||||
|
}
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeDatabase(): void {
|
||||||
|
if (db) {
|
||||||
|
db.close();
|
||||||
|
db = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export function runMigrations(db: Database.Database): void {
|
||||||
|
// Criar tabela de migrações
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS _migrations (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
applied_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Verificar migrações aplicadas
|
||||||
|
const applied = db.prepare("SELECT name FROM _migrations").all() as Array<{
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
const appliedNames = new Set(applied.map((m) => m.name));
|
||||||
|
|
||||||
|
// Aplicar migração inicial se não foi aplicada
|
||||||
|
if (!appliedNames.has("001_initial_schema")) {
|
||||||
|
console.log("📋 Aplicando migração: 001_initial_schema");
|
||||||
|
|
||||||
|
const schemaPath = path.join(process.cwd(), "database", "schema.sql");
|
||||||
|
const schema = fs.readFileSync(schemaPath, "utf-8");
|
||||||
|
|
||||||
|
db.exec(schema);
|
||||||
|
|
||||||
|
// Registrar migração
|
||||||
|
db.prepare("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)").run(
|
||||||
|
"001_initial_schema",
|
||||||
|
Date.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ Migração 001_initial_schema aplicada");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar extensão vetorial se disponível
|
||||||
|
try {
|
||||||
|
const extPath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"database",
|
||||||
|
"extensions",
|
||||||
|
"vector.so"
|
||||||
|
);
|
||||||
|
if (fs.existsSync(extPath)) {
|
||||||
|
db.loadExtension(extPath);
|
||||||
|
|
||||||
|
// Inicializar índice vetorial
|
||||||
|
db.exec(`
|
||||||
|
SELECT vector_init('embeddings', 'vector', 'type=FLOAT32,dimension=384');
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Quantizar para busca mais rápida
|
||||||
|
db.exec(`
|
||||||
|
SELECT vector_quantize('embeddings', 'vector');
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("🔍 Extensão vetorial carregada e inicializada");
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"⚠️ Extensão vetorial não encontrada - busca vetorial desabilitada"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("⚠️ Erro ao carregar extensão vetorial:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,78 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
export interface Conversation {
|
||||||
|
id: string;
|
||||||
|
title: string | null;
|
||||||
|
model: string;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConversationRepository {
|
||||||
|
constructor(private db: Database.Database) {}
|
||||||
|
|
||||||
|
create(model: string, title?: string): string {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO conversations (id, title, model, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run(id, title || "Nova conversa", model, now, now);
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll(): Conversation[] {
|
||||||
|
return this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT * FROM conversations
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all() as Conversation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Conversation | null {
|
||||||
|
return this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT * FROM conversations WHERE id = ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(id) as Conversation | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTitle(id: string, title: string): void {
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
UPDATE conversations
|
||||||
|
SET title = ?, updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run(title, Date.now(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimestamp(id: string): void {
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
UPDATE conversations
|
||||||
|
SET updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run(Date.now(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id: string): void {
|
||||||
|
this.db.prepare("DELETE FROM conversations WHERE id = ?").run(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
|
||||||
|
export interface Embedding {
|
||||||
|
id: string;
|
||||||
|
message_id: string;
|
||||||
|
vector: Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
role: string;
|
||||||
|
distance: number;
|
||||||
|
conversation_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmbeddingRepository {
|
||||||
|
constructor(private db: Database.Database) {}
|
||||||
|
|
||||||
|
async create(messageId: string, vector: Float32Array): Promise<void> {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const blob = Buffer.from(vector.buffer);
|
||||||
|
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO embeddings (id, message_id, vector)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run(id, messageId, blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchSimilar(
|
||||||
|
queryVector: Float32Array,
|
||||||
|
limit: number = 5
|
||||||
|
): Promise<SearchResult[]> {
|
||||||
|
const queryBlob = Buffer.from(queryVector.buffer);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Tentar busca vetorial se extensão estiver disponível
|
||||||
|
return this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
m.id, m.content, m.role, m.conversation_id,
|
||||||
|
vector_distance(e.vector, ?) as distance
|
||||||
|
FROM embeddings e
|
||||||
|
JOIN messages m ON m.id = e.message_id
|
||||||
|
ORDER BY distance ASC
|
||||||
|
LIMIT ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all(queryBlob, limit) as SearchResult[];
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"Busca vetorial não disponível, usando busca simples:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fallback: busca simples por conteúdo
|
||||||
|
return this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
m.id, m.content, m.role, m.conversation_id,
|
||||||
|
0.0 as distance
|
||||||
|
FROM messages m
|
||||||
|
WHERE m.content LIKE ?
|
||||||
|
ORDER BY m.timestamp DESC
|
||||||
|
LIMIT ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all(`%${queryVector.toString()}%`, limit) as SearchResult[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findByMessage(messageId: string): Embedding | null {
|
||||||
|
return this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT * FROM embeddings WHERE message_id = ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(messageId) as Embedding | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteByMessage(messageId: string): void {
|
||||||
|
this.db
|
||||||
|
.prepare("DELETE FROM embeddings WHERE message_id = ?")
|
||||||
|
.run(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
count(): number {
|
||||||
|
const result = this.db
|
||||||
|
.prepare("SELECT COUNT(*) as count FROM embeddings")
|
||||||
|
.get() as { count: number };
|
||||||
|
return result.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
import type { ChatRole } from "@/lib/chat";
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
id: string;
|
||||||
|
conversation_id: string;
|
||||||
|
role: ChatRole;
|
||||||
|
content: string;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MessageRepository {
|
||||||
|
constructor(private db: Database.Database) {}
|
||||||
|
|
||||||
|
create(conversationId: string, role: ChatRole, content: string): string {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
INSERT INTO messages (id, conversation_id, role, content, timestamp)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run(id, conversationId, role, content, Date.now());
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
findByConversation(conversationId: string): Message[] {
|
||||||
|
return this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT * FROM messages
|
||||||
|
WHERE conversation_id = ?
|
||||||
|
ORDER BY timestamp ASC
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.all(conversationId) as Message[];
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Message | null {
|
||||||
|
return this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT * FROM messages WHERE id = ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(id) as Message | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContent(id: string, content: string): void {
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
UPDATE messages
|
||||||
|
SET content = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.run(content, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id: string): void {
|
||||||
|
this.db.prepare("DELETE FROM messages WHERE id = ?").run(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteByConversation(conversationId: string): void {
|
||||||
|
this.db
|
||||||
|
.prepare("DELETE FROM messages WHERE conversation_id = ?")
|
||||||
|
.run(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
countByConversation(conversationId: string): number {
|
||||||
|
const result = this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT COUNT(*) as count FROM messages
|
||||||
|
WHERE conversation_id = ?
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.get(conversationId) as { count: number };
|
||||||
|
|
||||||
|
return result.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- Schema inicial do banco de dados OllaHub
|
||||||
|
-- Conversas
|
||||||
|
CREATE TABLE conversations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Mensagens
|
||||||
|
CREATE TABLE messages (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
conversation_id TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Embeddings vetoriais (384 dimensões do Ollama)
|
||||||
|
CREATE TABLE embeddings (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
message_id TEXT NOT NULL,
|
||||||
|
vector BLOB NOT NULL,
|
||||||
|
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices para performance
|
||||||
|
CREATE INDEX idx_messages_conversation ON messages(conversation_id, timestamp);
|
||||||
|
CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC);
|
||||||
|
CREATE INDEX idx_embeddings_message ON embeddings(message_id);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export type CopyMode = "markdown" | "text";
|
||||||
|
|
||||||
|
const KEY = "ollahub-copy-mode";
|
||||||
|
|
||||||
|
export function useCopyPrefs() {
|
||||||
|
const [copyMode, setCopyModeState] = useState<CopyMode>("markdown");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
const saved = localStorage.getItem(KEY) as CopyMode | null;
|
||||||
|
if (saved === "markdown" || saved === "text") {
|
||||||
|
setCopyModeState(saved);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setCopyMode = useCallback((mode: CopyMode) => {
|
||||||
|
setCopyModeState(mode);
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem(KEY, mode);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { copyMode, setCopyMode };
|
||||||
|
}
|
||||||
+26
-29
@@ -5,20 +5,15 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
export type DevicePref = "auto" | "cpu" | "gpu";
|
export type DevicePref = "auto" | "cpu" | "gpu";
|
||||||
|
|
||||||
type Prefs = {
|
type Prefs = {
|
||||||
|
selectedModel: string | null; // NOVO
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
device: DevicePref;
|
device: DevicePref;
|
||||||
gpuIndex: number; // main_gpu
|
gpuIndex: number;
|
||||||
numGpu: number; // num_gpu
|
numGpu: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const KEYS = {
|
|
||||||
systemPrompt: "ollahub-system-prompt",
|
|
||||||
device: "ollahub-device-pref",
|
|
||||||
gpuIndex: "ollahub-gpu-index",
|
|
||||||
numGpu: "ollahub-num-gpu",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const defaultPrefs: Prefs = {
|
const defaultPrefs: Prefs = {
|
||||||
|
selectedModel: null, // NOVO
|
||||||
systemPrompt: "",
|
systemPrompt: "",
|
||||||
device: "auto",
|
device: "auto",
|
||||||
gpuIndex: 0,
|
gpuIndex: 0,
|
||||||
@@ -29,32 +24,34 @@ export function useUserPrefs() {
|
|||||||
const [prefs, setPrefs] = useState<Prefs>(defaultPrefs);
|
const [prefs, setPrefs] = useState<Prefs>(defaultPrefs);
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
|
// Carregar do Redis
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
fetch("/api/preferences")
|
||||||
const p: Prefs = {
|
.then((res) => res.json())
|
||||||
systemPrompt:
|
.then((data) => {
|
||||||
localStorage.getItem(KEYS.systemPrompt) || defaultPrefs.systemPrompt,
|
setPrefs(data);
|
||||||
device:
|
setReady(true);
|
||||||
(localStorage.getItem(KEYS.device) as DevicePref) ||
|
})
|
||||||
defaultPrefs.device,
|
.catch((err) => {
|
||||||
gpuIndex: Number(
|
console.error("Erro ao carregar preferências:", err);
|
||||||
localStorage.getItem(KEYS.gpuIndex) || defaultPrefs.gpuIndex
|
setReady(true);
|
||||||
),
|
});
|
||||||
numGpu: Number(localStorage.getItem(KEYS.numGpu) || defaultPrefs.numGpu),
|
|
||||||
};
|
|
||||||
setPrefs(p);
|
|
||||||
setReady(true);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Salvar no Redis
|
||||||
const update = useCallback((next: Partial<Prefs>) => {
|
const update = useCallback((next: Partial<Prefs>) => {
|
||||||
setPrefs((prev) => {
|
setPrefs((prev) => {
|
||||||
const merged: Prefs = { ...prev, ...next };
|
const merged: Prefs = { ...prev, ...next };
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
localStorage.setItem(KEYS.systemPrompt, merged.systemPrompt);
|
// Salvar no Redis
|
||||||
localStorage.setItem(KEYS.device, merged.device);
|
fetch("/api/preferences", {
|
||||||
localStorage.setItem(KEYS.gpuIndex, String(merged.gpuIndex));
|
method: "POST",
|
||||||
localStorage.setItem(KEYS.numGpu, String(merged.numGpu));
|
headers: { "Content-Type": "application/json" },
|
||||||
}
|
body: JSON.stringify(merged),
|
||||||
|
}).catch((err) =>
|
||||||
|
console.error("Erro ao salvar preferências:", err)
|
||||||
|
);
|
||||||
|
|
||||||
return merged;
|
return merged;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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 });
|
const client = new Ollama({ host: OLLAMA_HOST });
|
||||||
return client.chat({
|
return client.chat({
|
||||||
model,
|
model,
|
||||||
messages,
|
messages: system
|
||||||
|
? [...messages, { role: "system", content: system }]
|
||||||
|
: messages,
|
||||||
stream: true,
|
stream: true,
|
||||||
options,
|
options,
|
||||||
system,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,3 +41,62 @@ export async function pullModel(model: string) {
|
|||||||
const client = new Ollama({ host: OLLAMA_HOST });
|
const client = new Ollama({ host: OLLAMA_HOST });
|
||||||
return client.pull({ model, stream: true });
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"predev": "tsx scripts/init-db.ts",
|
||||||
|
"prebuild": "tsx scripts/init-db.ts",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
@@ -10,6 +12,7 @@
|
|||||||
"format": "biome format --write"
|
"format": "biome format --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^12.4.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.548.0",
|
||||||
@@ -29,10 +32,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.2.0",
|
"@biomejs/biome": "2.2.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
"tsx": "^4.20.6",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+573
-2
@@ -8,6 +8,9 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
better-sqlite3:
|
||||||
|
specifier: ^12.4.1
|
||||||
|
version: 12.4.1
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -60,6 +63,9 @@ importers:
|
|||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
version: 4.1.16
|
version: 4.1.16
|
||||||
|
'@types/better-sqlite3':
|
||||||
|
specifier: ^7.6.13
|
||||||
|
version: 7.6.13
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20
|
specifier: ^20
|
||||||
version: 20.19.23
|
version: 20.19.23
|
||||||
@@ -72,6 +78,9 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
version: 4.1.16
|
version: 4.1.16
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.20.6
|
||||||
|
version: 4.20.6
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5
|
specifier: ^5
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -142,6 +151,162 @@ packages:
|
|||||||
'@emnapi/[email protected]':
|
'@emnapi/[email protected]':
|
||||||
resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==}
|
resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==}
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@img/[email protected]':
|
'@img/[email protected]':
|
||||||
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -426,6 +591,9 @@ packages:
|
|||||||
'@tailwindcss/[email protected]':
|
'@tailwindcss/[email protected]':
|
||||||
resolution: {integrity: sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==}
|
resolution: {integrity: sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
@@ -473,6 +641,22 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==}
|
||||||
|
engines: {node: 20.x || 22.x || 23.x || 24.x}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==}
|
resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==}
|
||||||
|
|
||||||
@@ -500,6 +684,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
@@ -531,6 +718,14 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -542,6 +737,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -550,6 +748,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -557,16 +760,37 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
|
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
|
resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||||
engines: {node: '>=0.4.x'}
|
engines: {node: '>=0.4.x'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
@@ -619,6 +843,15 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
|
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
|
||||||
|
|
||||||
@@ -873,6 +1106,16 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
|
resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -881,6 +1124,9 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==}
|
resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -908,9 +1154,16 @@ packages:
|
|||||||
sass:
|
sass:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg==}
|
resolution: {integrity: sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
|
resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
|
||||||
|
|
||||||
@@ -931,6 +1184,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
|
resolution: {integrity: sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -948,6 +1206,13 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
|
resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -968,6 +1233,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==}
|
resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==}
|
||||||
|
|
||||||
@@ -992,6 +1261,12 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@@ -1004,6 +1279,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==}
|
resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -1014,9 +1295,16 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==}
|
resolution: {integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==}
|
||||||
|
|
||||||
@@ -1046,6 +1334,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||||
|
|
||||||
@@ -1055,6 +1350,14 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@@ -1084,6 +1387,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
|
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
|
||||||
|
|
||||||
@@ -1099,6 +1405,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
|
resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
@@ -1152,6 +1461,84 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/[email protected]':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@img/[email protected]':
|
'@img/[email protected]':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1359,6 +1746,10 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
tailwindcss: 4.1.16
|
tailwindcss: 4.1.16
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.19.23
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
@@ -1405,6 +1796,28 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
bindings: 1.5.0
|
||||||
|
prebuild-install: 7.1.3
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
file-uri-to-path: 1.0.0
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
buffer: 5.7.1
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -1423,6 +1836,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
@@ -1445,6 +1860,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
mimic-response: 3.1.0
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -1453,6 +1874,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -1460,18 +1885,62 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.25.11
|
||||||
|
'@esbuild/android-arm': 0.25.11
|
||||||
|
'@esbuild/android-arm64': 0.25.11
|
||||||
|
'@esbuild/android-x64': 0.25.11
|
||||||
|
'@esbuild/darwin-arm64': 0.25.11
|
||||||
|
'@esbuild/darwin-x64': 0.25.11
|
||||||
|
'@esbuild/freebsd-arm64': 0.25.11
|
||||||
|
'@esbuild/freebsd-x64': 0.25.11
|
||||||
|
'@esbuild/linux-arm': 0.25.11
|
||||||
|
'@esbuild/linux-arm64': 0.25.11
|
||||||
|
'@esbuild/linux-ia32': 0.25.11
|
||||||
|
'@esbuild/linux-loong64': 0.25.11
|
||||||
|
'@esbuild/linux-mips64el': 0.25.11
|
||||||
|
'@esbuild/linux-ppc64': 0.25.11
|
||||||
|
'@esbuild/linux-riscv64': 0.25.11
|
||||||
|
'@esbuild/linux-s390x': 0.25.11
|
||||||
|
'@esbuild/linux-x64': 0.25.11
|
||||||
|
'@esbuild/netbsd-arm64': 0.25.11
|
||||||
|
'@esbuild/netbsd-x64': 0.25.11
|
||||||
|
'@esbuild/openbsd-arm64': 0.25.11
|
||||||
|
'@esbuild/openbsd-x64': 0.25.11
|
||||||
|
'@esbuild/openharmony-arm64': 0.25.11
|
||||||
|
'@esbuild/sunos-x64': 0.25.11
|
||||||
|
'@esbuild/win32-arm64': 0.25.11
|
||||||
|
'@esbuild/win32-ia32': 0.25.11
|
||||||
|
'@esbuild/win32-x64': 0.25.11
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
format: 0.2.2
|
format: 0.2.2
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -1578,6 +2047,12 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -2022,10 +2497,18 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]([email protected]([email protected]))([email protected]):
|
[email protected]([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
@@ -2054,10 +2537,18 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
semver: 7.7.3
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-fetch: 3.6.20
|
whatwg-fetch: 3.6.20
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
wrappy: 1.0.2
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 1.2.4
|
character-entities: 1.2.4
|
||||||
@@ -2095,6 +2586,21 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
expand-template: 2.0.3
|
||||||
|
github-from-package: 0.0.0
|
||||||
|
minimist: 1.2.8
|
||||||
|
mkdirp-classic: 0.5.3
|
||||||
|
napi-build-utils: 2.0.0
|
||||||
|
node-abi: 3.78.0
|
||||||
|
pump: 3.0.3
|
||||||
|
rc: 1.2.8
|
||||||
|
simple-get: 4.0.1
|
||||||
|
tar-fs: 2.1.4
|
||||||
|
tunnel-agent: 0.6.0
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -2107,6 +2613,18 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
deep-extend: 0.6.0
|
||||||
|
ini: 1.3.8
|
||||||
|
minimist: 1.2.8
|
||||||
|
strip-json-comments: 2.0.1
|
||||||
|
|
||||||
[email protected]([email protected]):
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
@@ -2142,6 +2660,12 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.4
|
||||||
|
string_decoder: 1.3.0
|
||||||
|
util-deprecate: 1.0.2
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
hastscript: 6.0.0
|
hastscript: 6.0.0
|
||||||
@@ -2203,10 +2727,13 @@ snapshots:
|
|||||||
mdast-util-to-markdown: 2.1.2
|
mdast-util-to-markdown: 2.1.2
|
||||||
unified: 11.0.5
|
unified: 11.0.5
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2238,17 +2765,31 @@ snapshots:
|
|||||||
'@img/sharp-win32-x64': 0.34.4
|
'@img/sharp-win32-x64': 0.34.4
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
decompress-response: 6.0.0
|
||||||
|
once: 1.4.0
|
||||||
|
simple-concat: 1.0.1
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities-html4: 2.1.0
|
character-entities-html4: 2.1.0
|
||||||
character-entities-legacy: 3.0.0
|
character-entities-legacy: 3.0.0
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
style-to-object: 1.0.11
|
style-to-object: 1.0.11
|
||||||
@@ -2268,12 +2809,38 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
chownr: 1.1.4
|
||||||
|
mkdirp-classic: 0.5.3
|
||||||
|
pump: 3.0.3
|
||||||
|
tar-stream: 2.2.0
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
bl: 4.1.0
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
fs-constants: 1.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.25.11
|
||||||
|
get-tsconfig: 4.13.0
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -2316,6 +2883,8 @@ snapshots:
|
|||||||
unist-util-is: 6.0.1
|
unist-util-is: 6.0.1
|
||||||
unist-util-visit-parents: 6.0.2
|
unist-util-visit-parents: 6.0.2
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
@@ -2335,6 +2904,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
- sharp
|
- sharp
|
||||||
|
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- better-sqlite3
|
||||||
|
- sqlite3
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import { ensureModelExists } from "@/lib/ollama";
|
||||||
|
|
||||||
|
let db: Database.Database | null = null;
|
||||||
|
|
||||||
|
function getDatabase(): Database.Database {
|
||||||
|
if (!db) {
|
||||||
|
const dbPath = path.join(process.cwd(), "database", "ollahub.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");
|
||||||
|
}
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runMigrations(db: Database.Database): void {
|
||||||
|
// Criar tabela de migrações
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS _migrations (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
applied_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Verificar migrações aplicadas
|
||||||
|
const applied = db.prepare("SELECT name FROM _migrations").all() as Array<{
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
const appliedNames = new Set(applied.map((m) => m.name));
|
||||||
|
|
||||||
|
// Aplicar migração inicial se não foi aplicada
|
||||||
|
if (!appliedNames.has("001_initial_schema")) {
|
||||||
|
console.log("📋 Aplicando migração: 001_initial_schema");
|
||||||
|
|
||||||
|
const schemaPath = path.join(process.cwd(), "database", "schema.sql");
|
||||||
|
const schema = fs.readFileSync(schemaPath, "utf-8");
|
||||||
|
|
||||||
|
db.exec(schema);
|
||||||
|
|
||||||
|
// Registrar migração
|
||||||
|
db.prepare("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)").run(
|
||||||
|
"001_initial_schema",
|
||||||
|
Date.now()
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ Migração 001_initial_schema aplicada");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar extensão vetorial se disponível
|
||||||
|
try {
|
||||||
|
const extPath = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"database",
|
||||||
|
"extensions",
|
||||||
|
"vector.so"
|
||||||
|
);
|
||||||
|
if (fs.existsSync(extPath)) {
|
||||||
|
db.loadExtension(extPath);
|
||||||
|
|
||||||
|
// Inicializar índice vetorial
|
||||||
|
db.exec(`
|
||||||
|
SELECT vector_init('embeddings', 'vector', 'type=FLOAT32,dimension=384');
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Quantizar para busca mais rápida
|
||||||
|
db.exec(`
|
||||||
|
SELECT vector_quantize('embeddings', 'vector');
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log("🔍 Extensão vetorial carregada e inicializada");
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
"⚠️ Extensão vetorial não encontrada - busca vetorial desabilitada"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("⚠️ Erro ao carregar extensão vetorial:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initializeDatabase(): Promise<void> {
|
||||||
|
console.log("🔧 Inicializando banco de dados OllaHub...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Conectar ao banco
|
||||||
|
const db = getDatabase();
|
||||||
|
|
||||||
|
// Executar migrações
|
||||||
|
runMigrations(db);
|
||||||
|
|
||||||
|
console.log("✅ Banco de dados inicializado com sucesso!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erro ao inicializar banco de dados:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initializeTitleModel(): Promise<void> {
|
||||||
|
console.log("🤖 Verificando modelo para geração de títulos...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureModelExists(
|
||||||
|
"qwen2.5:0.5b",
|
||||||
|
"Este modelo será usado para gerar títulos automáticos das conversas"
|
||||||
|
);
|
||||||
|
console.log("✅ Modelo de título pronto!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erro ao inicializar modelo de título:", error);
|
||||||
|
console.log("⚠️ Títulos de conversa podem não funcionar corretamente");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initializeEmbeddingModel(): Promise<void> {
|
||||||
|
console.log("🔍 Verificando modelo para embeddings...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ensureModelExists(
|
||||||
|
"nomic-embed-text",
|
||||||
|
"Este modelo será usado para busca vetorial das mensagens"
|
||||||
|
);
|
||||||
|
console.log("✅ Modelo de embedding pronto!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erro ao inicializar modelo de embedding:", error);
|
||||||
|
console.log("⚠️ Busca vetorial desabilitada - usando fallback");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executar se chamado diretamente
|
||||||
|
if (require.main === module) {
|
||||||
|
Promise.all([
|
||||||
|
initializeDatabase(),
|
||||||
|
initializeTitleModel(),
|
||||||
|
initializeEmbeddingModel(),
|
||||||
|
])
|
||||||
|
.then(() => {
|
||||||
|
console.log("🎉 Inicialização concluída");
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("💥 Falha na inicialização:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user