From ad9d5d32d2d9bda16a6e9c174cb272b446ac7923 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Tue, 18 Nov 2025 21:43:11 -0300 Subject: [PATCH] chore: remove deprecated files and configurations - Deleted .gitignore, biome.json, next.config.ts, package.json, pnpm-lock.yaml, pnpm-workspace.yaml, postcss.config.mjs, README.md, and various API, component, and utility files to clean up the codebase. - Removed unused database files and migration scripts to streamline the project structure. - Eliminated obsolete hooks and services to enhance maintainability and reduce clutter. --- .gitignore | 52 - README.md | 36 - app/api/conversations/[id]/messages/route.ts | 77 - app/api/conversations/[id]/route.ts | 58 - app/api/conversations/route.ts | 54 - app/api/mcp/available-for-chat/route.ts | 58 - app/api/mcp/execute-tool/route.ts | 89 - app/api/mcp/install-manual/route.ts | 359 --- app/api/mcp/install-status/route.ts | 58 - app/api/mcp/install/route.ts | 226 -- app/api/mcp/installed/route.ts | 46 - app/api/mcp/server-config/route.ts | 53 - app/api/mcp/test-server/route.ts | 128 - app/api/mcp/uninstall/route.ts | 62 - app/api/mcp/update-config/route.ts | 52 - app/api/ollama/catalog/route.ts | 0 app/api/ollama/chat/route.ts | 377 --- app/api/ollama/delete/route.ts | 28 - app/api/ollama/health/route.ts | 18 - app/api/ollama/models/route.ts | 59 - app/api/ollama/pull/route.ts | 114 - app/api/ollama/search/route.ts | 32 - app/api/ollama/tags/route.ts | 74 - app/api/ollama/warmup/route.ts | 29 - app/api/preferences/route.ts | 42 - app/api/search/route.ts | 25 - app/api/system/status/route.ts | 108 - app/favicon.ico | Bin 25931 -> 0 bytes app/globals.css | 164 - app/layout.tsx | 25 - app/page.tsx | 21 - biome.json | 37 - components/ClientHome.tsx | 75 - components/chat/ChatContainer.tsx | 808 ----- components/chat/ChatMessage.tsx | 165 - components/chat/ConversationSidebar.tsx | 176 -- components/chat/MCPSelector.tsx | 293 -- components/chat/SettingsModal.tsx | 164 - components/chat/SystemPrompt.tsx | 111 - components/chat/SystemPromptInfo.tsx | 50 - components/layout/Header.tsx | 0 components/layout/Sidebar.tsx | 0 components/mcp/MCPCard.tsx | 297 -- components/mcp/MCPDetailModal.tsx | 356 --- components/mcp/MCPInstallConfigModal.tsx | 272 -- components/mcp/MCPManualInstallModal.tsx | 309 -- components/message/MarkdownRenderer.tsx | 101 - components/message/MessageActions.tsx | 111 - components/mode-toggle.tsx | 27 - components/model/ModelCard.tsx | 61 - components/model/ModelDropdown.tsx | 475 --- components/model/ModelList.tsx | 21 - components/model/ModelPullDialog.tsx | 298 -- components/settings/SettingsModal.tsx | 336 -- components/theme-provider.tsx | 20 - components/ui/Badge.tsx | 0 components/ui/Button.tsx | 0 components/ui/Card.tsx | 0 components/ui/Input.tsx | 0 components/ui/Separator.tsx | 0 data/models.json | 967 ------ database/client.ts | 34 - database/database.ts | 34 - database/migrations.ts | 130 - database/models.json | 967 ------ database/ollahub-preferences.db-shm | Bin 32768 -> 0 bytes database/ollahub-preferences.db-wal | Bin 259592 -> 0 bytes database/ollahub.db-shm | Bin 32768 -> 0 bytes database/ollahub.db-wal | Bin 2311352 -> 0 bytes database/repositories/conversation.ts | 78 - database/repositories/embedding.ts | 100 - database/repositories/mcp-active.ts | 156 - database/repositories/mcp.ts | 452 --- database/repositories/message.ts | 101 - database/schema.sql | 69 - database/user-remote-models.json | 9 - hooks/useCopyPrefs.ts | 28 - hooks/useMCPMarketplace.ts | 192 -- hooks/useMarkdown.ts | 27 - hooks/useUserPrefs.ts | 79 - lib/cache/mcp-cache.ts | 173 - lib/catalog.ts | 50 - lib/chat.ts | 50 - lib/config/mcp.ts | 33 - lib/embeddings.ts | 131 - lib/hardware.ts | 0 lib/models.ts | 100 - lib/ollama.ts | 200 -- lib/preferences.ts | 99 - lib/prompts/mcp-tools-prompt.ts | 242 -- lib/services/deepnlp.ts | 341 -- lib/services/intent-detector.ts | 210 -- lib/services/mcp-environment.ts | 214 -- lib/services/mcp-executor.ts | 342 -- lib/services/mcp-installer.ts | 429 --- lib/services/mcp-validator.ts | 301 -- lib/services/model-capabilities.ts | 77 - lib/systemPrompt.ts | 30 - lib/types/mcp-chat.ts | 74 - lib/types/mcp-installer.ts | 65 - lib/types/mcp.ts | 72 - lib/user-remote-models.ts | 62 - lib/utils/system.ts | 166 - lib/vector-search.ts | 32 - next.config.ts | 7 - package.json | 47 - pnpm-lock.yaml | 2967 ------------------ pnpm-workspace.yaml | 7 - postcss.config.mjs | 7 - public/file.svg | 1 - public/globe.svg | 1 - public/next.svg | 1 - public/vercel.svg | 1 - public/window.svg | 1 - scripts/ensure-ollama.ps1 | 123 - scripts/init-db.ts | 191 -- scripts/test-mcp-installation.ts | 231 -- test-markdown.tsx | 1 - tsconfig.json | 44 - 119 files changed, 17633 deletions(-) delete mode 100644 .gitignore delete mode 100644 README.md delete mode 100644 app/api/conversations/[id]/messages/route.ts delete mode 100644 app/api/conversations/[id]/route.ts delete mode 100644 app/api/conversations/route.ts delete mode 100644 app/api/mcp/available-for-chat/route.ts delete mode 100644 app/api/mcp/execute-tool/route.ts delete mode 100644 app/api/mcp/install-manual/route.ts delete mode 100644 app/api/mcp/install-status/route.ts delete mode 100644 app/api/mcp/install/route.ts delete mode 100644 app/api/mcp/installed/route.ts delete mode 100644 app/api/mcp/server-config/route.ts delete mode 100644 app/api/mcp/test-server/route.ts delete mode 100644 app/api/mcp/uninstall/route.ts delete mode 100644 app/api/mcp/update-config/route.ts delete mode 100644 app/api/ollama/catalog/route.ts delete mode 100644 app/api/ollama/chat/route.ts delete mode 100644 app/api/ollama/delete/route.ts delete mode 100644 app/api/ollama/health/route.ts delete mode 100644 app/api/ollama/models/route.ts delete mode 100644 app/api/ollama/pull/route.ts delete mode 100644 app/api/ollama/search/route.ts delete mode 100644 app/api/ollama/tags/route.ts delete mode 100644 app/api/ollama/warmup/route.ts delete mode 100644 app/api/preferences/route.ts delete mode 100644 app/api/search/route.ts delete mode 100644 app/api/system/status/route.ts delete mode 100644 app/favicon.ico delete mode 100644 app/globals.css delete mode 100644 app/layout.tsx delete mode 100644 app/page.tsx delete mode 100644 biome.json delete mode 100644 components/ClientHome.tsx delete mode 100644 components/chat/ChatContainer.tsx delete mode 100644 components/chat/ChatMessage.tsx delete mode 100644 components/chat/ConversationSidebar.tsx delete mode 100644 components/chat/MCPSelector.tsx delete mode 100644 components/chat/SettingsModal.tsx delete mode 100644 components/chat/SystemPrompt.tsx delete mode 100644 components/chat/SystemPromptInfo.tsx delete mode 100644 components/layout/Header.tsx delete mode 100644 components/layout/Sidebar.tsx delete mode 100644 components/mcp/MCPCard.tsx delete mode 100644 components/mcp/MCPDetailModal.tsx delete mode 100644 components/mcp/MCPInstallConfigModal.tsx delete mode 100644 components/mcp/MCPManualInstallModal.tsx delete mode 100644 components/message/MarkdownRenderer.tsx delete mode 100644 components/message/MessageActions.tsx delete mode 100644 components/mode-toggle.tsx delete mode 100644 components/model/ModelCard.tsx delete mode 100644 components/model/ModelDropdown.tsx delete mode 100644 components/model/ModelList.tsx delete mode 100644 components/model/ModelPullDialog.tsx delete mode 100644 components/settings/SettingsModal.tsx delete mode 100644 components/theme-provider.tsx delete mode 100644 components/ui/Badge.tsx delete mode 100644 components/ui/Button.tsx delete mode 100644 components/ui/Card.tsx delete mode 100644 components/ui/Input.tsx delete mode 100644 components/ui/Separator.tsx delete mode 100644 data/models.json delete mode 100644 database/client.ts delete mode 100644 database/database.ts delete mode 100644 database/migrations.ts delete mode 100644 database/models.json delete mode 100644 database/ollahub-preferences.db-shm delete mode 100644 database/ollahub-preferences.db-wal delete mode 100644 database/ollahub.db-shm delete mode 100644 database/ollahub.db-wal delete mode 100644 database/repositories/conversation.ts delete mode 100644 database/repositories/embedding.ts delete mode 100644 database/repositories/mcp-active.ts delete mode 100644 database/repositories/mcp.ts delete mode 100644 database/repositories/message.ts delete mode 100644 database/schema.sql delete mode 100644 database/user-remote-models.json delete mode 100644 hooks/useCopyPrefs.ts delete mode 100644 hooks/useMCPMarketplace.ts delete mode 100644 hooks/useMarkdown.ts delete mode 100644 hooks/useUserPrefs.ts delete mode 100644 lib/cache/mcp-cache.ts delete mode 100644 lib/catalog.ts delete mode 100644 lib/chat.ts delete mode 100644 lib/config/mcp.ts delete mode 100644 lib/embeddings.ts delete mode 100644 lib/hardware.ts delete mode 100644 lib/models.ts delete mode 100644 lib/ollama.ts delete mode 100644 lib/preferences.ts delete mode 100644 lib/prompts/mcp-tools-prompt.ts delete mode 100644 lib/services/deepnlp.ts delete mode 100644 lib/services/intent-detector.ts delete mode 100644 lib/services/mcp-environment.ts delete mode 100644 lib/services/mcp-executor.ts delete mode 100644 lib/services/mcp-installer.ts delete mode 100644 lib/services/mcp-validator.ts delete mode 100644 lib/services/model-capabilities.ts delete mode 100644 lib/systemPrompt.ts delete mode 100644 lib/types/mcp-chat.ts delete mode 100644 lib/types/mcp-installer.ts delete mode 100644 lib/types/mcp.ts delete mode 100644 lib/user-remote-models.ts delete mode 100644 lib/utils/system.ts delete mode 100644 lib/vector-search.ts delete mode 100644 next.config.ts delete mode 100644 package.json delete mode 100644 pnpm-lock.yaml delete mode 100644 pnpm-workspace.yaml delete mode 100644 postcss.config.mjs delete mode 100644 public/file.svg delete mode 100644 public/globe.svg delete mode 100644 public/next.svg delete mode 100644 public/vercel.svg delete mode 100644 public/window.svg delete mode 100644 scripts/ensure-ollama.ps1 delete mode 100644 scripts/init-db.ts delete mode 100644 scripts/test-mcp-installation.ts delete mode 100644 test-markdown.tsx delete mode 100644 tsconfig.json 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 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m 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)} - /> - -