diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 000000000..612621fd9 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,89 @@ +# Sidecar Frontend + +A standalone frontend for the Sidecar AI assistant, built with Next.js, TypeScript, and CodeMirror. + +## Features + +- File explorer to browse and select files +- Code editor with syntax highlighting for various languages +- Chat interface to interact with Sidecar AI +- Split panel layout for optimal workspace organization + +## Prerequisites + +- Node.js 18+ and npm/yarn +- Sidecar webserver running locally + +## Getting Started + +1. Clone this repository +2. Install dependencies: + +```bash +npm install +# or +yarn install +``` + +3. Make sure the Sidecar webserver is running: + +```bash +# In the sidecar repository +cargo build --bin webserver +./target/debug/webserver +``` + +4. Start the development server: + +```bash +npm run dev +# or +yarn dev +``` + +5. Open [http://localhost:3000](http://localhost:3000) in your browser + +## Configuration + +The frontend is configured to proxy API requests to the Sidecar webserver running on port 3000. If your Sidecar webserver is running on a different port, update the `next.config.js` file: + +```javascript +async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'http://localhost:YOUR_PORT/api/:path*', // Change to your port + }, + ]; +} +``` + +## Usage + +1. Browse files in the file explorer panel +2. Click on a file to open it in the code editor +3. Edit files directly in the code editor +4. Use the chat interface to interact with Sidecar AI +5. Ask questions about your code or request assistance with coding tasks + +## Building for Production + +To build the application for production: + +```bash +npm run build +# or +yarn build +``` + +Then start the production server: + +```bash +npm run start +# or +yarn start +``` + +## License + +This project is licensed under the same license as the Sidecar project. \ No newline at end of file diff --git a/frontend/components/ChatInterface.tsx b/frontend/components/ChatInterface.tsx new file mode 100644 index 000000000..ecd54f5c0 --- /dev/null +++ b/frontend/components/ChatInterface.tsx @@ -0,0 +1,142 @@ +import { useState } from 'react'; +import { FiSend, FiSettings } from 'react-icons/fi'; +import { sendAgentMessage } from '@/services/api'; + +interface ChatMessage { + role: 'user' | 'assistant'; + content: string; +} + +interface ChatInterfaceProps { + selectedFile: string | null; + allFiles: string[]; +} + +const ChatInterface: React.FC = ({ selectedFile, allFiles }) => { + const [message, setMessage] = useState(''); + const [chatHistory, setChatHistory] = useState([]); + const [loading, setLoading] = useState(false); + + const handleSendMessage = async () => { + if (!message.trim()) return; + + // Add user message to chat history + const updatedHistory = [...chatHistory, { role: 'user', content: message }]; + setChatHistory(updatedHistory); + + // Clear input + setMessage(''); + setLoading(true); + + try { + // Send message to sidecar API + await sendAgentMessage({ + sessionId: 'frontend-session', + exchangeId: `exchange-${Date.now()}`, + editorUrl: window.location.origin, + query: message, + userContext: { + visibleFiles: selectedFile ? [selectedFile] : [], + openFiles: selectedFile ? [selectedFile] : [], + }, + repoRef: { + name: 'local', + url: '', + }, + projectLabels: [], + rootDirectory: '.', + accessToken: '', + modelConfiguration: { + fastModel: 'gpt-3.5-turbo', + slowModel: 'gpt-4', + }, + allFiles, + openFiles: selectedFile ? [selectedFile] : [], + shell: 'bash', + }); + + // In a real implementation, we would handle streaming responses here + // For now, we'll just add a placeholder response + setChatHistory([ + ...updatedHistory, + { + role: 'assistant', + content: 'I received your message. In a real implementation, this would be a streaming response from the Sidecar API.' + } + ]); + } catch (error) { + console.error('Error sending message:', error); + setChatHistory([ + ...updatedHistory, + { + role: 'assistant', + content: 'Error: Could not connect to server' + } + ]); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Chat

+ +
+ +
+ {chatHistory.map((msg, index) => ( +
+
+ {msg.content} +
+
+ ))} + {loading && ( +
+
+ Thinking... +
+
+ )} +
+ +
+
+ setMessage(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} + placeholder="Ask Sidecar..." + className="flex-grow bg-gray-800 text-white p-2 rounded-l-md focus:outline-none" + disabled={loading} + /> + +
+
+
+ ); +}; + +export default ChatInterface; \ No newline at end of file diff --git a/frontend/components/CodeEditor.tsx b/frontend/components/CodeEditor.tsx new file mode 100644 index 000000000..ebc3d2a1b --- /dev/null +++ b/frontend/components/CodeEditor.tsx @@ -0,0 +1,158 @@ +import dynamic from 'next/dynamic'; +import { javascript } from '@codemirror/lang-javascript'; +import { python } from '@codemirror/lang-python'; +import { vscodeDark } from '@uiw/codemirror-theme-vscode'; +import { readFile, editFile } from '@/services/api'; +import { useEffect, useState } from 'react'; + +// Dynamically import CodeMirror to avoid SSR issues +const CodeMirror = dynamic( + () => import('@uiw/react-codemirror').then((mod) => mod.default), + { ssr: false } +); + +interface CodeEditorProps { + selectedFile: string | null; +} + +const CodeEditor: React.FC = ({ selectedFile }) => { + const [fileContent, setFileContent] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [saved, setSaved] = useState(true); + + useEffect(() => { + const fetchFileContent = async () => { + if (!selectedFile) { + setFileContent(''); + return; + } + + setLoading(true); + setError(null); + try { + const response = await readFile(selectedFile); + setFileContent(response.content || ''); + setSaved(true); + } catch (err) { + console.error('Error fetching file content:', err); + setError('Failed to load file content'); + } finally { + setLoading(false); + } + }; + + fetchFileContent(); + }, [selectedFile]); + + const handleCodeChange = (value: string) => { + setFileContent(value); + setSaved(false); + }; + + const handleSaveFile = async () => { + if (!selectedFile) return; + + setLoading(true); + setError(null); + try { + await editFile(selectedFile, fileContent); + setSaved(true); + } catch (err) { + console.error('Error saving file:', err); + setError('Failed to save file'); + } finally { + setLoading(false); + } + }; + + // Get language support based on file extension + const getLanguageExtension = () => { + if (!selectedFile) return javascript(); + + if (selectedFile.endsWith('.js') || selectedFile.endsWith('.jsx') || + selectedFile.endsWith('.ts') || selectedFile.endsWith('.tsx')) { + return javascript(); + } else if (selectedFile.endsWith('.py')) { + return python(); + } + + return javascript(); // Default to JavaScript + }; + + return ( +
+
+

+ {selectedFile || 'No file selected'} + {!saved && *} +

+ {selectedFile && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} + +
+ {selectedFile ? ( + + ) : ( +
+ Select a file to edit +
+ )} +
+
+ ); +}; + +export default CodeEditor; \ No newline at end of file diff --git a/frontend/components/FileExplorer.tsx b/frontend/components/FileExplorer.tsx new file mode 100644 index 000000000..8e1605e41 --- /dev/null +++ b/frontend/components/FileExplorer.tsx @@ -0,0 +1,88 @@ +import { useState, useEffect } from 'react'; +import { FiFolder, FiFile, FiRefreshCw } from 'react-icons/fi'; +import { listFiles } from '@/services/api'; + +interface FileExplorerProps { + onFileSelect: (file: string) => void; + selectedFile: string | null; +} + +const FileExplorer: React.FC = ({ onFileSelect, selectedFile }) => { + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchFiles = async () => { + setLoading(true); + setError(null); + try { + const response = await listFiles('.'); + const fileList = response.files || []; + setFiles(fileList); + + // Notify parent component about loaded files + if (onFilesLoaded) { + onFilesLoaded(fileList); + } + } catch (err) { + console.error('Error fetching files:', err); + setError('Failed to load files'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchFiles(); + }, []); + + return ( +
+
+

Files

+ +
+ + {error && ( +
+ {error} +
+ )} + +
+
    + {files.map((file, index) => ( +
  • onFileSelect(file)} + > + {file.includes('.') ? : } + {file} +
  • + ))} + + {files.length === 0 && !loading && ( +
  • + No files found +
  • + )} + + {loading && files.length === 0 && ( +
  • + Loading... +
  • + )} +
+
+
+ ); +}; + +export default FileExplorer; \ No newline at end of file diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 000000000..a34014a25 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,14 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'http://localhost:3000/api/:path*', // Proxy to sidecar API + }, + ]; + }, +}; + +module.exports = nextConfig; \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..98347f224 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,41 @@ +{ + "name": "sidecar-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@codemirror/autocomplete": "^6.11.1", + "@codemirror/commands": "^6.3.0", + "@codemirror/lang-javascript": "^6.2.1", + "@codemirror/lang-python": "^6.1.3", + "@codemirror/language": "^6.9.3", + "@codemirror/state": "^6.3.3", + "@codemirror/view": "^6.22.1", + "@uiw/codemirror-theme-vscode": "^4.21.21", + "@uiw/react-codemirror": "^4.21.21", + "axios": "^1.6.2", + "next": "14.0.3", + "react": "^18", + "react-dom": "^18", + "react-icons": "^4.12.0", + "react-split": "^2.0.14", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-types": "^3.17.5" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.0.3", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "typescript": "^5" + } +} \ No newline at end of file diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx new file mode 100644 index 000000000..3d5b661a2 --- /dev/null +++ b/frontend/pages/_app.tsx @@ -0,0 +1,6 @@ +import '@/styles/globals.css'; +import type { AppProps } from 'next/app'; + +export default function App({ Component, pageProps }: AppProps) { + return ; +} \ No newline at end of file diff --git a/frontend/pages/api/agentic/agent_tool_use.ts b/frontend/pages/api/agentic/agent_tool_use.ts new file mode 100644 index 000000000..05dbe916b --- /dev/null +++ b/frontend/pages/api/agentic/agent_tool_use.ts @@ -0,0 +1,41 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; + +// This is a simple proxy to the sidecar API +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { method } = req; + + // Set the sidecar API URL + const SIDECAR_API_URL = process.env.SIDECAR_API_URL || 'http://localhost:3000/api'; + + try { + if (method === 'POST') { + // Forward the request to the sidecar API + const response = await axios.post( + `${SIDECAR_API_URL}/agentic/agent_tool_use`, + req.body, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + return res.status(200).json(response.data); + } + + return res.status(405).json({ error: 'Method not allowed' }); + } catch (error) { + console.error('API route error:', error); + + // If the error is from axios, return the error response + if (axios.isAxiosError(error) && error.response) { + return res.status(error.response.status).json(error.response.data); + } + + return res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/frontend/pages/api/file.ts b/frontend/pages/api/file.ts new file mode 100644 index 000000000..6e9acc89a --- /dev/null +++ b/frontend/pages/api/file.ts @@ -0,0 +1,55 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; + +// This is a simple proxy to the sidecar API +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { method } = req; + + // Set the sidecar API URL + const SIDECAR_API_URL = process.env.SIDECAR_API_URL || 'http://localhost:3000/api'; + + try { + if (method === 'GET') { + // Handle GET requests (list files, read file) + const { directory_path, fs_file_path } = req.query; + + if (directory_path) { + // List files in directory + const response = await axios.get(`${SIDECAR_API_URL}/file`, { + params: { directory_path } + }); + return res.status(200).json(response.data); + } else if (fs_file_path) { + // Read file content + const response = await axios.get(`${SIDECAR_API_URL}/file`, { + params: { fs_file_path } + }); + return res.status(200).json(response.data); + } + + return res.status(400).json({ error: 'Missing required parameters' }); + } else if (method === 'POST') { + // Handle POST requests (edit file) + const { fs_file_path, content } = req.body; + + if (!fs_file_path || content === undefined) { + return res.status(400).json({ error: 'Missing required parameters' }); + } + + const response = await axios.post(`${SIDECAR_API_URL}/file/edit_file`, { + fs_file_path, + content + }); + + return res.status(200).json(response.data); + } + + return res.status(405).json({ error: 'Method not allowed' }); + } catch (error) { + console.error('API route error:', error); + return res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/frontend/pages/api/health.ts b/frontend/pages/api/health.ts new file mode 100644 index 000000000..8f8fc191d --- /dev/null +++ b/frontend/pages/api/health.ts @@ -0,0 +1,40 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; + +// This is a simple proxy to the sidecar API health endpoint +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { method } = req; + + // Set the sidecar API URL + const SIDECAR_API_URL = process.env.SIDECAR_API_URL || 'http://localhost:3000/api'; + + try { + if (method === 'GET') { + // Forward the request to the sidecar API + const response = await axios.get(`${SIDECAR_API_URL}/health`); + return res.status(200).json(response.data); + } + + return res.status(405).json({ error: 'Method not allowed' }); + } catch (error) { + console.error('API route error:', error); + + // If the error is from axios, return the error response + if (axios.isAxiosError(error) && error.response) { + return res.status(error.response.status).json(error.response.data); + } + + // If the error is a connection error, return a custom message + if (axios.isAxiosError(error) && error.code === 'ECONNREFUSED') { + return res.status(503).json({ + error: 'Sidecar API is not available', + message: 'Make sure the sidecar webserver is running on the configured port' + }); + } + + return res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/frontend/pages/api/lsp/[language].ts b/frontend/pages/api/lsp/[language].ts new file mode 100644 index 000000000..6a4c8b4ad --- /dev/null +++ b/frontend/pages/api/lsp/[language].ts @@ -0,0 +1,49 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; + +// This is a proxy to the language server +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { method } = req; + const { language } = req.query; + + // Set the language server URL based on the language + let languageServerUrl = ''; + + switch (language) { + case 'typescript': + languageServerUrl = process.env.TS_LANGUAGE_SERVER_URL || 'http://localhost:3001'; + break; + case 'python': + languageServerUrl = process.env.PYTHON_LANGUAGE_SERVER_URL || 'http://localhost:3002'; + break; + default: + return res.status(400).json({ error: `Unsupported language: ${language}` }); + } + + try { + if (method === 'POST') { + // Forward the LSP request to the language server + const response = await axios.post(languageServerUrl, req.body, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + return res.status(200).json(response.data); + } + + return res.status(405).json({ error: 'Method not allowed' }); + } catch (error) { + console.error(`LSP ${language} API route error:`, error); + + // If the error is from axios, return the error response + if (axios.isAxiosError(error) && error.response) { + return res.status(error.response.status).json(error.response.data); + } + + return res.status(500).json({ error: 'Internal server error' }); + } +} \ No newline at end of file diff --git a/frontend/pages/api/lsp/connect.ts b/frontend/pages/api/lsp/connect.ts new file mode 100644 index 000000000..4683a3f87 --- /dev/null +++ b/frontend/pages/api/lsp/connect.ts @@ -0,0 +1,40 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; + +// This endpoint establishes a connection to the language server +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { method } = req; + const { language, rootPath } = req.body; + + // Set the sidecar API URL + const SIDECAR_API_URL = process.env.SIDECAR_API_URL || 'http://localhost:3000/api'; + + try { + if (method === 'POST') { + // Forward the LSP connection request to the sidecar API + const response = await axios.post(`${SIDECAR_API_URL}/lsp/connect`, { + language, + rootPath, + }); + + return res.status(200).json(response.data); + } + + return res.status(405).json({ error: 'Method not allowed' }); + } catch (error) { + console.error('LSP connect API route error:', error); + + // If the error is from axios, return the error response + if (axios.isAxiosError(error) && error.response) { + return res.status(error.response.status).json(error.response.data); + } + + return res.status(500).json({ + error: 'Internal server error', + message: 'Could not connect to language server. Make sure the Sidecar API is running.' + }); + } +} \ No newline at end of file diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx new file mode 100644 index 000000000..3dd7d348a --- /dev/null +++ b/frontend/pages/index.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import Head from 'next/head'; +import Split from 'react-split'; +import FileExplorer from '@/components/FileExplorer'; +import CodeEditor from '@/components/CodeEditor'; +import ChatInterface from '@/components/ChatInterface'; + +export default function Home() { + const [selectedFile, setSelectedFile] = useState(null); + const [files, setFiles] = useState([]); + + const handleFileSelect = (file: string) => { + setSelectedFile(file); + }; + + return ( + <> + + Sidecar Frontend + + + + + +
+
+

Sidecar

+
+ +
+ + {/* File Explorer */} +
+ +
+ + {/* Code Editor */} +
+ +
+ + {/* Chat Interface */} +
+ +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 000000000..8567b4c40 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/frontend/services/api.ts b/frontend/services/api.ts new file mode 100644 index 000000000..e94a16d45 --- /dev/null +++ b/frontend/services/api.ts @@ -0,0 +1,185 @@ +import axios from 'axios'; + +const API_BASE_URL = '/api'; + +// File operations +export const listFiles = async (directoryPath: string) => { + try { + const response = await axios.get(`${API_BASE_URL}/file`, { + params: { directory_path: directoryPath } + }); + return response.data; + } catch (error) { + console.error('Error listing files:', error); + throw error; + } +}; + +export const readFile = async (filePath: string) => { + try { + const response = await axios.get(`${API_BASE_URL}/file`, { + params: { fs_file_path: filePath } + }); + return response.data; + } catch (error) { + console.error('Error reading file:', error); + throw error; + } +}; + +export const editFile = async (filePath: string, content: string) => { + try { + const response = await axios.post(`${API_BASE_URL}/file/edit_file`, { + fs_file_path: filePath, + content + }); + return response.data; + } catch (error) { + console.error('Error editing file:', error); + throw error; + } +}; + +// Agent operations with streaming support +export const sendAgentMessage = async ( + params: { + sessionId: string; + exchangeId: string; + editorUrl: string; + query: string; + userContext: { + visibleFiles: string[]; + openFiles: string[]; + }; + repoRef: { + name: string; + url: string; + }; + projectLabels: string[]; + rootDirectory: string; + accessToken: string; + modelConfiguration: { + fastModel: string; + slowModel: string; + }; + allFiles: string[]; + openFiles: string[]; + shell: string; + }, + onMessageChunk?: (chunk: string) => void +) => { + try { + // If streaming is requested, use fetch with streaming + if (onMessageChunk) { + const response = await fetch(`${API_BASE_URL}/agentic/agent_tool_use`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + session_id: params.sessionId, + exchange_id: params.exchangeId, + editor_url: params.editorUrl, + query: params.query, + user_context: { + visible_files: params.userContext.visibleFiles, + open_files: params.userContext.openFiles, + }, + repo_ref: params.repoRef, + project_labels: params.projectLabels, + root_directory: params.rootDirectory, + access_token: params.accessToken, + model_configuration: { + fast_model: params.modelConfiguration.fastModel, + slow_model: params.modelConfiguration.slowModel, + }, + all_files: params.allFiles, + open_files: params.openFiles, + shell: params.shell, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (!response.body) { + throw new Error('Response body is null'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + // Read the stream + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + onMessageChunk(chunk); + } + + return { success: true }; + } else { + // If no streaming is requested, use axios as before + const response = await axios.post(`${API_BASE_URL}/agentic/agent_tool_use`, { + session_id: params.sessionId, + exchange_id: params.exchangeId, + editor_url: params.editorUrl, + query: params.query, + user_context: { + visible_files: params.userContext.visibleFiles, + open_files: params.userContext.openFiles, + }, + repo_ref: params.repoRef, + project_labels: params.projectLabels, + root_directory: params.rootDirectory, + access_token: params.accessToken, + model_configuration: { + fast_model: params.modelConfiguration.fastModel, + slow_model: params.modelConfiguration.slowModel, + }, + all_files: params.allFiles, + open_files: params.openFiles, + shell: params.shell, + }); + return response.data; + } + } catch (error) { + console.error('Error sending agent message:', error); + throw error; + } +}; + +// Health check +export const checkHealth = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/health`); + return response.data; + } catch (error) { + console.error('Error checking health:', error); + throw error; + } +}; + +// Config +export const getConfig = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/config`); + return response.data; + } catch (error) { + console.error('Error getting config:', error); + throw error; + } +}; + +// Version +export const getVersion = async () => { + try { + const response = await axios.get(`${API_BASE_URL}/version`); + return response.data; + } catch (error) { + console.error('Error getting version:', error); + throw error; + } +}; \ No newline at end of file diff --git a/frontend/services/lsp.ts b/frontend/services/lsp.ts new file mode 100644 index 000000000..2b7b56f9b --- /dev/null +++ b/frontend/services/lsp.ts @@ -0,0 +1,126 @@ +import { LanguageServerManager } from '@codemirror/lsp'; +import { Diagnostic } from 'vscode-languageserver-types'; + +// Define the LSP client interface +export interface LSPClient { + connect(): Promise; + disconnect(): void; + getDiagnostics(uri: string): Diagnostic[]; + isConnected(): boolean; +} + +// Create a class to manage LSP connections +export class LSPManager { + private static instance: LSPManager; + private servers: Map = new Map(); + private diagnostics: Map = new Map(); + + private constructor() {} + + public static getInstance(): LSPManager { + if (!LSPManager.instance) { + LSPManager.instance = new LSPManager(); + } + return LSPManager.instance; + } + + // Get or create a language server for a specific language + public getServer(language: string): LanguageServerManager | null { + if (this.servers.has(language)) { + return this.servers.get(language) || null; + } + + // Create a new language server based on the language + let server: LanguageServerManager | null = null; + + switch (language) { + case 'javascript': + case 'typescript': + server = this.createTypeScriptServer(); + break; + case 'python': + server = this.createPythonServer(); + break; + // Add more language servers as needed + default: + return null; + } + + if (server) { + this.servers.set(language, server); + } + + return server; + } + + // Create a TypeScript language server + private createTypeScriptServer(): LanguageServerManager { + const serverOptions = { + serverUri: '/api/lsp/typescript', + workspaceFolders: [{ name: 'root', uri: 'file:///' }], + documentSelector: [{ language: 'typescript' }, { language: 'javascript' }], + }; + + const server = new LanguageServerManager(serverOptions); + + // Listen for diagnostic events + server.on('diagnostics', (params) => { + this.diagnostics.set(params.uri, params.diagnostics); + }); + + return server; + } + + // Create a Python language server + private createPythonServer(): LanguageServerManager { + const serverOptions = { + serverUri: '/api/lsp/python', + workspaceFolders: [{ name: 'root', uri: 'file:///' }], + documentSelector: [{ language: 'python' }], + }; + + const server = new LanguageServerManager(serverOptions); + + // Listen for diagnostic events + server.on('diagnostics', (params) => { + this.diagnostics.set(params.uri, params.diagnostics); + }); + + return server; + } + + // Get diagnostics for a specific file + public getDiagnostics(uri: string): Diagnostic[] { + return this.diagnostics.get(uri) || []; + } + + // Determine language from file extension + public static getLanguageFromFilePath(filePath: string): string { + if (!filePath) return ''; + + if (filePath.endsWith('.js')) return 'javascript'; + if (filePath.endsWith('.jsx')) return 'javascript'; + if (filePath.endsWith('.ts')) return 'typescript'; + if (filePath.endsWith('.tsx')) return 'typescript'; + if (filePath.endsWith('.py')) return 'python'; + if (filePath.endsWith('.html')) return 'html'; + if (filePath.endsWith('.css')) return 'css'; + if (filePath.endsWith('.json')) return 'json'; + if (filePath.endsWith('.md')) return 'markdown'; + if (filePath.endsWith('.java')) return 'java'; + if (filePath.endsWith('.c')) return 'c'; + if (filePath.endsWith('.cpp')) return 'cpp'; + if (filePath.endsWith('.h')) return 'c'; + if (filePath.endsWith('.hpp')) return 'cpp'; + if (filePath.endsWith('.go')) return 'go'; + if (filePath.endsWith('.rs')) return 'rust'; + if (filePath.endsWith('.rb')) return 'ruby'; + if (filePath.endsWith('.php')) return 'php'; + + // Default to javascript + return 'javascript'; + } +} + +// Export a singleton instance +export const lspManager = LSPManager.getInstance(); \ No newline at end of file diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css new file mode 100644 index 000000000..a18ebe650 --- /dev/null +++ b/frontend/styles/globals.css @@ -0,0 +1,36 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 255, 255, 255; + --background-rgb: 30, 30, 30; +} + +body { + color: rgb(var(--foreground-rgb)); + background: rgb(var(--background-rgb)); +} + +.cm-editor { + height: 100%; + min-height: 300px; + border: 1px solid #333; + border-radius: 4px; +} + +.split { + display: flex; + flex-direction: row; +} + +.gutter { + background-color: #333; + background-repeat: no-repeat; + background-position: 50%; +} + +.gutter.gutter-horizontal { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); + cursor: col-resize; +} \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 000000000..2dbe10ba7 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,20 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + './app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + primary: '#4A90E2', + secondary: '#F5A623', + background: '#1E1E1E', + foreground: '#FFFFFF', + border: '#333333', + }, + }, + }, + plugins: [], +}; \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 000000000..9b9948d58 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} \ No newline at end of file