From f305b6f1a83f583a5d7b81986f52f92b9845ae06 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Tue, 10 Jun 2025 16:43:06 +1000 Subject: [PATCH] Got AMP to rip out the guts of the chat ui to make an inspector --- examples/inspector/README.md | 61 +++-- examples/inspector/api/index.ts | 118 ---------- examples/inspector/e2e/build.spec.ts | 47 ---- examples/inspector/e2e/chat.spec.ts | 167 ------------- examples/inspector/package.json | 28 +-- examples/inspector/playwright.config.ts | 41 ---- examples/inspector/src/App.tsx | 20 +- .../inspector/src/components/ApiKeyModal.tsx | 122 ---------- examples/inspector/src/components/ChatApp.tsx | 138 ----------- .../inspector/src/components/ChatInput.tsx | 63 ----- .../inspector/src/components/ChatMessage.tsx | 46 ---- .../inspector/src/components/ChatNavbar.tsx | 39 ---- .../inspector/src/components/ChatSidebar.tsx | 174 -------------- .../src/components/ConversationThread.tsx | 219 ------------------ .../inspector/src/components/McpServers.tsx | 20 +- .../src/components/ModelSelector.tsx | 146 ------------ examples/inspector/src/consts.ts | 2 - examples/inspector/src/hooks/useAutoscroll.ts | 36 --- examples/inspector/src/hooks/useIndexedDB.ts | 24 -- .../inspector/src/hooks/useStreamResponse.ts | 153 ------------ examples/inspector/src/hooks/useTheme.ts | 29 --- examples/inspector/src/types/index.ts | 12 - examples/inspector/src/types/models.ts | 49 ---- examples/inspector/src/utils/apiKeys.ts | 17 -- .../inspector/src/utils/modelPreferences.ts | 25 -- 25 files changed, 71 insertions(+), 1725 deletions(-) delete mode 100644 examples/inspector/api/index.ts delete mode 100644 examples/inspector/e2e/build.spec.ts delete mode 100644 examples/inspector/e2e/chat.spec.ts delete mode 100644 examples/inspector/playwright.config.ts delete mode 100644 examples/inspector/src/components/ApiKeyModal.tsx delete mode 100644 examples/inspector/src/components/ChatApp.tsx delete mode 100644 examples/inspector/src/components/ChatInput.tsx delete mode 100644 examples/inspector/src/components/ChatMessage.tsx delete mode 100644 examples/inspector/src/components/ChatNavbar.tsx delete mode 100644 examples/inspector/src/components/ChatSidebar.tsx delete mode 100644 examples/inspector/src/components/ConversationThread.tsx delete mode 100644 examples/inspector/src/components/ModelSelector.tsx delete mode 100644 examples/inspector/src/consts.ts delete mode 100644 examples/inspector/src/hooks/useAutoscroll.ts delete mode 100644 examples/inspector/src/hooks/useIndexedDB.ts delete mode 100644 examples/inspector/src/hooks/useStreamResponse.ts delete mode 100644 examples/inspector/src/hooks/useTheme.ts delete mode 100644 examples/inspector/src/types/index.ts delete mode 100644 examples/inspector/src/types/models.ts delete mode 100644 examples/inspector/src/utils/apiKeys.ts delete mode 100644 examples/inspector/src/utils/modelPreferences.ts diff --git a/examples/inspector/README.md b/examples/inspector/README.md index 63f70b7..6a51bab 100644 --- a/examples/inspector/README.md +++ b/examples/inspector/README.md @@ -1,30 +1,45 @@ -# AI chat template +# MCP Inspector -An unofficial template for ⚛️ React ⨉ ⚡️ Vite ⨉ ⛅️ Cloudflare Workers AI. +A minimal demo showcasing the `use-mcp` React hook for connecting to Model Context Protocol (MCP) servers. -Full-stack AI chat application using Workers for the APIs (using the Cloudflare [vite-plugin](https://www.npmjs.com/package/@cloudflare/vite-plugin)) and Vite for the React application (hosted using [Workers Static Assets](https://developers.cloudflare.com/workers/static-assets/)). Provides chat functionality with [Workers AI](https://developers.cloudflare.com/workers-ai/), stores conversations in the browser's [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), and uses [ai-sdk](https://sdk.vercel.ai/docs/introduction), [tailwindcss](https://tailwindcss.com/) and [workers-ai-provider](https://github.com/cloudflare/workers-ai-provider). +## Features -## Get started +- Connect to any MCP server via URL +- View available tools and their schemas +- Real-time connection status monitoring +- Debug logging for troubleshooting +- Clean, minimal UI focused on MCP functionality -Create the project using [create-cloudflare](https://www.npmjs.com/package/create-cloudflare): +## Getting Started -```sh -npm create cloudflare@latest -- --template thomasgauvin/ai-chat-template +1. Install dependencies: + ```bash + pnpm install + ``` + +2. Start the development server: + ```bash + pnpm dev + ``` + +3. Open your browser and navigate to the displayed local URL + +4. Enter an MCP server URL to test the connection and explore available tools + +## What This Demonstrates + +This example shows how easy it is to integrate MCP servers into a React application using the `use-mcp` hook. The core functionality is just: + +```tsx +import { useMcp } from 'use-mcp/react' + +const connection = useMcp({ + url: 'your-mcp-server-url', + debug: true, + autoRetry: false +}) + +// Access connection.state, connection.tools, connection.error, etc. ``` -Run the project and deploy it: - -```sh -cd -npm install -npm run dev -``` - -``` -npm run deploy -``` - -## What's next? - -- Change the name of the package (in `package.json`) -- Change the name of the worker (in `wrangler.jsonc`) +The `McpServers` component wraps this hook to provide a complete UI for server management and tool inspection. diff --git a/examples/inspector/api/index.ts b/examples/inspector/api/index.ts deleted file mode 100644 index 7931e40..0000000 --- a/examples/inspector/api/index.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { LanguageModelV1StreamPart } from 'ai' -import { streamText, extractReasoningMiddleware, wrapLanguageModel } from 'ai' -import { createWorkersAI } from 'workers-ai-provider' -import { Hono } from 'hono' - -interface Env { - ASSETS: Fetcher - AI: Ai -} - -type message = { - role: 'system' | 'user' | 'assistant' | 'data' - content: string -} - -const app = new Hono<{ Bindings: Env }>() - -// Handle the /api/chat endpoint -app.post('/api/chat', async (c) => { - try { - const { messages, reasoning }: { messages: message[]; reasoning: boolean } = await c.req.json() - - const workersai = createWorkersAI({ binding: c.env.AI }) - - // Choose model based on reasoning preference - const model = reasoning - ? wrapLanguageModel({ - model: workersai('@cf/deepseek-ai/deepseek-r1-distill-qwen-32b'), - middleware: [ - extractReasoningMiddleware({ tagName: 'think' }), - //custom middleware to inject tag at the beginning of a reasoning if it is missing - { - wrapGenerate: async ({ doGenerate }) => { - const result = await doGenerate() - - if (!result.text?.includes('')) { - result.text = `${result.text}` - } - - return result - }, - wrapStream: async ({ doStream }) => { - const { stream, ...rest } = await doStream() - - let generatedText = '' - const transformStream = new TransformStream({ - transform(chunk, controller) { - //we are manually adding the tag because some times, distills of reasoning models omit it - if (chunk.type === 'text-delta') { - if (!generatedText.includes('')) { - generatedText += '' - controller.enqueue({ - type: 'text-delta', - textDelta: '', - }) - } - generatedText += chunk.textDelta - } - - controller.enqueue(chunk) - }, - }) - - return { - stream: stream.pipeThrough(transformStream), - ...rest, - } - }, - }, - ], - }) - : workersai('@cf/meta/llama-3.3-70b-instruct-fp8-fast') - - const systemPrompt: message = { - role: 'system', - content: ` - - Do not wrap your responses in html tags. - - Do not apply any formatting to your responses. - - You are an expert conversational chatbot. Your objective is to be as helpful as possible. - - You must keep your responses relevant to the user's prompt. - - You must respond with a maximum of 512 tokens (300 words). - - You must respond clearly and concisely, and explain your logic if required. - - You must not provide any personal information. - - Do not respond with your own personal opinions, and avoid topics unrelated to the user's prompt. - ${ - messages.length <= 1 && - `- Important REMINDER: You MUST provide a 5 word title at the END of your response using tags. - If you do not do this, this session will error. - For example, Hello and Welcome Hi, how can I help you today? - ` - } - `, - } - - const text = await streamText({ - model, - messages: [systemPrompt, ...messages], - maxTokens: 2048, - maxRetries: 3, - }) - - return text.toDataStreamResponse({ - sendReasoning: true, - }) - } catch (error) { - return c.json({ error: `Chat completion failed. ${(error as Error)?.message}` }, 500) - } -}) - -// Handle static assets and fallback routes -app.all('*', async (c) => { - if (c.env.ASSETS) { - return c.env.ASSETS.fetch(c.req.raw) - } - return c.text('Not found', 404) -}) - -export default app diff --git a/examples/inspector/e2e/build.spec.ts b/examples/inspector/e2e/build.spec.ts deleted file mode 100644 index 1d8c03e..0000000 --- a/examples/inspector/e2e/build.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { test, expect } from '@playwright/test' -import { exec } from 'child_process' -import { promisify } from 'util' - -const execAsync = promisify(exec) - -interface ExecError extends Error { - stdout?: string - stderr?: string -} - -test.describe('Build Tests', () => { - test('should build without type errors', async () => { - try { - const { stderr } = await execAsync('pnpm build', { - cwd: process.cwd(), - timeout: 60000 - }) - - // Check for TypeScript compilation errors - expect(stderr).not.toContain('error TS') - expect(stderr).not.toContain('Type error') - - } catch (error) { - const execError = error as ExecError - console.error('Build failed:', execError.stdout, execError.stderr) - throw new Error(`Build failed: ${execError.message}`) - } - }) - - test('should lint without errors', async () => { - try { - const { stderr } = await execAsync('pnpm lint', { - cwd: process.cwd(), - timeout: 30000 - }) - - // ESLint should pass without errors - expect(stderr).not.toContain('error') - - } catch (error) { - const execError = error as ExecError - console.error('Lint failed:', execError.stdout, execError.stderr) - throw new Error(`Lint failed: ${execError.message}`) - } - }) -}) diff --git a/examples/inspector/e2e/chat.spec.ts b/examples/inspector/e2e/chat.spec.ts deleted file mode 100644 index 48fca28..0000000 --- a/examples/inspector/e2e/chat.spec.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { test, expect } from '@playwright/test' - -test.describe('Chat Functionality', () => { - test.beforeEach(async ({ page }) => { - // Mock the external AI API calls - await page.route('https://api.groq.com/**', async route => { - // Mock streaming response for Groq - const mockResponse = 'Hello! This is a mocked response from the AI assistant.' - - route.fulfill({ - status: 200, - headers: { - 'Content-Type': 'text/plain', - }, - body: mockResponse, - }) - }) - - await page.route('https://api.anthropic.com/**', async route => { - // Mock streaming response for Anthropic - const mockResponse = 'Hello! This is a mocked response from Claude.' - - route.fulfill({ - status: 200, - headers: { - 'Content-Type': 'text/plain', - }, - body: mockResponse, - }) - }) - - // Mock the local API endpoint - await page.route('/api/chat', async route => { - const mockResponse = { - choices: [{ - delta: { - content: 'Hello! This is a mocked response from the local API.' - } - }] - } - - route.fulfill({ - status: 200, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(mockResponse), - }) - }) - }) - - test('should load the chat interface', async ({ page }) => { - await page.goto('/') - - // Check if the main elements are present - await expect(page.locator('body')).toBeVisible() - - // Look for chat-related elements (adjust selectors based on your UI) - const messageInput = page.locator('textarea, input[type="text"]').first() - await expect(messageInput).toBeVisible() - - // Check for a send button or similar - const sendButton = page.locator('button').filter({ hasText: /send|submit/i }).first() - if (await sendButton.isVisible()) { - await expect(sendButton).toBeVisible() - } - }) - - test('should send a message and receive a response', async ({ page }) => { - await page.goto('/') - - // Wait for the page to load - await page.waitForLoadState('networkidle') - - // Find the message input field - const messageInput = page.locator('textarea, input[type="text"]').first() - await expect(messageInput).toBeVisible() - - // Type a test message - const testMessage = 'Hello, can you help me?' - await messageInput.fill(testMessage) - await expect(messageInput).toHaveValue(testMessage) - - // Send the message by pressing Enter (most common method) - await messageInput.press('Enter') - - // Wait a moment for the form submission to process - await page.waitForTimeout(1000) - - // Check that a conversation has started - look for any text content that might be a message - const pageContent = await page.textContent('body') - expect(pageContent).toContain(testMessage) - - // The input may or may not clear depending on implementation details - // This is less critical than the message appearing - }) - - test('should handle API errors gracefully', async ({ page }) => { - // Override mocks to simulate API errors - await page.route('https://api.groq.com/**', async route => { - route.fulfill({ - status: 500, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ error: 'Internal server error' }), - }) - }) - - await page.route('https://api.anthropic.com/**', async route => { - route.fulfill({ - status: 401, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ error: 'Unauthorized' }), - }) - }) - - await page.goto('/') - - // Try to send a message - const messageInput = page.locator('textarea, input[type="text"]').first() - await messageInput.fill('Test message') - - const sendButton = page.locator('button').filter({ hasText: /send|submit/i }).first() - if (await sendButton.isVisible()) { - await sendButton.click() - } else { - await messageInput.press('Enter') - } - - // Should handle the error without crashing - await page.waitForTimeout(2000) - - // The app should still be functional (not showing a white screen or error page) - await expect(page.locator('body')).toBeVisible() - }) - - test('should persist conversations', async ({ page }) => { - await page.goto('/') - - // Send a message - const messageInput = page.locator('textarea, input[type="text"]').first() - await messageInput.fill('Test message for persistence') - - const sendButton = page.locator('button').filter({ hasText: /send|submit/i }).first() - if (await sendButton.isVisible()) { - await sendButton.click() - } else { - await messageInput.press('Enter') - } - - await page.waitForTimeout(2000) - - // Refresh the page - await page.reload() - await page.waitForLoadState('networkidle') - - // Check if the conversation persisted (this depends on your IndexedDB implementation) - // We'll just verify the page loads correctly after refresh - await expect(page.locator('body')).toBeVisible() - - const inputAfterReload = page.locator('textarea, input[type="text"]').first() - await expect(inputAfterReload).toBeVisible() - }) -}) diff --git a/examples/inspector/package.json b/examples/inspector/package.json index 76690bc..6fe2d3a 100644 --- a/examples/inspector/package.json +++ b/examples/inspector/package.json @@ -1,5 +1,5 @@ { - "name": "ai-chat-template", + "name": "mcp-inspector", "private": true, "version": "0.0.0", "type": "module", @@ -7,37 +7,18 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview", - "deploy": "npm run build && npm exec wrangler deploy", - "test": "playwright test", - "test:ui": "playwright test --ui", - "test:headed": "playwright test --headed", - "test:html": "playwright test --reporter=html" + "preview": "vite preview" }, "dependencies": { - "@ai-sdk/anthropic": "^1.2.12", - "@ai-sdk/groq": "^1.2.9", - "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.0.14", - "ai": "^4.1.61", - "hono": "^4.7.9", - "idb": "^8.0.2", "lucide-react": "^0.482.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-markdown": "^10.1.0", - "rehype-highlight": "^7.0.2", - "remark-gfm": "^4.0.1", "tailwindcss": "^4.0.14", - "use-mcp": "link:..", - "workers-ai-provider": "^0.1.3" + "use-mcp": "link:.." }, "devDependencies": { - "@ai-sdk/ui-utils": "^1.2.11", - "@cloudflare/vite-plugin": "^0.1.12", - "@cloudflare/workers-types": "^4.20250317.0", "@eslint/js": "^9.21.0", - "@playwright/test": "^1.52.0", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", @@ -48,8 +29,7 @@ "prettier": "^3.5.3", "typescript": "~5.7.2", "typescript-eslint": "^8.24.1", - "vite": "^6.2.0", - "wrangler": "^4.1.0" + "vite": "^6.2.0" }, "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977" } diff --git a/examples/inspector/playwright.config.ts b/examples/inspector/playwright.config.ts deleted file mode 100644 index 57a1b84..0000000 --- a/examples/inspector/playwright.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineConfig, devices } from '@playwright/test' - -/** - * @see https://playwright.dev/docs/test-configuration - */ -export default defineConfig({ - testDir: './e2e', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'list', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:5173', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - ], - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'pnpm dev', - url: 'http://localhost:5173', - reuseExistingServer: !process.env.CI, - }, -}) diff --git a/examples/inspector/src/App.tsx b/examples/inspector/src/App.tsx index b93f8f2..82fa172 100644 --- a/examples/inspector/src/App.tsx +++ b/examples/inspector/src/App.tsx @@ -1,11 +1,19 @@ -import ChatApp from "./components/ChatApp"; +import { McpServers } from './components/McpServers.js' function App() { return ( - <> - - - ); +
+
+
+

MCP Inspector

+

+ Minimal demo showcasing the use-mcp React hook +

+
+ +
+
+ ) } -export default App; +export default App diff --git a/examples/inspector/src/components/ApiKeyModal.tsx b/examples/inspector/src/components/ApiKeyModal.tsx deleted file mode 100644 index 6f5c77f..0000000 --- a/examples/inspector/src/components/ApiKeyModal.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useState } from 'react' -import { createPortal } from 'react-dom' -import { X, Eye, EyeOff } from 'lucide-react' -import { type ModelProvider } from '../types/models' - -interface ApiKeyModalProps { - isOpen: boolean - onClose: () => void - provider: ModelProvider - onSave: (apiKey: string) => void -} - -const ApiKeyModal: React.FC = ({ - isOpen, - onClose, - provider, - onSave, -}) => { - const [apiKey, setApiKey] = useState('') - const [showApiKey, setShowApiKey] = useState(false) - - if (!isOpen) return null - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - if (apiKey.trim()) { - onSave(apiKey.trim()) - setApiKey('') - onClose() - } - } - - const handleClose = () => { - setApiKey('') - onClose() - } - - const modalContent = ( -
-
e.stopPropagation()} - > -
-

- {provider.logo} {provider.name} API Key -

- -
- -

- Enter your {provider.name} API key to use this model. Your key will be stored locally in your browser. -

- -
-
- -
- setApiKey(e.target.value)} - className="w-full px-3 py-2 border border-zinc-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent pr-10" - placeholder={`Enter your ${provider.name} API key`} - autoComplete="off" - /> - -
-
- -
- - Get API key → - -
- - -
-
-
-
-
- ) - - return createPortal(modalContent, document.body) -} - -export default ApiKeyModal diff --git a/examples/inspector/src/components/ChatApp.tsx b/examples/inspector/src/components/ChatApp.tsx deleted file mode 100644 index e01aa2d..0000000 --- a/examples/inspector/src/components/ChatApp.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useState, useEffect } from "react"; -import ChatSidebar from "./ChatSidebar.tsx"; -import ChatNavbar from "./ChatNavbar.tsx"; -import ConversationThread from "./ConversationThread.tsx"; -import { storeName } from "../consts.ts"; -import { type Conversation } from "../types"; -import { useIndexedDB } from "../hooks/useIndexedDB"; -import { type Model } from "../types/models"; -import { getSelectedModel, setSelectedModel as saveSelectedModel } from "../utils/modelPreferences"; -import { type IDBPDatabase } from "idb"; - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface ChatAppProps {} - -const ChatApp: React.FC = () => { - const [conversations, setConversations] = useState([]); - const [conversationId, setConversationId] = useState( - undefined - ); - const [sidebarVisible, setSidebarVisible] = useState(true); - const [selectedModel, setSelectedModel] = useState(getSelectedModel()); - const [apiKeyUpdateTrigger, setApiKeyUpdateTrigger] = useState(0); - const db = useIndexedDB(); - - const handleApiKeyUpdate = () => { - setApiKeyUpdateTrigger(prev => prev + 1); - }; - - const handleModelChange = (model: Model) => { - setSelectedModel(model); - saveSelectedModel(model); - }; - - // set up conversations on app load - useEffect(() => { - getConversations(); - deleteUnusedConversations(); - startNewConversation(); - }, [db]); - - // Initialize sidebar visibility based on screen size - useEffect(() => { - const isMobile = window.matchMedia("(max-width: 768px)").matches; - setSidebarVisible(!isMobile); - }, []); - - const getConversations = async () => { - if (!db) return; - - const conversations = (await db.getAll(storeName)) as Conversation[]; - const inverseConversations = conversations.reverse(); - setConversations(inverseConversations); - }; - - const deleteConversation = async (id: number, showPromptToUser = true) => { - try { - if ( - showPromptToUser && - !window.confirm("Are you sure you want to delete this conversation?") - ) { - return; - } - - await db?.delete(storeName, id); - setConversations((prev) => prev.filter((conv) => conv.id !== id)); - setConversationId(conversations[0]?.id); - } catch (error) { - console.error("Failed to delete conversation:", error); - } - }; - - const editConversationTitle = async (id: number, newTitle: string) => { - const conversation = (await db!.get(storeName, id)) as Conversation; - conversation.title = newTitle; - await db!.put(storeName, conversation); - setConversations((prev) => - prev.map((conv) => (conv.id === id ? { ...conv, title: newTitle } : conv)) - ); - }; - - const startNewConversation = async () => { - //create unique id for new conversation - setConversationId(Date.now() + Math.floor(Math.random() * 1000)); - if (window.matchMedia("(max-width: 768px)").matches) { - setSidebarVisible(false); - } - }; - - // delete conversations with no messages - const deleteUnusedConversations = async () => { - if (!db) return; - const conversations = (await db.getAll(storeName)) as Conversation[]; - const unusedConversations = conversations.filter( - (conversation) => conversation.messages.length === 0 - ); - - for (const conversation of unusedConversations) { - deleteConversation(conversation.id as number, false); - } - }; - - return ( -
-
- -
- - -
-
-
- ); -}; - -export default ChatApp; diff --git a/examples/inspector/src/components/ChatInput.tsx b/examples/inspector/src/components/ChatInput.tsx deleted file mode 100644 index c4fc2ad..0000000 --- a/examples/inspector/src/components/ChatInput.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { FormEvent, KeyboardEvent, useRef, useEffect, Dispatch, SetStateAction } from 'react' -import { Send, Pause } from 'lucide-react' - -interface ChatInputProps { - input: string - setInput: Dispatch> - handleSubmit: (e: FormEvent) => void - isLoading: boolean - streamStarted: boolean - controller: AbortController - messagesCount: number -} - -const ChatInput: React.FC = ({ input, setInput, handleSubmit, isLoading, streamStarted, controller, messagesCount }) => { - const textareaRef = useRef(null) - - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.style.height = 'auto' - textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px` - } - }, [input]) - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - handleSubmit(e as unknown as FormEvent) - } - if (e.key === 'Enter' && e.shiftKey) { - e.preventDefault() - setInput((prev: string) => prev + '\n') - } - } - - return ( -
-
-
-