diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 6e325fc..0000000 --- a/.gitignore +++ /dev/null @@ -1,52 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts - -# database -database/*.db -database/extensions/*.so -database/extensions/*.dll -database/extensions/*.dylib - -# mcp servers installation directory -/mcp-servers/ - -/.cursor \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index e215bc4..0000000 --- a/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/api/conversations/[id]/messages/route.ts b/app/api/conversations/[id]/messages/route.ts deleted file mode 100644 index 33e4521..0000000 --- a/app/api/conversations/[id]/messages/route.ts +++ /dev/null @@ -1,77 +0,0 @@ -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 url = new URL(request.url); - const limitParam = url.searchParams.get("limit"); - const beforeParam = url.searchParams.get("before"); - - const limit = Math.min(Math.max(Number(limitParam) || 0, 1), 200) || 50; - const beforeTs = beforeParam ? Number(beforeParam) : undefined; - - const db = getDatabase(); - const messageRepo = new MessageRepository(db); - - const messages = messageRepo.findByConversationPaged(id, limit, beforeTs); - const hasMore = messages.length === limit; - const cursor = messages.length > 0 ? messages[0].timestamp : null; - - return NextResponse.json({ messages, hasMore, cursor }); - } 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 } - ); - } -} diff --git a/app/api/conversations/[id]/route.ts b/app/api/conversations/[id]/route.ts deleted file mode 100644 index 2c7181b..0000000 --- a/app/api/conversations/[id]/route.ts +++ /dev/null @@ -1,58 +0,0 @@ -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 } - ); - } -} diff --git a/app/api/conversations/route.ts b/app/api/conversations/route.ts deleted file mode 100644 index 95d1b24..0000000 --- a/app/api/conversations/route.ts +++ /dev/null @@ -1,54 +0,0 @@ -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 } - ); - } -} diff --git a/app/api/mcp/available-for-chat/route.ts b/app/api/mcp/available-for-chat/route.ts deleted file mode 100644 index fe65f60..0000000 --- a/app/api/mcp/available-for-chat/route.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * API endpoint para listar MCPs disponíveis para uso no chat - */ - -import { NextResponse } from "next/server"; -import { MCPRepository } from "@/database/repositories/mcp"; -import { MCPExecutor } from "@/lib/services/mcp-executor"; -import type { MCPTool } from "@/lib/types/mcp-chat"; - -export const dynamic = "force-dynamic"; - -export async function GET() { - try { - // Buscar todos os MCPs instalados com status "ready" - const allMcps = MCPRepository.getAllInstalled(); - - const availableMcps = allMcps - .filter((mcp: any) => !mcp.status || mcp.status === "ready") - .map((mcp: any) => { - const tools = MCPExecutor.getAvailableTools(mcp.id); - - // Converter tools para formato OpenAI function calling - const toolsFormatted = tools.map((tool: MCPTool) => ({ - type: "function" as const, - function: { - name: `${mcp.id}__${tool.name}`, // Prefixar com mcpId para evitar conflitos - description: tool.description, - parameters: tool.inputSchema, - }, - })); - - return { - mcpId: mcp.id, - name: mcp.name, - owner: mcp.owner, - repo: mcp.repo, - toolCount: tools.length, - tools: toolsFormatted, - rawTools: tools, // Tools originais para referência - }; - }); - - return NextResponse.json({ - success: true, - mcps: availableMcps, - count: availableMcps.length, - }); - } catch (error: any) { - console.error("Error listing available MCPs:", error); - return NextResponse.json( - { - success: false, - error: error.message || "Failed to list available MCPs", - }, - { status: 500 } - ); - } -} diff --git a/app/api/mcp/execute-tool/route.ts b/app/api/mcp/execute-tool/route.ts deleted file mode 100644 index 9370c2c..0000000 --- a/app/api/mcp/execute-tool/route.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * API endpoint para executar um tool de MCP - */ - -import { NextRequest, NextResponse } from "next/server"; -import { MCPExecutor } from "@/lib/services/mcp-executor"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { mcpId, toolName, parameters } = body; - - // Validar parâmetros - if (!mcpId || !toolName) { - return NextResponse.json( - { - success: false, - error: "Missing mcpId or toolName", - }, - { status: 400 } - ); - } - - console.log(`📞 API call to execute tool: ${mcpId}.${toolName}`); - - // Obter tools disponíveis - const availableTools = MCPExecutor.getAvailableTools(mcpId); - const tool = availableTools.find((t) => t.name === toolName); - - if (!tool) { - return NextResponse.json( - { - success: false, - error: `Tool ${toolName} not found in MCP ${mcpId}`, - }, - { status: 404 } - ); - } - - // Validar parâmetros contra schema - const validation = MCPExecutor.validateParameters(parameters || {}, tool); - if (!validation.valid) { - return NextResponse.json( - { - success: false, - error: `Invalid parameters: ${validation.error}`, - }, - { status: 400 } - ); - } - - // Executar tool - const result = await MCPExecutor.executeMCPTool( - mcpId, - toolName, - parameters || {} - ); - - if (!result.success) { - return NextResponse.json( - { - success: false, - error: result.error, - executionTime: result.executionTime, - }, - { status: 500 } - ); - } - - return NextResponse.json({ - success: true, - mcpId, - toolName, - result: result.result, - executionTime: result.executionTime, - }); - } catch (error: any) { - console.error("Error executing MCP tool:", error); - return NextResponse.json( - { - success: false, - error: error.message || "Failed to execute MCP tool", - }, - { status: 500 } - ); - } -} diff --git a/app/api/mcp/install-manual/route.ts b/app/api/mcp/install-manual/route.ts deleted file mode 100644 index 023560c..0000000 --- a/app/api/mcp/install-manual/route.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { NextResponse } from "next/server"; -import { MCPRepository } from "@/database/repositories/mcp"; -import { MCPInstallerService } from "@/lib/services/mcp-installer"; -import { MCPValidatorService } from "@/lib/services/mcp-validator"; -import { - MCPInstallationStatus, - type InstallationConfig, - type InstallationProgress, - type PackageRegistry, -} from "@/lib/types/mcp-installer"; - -/** - * POST /api/mcp/install-manual - * Instala um MCP a partir de uma configuração manual (formato Claude Desktop) - */ -export async function POST(request: Request) { - try { - const body = await request.json(); - const { config, mcpId, enableLogs } = body; - - // Validar estrutura básica - if (!config || typeof config !== "object") { - return NextResponse.json( - { success: false, error: "Invalid configuration format" }, - { status: 400 } - ); - } - - // Extrair primeiro servidor da configuração (se for formato mcpServers) - let serverConfig: any; - let serverId: string; - - if (config.mcpServers) { - // Formato: { "mcpServers": { "server-name": { ... } } } - const serverNames = Object.keys(config.mcpServers); - if (serverNames.length === 0) { - return NextResponse.json( - { success: false, error: "No MCP server found in configuration" }, - { status: 400 } - ); - } - - serverId = mcpId || serverNames[0]; - serverConfig = config.mcpServers[serverNames[0]]; - } else { - // Formato direto: { "command": "...", "args": [...] } - serverId = mcpId || "manual-mcp"; - serverConfig = config; - } - - // Validar campos obrigatórios - if (!serverConfig.command) { - return NextResponse.json( - { success: false, error: "Missing required field: command" }, - { status: 400 } - ); - } - - console.log(`🚀 Starting manual installation for: ${serverId}`); - console.log( - `Command: ${serverConfig.command} ${(serverConfig.args || []).join(" ")}` - ); - - // Detectar tipo de pacote baseado no comando - const { packageRegistry, packageName, repositoryUrl } = - detectPackageInfo(serverConfig); - - console.log(`Detected package type: ${packageRegistry} (${packageName})`); - - // Verificar se já está instalado - if (MCPRepository.isInstalled(serverId)) { - const existing = MCPRepository.getInstalledMCP(serverId); - - if (existing.status === "ready") { - return NextResponse.json( - { - success: false, - error: `MCP ${serverId} is already installed`, - }, - { status: 400 } - ); - } - - console.log( - `Re-installing MCP ${serverId} (current status: ${existing.status})` - ); - } - - // Preparar configuração de instalação - const installConfig: InstallationConfig = { - mcpId: serverId, - packageName, - packageRegistry, - repositoryUrl, - config: serverConfig, - enableLogs: enableLogs === true, - }; - - // Salvar no banco como PENDING - if (!MCPRepository.isInstalled(serverId)) { - MCPRepository.saveInstallation(serverId, serverConfig, []); - } - MCPRepository.updateInstallationStatus( - serverId, - "pending", - "Starting manual installation..." - ); - - // Iniciar instalação em background - installInBackground(installConfig); - - return NextResponse.json({ - success: true, - message: "Manual installation started", - mcpId: serverId, - status: "pending", - detectedType: packageRegistry, - }); - } catch (error: any) { - console.error("Error starting manual installation:", error); - return NextResponse.json( - { - success: false, - error: error.message || "Failed to start manual installation", - }, - { status: 500 } - ); - } -} - -/** - * Detecta informações do pacote baseado no comando - */ -function detectPackageInfo(serverConfig: any): { - packageRegistry: PackageRegistry; - packageName: string; - repositoryUrl?: string; -} { - const command = serverConfig.command.toLowerCase(); - const args = serverConfig.args || []; - const firstArg = args[0] || ""; - - // NPM/NPX - if (command === "npx" || command === "npm") { - // npx @mcpcentral/mcp-time - // npx -y @modelcontextprotocol/server-filesystem - // npx -y github:user/repo - const packageArg = args.find( - (arg: string) => - arg.startsWith("@") || arg.startsWith("github:") || !arg.startsWith("-") - ); - let packageName = packageArg || firstArg || "unknown-npm-package"; - packageName = packageName.replace(/^-y\s*/, "").trim(); - - // Converter github:user/repo para git+https://github.com/user/repo.git - // Isso evita problemas com SSH e funciona melhor com npm install - if (packageName.startsWith("github:")) { - const repoPath = packageName.replace("github:", ""); - packageName = `git+https://github.com/${repoPath}.git`; - console.log(`Converted github: format to: ${packageName}`); - } - - return { - packageRegistry: "npm", - packageName, - }; - } - - // Node - if (command === "node" || command.endsWith("node")) { - // node dist/index.js -> assume que é local ou já instalado - return { - packageRegistry: "npm", - packageName: "local-node-package", - }; - } - - // Python - if ( - command === "python" || - command === "python3" || - command.includes("python") - ) { - // python -m mcp_server_git - // python /path/to/server.py - const moduleIndex = args.indexOf("-m"); - if (moduleIndex !== -1 && args[moduleIndex + 1]) { - return { - packageRegistry: "pypi", - packageName: args[moduleIndex + 1], - }; - } - - return { - packageRegistry: "pypi", - packageName: "local-python-package", - }; - } - - // UV (Python package manager) - if (command === "uv" || command === "uvx") { - // uvx mcp-server-git - const packageName = args[0] || "unknown-uv-package"; - return { - packageRegistry: "pypi", - packageName, - }; - } - - // Cargo/Rust - if (command === "cargo") { - // cargo run --bin mcp-server - const binIndex = args.indexOf("--bin"); - if (binIndex !== -1 && args[binIndex + 1]) { - return { - packageRegistry: "cargo", - packageName: args[binIndex + 1], - }; - } - - return { - packageRegistry: "cargo", - packageName: "local-rust-package", - }; - } - - // Docker - if (command === "docker") { - return { - packageRegistry: "other", - packageName: "docker-container", - }; - } - - // Caminho absoluto ou relativo (executável local) - if (command.startsWith("/") || command.startsWith("./")) { - return { - packageRegistry: "other", - packageName: "local-executable", - }; - } - - // Padrão: tratar como "other" - return { - packageRegistry: "other", - packageName: command, - }; -} - -/** - * Executa instalação em background (não bloqueia resposta HTTP) - */ -async function installInBackground(config: InstallationConfig): Promise { - const { mcpId, enableLogs } = config; - - try { - console.log(`📦 Installing ${mcpId} in background (manual mode)...`); - - // Callback de progresso - const onProgress = (progress: InstallationProgress) => { - MCPRepository.updateInstallationStatus( - mcpId, - progress.status, - progress.message - ); - - if (enableLogs && progress.logs && progress.logs.length > 0) { - MCPRepository.appendLogs(mcpId, progress.logs); - } - - console.log( - `[${mcpId}] ${progress.status}: ${progress.message} (${progress.percentage}%)` - ); - }; - - // Instalar - onProgress({ - status: MCPInstallationStatus.DOWNLOADING, - message: "Preparing manual installation...", - percentage: 5, - }); - - const environment = await MCPInstallerService.install(config, onProgress); - - // Salvar ambiente - MCPRepository.saveEnvironment(mcpId, environment); - - onProgress({ - status: MCPInstallationStatus.TESTING, - message: "Validating MCP server...", - percentage: 85, - }); - - // Validar servidor MCP - const validation = await MCPValidatorService.validateMCPServer( - environment, - config.config - ); - - // Salvar resultado de validação - MCPRepository.saveValidationResult(mcpId, validation); - - if (validation.success) { - // Salvar ferramentas descobertas - MCPRepository.saveTools(mcpId, validation.tools); - - MCPRepository.updateInstallationStatus( - mcpId, - "ready", - `Installed successfully with ${validation.tools.length} tools` - ); - - console.log( - `✅ MCP ${mcpId} (manual) installed and validated successfully` - ); - } else { - // Validação falhou, mas instalação pode estar OK - console.warn( - `⚠️ MCP ${mcpId} (manual) installed but validation failed: ${validation.error}` - ); - - // Tentar validação simples como fallback - const simpleValidation = await MCPValidatorService.simpleValidation( - environment, - config.config - ); - - if (simpleValidation.success) { - MCPRepository.updateInstallationStatus( - mcpId, - "ready", - "Installed (validation partial)" - ); - console.log( - `✅ MCP ${mcpId} (manual) installed (simple validation passed)` - ); - } else { - throw new Error( - validation.error || simpleValidation.error || "Validation failed" - ); - } - } - } catch (error: any) { - console.error(`❌ Manual installation failed for ${mcpId}:`, error); - - MCPRepository.updateInstallationStatus( - mcpId, - "failed", - error.message || "Manual installation failed" - ); - - if (enableLogs) { - MCPRepository.appendLogs(mcpId, [ - `ERROR: ${error.message}`, - error.stack || "", - ]); - } - } -} diff --git a/app/api/mcp/install-status/route.ts b/app/api/mcp/install-status/route.ts deleted file mode 100644 index 02297ff..0000000 --- a/app/api/mcp/install-status/route.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { NextResponse } from "next/server"; -import { MCPRepository } from "@/database/repositories/mcp"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url); - const mcpId = searchParams.get("mcpId"); - - if (!mcpId) { - return NextResponse.json( - { success: false, error: "MCP ID is required" }, - { status: 400 } - ); - } - - const installation = MCPRepository.getInstalledMCP(mcpId); - - if (!installation) { - return NextResponse.json( - { success: false, error: "MCP not found" }, - { status: 404 } - ); - } - - // Parsear logs se existirem - let logs: string[] = []; - if (installation.install_logs) { - try { - logs = JSON.parse(installation.install_logs); - } catch { - logs = [installation.install_logs]; - } - } - - return NextResponse.json({ - success: true, - mcpId: installation.id, - status: installation.status || "unknown", - message: installation.status_message || "", - logs, - environment: installation.environment_path, - executableCommand: installation.executable_command, - installedAt: installation.installed_at, - updatedAt: installation.updated_at, - }); - } catch (error: any) { - console.error("Error getting installation status:", error); - return NextResponse.json( - { - success: false, - error: error.message || "Failed to get installation status", - }, - { status: 500 } - ); - } -} diff --git a/app/api/mcp/install/route.ts b/app/api/mcp/install/route.ts deleted file mode 100644 index a4dfd04..0000000 --- a/app/api/mcp/install/route.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { NextResponse } from "next/server"; -import { MCPRepository } from "@/database/repositories/mcp"; -import { MCPInstallerService } from "@/lib/services/mcp-installer"; -import { MCPValidatorService } from "@/lib/services/mcp-validator"; -import { DeepNLPService } from "@/lib/services/deepnlp"; -import type { - InstallationConfig, - InstallationProgress, -} from "@/lib/types/mcp-installer"; - -export async function POST(request: Request) { - try { - const { mcpId, customConfig, enableLogs } = await request.json(); - - if (!mcpId) { - return NextResponse.json( - { success: false, error: "MCP ID is required" }, - { status: 400 } - ); - } - - console.log(`🚀 Starting installation for MCP: ${mcpId}`); - - // Verificar se já está instalado - if (MCPRepository.isInstalled(mcpId)) { - const existing = MCPRepository.getInstalledMCP(mcpId); - - // Se já está instalado e pronto, retornar erro - if (existing.status === "ready") { - return NextResponse.json( - { - success: false, - error: `MCP ${mcpId} is already installed`, - }, - { status: 400 } - ); - } - - // Se está em processo de instalação ou falhou, permitir reinstalação - console.log( - `Re-installing MCP ${mcpId} (current status: ${existing.status})` - ); - } - - // Obter configuração - let serverConfig: any; - let packageName: string; - let packageRegistry: string | null = null; - let repositoryUrl: string | undefined; - - if (customConfig) { - // Usar configuração personalizada - console.log(`Using custom configuration for ${mcpId}`); - serverConfig = customConfig; - packageName = customConfig.packageName || mcpId; - packageRegistry = customConfig.packageRegistry || null; - repositoryUrl = customConfig.repositoryUrl; - } else { - // Buscar configuração da API - console.log(`Fetching configuration for ${mcpId}`); - - try { - // Tentar DeepNLP primeiro - const deepnlpConfig = await DeepNLPService.getServerConfig(mcpId); - if (deepnlpConfig.items && deepnlpConfig.items.length > 0) { - serverConfig = deepnlpConfig.items[0]; - } - } catch (error) { - console.log(`DeepNLP config not found, trying cache...`); - } - - if (!serverConfig) { - throw new Error(`No configuration found for MCP ${mcpId}`); - } - - // Extrair informações do pacote - packageName = serverConfig.packageName || serverConfig.name || mcpId; - } - - // Preparar configuração de instalação - const installConfig: InstallationConfig = { - mcpId, - packageName, - packageRegistry: packageRegistry as any, - repositoryUrl, - config: serverConfig, - enableLogs: enableLogs === true, - }; - - // Salvar como PENDING no banco - if (!MCPRepository.isInstalled(mcpId)) { - MCPRepository.saveInstallation(mcpId, serverConfig, []); - } - MCPRepository.updateInstallationStatus( - mcpId, - "pending", - "Starting installation..." - ); - - // Iniciar instalação em background - installInBackground(installConfig); - - return NextResponse.json({ - success: true, - message: "Installation started", - mcpId, - status: "pending", - }); - } catch (error: any) { - console.error("Error starting installation:", error); - return NextResponse.json( - { - success: false, - error: error.message || "Failed to start installation", - }, - { status: 500 } - ); - } -} - -/** - * Executa instalação em background (não bloqueia resposta HTTP) - */ -async function installInBackground(config: InstallationConfig): Promise { - const { mcpId, enableLogs } = config; - - try { - console.log(`📦 Installing ${mcpId} in background...`); - - // Callback de progresso - const onProgress = (progress: InstallationProgress) => { - MCPRepository.updateInstallationStatus( - mcpId, - progress.status, - progress.message - ); - - if (enableLogs && progress.logs && progress.logs.length > 0) { - MCPRepository.appendLogs(mcpId, progress.logs); - } - - console.log( - `[${mcpId}] ${progress.status}: ${progress.message} (${progress.percentage}%)` - ); - }; - - // Instalar - onProgress({ - status: "downloading", - message: "Preparing installation...", - percentage: 5, - }); - - const environment = await MCPInstallerService.install(config, onProgress); - - // Salvar ambiente - MCPRepository.saveEnvironment(mcpId, environment); - - onProgress({ - status: "testing", - message: "Validating MCP server...", - percentage: 85, - }); - - // Validar servidor MCP - const validation = await MCPValidatorService.validateMCPServer( - environment, - config.config - ); - - // Salvar resultado de validação - MCPRepository.saveValidationResult(mcpId, validation); - - if (validation.success) { - // Salvar ferramentas descobertas - MCPRepository.saveTools(mcpId, validation.tools); - - MCPRepository.updateInstallationStatus( - mcpId, - "ready", - `Installed successfully with ${validation.tools.length} tools` - ); - - console.log(`✅ MCP ${mcpId} installed and validated successfully`); - } else { - // Validação falhou, mas instalação pode estar OK - console.warn( - `⚠️ MCP ${mcpId} installed but validation failed: ${validation.error}` - ); - - // Tentar validação simples como fallback - const simpleValidation = await MCPValidatorService.simpleValidation( - environment, - config.config - ); - - if (simpleValidation.success) { - MCPRepository.updateInstallationStatus( - mcpId, - "ready", - "Installed (validation partial)" - ); - console.log(`✅ MCP ${mcpId} installed (simple validation passed)`); - } else { - throw new Error( - validation.error || simpleValidation.error || "Validation failed" - ); - } - } - } catch (error: any) { - console.error(`❌ Installation failed for ${mcpId}:`, error); - - MCPRepository.updateInstallationStatus( - mcpId, - "failed", - error.message || "Installation failed" - ); - - if (enableLogs) { - MCPRepository.appendLogs(mcpId, [ - `ERROR: ${error.message}`, - error.stack || "", - ]); - } - } -} diff --git a/app/api/mcp/installed/route.ts b/app/api/mcp/installed/route.ts deleted file mode 100644 index 69cbdc2..0000000 --- a/app/api/mcp/installed/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextResponse } from "next/server"; -import { MCPRepository } from "@/database/repositories/mcp"; -// Marketplace cache removido - -export const dynamic = "force-dynamic"; - -export async function GET() { - try { - console.log("Fetching installed MCPs..."); - - // Buscar todos os MCPs instalados com detalhes - const installedMcps = MCPRepository.listInstalledWithDetails(); - - // Retornar dados de instalados sem enriquecimento de marketplace - const enrichedMcps = installedMcps.map((mcp) => ({ - ...mcp, - author: mcp.owner !== "unknown" ? mcp.owner : "Unknown", - description: `MCP instalado: ${mcp.name}`, - repository: "", - homepage: "", - rating: 0, - totalRatings: 0, - category: "other", - tags: [], - status: mcp.status || "unknown", - statusMessage: mcp.status_message || "", - })); - - console.log(`Found ${enrichedMcps.length} installed MCPs`); - - return NextResponse.json({ - success: true, - mcps: enrichedMcps, - total: enrichedMcps.length, - }); - } catch (error: any) { - console.error("Error fetching installed MCPs:", error); - return NextResponse.json( - { - success: false, - error: error.message || "Failed to fetch installed MCPs", - }, - { status: 500 } - ); - } -} diff --git a/app/api/mcp/server-config/route.ts b/app/api/mcp/server-config/route.ts deleted file mode 100644 index 4416836..0000000 --- a/app/api/mcp/server-config/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NextResponse } from "next/server"; -import { DeepNLPService } from "@/lib/services/deepnlp"; - -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url); - const mcpId = searchParams.get("mcpId"); - - if (!mcpId) { - return NextResponse.json( - { success: false, error: "MCP ID is required" }, - { status: 400 } - ); - } - - console.log(`Fetching server config for: ${mcpId}`); - - // Marketplace removido: pular cache local - - // Tentar DeepNLP - try { - const serverConfig = await DeepNLPService.getServerConfig(mcpId); - - if (serverConfig.items && serverConfig.items.length > 0) { - console.log(`Found config from DeepNLP for ${mcpId}`); - return NextResponse.json({ - success: true, - config: serverConfig.items[0], - source: "deepnlp", - }); - } - } catch (error) { - console.log(`DeepNLP lookup failed for ${mcpId}, continuing...`); - } - - // Se não encontrou configuração em nenhum lugar, retornar config vazio - console.log(`No config found for ${mcpId}, returning empty config`); - return NextResponse.json({ - success: true, - config: null, - source: "none", - }); - } catch (error: any) { - console.error("Error fetching server config:", error); - return NextResponse.json( - { - success: false, - error: error.message || "Failed to fetch server config", - }, - { status: 500 } - ); - } -} diff --git a/app/api/mcp/test-server/route.ts b/app/api/mcp/test-server/route.ts deleted file mode 100644 index b50187e..0000000 --- a/app/api/mcp/test-server/route.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { NextResponse } from "next/server"; -import { MCPRepository } from "@/database/repositories/mcp"; -import { MCPValidatorService } from "@/lib/services/mcp-validator"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - try { - const { mcpId } = await request.json(); - - if (!mcpId) { - return NextResponse.json( - { success: false, error: "MCP ID é obrigatório" }, - { status: 400 } - ); - } - - console.log(`🧪 Testing MCP server: ${mcpId}`); - - // Buscar dados do MCP no banco - const mcpData = MCPRepository.getInstalledMCP(mcpId); - if (!mcpData) { - return NextResponse.json( - { success: false, error: "MCP não encontrado" }, - { status: 404 } - ); - } - - // Obter ambiente de instalação - const environment = MCPRepository.getEnvironment(mcpId); - - if (!environment) { - return NextResponse.json( - { - success: false, - error: - "MCP não está instalado corretamente. Ambiente não encontrado.", - }, - { status: 400 } - ); - } - - console.log( - `Testing environment: ${environment.executable} ${environment.args?.join( - " " - )}` - ); - - // Validar servidor usando protocolo JSON-RPC MCP - const validationResult = await MCPValidatorService.validateMCPServer( - environment, - mcpData.config - ); - - // Salvar resultado da validação - MCPRepository.saveValidationResult(mcpId, validationResult); - - if (validationResult.success) { - // Atualizar ferramentas se validação foi bem-sucedida - if (validationResult.tools && validationResult.tools.length > 0) { - MCPRepository.saveTools(mcpId, validationResult.tools); - } - - console.log(`✅ Test successful for ${mcpId}:`, { - protocol: validationResult.protocol, - tools: validationResult.tools.length, - }); - - return NextResponse.json({ - success: true, - result: { - success: true, - protocol: validationResult.protocol, - toolsCount: validationResult.tools.length, - tools: validationResult.tools, - capabilities: validationResult.capabilities, - }, - }); - } - - // Se validação falhou, tentar validação simples - console.warn(`⚠️ JSON-RPC validation failed, trying simple validation...`); - - const simpleResult = await MCPValidatorService.simpleValidation( - environment, - mcpData.config, - 5000 - ); - - if (simpleResult.success) { - console.log(`✅ Simple validation passed for ${mcpId}`); - - return NextResponse.json({ - success: true, - result: { - success: true, - protocol: "unknown", - message: "Server starts but JSON-RPC validation failed", - toolsCount: 0, - tools: [], - }, - }); - } - - // Ambas validações falharam - console.error(`❌ All validations failed for ${mcpId}`); - - return NextResponse.json({ - success: false, - error: - validationResult.error || simpleResult.error || "Validation failed", - result: { - success: false, - protocol: validationResult.protocol, - error: validationResult.error, - }, - }); - } catch (error: any) { - console.error("Error testing MCP server:", error); - return NextResponse.json( - { - success: false, - error: error.message || "Internal server error", - }, - { status: 500 } - ); - } -} diff --git a/app/api/mcp/uninstall/route.ts b/app/api/mcp/uninstall/route.ts deleted file mode 100644 index 73880a1..0000000 --- a/app/api/mcp/uninstall/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { NextResponse } from "next/server"; -import { MCPRepository } from "@/database/repositories/mcp"; -import { MCPEnvironmentService } from "@/lib/services/mcp-environment"; - -export async function DELETE(request: Request) { - try { - const { mcpId } = await request.json(); - - if (!mcpId) { - return NextResponse.json( - { success: false, error: "MCP ID is required" }, - { status: 400 } - ); - } - - console.log(`🗑️ Uninstalling MCP: ${mcpId}`); - - // Verificar se está instalado - if (!MCPRepository.isInstalled(mcpId)) { - return NextResponse.json( - { - success: false, - error: `MCP ${mcpId} is not installed`, - }, - { status: 400 } - ); - } - - // Obter ambiente antes de remover do banco - const environment = MCPRepository.getEnvironment(mcpId); - - // Remover do banco local - MCPRepository.removeInstallation(mcpId); - - console.log(`✓ MCP ${mcpId} removed from database`); - - // Limpar arquivos de instalação se ambiente existe - if (environment) { - try { - await MCPEnvironmentService.cleanupEnvironment(environment); - console.log(`✓ MCP ${mcpId} environment cleaned up`); - } catch (error: any) { - console.warn( - `⚠️ Could not clean up environment for ${mcpId}: ${error.message}` - ); - // Não falhar a desinstalação se limpeza falhar - } - } - - return NextResponse.json({ - success: true, - message: `MCP ${mcpId} uninstalled successfully`, - mcpId, - }); - } catch (error: any) { - console.error("Error uninstalling MCP:", error); - return NextResponse.json( - { success: false, error: error.message || "Failed to uninstall MCP" }, - { status: 500 } - ); - } -} diff --git a/app/api/mcp/update-config/route.ts b/app/api/mcp/update-config/route.ts deleted file mode 100644 index 9b65b62..0000000 --- a/app/api/mcp/update-config/route.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { NextResponse } from "next/server"; -import { MCPRepository } from "@/database/repositories/mcp"; - -export async function PUT(request: Request) { - try { - const { mcpId, newConfig } = await request.json(); - - if (!mcpId) { - return NextResponse.json( - { success: false, error: "MCP ID is required" }, - { status: 400 } - ); - } - - if (!newConfig) { - return NextResponse.json( - { success: false, error: "New configuration is required" }, - { status: 400 } - ); - } - - console.log(`Updating config for MCP: ${mcpId}`); - - // Verificar se o MCP está instalado - if (!MCPRepository.isInstalled(mcpId)) { - return NextResponse.json( - { - success: false, - error: `MCP ${mcpId} is not installed`, - }, - { status: 404 } - ); - } - - // Atualizar configuração - MCPRepository.updateConfig(mcpId, newConfig); - - console.log(`MCP ${mcpId} configuration updated successfully`); - - return NextResponse.json({ - success: true, - message: `MCP ${mcpId} configuration updated successfully`, - mcpId, - }); - } catch (error: any) { - console.error("Error updating MCP configuration:", error); - return NextResponse.json( - { success: false, error: error.message || "Failed to update MCP configuration" }, - { status: 500 } - ); - } -} diff --git a/app/api/ollama/catalog/route.ts b/app/api/ollama/catalog/route.ts deleted file mode 100644 index e69de29..0000000 diff --git a/app/api/ollama/chat/route.ts b/app/api/ollama/chat/route.ts deleted file mode 100644 index 83b0238..0000000 --- a/app/api/ollama/chat/route.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { NextRequest } from "next/server"; -import { chatWithStream, ensureOllamaAvailable } from "@/lib/ollama"; -import { convertToOllamaMessages } from "@/lib/chat"; -import { detectModelCapabilities } from "@/lib/services/model-capabilities"; -import { MCPExecutor } from "@/lib/services/mcp-executor"; -import { - generateToolsPrompt, - extractToolCallFromResponse, - formatToolResultForContext, -} from "@/lib/prompts/mcp-tools-prompt"; - -export const dynamic = "force-dynamic"; - -export async function POST(req: NextRequest) { - try { - const { model, messages, options, system, activeMcps } = await req.json(); - - if (!model || !messages?.length) { - return new Response("Missing model or messages", { status: 400 }); - } - - // Detectar capabilities do modelo - const capabilities = detectModelCapabilities(model); - - // Preparar tools se MCPs ativos foram fornecidos - let tools: Array = []; - let toolsPrompt = ""; - const mcpToolsMap = new Map(); - - if (activeMcps && activeMcps.length > 0) { - console.log(`🔧 Active MCPs for chat: ${activeMcps.join(", ")}`); - - // Buscar tools de todos os MCPs ativos - for (const mcpId of activeMcps) { - const mcpTools = MCPExecutor.getAvailableTools(mcpId); - - for (const tool of mcpTools) { - const toolKey = `${mcpId}__${tool.name}`; - - if (capabilities.supportsNativeTools) { - // Formato OpenAI para modelos com suporte nativo - tools.push({ - type: "function", - function: { - name: toolKey, - description: tool.description, - parameters: tool.inputSchema, - }, - }); - } - - // Mapa para lookup rápido - mcpToolsMap.set(toolKey, { mcpId, toolName: tool.name }); - } - } - - // Se modelo não suporta native tools, usar prompt engineering - if (!capabilities.supportsNativeTools) { - const allTools = Array.from(mcpToolsMap.entries()).map( - ([key, { mcpId, toolName }]) => { - const mcpTools = MCPExecutor.getAvailableTools(mcpId); - return mcpTools.find((t) => t.name === toolName)!; - } - ); - toolsPrompt = generateToolsPrompt(allTools); - } - } - - const stream = new ReadableStream({ - async start(controller) { - let isClosed = false; - let accumulatedResponse = ""; - let toolExecuted = false; - let lastExtractedToolCall: { name: string; parameters: any } | null = - null; - - 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 { - // Garantir disponibilidade com timeout curto (não bloquear) - ensureOllamaAvailable({ timeoutMs: 2000 }).catch(() => {}); - const ollamaMessages = convertToOllamaMessages(messages); - if (process.env.NODE_ENV !== "production") { - console.log("🗣️ [CHAT] Messages ->", ollamaMessages); - } - - // Merge system prompt com tools prompt - const finalSystemPrompt = toolsPrompt - ? `${system || ""}\n\n${toolsPrompt}` - : system; - if (toolsPrompt && process.env.NODE_ENV !== "production") console.log("🧩 [CHAT] Tools prompt injected"); - - // Preparar parâmetros para chat - const chatParams = capabilities.supportsNativeTools ? { tools } : {}; - if (process.env.NODE_ENV !== "production") { - if (capabilities.supportsNativeTools) { - console.log( - "🧰 [CHAT] Native tools enabled:", - tools?.map((t: any) => t.function?.name) - ); - } else { - console.log("🧰 [CHAT] Prompt-engineering tools mode"); - } - } - - const response = await chatWithStream( - model, - ollamaMessages, - options, - finalSystemPrompt, - capabilities.supportsNativeTools ? tools : undefined - ); - if (process.env.NODE_ENV !== "production") console.log("🌊 [CHAT] Streaming started for model:", model); - - const normalizeParameters = (params: any) => { - try { - if (typeof params === "string") { - params = JSON.parse(params); - } - } catch { - // ignora erro de parse e mantém original - } - if (params && typeof params === "object") { - const cleaned: Record = {}; - for (const [k, v] of Object.entries(params)) { - if (v && typeof v === "object" && Object.keys(v).length === 0) { - // remove objetos vazios que costumam vir de UIs (ex.: count: {}) - continue; - } - cleaned[k] = v; - } - return cleaned; - } - return params ?? {}; - }; - - for await (const chunk of response) { - if (process.env.NODE_ENV !== "production") console.log("📦 [CHAT stream chunk]", JSON.stringify(chunk)); - // Verificar se há tool calls (modelos com suporte nativo) - if ( - (chunk.message?.tool_calls && - chunk.message.tool_calls.length > 0) || - (Array.isArray((chunk as any).tool_calls) && - (chunk as any).tool_calls.length > 0) - ) { - if (process.env.NODE_ENV !== "production") console.log("🔧 Model requested tool calls (native)"); - - const nativeCalls = - chunk.message?.tool_calls || (chunk as any).tool_calls || []; - for (const toolCall of nativeCalls) { - const toolKey = toolCall.function.name; - const toolInfo = mcpToolsMap.get(toolKey); - if (process.env.NODE_ENV !== "production") { - console.log("🧭 [CHAT] Native tool call:", toolKey, "->", toolInfo); - } - - if (toolInfo) { - // Executar tool - const result = await MCPExecutor.executeMCPTool( - toolInfo.mcpId, - toolInfo.toolName, - normalizeParameters( - toolCall.function.arguments || - toolCall.function.parameters || - {} - ) - ); - - if (result.success) { - toolExecuted = true; - if (process.env.NODE_ENV !== "production") { - console.log("✅ [CHAT] Tool executed successfully", { - mcpId: toolInfo.mcpId, - toolName: toolInfo.toolName, - executionTime: result.executionTime, - }); - } - // Enviar indicador de execução de tool - safeEnqueue( - JSON.stringify({ - toolExecution: { - mcpId: toolInfo.mcpId, - toolName: toolInfo.toolName, - status: "success", - }, - }) + "\n" - ); - - // Injetar resultado no contexto e continuar - const resultMessage = formatToolResultForContext( - toolInfo.toolName, - result.result - ); - if (process.env.NODE_ENV !== "production") { - console.log( - "🧩 [CHAT] Injecting tool result into context (length)", - String(resultMessage).length - ); - } - - // Fazer nova chamada ao modelo com o resultado - const updatedMessages = [ - ...ollamaMessages, - { - role: "assistant", - content: chunk.message.content || "", - }, - { role: "system", content: resultMessage }, - ]; - - const followUpResponse = await chatWithStream( - model, - updatedMessages, - options, - finalSystemPrompt, - capabilities.supportsNativeTools ? tools : undefined - ); - - for await (const followUpChunk of followUpResponse) { - const token = followUpChunk.message?.content || ""; - if (token) { - safeEnqueue(JSON.stringify({ token }) + "\n"); - } - } - } else { - console.error("Tool execution failed:", result.error); - } - } - } - } else { - // Streaming normal de tokens - const token = chunk.message?.content || ""; - if (token) { - accumulatedResponse += token; - safeEnqueue(JSON.stringify({ token }) + "\n"); - } - } - } - - // Verificar se resposta contém tool call via prompt engineering (fallback também para nativo) - if (accumulatedResponse) { - console.log( - "🧾 [CHAT] Accumulated response length:", - accumulatedResponse.length - ); - console.log( - "🧾 [CHAT] Accumulated preview:", - accumulatedResponse.slice(0, 400) - ); - const extractedToolCall = - extractToolCallFromResponse(accumulatedResponse); - - if (extractedToolCall) { - if (process.env.NODE_ENV !== "production") console.log("🔧 Model requested tool call (prompt engineering)"); - lastExtractedToolCall = extractedToolCall; - if (process.env.NODE_ENV !== "production") console.log("🧭 [CHAT] Extracted tool call:", extractedToolCall); - - // Encontrar MCP e tool - let foundToolInfo: { mcpId: string; toolName: string } | null = - null; - - for (const [key, info] of mcpToolsMap.entries()) { - if ( - extractedToolCall.name === info.toolName || - extractedToolCall.name === key - ) { - foundToolInfo = info; - break; - } - } - - if (foundToolInfo) { - // Executar tool - const result = await MCPExecutor.executeMCPTool( - foundToolInfo.mcpId, - foundToolInfo.toolName, - normalizeParameters(extractedToolCall.parameters) - ); - - if (result.success) { - toolExecuted = true; - // Limpar resposta anterior (que continha o JSON) - safeEnqueue(JSON.stringify({ clearPrevious: true }) + "\n"); - - // Enviar indicador de execução - safeEnqueue( - JSON.stringify({ - toolExecution: { - mcpId: foundToolInfo.mcpId, - toolName: foundToolInfo.toolName, - status: "success", - }, - }) + "\n" - ); - - // Fazer nova chamada com resultado - const resultMessage = formatToolResultForContext( - foundToolInfo.toolName, - result.result - ); - - const updatedMessages = [ - ...ollamaMessages, - { - role: "assistant", - content: JSON.stringify({ - tool_call: extractedToolCall, - }), - }, - { role: "system", content: resultMessage }, - ]; - - const followUpResponse = await chatWithStream( - model, - updatedMessages, - options, - finalSystemPrompt - ); - - for await (const followUpChunk of followUpResponse) { - const token = followUpChunk.message?.content || ""; - if (token) { - safeEnqueue(JSON.stringify({ token }) + "\n"); - } - } - } - } - } else { - if (process.env.NODE_ENV !== "production") - console.log("🕵️ [CHAT] No tool call extracted from accumulated response"); - } - } - - safeEnqueue(JSON.stringify({ done: true }) + "\n"); - safeClose(); - } catch (error) { - console.error("Chat streaming error:", error); - safeEnqueue( - JSON.stringify({ error: "Failed to generate response" }) + "\n" - ); - safeClose(); - } - }, - }); - - return new Response(stream, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, - }); - } catch (error) { - console.error("Chat API error:", error); - return new Response("Internal server error", { status: 500 }); - } -} diff --git a/app/api/ollama/delete/route.ts b/app/api/ollama/delete/route.ts deleted file mode 100644 index dd4a3f7..0000000 --- a/app/api/ollama/delete/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { deleteModel } from "@/lib/ollama"; - -export async function DELETE(request: NextRequest) { - try { - const { modelName } = await request.json(); - - if (!modelName) { - return NextResponse.json( - { error: "Model name is required" }, - { status: 400 } - ); - } - - await deleteModel(modelName); - - return NextResponse.json({ - success: true, - message: `Model ${modelName} deleted successfully`, - }); - } catch (error) { - console.error("Error deleting model:", error); - return NextResponse.json( - { error: "Failed to delete model" }, - { status: 500 } - ); - } -} diff --git a/app/api/ollama/health/route.ts b/app/api/ollama/health/route.ts deleted file mode 100644 index e26c6ff..0000000 --- a/app/api/ollama/health/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NextResponse } from "next/server"; -import { listModelsViaSdk, listModelsViaHttp } from "@/lib/ollama"; - -export const dynamic = "force-dynamic"; - -export async function GET() { - try { - try { - await listModelsViaSdk(); - return NextResponse.json({ ok: true }); - } catch { - await listModelsViaHttp(); - return NextResponse.json({ ok: true }); - } - } catch { - return NextResponse.json({ ok: false }); - } -} diff --git a/app/api/ollama/models/route.ts b/app/api/ollama/models/route.ts deleted file mode 100644 index 416be55..0000000 --- a/app/api/ollama/models/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { NextResponse } from "next/server"; -import { listModelsViaSdk, listModelsViaHttp, ensureOllamaAvailable } from "@/lib/ollama"; -import { - estimateCpuVram, - inferDevice, - inferQuantization, - mockModels, - type ModelInfo, -} from "@/lib/models"; - -export const dynamic = "force-dynamic"; - -// Cache leve em memória para reduzir chamadas repetidas em curto intervalo -let __modelsCache: { ts: number; payload: any } | null = null; -const CACHE_TTL_MS = 8000; // 8s - -function normalizeModel(model: any): ModelInfo { - const name: string = model?.model || model?.name || "unknown"; - const sizeBytes: number = model?.size || 0; - const sizeGB = Number(Math.max(0.1, sizeBytes / 1024 ** 3).toFixed(2)); - const quantization = inferQuantization(name); - const device = inferDevice(model?.details || name); - const { cpuPct, vramGB } = estimateCpuVram({ sizeGB, quantization, device }); - return { - id: name, - name, - sizeGB, - quantization, - device, - estCpuUsagePct: cpuPct, - estVramGB: vramGB, - }; -} - -export async function GET() { - try { - // Tentar retorno via cache recente - if (__modelsCache && Date.now() - __modelsCache.ts < CACHE_TTL_MS) { - return NextResponse.json(__modelsCache.payload); - } - - let models: any[] = []; - // Tentar garantir disponibilidade rapidamente (não bloquear longamente a UI) - await ensureOllamaAvailable({ timeoutMs: 3000 }).catch(() => {}); - try { - models = await listModelsViaSdk(); - } catch { - models = await listModelsViaHttp(); - } - const normalized = models.map((m) => normalizeModel(m)); - const payload = { offline: false, models: normalized }; - __modelsCache = { ts: Date.now(), payload }; - return NextResponse.json(payload); - } catch (e) { - const payload = { offline: true, models: mockModels }; - __modelsCache = { ts: Date.now(), payload }; - return NextResponse.json(payload); - } -} diff --git a/app/api/ollama/pull/route.ts b/app/api/ollama/pull/route.ts deleted file mode 100644 index 984d9e3..0000000 --- a/app/api/ollama/pull/route.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { NextRequest } from "next/server"; -import { pullModel, ensureOllamaAvailable } from "@/lib/ollama"; -import { getCatalogModels } from "@/lib/catalog"; -import { addSavedRemoteModel } from "@/lib/user-remote-models"; - -export const dynamic = "force-dynamic"; - -export async function POST(req: NextRequest) { - try { - const { model } = await req.json(); - - if (!model) { - return new Response("Missing model", { status: 400 }); - } - - const stream = new ReadableStream({ - async start(controller) { - try { - // Garantir que o servidor esteja disponível antes do pull - const ok = await ensureOllamaAvailable({ timeoutMs: 20000 }); - if (!ok) { - controller.enqueue( - new TextEncoder().encode( - JSON.stringify({ error: "Ollama indisponível" }) + "\n", - ), - ); - controller.close(); - return; - } - - // Buscar metadados do catálogo para melhorar mensagens de erro - const baseName = String(model).split(":")[0]; - const catalog = await getCatalogModels(); - const catalogEntry = catalog.find((m) => m.name === baseName); - - const response = await pullModel(model); - - for await (const chunk of response) { - controller.enqueue( - new TextEncoder().encode(JSON.stringify(chunk) + "\n"), - ); - } - - // Persistir referência do modelo baixado para reutilização futura - try { - await addSavedRemoteModel({ name: String(model), installed: true }); - } catch {} - - controller.enqueue( - new TextEncoder().encode( - JSON.stringify({ status: "success" }) + "\n", - ), - ); - controller.close(); - } catch (error: any) { - console.error("Pull streaming error:", error); - const message = typeof error?.message === "string" ? error.message : "Failed to pull model"; - const baseName = String(model).split(":")[0]; - let hint: string | undefined; - - // Recarregar entrada de catálogo para enriquecer erro - let catalogEntry: { url?: string; tags_count?: number } | undefined; - try { - const catalog = await getCatalogModels(); - catalogEntry = catalog.find((m) => m.name === baseName); - } catch {} - - // Heurísticas: manifest não encontrado → provavelmente falta tag válida - if (/manifest/i.test(message) || /does not exist/i.test(message)) { - hint = "Modelo/tag não encontrado no registro. Selecione uma tag válida ou verifique a página do modelo."; - } - - const payload: Record = { - error: "Failed to pull model", - detail: message, - model, - name: baseName, - }; - - // Anexar URL do catálogo quando disponível - if (catalogEntry?.url) { - payload.url = catalogEntry.url; - } - - // Se usuário não especificou tag (sem ":") e sabemos que há múltiplas tags - if (!String(model).includes(":") && typeof catalogEntry?.tags_count === "number") { - payload.tags_count = catalogEntry.tags_count; - if (!hint && catalogEntry.tags_count > 0) { - hint = "Este modelo pode não ter a tag 'latest'. Escolha uma tag específica."; - } - } - - if (hint) payload.hint = hint; - - controller.enqueue( - new TextEncoder().encode(JSON.stringify(payload) + "\n"), - ); - controller.close(); - } - }, - }); - - return new Response(stream, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }, - }); - } catch (error) { - console.error("Pull API error:", error); - return new Response("Internal server error", { status: 500 }); - } -} diff --git a/app/api/ollama/search/route.ts b/app/api/ollama/search/route.ts deleted file mode 100644 index c1f6574..0000000 --- a/app/api/ollama/search/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getSavedRemoteModels } from "@/lib/user-remote-models"; - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const query = searchParams.get("q") || undefined; - - const saved = await getSavedRemoteModels(); - const q = (query || "").trim().toLowerCase(); - const filtered = q - ? saved.filter((m) => m.name.toLowerCase().includes(q)) - : saved; - - const models = filtered.map((m) => ({ - name: m.name, - description: m.description, - url: m.url, - tags_count: m.tags_count, - installed: m.installed === true, - remote: true, - })); - - return NextResponse.json({ success: true, models }); - } catch (error) { - console.error("Error searching remote models:", error); - return NextResponse.json( - { error: "Failed to search remote models" }, - { status: 500 } - ); - } -} diff --git a/app/api/ollama/tags/route.ts b/app/api/ollama/tags/route.ts deleted file mode 100644 index ab3de1b..0000000 --- a/app/api/ollama/tags/route.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getCatalogModels } from "@/lib/catalog"; - -// Cache simples em memória para evitar múltiplos scrapes por nome -const TAGS_CACHE = new Map(); -const CACHE_TTL_MS = 60 * 60 * 1000; // 1h - -async function fetchTagsFromPage(url: string): Promise { - try { - const res = await fetch(url, { cache: "no-store" }); - if (!res.ok) return []; - const html = await res.text(); - - // Heurística best-effort: procurar padrões de tag na página - // Exemplos possíveis: ":7b", ":8b", ":q4_K_S", etc. - // Evitar listas enormes ou falsas-positivas: limitar tamanho e caracteres - const tagRegex = /:\s*([A-Za-z0-9._-]{1,32})/g; - const found = new Set(); - let match: RegExpExecArray | null; - while ((match = tagRegex.exec(html)) !== null) { - const tag = match[1]; - if (tag && !/^(http|https)$/i.test(tag)) { - found.add(tag); - } - } - - // Retornar até 50 tags para não sobrecarregar UI - return Array.from(found).slice(0, 50); - } catch { - return []; - } -} - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const name = (searchParams.get("name") || "").trim(); - if (!name) { - return NextResponse.json( - { error: "Missing name" }, - { status: 400 }, - ); - } - - // Cache - const cached = TAGS_CACHE.get(name); - const now = Date.now(); - if (cached && cached.expiresAt > now) { - return NextResponse.json({ success: true, name, tags: cached.tags }); - } - - const catalog = await getCatalogModels(); - const entry = catalog.find((m) => m.name === name); - const url = entry?.url; - - // Sem URL conhecida → não conseguimos buscar tags no registro - if (!url) { - TAGS_CACHE.set(name, { tags: [], expiresAt: now + CACHE_TTL_MS }); - return NextResponse.json({ success: true, name, tags: [] }); - } - - const tags = await fetchTagsFromPage(url); - TAGS_CACHE.set(name, { tags, expiresAt: now + CACHE_TTL_MS }); - return NextResponse.json({ success: true, name, url, tags }); - } catch (error) { - console.error("Error fetching model tags:", error); - return NextResponse.json( - { error: "Failed to fetch tags" }, - { status: 500 }, - ); - } -} - - diff --git a/app/api/ollama/warmup/route.ts b/app/api/ollama/warmup/route.ts deleted file mode 100644 index 1c5b43c..0000000 --- a/app/api/ollama/warmup/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { Ollama } from "ollama"; -import { OLLAMA_HOST } from "@/lib/ollama"; - -export const dynamic = "force-dynamic"; - -export async function GET(req: NextRequest) { - try { - const url = new URL(req.url); - const model = url.searchParams.get("model"); - if (!model) { - return NextResponse.json({ error: "model required" }, { status: 400 }); - } - - const client = new Ollama({ host: OLLAMA_HOST }); - await client.chat({ - model, - messages: [{ role: "system", content: "warm" }], - stream: false, - options: { keep_alive: "5m" }, - }); - - return NextResponse.json({ warmed: true }); - } catch (e) { - return NextResponse.json({ warmed: false }, { status: 200 }); - } -} - - diff --git a/app/api/preferences/route.ts b/app/api/preferences/route.ts deleted file mode 100644 index 0da9a7c..0000000 --- a/app/api/preferences/route.ts +++ /dev/null @@ -1,42 +0,0 @@ -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 } - ); - } -} diff --git a/app/api/search/route.ts b/app/api/search/route.ts deleted file mode 100644 index 9a0fe57..0000000 --- a/app/api/search/route.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 } - ); - } -} diff --git a/app/api/system/status/route.ts b/app/api/system/status/route.ts deleted file mode 100644 index 1251693..0000000 --- a/app/api/system/status/route.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { NextResponse } from "next/server"; -import os from "os"; - -export async function GET() { - try { - const totalMem = os.totalmem(); - const freeMem = os.freemem(); - const usedMem = totalMem - freeMem; - const load = os.loadavg(); - const memUsage = process.memoryUsage(); - - // Base com os/fallback - const baseInfo: any = { - os: { - platform: os.platform(), - arch: os.arch(), - release: os.release(), - uptimeSec: os.uptime(), - }, - cpu: { - model: os.cpus()?.[0]?.model || "unknown", - cores: os.cpus()?.length || 0, - speed: os.cpus()?.[0]?.speed || 0, - }, - memory: { - total: totalMem, - free: freeMem, - used: usedMem, - modules: [] as Array<{ size: number; clockSpeed?: number; type?: string }>, - }, - gpus: [] as Array<{ index: number; name: string; vendor?: string; vramMB?: number; bus?: string }>, - metrics: { - loadAvg: { "1m": load[0] ?? 0, "5m": load[1] ?? 0, "15m": load[2] ?? 0 }, - process: { - rss: memUsage.rss, - heapUsed: memUsage.heapUsed, - heapTotal: memUsage.heapTotal, - external: memUsage.external, - }, - }, - }; - - // Tentar usar systeminformation, se disponível - try { - // Import dinâmico sem acionar o bundler (evita Module not found) - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const dynImport: (m: string) => Promise = new Function( - "m", - "return import(m)" - ) as any; - const si = await dynImport("systeminformation"); - - const [cpu, osInfo, memLayout, graphics, currentLoad] = await Promise.all([ - si.cpu(), - si.osInfo(), - si.memLayout(), - si.graphics(), - si.currentLoad(), - ]); - - baseInfo.os = { - platform: osInfo.platform || baseInfo.os.platform, - arch: os.arch(), - release: osInfo.release || baseInfo.os.release, - uptimeSec: baseInfo.os.uptimeSec, - distro: osInfo.distro, - }; - baseInfo.cpu = { - model: cpu.brand || baseInfo.cpu.model, - cores: cpu.cores || baseInfo.cpu.cores, - physicalCores: cpu.physicalCores, - speed: Number(cpu.speed) || baseInfo.cpu.speed, - }; - baseInfo.memory.modules = (memLayout || []).map((m: any) => ({ size: m.size, clockSpeed: m.clockSpeed, type: m.type })); - baseInfo.gpus = (graphics?.controllers || []).map((c: any, idx: number) => ({ index: idx, name: c.model || c.vendor || "GPU", vendor: c.vendor, vramMB: c.vram, bus: c.bus })); - baseInfo.metrics.loadAvg = { - "1m": currentLoad?.avgload ?? baseInfo.metrics.loadAvg["1m"], - "5m": baseInfo.metrics.loadAvg["5m"], - "15m": baseInfo.metrics.loadAvg["15m"], - }; - } catch { - // sem systeminformation, seguimos com fallback - } - - // Fallback adicional para detectar GPUs no Windows via PowerShell - try { - if ((!baseInfo.gpus || baseInfo.gpus.length === 0) && (baseInfo.os.platform === "win32" || os.platform() === "win32")) { - const { execFileSync } = await import("child_process"); - const out = execFileSync("powershell", [ - "-NoProfile", - "-Command", - "Get-CimInstance Win32_VideoController | Select-Object Name,AdapterRAM,PNPDeviceID | ConvertTo-Json -Depth 3" - ], { encoding: "utf8", timeout: 2000 }); - const parsed = JSON.parse(out); - const arr = Array.isArray(parsed) ? parsed : [parsed]; - baseInfo.gpus = arr.map((g: any, idx: number) => ({ index: idx, name: g.Name || "GPU", vendor: undefined, vramMB: g.AdapterRAM ? Math.round(g.AdapterRAM / 1024 / 1024) : undefined, bus: g.PNPDeviceID })); - } - } catch { - // ignore fallback errors - } - - return NextResponse.json({ success: true, info: baseInfo }); - } catch (e) { - return NextResponse.json({ success: false, error: String(e) }, { status: 500 }); - } -} - - diff --git a/app/favicon.ico b/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/app/favicon.ico and /dev/null differ diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index 10086ed..0000000 --- a/app/globals.css +++ /dev/null @@ -1,164 +0,0 @@ -@import "tailwindcss"; - -:root { - --background: #ffffff; - --foreground: #0b0c0f; - --accent: #2563eb; /* blue-600 */ - --accent-foreground: #ffffff; - --border: #e5e7ef; - --surface: #f6f7fb; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-accent: var(--accent); - --color-border: var(--border); - --font-sans: var(--font-inter); -} - -.dark { - --background: #0b0c0f; - --foreground: #e6e8ef; - --accent: #3b82f6; /* blue-500 */ - --accent-foreground: #0b0c0f; - --border: #2a2f3a; - --surface: #12141a; -} - -body { - background: var(--background); - color: var(--foreground); - font-family: var(--font-inter), ui-sans-serif, system-ui; -} - -/* Custom scrollbar para o seletor de modelos */ -.custom-scrollbar { - scrollbar-width: thin; - scrollbar-color: var(--border) transparent; -} - -.custom-scrollbar::-webkit-scrollbar { - width: 6px; -} - -.custom-scrollbar::-webkit-scrollbar-track { - background: transparent; -} - -.custom-scrollbar::-webkit-scrollbar-thumb { - background: var(--border); - border-radius: 3px; -} - -.custom-scrollbar::-webkit-scrollbar-thumb:hover { - background: var(--foreground); - opacity: 0.3; -} - -/* MCP Marketplace Styles */ -.mcp-card { - transition: all 0.2s ease-in-out; -} - -.mcp-card:hover { - transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); -} - -.mcp-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 1rem; -} - -@media (max-width: 768px) { - .mcp-grid { - grid-template-columns: 1fr; - } -} - -.mcp-modal { - backdrop-filter: blur(8px); -} - -.mcp-category-badge { - transition: all 0.2s ease-in-out; -} - -.mcp-category-badge:hover { - transform: scale(1.05); -} - -.mcp-rating-star { - transition: color 0.2s ease-in-out; -} - -.mcp-rating-star:hover { - color: #fbbf24; -} - -.mcp-install-button { - transition: all 0.2s ease-in-out; -} - -.mcp-install-button:hover { - transform: scale(1.02); -} - -.mcp-install-button:active { - transform: scale(0.98); -} - -/* Loading animation for MCP cards */ -.mcp-loading { - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -} - -@keyframes pulse { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.5; - } -} - -/* MCP detail modal animations */ -.mcp-detail-modal { - animation: slideIn 0.3s ease-out; -} - -@keyframes slideIn { - from { - opacity: 0; - transform: scale(0.95) translateY(-20px); - } - to { - opacity: 1; - transform: scale(1) translateY(0); - } -} - -/* MCP search bar focus effects */ -.mcp-search-input:focus { - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -/* MCP category filter active state */ -.mcp-category-active { - background: linear-gradient(135deg, var(--primary), var(--primary)); - color: var(--primary-foreground); - box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); -} - -/* MCP tag hover effects */ -.mcp-tag { - transition: all 0.2s ease-in-out; -} - -.mcp-tag:hover { - background: var(--primary); - color: var(--primary-foreground); - transform: translateY(-1px); -} diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index 2ea6b33..0000000 --- a/app/layout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; -import { ThemeProvider } from "../components/theme-provider"; - -const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - - {children} - - - ); -} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 17f5c3a..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ModeToggle } from "@/components/mode-toggle"; -import { ChatContainer } from "@/components/chat/ChatContainer"; -import { ClientHome } from "@/components/ClientHome"; - -async function fetchModels() { - try { - const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; - const res = await fetch(`${baseUrl}/api/ollama/models`, { - cache: "no-store", - }); - if (!res.ok) return { offline: true, models: [] }; - return res.json(); - } catch { - return { offline: true, models: [] }; - } -} - -export default async function Home() { - const { offline, models } = await fetchModels(); - return ; -} diff --git a/biome.json b/biome.json deleted file mode 100644 index 41b3b95..0000000 --- a/biome.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": true, - "includes": ["**", "!node_modules", "!.next", "!dist", "!build"] - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "suspicious": { - "noUnknownAtRules": "off" - } - }, - "domains": { - "next": "recommended", - "react": "recommended" - } - }, - "assist": { - "actions": { - "source": { - "organizeImports": "on" - } - } - } -} diff --git a/components/ClientHome.tsx b/components/ClientHome.tsx deleted file mode 100644 index 0339a16..0000000 --- a/components/ClientHome.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { ModeToggle } from "@/components/mode-toggle"; -import { ChatContainer } from "@/components/chat/ChatContainer"; -import { ConversationSidebar } from "@/components/chat/ConversationSidebar"; -import { - SettingsButton, - SettingsModal, -} from "@/components/settings/SettingsModal"; -import { Store } from "lucide-react"; - -interface ClientHomeProps { - offline: boolean; - models: any[]; -} - -export function ClientHome({ offline, models }: ClientHomeProps) { - 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 ( -
-
-
-

Ollahub

-
- {offline && ( - - Ollama offline - - )} - setOpen(true)} /> - -
-
-
-
-
- - -
-
- setOpen(false)} /> - -
- ); -} diff --git a/components/chat/ChatContainer.tsx b/components/chat/ChatContainer.tsx deleted file mode 100644 index 3c44149..0000000 --- a/components/chat/ChatContainer.tsx +++ /dev/null @@ -1,808 +0,0 @@ -"use client"; - -import { useState, useEffect, useRef, useCallback } from "react"; -import { ModelDropdown } from "@/components/model/ModelDropdown"; -import { ModelPullDialog } from "@/components/model/ModelPullDialog"; -import { ChatMessage } from "@/components/chat/ChatMessage"; -// Settings are controlled globally in header -import { MCPSelector, ActiveMCPBadges } from "@/components/chat/MCPSelector"; -import { MCPManualInstallModal } from "@/components/mcp/MCPManualInstallModal"; -import { Send, Square } from "lucide-react"; -import type { ModelInfo } from "@/lib/models"; -import type { Message } from "@/lib/chat"; -import { createMessage } from "@/lib/chat"; -import { useUserPrefs, buildOllamaOptions } from "@/hooks/useUserPrefs"; - -interface ChatContainerProps { - models: ModelInfo[]; - offline: boolean; - onConversationCreated?: () => void; - currentConversationId?: string | null; -} - -export function ChatContainer({ - models, - offline, - onConversationCreated, - currentConversationId: propCurrentConversationId, -}: ChatContainerProps) { - const { prefs, ready, update } = useUserPrefs(); - const [messages, setMessages] = useState([]); - const [selectedModel, setSelectedModel] = useState(null); - const [conversationId, setConversationId] = useState(null); - const [isStreaming, setIsStreaming] = useState(false); - const [streamingContent, setStreamingContent] = useState(""); - const [inputContent, setInputContent] = useState(""); - const abortRef = useRef(null); - const [showPullDialog, setShowPullDialog] = useState(false); - const [modelToPull, setModelToPull] = useState(""); - const [systemPrompt, setSystemPrompt] = useState(""); - const messagesEndRef = useRef(null); - const inputRef = useRef(null); - const [showManualInstall, setShowManualInstall] = useState(false); - const [showCommandMenu, setShowCommandMenu] = useState(false); - const [commandQuery, setCommandQuery] = useState(""); - const [highlightedCmd, setHighlightedCmd] = useState(0); - - // Estado para MCPs ativos - const [activeMcps, setActiveMcps] = useState([]); - const [mcpNames, setMcpNames] = useState>(new Map()); - - // Carregar modelo salvo ao iniciar - useEffect(() => { - if (ready && prefs.selectedModel && !selectedModel) { - setSelectedModel(prefs.selectedModel); - // Warmup do modelo em background - fetch(`/api/ollama/warmup?model=${encodeURIComponent(prefs.selectedModel)}`).catch(() => {}); - } - }, [ready, prefs.selectedModel]); - - // Carregar mensagens quando uma conversa é selecionada - useEffect(() => { - if (propCurrentConversationId) { - loadConversation(propCurrentConversationId); - } else { - // Limpar mensagens quando nenhuma conversa está selecionada - setMessages([]); - setConversationId(null); - } - }, [propCurrentConversationId]); - - // Salvar modelo quando selecionado - const handleModelChange = (modelName: string) => { - setSelectedModel(modelName); - update({ selectedModel: modelName }); // Salvar no Redis - // Disparar warmup em background - fetch(`/api/ollama/warmup?model=${encodeURIComponent(modelName)}`).catch(() => {}); - }; - - // Carregar MCPs ativos do localStorage - useEffect(() => { - const saved = localStorage.getItem("ollahub-active-mcps"); - if (saved) { - try { - const parsed = JSON.parse(saved); - setActiveMcps(parsed.mcps || []); - setMcpNames(new Map(parsed.names || [])); - } catch (e) { - console.error("Error loading active MCPs:", e); - } - } - }, []); - - // Salvar MCPs ativos no localStorage quando mudarem - useEffect(() => { - if (activeMcps.length > 0 || mcpNames.size > 0) { - localStorage.setItem( - "ollahub-active-mcps", - JSON.stringify({ - mcps: activeMcps, - names: Array.from(mcpNames.entries()), - }) - ); - } - }, [activeMcps, mcpNames]); - - // Handlers para MCP Selector - const handleToggleMCP = useCallback( - async (mcpId: string) => { - setActiveMcps((prev) => { - if (prev.includes(mcpId)) { - return prev.filter((id) => id !== mcpId); - } else { - // Buscar nome do MCP se ainda não temos - if (!mcpNames.has(mcpId)) { - fetch("/api/mcp/available-for-chat") - .then((res) => res.json()) - .then((data) => { - if (data.success) { - const mcp = data.mcps.find((m: any) => m.mcpId === mcpId); - if (mcp) { - setMcpNames((prev) => new Map(prev).set(mcpId, mcp.name)); - } - } - }) - .catch(console.error); - } - return [...prev, mcpId]; - } - }); - }, - [mcpNames] - ); - - const handleClearAllMCPs = useCallback(() => { - setActiveMcps([]); - }, []); - - const handleRemoveMCP = useCallback((mcpId: string) => { - setActiveMcps((prev) => prev.filter((id) => id !== mcpId)); - }, []); - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - useEffect(() => { - scrollToBottom(); - }, [messages, streamingContent]); - - // Auto-expansão do textarea conforme o usuário digita - useEffect(() => { - if (inputRef.current) { - const el = inputRef.current; - el.style.height = "auto"; - const maxPx = 160; // ~10rem - el.style.height = `${Math.min(el.scrollHeight, maxPx)}px`; - } - }, [inputContent]); - - 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) => { - 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 newMessages = [...messages, userMessage]; - - // Add system prompt if it exists and this is the first user message - const messagesToSend = - systemPrompt && messages.length === 0 - ? [createMessage("system", systemPrompt), ...newMessages] - : newMessages; - - setMessages(newMessages); - setIsStreaming(true); - 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 { - const options = buildOllamaOptions( - prefs.device, - prefs.numGpu, - prefs.gpuIndex - ); - abortRef.current = new AbortController(); - const response = await fetch("/api/ollama/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: selectedModel, - messages: messagesToSend, - options, - system: systemPrompt || undefined, - activeMcps: activeMcps.length > 0 ? activeMcps : undefined, - }), - signal: abortRef.current.signal, - }); - - if (!response.ok) { - throw new Error("Failed to send message"); - } - - const reader = response.body?.getReader(); - if (!reader) throw new Error("No response body"); - - const decoder = new TextDecoder(); - let buffer = ""; - let assistantContent = ""; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (!line.trim()) continue; - - try { - const data = JSON.parse(line); - - // Tool execution indicator - if (data.toolExecution) { - console.log( - `🔧 MCP Tool executed: ${data.toolExecution.mcpId}.${data.toolExecution.toolName}` - ); - // Opcional: Mostrar indicador visual - } - - // Clear previous response (para prompt engineering) - if (data.clearPrevious) { - assistantContent = ""; - setStreamingContent(""); - } - - if (data.token) { - assistantContent += data.token; - setStreamingContent(assistantContent); - } - if (data.done) { - const assistantMessage = createMessage( - "assistant", - assistantContent - ); - setMessages((prev) => [...prev, assistantMessage]); - 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) { - throw new Error(data.error); - } - } catch (e) { - console.error("Error parsing stream:", e); - } - } - } - } catch (error) { - console.error("Chat error:", error); - const errorMessage = createMessage( - "assistant", - "Sorry, I encountered an error. Please try again." - ); - setMessages((prev) => [...prev, errorMessage]); - } finally { - setIsStreaming(false); - setStreamingContent(""); - abortRef.current = null; - } - }; - - // Parse de comando no submit - const tryHandleSlashCommand = (text: string): boolean => { - if (!text.startsWith("/")) return false; - const [cmd, ...rest] = text.trim().split(/\s+/); - if (cmd === "/clear") { - startNewConversation(); - setInputContent(""); - return true; - } - if (cmd === "/model") { - const newModel = rest.join(" "); - if (newModel) { - handleSelectModel(newModel); - setInputContent(""); - } - return true; - } - return false; - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (inputContent.trim() && !isStreaming) { - if (tryHandleSlashCommand(inputContent.trim())) return; - handleSendMessage(inputContent.trim()); - setInputContent(""); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (showCommandMenu) { - const commands = filteredCommands; - if (e.key === "ArrowDown") { - e.preventDefault(); - setHighlightedCmd((h) => (commands.length ? (h + 1) % commands.length : 0)); - return; - } - if (e.key === "ArrowUp") { - e.preventDefault(); - setHighlightedCmd((h) => (commands.length ? (h - 1 + commands.length) % commands.length : 0)); - return; - } - if (e.key === "Escape") { - setShowCommandMenu(false); - return; - } - if (e.key === "Enter") { - e.preventDefault(); - const cmd = commands[highlightedCmd] || commands[0]; - if (cmd) executeCommand(cmd); - return; - } - } - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(e); - } - }; - - const handleStop = () => { - if (abortRef.current && isStreaming) { - abortRef.current.abort(); - } - }; - - const handleSystemPromptChange = (prompt: string) => { - setSystemPrompt(prompt); - update({ systemPrompt: prompt }); // Salvar no SQLite - }; - - // Regenerar última resposta do assistente - const handleRegenerate = async () => { - if (isStreaming || messages.length === 0 || !selectedModel) return; - const lastUserIndex = [...messages].map((m) => m.role).lastIndexOf("user"); - if (lastUserIndex === -1) return; - const baseMessages = messages.slice(0, lastUserIndex + 1); - setMessages(baseMessages); - setIsStreaming(true); - setStreamingContent(""); - try { - const options = buildOllamaOptions( - prefs.device, - prefs.numGpu, - prefs.gpuIndex - ); - abortRef.current = new AbortController(); - const response = await fetch("/api/ollama/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model: selectedModel, - messages: baseMessages, - options, - system: systemPrompt || undefined, - activeMcps: activeMcps.length > 0 ? activeMcps : undefined, - }), - signal: abortRef.current.signal, - }); - if (!response.ok) throw new Error("Failed to regenerate"); - const reader = response.body?.getReader(); - if (!reader) throw new Error("No response body"); - const decoder = new TextDecoder(); - let buffer = ""; - let assistantContent = ""; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - for (const line of lines) { - if (!line.trim()) continue; - try { - const data = JSON.parse(line); - if (data.clearPrevious) { - assistantContent = ""; - setStreamingContent(""); - } - if (data.token) { - assistantContent += data.token; - setStreamingContent(assistantContent); - } - if (data.done) { - const assistantMessage = createMessage("assistant", assistantContent); - setMessages((prev) => [...prev, assistantMessage]); - setStreamingContent(""); - if (conversationId) { - try { - await fetch(`/api/conversations/${conversationId}/messages`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ role: "assistant", content: assistantContent }), - }); - } catch {} - } - } - if (data.error) throw new Error(data.error); - } catch (e) { - console.error("Error parsing stream:", e); - } - } - } - } catch (error) { - console.error("Regenerate error:", error); - } finally { - setIsStreaming(false); - setStreamingContent(""); - abortRef.current = null; - } - }; - - // Comandos do chat - type ChatCommand = { id: string; label: string; hint?: string }; - const availableCommands: ChatCommand[] = [ - { id: "clear", label: "/clear", hint: "Limpar o histórico atual" }, - { id: "model", label: "/model ", hint: "Trocar de modelo ativo" }, - ]; - const filteredCommands = availableCommands.filter((c) => - c.label.toLowerCase().includes(commandQuery.toLowerCase()) - ); - const executeCommand = (cmd: ChatCommand) => { - if (cmd.id === "clear") { - startNewConversation(); - setInputContent(""); - setShowCommandMenu(false); - return; - } - if (cmd.id === "model") { - setInputContent("/model "); - setTimeout(() => inputRef.current?.focus(), 0); - return; - } - }; - - const handleSelectModel = (modelId: string) => { - const model = models.find((m) => m.id === modelId); - if (model) { - handleModelChange(modelId); - // Optional: clear chat when switching models - // setMessages([]); - } - }; - - const handleDeleteModel = async (modelId: string) => { - try { - const response = await fetch("/api/ollama/delete", { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ modelName: modelId }), - }); - - if (!response.ok) { - throw new Error("Failed to delete model"); - } - - // Atualizar lista de modelos após exclusão - // Aqui você pode recarregar a lista ou remover o modelo da lista local - console.log(`Model ${modelId} deleted successfully`); - } catch (error) { - console.error("Error deleting model:", error); - } - }; - - const handlePullModel = async (modelName: string) => { - try { - const response = await fetch("/api/ollama/pull", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ model: modelName }), - }); - - if (!response.ok) { - throw new Error("Failed to pull model"); - } - - const reader = response.body?.getReader(); - if (!reader) throw new Error("No response body"); - - const decoder = new TextDecoder(); - let buffer = ""; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop() || ""; - - for (const line of lines) { - if (!line.trim()) continue; - - try { - const data = JSON.parse(line); - if (data.status === "success") { - // Model pulled successfully, refresh models list - window.location.reload(); - } else if (data.error) { - // Mostrar erro amigável quando backend identifica problema com tag/manifest - const hint = data.hint ? `\nHint: ${data.hint}` : ""; - const url = data.url ? `\nPágina do modelo: ${data.url}` : ""; - alert(`Falha ao baixar o modelo: ${data.detail || data.error}${hint}${url}`); - } - } catch (e) { - console.error("Error parsing pull stream:", e); - } - } - } - } catch (error) { - throw error; - } - }; - - // Abrir diálogo de instalação (seleção/edição de tag) - const openPullDialog = (name: string) => { - setModelToPull(name); - setShowPullDialog(true); - }; - - const selectedModelData = models.find((m) => m.id === selectedModel); - - return ( -
- {/* Chat Header */} -
-
-

- {selectedModelData ? "Chat with" : "Select a model to start chatting"} -

- -
-
- - -
-
- - {/* Messages */} -
-
- {messages.length === 0 ? ( -
-
-

Bem-vindo ao Ollahub

-

Selecione um modelo e comece a conversar.

-
-
- ) : ( - <> - {messages.map((message) => ( - - ))} - {isStreaming && streamingContent && ( - - )} -
- - )} -
-
- - {/* Input */} -
-
-
- {/* Active MCP Badges */} - - -
-
- {/* MCP Selector */} - setShowManualInstall(true)} - /> - -