mirror of
https://github.com/cdr/code-server.git
synced 2026-05-09 04:50:49 +02:00
feat: Set up fullstack Cursor IDE with backend and frontend
Co-authored-by: logato7838 <logato7838@vsihay.com>
This commit is contained in:
parent
cd40509fbb
commit
05acbfff1e
18 changed files with 913 additions and 0 deletions
51
cursor-fullstack/docker-compose.yml
Normal file
51
cursor-fullstack/docker-compose.yml
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./packages/backend/claudable
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3001
|
||||||
|
- WS_PORT=8080
|
||||||
|
volumes:
|
||||||
|
- ./workspace:/app/workspace
|
||||||
|
networks:
|
||||||
|
- cursor-network
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./packages/frontend/cursor-web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
environment:
|
||||||
|
- VITE_BACKEND_URL=http://localhost:3001
|
||||||
|
- VITE_WS_URL=ws://localhost:8080
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- cursor-network
|
||||||
|
|
||||||
|
code-server:
|
||||||
|
image: codercom/code-server:latest
|
||||||
|
ports:
|
||||||
|
- "8081:8080"
|
||||||
|
environment:
|
||||||
|
- PASSWORD=cursor123
|
||||||
|
volumes:
|
||||||
|
- ./workspace:/home/coder/workspace
|
||||||
|
command: --bind-addr 0.0.0.0:8080 --auth password --disable-telemetry
|
||||||
|
networks:
|
||||||
|
- cursor-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
cursor-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
workspace:
|
||||||
30
cursor-fullstack/packages/backend/claudable/Dockerfile
Normal file
30
cursor-fullstack/packages/backend/claudable/Dockerfile
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
FROM oven/bun:1 as base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY package.json bun.lockb* ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM oven/bun:1-slim
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy built application
|
||||||
|
COPY --from=base /app/dist ./dist
|
||||||
|
COPY --from=base /app/node_modules ./node_modules
|
||||||
|
COPY --from=base /app/package.json ./
|
||||||
|
|
||||||
|
# Create workspace directory
|
||||||
|
RUN mkdir -p /app/workspace
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 3001 8080
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["bun", "run", "start"]
|
||||||
27
cursor-fullstack/packages/backend/claudable/package.json
Normal file
27
cursor-fullstack/packages/backend/claudable/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "cursor-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Cursor Full Stack AI IDE Backend",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --hot src/index.ts",
|
||||||
|
"build": "bun build src/index.ts --outdir dist --target bun",
|
||||||
|
"start": "bun run dist/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.24.3",
|
||||||
|
"@google/generative-ai": "^0.2.1",
|
||||||
|
"bun-types": "latest",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"openai": "^4.20.1",
|
||||||
|
"socket.io": "^4.7.4",
|
||||||
|
"ws": "^8.14.2",
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/ws": "^8.5.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
import Anthropic from '@anthropic-ai/sdk';
|
||||||
|
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||||
|
|
||||||
|
interface AIProvider {
|
||||||
|
chat: (message: string, apiKey: string, model?: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openaiProvider: AIProvider = {
|
||||||
|
async chat(message: string, apiKey: string, model = 'gpt-4') {
|
||||||
|
const openai = new OpenAI({ apiKey });
|
||||||
|
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
messages: [{ role: 'user', content: message }],
|
||||||
|
model: model,
|
||||||
|
stream: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return completion.choices[0]?.message?.content || 'No response generated';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const anthropicProvider: AIProvider = {
|
||||||
|
async chat(message: string, apiKey: string, model = 'claude-3-sonnet-20240229') {
|
||||||
|
const anthropic = new Anthropic({ apiKey });
|
||||||
|
|
||||||
|
const response = await anthropic.messages.create({
|
||||||
|
model: model,
|
||||||
|
max_tokens: 1000,
|
||||||
|
messages: [{ role: 'user', content: message }]
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.content[0]?.type === 'text' ? response.content[0].text : 'No response generated';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const googleProvider: AIProvider = {
|
||||||
|
async chat(message: string, apiKey: string, model = 'gemini-pro') {
|
||||||
|
const genAI = new GoogleGenerativeAI(apiKey);
|
||||||
|
const model_instance = genAI.getGenerativeModel({ model: model });
|
||||||
|
|
||||||
|
const result = await model_instance.generateContent(message);
|
||||||
|
const response = await result.response;
|
||||||
|
|
||||||
|
return response.text() || 'No response generated';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mistralProvider: AIProvider = {
|
||||||
|
async chat(message: string, apiKey: string, model = 'mistral-large-latest') {
|
||||||
|
// Using OpenAI-compatible API for Mistral
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: apiKey,
|
||||||
|
baseURL: 'https://api.mistral.ai/v1'
|
||||||
|
});
|
||||||
|
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
messages: [{ role: 'user', content: message }],
|
||||||
|
model: model,
|
||||||
|
stream: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return completion.choices[0]?.message?.content || 'No response generated';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const aiProviders: Record<string, AIProvider> = {
|
||||||
|
openai: openaiProvider,
|
||||||
|
anthropic: anthropicProvider,
|
||||||
|
google: googleProvider,
|
||||||
|
mistral: mistralProvider
|
||||||
|
};
|
||||||
68
cursor-fullstack/packages/backend/claudable/src/index.ts
Normal file
68
cursor-fullstack/packages/backend/claudable/src/index.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import { Server as SocketIOServer } from 'socket.io';
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import { sessionRoutes } from './routes/session';
|
||||||
|
import { setupWebSocket } from './ws';
|
||||||
|
import { aiProviders } from './ai/providers';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const server = createServer(app);
|
||||||
|
const io = new SocketIOServer(server, {
|
||||||
|
cors: {
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocket server for real-time communication
|
||||||
|
const wss = new WebSocketServer({ port: 8080 });
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/api', sessionRoutes);
|
||||||
|
|
||||||
|
// AI Providers endpoint
|
||||||
|
app.get('/api/providers', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
providers: [
|
||||||
|
{ id: 'openai', name: 'OpenAI', models: ['gpt-4', 'gpt-3.5-turbo'] },
|
||||||
|
{ id: 'anthropic', name: 'Anthropic', models: ['claude-3-sonnet', 'claude-3-haiku'] },
|
||||||
|
{ id: 'google', name: 'Google Gemini', models: ['gemini-pro', 'gemini-pro-vision'] },
|
||||||
|
{ id: 'mistral', name: 'Mistral', models: ['mistral-large', 'mistral-medium'] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chat endpoint
|
||||||
|
app.post('/api/chat', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { message, provider, apiKey, model } = req.body;
|
||||||
|
|
||||||
|
if (!message || !provider || !apiKey) {
|
||||||
|
return res.status(400).json({ error: 'Missing required fields' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await aiProviders[provider].chat(message, apiKey, model);
|
||||||
|
res.json({ response });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chat error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to process chat request' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup WebSocket handlers
|
||||||
|
setupWebSocket(io, wss);
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Backend server running on port ${PORT}`);
|
||||||
|
console.log(`🔌 WebSocket server running on port 8080`);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { app, server, io, wss };
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Start code-server session
|
||||||
|
router.post('/session/start', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { workspace } = req.body;
|
||||||
|
const workspacePath = workspace || '/app/workspace';
|
||||||
|
|
||||||
|
// Start code-server process
|
||||||
|
const codeServer = spawn('code-server', [
|
||||||
|
'--bind-addr', '0.0.0.0:8080',
|
||||||
|
'--auth', 'none',
|
||||||
|
'--disable-telemetry',
|
||||||
|
workspacePath
|
||||||
|
], {
|
||||||
|
stdio: 'pipe',
|
||||||
|
detached: true
|
||||||
|
});
|
||||||
|
|
||||||
|
codeServer.stdout?.on('data', (data) => {
|
||||||
|
console.log(`code-server: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
codeServer.stderr?.on('data', (data) => {
|
||||||
|
console.error(`code-server error: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
codeServer.on('close', (code) => {
|
||||||
|
console.log(`code-server process exited with code ${code}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Code server started',
|
||||||
|
url: 'http://localhost:8081'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start code-server:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to start code-server' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get workspace files
|
||||||
|
router.get('/workspace/files', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
const workspacePath = '/app/workspace';
|
||||||
|
|
||||||
|
const files = await getWorkspaceFiles(workspacePath);
|
||||||
|
res.json({ files });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get workspace files:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to get workspace files' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get file content
|
||||||
|
router.get('/workspace/file/:filepath(*)', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
const filePath = path.join('/app/workspace', req.params.filepath);
|
||||||
|
|
||||||
|
const content = await fs.readFile(filePath, 'utf-8');
|
||||||
|
res.json({ content });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to read file:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to read file' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save file content
|
||||||
|
router.post('/workspace/file/:filepath(*)', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
const filePath = path.join('/app/workspace', req.params.filepath);
|
||||||
|
const { content } = req.body;
|
||||||
|
|
||||||
|
await fs.writeFile(filePath, content, 'utf-8');
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save file:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to save file' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getWorkspaceFiles(dir: string, basePath = ''): Promise<any[]> {
|
||||||
|
const fs = await import('fs/promises');
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
const relativePath = path.join(basePath, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
||||||
|
const subFiles = await getWorkspaceFiles(fullPath, relativePath);
|
||||||
|
files.push(...subFiles);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
files.push({
|
||||||
|
name: entry.name,
|
||||||
|
path: relativePath,
|
||||||
|
type: 'file',
|
||||||
|
extension: path.extname(entry.name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading directory:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { router as sessionRoutes };
|
||||||
95
cursor-fullstack/packages/backend/claudable/src/ws.ts
Normal file
95
cursor-fullstack/packages/backend/claudable/src/ws.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { Server as SocketIOServer } from 'socket.io';
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import { aiProviders } from './ai/providers';
|
||||||
|
|
||||||
|
export function setupWebSocket(io: SocketIOServer, wss: WebSocketServer) {
|
||||||
|
// Socket.IO for real-time communication
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
console.log('Client connected via Socket.IO:', socket.id);
|
||||||
|
|
||||||
|
socket.on('chat-message', async (data) => {
|
||||||
|
try {
|
||||||
|
const { message, provider, apiKey, model } = data;
|
||||||
|
|
||||||
|
if (!message || !provider || !apiKey) {
|
||||||
|
socket.emit('chat-error', { error: 'Missing required fields' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send typing indicator
|
||||||
|
socket.emit('typing-start');
|
||||||
|
|
||||||
|
// Get AI response
|
||||||
|
const response = await aiProviders[provider].chat(message, apiKey, model);
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
socket.emit('chat-response', {
|
||||||
|
response,
|
||||||
|
provider,
|
||||||
|
model
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop typing indicator
|
||||||
|
socket.emit('typing-stop');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket chat error:', error);
|
||||||
|
socket.emit('chat-error', { error: 'Failed to process chat request' });
|
||||||
|
socket.emit('typing-stop');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('Client disconnected:', socket.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Native WebSocket for additional real-time features
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
console.log('Client connected via WebSocket');
|
||||||
|
|
||||||
|
ws.on('message', async (data) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(data.toString());
|
||||||
|
|
||||||
|
if (message.type === 'chat') {
|
||||||
|
const { content, provider, apiKey, model } = message;
|
||||||
|
|
||||||
|
if (!content || !provider || !apiKey) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
error: 'Missing required fields'
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send typing indicator
|
||||||
|
ws.send(JSON.stringify({ type: 'typing-start' }));
|
||||||
|
|
||||||
|
// Get AI response
|
||||||
|
const response = await aiProviders[provider].chat(content, apiKey, model);
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'chat-response',
|
||||||
|
response,
|
||||||
|
provider,
|
||||||
|
model
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Stop typing indicator
|
||||||
|
ws.send(JSON.stringify({ type: 'typing-stop' }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('WebSocket message error:', error);
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
error: 'Failed to process message'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.log('WebSocket client disconnected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
28
cursor-fullstack/packages/frontend/cursor-web/Dockerfile
Normal file
28
cursor-fullstack/packages/frontend/cursor-web/Dockerfile
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
FROM oven/bun:1 as base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY package.json bun.lockb* ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy built application
|
||||||
|
COPY --from=base /app/dist .
|
||||||
|
|
||||||
|
# Copy nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 5173
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
13
cursor-fullstack/packages/frontend/cursor-web/index.html
Normal file
13
cursor-fullstack/packages/frontend/cursor-web/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Cursor Full Stack AI IDE</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
cursor-fullstack/packages/frontend/cursor-web/nginx.conf
Normal file
33
cursor-fullstack/packages/frontend/cursor-web/nginx.conf
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
server {
|
||||||
|
listen 5173;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://backend:3001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /socket.io/ {
|
||||||
|
proxy_pass http://backend:3001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
cursor-fullstack/packages/frontend/cursor-web/package.json
Normal file
28
cursor-fullstack/packages/frontend/cursor-web/package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "cursor-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run vite",
|
||||||
|
"build": "bun run vite build",
|
||||||
|
"preview": "bun run vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
|
"lucide-react": "^0.294.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"socket.io-client": "^4.7.4",
|
||||||
|
"tailwindcss": "^3.3.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
95
cursor-fullstack/packages/frontend/cursor-web/src/App.tsx
Normal file
95
cursor-fullstack/packages/frontend/cursor-web/src/App.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Sidebar } from './components/Sidebar';
|
||||||
|
import { EditorPanel } from './components/EditorPanel';
|
||||||
|
import { ChatAssistant } from './components/ChatAssistant';
|
||||||
|
import { ProviderForm } from './components/ProviderForm';
|
||||||
|
import { io } from 'socket.io-client';
|
||||||
|
|
||||||
|
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001';
|
||||||
|
const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:8080';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||||
|
const [files, setFiles] = useState<any[]>([]);
|
||||||
|
const [showChat, setShowChat] = useState(false);
|
||||||
|
const [showProviderForm, setShowProviderForm] = useState(false);
|
||||||
|
const [socket, setSocket] = useState<any>(null);
|
||||||
|
const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState<string>('openai');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize Socket.IO connection
|
||||||
|
const newSocket = io(BACKEND_URL);
|
||||||
|
setSocket(newSocket);
|
||||||
|
|
||||||
|
// Load workspace files
|
||||||
|
loadWorkspaceFiles();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
newSocket.close();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadWorkspaceFiles = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/api/workspace/files`);
|
||||||
|
const data = await response.json();
|
||||||
|
setFiles(data.files || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load workspace files:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (filePath: string) => {
|
||||||
|
setSelectedFile(filePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApiKeySave = (provider: string, apiKey: string) => {
|
||||||
|
setApiKeys(prev => ({ ...prev, [provider]: apiKey }));
|
||||||
|
setShowProviderForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-cursor-bg text-cursor-text">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<Sidebar
|
||||||
|
files={files}
|
||||||
|
selectedFile={selectedFile}
|
||||||
|
onFileSelect={handleFileSelect}
|
||||||
|
onShowChat={() => setShowChat(!showChat)}
|
||||||
|
onShowProviderForm={() => setShowProviderForm(true)}
|
||||||
|
showChat={showChat}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{/* Editor Panel */}
|
||||||
|
<EditorPanel
|
||||||
|
selectedFile={selectedFile}
|
||||||
|
onFileChange={loadWorkspaceFiles}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Chat Assistant */}
|
||||||
|
{showChat && (
|
||||||
|
<ChatAssistant
|
||||||
|
socket={socket}
|
||||||
|
apiKeys={apiKeys}
|
||||||
|
selectedProvider={selectedProvider}
|
||||||
|
onProviderChange={setSelectedProvider}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Provider Form Modal */}
|
||||||
|
{showProviderForm && (
|
||||||
|
<ProviderForm
|
||||||
|
onSave={handleApiKeySave}
|
||||||
|
onClose={() => setShowProviderForm(false)}
|
||||||
|
existingKeys={apiKeys}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
MessageSquare,
|
||||||
|
Settings,
|
||||||
|
File,
|
||||||
|
Folder,
|
||||||
|
FolderOpen,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronDown
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
files: any[];
|
||||||
|
selectedFile: string | null;
|
||||||
|
onFileSelect: (filePath: string) => void;
|
||||||
|
onShowChat: () => void;
|
||||||
|
onShowProviderForm: () => void;
|
||||||
|
showChat: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar: React.FC<SidebarProps> = ({
|
||||||
|
files,
|
||||||
|
selectedFile,
|
||||||
|
onFileSelect,
|
||||||
|
onShowChat,
|
||||||
|
onShowProviderForm,
|
||||||
|
showChat
|
||||||
|
}) => {
|
||||||
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const toggleFolder = (folderPath: string) => {
|
||||||
|
const newExpanded = new Set(expandedFolders);
|
||||||
|
if (newExpanded.has(folderPath)) {
|
||||||
|
newExpanded.delete(folderPath);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(folderPath);
|
||||||
|
}
|
||||||
|
setExpandedFolders(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFileTree = (files: any[], level = 0) => {
|
||||||
|
const groupedFiles = files.reduce((acc, file) => {
|
||||||
|
const pathParts = file.path.split('/');
|
||||||
|
const folder = pathParts.slice(0, -1).join('/');
|
||||||
|
|
||||||
|
if (!acc[folder]) {
|
||||||
|
acc[folder] = [];
|
||||||
|
}
|
||||||
|
acc[folder].push(file);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, any[]>);
|
||||||
|
|
||||||
|
return Object.entries(groupedFiles).map(([folder, folderFiles]) => {
|
||||||
|
const isExpanded = expandedFolders.has(folder);
|
||||||
|
const hasSubfolders = folderFiles.some(f => f.type === 'directory');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={folder} className="select-none">
|
||||||
|
{folder && (
|
||||||
|
<div
|
||||||
|
className="flex items-center py-1 px-2 hover:bg-cursor-hover cursor-pointer"
|
||||||
|
style={{ paddingLeft: `${level * 12 + 8}px` }}
|
||||||
|
onClick={() => toggleFolder(folder)}
|
||||||
|
>
|
||||||
|
{hasSubfolders && (
|
||||||
|
<div className="w-4 h-4 flex items-center justify-center">
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasSubfolders && <div className="w-4" />}
|
||||||
|
<Folder className="w-4 h-4 mr-2" />
|
||||||
|
<span className="text-sm">{folder.split('/').pop() || 'Root'}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div>
|
||||||
|
{folderFiles.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.path}
|
||||||
|
className={`flex items-center py-1 px-2 hover:bg-cursor-hover cursor-pointer ${
|
||||||
|
selectedFile === file.path ? 'bg-cursor-accent' : ''
|
||||||
|
}`}
|
||||||
|
style={{ paddingLeft: `${(level + 1) * 12 + 8}px` }}
|
||||||
|
onClick={() => onFileSelect(file.path)}
|
||||||
|
>
|
||||||
|
<File className="w-4 h-4 mr-2" />
|
||||||
|
<span className="text-sm">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-64 bg-cursor-sidebar border-r border-cursor-border flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-4 border-b border-cursor-border">
|
||||||
|
<h1 className="text-lg font-semibold text-white">Cursor IDE</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<div className="p-2 border-b border-cursor-border">
|
||||||
|
<button
|
||||||
|
onClick={onShowChat}
|
||||||
|
className={`w-full flex items-center px-3 py-2 rounded text-sm transition-colors ${
|
||||||
|
showChat ? 'bg-cursor-accent text-white' : 'hover:bg-cursor-hover'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
|
AI Chat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Explorer */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="text-xs text-gray-400 mb-2 px-2">EXPLORER</div>
|
||||||
|
{renderFileTree(files)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Settings */}
|
||||||
|
<div className="p-2 border-t border-cursor-border">
|
||||||
|
<button
|
||||||
|
onClick={onShowProviderForm}
|
||||||
|
className="w-full flex items-center px-3 py-2 rounded text-sm hover:bg-cursor-hover transition-colors"
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
|
AI Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
48
cursor-fullstack/packages/frontend/cursor-web/src/index.css
Normal file
48
cursor-fullstack/packages/frontend/cursor-web/src/index.css
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #cccccc;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monaco-editor {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
10
cursor-fullstack/packages/frontend/cursor-web/src/main.tsx
Normal file
10
cursor-fullstack/packages/frontend/cursor-web/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
'cursor-bg': '#1e1e1e',
|
||||||
|
'cursor-sidebar': '#252526',
|
||||||
|
'cursor-border': '#3c3c3c',
|
||||||
|
'cursor-text': '#cccccc',
|
||||||
|
'cursor-accent': '#007acc',
|
||||||
|
'cursor-hover': '#2a2d2e',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
25
cursor-fullstack/packages/frontend/cursor-web/vite.config.ts
Normal file
25
cursor-fullstack/packages/frontend/cursor-web/vite.config.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3001',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/socket.io': {
|
||||||
|
target: 'http://localhost:3001',
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: false
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue