From 741626831865eafc864a6a46e75a41d71c44c4d0 Mon Sep 17 00:00:00 2001 From: viktormarinho Date: Fri, 6 Feb 2026 16:01:26 -0300 Subject: [PATCH] wip --- apps/feature-request/.env.example | 10 ++ apps/feature-request/.gitignore | 31 ++++ apps/feature-request/app/api/chat/route.ts | 78 +++++++++ apps/feature-request/app/globals.css | 75 +++++++++ apps/feature-request/app/layout.tsx | 20 +++ apps/feature-request/app/page.tsx | 157 ++++++++++++++++++ .../feature-request/components/chat-input.tsx | 90 ++++++++++ .../components/chat-messages.tsx | 132 +++++++++++++++ .../components/message-content.tsx | 12 ++ apps/feature-request/lib/system-prompt.ts | 84 ++++++++++ apps/feature-request/middleware.ts | 56 +++++++ apps/feature-request/next.config.ts | 7 + apps/feature-request/package.json | 29 ++++ apps/feature-request/postcss.config.mjs | 8 + apps/feature-request/tsconfig.json | 23 +++ 15 files changed, 812 insertions(+) create mode 100644 apps/feature-request/.env.example create mode 100644 apps/feature-request/.gitignore create mode 100644 apps/feature-request/app/api/chat/route.ts create mode 100644 apps/feature-request/app/globals.css create mode 100644 apps/feature-request/app/layout.tsx create mode 100644 apps/feature-request/app/page.tsx create mode 100644 apps/feature-request/components/chat-input.tsx create mode 100644 apps/feature-request/components/chat-messages.tsx create mode 100644 apps/feature-request/components/message-content.tsx create mode 100644 apps/feature-request/lib/system-prompt.ts create mode 100644 apps/feature-request/middleware.ts create mode 100644 apps/feature-request/next.config.ts create mode 100644 apps/feature-request/package.json create mode 100644 apps/feature-request/postcss.config.mjs create mode 100644 apps/feature-request/tsconfig.json diff --git a/apps/feature-request/.env.example b/apps/feature-request/.env.example new file mode 100644 index 0000000000..d56555eec1 --- /dev/null +++ b/apps/feature-request/.env.example @@ -0,0 +1,10 @@ +# Mesh connection +MESH_URL=http://localhost:3000 +MESH_API_KEY=mk_xxxxxxx +MESH_ORG_SLUG=my-org + +# Chat config +AGENT_ID=vmcp_xxxxxxx +MODEL_CONNECTION_ID=conn_xxxxxxx +MODEL_ID=google/gemini-2.5-flash +TOOL_MODE=smart_tool_selection diff --git a/apps/feature-request/.gitignore b/apps/feature-request/.gitignore new file mode 100644 index 0000000000..003c9c8757 --- /dev/null +++ b/apps/feature-request/.gitignore @@ -0,0 +1,31 @@ +# dependencies +node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# next.js +/.next/ +/out/ + +# production +/build + +# env files +.env +.env.local +.env.*.local + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/feature-request/app/api/chat/route.ts b/apps/feature-request/app/api/chat/route.ts new file mode 100644 index 0000000000..100e68f1b5 --- /dev/null +++ b/apps/feature-request/app/api/chat/route.ts @@ -0,0 +1,78 @@ +import { SYSTEM_PROMPT } from "@/lib/system-prompt"; + +export async function POST(req: Request) { + const { messages } = await req.json(); + + const meshUrl = process.env.MESH_URL; + const apiKey = process.env.MESH_API_KEY; + const orgSlug = process.env.MESH_ORG_SLUG; + const agentId = process.env.AGENT_ID; + const modelConnectionId = process.env.MODEL_CONNECTION_ID; + const modelId = process.env.MODEL_ID; + const toolMode = process.env.TOOL_MODE || "smart_tool_selection"; + + if ( + !meshUrl || + !apiKey || + !orgSlug || + !agentId || + !modelConnectionId || + !modelId + ) { + return new Response( + JSON.stringify({ + error: "Server misconfigured — missing environment variables", + }), + { status: 500, headers: { "Content-Type": "application/json" } }, + ); + } + + const response = await fetch(`${meshUrl}/api/${orgSlug}/decopilot/stream`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + messages: [ + { + id: crypto.randomUUID(), + role: "system", + parts: [{ type: "text", text: SYSTEM_PROMPT }], + }, + ...messages, + ], + model: { + id: modelId, + connectionId: modelConnectionId, + }, + agent: { + id: agentId, + mode: toolMode, + }, + }), + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => "Unknown error"); + return new Response( + JSON.stringify({ + error: `Mesh API error: ${response.status}`, + details: errorText, + }), + { + status: response.status, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + return new Response(response.body, { + status: response.status, + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); +} diff --git a/apps/feature-request/app/globals.css b/apps/feature-request/app/globals.css new file mode 100644 index 0000000000..768cf637b9 --- /dev/null +++ b/apps/feature-request/app/globals.css @@ -0,0 +1,75 @@ +@import "tailwindcss"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.961 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.961 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.961 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --radius: 0.625rem; +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --border: oklch(0.275 0 0); + --input: oklch(0.275 0 0); + --ring: oklch(0.556 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-family: "Inter", system-ui, -apple-system, sans-serif; + } +} diff --git a/apps/feature-request/app/layout.tsx b/apps/feature-request/app/layout.tsx new file mode 100644 index 0000000000..f9601ec1f6 --- /dev/null +++ b/apps/feature-request/app/layout.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Feature Request - MCP Mesh", + description: + "Propose a feature for MCP Mesh. Chat with our AI tech lead to shape your idea into a clear plan.", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/apps/feature-request/app/page.tsx b/apps/feature-request/app/page.tsx new file mode 100644 index 0000000000..6b837e7595 --- /dev/null +++ b/apps/feature-request/app/page.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "ai"; +import { useRef, useEffect, useState } from "react"; +import { ChatMessages } from "@/components/chat-messages"; +import { ChatInput } from "@/components/chat-input"; + +const transport = new DefaultChatTransport({ + api: "/api/chat", +}); + +const ICE_BREAKERS = [ + "I'd like a way to see which MCP tools are most used", + "Can we add keyboard shortcuts to the chat?", + "I want to export my conversation history", +]; + +function EmptyState({ onSelect }: { onSelect: (text: string) => void }) { + return ( +
+
+
+ + + +
+

+ Request a Feature +

+

+ Describe a feature you'd like to see in MCP Mesh. I'll help + you shape it into a clear plan and create a GitHub issue. +

+
+ +
+

+ Try one of these +

+ {ICE_BREAKERS.map((text) => ( + + ))} +
+
+ ); +} + +export default function FeatureRequestPage() { + const scrollRef = useRef(null); + const [input, setInput] = useState(""); + + const { messages, sendMessage, status, stop } = useChat({ + transport, + }); + + const isStreaming = status === "streaming" || status === "submitted"; + const isEmpty = messages.length === 0; + + // Auto-scroll to bottom on new messages + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages]); + + const handleSend = () => { + const text = input.trim(); + if (!text) return; + setInput(""); + sendMessage({ text }); + }; + + const handleIceBreaker = (text: string) => { + setInput(""); + sendMessage({ text }); + }; + + return ( +
+ {/* Header */} +
+
+
+ + + +
+
+

+ MCP Mesh — Feature Request +

+

+ Describe your idea and we'll shape it into a plan +

+
+
+
+ + {/* Messages area */} +
+
+ {isEmpty ? ( + + ) : ( +
+ +
+ )} +
+
+ + {/* Input area */} +
+
+ +

+ AI-powered feature planning. Responses may not always be accurate. +

+
+
+
+ ); +} diff --git a/apps/feature-request/components/chat-input.tsx b/apps/feature-request/components/chat-input.tsx new file mode 100644 index 0000000000..ca6032e26c --- /dev/null +++ b/apps/feature-request/components/chat-input.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useRef } from "react"; + +interface ChatInputProps { + value: string; + onChange: (value: string) => void; + onSubmit: () => void; + onStop: () => void; + isStreaming: boolean; + disabled?: boolean; +} + +export function ChatInput({ + value, + onChange, + onSubmit, + onStop, + isStreaming, + disabled, +}: ChatInputProps) { + const textareaRef = useRef(null); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + if (!isStreaming && value.trim()) { + onSubmit(); + } + } + }; + + const handleInput = (e: React.ChangeEvent) => { + onChange(e.target.value); + // Auto-resize + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = "auto"; + textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; + } + }; + + return ( +
+