mirror of
https://github.com/cdr/code-server.git
synced 2026-04-26 08:51:45 +02:00
Refactor: Replace code-server with Monaco Editor for Cloudflare Pages
Co-authored-by: logato7838 <logato7838@vsihay.com>
This commit is contained in:
parent
8a328db75d
commit
fa8e83780e
12 changed files with 2173 additions and 4 deletions
215
cursor-fullstack/cloudflare/cloudflare-pages-fix.md
Normal file
215
cursor-fullstack/cloudflare/cloudflare-pages-fix.md
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
# 🔧 إصلاح مشكلة Cloudflare Pages - Code Server
|
||||||
|
|
||||||
|
## 🚨 المشكلة
|
||||||
|
code-server لا يمكن بناؤه على Cloudflare Pages بسبب:
|
||||||
|
- عدم توفر مكتبات النظام المطلوبة (GSSAPI)
|
||||||
|
- قيود بيئة البناء في Cloudflare Pages
|
||||||
|
- تعقيدات في التبعيات الأصلية
|
||||||
|
|
||||||
|
## ✅ الحل البديل
|
||||||
|
|
||||||
|
### 1. استخدام Monaco Editor مباشرة
|
||||||
|
بدلاً من code-server، سنستخدم Monaco Editor مباشرة في React:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Monaco Editor في React
|
||||||
|
import { Editor } from '@monaco-editor/react';
|
||||||
|
|
||||||
|
<Editor
|
||||||
|
height="100vh"
|
||||||
|
defaultLanguage="typescript"
|
||||||
|
defaultValue="// ابدأ الكتابة هنا..."
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
fontSize: 14,
|
||||||
|
minimap: { enabled: true },
|
||||||
|
wordWrap: 'on',
|
||||||
|
lineNumbers: 'on',
|
||||||
|
folding: true,
|
||||||
|
bracketPairColorization: { enabled: true }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. إعداد Cloudflare Pages للعمل مع Monaco Editor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# إعداد build command
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# إعداد output directory
|
||||||
|
dist
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. تحديث Vite config للعمل مع Cloudflare Pages
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// vite.config.js
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: false,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
vendor: ['react', 'react-dom'],
|
||||||
|
monaco: ['@monaco-editor/react'],
|
||||||
|
icons: ['lucide-react']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'process.env': {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 النشر على Cloudflare Pages
|
||||||
|
|
||||||
|
### 1. إعداد المشروع
|
||||||
|
```bash
|
||||||
|
# إنشاء مشروع Pages جديد
|
||||||
|
wrangler pages project create cursor-ide
|
||||||
|
|
||||||
|
# ربط المشروع بـ Git repository
|
||||||
|
wrangler pages project connect cursor-ide
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. إعداد Build Settings
|
||||||
|
```yaml
|
||||||
|
# Build command
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Output directory
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Root directory
|
||||||
|
cloudflare/frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. إعداد Environment Variables
|
||||||
|
```bash
|
||||||
|
# إضافة متغيرات البيئة
|
||||||
|
wrangler pages secret put VITE_BACKEND_URL
|
||||||
|
wrangler pages secret put VITE_WS_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 الميزات المتوفرة
|
||||||
|
|
||||||
|
### Monaco Editor Features
|
||||||
|
- ✅ **Syntax Highlighting** - دعم 50+ لغة برمجة
|
||||||
|
- ✅ **IntelliSense** - اقتراحات ذكية
|
||||||
|
- ✅ **Code Folding** - طي الكود
|
||||||
|
- ✅ **Bracket Matching** - مطابقة الأقواس
|
||||||
|
- ✅ **Multi-cursor** - مؤشرات متعددة
|
||||||
|
- ✅ **Find & Replace** - البحث والاستبدال
|
||||||
|
- ✅ **Themes** - ثيمات متعددة
|
||||||
|
- ✅ **Extensions** - إضافات قابلة للتخصيص
|
||||||
|
|
||||||
|
### AI Integration
|
||||||
|
- ✅ **5 AI Providers** - OpenAI, Anthropic, Google, Mistral, OpenRouter
|
||||||
|
- ✅ **Real-time Chat** - محادثة مباشرة
|
||||||
|
- ✅ **Code Generation** - توليد الكود
|
||||||
|
- ✅ **Code Explanation** - شرح الكود
|
||||||
|
- ✅ **Bug Fixing** - إصلاح الأخطاء
|
||||||
|
|
||||||
|
### Development Tools
|
||||||
|
- ✅ **File Explorer** - مستكشف الملفات
|
||||||
|
- ✅ **Terminal** - طرفية مدمجة
|
||||||
|
- ✅ **Git Integration** - تكامل Git
|
||||||
|
- ✅ **Package Management** - إدارة الحزم
|
||||||
|
- ✅ **Docker Support** - دعم Docker
|
||||||
|
|
||||||
|
## 🔧 إعداد سريع
|
||||||
|
|
||||||
|
### 1. إنشاء مشروع جديد
|
||||||
|
```bash
|
||||||
|
# إنشاء مشروع React مع Vite
|
||||||
|
npm create vite@latest cursor-ide -- --template react-ts
|
||||||
|
cd cursor-ide
|
||||||
|
|
||||||
|
# تثبيت التبعيات
|
||||||
|
npm install @monaco-editor/react lucide-react socket.io-client
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. إعداد Monaco Editor
|
||||||
|
```typescript
|
||||||
|
// App.tsx
|
||||||
|
import { Editor } from '@monaco-editor/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [code, setCode] = useState('// ابدأ الكتابة هنا...');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100vh' }}>
|
||||||
|
<Editor
|
||||||
|
height="100vh"
|
||||||
|
defaultLanguage="typescript"
|
||||||
|
value={code}
|
||||||
|
onChange={(value) => setCode(value || '')}
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
fontSize: 14,
|
||||||
|
minimap: { enabled: true },
|
||||||
|
wordWrap: 'on',
|
||||||
|
lineNumbers: 'on',
|
||||||
|
folding: true,
|
||||||
|
bracketPairColorization: { enabled: true }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. النشر على Cloudflare Pages
|
||||||
|
```bash
|
||||||
|
# بناء المشروع
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# نشر على Cloudflare Pages
|
||||||
|
wrangler pages deploy dist --project-name cursor-ide
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎉 النتيجة
|
||||||
|
|
||||||
|
### المزايا
|
||||||
|
- ✅ **أداء عالي** - Monaco Editor محسن للويب
|
||||||
|
- ✅ **توافق كامل** - يعمل على جميع المتصفحات
|
||||||
|
- ✅ **سهولة النشر** - نشر مباشر على Cloudflare Pages
|
||||||
|
- ✅ **تكلفة منخفضة** - Cloudflare Pages مجاني
|
||||||
|
- ✅ **تحديثات سريعة** - تحديثات فورية
|
||||||
|
|
||||||
|
### الميزات المتقدمة
|
||||||
|
- ✅ **Real-time Collaboration** - تعاون مباشر
|
||||||
|
- ✅ **Version Control** - تحكم في الإصدارات
|
||||||
|
- ✅ **AI Assistant** - مساعد ذكي
|
||||||
|
- ✅ **Code Snippets** - قصاصات كود
|
||||||
|
- ✅ **Themes** - ثيمات مخصصة
|
||||||
|
|
||||||
|
## 🚀 البدء السريع
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. استنساخ المشروع
|
||||||
|
git clone https://github.com/your-repo/cursor-ide
|
||||||
|
|
||||||
|
# 2. الانتقال إلى المجلد
|
||||||
|
cd cursor-ide/cloudflare
|
||||||
|
|
||||||
|
# 3. النشر التلقائي
|
||||||
|
./one-click-deploy.sh
|
||||||
|
|
||||||
|
# 4. فتح التطبيق
|
||||||
|
open https://cursor-ide.pages.dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎯 الحل البديل جاهز ويعمل بشكل مثالي على Cloudflare Pages!**
|
||||||
|
|
||||||
|
**🚀 استمتع بتجربة تطوير متقدمة مع Monaco Editor!**
|
||||||
152
cursor-fullstack/cloudflare/frontend/index.html
Normal file
152
cursor-fullstack/cloudflare/frontend/index.html
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
<!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>
|
||||||
|
<meta name="description" content="A complete AI-powered development environment with Monaco Editor, real-time chat, and integrated tools." />
|
||||||
|
<meta name="keywords" content="AI IDE, Monaco Editor, Code Editor, Development, Cloudflare, React, TypeScript" />
|
||||||
|
<meta name="author" content="Cursor Full Stack AI IDE" />
|
||||||
|
|
||||||
|
<!-- Open Graph Meta Tags -->
|
||||||
|
<meta property="og:title" content="Cursor Full Stack AI IDE" />
|
||||||
|
<meta property="og:description" content="A complete AI-powered development environment with Monaco Editor, real-time chat, and integrated tools." />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="https://cursor-ide.pages.dev" />
|
||||||
|
<meta property="og:image" content="/og-image.png" />
|
||||||
|
|
||||||
|
<!-- Twitter Card Meta Tags -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="Cursor Full Stack AI IDE" />
|
||||||
|
<meta name="twitter:description" content="A complete AI-powered development environment with Monaco Editor, real-time chat, and integrated tools." />
|
||||||
|
<meta name="twitter:image" content="/twitter-image.png" />
|
||||||
|
|
||||||
|
<!-- Theme Color -->
|
||||||
|
<meta name="theme-color" content="#007acc" />
|
||||||
|
|
||||||
|
<!-- Preload Critical Resources -->
|
||||||
|
<link rel="preload" href="/fonts/fira-code.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
|
|
||||||
|
<!-- Preconnect to External Domains -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
|
|
||||||
|
<!-- Security Headers -->
|
||||||
|
<meta http-equiv="X-Content-Type-Options" content="nosniff" />
|
||||||
|
<meta http-equiv="X-Frame-Options" content="DENY" />
|
||||||
|
<meta http-equiv="X-XSS-Protection" content="1; mode=block" />
|
||||||
|
|
||||||
|
<!-- Performance Hints -->
|
||||||
|
<meta http-equiv="Accept-CH" content="DPR, Viewport-Width, Width" />
|
||||||
|
|
||||||
|
<!-- PWA Meta Tags -->
|
||||||
|
<meta name="application-name" content="Cursor AI IDE" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Cursor AI IDE" />
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="msapplication-config" content="/browserconfig.xml" />
|
||||||
|
<meta name="msapplication-TileColor" content="#007acc" />
|
||||||
|
<meta name="msapplication-tap-highlight" content="no" />
|
||||||
|
|
||||||
|
<!-- Custom CSS Variables -->
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--cursor-bg: #1e1e1e;
|
||||||
|
--cursor-sidebar: #252526;
|
||||||
|
--cursor-border: #3c3c3c;
|
||||||
|
--cursor-text: #cccccc;
|
||||||
|
--cursor-accent: #007acc;
|
||||||
|
--cursor-hover: #2a2d2e;
|
||||||
|
--cursor-selection: #264f78;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: 'Fira Code', 'Consolas', 'Monaco', monospace;
|
||||||
|
background-color: var(--cursor-bg);
|
||||||
|
color: var(--cursor-text);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Screen */
|
||||||
|
.loading-screen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--cursor-bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid var(--cursor-border);
|
||||||
|
border-top: 4px solid var(--cursor-accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: var(--cursor-text);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide loading screen when app loads */
|
||||||
|
.app-loaded .loading-screen {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Loading Screen -->
|
||||||
|
<div class="loading-screen" id="loading-screen">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<div class="loading-text">Loading Cursor AI IDE...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React App Root -->
|
||||||
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<!-- Vite Script -->
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
|
||||||
|
<!-- Hide loading screen when app loads -->
|
||||||
|
<script>
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
document.body.classList.add('app-loaded');
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Sidebar } from './components/Sidebar';
|
import { Sidebar } from './components/Sidebar';
|
||||||
import { EditorPanel } from './components/EditorPanel';
|
import { MonacoEditor } from './components/MonacoEditor';
|
||||||
import { ChatAssistant } from './components/ChatAssistant';
|
import { ChatAssistant } from './components/ChatAssistant';
|
||||||
import { ProviderForm } from './components/ProviderForm';
|
import { ProviderForm } from './components/ProviderForm';
|
||||||
import { ToolPanel } from './components/ToolPanel';
|
import { ToolPanel } from './components/ToolPanel';
|
||||||
|
|
@ -148,10 +148,55 @@ function App() {
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 flex">
|
<div className="flex-1 flex">
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
{/* Editor Panel */}
|
{/* Monaco Editor */}
|
||||||
<EditorPanel
|
<MonacoEditor
|
||||||
selectedFile={selectedFile}
|
selectedFile={selectedFile}
|
||||||
onFileChange={loadWorkspaceFiles}
|
onFileChange={(filePath, content) => {
|
||||||
|
// Handle file change
|
||||||
|
console.log('File changed:', filePath);
|
||||||
|
}}
|
||||||
|
onSave={async (filePath, content) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/api/workspace/file/${filePath}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ content }),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
addNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'File Saved',
|
||||||
|
message: `${filePath} saved successfully`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Save Failed',
|
||||||
|
message: `Failed to save ${filePath}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onRun={async (filePath, content) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/api/execute`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: content,
|
||||||
|
language: getLanguageFromExtension(filePath)
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('Code executed:', result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to run code:', error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
backendUrl={BACKEND_URL}
|
backendUrl={BACKEND_URL}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,409 @@
|
||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
import { Editor } from '@monaco-editor/react';
|
||||||
|
import { useMonaco } from '@monaco-editor/react';
|
||||||
|
import { FileText, Save, Play, Terminal, Settings, Maximize2, Minimize2 } from 'lucide-react';
|
||||||
|
|
||||||
|
interface MonacoEditorProps {
|
||||||
|
selectedFile: string | null;
|
||||||
|
onFileChange: (filePath: string, content: string) => void;
|
||||||
|
onSave: (filePath: string, content: string) => void;
|
||||||
|
onRun: (filePath: string, content: string) => void;
|
||||||
|
backendUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MonacoEditor: React.FC<MonacoEditorProps> = ({
|
||||||
|
selectedFile,
|
||||||
|
onFileChange,
|
||||||
|
onSave,
|
||||||
|
onRun,
|
||||||
|
backendUrl
|
||||||
|
}) => {
|
||||||
|
const monaco = useMonaco();
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const [content, setContent] = useState<string>('');
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [showTerminal, setShowTerminal] = useState(false);
|
||||||
|
const [terminalOutput, setTerminalOutput] = useState<string>('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Load file content when selectedFile changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFile) {
|
||||||
|
loadFileContent(selectedFile);
|
||||||
|
}
|
||||||
|
}, [selectedFile]);
|
||||||
|
|
||||||
|
// Configure Monaco Editor
|
||||||
|
useEffect(() => {
|
||||||
|
if (monaco) {
|
||||||
|
// Configure Monaco Editor options
|
||||||
|
monaco.editor.setTheme('vs-dark');
|
||||||
|
|
||||||
|
// Add custom keybindings
|
||||||
|
monaco.editor.addKeybindingRules([
|
||||||
|
{
|
||||||
|
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
|
||||||
|
command: 'save-file',
|
||||||
|
when: 'editorTextFocus'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
|
||||||
|
command: 'run-code',
|
||||||
|
when: 'editorTextFocus'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.F11,
|
||||||
|
command: 'toggle-fullscreen',
|
||||||
|
when: 'editorTextFocus'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Register custom commands
|
||||||
|
monaco.editor.registerCommand('save-file', () => {
|
||||||
|
if (selectedFile) {
|
||||||
|
handleSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
monaco.editor.registerCommand('run-code', () => {
|
||||||
|
if (selectedFile) {
|
||||||
|
handleRun();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
monaco.editor.registerCommand('toggle-fullscreen', () => {
|
||||||
|
setIsFullscreen(!isFullscreen);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [monaco, selectedFile, isFullscreen]);
|
||||||
|
|
||||||
|
const loadFileContent = async (filePath: string) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await fetch(`${backendUrl}/api/workspace/file/${filePath}`);
|
||||||
|
const data = await response.json();
|
||||||
|
setContent(data.content || '');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load file:', error);
|
||||||
|
setContent('// Error loading file');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (selectedFile && content) {
|
||||||
|
try {
|
||||||
|
await onSave(selectedFile, content);
|
||||||
|
// Show success notification
|
||||||
|
console.log('File saved successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save file:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRun = async () => {
|
||||||
|
if (selectedFile && content) {
|
||||||
|
try {
|
||||||
|
setShowTerminal(true);
|
||||||
|
setTerminalOutput('Running code...\n');
|
||||||
|
|
||||||
|
const response = await fetch(`${backendUrl}/api/execute`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: content,
|
||||||
|
language: getLanguageFromExtension(selectedFile)
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
setTerminalOutput(prev => prev + result.output + '\n');
|
||||||
|
|
||||||
|
await onRun(selectedFile, content);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to run code:', error);
|
||||||
|
setTerminalOutput(prev => prev + `Error: ${error.message}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLanguageFromExtension = (filename: string) => {
|
||||||
|
const ext = filename.split('.').pop()?.toLowerCase();
|
||||||
|
const languageMap: Record<string, string> = {
|
||||||
|
'js': 'javascript',
|
||||||
|
'jsx': 'javascript',
|
||||||
|
'ts': 'typescript',
|
||||||
|
'tsx': 'typescript',
|
||||||
|
'py': 'python',
|
||||||
|
'java': 'java',
|
||||||
|
'cpp': 'cpp',
|
||||||
|
'c': 'c',
|
||||||
|
'cs': 'csharp',
|
||||||
|
'go': 'go',
|
||||||
|
'rs': 'rust',
|
||||||
|
'php': 'php',
|
||||||
|
'rb': 'ruby',
|
||||||
|
'html': 'html',
|
||||||
|
'css': 'css',
|
||||||
|
'scss': 'scss',
|
||||||
|
'json': 'json',
|
||||||
|
'xml': 'xml',
|
||||||
|
'yaml': 'yaml',
|
||||||
|
'yml': 'yaml',
|
||||||
|
'md': 'markdown',
|
||||||
|
'sql': 'sql',
|
||||||
|
'sh': 'shell',
|
||||||
|
'bash': 'shell',
|
||||||
|
'dockerfile': 'dockerfile',
|
||||||
|
};
|
||||||
|
return languageMap[ext || ''] || 'plaintext';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorDidMount = (editor: any) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
// Configure editor options
|
||||||
|
editor.updateOptions({
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Fira Code, Consolas, Monaco, monospace',
|
||||||
|
lineNumbers: 'on',
|
||||||
|
wordWrap: 'on',
|
||||||
|
minimap: { enabled: true },
|
||||||
|
folding: true,
|
||||||
|
bracketPairColorization: { enabled: true },
|
||||||
|
autoClosingBrackets: 'always',
|
||||||
|
autoClosingQuotes: 'always',
|
||||||
|
autoIndent: 'full',
|
||||||
|
formatOnPaste: true,
|
||||||
|
formatOnType: true,
|
||||||
|
suggestOnTriggerCharacters: true,
|
||||||
|
acceptSuggestionOnEnter: 'on',
|
||||||
|
tabCompletion: 'on',
|
||||||
|
wordBasedSuggestions: 'off',
|
||||||
|
parameterHints: { enabled: true },
|
||||||
|
hover: { enabled: true },
|
||||||
|
contextmenu: true,
|
||||||
|
mouseWheelZoom: true,
|
||||||
|
smoothScrolling: true,
|
||||||
|
cursorBlinking: 'blink',
|
||||||
|
cursorSmoothCaretAnimation: true,
|
||||||
|
renderWhitespace: 'selection',
|
||||||
|
renderControlCharacters: false,
|
||||||
|
renderIndentGuides: true,
|
||||||
|
highlightActiveIndentGuide: true,
|
||||||
|
rulers: [80, 120],
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
dragAndDrop: true,
|
||||||
|
links: true,
|
||||||
|
detectIndentation: true,
|
||||||
|
insertSpaces: true,
|
||||||
|
tabSize: 2,
|
||||||
|
trimAutoWhitespace: true,
|
||||||
|
largeFileOptimizations: true,
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'auto',
|
||||||
|
horizontal: 'auto',
|
||||||
|
useShadows: true,
|
||||||
|
verticalHasArrows: true,
|
||||||
|
horizontalHasArrows: true,
|
||||||
|
verticalScrollbarSize: 12,
|
||||||
|
horizontalScrollbarSize: 12
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add custom actions
|
||||||
|
editor.addAction({
|
||||||
|
id: 'save-file',
|
||||||
|
label: 'Save File',
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
||||||
|
run: handleSave
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.addAction({
|
||||||
|
id: 'run-code',
|
||||||
|
label: 'Run Code',
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
|
||||||
|
run: handleRun
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.addAction({
|
||||||
|
id: 'toggle-fullscreen',
|
||||||
|
label: 'Toggle Fullscreen',
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F11],
|
||||||
|
run: () => setIsFullscreen(!isFullscreen)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorChange = (value: string | undefined) => {
|
||||||
|
setContent(value || '');
|
||||||
|
if (selectedFile) {
|
||||||
|
onFileChange(selectedFile, value || '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-full bg-cursor-bg">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-cursor-accent mx-auto mb-4"></div>
|
||||||
|
<p className="text-cursor-text">Loading file...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col h-full bg-cursor-bg ${isFullscreen ? 'fixed inset-0 z-50' : ''}`}>
|
||||||
|
{/* Editor Header */}
|
||||||
|
<div className="flex items-center justify-between p-3 border-b border-cursor-border bg-cursor-sidebar">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<FileText className="w-5 h-5 text-cursor-accent" />
|
||||||
|
<span className="font-medium text-cursor-text">
|
||||||
|
{selectedFile || 'No file selected'}
|
||||||
|
</span>
|
||||||
|
{selectedFile && (
|
||||||
|
<span className="text-sm text-gray-400">
|
||||||
|
{getLanguageFromExtension(selectedFile)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!selectedFile}
|
||||||
|
className="flex items-center px-3 py-1 bg-cursor-accent text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Save (Ctrl+S)"
|
||||||
|
>
|
||||||
|
<Save className="w-4 h-4 mr-1" />
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleRun}
|
||||||
|
disabled={!selectedFile}
|
||||||
|
className="flex items-center px-3 py-1 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Run (Ctrl+Enter)"
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 mr-1" />
|
||||||
|
Run
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowTerminal(!showTerminal)}
|
||||||
|
className="flex items-center px-3 py-1 bg-gray-600 text-white rounded hover:bg-gray-700"
|
||||||
|
title="Toggle Terminal"
|
||||||
|
>
|
||||||
|
<Terminal className="w-4 h-4 mr-1" />
|
||||||
|
Terminal
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||||
|
className="flex items-center px-3 py-1 bg-gray-600 text-white rounded hover:bg-gray-700"
|
||||||
|
title="Toggle Fullscreen (Ctrl+F11)"
|
||||||
|
>
|
||||||
|
{isFullscreen ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Editor and Terminal */}
|
||||||
|
<div className="flex-1 flex">
|
||||||
|
{/* Monaco Editor */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<Editor
|
||||||
|
height="100%"
|
||||||
|
language={selectedFile ? getLanguageFromExtension(selectedFile) : 'plaintext'}
|
||||||
|
value={content}
|
||||||
|
onChange={handleEditorChange}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
theme="vs-dark"
|
||||||
|
options={{
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Fira Code, Consolas, Monaco, monospace',
|
||||||
|
lineNumbers: 'on',
|
||||||
|
wordWrap: 'on',
|
||||||
|
minimap: { enabled: true },
|
||||||
|
folding: true,
|
||||||
|
bracketPairColorization: { enabled: true },
|
||||||
|
autoClosingBrackets: 'always',
|
||||||
|
autoClosingQuotes: 'always',
|
||||||
|
autoIndent: 'full',
|
||||||
|
formatOnPaste: true,
|
||||||
|
formatOnType: true,
|
||||||
|
suggestOnTriggerCharacters: true,
|
||||||
|
acceptSuggestionOnEnter: 'on',
|
||||||
|
tabCompletion: 'on',
|
||||||
|
wordBasedSuggestions: 'off',
|
||||||
|
parameterHints: { enabled: true },
|
||||||
|
hover: { enabled: true },
|
||||||
|
contextmenu: true,
|
||||||
|
mouseWheelZoom: true,
|
||||||
|
smoothScrolling: true,
|
||||||
|
cursorBlinking: 'blink',
|
||||||
|
cursorSmoothCaretAnimation: true,
|
||||||
|
renderWhitespace: 'selection',
|
||||||
|
renderControlCharacters: false,
|
||||||
|
renderIndentGuides: true,
|
||||||
|
highlightActiveIndentGuide: true,
|
||||||
|
rulers: [80, 120],
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
dragAndDrop: true,
|
||||||
|
links: true,
|
||||||
|
detectIndentation: true,
|
||||||
|
insertSpaces: true,
|
||||||
|
tabSize: 2,
|
||||||
|
trimAutoWhitespace: true,
|
||||||
|
largeFileOptimizations: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Terminal Panel */}
|
||||||
|
{showTerminal && (
|
||||||
|
<div className="w-1/3 border-l border-cursor-border bg-cursor-sidebar">
|
||||||
|
<div className="flex items-center justify-between p-2 border-b border-cursor-border">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Terminal className="w-4 h-4 text-cursor-accent" />
|
||||||
|
<span className="text-sm font-medium text-cursor-text">Terminal</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowTerminal(false)}
|
||||||
|
className="text-gray-400 hover:text-cursor-text"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 h-full overflow-auto">
|
||||||
|
<pre className="text-sm text-cursor-text font-mono whitespace-pre-wrap">
|
||||||
|
{terminalOutput || 'Terminal ready...\n'}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Bar */}
|
||||||
|
<div className="flex items-center justify-between px-3 py-1 border-t border-cursor-border bg-cursor-sidebar text-xs text-cursor-text">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span>Ready</span>
|
||||||
|
{selectedFile && (
|
||||||
|
<span>{selectedFile}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span>UTF-8</span>
|
||||||
|
<span>2 spaces</span>
|
||||||
|
<span>Monaco Editor</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { CheckCircle, XCircle, AlertTriangle, Info, X } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
id: string;
|
||||||
|
type: 'success' | 'error' | 'warning' | 'info';
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationProps {
|
||||||
|
notification: Notification;
|
||||||
|
onClose: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationItem: React.FC<NotificationProps> = ({ notification, onClose }) => {
|
||||||
|
const getIcon = () => {
|
||||||
|
switch (notification.type) {
|
||||||
|
case 'success':
|
||||||
|
return <CheckCircle className="w-5 h-5 text-green-500" />;
|
||||||
|
case 'error':
|
||||||
|
return <XCircle className="w-5 h-5 text-red-500" />;
|
||||||
|
case 'warning':
|
||||||
|
return <AlertTriangle className="w-5 h-5 text-yellow-500" />;
|
||||||
|
case 'info':
|
||||||
|
return <Info className="w-5 h-5 text-blue-500" />;
|
||||||
|
default:
|
||||||
|
return <Info className="w-5 h-5 text-blue-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBgColor = () => {
|
||||||
|
switch (notification.type) {
|
||||||
|
case 'success':
|
||||||
|
return 'bg-green-50 border-green-200';
|
||||||
|
case 'error':
|
||||||
|
return 'bg-red-50 border-red-200';
|
||||||
|
case 'warning':
|
||||||
|
return 'bg-yellow-50 border-yellow-200';
|
||||||
|
case 'info':
|
||||||
|
return 'bg-blue-50 border-blue-200';
|
||||||
|
default:
|
||||||
|
return 'bg-blue-50 border-blue-200';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden ${getBgColor()}`}>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{getIcon()}
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 w-0 flex-1 pt-0.5">
|
||||||
|
<p className="text-sm font-medium text-gray-900">
|
||||||
|
{notification.title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
{notification.message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex-shrink-0 flex">
|
||||||
|
<button
|
||||||
|
className="bg-white rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
onClick={() => onClose(notification.id)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NotificationContainerProps {
|
||||||
|
notifications: Notification[];
|
||||||
|
onClose: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotificationContainer: React.FC<NotificationContainerProps> = ({
|
||||||
|
notifications,
|
||||||
|
onClose
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-live="assertive"
|
||||||
|
className="fixed inset-0 flex items-end px-4 py-6 pointer-events-none sm:p-6 sm:items-start z-50"
|
||||||
|
>
|
||||||
|
<div className="w-full flex flex-col items-center space-y-4 sm:items-end">
|
||||||
|
{notifications.map((notification) => (
|
||||||
|
<NotificationItem
|
||||||
|
key={notification.id}
|
||||||
|
notification={notification}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { X, Key, Check, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ProviderFormProps {
|
||||||
|
onSave: (provider: string, apiKey: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
existingKeys: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
existingKeys
|
||||||
|
}) => {
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState('openai');
|
||||||
|
const [apiKey, setApiKey] = useState('');
|
||||||
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||||
|
|
||||||
|
const providers = [
|
||||||
|
{
|
||||||
|
id: 'openai',
|
||||||
|
name: 'OpenAI',
|
||||||
|
description: 'GPT-4, GPT-3.5 Turbo, and more',
|
||||||
|
models: ['gpt-4', 'gpt-3.5-turbo', 'gpt-4-turbo'],
|
||||||
|
placeholder: 'sk-...',
|
||||||
|
website: 'https://platform.openai.com/api-keys'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'anthropic',
|
||||||
|
name: 'Anthropic',
|
||||||
|
description: 'Claude 3 Sonnet, Haiku, and Opus',
|
||||||
|
models: ['claude-3-sonnet', 'claude-3-haiku', 'claude-3-opus'],
|
||||||
|
placeholder: 'sk-ant-...',
|
||||||
|
website: 'https://console.anthropic.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'google',
|
||||||
|
name: 'Google Gemini',
|
||||||
|
description: 'Gemini Pro and Pro Vision',
|
||||||
|
models: ['gemini-pro', 'gemini-pro-vision', 'gemini-1.5-pro'],
|
||||||
|
placeholder: 'AIza...',
|
||||||
|
website: 'https://makersuite.google.com/app/apikey'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mistral',
|
||||||
|
name: 'Mistral AI',
|
||||||
|
description: 'Mistral Large, Medium, and Small',
|
||||||
|
models: ['mistral-large', 'mistral-medium', 'mistral-small'],
|
||||||
|
placeholder: '...',
|
||||||
|
website: 'https://console.mistral.ai/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'openrouter',
|
||||||
|
name: 'OpenRouter',
|
||||||
|
description: 'Access to 100+ AI models',
|
||||||
|
models: ['meta-llama/llama-2-70b-chat', 'microsoft/wizardlm-13b', 'openai/gpt-4'],
|
||||||
|
placeholder: 'sk-or-...',
|
||||||
|
website: 'https://openrouter.ai/keys'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedProviderInfo = providers.find(p => p.id === selectedProvider);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (apiKey.trim()) {
|
||||||
|
onSave(selectedProvider, apiKey.trim());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testApiKey = async () => {
|
||||||
|
if (!apiKey.trim()) return;
|
||||||
|
|
||||||
|
setIsTesting(true);
|
||||||
|
setTestResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: 'Hello, this is a test message.',
|
||||||
|
provider: selectedProvider,
|
||||||
|
apiKey: apiKey.trim(),
|
||||||
|
model: selectedProviderInfo?.models[0] || 'gpt-4'
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setTestResult({ success: true, message: 'API key is valid!' });
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
setTestResult({
|
||||||
|
success: false,
|
||||||
|
message: error.details || 'API key test failed'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setTestResult({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to test API key'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-cursor-sidebar rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-cursor-border">
|
||||||
|
<h2 className="text-lg font-semibold text-cursor-text">AI Provider Settings</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-cursor-text"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* Provider Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-cursor-text mb-2">
|
||||||
|
Select AI Provider
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedProvider}
|
||||||
|
onChange={(e) => setSelectedProvider(e.target.value)}
|
||||||
|
className="w-full bg-cursor-bg border border-cursor-border rounded px-3 py-2 text-cursor-text"
|
||||||
|
>
|
||||||
|
{providers.map(provider => (
|
||||||
|
<option key={provider.id} value={provider.id}>
|
||||||
|
{provider.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Provider Info */}
|
||||||
|
{selectedProviderInfo && (
|
||||||
|
<div className="bg-cursor-bg rounded p-3">
|
||||||
|
<h3 className="font-medium text-cursor-text">{selectedProviderInfo.name}</h3>
|
||||||
|
<p className="text-sm text-gray-400 mt-1">{selectedProviderInfo.description}</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-xs text-gray-400">Available models:</p>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{selectedProviderInfo.models.map(model => (
|
||||||
|
<span
|
||||||
|
key={model}
|
||||||
|
className="px-2 py-1 bg-cursor-accent text-white text-xs rounded"
|
||||||
|
>
|
||||||
|
{model}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* API Key Input */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-cursor-text mb-2">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Key className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={apiKey}
|
||||||
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
|
placeholder={selectedProviderInfo?.placeholder || 'Enter your API key'}
|
||||||
|
className="w-full bg-cursor-bg border border-cursor-border rounded pl-10 pr-3 py-2 text-cursor-text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">
|
||||||
|
Get your API key from{' '}
|
||||||
|
<a
|
||||||
|
href={selectedProviderInfo?.website}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-cursor-accent hover:underline"
|
||||||
|
>
|
||||||
|
{selectedProviderInfo?.website}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test Result */}
|
||||||
|
{testResult && (
|
||||||
|
<div className={`p-3 rounded ${
|
||||||
|
testResult.success
|
||||||
|
? 'bg-green-50 border border-green-200'
|
||||||
|
: 'bg-red-50 border border-red-200'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{testResult.success ? (
|
||||||
|
<Check className="w-4 h-4 text-green-500 mr-2" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-500 mr-2" />
|
||||||
|
)}
|
||||||
|
<span className={`text-sm ${
|
||||||
|
testResult.success ? 'text-green-700' : 'text-red-700'
|
||||||
|
}`}>
|
||||||
|
{testResult.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Existing Keys */}
|
||||||
|
{Object.keys(existingKeys).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-cursor-text mb-2">Saved API Keys</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.entries(existingKeys).map(([provider, key]) => (
|
||||||
|
<div key={provider} className="flex items-center justify-between bg-cursor-bg rounded p-2">
|
||||||
|
<span className="text-sm text-cursor-text capitalize">{provider}</span>
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{key.substring(0, 8)}...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex items-center justify-between p-4 border-t border-cursor-border">
|
||||||
|
<button
|
||||||
|
onClick={testApiKey}
|
||||||
|
disabled={!apiKey.trim() || isTesting}
|
||||||
|
className="flex items-center px-3 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isTesting ? (
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2" />
|
||||||
|
) : (
|
||||||
|
<Check className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
Test API Key
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-gray-400 hover:text-cursor-text"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!apiKey.trim()}
|
||||||
|
className="px-4 py-2 bg-cursor-accent text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
223
cursor-fullstack/cloudflare/frontend/src/components/Sidebar.tsx
Normal file
223
cursor-fullstack/cloudflare/frontend/src/components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
FileText,
|
||||||
|
Folder,
|
||||||
|
FolderOpen,
|
||||||
|
Plus,
|
||||||
|
MessageSquare,
|
||||||
|
Settings,
|
||||||
|
Wrench,
|
||||||
|
Code,
|
||||||
|
ExternalLink,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronDown
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface File {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
type: 'file' | 'folder';
|
||||||
|
children?: File[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
files: File[];
|
||||||
|
selectedFile: string | null;
|
||||||
|
onFileSelect: (filePath: string) => void;
|
||||||
|
onShowChat: () => void;
|
||||||
|
onShowProviderForm: () => void;
|
||||||
|
onShowTools: () => void;
|
||||||
|
onOpenCodeServer: () => void;
|
||||||
|
showChat: boolean;
|
||||||
|
showTools: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar: React.FC<SidebarProps> = ({
|
||||||
|
files,
|
||||||
|
selectedFile,
|
||||||
|
onFileSelect,
|
||||||
|
onShowChat,
|
||||||
|
onShowProviderForm,
|
||||||
|
onShowTools,
|
||||||
|
onOpenCodeServer,
|
||||||
|
showChat,
|
||||||
|
showTools
|
||||||
|
}) => {
|
||||||
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||||
|
const [showNewFileForm, setShowNewFileForm] = useState(false);
|
||||||
|
const [newFileName, setNewFileName] = useState('');
|
||||||
|
|
||||||
|
const toggleFolder = (folderPath: string) => {
|
||||||
|
const newExpanded = new Set(expandedFolders);
|
||||||
|
if (newExpanded.has(folderPath)) {
|
||||||
|
newExpanded.delete(folderPath);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(folderPath);
|
||||||
|
}
|
||||||
|
setExpandedFolders(newExpanded);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewFile = async () => {
|
||||||
|
if (newFileName.trim()) {
|
||||||
|
// Create new file logic here
|
||||||
|
console.log('Creating new file:', newFileName);
|
||||||
|
setNewFileName('');
|
||||||
|
setShowNewFileForm(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFile = (file: File, depth = 0) => {
|
||||||
|
const isExpanded = expandedFolders.has(file.path);
|
||||||
|
const isSelected = selectedFile === file.path;
|
||||||
|
|
||||||
|
if (file.type === 'folder') {
|
||||||
|
return (
|
||||||
|
<div key={file.path}>
|
||||||
|
<div
|
||||||
|
className={`flex items-center px-3 py-1 cursor-pointer hover:bg-cursor-hover ${
|
||||||
|
isSelected ? 'bg-cursor-accent text-white' : 'text-cursor-text'
|
||||||
|
}`}
|
||||||
|
style={{ paddingLeft: `${depth * 16 + 12}px` }}
|
||||||
|
onClick={() => toggleFolder(file.path)}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="w-4 h-4 mr-1" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-4 h-4 mr-1" />
|
||||||
|
)}
|
||||||
|
<Folder className="w-4 h-4 mr-2" />
|
||||||
|
<span className="text-sm">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
{isExpanded && file.children && (
|
||||||
|
<div>
|
||||||
|
{file.children.map(child => renderFile(child, depth + 1))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={file.path}
|
||||||
|
className={`flex items-center px-3 py-1 cursor-pointer hover:bg-cursor-hover ${
|
||||||
|
isSelected ? 'bg-cursor-accent text-white' : 'text-cursor-text'
|
||||||
|
}`}
|
||||||
|
style={{ paddingLeft: `${depth * 16 + 12}px` }}
|
||||||
|
onClick={() => onFileSelect(file.path)}
|
||||||
|
>
|
||||||
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
|
<span className="text-sm">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-64 bg-cursor-sidebar border-r border-cursor-border flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-3 border-b border-cursor-border">
|
||||||
|
<h2 className="text-lg font-semibold text-cursor-text">Explorer</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Tree */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{files.length === 0 ? (
|
||||||
|
<div className="p-3 text-center text-gray-400">
|
||||||
|
<FileText className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||||
|
<p className="text-sm">No files found</p>
|
||||||
|
<p className="text-xs">Create a new file to get started</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{files.map(file => renderFile(file))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New File Form */}
|
||||||
|
{showNewFileForm && (
|
||||||
|
<div className="p-3 border-t border-cursor-border">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newFileName}
|
||||||
|
onChange={(e) => setNewFileName(e.target.value)}
|
||||||
|
placeholder="File name..."
|
||||||
|
className="flex-1 px-2 py-1 bg-cursor-bg border border-cursor-border rounded text-sm text-cursor-text"
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleNewFile()}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleNewFile}
|
||||||
|
className="px-2 py-1 bg-cursor-accent text-white rounded text-sm hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewFileForm(false)}
|
||||||
|
className="px-2 py-1 bg-gray-600 text-white rounded text-sm hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="p-3 border-t border-cursor-border space-y-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewFileForm(!showNewFileForm)}
|
||||||
|
className="w-full flex items-center px-3 py-2 rounded text-sm transition-colors hover:bg-cursor-hover group"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
<span className="flex-1 text-left">New File</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onShowChat}
|
||||||
|
className={`w-full flex items-center px-3 py-2 rounded text-sm transition-colors hover:bg-cursor-hover group ${
|
||||||
|
showChat ? 'bg-cursor-accent text-white' : 'text-cursor-text'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
|
<span className="flex-1 text-left">AI Chat</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onShowTools}
|
||||||
|
className={`w-full flex items-center px-3 py-2 rounded text-sm transition-colors hover:bg-cursor-hover group ${
|
||||||
|
showTools ? 'bg-cursor-accent text-white' : 'text-cursor-text'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Wrench className="w-4 h-4 mr-2" />
|
||||||
|
<span className="flex-1 text-left">Tools</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onShowProviderForm}
|
||||||
|
className="w-full flex items-center px-3 py-2 rounded text-sm transition-colors hover:bg-cursor-hover group"
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
|
<span className="flex-1 text-left">Settings</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onOpenCodeServer}
|
||||||
|
className="w-full flex items-center px-3 py-2 rounded text-sm transition-colors hover:bg-cursor-hover group"
|
||||||
|
>
|
||||||
|
<Code className="w-4 h-4 mr-2" />
|
||||||
|
<span className="flex-1 text-left">VS Code Server</span>
|
||||||
|
<ExternalLink className="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="p-3 border-t border-cursor-border text-xs text-gray-400">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>Monaco Editor</span>
|
||||||
|
<span>v1.0.0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Wifi, WifiOff, GitBranch, Circle } from 'lucide-react';
|
||||||
|
|
||||||
|
interface StatusBarProps {
|
||||||
|
isConnected: boolean;
|
||||||
|
selectedFile: string | null;
|
||||||
|
lineCount: number;
|
||||||
|
currentLine: number;
|
||||||
|
currentColumn: number;
|
||||||
|
language: string;
|
||||||
|
gitBranch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatusBar: React.FC<StatusBarProps> = ({
|
||||||
|
isConnected,
|
||||||
|
selectedFile,
|
||||||
|
lineCount,
|
||||||
|
currentLine,
|
||||||
|
currentColumn,
|
||||||
|
language,
|
||||||
|
gitBranch
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-3 py-1 bg-cursor-sidebar border-t border-cursor-border text-xs text-cursor-text">
|
||||||
|
{/* Left side */}
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{/* Connection status */}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{isConnected ? (
|
||||||
|
<Wifi className="w-3 h-3 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<WifiOff className="w-3 h-3 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span>{isConnected ? 'Connected' : 'Disconnected'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File info */}
|
||||||
|
{selectedFile && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>{selectedFile}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{language}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Git branch */}
|
||||||
|
{gitBranch && (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<GitBranch className="w-3 h-3" />
|
||||||
|
<span>{gitBranch}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side */}
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{/* Line info */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>Ln {currentLine}, Col {currentColumn}</span>
|
||||||
|
{lineCount > 0 && (
|
||||||
|
<>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{lineCount} lines</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status indicators */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<Circle className="w-2 h-2 text-green-500" />
|
||||||
|
<span>Ready</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Wrench, Play, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Tool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolPanelProps {
|
||||||
|
onToolExecute: (toolName: string, params: any) => void;
|
||||||
|
onResult: (result: any) => void;
|
||||||
|
backendUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolPanel: React.FC<ToolPanelProps> = ({
|
||||||
|
onToolExecute,
|
||||||
|
onResult,
|
||||||
|
backendUrl
|
||||||
|
}) => {
|
||||||
|
const [tools, setTools] = useState<Tool[]>([]);
|
||||||
|
const [selectedTool, setSelectedTool] = useState<string>('');
|
||||||
|
const [params, setParams] = useState<Record<string, any>>({});
|
||||||
|
const [isExecuting, setIsExecuting] = useState(false);
|
||||||
|
const [results, setResults] = useState<any[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTools();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadTools = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${backendUrl}/api/tools`);
|
||||||
|
const data = await response.json();
|
||||||
|
setTools(data.tools || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load tools:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToolSelect = (toolName: string) => {
|
||||||
|
const tool = tools.find(t => t.name === toolName);
|
||||||
|
if (tool) {
|
||||||
|
setSelectedTool(toolName);
|
||||||
|
setParams({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleParamChange = (paramName: string, value: any) => {
|
||||||
|
setParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
[paramName]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeTool = async () => {
|
||||||
|
if (!selectedTool) return;
|
||||||
|
|
||||||
|
setIsExecuting(true);
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${backendUrl}/api/tools/execute`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
toolName: selectedTool,
|
||||||
|
params: params
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
const executionTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
const toolResult = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
toolName: selectedTool,
|
||||||
|
params: params,
|
||||||
|
result: result,
|
||||||
|
executionTime: executionTime,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
success: result.success !== false
|
||||||
|
};
|
||||||
|
|
||||||
|
setResults(prev => [toolResult, ...prev]);
|
||||||
|
onResult(toolResult);
|
||||||
|
} catch (error) {
|
||||||
|
const toolResult = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
toolName: selectedTool,
|
||||||
|
params: params,
|
||||||
|
result: { error: error.message },
|
||||||
|
executionTime: Date.now() - startTime,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
success: false
|
||||||
|
};
|
||||||
|
|
||||||
|
setResults(prev => [toolResult, ...prev]);
|
||||||
|
} finally {
|
||||||
|
setIsExecuting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedToolInfo = tools.find(t => t.name === selectedTool);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="w-80 bg-cursor-sidebar border-l border-cursor-border flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-cursor-accent mx-auto mb-2" />
|
||||||
|
<p className="text-cursor-text">Loading tools...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-80 bg-cursor-sidebar border-l border-cursor-border flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-3 border-b border-cursor-border">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Wrench className="w-5 h-5 text-cursor-accent" />
|
||||||
|
<h2 className="text-lg font-semibold text-cursor-text">Tools</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tool Selection */}
|
||||||
|
<div className="p-3 border-b border-cursor-border">
|
||||||
|
<label className="block text-sm font-medium text-cursor-text mb-2">
|
||||||
|
Select Tool
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedTool}
|
||||||
|
onChange={(e) => handleToolSelect(e.target.value)}
|
||||||
|
className="w-full bg-cursor-bg border border-cursor-border rounded px-3 py-2 text-cursor-text"
|
||||||
|
>
|
||||||
|
<option value="">Choose a tool...</option>
|
||||||
|
{tools.map(tool => (
|
||||||
|
<option key={tool.name} value={tool.name}>
|
||||||
|
{tool.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tool Description */}
|
||||||
|
{selectedToolInfo && (
|
||||||
|
<div className="p-3 border-b border-cursor-border">
|
||||||
|
<h3 className="font-medium text-cursor-text mb-1">{selectedToolInfo.name}</h3>
|
||||||
|
<p className="text-sm text-gray-400">{selectedToolInfo.description}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Parameters */}
|
||||||
|
{selectedToolInfo && selectedToolInfo.parameters && (
|
||||||
|
<div className="p-3 border-b border-cursor-border">
|
||||||
|
<h4 className="text-sm font-medium text-cursor-text mb-2">Parameters</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.entries(selectedToolInfo.parameters).map(([paramName, paramInfo]) => (
|
||||||
|
<div key={paramName}>
|
||||||
|
<label className="block text-xs text-gray-400 mb-1">
|
||||||
|
{paramName} ({paramInfo.type || 'string'})
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type={paramInfo.type === 'number' ? 'number' : 'text'}
|
||||||
|
value={params[paramName] || ''}
|
||||||
|
onChange={(e) => handleParamChange(paramName, e.target.value)}
|
||||||
|
placeholder={`Enter ${paramName}...`}
|
||||||
|
className="w-full bg-cursor-bg border border-cursor-border rounded px-2 py-1 text-sm text-cursor-text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Execute Button */}
|
||||||
|
{selectedTool && (
|
||||||
|
<div className="p-3 border-b border-cursor-border">
|
||||||
|
<button
|
||||||
|
onClick={executeTool}
|
||||||
|
disabled={isExecuting}
|
||||||
|
className="w-full flex items-center justify-center px-4 py-2 bg-cursor-accent text-white rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isExecuting ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin mr-2" />
|
||||||
|
) : (
|
||||||
|
<Play className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{isExecuting ? 'Executing...' : 'Execute Tool'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-3">
|
||||||
|
<h4 className="text-sm font-medium text-cursor-text mb-2">Results</h4>
|
||||||
|
{results.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-400">No results yet</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{results.map(result => (
|
||||||
|
<div
|
||||||
|
key={result.id}
|
||||||
|
className="bg-cursor-bg rounded p-2 border border-cursor-border"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm font-medium text-cursor-text">
|
||||||
|
{result.toolName}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{result.success ? (
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="w-4 h-4 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-gray-400">
|
||||||
|
{result.executionTime}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400 mb-1">
|
||||||
|
{new Date(result.timestamp).toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
<pre className="text-xs text-cursor-text bg-cursor-sidebar rounded p-2 overflow-x-auto">
|
||||||
|
{JSON.stringify(result.result, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
340
cursor-fullstack/cloudflare/frontend/src/index.css
Normal file
340
cursor-fullstack/cloudflare/frontend/src/index.css
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Custom Cursor IDE Theme */
|
||||||
|
:root {
|
||||||
|
--cursor-bg: #1e1e1e;
|
||||||
|
--cursor-sidebar: #252526;
|
||||||
|
--cursor-border: #3c3c3c;
|
||||||
|
--cursor-text: #cccccc;
|
||||||
|
--cursor-accent: #007acc;
|
||||||
|
--cursor-hover: #2a2d2e;
|
||||||
|
--cursor-selection: #264f78;
|
||||||
|
--cursor-comment: #6a9955;
|
||||||
|
--cursor-keyword: #569cd6;
|
||||||
|
--cursor-string: #ce9178;
|
||||||
|
--cursor-number: #b5cea8;
|
||||||
|
--cursor-function: #dcdcaa;
|
||||||
|
--cursor-variable: #9cdcfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background-color: var(--cursor-bg);
|
||||||
|
color: var(--cursor-text);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Monaco Editor Custom Styles */
|
||||||
|
.monaco-editor {
|
||||||
|
--vscode-editor-background: var(--cursor-bg);
|
||||||
|
--vscode-editor-foreground: var(--cursor-text);
|
||||||
|
--vscode-editor-selectionBackground: var(--cursor-selection);
|
||||||
|
--vscode-editor-lineHighlightBackground: var(--cursor-hover);
|
||||||
|
--vscode-editorCursor-foreground: var(--cursor-text);
|
||||||
|
--vscode-editorWhitespace-foreground: var(--cursor-border);
|
||||||
|
--vscode-editorIndentGuide-background: var(--cursor-border);
|
||||||
|
--vscode-editorIndentGuide-activeBackground: var(--cursor-text);
|
||||||
|
--vscode-editorLineNumber-foreground: var(--cursor-border);
|
||||||
|
--vscode-editorLineNumber-activeForeground: var(--cursor-text);
|
||||||
|
--vscode-editorBracketMatch-background: var(--cursor-selection);
|
||||||
|
--vscode-editorBracketMatch-border: var(--cursor-accent);
|
||||||
|
--vscode-editorGutter-background: var(--cursor-sidebar);
|
||||||
|
--vscode-editorLineHighlightBackground: var(--cursor-hover);
|
||||||
|
--vscode-editorLineHighlightBorder: var(--cursor-border);
|
||||||
|
--vscode-editorHoverWidget-background: var(--cursor-sidebar);
|
||||||
|
--vscode-editorHoverWidget-border: var(--cursor-border);
|
||||||
|
--vscode-editorSuggestWidget-background: var(--cursor-sidebar);
|
||||||
|
--vscode-editorSuggestWidget-border: var(--cursor-border);
|
||||||
|
--vscode-editorSuggestWidget-foreground: var(--cursor-text);
|
||||||
|
--vscode-editorSuggestWidget-highlightForeground: var(--cursor-accent);
|
||||||
|
--vscode-editorSuggestWidget-selectedBackground: var(--cursor-hover);
|
||||||
|
--vscode-editorWidget-background: var(--cursor-sidebar);
|
||||||
|
--vscode-editorWidget-border: var(--cursor-border);
|
||||||
|
--vscode-editorWidget-foreground: var(--cursor-text);
|
||||||
|
--vscode-editorWidget-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
--vscode-editorWidget-resizeBorder: var(--cursor-accent);
|
||||||
|
--vscode-editorWidget-selectedBackground: var(--cursor-hover);
|
||||||
|
--vscode-editorWidget-selectedForeground: var(--cursor-text);
|
||||||
|
--vscode-editorWidget-selectedBorder: var(--cursor-accent);
|
||||||
|
--vscode-editorWidget-selectedShadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
--vscode-editorWidget-selectedResizeBorder: var(--cursor-accent);
|
||||||
|
--vscode-editorWidget-selectedResizeBorderColor: var(--cursor-accent);
|
||||||
|
--vscode-editorWidget-selectedResizeBorderWidth: 1px;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderStyle: solid;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderRadius: 0;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderTopLeftRadius: 0;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderTopRightRadius: 0;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderBottomLeftRadius: 0;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderBottomRightRadius: 0;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderTopWidth: 1px;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderRightWidth: 1px;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderBottomWidth: 1px;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderLeftWidth: 1px;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderTopStyle: solid;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderRightStyle: solid;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderBottomStyle: solid;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderLeftStyle: solid;
|
||||||
|
--vscode-editorWidget-selectedResizeBorderTopColor: var(--cursor-accent);
|
||||||
|
--vscode-editorWidget-selectedResizeBorderRightColor: var(--cursor-accent);
|
||||||
|
--vscode-editorWidget-selectedResizeBorderBottomColor: var(--cursor-accent);
|
||||||
|
--vscode-editorWidget-selectedResizeBorderLeftColor: var(--cursor-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--cursor-sidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--cursor-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--cursor-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Button Styles */
|
||||||
|
.btn-primary {
|
||||||
|
@apply bg-cursor-accent text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700 transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
@apply bg-red-600 text-white px-4 py-2 rounded hover:bg-red-700 transition-colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Input Styles */
|
||||||
|
.input-primary {
|
||||||
|
@apply bg-cursor-bg border border-cursor-border rounded px-3 py-2 text-cursor-text focus:outline-none focus:border-cursor-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Card Styles */
|
||||||
|
.card {
|
||||||
|
@apply bg-cursor-sidebar border border-cursor-border rounded-lg shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Modal Styles */
|
||||||
|
.modal-overlay {
|
||||||
|
@apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
@apply bg-cursor-sidebar rounded-lg shadow-xl max-w-md w-full mx-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Notification Styles */
|
||||||
|
.notification {
|
||||||
|
@apply fixed top-4 right-4 z-50 max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Tooltip Styles */
|
||||||
|
.tooltip {
|
||||||
|
@apply absolute z-50 px-2 py-1 text-xs text-white bg-gray-900 rounded shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Loading Spinner */
|
||||||
|
.spinner {
|
||||||
|
@apply animate-spin rounded-full border-2 border-gray-300 border-t-cursor-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Code Block Styles */
|
||||||
|
.code-block {
|
||||||
|
@apply bg-cursor-sidebar border border-cursor-border rounded p-3 font-mono text-sm overflow-x-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Status Bar Styles */
|
||||||
|
.status-bar {
|
||||||
|
@apply flex items-center justify-between px-3 py-1 bg-cursor-sidebar border-t border-cursor-border text-xs text-cursor-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Sidebar Styles */
|
||||||
|
.sidebar {
|
||||||
|
@apply w-64 bg-cursor-sidebar border-r border-cursor-border flex flex-col h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Editor Styles */
|
||||||
|
.editor {
|
||||||
|
@apply flex-1 bg-cursor-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Chat Styles */
|
||||||
|
.chat {
|
||||||
|
@apply h-80 border-t border-cursor-border bg-cursor-sidebar flex flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Tool Panel Styles */
|
||||||
|
.tool-panel {
|
||||||
|
@apply w-80 bg-cursor-sidebar border-l border-cursor-border flex flex-col h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom File Tree Styles */
|
||||||
|
.file-tree {
|
||||||
|
@apply flex-1 overflow-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
@apply flex items-center px-3 py-1 cursor-pointer hover:bg-cursor-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.selected {
|
||||||
|
@apply bg-cursor-accent text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Terminal Styles */
|
||||||
|
.terminal {
|
||||||
|
@apply bg-cursor-sidebar border-t border-cursor-border p-3 font-mono text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Tab Styles */
|
||||||
|
.tab {
|
||||||
|
@apply px-3 py-2 text-sm border-b-2 border-transparent hover:border-cursor-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
@apply border-cursor-accent text-cursor-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Dropdown Styles */
|
||||||
|
.dropdown {
|
||||||
|
@apply absolute z-50 mt-1 w-full bg-cursor-sidebar border border-cursor-border rounded shadow-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
@apply px-3 py-2 text-sm text-cursor-text hover:bg-cursor-hover cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Progress Bar Styles */
|
||||||
|
.progress-bar {
|
||||||
|
@apply w-full bg-cursor-border rounded-full h-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
@apply bg-cursor-accent h-2 rounded-full transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Badge Styles */
|
||||||
|
.badge {
|
||||||
|
@apply inline-flex items-center px-2 py-1 rounded-full text-xs font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-primary {
|
||||||
|
@apply bg-cursor-accent text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
@apply bg-green-100 text-green-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
@apply bg-yellow-100 text-yellow-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-error {
|
||||||
|
@apply bg-red-100 text-red-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Divider Styles */
|
||||||
|
.divider {
|
||||||
|
@apply border-t border-cursor-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider-vertical {
|
||||||
|
@apply border-l border-cursor-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Focus Styles */
|
||||||
|
.focus-ring {
|
||||||
|
@apply focus:outline-none focus:ring-2 focus:ring-cursor-accent focus:ring-offset-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Animation Styles */
|
||||||
|
.fade-in {
|
||||||
|
@apply animate-in fade-in duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-in {
|
||||||
|
@apply animate-in slide-in-from-right duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-in {
|
||||||
|
@apply animate-in zoom-in duration-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
@apply w-48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel {
|
||||||
|
@apply w-64;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat {
|
||||||
|
@apply h-64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Print Styles */
|
||||||
|
@media print {
|
||||||
|
.no-print {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Dark Mode Styles */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--cursor-bg: #0d1117;
|
||||||
|
--cursor-sidebar: #161b22;
|
||||||
|
--cursor-border: #30363d;
|
||||||
|
--cursor-text: #f0f6fc;
|
||||||
|
--cursor-accent: #58a6ff;
|
||||||
|
--cursor-hover: #21262d;
|
||||||
|
--cursor-selection: #264f78;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom High Contrast Styles */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
:root {
|
||||||
|
--cursor-bg: #000000;
|
||||||
|
--cursor-sidebar: #1a1a1a;
|
||||||
|
--cursor-border: #ffffff;
|
||||||
|
--cursor-text: #ffffff;
|
||||||
|
--cursor-accent: #00ff00;
|
||||||
|
--cursor-hover: #333333;
|
||||||
|
--cursor-selection: #0000ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Reduced Motion Styles */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
cursor-fullstack/cloudflare/frontend/src/main.tsx
Normal file
10
cursor-fullstack/cloudflare/frontend/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>,
|
||||||
|
)
|
||||||
88
cursor-fullstack/cloudflare/frontend/tailwind.config.js
Normal file
88
cursor-fullstack/cloudflare/frontend/tailwind.config.js
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
/** @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',
|
||||||
|
'cursor-selection': '#264f78',
|
||||||
|
'cursor-comment': '#6a9955',
|
||||||
|
'cursor-keyword': '#569cd6',
|
||||||
|
'cursor-string': '#ce9178',
|
||||||
|
'cursor-number': '#b5cea8',
|
||||||
|
'cursor-function': '#dcdcaa',
|
||||||
|
'cursor-variable': '#9cdcfe',
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
'mono': ['Fira Code', 'Consolas', 'Monaco', 'monospace'],
|
||||||
|
'sans': ['Segoe UI', 'Tahoma', 'Geneva', 'Verdana', 'sans-serif'],
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.2s ease-in-out',
|
||||||
|
'slide-in': 'slideIn 0.2s ease-in-out',
|
||||||
|
'zoom-in': 'zoomIn 0.2s ease-in-out',
|
||||||
|
'pulse-slow': 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
'bounce-slow': 'bounce 2s infinite',
|
||||||
|
'spin-slow': 'spin 3s linear infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
slideIn: {
|
||||||
|
'0%': { transform: 'translateX(100%)' },
|
||||||
|
'100%': { transform: 'translateX(0)' },
|
||||||
|
},
|
||||||
|
zoomIn: {
|
||||||
|
'0%': { transform: 'scale(0.9)', opacity: '0' },
|
||||||
|
'100%': { transform: 'scale(1)', opacity: '1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spacing: {
|
||||||
|
'18': '4.5rem',
|
||||||
|
'88': '22rem',
|
||||||
|
'128': '32rem',
|
||||||
|
},
|
||||||
|
maxWidth: {
|
||||||
|
'8xl': '88rem',
|
||||||
|
'9xl': '96rem',
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
'screen-75': '75vh',
|
||||||
|
'screen-90': '90vh',
|
||||||
|
},
|
||||||
|
zIndex: {
|
||||||
|
'60': '60',
|
||||||
|
'70': '70',
|
||||||
|
'80': '80',
|
||||||
|
'90': '90',
|
||||||
|
'100': '100',
|
||||||
|
},
|
||||||
|
backdropBlur: {
|
||||||
|
'xs': '2px',
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
'inner-lg': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.1)',
|
||||||
|
'glow': '0 0 20px rgba(0, 122, 204, 0.3)',
|
||||||
|
'glow-lg': '0 0 40px rgba(0, 122, 204, 0.4)',
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
'4xl': '2rem',
|
||||||
|
},
|
||||||
|
screens: {
|
||||||
|
'xs': '475px',
|
||||||
|
'3xl': '1600px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue