Commit ·
c2c8c8d
0
Parent(s):
Initial commit: Rebranded to GLMPilot and migrated to GLM-5 API
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +6 -0
- .env.example +18 -0
- .gitignore +11 -0
- ARCHITECTURE.md +73 -0
- Dockerfile +38 -0
- README.md +174 -0
- docker-compose.yml +44 -0
- package-lock.json +0 -0
- package.json +22 -0
- packages/client/README.md +26 -0
- packages/client/index.html +14 -0
- packages/client/package.json +47 -0
- packages/client/postcss.config.js +6 -0
- packages/client/src/App.tsx +20 -0
- packages/client/src/components/ai/AIChatPanel.tsx +141 -0
- packages/client/src/components/ai/AgentResultsPanel.tsx +134 -0
- packages/client/src/components/ai/MarkdownMessage.tsx +43 -0
- packages/client/src/components/editor/CodeEditor.tsx +126 -0
- packages/client/src/components/editor/EditorTabs.tsx +46 -0
- packages/client/src/components/editor/MultiFileEditor.tsx +30 -0
- packages/client/src/components/explorer/FileTree.tsx +108 -0
- packages/client/src/components/github/RepoImporter.tsx +74 -0
- packages/client/src/components/ide/EnvironmentSelector.tsx +52 -0
- packages/client/src/components/landing/CTAFooterWrapper.tsx +27 -0
- packages/client/src/components/landing/CTASection.tsx +25 -0
- packages/client/src/components/landing/ChessSection.tsx +52 -0
- packages/client/src/components/landing/FeaturesSection.tsx +79 -0
- packages/client/src/components/landing/FooterSection.tsx +59 -0
- packages/client/src/components/landing/HLSVideo.tsx +45 -0
- packages/client/src/components/landing/HeroSection.tsx +69 -0
- packages/client/src/components/landing/LandingPage.tsx +19 -0
- packages/client/src/components/landing/MarqueeRow.tsx +22 -0
- packages/client/src/components/landing/Navbar.tsx +36 -0
- packages/client/src/components/landing/NumbersSection.tsx +46 -0
- packages/client/src/components/landing/ReverseChessSection.tsx +56 -0
- packages/client/src/components/landing/SectionBadge.tsx +20 -0
- packages/client/src/components/landing/TestimonialsSection.tsx +56 -0
- packages/client/src/components/layout/IDEShell.tsx +98 -0
- packages/client/src/components/layout/PanelLayout.tsx +67 -0
- packages/client/src/components/layout/Sidebar.tsx +95 -0
- packages/client/src/components/layout/StatusBar.tsx +64 -0
- packages/client/src/components/layout/TopBar.tsx +103 -0
- packages/client/src/components/preview/LivePreview.tsx +124 -0
- packages/client/src/components/terminal/TerminalPanel.tsx +189 -0
- packages/client/src/components/ui/badge.tsx +32 -0
- packages/client/src/components/ui/button.tsx +50 -0
- packages/client/src/components/ui/card.tsx +46 -0
- packages/client/src/components/ui/dialog.tsx +71 -0
- packages/client/src/components/ui/dropdown-menu.tsx +57 -0
- packages/client/src/components/ui/input.tsx +21 -0
.dockerignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
.env
|
| 3 |
+
.env.*
|
| 4 |
+
dist
|
| 5 |
+
.git
|
| 6 |
+
.gitignore
|
.env.example
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Server
|
| 2 |
+
NODE_ENV=development
|
| 3 |
+
PORT=3001
|
| 4 |
+
CLIENT_URL=http://localhost:5173
|
| 5 |
+
|
| 6 |
+
# GLM API
|
| 7 |
+
GLM_API_KEY=your_glm_api_key_here
|
| 8 |
+
GLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4
|
| 9 |
+
GLM_MODEL=glm-5
|
| 10 |
+
|
| 11 |
+
# GitHub Integration
|
| 12 |
+
GITHUB_TOKEN=your_github_token_here
|
| 13 |
+
|
| 14 |
+
# Redis
|
| 15 |
+
REDIS_URL=redis://localhost:6379
|
| 16 |
+
|
| 17 |
+
# Logging
|
| 18 |
+
LOG_LEVEL=debug
|
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
| 3 |
+
build/
|
| 4 |
+
.env
|
| 5 |
+
.env.local
|
| 6 |
+
*.log
|
| 7 |
+
.DS_Store
|
| 8 |
+
coverage/
|
| 9 |
+
.next/
|
| 10 |
+
.turbo/
|
| 11 |
+
*.tsbuildinfo
|
ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GLMPilot Architecture
|
| 2 |
+
|
| 3 |
+
GLMPilot is built as a modern, full-stack monorepo designed to provide a seamless, rich browser-based IDE experience. It leverages React for the frontend, Node.js/Express for the backend, and WebSockets for real-time bidirectional communication.
|
| 4 |
+
|
| 5 |
+
## 🏗️ Repository Structure
|
| 6 |
+
|
| 7 |
+
The project uses npm workspaces to manage its packages.
|
| 8 |
+
|
| 9 |
+
```text
|
| 10 |
+
GLMPilot/
|
| 11 |
+
├── packages/
|
| 12 |
+
│ ├── client/ # React frontend application
|
| 13 |
+
│ ├── server/ # Node.js Express backend & WebSocket server
|
| 14 |
+
│ └── shared/ # Shared TypeScript types, constants, and utilities
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
### 1. `packages/client` (Frontend)
|
| 18 |
+
|
| 19 |
+
The frontend is a Single Page Application (SPA) built with React 18 and bundled using Vite.
|
| 20 |
+
|
| 21 |
+
**Key Technologies:**
|
| 22 |
+
- **React 18**: UI component library.
|
| 23 |
+
- **React Router**: For client-side routing (`/` for Landing, `/ide` for environment selection, `/ide/:env` for the IDE shell).
|
| 24 |
+
- **Zustand**: For lightweight, global state management (e.g., Editor state, File system state, AI chat state).
|
| 25 |
+
- **Tailwind CSS**: For utility-first styling.
|
| 26 |
+
- **Monaco Editor**: The core code editor component (same engine as VS Code).
|
| 27 |
+
|
| 28 |
+
**Core Components (`src/`):**
|
| 29 |
+
- `components/layout/IDEShell.tsx`: The main IDE interface containing the file explorer, Monaco editor, and terminal/preview panes.
|
| 30 |
+
- `components/ide/EnvironmentSelector.tsx`: Allows users to choose their development environment (e.g., Web, Node, Python).
|
| 31 |
+
- `stores/`: Contains Zustand stores for managing global application state.
|
| 32 |
+
- `services/`: API client wrappers and WebSocket connections to the backend.
|
| 33 |
+
|
| 34 |
+
### 2. `packages/server` (Backend)
|
| 35 |
+
|
| 36 |
+
The backend is an Express.js server that handles API requests, serves the frontend in production, and manages real-time WebSocket connections.
|
| 37 |
+
|
| 38 |
+
**Key Technologies:**
|
| 39 |
+
- **Node.js & Express**: HTTP server framework.
|
| 40 |
+
- **Socket.io**: For real-time bi-directional event-based communication (Terminal, Code Sync).
|
| 41 |
+
- **Redis** (Optional/External): Used for pub/sub and state management across potential multiple instances.
|
| 42 |
+
|
| 43 |
+
**Core Components (`src/`):**
|
| 44 |
+
- `index.ts`: The main entry point. Sets up Express middleware, API routes, Socket.io, and serves the static React build in production.
|
| 45 |
+
- `websocket/handler.ts`: Manages incoming WebSocket connections for real-time terminal emulation and IDE collaboration events.
|
| 46 |
+
- `agents/`: Contains the logic for the different Multi-Agent AI Code Reviewers (Security, Performance, Style, Documentation). These interfaces communicate with the GLM model.
|
| 47 |
+
- `routes/`: Express API endpoints (e.g., authentication, project importing, AI proxy).
|
| 48 |
+
- `config/env.ts`: Centralized environment variable parsing and validation.
|
| 49 |
+
|
| 50 |
+
### 3. `packages/shared` (Shared Logic)
|
| 51 |
+
|
| 52 |
+
This package contains code that is utilized by both the `client` and the `server` to guarantee type safety and consistency across the stack.
|
| 53 |
+
|
| 54 |
+
**Core Components (`src/`):**
|
| 55 |
+
- `types.ts`: TypeScript interfaces for payloads, events, and data models (e.g., WebSocket event definitions, File abstractions).
|
| 56 |
+
- `constants.ts`: Shared static values (e.g., Event names, Environment types).
|
| 57 |
+
- `utils.ts`: Helper functions safe to run in both Node.js and Browser environments.
|
| 58 |
+
|
| 59 |
+
## 🔄 Data Flow & Communication
|
| 60 |
+
|
| 61 |
+
1. **HTTP/REST**: The client uses standard HTTP requests for one-off operations such as fetching initial configuration, authenticating, or triggering a specific AI agent review.
|
| 62 |
+
2. **WebSocket (Socket.io)**: Once the IDE shell is loaded, a persistent WebSocket connection is established. This channel is used for:
|
| 63 |
+
- Streaming terminal input/output.
|
| 64 |
+
- Live code collaboration/syncing.
|
| 65 |
+
- Streaming AI chat responses.
|
| 66 |
+
3. **AI Integration**: The Express server acts as a proxy for the GLM Mini API. When the client requests an AI action (like a code completion or chat), it sends the request to the backend. The backend constructs the prompt (often reading workspace context), queries the AI model securely (using `GLM_API_KEY`), and streams the response back to the client.
|
| 67 |
+
|
| 68 |
+
## 🚢 Deployment Architecture
|
| 69 |
+
|
| 70 |
+
In a production environment like Hugging Face Spaces or a standard Docker container:
|
| 71 |
+
- The `client` is built into static HTML/JS/CSS files (`packages/client/dist`).
|
| 72 |
+
- The `server` is built to JavaScript (`packages/server/dist`).
|
| 73 |
+
- When the Node application (server) starts, it serves the React `dist` folder natively on the same port alongside the API and WebSocket server. This allows single-port deployment configurations heavily favored by containerized platforms.
|
Dockerfile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-bookworm
|
| 2 |
+
|
| 3 |
+
# Install Python and Java
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
python3 \
|
| 6 |
+
openjdk-17-jdk \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
# Set working directory
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
# Copy root package files
|
| 13 |
+
COPY package.json package-lock.json ./
|
| 14 |
+
|
| 15 |
+
# Copy workspace package files
|
| 16 |
+
COPY packages/shared/package.json ./packages/shared/
|
| 17 |
+
COPY packages/server/package.json ./packages/server/
|
| 18 |
+
COPY packages/client/package.json ./packages/client/
|
| 19 |
+
|
| 20 |
+
# Install dependencies
|
| 21 |
+
RUN npm ci
|
| 22 |
+
|
| 23 |
+
# Copy full source
|
| 24 |
+
COPY . .
|
| 25 |
+
|
| 26 |
+
# Build packages
|
| 27 |
+
RUN npm run build
|
| 28 |
+
|
| 29 |
+
# Set environment variables for Hugging Face Spaces
|
| 30 |
+
ENV NODE_ENV=production
|
| 31 |
+
ENV PORT=7860
|
| 32 |
+
ENV CLIENT_URL=http://localhost:7860
|
| 33 |
+
|
| 34 |
+
# Expose the HF port
|
| 35 |
+
EXPOSE 7860
|
| 36 |
+
|
| 37 |
+
# Start the server
|
| 38 |
+
CMD ["npm", "run", "start", "-w", "packages/server"]
|
README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: GLMPilot
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# GLMPilot
|
| 12 |
+
|
| 13 |
+
GLMPilot is an AI-native, browser-based IDE built around GLM. It combines code editing, AI chat, code completion, multi-agent review, terminal execution, and GitHub workflows in a single application.
|
| 14 |
+
|
| 15 |
+
## Executive Summary
|
| 16 |
+
|
| 17 |
+
| Item | Details |
|
| 18 |
+
| --- | --- |
|
| 19 |
+
| Product Type | Browser-based AI IDE |
|
| 20 |
+
| Primary Use Case | Build, review, and ship code faster from one workspace |
|
| 21 |
+
| AI Backbone | GLM API (chat, completion, analysis, orchestration) |
|
| 22 |
+
| Deployment Model | Monorepo with Docker and Docker Compose support |
|
| 23 |
+
| Key Integrations | GitHub, Redis, WebSocket streaming |
|
| 24 |
+
|
| 25 |
+
## Key Capabilities
|
| 26 |
+
|
| 27 |
+
| Capability | Description | Business Value |
|
| 28 |
+
| --- | --- | --- |
|
| 29 |
+
| AI Chat and Streaming | Real-time GLM assistant integrated in the IDE | Faster iteration and lower context switching |
|
| 30 |
+
| Code Completion | Context-aware inline suggestions | Higher coding velocity |
|
| 31 |
+
| Multi-Agent Review | Security, performance, style, and documentation agents | Better code quality before PR |
|
| 32 |
+
| Monaco-Based Editor | Multi-file editing with VS Code-grade engine | Familiar professional developer experience |
|
| 33 |
+
| Integrated Terminal | Browser terminal over WebSocket | In-app execution and validation |
|
| 34 |
+
| Live Preview | Real-time HTML/CSS/JS preview | Rapid UI feedback loop |
|
| 35 |
+
| GitHub Workflow | Repository import and patch/PR flow | Shorter path from idea to merge |
|
| 36 |
+
|
| 37 |
+
## Architecture Overview
|
| 38 |
+
|
| 39 |
+
```text
|
| 40 |
+
Client (React + Monaco + Zustand)
|
| 41 |
+
-> REST API (Express routes)
|
| 42 |
+
-> WebSocket channel (chat tokens, terminal, execution events)
|
| 43 |
+
|
| 44 |
+
Server (Express + Agent Orchestrator)
|
| 45 |
+
-> GLM service client
|
| 46 |
+
-> GitHub service
|
| 47 |
+
-> Redis (cache/pub-sub/coordination)
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Detailed architecture: [ARCHITECTURE.md](ARCHITECTURE.md)
|
| 51 |
+
|
| 52 |
+
## Repository Structure
|
| 53 |
+
|
| 54 |
+
| Path | Purpose |
|
| 55 |
+
| --- | --- |
|
| 56 |
+
| packages/client | React application, IDE shell, editor and UI components |
|
| 57 |
+
| packages/server | Express API, WebSocket handlers, AI services, agent orchestration |
|
| 58 |
+
| packages/shared | Shared types, constants, and cross-package utilities |
|
| 59 |
+
| Dockerfile | Production image build configuration |
|
| 60 |
+
| docker-compose.yml | Local multi-service orchestration |
|
| 61 |
+
|
| 62 |
+
## Technology Stack
|
| 63 |
+
|
| 64 |
+
| Layer | Technologies |
|
| 65 |
+
| --- | --- |
|
| 66 |
+
| Frontend | React 18, TypeScript, Vite, Tailwind CSS, Monaco, Zustand |
|
| 67 |
+
| Backend | Node.js, Express, Socket.io |
|
| 68 |
+
| AI | GLM API with streaming and retry support |
|
| 69 |
+
| Infrastructure | Docker, Docker Compose, Redis |
|
| 70 |
+
| Repository | npm workspaces monorepo |
|
| 71 |
+
|
| 72 |
+
## Local Development
|
| 73 |
+
|
| 74 |
+
### Prerequisites
|
| 75 |
+
|
| 76 |
+
| Requirement | Minimum Version |
|
| 77 |
+
| --- | --- |
|
| 78 |
+
| Node.js | 18+ |
|
| 79 |
+
| npm | 9+ |
|
| 80 |
+
| Docker | Recommended |
|
| 81 |
+
|
| 82 |
+
### Setup
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
git clone https://github.com/your-username/GLMPilot.git
|
| 86 |
+
cd GLMPilot
|
| 87 |
+
npm install
|
| 88 |
+
cp .env.example .env
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### Environment Variables
|
| 92 |
+
|
| 93 |
+
| Variable | Required | Purpose |
|
| 94 |
+
| --- | --- | --- |
|
| 95 |
+
| NODE_ENV | No | Runtime mode (development/production) |
|
| 96 |
+
| PORT | No | Server port |
|
| 97 |
+
| CLIENT_URL | No | Allowed client origin |
|
| 98 |
+
| GLM_API_KEY | Yes | GLM authentication key |
|
| 99 |
+
| GLM_BASE_URL | No | GLM API base URL |
|
| 100 |
+
| GLM_MODEL | No | Default GLM model |
|
| 101 |
+
| GITHUB_TOKEN | Optional | GitHub integration and PR automation |
|
| 102 |
+
| REDIS_URL | No | Redis connection string |
|
| 103 |
+
| LOG_LEVEL | No | Server logging verbosity |
|
| 104 |
+
|
| 105 |
+
Start Redis and run development servers:
|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
docker run -d --name glmpilot-redis -p 6379:6379 redis
|
| 109 |
+
npm run dev:all
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
| Service | URL |
|
| 113 |
+
| --- | --- |
|
| 114 |
+
| Client | http://localhost:5173 |
|
| 115 |
+
| Server | http://localhost:3001 |
|
| 116 |
+
|
| 117 |
+
## Scripts
|
| 118 |
+
|
| 119 |
+
| Command | Description |
|
| 120 |
+
| --- | --- |
|
| 121 |
+
| npm run dev | Start client development server |
|
| 122 |
+
| npm run dev:server | Start server development process |
|
| 123 |
+
| npm run dev:all | Run client and server concurrently |
|
| 124 |
+
| npm run build | Build shared, server, and client packages |
|
| 125 |
+
| npm run typecheck | Run TypeScript project-reference checks |
|
| 126 |
+
|
| 127 |
+
## Deployment
|
| 128 |
+
|
| 129 |
+
| Option | Command |
|
| 130 |
+
| --- | --- |
|
| 131 |
+
| Docker Compose | docker-compose up --build |
|
| 132 |
+
| Single Image | docker build -t glmpilot-ide . |
|
| 133 |
+
| Single Image Run | docker run --env-file .env -p 7860:7860 glmpilot-ide |
|
| 134 |
+
|
| 135 |
+
In containerized mode, the backend serves static frontend assets and API/WebSocket traffic from a single port.
|
| 136 |
+
|
| 137 |
+
## API and Realtime Surface
|
| 138 |
+
|
| 139 |
+
| Surface | Capability |
|
| 140 |
+
| --- | --- |
|
| 141 |
+
| REST | Chat, completion, review, docs and integration endpoints |
|
| 142 |
+
| SSE | Token streaming for chat responses |
|
| 143 |
+
| WebSocket | Terminal IO, execution events, chat token streaming |
|
| 144 |
+
|
| 145 |
+
Package-level docs:
|
| 146 |
+
|
| 147 |
+
- [packages/server/README.md](packages/server/README.md)
|
| 148 |
+
- [packages/client/README.md](packages/client/README.md)
|
| 149 |
+
- [packages/shared/README.md](packages/shared/README.md)
|
| 150 |
+
|
| 151 |
+
## Hackathon Readiness
|
| 152 |
+
|
| 153 |
+
| Requirement Area | Current Status |
|
| 154 |
+
| --- | --- |
|
| 155 |
+
| GLM Integration Depth | Implemented across chat, completion, and multi-agent analysis |
|
| 156 |
+
| Codebase Documentation | Root and package-level documentation available |
|
| 157 |
+
| Setup Reproducibility | .env template and Docker-based startup provided |
|
| 158 |
+
| Demonstration Path | Import -> Generate -> Review -> Validate -> PR workflow supported |
|
| 159 |
+
|
| 160 |
+
## Recommended Demo Flow
|
| 161 |
+
|
| 162 |
+
1. Import a GitHub repository.
|
| 163 |
+
2. Use GLM chat to generate or refactor feature code.
|
| 164 |
+
3. Run multi-agent review and inspect findings.
|
| 165 |
+
4. Apply fixes and verify using preview/terminal.
|
| 166 |
+
5. Create a PR with summarized changes.
|
| 167 |
+
|
| 168 |
+
## Contributing
|
| 169 |
+
|
| 170 |
+
1. Fork the repository.
|
| 171 |
+
2. Create a feature branch.
|
| 172 |
+
3. Commit changes.
|
| 173 |
+
4. Push branch.
|
| 174 |
+
5. Open a pull request.
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
server:
|
| 5 |
+
build:
|
| 6 |
+
context: .
|
| 7 |
+
dockerfile: packages/server/Dockerfile
|
| 8 |
+
ports:
|
| 9 |
+
- "3001:3001"
|
| 10 |
+
environment:
|
| 11 |
+
- NODE_ENV=production
|
| 12 |
+
- PORT=3001
|
| 13 |
+
- REDIS_URL=redis://redis:6379
|
| 14 |
+
- GLM_API_KEY=${GLM_API_KEY}
|
| 15 |
+
- GITHUB_TOKEN=${GITHUB_TOKEN}
|
| 16 |
+
- CLIENT_URL=http://localhost:5173
|
| 17 |
+
depends_on:
|
| 18 |
+
- redis
|
| 19 |
+
volumes:
|
| 20 |
+
- ./packages/server:/app/packages/server
|
| 21 |
+
- ./packages/shared:/app/packages/shared
|
| 22 |
+
|
| 23 |
+
redis:
|
| 24 |
+
image: redis:7-alpine
|
| 25 |
+
ports:
|
| 26 |
+
- "6379:6379"
|
| 27 |
+
volumes:
|
| 28 |
+
- redis-data:/data
|
| 29 |
+
|
| 30 |
+
client:
|
| 31 |
+
build:
|
| 32 |
+
context: .
|
| 33 |
+
dockerfile: packages/client/Dockerfile
|
| 34 |
+
ports:
|
| 35 |
+
- "5173:5173"
|
| 36 |
+
environment:
|
| 37 |
+
- VITE_API_URL=http://localhost:3001
|
| 38 |
+
- VITE_WS_URL=ws://localhost:3001
|
| 39 |
+
volumes:
|
| 40 |
+
- ./packages/client:/app/packages/client
|
| 41 |
+
- ./packages/shared:/app/packages/shared
|
| 42 |
+
|
| 43 |
+
volumes:
|
| 44 |
+
redis-data:
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "glmpilot",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"workspaces": [
|
| 6 |
+
"packages/*"
|
| 7 |
+
],
|
| 8 |
+
"scripts": {
|
| 9 |
+
"dev": "npm run dev -w packages/client",
|
| 10 |
+
"dev:server": "npm run dev -w packages/server",
|
| 11 |
+
"dev:all": "concurrently \"npm run dev:server\" \"npm run dev\"",
|
| 12 |
+
"build": "npm run build -w packages/shared && npm run build -w packages/server && npm run build -w packages/client",
|
| 13 |
+
"build:shared": "npm run build -w packages/shared",
|
| 14 |
+
"build:server": "npm run build -w packages/server",
|
| 15 |
+
"build:client": "npm run build -w packages/client",
|
| 16 |
+
"typecheck": "tsc --build"
|
| 17 |
+
},
|
| 18 |
+
"devDependencies": {
|
| 19 |
+
"concurrently": "^8.2.2",
|
| 20 |
+
"typescript": "^5.4.0"
|
| 21 |
+
}
|
| 22 |
+
}
|
packages/client/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🖥️ GLMPilot Client
|
| 2 |
+
|
| 3 |
+
The `client` package contains the React SPA that powers the browser-based IDE interface.
|
| 4 |
+
|
| 5 |
+
## Key Responsibilities
|
| 6 |
+
|
| 7 |
+
1. **IDE Shell**: The main workspace interface (`IDEShell.tsx`) containing the file explorer, Monaco editor, and terminal panes.
|
| 8 |
+
2. **Code Editing**: Integrates `@monaco-editor/react` to provide a VS Code-like editing experience with syntax highlighting and autocompletion.
|
| 9 |
+
3. **Live Web Preview**: Supports real-time rendering of HTML/JS/CSS within an iframe or a new tab.
|
| 10 |
+
4. **State Management**: Uses `zustand` stores located in `src/stores/` to manage complex client-side state efficiently without extensive prop-drilling. Stores typically manage:
|
| 11 |
+
- Editor state (open files, current contents)
|
| 12 |
+
- Chat state (conversation history with ASI-1)
|
| 13 |
+
- Settings (user preferences, environment choices)
|
| 14 |
+
|
| 15 |
+
## Tech Stack
|
| 16 |
+
|
| 17 |
+
- **Framework**: React 18
|
| 18 |
+
- **Bundler**: Vite
|
| 19 |
+
- **Styling**: Tailwind CSS
|
| 20 |
+
- **Routing**: React Router
|
| 21 |
+
- **Global State**: Zustand
|
| 22 |
+
- **Editor**: Monaco Editor
|
| 23 |
+
|
| 24 |
+
## Development
|
| 25 |
+
|
| 26 |
+
Run `npm run dev` (or `npm run dev:client` from the root) to start the Vite development server.
|
packages/client/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta name="description" content="GLMPilot — AI-powered browser IDE for frontend development with multi-agent code review, inline completions, and GitHub integration." />
|
| 7 |
+
<title>GLMPilot — AI-Powered Browser IDE</title>
|
| 8 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div id="root"></div>
|
| 12 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
packages/client/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@glmpilot/client",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@fontsource/geist-sans": "^5.2.5",
|
| 13 |
+
"@glmpilot/shared": "*",
|
| 14 |
+
"@monaco-editor/react": "^4.6.0",
|
| 15 |
+
"@radix-ui/react-dialog": "^1.0.5",
|
| 16 |
+
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
| 17 |
+
"@radix-ui/react-tabs": "^1.0.4",
|
| 18 |
+
"@radix-ui/react-tooltip": "^1.0.7",
|
| 19 |
+
"@tanstack/react-query": "^5.17.0",
|
| 20 |
+
"axios": "^1.6.0",
|
| 21 |
+
"class-variance-authority": "^0.7.0",
|
| 22 |
+
"clsx": "^2.1.0",
|
| 23 |
+
"hls.js": "^1.5.0",
|
| 24 |
+
"lucide-react": "^0.303.0",
|
| 25 |
+
"monaco-editor": "^0.44.0",
|
| 26 |
+
"react": "^18.2.0",
|
| 27 |
+
"react-dom": "^18.2.0",
|
| 28 |
+
"react-resizable-panels": "^1.0.0",
|
| 29 |
+
"react-router-dom": "^6.21.0",
|
| 30 |
+
"socket.io-client": "^4.7.0",
|
| 31 |
+
"tailwind-merge": "^2.2.0",
|
| 32 |
+
"tailwindcss-animate": "^1.0.7",
|
| 33 |
+
"xterm": "^5.3.0",
|
| 34 |
+
"xterm-addon-fit": "^0.8.0",
|
| 35 |
+
"zustand": "^4.4.0"
|
| 36 |
+
},
|
| 37 |
+
"devDependencies": {
|
| 38 |
+
"@types/react": "^18.2.48",
|
| 39 |
+
"@types/react-dom": "^18.2.18",
|
| 40 |
+
"@vitejs/plugin-react": "^4.2.0",
|
| 41 |
+
"autoprefixer": "^10.4.17",
|
| 42 |
+
"postcss": "^8.4.33",
|
| 43 |
+
"tailwindcss": "^3.4.1",
|
| 44 |
+
"typescript": "^5.4.0",
|
| 45 |
+
"vite": "^5.0.0"
|
| 46 |
+
}
|
| 47 |
+
}
|
packages/client/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
};
|
packages/client/src/App.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
| 2 |
+
import LandingPage from './components/landing/LandingPage';
|
| 3 |
+
import IDEShell from './components/layout/IDEShell';
|
| 4 |
+
|
| 5 |
+
import EnvironmentSelector from './components/ide/EnvironmentSelector';
|
| 6 |
+
|
| 7 |
+
function App() {
|
| 8 |
+
return (
|
| 9 |
+
<BrowserRouter>
|
| 10 |
+
<Routes>
|
| 11 |
+
<Route path="/" element={<LandingPage />} />
|
| 12 |
+
<Route path="/ide" element={<EnvironmentSelector />} />
|
| 13 |
+
<Route path="/ide/:env" element={<IDEShell />} />
|
| 14 |
+
<Route path="*" element={<Navigate to="/" replace />} />
|
| 15 |
+
</Routes>
|
| 16 |
+
</BrowserRouter>
|
| 17 |
+
);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export default App;
|
packages/client/src/components/ai/AIChatPanel.tsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import { ArrowUp, Square, Sparkles, Bug, TestTube2, Zap } from 'lucide-react';
|
| 3 |
+
import { useGLMChat } from '@/hooks/useGLMChat';
|
| 4 |
+
import { useAIStore } from '@/stores/aiStore';
|
| 5 |
+
import { Textarea } from '@/components/ui/textarea';
|
| 6 |
+
import { Button } from '@/components/ui/button';
|
| 7 |
+
import { cn } from '@/lib/utils';
|
| 8 |
+
|
| 9 |
+
import { MarkdownMessage } from './MarkdownMessage';
|
| 10 |
+
|
| 11 |
+
export default function AIChatPanel() {
|
| 12 |
+
const { sendMessage, stopGeneration, isStreaming } = useGLMChat();
|
| 13 |
+
const messages = useAIStore((s) => s.messages);
|
| 14 |
+
const streamingMessage = useAIStore((s) => s.streamingMessage);
|
| 15 |
+
const [input, setInput] = useState('');
|
| 16 |
+
const scrollRef = useRef<HTMLDivElement>(null);
|
| 17 |
+
|
| 18 |
+
useEffect(() => {
|
| 19 |
+
if (scrollRef.current) {
|
| 20 |
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 21 |
+
}
|
| 22 |
+
}, [messages, streamingMessage]);
|
| 23 |
+
|
| 24 |
+
const handleSend = () => {
|
| 25 |
+
if (!input.trim() || isStreaming) return;
|
| 26 |
+
const history = messages.map((m) => ({ role: m.role, content: m.content }));
|
| 27 |
+
sendMessage(input.trim(), history);
|
| 28 |
+
setInput('');
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
| 32 |
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
| 33 |
+
e.preventDefault();
|
| 34 |
+
handleSend();
|
| 35 |
+
}
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
const quickActions = [
|
| 39 |
+
{ icon: Sparkles, label: 'Explain', prefix: 'Explain this code:\n' },
|
| 40 |
+
{ icon: Bug, label: 'Fix', prefix: 'Fix any issues in this code:\n' },
|
| 41 |
+
{ icon: TestTube2, label: 'Tests', prefix: 'Write unit tests for this code:\n' },
|
| 42 |
+
{ icon: Zap, label: 'Optimize', prefix: 'Optimize this code for performance:\n' },
|
| 43 |
+
];
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<div className="flex flex-col h-full bg-card/50">
|
| 47 |
+
{/* Header */}
|
| 48 |
+
<div className="px-4 py-3 border-b border-border">
|
| 49 |
+
<h3 className="text-sm font-semibold flex items-center gap-2">
|
| 50 |
+
<Sparkles className="w-4 h-4 text-primary" />
|
| 51 |
+
AI Assistant
|
| 52 |
+
</h3>
|
| 53 |
+
<p className="text-xs text-muted-foreground mt-0.5">Powered by GLM-5</p>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
{/* Messages */}
|
| 57 |
+
<div ref={scrollRef} className="flex-1 overflow-auto p-4 space-y-4 min-h-0">
|
| 58 |
+
{messages.length === 0 && !streamingMessage && (
|
| 59 |
+
<div className="text-center text-muted-foreground text-sm mt-8">
|
| 60 |
+
<Sparkles className="w-8 h-8 mx-auto mb-3 text-primary/40" />
|
| 61 |
+
<p className="font-medium">How can I help?</p>
|
| 62 |
+
<p className="text-xs mt-1">Ask about your code, request fixes, or get explanations.</p>
|
| 63 |
+
</div>
|
| 64 |
+
)}
|
| 65 |
+
|
| 66 |
+
{messages.map((msg) => (
|
| 67 |
+
<div
|
| 68 |
+
key={msg.id}
|
| 69 |
+
className={cn(
|
| 70 |
+
'max-w-[100%] text-sm',
|
| 71 |
+
msg.role === 'user' ? 'ml-auto' : 'mr-auto w-full'
|
| 72 |
+
)}
|
| 73 |
+
>
|
| 74 |
+
<div
|
| 75 |
+
className={cn(
|
| 76 |
+
'px-4 py-3 rounded-2xl',
|
| 77 |
+
msg.role === 'user'
|
| 78 |
+
? 'bg-primary/10 rounded-br-md whitespace-pre-wrap break-words max-w-[90%] float-right'
|
| 79 |
+
: 'liquid-glass rounded-bl-md w-full'
|
| 80 |
+
)}
|
| 81 |
+
>
|
| 82 |
+
{msg.role === 'user' ? msg.content : <MarkdownMessage content={msg.content} />}
|
| 83 |
+
</div>
|
| 84 |
+
<div className="clear-both"></div>
|
| 85 |
+
</div>
|
| 86 |
+
))}
|
| 87 |
+
|
| 88 |
+
{streamingMessage && (
|
| 89 |
+
<div className="mr-auto w-full text-sm">
|
| 90 |
+
<div className="liquid-glass px-4 py-3 rounded-2xl rounded-bl-md">
|
| 91 |
+
<MarkdownMessage content={streamingMessage + '▌'} />
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
)}
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
{/* Quick Actions */}
|
| 98 |
+
<div className="px-4 py-2 flex gap-1.5 border-t border-border/50">
|
| 99 |
+
{quickActions.map((action) => (
|
| 100 |
+
<button
|
| 101 |
+
key={action.label}
|
| 102 |
+
onClick={() => setInput(action.prefix)}
|
| 103 |
+
className="flex items-center gap-1 px-2 py-1 text-xs text-muted-foreground hover:text-foreground hover:bg-secondary rounded-md transition-colors"
|
| 104 |
+
>
|
| 105 |
+
<action.icon className="w-3 h-3" />
|
| 106 |
+
{action.label}
|
| 107 |
+
</button>
|
| 108 |
+
))}
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
{/* Input */}
|
| 112 |
+
<div className="p-3 border-t border-border">
|
| 113 |
+
<div className="flex gap-2">
|
| 114 |
+
<Textarea
|
| 115 |
+
value={input}
|
| 116 |
+
onChange={(e) => setInput(e.target.value)}
|
| 117 |
+
onKeyDown={handleKeyDown}
|
| 118 |
+
placeholder="Ask anything... (⌘+Enter to send)"
|
| 119 |
+
rows={1}
|
| 120 |
+
className="min-h-[40px] max-h-[120px] resize-none text-sm"
|
| 121 |
+
/>
|
| 122 |
+
{isStreaming ? (
|
| 123 |
+
<Button size="icon" variant="ghost" onClick={stopGeneration}>
|
| 124 |
+
<Square className="w-4 h-4" />
|
| 125 |
+
</Button>
|
| 126 |
+
) : (
|
| 127 |
+
<Button
|
| 128 |
+
size="icon"
|
| 129 |
+
variant="default"
|
| 130 |
+
onClick={handleSend}
|
| 131 |
+
disabled={!input.trim()}
|
| 132 |
+
className="bg-primary hover:bg-primary/90 shrink-0"
|
| 133 |
+
>
|
| 134 |
+
<ArrowUp className="w-4 h-4" />
|
| 135 |
+
</Button>
|
| 136 |
+
)}
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
);
|
| 141 |
+
}
|
packages/client/src/components/ai/AgentResultsPanel.tsx
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useAIStore } from '@/stores/aiStore';
|
| 2 |
+
import { Badge } from '@/components/ui/badge';
|
| 3 |
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
| 4 |
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
| 5 |
+
import { Button } from '@/components/ui/button';
|
| 6 |
+
import { Shield, Zap, Paintbrush, FileText, ChevronDown, ChevronUp } from 'lucide-react';
|
| 7 |
+
import { useEditorStore } from '@/stores/editorStore';
|
| 8 |
+
import { cn } from '@/lib/utils';
|
| 9 |
+
import type { Finding, Severity } from '@glmpilot/shared';
|
| 10 |
+
import { useState } from 'react';
|
| 11 |
+
|
| 12 |
+
const agentIcons: Record<string, typeof Shield> = {
|
| 13 |
+
security: Shield,
|
| 14 |
+
performance: Zap,
|
| 15 |
+
style: Paintbrush,
|
| 16 |
+
documentation: FileText,
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
const severityColors: Record<Severity, string> = {
|
| 20 |
+
critical: 'critical',
|
| 21 |
+
high: 'high',
|
| 22 |
+
medium: 'medium',
|
| 23 |
+
low: 'low',
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
function FindingCard({ finding }: { finding: Finding }) {
|
| 27 |
+
const [expanded, setExpanded] = useState(false);
|
| 28 |
+
const addFile = useEditorStore((s) => s.addFile);
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<div className="liquid-glass rounded-xl p-4 space-y-2">
|
| 32 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 33 |
+
<Badge variant={severityColors[finding.severity] as any}>{finding.severity}</Badge>
|
| 34 |
+
<Badge variant="outline" className="text-[10px]">{finding.category}</Badge>
|
| 35 |
+
<button
|
| 36 |
+
onClick={() => addFile(finding.file, '', 'typescript')}
|
| 37 |
+
className="text-xs text-primary hover:underline truncate"
|
| 38 |
+
>
|
| 39 |
+
{finding.file}{finding.lineStart ? `:${finding.lineStart}` : ''}
|
| 40 |
+
</button>
|
| 41 |
+
</div>
|
| 42 |
+
<p className="text-sm font-medium">{finding.title}</p>
|
| 43 |
+
<p className="text-xs text-muted-foreground line-clamp-2">{finding.description}</p>
|
| 44 |
+
|
| 45 |
+
{(finding.currentCode || finding.fixedCode) && (
|
| 46 |
+
<button
|
| 47 |
+
onClick={() => setExpanded(!expanded)}
|
| 48 |
+
className="flex items-center gap-1 text-xs text-primary hover:underline"
|
| 49 |
+
>
|
| 50 |
+
{expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
| 51 |
+
{expanded ? 'Hide details' : 'Show code'}
|
| 52 |
+
</button>
|
| 53 |
+
)}
|
| 54 |
+
|
| 55 |
+
{expanded && (
|
| 56 |
+
<div className="space-y-2 mt-2">
|
| 57 |
+
{finding.currentCode && (
|
| 58 |
+
<div>
|
| 59 |
+
<p className="text-xs text-muted-foreground mb-1">Current code:</p>
|
| 60 |
+
<pre className="text-xs bg-background rounded p-2 overflow-x-auto">{finding.currentCode}</pre>
|
| 61 |
+
</div>
|
| 62 |
+
)}
|
| 63 |
+
{finding.fixedCode && (
|
| 64 |
+
<div>
|
| 65 |
+
<p className="text-xs text-muted-foreground mb-1">Fixed code:</p>
|
| 66 |
+
<pre className="text-xs bg-background rounded p-2 overflow-x-auto text-primary/80">{finding.fixedCode}</pre>
|
| 67 |
+
</div>
|
| 68 |
+
)}
|
| 69 |
+
{finding.fixExplanation && (
|
| 70 |
+
<p className="text-xs text-muted-foreground">{finding.fixExplanation}</p>
|
| 71 |
+
)}
|
| 72 |
+
</div>
|
| 73 |
+
)}
|
| 74 |
+
</div>
|
| 75 |
+
);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
export default function AgentResultsPanel() {
|
| 79 |
+
const agentResults = useAIStore((s) => s.agentResults);
|
| 80 |
+
|
| 81 |
+
const allFindings: Finding[] = Object.values(agentResults)
|
| 82 |
+
.flatMap((r) => r?.findings || [])
|
| 83 |
+
.sort((a, b) => {
|
| 84 |
+
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
| 85 |
+
return (order[a.severity] || 9) - (order[b.severity] || 9);
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
const byAgent = (agent: string) => allFindings.filter((f) => f.agent === agent);
|
| 89 |
+
|
| 90 |
+
if (allFindings.length === 0) {
|
| 91 |
+
return (
|
| 92 |
+
<div className="h-full flex items-center justify-center text-muted-foreground text-sm p-4 text-center">
|
| 93 |
+
<div>
|
| 94 |
+
<Shield className="w-8 h-8 mx-auto mb-2 text-muted-foreground/40" />
|
| 95 |
+
<p>No review results yet.</p>
|
| 96 |
+
<p className="text-xs mt-1">Run a review to see agent findings here.</p>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
return (
|
| 103 |
+
<div className="h-full flex flex-col">
|
| 104 |
+
{/* Summary */}
|
| 105 |
+
<div className="px-4 py-2 border-b border-border flex items-center gap-3 text-xs">
|
| 106 |
+
<span className="font-medium">{allFindings.length} findings</span>
|
| 107 |
+
<Badge variant="critical" className="text-[10px]">{allFindings.filter(f => f.severity === 'critical').length}</Badge>
|
| 108 |
+
<Badge variant="high" className="text-[10px]">{allFindings.filter(f => f.severity === 'high').length}</Badge>
|
| 109 |
+
<Badge variant="medium" className="text-[10px]">{allFindings.filter(f => f.severity === 'medium').length}</Badge>
|
| 110 |
+
<Badge variant="low" className="text-[10px]">{allFindings.filter(f => f.severity === 'low').length}</Badge>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<Tabs defaultValue="all" className="flex-1 flex flex-col min-h-0">
|
| 114 |
+
<TabsList className="mx-4 mt-2 justify-start h-8">
|
| 115 |
+
<TabsTrigger value="all" className="text-xs">All ({allFindings.length})</TabsTrigger>
|
| 116 |
+
<TabsTrigger value="security" className="text-xs">Security ({byAgent('security').length})</TabsTrigger>
|
| 117 |
+
<TabsTrigger value="performance" className="text-xs">Perf ({byAgent('performance').length})</TabsTrigger>
|
| 118 |
+
<TabsTrigger value="style" className="text-xs">Style ({byAgent('style').length})</TabsTrigger>
|
| 119 |
+
</TabsList>
|
| 120 |
+
|
| 121 |
+
<div className="flex-1 min-h-0 overflow-auto p-4">
|
| 122 |
+
<TabsContent value="all" className="space-y-3 mt-0">
|
| 123 |
+
{allFindings.map((f, i) => <FindingCard key={`${f.id}-${i}`} finding={f} />)}
|
| 124 |
+
</TabsContent>
|
| 125 |
+
{['security', 'performance', 'style'].map((agent) => (
|
| 126 |
+
<TabsContent key={agent} value={agent} className="space-y-3 mt-0">
|
| 127 |
+
{byAgent(agent).map((f, i) => <FindingCard key={`${f.id}-${i}`} finding={f} />)}
|
| 128 |
+
</TabsContent>
|
| 129 |
+
))}
|
| 130 |
+
</div>
|
| 131 |
+
</Tabs>
|
| 132 |
+
</div>
|
| 133 |
+
);
|
| 134 |
+
}
|
packages/client/src/components/ai/MarkdownMessage.tsx
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Check } from 'lucide-react';
|
| 2 |
+
import { parseMarkdown } from '@/utils/markdownParser';
|
| 3 |
+
|
| 4 |
+
interface MarkdownMessageProps {
|
| 5 |
+
content: string;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export function MarkdownMessage({ content }: MarkdownMessageProps) {
|
| 9 |
+
const segments = parseMarkdown(content);
|
| 10 |
+
|
| 11 |
+
return (
|
| 12 |
+
<div className="space-y-3">
|
| 13 |
+
{segments.map((segment, i) => {
|
| 14 |
+
if (segment.type === 'text') {
|
| 15 |
+
return (
|
| 16 |
+
<div key={i} className="whitespace-pre-wrap break-words text-sm">
|
| 17 |
+
{segment.content}
|
| 18 |
+
</div>
|
| 19 |
+
);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div key={i} className="my-3 flex flex-col gap-2">
|
| 24 |
+
{segment.filename && (
|
| 25 |
+
<div className="flex items-center gap-1.5 py-1.5 px-3 bg-primary/10 text-primary border border-primary/20 rounded-md w-fit text-xs font-medium">
|
| 26 |
+
<Check className="w-3.5 h-3.5" />
|
| 27 |
+
Applied changes to {segment.filename}
|
| 28 |
+
</div>
|
| 29 |
+
)}
|
| 30 |
+
<div className="relative rounded-md bg-muted overflow-hidden border border-border">
|
| 31 |
+
<div className="flex items-center justify-between px-3 py-1.5 bg-muted-foreground/10 text-xs text-muted-foreground border-b border-border">
|
| 32 |
+
<span>{segment.filename || segment.language || 'code'}</span>
|
| 33 |
+
</div>
|
| 34 |
+
<pre className="p-3 overflow-x-auto text-[13px] leading-relaxed font-mono text-foreground whitespace-pre-wrap">
|
| 35 |
+
<code>{segment.code}</code>
|
| 36 |
+
</pre>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
);
|
| 40 |
+
})}
|
| 41 |
+
</div>
|
| 42 |
+
);
|
| 43 |
+
}
|
packages/client/src/components/editor/CodeEditor.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef, useCallback, useEffect } from 'react';
|
| 2 |
+
import Editor, { OnMount, OnChange } from '@monaco-editor/react';
|
| 3 |
+
import { useEditorStore } from '@/stores/editorStore';
|
| 4 |
+
import { useSettingsStore } from '@/stores/settingsStore';
|
| 5 |
+
import { useCodeCompletion } from '@/hooks/useCodeCompletion';
|
| 6 |
+
|
| 7 |
+
interface CodeEditorProps {
|
| 8 |
+
filePath: string;
|
| 9 |
+
language: string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default function CodeEditor({ filePath, language }: CodeEditorProps) {
|
| 13 |
+
const content = useEditorStore((s) => s.openFiles[filePath]?.content || '');
|
| 14 |
+
const updateContent = useEditorStore((s) => s.updateContent);
|
| 15 |
+
const setCursorPosition = useEditorStore((s) => s.setCursorPosition);
|
| 16 |
+
const fontSize = useSettingsStore((s) => s.fontSize);
|
| 17 |
+
const tabSize = useSettingsStore((s) => s.tabSize);
|
| 18 |
+
const wordWrap = useSettingsStore((s) => s.wordWrap);
|
| 19 |
+
const minimap = useSettingsStore((s) => s.minimap);
|
| 20 |
+
const { requestCompletion, dismissCompletion } = useCodeCompletion();
|
| 21 |
+
const editorRef = useRef<any>(null);
|
| 22 |
+
|
| 23 |
+
const handleMount: OnMount = (editor, monaco) => {
|
| 24 |
+
editorRef.current = editor;
|
| 25 |
+
|
| 26 |
+
// Define custom theme
|
| 27 |
+
monaco.editor.defineTheme('glmpilot-dark', {
|
| 28 |
+
base: 'vs-dark',
|
| 29 |
+
inherit: true,
|
| 30 |
+
rules: [
|
| 31 |
+
{ token: 'comment', foreground: '6a737d', fontStyle: 'italic' },
|
| 32 |
+
{ token: 'keyword', foreground: 'c792ea' },
|
| 33 |
+
{ token: 'string', foreground: 'c3e88d' },
|
| 34 |
+
{ token: 'number', foreground: 'f78c6c' },
|
| 35 |
+
{ token: 'type', foreground: 'ffcb6b' },
|
| 36 |
+
{ token: 'function', foreground: '82aaff' },
|
| 37 |
+
],
|
| 38 |
+
colors: {
|
| 39 |
+
'editor.background': '#080810',
|
| 40 |
+
'editor.foreground': '#e8e8e8',
|
| 41 |
+
'editorLineNumber.foreground': '#4a4a5a',
|
| 42 |
+
'editor.selectionBackground': '#81f08433',
|
| 43 |
+
'editorCursor.foreground': '#81f084',
|
| 44 |
+
'editor.lineHighlightBackground': '#0f0f1a',
|
| 45 |
+
'editorIndentGuide.background': '#1a1a2a',
|
| 46 |
+
'editorWidget.background': '#121220',
|
| 47 |
+
'editorSuggestWidget.background': '#121220',
|
| 48 |
+
'editorSuggestWidget.border': '#2a2a3a',
|
| 49 |
+
'scrollbarSlider.background': '#2a2a3a80',
|
| 50 |
+
'scrollbarSlider.hoverBackground': '#3a3a4a80',
|
| 51 |
+
},
|
| 52 |
+
});
|
| 53 |
+
monaco.editor.setTheme('glmpilot-dark');
|
| 54 |
+
|
| 55 |
+
// Track cursor position
|
| 56 |
+
editor.onDidChangeCursorPosition((e: any) => {
|
| 57 |
+
setCursorPosition(filePath, {
|
| 58 |
+
lineNumber: e.position.lineNumber,
|
| 59 |
+
column: e.position.column,
|
| 60 |
+
});
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
// Trigger completion on cursor position change
|
| 64 |
+
editor.onDidChangeCursorPosition((e: any) => {
|
| 65 |
+
const model = editor.getModel();
|
| 66 |
+
if (!model) return;
|
| 67 |
+
const position = e.position;
|
| 68 |
+
const textBefore = model.getValueInRange({
|
| 69 |
+
startLineNumber: 1,
|
| 70 |
+
startColumn: 1,
|
| 71 |
+
endLineNumber: position.lineNumber,
|
| 72 |
+
endColumn: position.column,
|
| 73 |
+
});
|
| 74 |
+
const textAfter = model.getValueInRange({
|
| 75 |
+
startLineNumber: position.lineNumber,
|
| 76 |
+
startColumn: position.column,
|
| 77 |
+
endLineNumber: model.getLineCount(),
|
| 78 |
+
endColumn: model.getLineMaxColumn(model.getLineCount()),
|
| 79 |
+
});
|
| 80 |
+
requestCompletion(filePath, textBefore, textAfter, language);
|
| 81 |
+
});
|
| 82 |
+
|
| 83 |
+
editor.focus();
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
const handleChange: OnChange = (value) => {
|
| 87 |
+
if (value !== undefined) {
|
| 88 |
+
updateContent(filePath, value);
|
| 89 |
+
dismissCompletion();
|
| 90 |
+
}
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
return (
|
| 94 |
+
<Editor
|
| 95 |
+
height="100%"
|
| 96 |
+
language={language}
|
| 97 |
+
value={content}
|
| 98 |
+
onChange={handleChange}
|
| 99 |
+
onMount={handleMount}
|
| 100 |
+
theme="glmpilot-dark"
|
| 101 |
+
options={{
|
| 102 |
+
fontSize,
|
| 103 |
+
tabSize,
|
| 104 |
+
wordWrap: wordWrap ? 'on' : 'off',
|
| 105 |
+
minimap: { enabled: minimap, renderCharacters: false },
|
| 106 |
+
formatOnPaste: true,
|
| 107 |
+
suggestOnTriggerCharacters: true,
|
| 108 |
+
quickSuggestions: { other: true, strings: true, comments: false },
|
| 109 |
+
bracketPairColorization: { enabled: true },
|
| 110 |
+
guides: { bracketPairs: true, indentation: true },
|
| 111 |
+
scrollBeyondLastLine: false,
|
| 112 |
+
smoothScrolling: true,
|
| 113 |
+
cursorBlinking: 'smooth',
|
| 114 |
+
cursorSmoothCaretAnimation: 'on',
|
| 115 |
+
renderWhitespace: 'selection',
|
| 116 |
+
lineNumbers: 'on',
|
| 117 |
+
folding: true,
|
| 118 |
+
links: true,
|
| 119 |
+
autoClosingBrackets: 'always',
|
| 120 |
+
autoClosingQuotes: 'always',
|
| 121 |
+
autoIndent: 'advanced',
|
| 122 |
+
padding: { top: 12 },
|
| 123 |
+
}}
|
| 124 |
+
/>
|
| 125 |
+
);
|
| 126 |
+
}
|
packages/client/src/components/editor/EditorTabs.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { X } from 'lucide-react';
|
| 2 |
+
import { useEditorStore } from '@/stores/editorStore';
|
| 3 |
+
import { cn } from '@/lib/utils';
|
| 4 |
+
import { getFileName } from '@glmpilot/shared';
|
| 5 |
+
|
| 6 |
+
export default function EditorTabs() {
|
| 7 |
+
const openFiles = useEditorStore((s) => s.openFiles);
|
| 8 |
+
const activeFilePath = useEditorStore((s) => s.activeFilePath);
|
| 9 |
+
const setActive = useEditorStore((s) => s.setActive);
|
| 10 |
+
const removeFile = useEditorStore((s) => s.removeFile);
|
| 11 |
+
|
| 12 |
+
const files = Object.values(openFiles);
|
| 13 |
+
|
| 14 |
+
if (files.length === 0) return null;
|
| 15 |
+
|
| 16 |
+
return (
|
| 17 |
+
<div className="flex items-center bg-background border-b border-border overflow-x-auto">
|
| 18 |
+
{files.map((file) => (
|
| 19 |
+
<button
|
| 20 |
+
key={file.path}
|
| 21 |
+
onClick={() => setActive(file.path)}
|
| 22 |
+
className={cn(
|
| 23 |
+
'group flex items-center gap-2 px-3 py-1.5 text-xs border-r border-border transition-colors min-w-0 shrink-0',
|
| 24 |
+
file.path === activeFilePath
|
| 25 |
+
? 'bg-card text-foreground'
|
| 26 |
+
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
|
| 27 |
+
)}
|
| 28 |
+
>
|
| 29 |
+
{file.isDirty && (
|
| 30 |
+
<span className="w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
|
| 31 |
+
)}
|
| 32 |
+
<span className="truncate max-w-[120px]">{getFileName(file.path)}</span>
|
| 33 |
+
<span
|
| 34 |
+
onClick={(e) => {
|
| 35 |
+
e.stopPropagation();
|
| 36 |
+
removeFile(file.path);
|
| 37 |
+
}}
|
| 38 |
+
className="opacity-0 group-hover:opacity-100 hover:bg-secondary rounded p-0.5 transition-opacity"
|
| 39 |
+
>
|
| 40 |
+
<X className="w-3 h-3" />
|
| 41 |
+
</span>
|
| 42 |
+
</button>
|
| 43 |
+
))}
|
| 44 |
+
</div>
|
| 45 |
+
);
|
| 46 |
+
}
|
packages/client/src/components/editor/MultiFileEditor.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEditorStore } from '@/stores/editorStore';
|
| 2 |
+
import CodeEditor from './CodeEditor';
|
| 3 |
+
import EditorTabs from './EditorTabs';
|
| 4 |
+
|
| 5 |
+
export default function MultiFileEditor() {
|
| 6 |
+
const activeFilePath = useEditorStore((s) => s.activeFilePath);
|
| 7 |
+
const activeFile = useEditorStore((s) => activeFilePath ? s.openFiles[activeFilePath] : null);
|
| 8 |
+
|
| 9 |
+
return (
|
| 10 |
+
<div className="flex flex-col h-full">
|
| 11 |
+
<EditorTabs />
|
| 12 |
+
<div className="flex-1 min-h-0">
|
| 13 |
+
{activeFile ? (
|
| 14 |
+
<CodeEditor
|
| 15 |
+
key={activeFile.path}
|
| 16 |
+
filePath={activeFile.path}
|
| 17 |
+
language={activeFile.language}
|
| 18 |
+
/>
|
| 19 |
+
) : (
|
| 20 |
+
<div className="h-full flex items-center justify-center text-muted-foreground">
|
| 21 |
+
<div className="text-center">
|
| 22 |
+
<p className="text-lg font-medium">No file open</p>
|
| 23 |
+
<p className="text-sm mt-1">Select a file from the explorer to start editing</p>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
)}
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
);
|
| 30 |
+
}
|
packages/client/src/components/explorer/FileTree.tsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { ChevronRight, ChevronDown, File, Folder, FolderOpen } from 'lucide-react';
|
| 3 |
+
import type { FileNode } from '@glmpilot/shared';
|
| 4 |
+
import { useEditorStore } from '@/stores/editorStore';
|
| 5 |
+
import { useFileStore } from '@/stores/fileStore';
|
| 6 |
+
import { getLanguageFromPath } from '@glmpilot/shared';
|
| 7 |
+
import { cn } from '@/lib/utils';
|
| 8 |
+
|
| 9 |
+
const FILE_ICONS: Record<string, string> = {
|
| 10 |
+
html: '🌐', css: '🎨', scss: '🎨', javascript: '⚡', typescript: '💠',
|
| 11 |
+
javascriptreact: '⚛️', typescriptreact: '⚛️', json: '📋', markdown: '📝', svg: '🖼️',
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
interface FileTreeItemProps {
|
| 15 |
+
node: FileNode;
|
| 16 |
+
depth: number;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function FileTreeItem({ node, depth }: FileTreeItemProps) {
|
| 20 |
+
const [expanded, setExpanded] = useState(depth < 2);
|
| 21 |
+
const addFile = useEditorStore((s) => s.addFile);
|
| 22 |
+
const activeFilePath = useEditorStore((s) => s.activeFilePath);
|
| 23 |
+
const files = useFileStore((s) => s.files);
|
| 24 |
+
|
| 25 |
+
const handleClick = () => {
|
| 26 |
+
if (node.type === 'directory') {
|
| 27 |
+
setExpanded(!expanded);
|
| 28 |
+
} else {
|
| 29 |
+
const language = getLanguageFromPath(node.path);
|
| 30 |
+
const content = files[node.path] || '';
|
| 31 |
+
addFile(node.path, content, language);
|
| 32 |
+
}
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
const isActive = node.path === activeFilePath;
|
| 36 |
+
const lang = node.type === 'file' ? getLanguageFromPath(node.path) : '';
|
| 37 |
+
const icon = FILE_ICONS[lang] || '';
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div>
|
| 41 |
+
<button
|
| 42 |
+
onClick={handleClick}
|
| 43 |
+
className={cn(
|
| 44 |
+
'w-full flex items-center gap-1 px-2 py-0.5 text-sm hover:bg-secondary/50 transition-colors rounded-sm',
|
| 45 |
+
isActive && 'bg-secondary text-foreground'
|
| 46 |
+
)}
|
| 47 |
+
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
| 48 |
+
>
|
| 49 |
+
{node.type === 'directory' ? (
|
| 50 |
+
<>
|
| 51 |
+
{expanded ? (
|
| 52 |
+
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
|
| 53 |
+
) : (
|
| 54 |
+
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
|
| 55 |
+
)}
|
| 56 |
+
{expanded ? (
|
| 57 |
+
<FolderOpen className="w-4 h-4 text-primary/70 shrink-0" />
|
| 58 |
+
) : (
|
| 59 |
+
<Folder className="w-4 h-4 text-primary/70 shrink-0" />
|
| 60 |
+
)}
|
| 61 |
+
</>
|
| 62 |
+
) : (
|
| 63 |
+
<>
|
| 64 |
+
<span className="w-3.5 shrink-0" />
|
| 65 |
+
{icon ? (
|
| 66 |
+
<span className="text-xs shrink-0">{icon}</span>
|
| 67 |
+
) : (
|
| 68 |
+
<File className="w-4 h-4 text-muted-foreground shrink-0" />
|
| 69 |
+
)}
|
| 70 |
+
</>
|
| 71 |
+
)}
|
| 72 |
+
<span className={cn('truncate', isActive ? 'text-foreground' : 'text-foreground/70')}>
|
| 73 |
+
{node.name}
|
| 74 |
+
</span>
|
| 75 |
+
</button>
|
| 76 |
+
|
| 77 |
+
{node.type === 'directory' && expanded && node.children && (
|
| 78 |
+
<div>
|
| 79 |
+
{node.children
|
| 80 |
+
.sort((a, b) => {
|
| 81 |
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
| 82 |
+
return a.name.localeCompare(b.name);
|
| 83 |
+
})
|
| 84 |
+
.map((child) => (
|
| 85 |
+
<FileTreeItem key={child.path} node={child} depth={depth + 1} />
|
| 86 |
+
))}
|
| 87 |
+
</div>
|
| 88 |
+
)}
|
| 89 |
+
</div>
|
| 90 |
+
);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
export default function FileTree() {
|
| 94 |
+
const fileTree = useFileStore((s) => s.fileTree);
|
| 95 |
+
|
| 96 |
+
return (
|
| 97 |
+
<div className="py-1">
|
| 98 |
+
{fileTree
|
| 99 |
+
.sort((a, b) => {
|
| 100 |
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
| 101 |
+
return a.name.localeCompare(b.name);
|
| 102 |
+
})
|
| 103 |
+
.map((node) => (
|
| 104 |
+
<FileTreeItem key={node.path} node={node} depth={0} />
|
| 105 |
+
))}
|
| 106 |
+
</div>
|
| 107 |
+
);
|
| 108 |
+
}
|
packages/client/src/components/github/RepoImporter.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { useGitHub } from '@/hooks/useGitHub';
|
| 3 |
+
import { useGitHubStore } from '@/stores/githubStore';
|
| 4 |
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
| 5 |
+
import { Input } from '@/components/ui/input';
|
| 6 |
+
import { Button } from '@/components/ui/button';
|
| 7 |
+
import { Github, Loader2 } from 'lucide-react';
|
| 8 |
+
|
| 9 |
+
interface RepoImporterProps {
|
| 10 |
+
open: boolean;
|
| 11 |
+
onClose: () => void;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default function RepoImporter({ open, onClose }: RepoImporterProps) {
|
| 15 |
+
const [url, setUrl] = useState('');
|
| 16 |
+
const { importRepo, isImporting } = useGitHub();
|
| 17 |
+
const importProgress = useGitHubStore((s) => s.importProgress);
|
| 18 |
+
const [error, setError] = useState('');
|
| 19 |
+
|
| 20 |
+
const handleImport = async () => {
|
| 21 |
+
if (!url.trim()) return;
|
| 22 |
+
setError('');
|
| 23 |
+
try {
|
| 24 |
+
await importRepo(url.trim());
|
| 25 |
+
onClose();
|
| 26 |
+
} catch (err) {
|
| 27 |
+
setError((err as Error).message || 'Failed to import repository');
|
| 28 |
+
}
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
return (
|
| 32 |
+
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
| 33 |
+
<DialogContent>
|
| 34 |
+
<DialogHeader>
|
| 35 |
+
<DialogTitle className="flex items-center gap-2">
|
| 36 |
+
<Github className="w-5 h-5" />
|
| 37 |
+
Import from GitHub
|
| 38 |
+
</DialogTitle>
|
| 39 |
+
<DialogDescription>
|
| 40 |
+
Paste a GitHub repository URL to import and analyze.
|
| 41 |
+
</DialogDescription>
|
| 42 |
+
</DialogHeader>
|
| 43 |
+
|
| 44 |
+
<div className="space-y-4 mt-2">
|
| 45 |
+
<Input
|
| 46 |
+
value={url}
|
| 47 |
+
onChange={(e) => setUrl(e.target.value)}
|
| 48 |
+
placeholder="https://github.com/owner/repo or owner/repo"
|
| 49 |
+
disabled={isImporting}
|
| 50 |
+
onKeyDown={(e) => e.key === 'Enter' && handleImport()}
|
| 51 |
+
/>
|
| 52 |
+
|
| 53 |
+
{error && <p className="text-sm text-red-400">{error}</p>}
|
| 54 |
+
|
| 55 |
+
{isImporting && (
|
| 56 |
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
| 57 |
+
<Loader2 className="w-4 h-4 animate-spin" />
|
| 58 |
+
{importProgress || 'Importing...'}
|
| 59 |
+
</div>
|
| 60 |
+
)}
|
| 61 |
+
|
| 62 |
+
<div className="flex justify-end gap-2">
|
| 63 |
+
<Button variant="ghost" onClick={onClose} disabled={isImporting}>
|
| 64 |
+
Cancel
|
| 65 |
+
</Button>
|
| 66 |
+
<Button onClick={handleImport} disabled={!url.trim() || isImporting}>
|
| 67 |
+
{isImporting ? 'Importing...' : 'Import Repository'}
|
| 68 |
+
</Button>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
</DialogContent>
|
| 72 |
+
</Dialog>
|
| 73 |
+
);
|
| 74 |
+
}
|
packages/client/src/components/ide/EnvironmentSelector.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Monitor, Code2, Terminal } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
export default function EnvironmentSelector() {
|
| 6 |
+
const navigate = useNavigate();
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<div className="min-h-screen flex flex-col items-center justify-center bg-zinc-950 text-zinc-100 p-4">
|
| 10 |
+
<div className="max-w-3xl w-full">
|
| 11 |
+
<h1 className="text-3xl font-bold text-center mb-2">Select Environment</h1>
|
| 12 |
+
<p className="text-center text-zinc-400 mb-10">
|
| 13 |
+
Choose your project type to launch the IDE
|
| 14 |
+
</p>
|
| 15 |
+
|
| 16 |
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
| 17 |
+
<EnvCard
|
| 18 |
+
title="Web Development"
|
| 19 |
+
description="HTML, CSS, JS/TS, React with Live Preview"
|
| 20 |
+
icon={<Monitor className="w-10 h-10 mb-4 text-blue-400" />}
|
| 21 |
+
onClick={() => navigate('/ide/web')}
|
| 22 |
+
/>
|
| 23 |
+
<EnvCard
|
| 24 |
+
title="Java"
|
| 25 |
+
description="Java JDK 17 with Console Output"
|
| 26 |
+
icon={<Code2 className="w-10 h-10 mb-4 text-orange-400" />}
|
| 27 |
+
onClick={() => navigate('/ide/java')}
|
| 28 |
+
/>
|
| 29 |
+
<EnvCard
|
| 30 |
+
title="Python"
|
| 31 |
+
description="Python 3 Environment with Console Output"
|
| 32 |
+
icon={<Terminal className="w-10 h-10 mb-4 text-green-400" />}
|
| 33 |
+
onClick={() => navigate('/ide/python')}
|
| 34 |
+
/>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function EnvCard({ title, description, icon, onClick }: { title: string, description: string, icon: React.ReactNode, onClick: () => void }) {
|
| 42 |
+
return (
|
| 43 |
+
<div
|
| 44 |
+
onClick={onClick}
|
| 45 |
+
className="flex flex-col items-center justify-center p-8 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 hover:bg-zinc-800/80 cursor-pointer rounded-xl transition-all duration-200 shadow-sm"
|
| 46 |
+
>
|
| 47 |
+
{icon}
|
| 48 |
+
<h3 className="text-xl font-medium mb-3">{title}</h3>
|
| 49 |
+
<p className="text-sm text-center text-zinc-400 leading-relaxed">{description}</p>
|
| 50 |
+
</div>
|
| 51 |
+
);
|
| 52 |
+
}
|
packages/client/src/components/landing/CTAFooterWrapper.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import CTASection from './CTASection';
|
| 2 |
+
import FooterSection from './FooterSection';
|
| 3 |
+
import HLSVideo from './HLSVideo';
|
| 4 |
+
|
| 5 |
+
export default function CTAFooterWrapper() {
|
| 6 |
+
return (
|
| 7 |
+
<section className="relative overflow-hidden">
|
| 8 |
+
{/* Background HLS Video */}
|
| 9 |
+
<HLSVideo
|
| 10 |
+
src="https://stream.mux.com/tLkHO1qZoaaQOUeVWo8hEBeGQfySP02EPS02BmnNFyXys.m3u8"
|
| 11 |
+
className="absolute inset-0 w-full h-full object-cover z-0"
|
| 12 |
+
/>
|
| 13 |
+
|
| 14 |
+
{/* Gradient Overlay */}
|
| 15 |
+
<div
|
| 16 |
+
className="absolute inset-0 z-[1]"
|
| 17 |
+
style={{
|
| 18 |
+
background:
|
| 19 |
+
'linear-gradient(to bottom, hsl(260 87% 3%) 0%, hsl(260 87% 3% / 0.85) 15%, hsl(260 87% 3% / 0.4) 40%, hsl(260 87% 3% / 0.15) 60%, hsl(260 87% 3% / 0.3) 100%)',
|
| 20 |
+
}}
|
| 21 |
+
/>
|
| 22 |
+
|
| 23 |
+
<CTASection />
|
| 24 |
+
<FooterSection />
|
| 25 |
+
</section>
|
| 26 |
+
);
|
| 27 |
+
}
|
packages/client/src/components/landing/CTASection.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from 'react-router-dom';
|
| 2 |
+
import { Button } from '@/components/ui/button';
|
| 3 |
+
|
| 4 |
+
export default function CTASection() {
|
| 5 |
+
return (
|
| 6 |
+
<div className="relative z-10 py-32 px-4">
|
| 7 |
+
<div className="liquid-glass rounded-[2rem] p-12 sm:p-20 max-w-4xl mx-auto text-center">
|
| 8 |
+
<h2 className="text-hero-heading text-3xl sm:text-5xl font-semibold">
|
| 9 |
+
Ready to Build
|
| 10 |
+
<br />
|
| 11 |
+
Something Amazing?
|
| 12 |
+
</h2>
|
| 13 |
+
<p className="text-hero-sub mt-4 max-w-lg mx-auto">
|
| 14 |
+
Join thousands of frontend developers using AI-powered code review and intelligent completions. Free to start.
|
| 15 |
+
</p>
|
| 16 |
+
<div className="flex justify-center gap-4 mt-8">
|
| 17 |
+
<Link to="/ide">
|
| 18 |
+
<Button variant="hero">Launch GLMPilot</Button>
|
| 19 |
+
</Link>
|
| 20 |
+
<Button variant="heroSecondary">View on GitHub</Button>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
);
|
| 25 |
+
}
|
packages/client/src/components/landing/ChessSection.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from 'react-router-dom';
|
| 2 |
+
import { Button } from '@/components/ui/button';
|
| 3 |
+
import SectionBadge from './SectionBadge';
|
| 4 |
+
import HLSVideo from './HLSVideo';
|
| 5 |
+
|
| 6 |
+
export default function ChessSection() {
|
| 7 |
+
return (
|
| 8 |
+
<section className="py-32 px-4 max-w-6xl mx-auto">
|
| 9 |
+
<div className="grid lg:grid-cols-2 gap-20 items-center">
|
| 10 |
+
{/* Left — Video */}
|
| 11 |
+
<div className="liquid-glass rounded-3xl aspect-[4/3] overflow-hidden">
|
| 12 |
+
<HLSVideo
|
| 13 |
+
src="https://stream.mux.com/1CCfG6mPC7LbMOAs6iBOfPeNd3WaKlZuHuKHp00G62j8.m3u8"
|
| 14 |
+
className="w-full h-full object-cover"
|
| 15 |
+
/>
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
{/* Right — Content */}
|
| 19 |
+
<div>
|
| 20 |
+
<SectionBadge label="Inline AI" action="New" />
|
| 21 |
+
<h2 className="text-hero-heading text-3xl sm:text-4xl font-semibold leading-tight mt-6">
|
| 22 |
+
Code Completions That
|
| 23 |
+
<br />
|
| 24 |
+
Actually Understand Context
|
| 25 |
+
</h2>
|
| 26 |
+
<p className="text-hero-sub mt-4 leading-relaxed">
|
| 27 |
+
Powered by ASI-1 Mini, get intelligent inline suggestions that consider your entire
|
| 28 |
+
workspace — not just the current file. Accept with Tab, dismiss with Escape.
|
| 29 |
+
</p>
|
| 30 |
+
<ul className="mt-6 space-y-3">
|
| 31 |
+
{[
|
| 32 |
+
'Full workspace context awareness',
|
| 33 |
+
'Framework-specific suggestions',
|
| 34 |
+
'Learns your coding patterns',
|
| 35 |
+
].map((item) => (
|
| 36 |
+
<li key={item} className="flex items-center gap-3">
|
| 37 |
+
<span className="w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
|
| 38 |
+
<span className="text-foreground/80">{item}</span>
|
| 39 |
+
</li>
|
| 40 |
+
))}
|
| 41 |
+
</ul>
|
| 42 |
+
<div className="flex gap-4 mt-8">
|
| 43 |
+
<Link to="/ide">
|
| 44 |
+
<Button variant="hero">Try It Now</Button>
|
| 45 |
+
</Link>
|
| 46 |
+
<Button variant="heroSecondary">Read the Docs</Button>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</section>
|
| 51 |
+
);
|
| 52 |
+
}
|
packages/client/src/components/landing/FeaturesSection.tsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Shield, Zap, FileText } from 'lucide-react';
|
| 2 |
+
import SectionBadge from './SectionBadge';
|
| 3 |
+
import HLSVideo from './HLSVideo';
|
| 4 |
+
|
| 5 |
+
const features = [
|
| 6 |
+
{
|
| 7 |
+
icon: Shield,
|
| 8 |
+
title: 'Security Agent',
|
| 9 |
+
description: 'Scans for XSS, CSRF, secrets exposure, and 15+ vulnerability categories. Get OWASP-referenced findings with one-click fixes.',
|
| 10 |
+
stat: '< 30s',
|
| 11 |
+
statLabel: 'full repo scan',
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
icon: Zap,
|
| 15 |
+
title: 'Performance Agent',
|
| 16 |
+
description: 'Detects memory leaks, unnecessary re-renders, bundle bloat, and missing optimizations. Every finding includes before/after code.',
|
| 17 |
+
stat: '148%',
|
| 18 |
+
statLabel: 'avg. performance improvement',
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
icon: FileText,
|
| 22 |
+
title: 'Auto Documentation',
|
| 23 |
+
description: 'Generates README, component API docs, and architecture overviews from your code. Never write docs from scratch again.',
|
| 24 |
+
stat: '100%',
|
| 25 |
+
statLabel: 'documentation coverage',
|
| 26 |
+
},
|
| 27 |
+
];
|
| 28 |
+
|
| 29 |
+
export default function FeaturesSection() {
|
| 30 |
+
return (
|
| 31 |
+
<section className="relative overflow-hidden">
|
| 32 |
+
{/* Background HLS Video */}
|
| 33 |
+
<HLSVideo
|
| 34 |
+
src="https://stream.mux.com/Jwr2RhmsNrd6GEspBNgm02vJsRZAGlaoQIh4AucGdASw.m3u8"
|
| 35 |
+
className="absolute inset-0 w-full h-full object-cover z-0"
|
| 36 |
+
/>
|
| 37 |
+
|
| 38 |
+
{/* Gradient Overlays */}
|
| 39 |
+
<div className="absolute top-0 left-0 right-0 h-[40%] bg-gradient-to-b from-background via-background/80 to-transparent z-[1]" />
|
| 40 |
+
<div className="absolute bottom-0 left-0 right-0 h-[40%] bg-gradient-to-t from-background via-background/80 to-transparent z-[1]" />
|
| 41 |
+
<div className="absolute inset-0 bg-background/40 z-[1]" />
|
| 42 |
+
|
| 43 |
+
{/* Content */}
|
| 44 |
+
<div className="relative z-10 py-32 px-4 max-w-6xl mx-auto">
|
| 45 |
+
<SectionBadge label="Core Platform" action="Overview" />
|
| 46 |
+
|
| 47 |
+
<h2 className="text-hero-heading text-3xl sm:text-5xl font-semibold text-center mt-6">
|
| 48 |
+
Built for Developers Who
|
| 49 |
+
<br />
|
| 50 |
+
Ship Relentlessly
|
| 51 |
+
</h2>
|
| 52 |
+
|
| 53 |
+
<p className="text-hero-sub text-center max-w-xl mx-auto mt-4">
|
| 54 |
+
Four AI agents that keep your codebase clean, secure, and well-documented without slowing you down.
|
| 55 |
+
</p>
|
| 56 |
+
|
| 57 |
+
<div className="grid md:grid-cols-3 gap-6 mt-16">
|
| 58 |
+
{features.map((feature) => (
|
| 59 |
+
<div
|
| 60 |
+
key={feature.title}
|
| 61 |
+
className="liquid-glass rounded-3xl p-8 flex flex-col transition-colors hover:bg-white/[0.03]"
|
| 62 |
+
>
|
| 63 |
+
<feature.icon className="text-primary w-8 h-8" />
|
| 64 |
+
<h3 className="text-hero-heading text-lg font-semibold mt-4">{feature.title}</h3>
|
| 65 |
+
<p className="text-muted-foreground text-sm mt-2 leading-relaxed flex-1">
|
| 66 |
+
{feature.description}
|
| 67 |
+
</p>
|
| 68 |
+
<div className="border-t border-border/50 my-6" />
|
| 69 |
+
<div>
|
| 70 |
+
<span className="text-2xl font-semibold text-hero-heading">{feature.stat}</span>
|
| 71 |
+
<span className="text-sm text-muted-foreground ml-2">{feature.statLabel}</span>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
))}
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</section>
|
| 78 |
+
);
|
| 79 |
+
}
|
packages/client/src/components/landing/FooterSection.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from 'react-router-dom';
|
| 2 |
+
import { Terminal } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
const footerLinks = {
|
| 5 |
+
Product: ['Features', 'Pricing', 'Agents', 'Changelog', 'Roadmap'],
|
| 6 |
+
Developers: ['Documentation', 'API Reference', 'Examples', 'Community'],
|
| 7 |
+
Company: ['About', 'Blog', 'Careers', 'Contact'],
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
export default function FooterSection() {
|
| 11 |
+
return (
|
| 12 |
+
<footer className="relative z-10 mt-20">
|
| 13 |
+
<div className="max-w-6xl mx-auto px-4 border-t border-border/30 pt-16 pb-8">
|
| 14 |
+
<div className="grid grid-cols-2 md:grid-cols-5 gap-8">
|
| 15 |
+
{/* Logo Column */}
|
| 16 |
+
<div className="col-span-2">
|
| 17 |
+
<Link to="/" className="flex items-center gap-2">
|
| 18 |
+
<div className="w-7 h-7 rounded-lg bg-gradient-to-b from-secondary to-muted flex items-center justify-center">
|
| 19 |
+
<Terminal className="w-4 h-4 text-primary" />
|
| 20 |
+
</div>
|
| 21 |
+
<span className="text-xl font-semibold tracking-tight">GLMPilot</span>
|
| 22 |
+
</Link>
|
| 23 |
+
<p className="text-muted-foreground text-sm mt-4 max-w-xs">
|
| 24 |
+
AI-powered browser IDE for frontend developers. Build, review, and ship with confidence.
|
| 25 |
+
</p>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
{/* Link Columns */}
|
| 29 |
+
{Object.entries(footerLinks).map(([title, links]) => (
|
| 30 |
+
<div key={title}>
|
| 31 |
+
<h4 className="text-sm font-semibold text-foreground mb-4">{title}</h4>
|
| 32 |
+
<ul className="space-y-2.5">
|
| 33 |
+
{links.map((link) => (
|
| 34 |
+
<li key={link}>
|
| 35 |
+
<a href="#" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
|
| 36 |
+
{link}
|
| 37 |
+
</a>
|
| 38 |
+
</li>
|
| 39 |
+
))}
|
| 40 |
+
</ul>
|
| 41 |
+
</div>
|
| 42 |
+
))}
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
{/* Bottom Bar */}
|
| 46 |
+
<div className="border-t border-border/30 mt-12 pt-8 flex flex-col sm:flex-row justify-between items-center gap-4">
|
| 47 |
+
<p className="text-sm text-muted-foreground">© 2025 GLMPilot</p>
|
| 48 |
+
<div className="flex gap-6">
|
| 49 |
+
{['Privacy', 'Terms', 'Cookies'].map((item) => (
|
| 50 |
+
<a key={item} href="#" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
|
| 51 |
+
{item}
|
| 52 |
+
</a>
|
| 53 |
+
))}
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</footer>
|
| 58 |
+
);
|
| 59 |
+
}
|
packages/client/src/components/landing/HLSVideo.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useRef } from 'react';
|
| 2 |
+
import Hls from 'hls.js';
|
| 3 |
+
|
| 4 |
+
interface HLSVideoProps {
|
| 5 |
+
src: string;
|
| 6 |
+
className?: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export default function HLSVideo({ src, className = '' }: HLSVideoProps) {
|
| 10 |
+
const videoRef = useRef<HTMLVideoElement>(null);
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
const video = videoRef.current;
|
| 14 |
+
if (!video) return;
|
| 15 |
+
|
| 16 |
+
if (Hls.isSupported()) {
|
| 17 |
+
const hls = new Hls({
|
| 18 |
+
enableWorker: true,
|
| 19 |
+
lowLatencyMode: false,
|
| 20 |
+
});
|
| 21 |
+
hls.loadSource(src);
|
| 22 |
+
hls.attachMedia(video);
|
| 23 |
+
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
| 24 |
+
video.play().catch(() => {});
|
| 25 |
+
});
|
| 26 |
+
return () => hls.destroy();
|
| 27 |
+
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
| 28 |
+
video.src = src;
|
| 29 |
+
video.addEventListener('loadedmetadata', () => {
|
| 30 |
+
video.play().catch(() => {});
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
}, [src]);
|
| 34 |
+
|
| 35 |
+
return (
|
| 36 |
+
<video
|
| 37 |
+
ref={videoRef}
|
| 38 |
+
className={className}
|
| 39 |
+
autoPlay
|
| 40 |
+
loop
|
| 41 |
+
muted
|
| 42 |
+
playsInline
|
| 43 |
+
/>
|
| 44 |
+
);
|
| 45 |
+
}
|
packages/client/src/components/landing/HeroSection.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from 'react-router-dom';
|
| 2 |
+
import { ChevronRight } from 'lucide-react';
|
| 3 |
+
import { Button } from '@/components/ui/button';
|
| 4 |
+
import Navbar from './Navbar';
|
| 5 |
+
|
| 6 |
+
export default function HeroSection() {
|
| 7 |
+
return (
|
| 8 |
+
<section className="relative min-h-screen flex flex-col overflow-hidden">
|
| 9 |
+
{/* Background Video */}
|
| 10 |
+
<video
|
| 11 |
+
autoPlay
|
| 12 |
+
loop
|
| 13 |
+
muted
|
| 14 |
+
playsInline
|
| 15 |
+
className="absolute inset-0 w-full h-full object-cover"
|
| 16 |
+
>
|
| 17 |
+
<source
|
| 18 |
+
src="https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260309_042944_4a2205b7-b061-490a-852b-92d9e9955ce9.mp4"
|
| 19 |
+
type="video/mp4"
|
| 20 |
+
/>
|
| 21 |
+
</video>
|
| 22 |
+
|
| 23 |
+
{/* Gradient Overlay */}
|
| 24 |
+
<div
|
| 25 |
+
className="absolute inset-0 pointer-events-none z-[1]"
|
| 26 |
+
style={{
|
| 27 |
+
background:
|
| 28 |
+
'linear-gradient(to bottom, transparent 0%, transparent 30%, hsl(260 87% 3% / 0.1) 45%, hsl(260 87% 3% / 0.4) 60%, hsl(260 87% 3% / 0.75) 75%, hsl(260 87% 3%) 95%)',
|
| 29 |
+
}}
|
| 30 |
+
/>
|
| 31 |
+
|
| 32 |
+
{/* Content */}
|
| 33 |
+
<div className="relative z-10 flex flex-col min-h-screen">
|
| 34 |
+
<Navbar />
|
| 35 |
+
|
| 36 |
+
<div className="flex-1 flex flex-col items-center justify-center px-4">
|
| 37 |
+
{/* Announcement Badge */}
|
| 38 |
+
<div className="liquid-glass rounded-full px-4 py-1.5 inline-flex items-center gap-2 text-sm mb-6">
|
| 39 |
+
<span className="text-foreground/80">AI Code Review</span>
|
| 40 |
+
<span className="bg-white/10 rounded-full px-2.5 py-0.5 text-xs inline-flex items-center gap-1 text-foreground/60">
|
| 41 |
+
Explore
|
| 42 |
+
<ChevronRight className="w-3 h-3" />
|
| 43 |
+
</span>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
{/* Heading */}
|
| 47 |
+
<h1 className="text-hero-heading text-4xl sm:text-6xl lg:text-7xl font-semibold leading-[1.05] tracking-tight text-center max-w-5xl mx-auto">
|
| 48 |
+
Build, Review, and Ship
|
| 49 |
+
<br />
|
| 50 |
+
Frontend Code Faster
|
| 51 |
+
</h1>
|
| 52 |
+
|
| 53 |
+
{/* Subheading */}
|
| 54 |
+
<p className="text-hero-sub text-lg max-w-md text-center mx-auto mt-4 opacity-80">
|
| 55 |
+
A browser-based IDE with AI-powered multi-agent code review, inline completions, and one-click GitHub integration.
|
| 56 |
+
</p>
|
| 57 |
+
|
| 58 |
+
{/* CTA Buttons */}
|
| 59 |
+
<div className="flex justify-center gap-4 mt-8">
|
| 60 |
+
<Link to="/ide">
|
| 61 |
+
<Button variant="hero">Launch IDE</Button>
|
| 62 |
+
</Link>
|
| 63 |
+
<Button variant="heroSecondary">Watch Demo</Button>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</section>
|
| 68 |
+
);
|
| 69 |
+
}
|
packages/client/src/components/landing/LandingPage.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import HeroSection from './HeroSection';
|
| 2 |
+
import FeaturesSection from './FeaturesSection';
|
| 3 |
+
import ChessSection from './ChessSection';
|
| 4 |
+
import ReverseChessSection from './ReverseChessSection';
|
| 5 |
+
import NumbersSection from './NumbersSection';
|
| 6 |
+
import CTAFooterWrapper from './CTAFooterWrapper';
|
| 7 |
+
|
| 8 |
+
export default function LandingPage() {
|
| 9 |
+
return (
|
| 10 |
+
<main className="bg-background min-h-screen overflow-x-hidden">
|
| 11 |
+
<HeroSection />
|
| 12 |
+
<FeaturesSection />
|
| 13 |
+
<ChessSection />
|
| 14 |
+
<ReverseChessSection />
|
| 15 |
+
<NumbersSection />
|
| 16 |
+
<CTAFooterWrapper />
|
| 17 |
+
</main>
|
| 18 |
+
);
|
| 19 |
+
}
|
packages/client/src/components/landing/MarqueeRow.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interface MarqueeRowProps {
|
| 2 |
+
brands: Array<{ name: string; initial: string }>;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
export default function MarqueeRow({ brands }: MarqueeRowProps) {
|
| 6 |
+
const duplicated = [...brands, ...brands];
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<div className="overflow-hidden relative">
|
| 10 |
+
<div className="flex items-center gap-8 animate-marquee w-max">
|
| 11 |
+
{duplicated.map((brand, i) => (
|
| 12 |
+
<div key={`${brand.name}-${i}`} className="flex items-center gap-2 shrink-0">
|
| 13 |
+
<div className="liquid-glass w-6 h-6 rounded-lg inline-flex items-center justify-center">
|
| 14 |
+
<span className="text-xs font-medium text-foreground/70">{brand.initial}</span>
|
| 15 |
+
</div>
|
| 16 |
+
<span className="text-sm text-muted-foreground whitespace-nowrap">{brand.name}</span>
|
| 17 |
+
</div>
|
| 18 |
+
))}
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
);
|
| 22 |
+
}
|
packages/client/src/components/landing/Navbar.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from 'react-router-dom';
|
| 2 |
+
import { Terminal, ChevronDown } from 'lucide-react';
|
| 3 |
+
import { Button } from '@/components/ui/button';
|
| 4 |
+
|
| 5 |
+
export default function Navbar() {
|
| 6 |
+
return (
|
| 7 |
+
<nav className="relative z-20 flex justify-center pt-4 px-4">
|
| 8 |
+
<div className="liquid-glass rounded-3xl px-6 py-3 flex items-center justify-between w-full max-w-[850px]">
|
| 9 |
+
{/* Logo */}
|
| 10 |
+
<Link to="/" className="flex items-center gap-2">
|
| 11 |
+
<div className="w-7 h-7 rounded-lg bg-gradient-to-b from-secondary to-muted flex items-center justify-center">
|
| 12 |
+
<Terminal className="w-4 h-4 text-primary" />
|
| 13 |
+
</div>
|
| 14 |
+
<span className="text-xl font-semibold tracking-tight text-foreground">GLMPilot</span>
|
| 15 |
+
</Link>
|
| 16 |
+
|
| 17 |
+
{/* Nav Items */}
|
| 18 |
+
<div className="hidden md:flex items-center gap-6">
|
| 19 |
+
<button className="text-sm text-foreground/70 hover:text-foreground transition-colors">Features</button>
|
| 20 |
+
<button className="text-sm text-foreground/70 hover:text-foreground transition-colors inline-flex items-center gap-1">
|
| 21 |
+
Agents <ChevronDown className="w-3 h-3" />
|
| 22 |
+
</button>
|
| 23 |
+
<button className="text-sm text-foreground/70 hover:text-foreground transition-colors">Pricing</button>
|
| 24 |
+
<button className="text-sm text-foreground/70 hover:text-foreground transition-colors inline-flex items-center gap-1">
|
| 25 |
+
Docs <ChevronDown className="w-3 h-3" />
|
| 26 |
+
</button>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
{/* CTA */}
|
| 30 |
+
<Link to="/ide">
|
| 31 |
+
<Button variant="hero" size="sm">Get Started</Button>
|
| 32 |
+
</Link>
|
| 33 |
+
</div>
|
| 34 |
+
</nav>
|
| 35 |
+
);
|
| 36 |
+
}
|
packages/client/src/components/landing/NumbersSection.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import HLSVideo from './HLSVideo';
|
| 2 |
+
|
| 3 |
+
export default function NumbersSection() {
|
| 4 |
+
return (
|
| 5 |
+
<section className="relative overflow-hidden">
|
| 6 |
+
{/* Background HLS Video */}
|
| 7 |
+
<HLSVideo
|
| 8 |
+
src="https://stream.mux.com/Kec29dVyJgiPdtWaQtPuEiiGHkJIYQAVUJcNiIHUYeo.m3u8"
|
| 9 |
+
className="absolute inset-0 w-full h-full object-cover z-0"
|
| 10 |
+
/>
|
| 11 |
+
|
| 12 |
+
{/* Gradient Overlay */}
|
| 13 |
+
<div
|
| 14 |
+
className="absolute inset-0 z-[1]"
|
| 15 |
+
style={{
|
| 16 |
+
background:
|
| 17 |
+
'linear-gradient(to top, hsl(260 87% 3%) 0%, hsl(260 87% 3% / 0.85) 15%, hsl(260 87% 3% / 0.4) 40%, hsl(260 87% 3% / 0.15) 60%, hsl(260 87% 3% / 0.3) 100%)',
|
| 18 |
+
}}
|
| 19 |
+
/>
|
| 20 |
+
|
| 21 |
+
{/* Content */}
|
| 22 |
+
<div className="relative z-10 py-32 max-w-6xl mx-auto text-center px-4">
|
| 23 |
+
<p className="text-7xl sm:text-[8rem] lg:text-[10rem] font-semibold tracking-tighter text-hero-heading leading-none">
|
| 24 |
+
50K+
|
| 25 |
+
</p>
|
| 26 |
+
<p className="text-primary text-lg font-medium mt-2">Lines of code reviewed</p>
|
| 27 |
+
<p className="text-hero-sub max-w-md mx-auto mt-4">
|
| 28 |
+
Every day, GLMPilot's AI agents analyze thousands of files to keep codebases secure and performant.
|
| 29 |
+
</p>
|
| 30 |
+
|
| 31 |
+
<div className="mt-24">
|
| 32 |
+
<div className="liquid-glass rounded-3xl p-12 grid md:grid-cols-2 max-w-3xl mx-auto">
|
| 33 |
+
<div className="md:border-r border-border/50 md:pr-12">
|
| 34 |
+
<p className="text-5xl font-semibold text-hero-heading">4</p>
|
| 35 |
+
<p className="text-hero-sub mt-2">Specialized AI agents</p>
|
| 36 |
+
</div>
|
| 37 |
+
<div className="md:pl-12 mt-8 md:mt-0">
|
| 38 |
+
<p className="text-5xl font-semibold text-hero-heading">99.9%</p>
|
| 39 |
+
<p className="text-hero-sub mt-2">API uptime</p>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</section>
|
| 45 |
+
);
|
| 46 |
+
}
|
packages/client/src/components/landing/ReverseChessSection.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Button } from '@/components/ui/button';
|
| 2 |
+
import SectionBadge from './SectionBadge';
|
| 3 |
+
import HLSVideo from './HLSVideo';
|
| 4 |
+
|
| 5 |
+
const stats = [
|
| 6 |
+
{ value: '4 agents', label: 'parallel analysis' },
|
| 7 |
+
{ value: '< 60s', label: 'full repo review' },
|
| 8 |
+
{ value: '1-click', label: 'PR generation' },
|
| 9 |
+
{ value: '100%', label: 'ASI-1 powered' },
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
export default function ReverseChessSection() {
|
| 13 |
+
return (
|
| 14 |
+
<section className="py-32 px-4 max-w-6xl mx-auto">
|
| 15 |
+
<div className="grid lg:grid-cols-2 gap-20 items-center">
|
| 16 |
+
{/* Left — Content */}
|
| 17 |
+
<div className="order-2 lg:order-1">
|
| 18 |
+
<SectionBadge label="GitHub Integration" action="Beta" />
|
| 19 |
+
<h2 className="text-hero-heading text-3xl sm:text-4xl font-semibold leading-tight mt-6">
|
| 20 |
+
Paste a Repo URL.
|
| 21 |
+
<br />
|
| 22 |
+
Get a Full Review.
|
| 23 |
+
</h2>
|
| 24 |
+
<p className="text-hero-sub mt-4 leading-relaxed">
|
| 25 |
+
Import any GitHub repository and get a comprehensive multi-agent analysis in seconds.
|
| 26 |
+
Security vulnerabilities, performance issues, and auto-generated documentation — all
|
| 27 |
+
delivered as PR-ready fixes.
|
| 28 |
+
</p>
|
| 29 |
+
|
| 30 |
+
<div className="grid grid-cols-2 gap-4 mt-8">
|
| 31 |
+
{stats.map((stat) => (
|
| 32 |
+
<div key={stat.label} className="liquid-glass rounded-2xl p-4">
|
| 33 |
+
<p className="text-lg font-semibold text-hero-heading">{stat.value}</p>
|
| 34 |
+
<p className="text-sm text-muted-foreground">{stat.label}</p>
|
| 35 |
+
</div>
|
| 36 |
+
))}
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<div className="mt-8">
|
| 40 |
+
<Button variant="hero">Import a Repo</Button>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
{/* Right — Video */}
|
| 45 |
+
<div className="order-1 lg:order-2">
|
| 46 |
+
<div className="liquid-glass rounded-3xl aspect-[4/3] overflow-hidden">
|
| 47 |
+
<HLSVideo
|
| 48 |
+
src="https://stream.mux.com/f0001qPDy00mvqP023lqK3lWx31uHvxirFCHK1yNLczzqxY.m3u8"
|
| 49 |
+
className="w-full h-full object-cover"
|
| 50 |
+
/>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</section>
|
| 55 |
+
);
|
| 56 |
+
}
|
packages/client/src/components/landing/SectionBadge.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ChevronRight } from 'lucide-react';
|
| 2 |
+
|
| 3 |
+
interface SectionBadgeProps {
|
| 4 |
+
label: string;
|
| 5 |
+
action: string;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default function SectionBadge({ label, action }: SectionBadgeProps) {
|
| 9 |
+
return (
|
| 10 |
+
<div className="flex justify-center">
|
| 11 |
+
<div className="liquid-glass rounded-full px-4 py-1.5 inline-flex items-center gap-2 text-sm">
|
| 12 |
+
<span className="text-foreground/80">{label}</span>
|
| 13 |
+
<span className="bg-white/10 rounded-full px-2.5 py-0.5 text-xs inline-flex items-center gap-1 text-foreground/60">
|
| 14 |
+
{action}
|
| 15 |
+
<ChevronRight className="w-3 h-3" />
|
| 16 |
+
</span>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
);
|
| 20 |
+
}
|
packages/client/src/components/landing/TestimonialsSection.tsx
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const testimonials = [
|
| 2 |
+
{
|
| 3 |
+
quote: 'The security agent caught an XSS vulnerability in our checkout flow that our entire team missed. The fix was ready in seconds.',
|
| 4 |
+
name: 'Alex Kim',
|
| 5 |
+
role: 'Senior Frontend Dev, Streamline',
|
| 6 |
+
initials: 'AK',
|
| 7 |
+
},
|
| 8 |
+
{
|
| 9 |
+
quote: 'Having AI that understands my entire workspace context is a game-changer. The completions are actually useful, not just autocomplete on steroids.',
|
| 10 |
+
name: 'Sarah Chen',
|
| 11 |
+
role: 'Tech Lead, BuildKit',
|
| 12 |
+
initials: 'SC',
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
quote: 'We imported our React monorepo and got a full review with docs in under a minute. The PR it generated was merge-ready.',
|
| 16 |
+
name: 'Marcus Webb',
|
| 17 |
+
role: 'Engineering Manager, Pixelform',
|
| 18 |
+
initials: 'MW',
|
| 19 |
+
},
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
export default function TestimonialsSection() {
|
| 23 |
+
return (
|
| 24 |
+
<section className="py-32 px-4 max-w-6xl mx-auto">
|
| 25 |
+
<div className="text-center">
|
| 26 |
+
<h2 className="text-hero-heading text-3xl sm:text-5xl font-semibold">
|
| 27 |
+
Loved by Frontend
|
| 28 |
+
<br />
|
| 29 |
+
Developers Everywhere
|
| 30 |
+
</h2>
|
| 31 |
+
<p className="text-hero-sub mt-4">Hear from the developers who made the switch.</p>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<div className="grid md:grid-cols-3 gap-6 mt-16">
|
| 35 |
+
{testimonials.map((t, i) => (
|
| 36 |
+
<div
|
| 37 |
+
key={t.name}
|
| 38 |
+
className={`liquid-glass rounded-3xl p-8 flex flex-col ${i === 1 ? 'md:-translate-y-6' : ''}`}
|
| 39 |
+
>
|
| 40 |
+
<p className="text-foreground/90 text-sm leading-relaxed flex-1">"{t.quote}"</p>
|
| 41 |
+
<div className="border-t border-border/50 my-6" />
|
| 42 |
+
<div className="flex items-center gap-3">
|
| 43 |
+
<div className="w-10 h-10 rounded-full bg-secondary flex items-center justify-center text-sm font-medium text-foreground">
|
| 44 |
+
{t.initials}
|
| 45 |
+
</div>
|
| 46 |
+
<div>
|
| 47 |
+
<p className="text-sm font-medium text-foreground">{t.name}</p>
|
| 48 |
+
<p className="text-muted-foreground text-sm">{t.role}</p>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
))}
|
| 53 |
+
</div>
|
| 54 |
+
</section>
|
| 55 |
+
);
|
| 56 |
+
}
|
packages/client/src/components/layout/IDEShell.tsx
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
import { useParams, useNavigate } from 'react-router-dom';
|
| 3 |
+
import TopBar from './TopBar';
|
| 4 |
+
import StatusBar from './StatusBar';
|
| 5 |
+
import PanelLayout from './PanelLayout';
|
| 6 |
+
import { useFileStore } from '@/stores/fileStore';
|
| 7 |
+
import { useEditorStore } from '@/stores/editorStore';
|
| 8 |
+
import { useEnvStore, Environment } from '@/stores/envStore';
|
| 9 |
+
|
| 10 |
+
export default function IDEShell() {
|
| 11 |
+
const { env } = useParams<{ env: string }>();
|
| 12 |
+
const navigate = useNavigate();
|
| 13 |
+
|
| 14 |
+
const [layout, setLayout] = useState<'editor' | 'split' | 'preview'>('split');
|
| 15 |
+
const initialize = useFileStore((s) => s.initialize);
|
| 16 |
+
const initialized = useFileStore((s) => s.initialized);
|
| 17 |
+
const files = useFileStore((s) => s.files);
|
| 18 |
+
const createFile = useFileStore((s) => s.createFile);
|
| 19 |
+
const deleteFile = useFileStore((s) => s.deleteFile);
|
| 20 |
+
const addFile = useEditorStore((s) => s.addFile);
|
| 21 |
+
const removeFile = useEditorStore((s) => s.removeFile);
|
| 22 |
+
const openFiles = useEditorStore((s) => s.openFiles);
|
| 23 |
+
const environment = useEnvStore((s) => s.environment);
|
| 24 |
+
const setEnvironment = useEnvStore((s) => s.setEnvironment);
|
| 25 |
+
const [hasAutoOpened, setHasAutoOpened] = useState(false);
|
| 26 |
+
|
| 27 |
+
useEffect(() => {
|
| 28 |
+
initialize();
|
| 29 |
+
}, [initialize]);
|
| 30 |
+
|
| 31 |
+
useEffect(() => {
|
| 32 |
+
if (env && !['web', 'java', 'python'].includes(env)) {
|
| 33 |
+
navigate('/ide', { replace: true });
|
| 34 |
+
}
|
| 35 |
+
}, [env, navigate]);
|
| 36 |
+
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
if (!initialized || !env || !['web', 'java', 'python'].includes(env)) return;
|
| 39 |
+
|
| 40 |
+
const targetEnv = env as Environment;
|
| 41 |
+
|
| 42 |
+
// If URL environment differs from store, clean up and scaffold
|
| 43 |
+
if (environment !== targetEnv) {
|
| 44 |
+
setEnvironment(targetEnv);
|
| 45 |
+
|
| 46 |
+
// Cleanup old files
|
| 47 |
+
Object.keys(files).forEach(f => {
|
| 48 |
+
if (f === 'README.md') return;
|
| 49 |
+
const isJava = f.endsWith('.java');
|
| 50 |
+
const isPython = f.endsWith('.py');
|
| 51 |
+
const isWeb = ['.html', '.css', '.js', '.ts', '.tsx', '.jsx'].some(ext => f.endsWith(ext));
|
| 52 |
+
|
| 53 |
+
if (targetEnv === 'java' && !isJava) { deleteFile(f); removeFile(f); }
|
| 54 |
+
if (targetEnv === 'python' && !isPython) { deleteFile(f); removeFile(f); }
|
| 55 |
+
if (targetEnv === 'web' && !isWeb) { deleteFile(f); removeFile(f); }
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
// Scaffold files
|
| 59 |
+
if (targetEnv === 'web') {
|
| 60 |
+
const content = `<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>Document</title>\n <style>\n body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }\n </style>\n</head>\n<body>\n <h1>Hello Web!</h1>\n</body>\n</html>`;
|
| 61 |
+
if (!files['index.html']) createFile('index.html', content);
|
| 62 |
+
} else if (targetEnv === 'java') {
|
| 63 |
+
const content = `public class Main {\n public static void main(String[] args) {\n System.out.println("Hello, Java!");\n }\n}`;
|
| 64 |
+
if (!files['Main.java']) createFile('Main.java', content);
|
| 65 |
+
} else if (targetEnv === 'python') {
|
| 66 |
+
const content = `print("Hello, Python!")`;
|
| 67 |
+
if (!files['main.py']) createFile('main.py', content);
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
}, [env, environment, initialized]); // Missing files to avoid loop on scaffolding
|
| 71 |
+
|
| 72 |
+
useEffect(() => {
|
| 73 |
+
if (initialized && environment === env && !hasAutoOpened) {
|
| 74 |
+
if (Object.keys(openFiles).length === 0) {
|
| 75 |
+
if (environment === 'web' && files['index.html']) addFile('index.html', files['index.html'], 'html');
|
| 76 |
+
else if (environment === 'java' && files['Main.java']) addFile('Main.java', files['Main.java'], 'java');
|
| 77 |
+
else if (environment === 'python' && files['main.py']) addFile('main.py', files['main.py'], 'python');
|
| 78 |
+
}
|
| 79 |
+
setHasAutoOpened(true);
|
| 80 |
+
}
|
| 81 |
+
}, [initialized, environment, env, hasAutoOpened, files, openFiles, addFile]);
|
| 82 |
+
|
| 83 |
+
if (!initialized) {
|
| 84 |
+
return (
|
| 85 |
+
<div className="h-screen flex items-center justify-center bg-background">
|
| 86 |
+
<div className="text-muted-foreground animate-pulse font-mono text-sm">Initializing workspace...</div>
|
| 87 |
+
</div>
|
| 88 |
+
);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
return (
|
| 92 |
+
<div className="h-screen flex flex-col bg-background overflow-hidden">
|
| 93 |
+
<TopBar layout={layout} onLayoutChange={setLayout} />
|
| 94 |
+
<PanelLayout layout={layout} />
|
| 95 |
+
<StatusBar />
|
| 96 |
+
</div>
|
| 97 |
+
);
|
| 98 |
+
}
|
packages/client/src/components/layout/PanelLayout.tsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
| 2 |
+
import Sidebar from './Sidebar';
|
| 3 |
+
import MultiFileEditor from '@/components/editor/MultiFileEditor';
|
| 4 |
+
import LivePreview from '@/components/preview/LivePreview';
|
| 5 |
+
import TerminalPanel from '@/components/terminal/TerminalPanel';
|
| 6 |
+
import AIChatPanel from '@/components/ai/AIChatPanel';
|
| 7 |
+
import { useEnvStore } from '@/stores/envStore';
|
| 8 |
+
|
| 9 |
+
interface PanelLayoutProps {
|
| 10 |
+
layout: 'editor' | 'split' | 'preview';
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
function ResizeHandle({ direction = 'vertical' }: { direction?: 'vertical' | 'horizontal' }) {
|
| 14 |
+
return (
|
| 15 |
+
<PanelResizeHandle
|
| 16 |
+
className={`group relative ${direction === 'vertical' ? 'w-1 hover:w-1.5' : 'h-1 hover:h-1.5'} bg-border/50 hover:bg-primary/30 transition-all`}
|
| 17 |
+
>
|
| 18 |
+
<div className={`absolute ${direction === 'vertical' ? 'inset-y-0 -left-0.5 -right-0.5' : 'inset-x-0 -top-0.5 -bottom-0.5'}`} />
|
| 19 |
+
</PanelResizeHandle>
|
| 20 |
+
);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export default function PanelLayout({ layout }: PanelLayoutProps) {
|
| 24 |
+
const environment = useEnvStore((s) => s.environment);
|
| 25 |
+
|
| 26 |
+
const renderSecondaryPanel = () => {
|
| 27 |
+
if (environment === 'java' || environment === 'python') {
|
| 28 |
+
return <TerminalPanel />;
|
| 29 |
+
}
|
| 30 |
+
return <LivePreview />;
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<PanelGroup direction="horizontal" className="flex-1 min-h-0">
|
| 35 |
+
{/* Left: Sidebar */}
|
| 36 |
+
<Panel defaultSize={18} minSize={10} maxSize={25} collapsible>
|
| 37 |
+
<Sidebar />
|
| 38 |
+
</Panel>
|
| 39 |
+
|
| 40 |
+
<ResizeHandle direction="vertical" />
|
| 41 |
+
|
| 42 |
+
{/* Center: Editor + Preview */}
|
| 43 |
+
<Panel defaultSize={52} minSize={30} className="flex flex-col overflow-hidden">
|
| 44 |
+
{layout === 'editor' && <MultiFileEditor />}
|
| 45 |
+
{layout === 'preview' && renderSecondaryPanel()}
|
| 46 |
+
{layout === 'split' && (
|
| 47 |
+
<PanelGroup direction="horizontal">
|
| 48 |
+
<Panel defaultSize={50}>
|
| 49 |
+
<MultiFileEditor />
|
| 50 |
+
</Panel>
|
| 51 |
+
<ResizeHandle direction="vertical" />
|
| 52 |
+
<Panel defaultSize={50}>
|
| 53 |
+
{renderSecondaryPanel()}
|
| 54 |
+
</Panel>
|
| 55 |
+
</PanelGroup>
|
| 56 |
+
)}
|
| 57 |
+
</Panel>
|
| 58 |
+
|
| 59 |
+
<ResizeHandle direction="vertical" />
|
| 60 |
+
|
| 61 |
+
{/* Right: AI Chat */}
|
| 62 |
+
<Panel defaultSize={30} minSize={20} maxSize={40} collapsible>
|
| 63 |
+
<AIChatPanel />
|
| 64 |
+
</Panel>
|
| 65 |
+
</PanelGroup>
|
| 66 |
+
);
|
| 67 |
+
}
|
packages/client/src/components/layout/Sidebar.tsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Files, Search, MessageSquare, GitBranch, Shield, Settings,
|
| 4 |
+
} from 'lucide-react';
|
| 5 |
+
import { cn } from '@/lib/utils';
|
| 6 |
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip';
|
| 7 |
+
import FileTree from '@/components/explorer/FileTree';
|
| 8 |
+
import AIChatPanel from '@/components/ai/AIChatPanel';
|
| 9 |
+
|
| 10 |
+
type SidebarPanel = 'files' | 'search' | 'chat' | 'git' | 'agents' | 'settings';
|
| 11 |
+
|
| 12 |
+
const sidebarItems: Array<{ id: SidebarPanel; icon: typeof Files; label: string }> = [
|
| 13 |
+
{ id: 'files', icon: Files, label: 'Explorer' },
|
| 14 |
+
{ id: 'search', icon: Search, label: 'Search' },
|
| 15 |
+
{ id: 'chat', icon: MessageSquare, label: 'AI Chat' },
|
| 16 |
+
{ id: 'git', icon: GitBranch, label: 'Git' },
|
| 17 |
+
{ id: 'agents', icon: Shield, label: 'Agents' },
|
| 18 |
+
{ id: 'settings', icon: Settings, label: 'Settings' },
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
export default function Sidebar() {
|
| 22 |
+
const [activePanel, setActivePanel] = useState<SidebarPanel>('files');
|
| 23 |
+
|
| 24 |
+
return (
|
| 25 |
+
<div className="flex h-full">
|
| 26 |
+
{/* Icon strip */}
|
| 27 |
+
<TooltipProvider delayDuration={300}>
|
| 28 |
+
<div className="w-12 bg-background border-r border-border flex flex-col items-center pt-2 gap-1">
|
| 29 |
+
{sidebarItems.map((item) => (
|
| 30 |
+
<Tooltip key={item.id}>
|
| 31 |
+
<TooltipTrigger asChild>
|
| 32 |
+
<button
|
| 33 |
+
onClick={() => setActivePanel(item.id)}
|
| 34 |
+
className={cn(
|
| 35 |
+
'w-10 h-10 flex items-center justify-center rounded-lg transition-colors',
|
| 36 |
+
activePanel === item.id
|
| 37 |
+
? 'text-foreground bg-secondary'
|
| 38 |
+
: 'text-muted-foreground hover:text-foreground hover:bg-secondary/50'
|
| 39 |
+
)}
|
| 40 |
+
>
|
| 41 |
+
<item.icon className="w-5 h-5" />
|
| 42 |
+
</button>
|
| 43 |
+
</TooltipTrigger>
|
| 44 |
+
<TooltipContent side="right">{item.label}</TooltipContent>
|
| 45 |
+
</Tooltip>
|
| 46 |
+
))}
|
| 47 |
+
</div>
|
| 48 |
+
</TooltipProvider>
|
| 49 |
+
|
| 50 |
+
{/* Panel content */}
|
| 51 |
+
<div className="flex-1 bg-card/30 overflow-hidden flex flex-col min-w-0">
|
| 52 |
+
<div className="px-3 py-2 border-b border-border">
|
| 53 |
+
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
| 54 |
+
{sidebarItems.find((i) => i.id === activePanel)?.label}
|
| 55 |
+
</h4>
|
| 56 |
+
</div>
|
| 57 |
+
<div className="flex-1 overflow-auto min-h-0">
|
| 58 |
+
{activePanel === 'files' && <FileTree />}
|
| 59 |
+
{activePanel === 'search' && (
|
| 60 |
+
<div className="p-3">
|
| 61 |
+
<input
|
| 62 |
+
placeholder="Search files..."
|
| 63 |
+
className="w-full bg-secondary border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
| 64 |
+
/>
|
| 65 |
+
<p className="text-xs text-muted-foreground mt-2">Type to search across workspace files.</p>
|
| 66 |
+
</div>
|
| 67 |
+
)}
|
| 68 |
+
{activePanel === 'chat' && (
|
| 69 |
+
<p className="p-3 text-xs text-muted-foreground">AI Chat is in the right panel.</p>
|
| 70 |
+
)}
|
| 71 |
+
{activePanel === 'git' && (
|
| 72 |
+
<div className="p-3 text-xs text-muted-foreground">
|
| 73 |
+
<p>Git integration</p>
|
| 74 |
+
<p className="mt-1">Import a repository to see Git status.</p>
|
| 75 |
+
</div>
|
| 76 |
+
)}
|
| 77 |
+
{activePanel === 'agents' && (
|
| 78 |
+
<div className="p-3 text-xs text-muted-foreground">
|
| 79 |
+
<p>Agent controls will appear here after running a review.</p>
|
| 80 |
+
</div>
|
| 81 |
+
)}
|
| 82 |
+
{activePanel === 'settings' && (
|
| 83 |
+
<div className="p-3 text-xs text-muted-foreground space-y-2">
|
| 84 |
+
<p className="font-medium text-foreground">Settings</p>
|
| 85 |
+
<p>Font Size: 14px</p>
|
| 86 |
+
<p>Tab Size: 2</p>
|
| 87 |
+
<p>Word Wrap: On</p>
|
| 88 |
+
<p>AI Completions: On</p>
|
| 89 |
+
</div>
|
| 90 |
+
)}
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
);
|
| 95 |
+
}
|
packages/client/src/components/layout/StatusBar.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEditorStore } from '@/stores/editorStore';
|
| 2 |
+
import { useAIStore } from '@/stores/aiStore';
|
| 3 |
+
import { useWebSocket } from '@/hooks/useWebSocket';
|
| 4 |
+
import { cn } from '@/lib/utils';
|
| 5 |
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip';
|
| 6 |
+
|
| 7 |
+
const agents = ['security', 'performance', 'style', 'documentation'] as const;
|
| 8 |
+
|
| 9 |
+
export default function StatusBar() {
|
| 10 |
+
const activeFilePath = useEditorStore((s) => s.activeFilePath);
|
| 11 |
+
const activeFile = useEditorStore((s) => activeFilePath ? s.openFiles[activeFilePath] : null);
|
| 12 |
+
const agentRunning = useAIStore((s) => s.agentRunning);
|
| 13 |
+
const { isConnected } = useWebSocket();
|
| 14 |
+
|
| 15 |
+
return (
|
| 16 |
+
<div className="h-6 bg-card/80 border-t border-border flex items-center px-3 text-[11px] text-muted-foreground gap-4">
|
| 17 |
+
{/* Left */}
|
| 18 |
+
<div className="flex items-center gap-3">
|
| 19 |
+
{activeFile && (
|
| 20 |
+
<span className="uppercase">{activeFile.language}</span>
|
| 21 |
+
)}
|
| 22 |
+
<span>main</span>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<div className="flex-1" />
|
| 26 |
+
|
| 27 |
+
{/* Center — Agent status dots */}
|
| 28 |
+
<TooltipProvider delayDuration={200}>
|
| 29 |
+
<div className="flex items-center gap-1.5">
|
| 30 |
+
{agents.map((agent) => {
|
| 31 |
+
const running = agentRunning[agent];
|
| 32 |
+
return (
|
| 33 |
+
<Tooltip key={agent}>
|
| 34 |
+
<TooltipTrigger>
|
| 35 |
+
<span
|
| 36 |
+
className={cn(
|
| 37 |
+
'w-2 h-2 rounded-full transition-colors',
|
| 38 |
+
running ? 'bg-yellow-400 animate-pulse' : 'bg-green-500/50'
|
| 39 |
+
)}
|
| 40 |
+
/>
|
| 41 |
+
</TooltipTrigger>
|
| 42 |
+
<TooltipContent>{agent} agent: {running ? 'running' : 'idle'}</TooltipContent>
|
| 43 |
+
</Tooltip>
|
| 44 |
+
);
|
| 45 |
+
})}
|
| 46 |
+
</div>
|
| 47 |
+
</TooltipProvider>
|
| 48 |
+
|
| 49 |
+
<div className="flex-1" />
|
| 50 |
+
|
| 51 |
+
{/* Right */}
|
| 52 |
+
<div className="flex items-center gap-3">
|
| 53 |
+
{activeFile && (
|
| 54 |
+
<span>Ln {activeFile.cursorPosition.lineNumber}, Col {activeFile.cursorPosition.column}</span>
|
| 55 |
+
)}
|
| 56 |
+
<span>UTF-8</span>
|
| 57 |
+
<span className="flex items-center gap-1">
|
| 58 |
+
<span className={cn('w-1.5 h-1.5 rounded-full', isConnected ? 'bg-green-500' : 'bg-red-500')} />
|
| 59 |
+
ASI-1
|
| 60 |
+
</span>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
);
|
| 64 |
+
}
|
packages/client/src/components/layout/TopBar.tsx
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Menu, Play, Shield, Github, Monitor, Columns, Eye } from 'lucide-react';
|
| 4 |
+
import { Button } from '@/components/ui/button';
|
| 5 |
+
import { useAgentReview } from '@/hooks/useAgentReview';
|
| 6 |
+
import RepoImporter from '@/components/github/RepoImporter';
|
| 7 |
+
import { useEnvStore } from '@/stores/envStore';
|
| 8 |
+
|
| 9 |
+
interface TopBarProps {
|
| 10 |
+
layout: 'editor' | 'split' | 'preview';
|
| 11 |
+
onLayoutChange: (layout: 'editor' | 'split' | 'preview') => void;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export default function TopBar({ layout, onLayoutChange }: TopBarProps) {
|
| 15 |
+
const navigate = useNavigate();
|
| 16 |
+
const { reviewWorkspace, isReviewing } = useAgentReview();
|
| 17 |
+
const [showImporter, setShowImporter] = useState(false);
|
| 18 |
+
const environment = useEnvStore((s) => s.environment);
|
| 19 |
+
|
| 20 |
+
// Define global window method or a custom hook to trigger execution that terminal listens to?
|
| 21 |
+
// We'll dispatch a custom event for now that terminal/execution handler can listen to.
|
| 22 |
+
const executeCode = () => {
|
| 23 |
+
window.dispatchEvent(new CustomEvent('glmpilot:execute'));
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<>
|
| 28 |
+
<div className="h-10 bg-background border-b border-border flex items-center px-2 gap-2">
|
| 29 |
+
<button className="p-1.5 hover:bg-secondary rounded text-muted-foreground">
|
| 30 |
+
<Menu className="w-4 h-4" />
|
| 31 |
+
</button>
|
| 32 |
+
<span className="text-sm font-medium text-foreground ml-1">GLMPilot</span>
|
| 33 |
+
|
| 34 |
+
<div className="flex-1" />
|
| 35 |
+
|
| 36 |
+
{/* Layout toggles */}
|
| 37 |
+
<div className="flex items-center border border-border rounded-lg overflow-hidden">
|
| 38 |
+
{([
|
| 39 |
+
{ id: 'editor' as const, icon: Monitor, label: 'Editor' },
|
| 40 |
+
{ id: 'split' as const, icon: Columns, label: 'Split' },
|
| 41 |
+
{ id: 'preview' as const, icon: Eye, label: 'Preview' },
|
| 42 |
+
]).map((item) => (
|
| 43 |
+
<button
|
| 44 |
+
key={item.id}
|
| 45 |
+
onClick={() => onLayoutChange(item.id)}
|
| 46 |
+
className={`p-1.5 transition-colors ${
|
| 47 |
+
layout === item.id
|
| 48 |
+
? 'bg-secondary text-foreground'
|
| 49 |
+
: 'text-muted-foreground hover:text-foreground'
|
| 50 |
+
}`}
|
| 51 |
+
title={item.label}
|
| 52 |
+
>
|
| 53 |
+
<item.icon className="w-3.5 h-3.5" />
|
| 54 |
+
</button>
|
| 55 |
+
))}
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
{(environment === 'java' || environment === 'python') && (
|
| 59 |
+
<Button
|
| 60 |
+
size="sm"
|
| 61 |
+
onClick={executeCode}
|
| 62 |
+
className="gap-1.5 text-xs bg-green-600 hover:bg-green-700 text-white border-0 shadow-sm transition-all"
|
| 63 |
+
>
|
| 64 |
+
<Play className="w-3.5 h-3.5 fill-current" />
|
| 65 |
+
Run
|
| 66 |
+
</Button>
|
| 67 |
+
)}
|
| 68 |
+
|
| 69 |
+
<Button
|
| 70 |
+
size="sm"
|
| 71 |
+
variant="ghost"
|
| 72 |
+
onClick={() => reviewWorkspace()}
|
| 73 |
+
disabled={isReviewing}
|
| 74 |
+
className="gap-1.5 text-xs"
|
| 75 |
+
>
|
| 76 |
+
<Shield className="w-3.5 h-3.5" />
|
| 77 |
+
{isReviewing ? 'Reviewing...' : 'Review'}
|
| 78 |
+
</Button>
|
| 79 |
+
|
| 80 |
+
<Button
|
| 81 |
+
size="sm"
|
| 82 |
+
variant="ghost"
|
| 83 |
+
onClick={() => setShowImporter(true)}
|
| 84 |
+
className="gap-1.5 text-xs"
|
| 85 |
+
>
|
| 86 |
+
<Github className="w-3.5 h-3.5" />
|
| 87 |
+
Import
|
| 88 |
+
</Button>
|
| 89 |
+
|
| 90 |
+
<Button
|
| 91 |
+
size="sm"
|
| 92 |
+
variant="ghost"
|
| 93 |
+
onClick={() => navigate('/ide')}
|
| 94 |
+
className="gap-1.5 text-xs text-muted-foreground hover:text-foreground ml-2"
|
| 95 |
+
>
|
| 96 |
+
Change Environment
|
| 97 |
+
</Button>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<RepoImporter open={showImporter} onClose={() => setShowImporter(false)} />
|
| 101 |
+
</>
|
| 102 |
+
);
|
| 103 |
+
}
|
packages/client/src/components/preview/LivePreview.tsx
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef, useEffect, useState } from 'react';
|
| 2 |
+
import { useLivePreview } from '@/hooks/useLivePreview';
|
| 3 |
+
import { RefreshCw, Monitor, Tablet, Smartphone, ExternalLink } from 'lucide-react';
|
| 4 |
+
import { cn } from '@/lib/utils';
|
| 5 |
+
|
| 6 |
+
const viewports = [
|
| 7 |
+
{ icon: Smartphone, width: 375, label: 'Mobile' },
|
| 8 |
+
{ icon: Tablet, width: 768, label: 'Tablet' },
|
| 9 |
+
{ icon: Monitor, width: '100%', label: 'Desktop' },
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
export default function LivePreview() {
|
| 13 |
+
const { srcdoc } = useLivePreview();
|
| 14 |
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
| 15 |
+
const [viewportIndex, setViewportIndex] = useState(2);
|
| 16 |
+
const [consoleOutput, setConsoleOutput] = useState<Array<{ method: string; args: string[] }>>([]);
|
| 17 |
+
const [showConsole, setShowConsole] = useState(false);
|
| 18 |
+
const [refreshKey, setRefreshKey] = useState(0);
|
| 19 |
+
|
| 20 |
+
useEffect(() => {
|
| 21 |
+
const handleMessage = (event: MessageEvent) => {
|
| 22 |
+
if (event.data?.type === 'console') {
|
| 23 |
+
setConsoleOutput((prev) => [...prev.slice(-50), { method: event.data.method, args: event.data.args }]);
|
| 24 |
+
}
|
| 25 |
+
};
|
| 26 |
+
window.addEventListener('message', handleMessage);
|
| 27 |
+
return () => window.removeEventListener('message', handleMessage);
|
| 28 |
+
}, []);
|
| 29 |
+
|
| 30 |
+
const viewport = viewports[viewportIndex];
|
| 31 |
+
|
| 32 |
+
const handleOpenInNewTab = () => {
|
| 33 |
+
const newWindow = window.open('', '_blank');
|
| 34 |
+
if (newWindow) {
|
| 35 |
+
newWindow.document.open();
|
| 36 |
+
newWindow.document.write(srcdoc);
|
| 37 |
+
newWindow.document.close();
|
| 38 |
+
newWindow.document.title = 'GLMPilot Preview';
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<div className="flex flex-col h-full bg-background">
|
| 44 |
+
{/* Toolbar */}
|
| 45 |
+
<div className="flex items-center gap-1 px-2 py-1 border-b border-border">
|
| 46 |
+
<button
|
| 47 |
+
onClick={() => setRefreshKey((k) => k + 1)}
|
| 48 |
+
className="p-1.5 hover:bg-secondary rounded transition-colors"
|
| 49 |
+
title="Refresh"
|
| 50 |
+
>
|
| 51 |
+
<RefreshCw className="w-3.5 h-3.5 text-muted-foreground" />
|
| 52 |
+
</button>
|
| 53 |
+
<button
|
| 54 |
+
onClick={handleOpenInNewTab}
|
| 55 |
+
className="p-1.5 hover:bg-secondary rounded transition-colors"
|
| 56 |
+
title="Open in new tab"
|
| 57 |
+
>
|
| 58 |
+
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground" />
|
| 59 |
+
</button>
|
| 60 |
+
<div className="w-px h-4 bg-border mx-1" />
|
| 61 |
+
{viewports.map((vp, i) => (
|
| 62 |
+
<button
|
| 63 |
+
key={vp.label}
|
| 64 |
+
onClick={() => setViewportIndex(i)}
|
| 65 |
+
className={cn(
|
| 66 |
+
'p-1.5 rounded transition-colors',
|
| 67 |
+
i === viewportIndex ? 'bg-secondary text-foreground' : 'hover:bg-secondary/50 text-muted-foreground'
|
| 68 |
+
)}
|
| 69 |
+
title={vp.label}
|
| 70 |
+
>
|
| 71 |
+
<vp.icon className="w-3.5 h-3.5" />
|
| 72 |
+
</button>
|
| 73 |
+
))}
|
| 74 |
+
<div className="flex-1" />
|
| 75 |
+
<button
|
| 76 |
+
onClick={() => setShowConsole(!showConsole)}
|
| 77 |
+
className={cn(
|
| 78 |
+
'px-2 py-1 text-xs rounded transition-colors',
|
| 79 |
+
showConsole ? 'bg-secondary text-foreground' : 'text-muted-foreground hover:bg-secondary/50'
|
| 80 |
+
)}
|
| 81 |
+
>
|
| 82 |
+
Console {consoleOutput.length > 0 && `(${consoleOutput.length})`}
|
| 83 |
+
</button>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
{/* Preview */}
|
| 87 |
+
<div className="flex-1 flex items-start justify-center overflow-auto p-2 min-h-0">
|
| 88 |
+
<iframe
|
| 89 |
+
key={refreshKey}
|
| 90 |
+
ref={iframeRef}
|
| 91 |
+
srcDoc={srcdoc}
|
| 92 |
+
sandbox="allow-scripts allow-modals"
|
| 93 |
+
className="bg-white rounded border border-border"
|
| 94 |
+
style={{
|
| 95 |
+
width: typeof viewport.width === 'number' ? `${viewport.width}px` : viewport.width,
|
| 96 |
+
height: '100%',
|
| 97 |
+
maxWidth: '100%',
|
| 98 |
+
}}
|
| 99 |
+
title="Live Preview"
|
| 100 |
+
/>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
{/* Console */}
|
| 104 |
+
{showConsole && (
|
| 105 |
+
<div className="h-32 border-t border-border overflow-auto p-2 text-xs font-mono">
|
| 106 |
+
{consoleOutput.map((entry, i) => (
|
| 107 |
+
<div
|
| 108 |
+
key={i}
|
| 109 |
+
className={cn(
|
| 110 |
+
'py-0.5',
|
| 111 |
+
entry.method === 'error' ? 'text-red-400' : entry.method === 'warn' ? 'text-yellow-400' : 'text-muted-foreground'
|
| 112 |
+
)}
|
| 113 |
+
>
|
| 114 |
+
{entry.args.join(' ')}
|
| 115 |
+
</div>
|
| 116 |
+
))}
|
| 117 |
+
{consoleOutput.length === 0 && (
|
| 118 |
+
<p className="text-muted-foreground">No console output yet.</p>
|
| 119 |
+
)}
|
| 120 |
+
</div>
|
| 121 |
+
)}
|
| 122 |
+
</div>
|
| 123 |
+
);
|
| 124 |
+
}
|
packages/client/src/components/terminal/TerminalPanel.tsx
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useRef, useEffect, useLayoutEffect, useState } from 'react';
|
| 2 |
+
import { Terminal } from 'xterm';
|
| 3 |
+
import { FitAddon } from 'xterm-addon-fit';
|
| 4 |
+
import { getSocket } from '@/services/socket';
|
| 5 |
+
import { WS_EVENTS } from '@glmpilot/shared';
|
| 6 |
+
import { useEditorStore } from '@/stores/editorStore';
|
| 7 |
+
import { useEnvStore } from '@/stores/envStore';
|
| 8 |
+
import 'xterm/css/xterm.css';
|
| 9 |
+
|
| 10 |
+
export default function TerminalPanel() {
|
| 11 |
+
const containerRef = useRef<HTMLDivElement>(null);
|
| 12 |
+
const terminalRef = useRef<Terminal>();
|
| 13 |
+
const [isExecuting, setIsExecuting] = useState(false);
|
| 14 |
+
const isExecutingRef = useRef(false);
|
| 15 |
+
|
| 16 |
+
const activeFilePath = useEditorStore((s) => s.activeFilePath);
|
| 17 |
+
const openFiles = useEditorStore((s) => s.openFiles);
|
| 18 |
+
const environment = useEnvStore((s) => s.environment);
|
| 19 |
+
/** Buffered line while a program is waiting on stdin (submit on Enter as one message). */
|
| 20 |
+
const pendingStdinLineRef = useRef('');
|
| 21 |
+
/** Server-assigned id for this run (stdin + process map); set by execute:started. */
|
| 22 |
+
const currentRunIdRef = useRef<string | null>(null);
|
| 23 |
+
|
| 24 |
+
useEffect(() => {
|
| 25 |
+
if (!containerRef.current) return;
|
| 26 |
+
|
| 27 |
+
const terminal = new Terminal({
|
| 28 |
+
theme: {
|
| 29 |
+
background: '#080810',
|
| 30 |
+
foreground: '#e8e8e8',
|
| 31 |
+
cursor: '#81f084',
|
| 32 |
+
selectionBackground: '#81f08433',
|
| 33 |
+
black: '#1a1a2a',
|
| 34 |
+
red: '#f07178',
|
| 35 |
+
green: '#81f084',
|
| 36 |
+
yellow: '#ffcb6b',
|
| 37 |
+
blue: '#82aaff',
|
| 38 |
+
magenta: '#c792ea',
|
| 39 |
+
cyan: '#89ddff',
|
| 40 |
+
white: '#e8e8e8',
|
| 41 |
+
},
|
| 42 |
+
fontFamily: "'Geist Mono', 'JetBrains Mono', 'Fira Code', monospace",
|
| 43 |
+
fontSize: 13,
|
| 44 |
+
cursorBlink: true,
|
| 45 |
+
cursorStyle: 'bar',
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
const fitAddon = new FitAddon();
|
| 49 |
+
terminal.loadAddon(fitAddon);
|
| 50 |
+
terminal.open(containerRef.current);
|
| 51 |
+
fitAddon.fit();
|
| 52 |
+
|
| 53 |
+
terminal.writeln('\x1b[1;32m✦ GLMPilot Terminal\x1b[0m');
|
| 54 |
+
terminal.writeln('\x1b[90mConnected to local environment.\x1b[0m');
|
| 55 |
+
terminal.writeln('');
|
| 56 |
+
terminal.write('\x1b[32m❯\x1b[0m ');
|
| 57 |
+
|
| 58 |
+
terminalRef.current = terminal;
|
| 59 |
+
|
| 60 |
+
terminal.onData((data) => {
|
| 61 |
+
if (!isExecutingRef.current) return;
|
| 62 |
+
|
| 63 |
+
const flushLine = () => {
|
| 64 |
+
const line = pendingStdinLineRef.current;
|
| 65 |
+
pendingStdinLineRef.current = '';
|
| 66 |
+
terminal.write('\r\n');
|
| 67 |
+
const runId = currentRunIdRef.current;
|
| 68 |
+
getSocket().emit(
|
| 69 |
+
WS_EVENTS.EXECUTE_INPUT,
|
| 70 |
+
{ runId: runId ?? '', input: line + '\n' },
|
| 71 |
+
(resp: { ok?: boolean } | undefined) => {
|
| 72 |
+
if (resp?.ok === false) {
|
| 73 |
+
terminal.writeln(
|
| 74 |
+
'\r\n\x1b[31m[Input did not reach the program — try Run again after output appears]\x1b[0m'
|
| 75 |
+
);
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
);
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const normalized = data.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
| 82 |
+
for (const ch of normalized) {
|
| 83 |
+
if (ch === '\n') {
|
| 84 |
+
flushLine();
|
| 85 |
+
} else if (ch === '\x7f' || ch === '\b') {
|
| 86 |
+
if (pendingStdinLineRef.current.length > 0) {
|
| 87 |
+
pendingStdinLineRef.current = pendingStdinLineRef.current.slice(0, -1);
|
| 88 |
+
terminal.write('\b \b');
|
| 89 |
+
}
|
| 90 |
+
} else if (ch === '\t') {
|
| 91 |
+
pendingStdinLineRef.current += '\t';
|
| 92 |
+
terminal.write('\t');
|
| 93 |
+
} else if (ch < ' ' && ch !== '\t') {
|
| 94 |
+
// ignore other C0 controls
|
| 95 |
+
} else {
|
| 96 |
+
pendingStdinLineRef.current += ch;
|
| 97 |
+
terminal.write(ch);
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
const resizeObserver = new ResizeObserver(() => fitAddon.fit());
|
| 103 |
+
resizeObserver.observe(containerRef.current);
|
| 104 |
+
|
| 105 |
+
return () => {
|
| 106 |
+
resizeObserver.disconnect();
|
| 107 |
+
terminal.dispose();
|
| 108 |
+
};
|
| 109 |
+
}, []);
|
| 110 |
+
|
| 111 |
+
useLayoutEffect(() => {
|
| 112 |
+
isExecutingRef.current = isExecuting;
|
| 113 |
+
}, [isExecuting]);
|
| 114 |
+
|
| 115 |
+
useEffect(() => {
|
| 116 |
+
const handleExecute = () => {
|
| 117 |
+
if (!activeFilePath) return;
|
| 118 |
+
const file = openFiles[activeFilePath];
|
| 119 |
+
if (!file || !environment) return;
|
| 120 |
+
|
| 121 |
+
console.log('[Terminal] handleExecute called, setting isExecuting=true');
|
| 122 |
+
setIsExecuting(true);
|
| 123 |
+
isExecutingRef.current = true; // Update ref immediately
|
| 124 |
+
pendingStdinLineRef.current = '';
|
| 125 |
+
currentRunIdRef.current = null;
|
| 126 |
+
terminalRef.current?.clear();
|
| 127 |
+
terminalRef.current?.writeln(`\x1b[33mRunning ${file.path}...\x1b[0m\n`);
|
| 128 |
+
|
| 129 |
+
// Focus the terminal to capture keyboard input
|
| 130 |
+
terminalRef.current?.focus();
|
| 131 |
+
|
| 132 |
+
const socket = getSocket();
|
| 133 |
+
socket.emit(WS_EVENTS.EXECUTE_REQUEST, {
|
| 134 |
+
language: environment,
|
| 135 |
+
content: file.content
|
| 136 |
+
});
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
window.addEventListener('glmpilot:execute', handleExecute);
|
| 140 |
+
return () => window.removeEventListener('glmpilot:execute', handleExecute);
|
| 141 |
+
}, [activeFilePath, openFiles, environment]);
|
| 142 |
+
|
| 143 |
+
useEffect(() => {
|
| 144 |
+
const socket = getSocket();
|
| 145 |
+
|
| 146 |
+
const onStarted = (data: { runId: string }) => {
|
| 147 |
+
currentRunIdRef.current = data.runId;
|
| 148 |
+
};
|
| 149 |
+
|
| 150 |
+
const onToken = (data: { token: string; isError?: boolean }) => {
|
| 151 |
+
const color = data.isError ? '\x1b[31m' : '\x1b[0m';
|
| 152 |
+
const text = data.token.replace(/\n/g, '\r\n');
|
| 153 |
+
terminalRef.current?.write(`${color}${text}\x1b[0m`);
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
const onComplete = () => {
|
| 157 |
+
console.log('[Terminal] EXECUTE_COMPLETE received, setting isExecuting=false');
|
| 158 |
+
setIsExecuting(false);
|
| 159 |
+
isExecutingRef.current = false; // Update ref immediately
|
| 160 |
+
pendingStdinLineRef.current = '';
|
| 161 |
+
currentRunIdRef.current = null;
|
| 162 |
+
terminalRef.current?.writeln('\n\n\x1b[32m❯ Execution finished.\x1b[0m ');
|
| 163 |
+
};
|
| 164 |
+
|
| 165 |
+
const onError = (data: { error: string }) => {
|
| 166 |
+
console.log('[Terminal] EXECUTE_ERROR received:', data.error);
|
| 167 |
+
setIsExecuting(false);
|
| 168 |
+
isExecutingRef.current = false; // Update ref immediately
|
| 169 |
+
pendingStdinLineRef.current = '';
|
| 170 |
+
currentRunIdRef.current = null;
|
| 171 |
+
terminalRef.current?.writeln(`\r\n\x1b[31m[Error] ${data.error}\x1b[0m\r\n`);
|
| 172 |
+
terminalRef.current?.writeln('\x1b[32m❯\x1b[0m ');
|
| 173 |
+
};
|
| 174 |
+
|
| 175 |
+
socket.on(WS_EVENTS.EXECUTE_STARTED, onStarted);
|
| 176 |
+
socket.on(WS_EVENTS.EXECUTE_TOKEN, onToken);
|
| 177 |
+
socket.on(WS_EVENTS.EXECUTE_COMPLETE, onComplete);
|
| 178 |
+
socket.on(WS_EVENTS.EXECUTE_ERROR, onError);
|
| 179 |
+
|
| 180 |
+
return () => {
|
| 181 |
+
socket.off(WS_EVENTS.EXECUTE_STARTED, onStarted);
|
| 182 |
+
socket.off(WS_EVENTS.EXECUTE_TOKEN, onToken);
|
| 183 |
+
socket.off(WS_EVENTS.EXECUTE_COMPLETE, onComplete);
|
| 184 |
+
socket.off(WS_EVENTS.EXECUTE_ERROR, onError);
|
| 185 |
+
};
|
| 186 |
+
}, []);
|
| 187 |
+
|
| 188 |
+
return <div ref={containerRef} className="h-full w-full" />;
|
| 189 |
+
}
|
packages/client/src/components/ui/badge.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import { cva, type VariantProps } from 'class-variance-authority';
|
| 3 |
+
import { cn } from '@/lib/utils';
|
| 4 |
+
|
| 5 |
+
const badgeVariants = cva(
|
| 6 |
+
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none',
|
| 7 |
+
{
|
| 8 |
+
variants: {
|
| 9 |
+
variant: {
|
| 10 |
+
default: 'border-transparent bg-primary text-primary-foreground',
|
| 11 |
+
secondary: 'border-transparent bg-secondary text-foreground',
|
| 12 |
+
destructive: 'border-transparent bg-destructive text-destructive-foreground',
|
| 13 |
+
outline: 'text-foreground border-border',
|
| 14 |
+
critical: 'border-transparent bg-red-500/20 text-red-400',
|
| 15 |
+
high: 'border-transparent bg-orange-500/20 text-orange-400',
|
| 16 |
+
medium: 'border-transparent bg-yellow-500/20 text-yellow-400',
|
| 17 |
+
low: 'border-transparent bg-blue-500/20 text-blue-400',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
defaultVariants: { variant: 'default' },
|
| 21 |
+
}
|
| 22 |
+
);
|
| 23 |
+
|
| 24 |
+
export interface BadgeProps
|
| 25 |
+
extends React.HTMLAttributes<HTMLDivElement>,
|
| 26 |
+
VariantProps<typeof badgeVariants> {}
|
| 27 |
+
|
| 28 |
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
| 29 |
+
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
export { Badge, badgeVariants };
|
packages/client/src/components/ui/button.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import { cva, type VariantProps } from 'class-variance-authority';
|
| 3 |
+
import { cn } from '@/lib/utils';
|
| 4 |
+
|
| 5 |
+
const buttonVariants = cva(
|
| 6 |
+
'inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50',
|
| 7 |
+
{
|
| 8 |
+
variants: {
|
| 9 |
+
variant: {
|
| 10 |
+
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90 rounded-lg',
|
| 11 |
+
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 rounded-lg',
|
| 12 |
+
outline: 'border border-border bg-transparent shadow-sm hover:bg-secondary hover:text-foreground rounded-lg',
|
| 13 |
+
secondary: 'bg-secondary text-foreground shadow-sm hover:bg-secondary/80 rounded-lg',
|
| 14 |
+
ghost: 'hover:bg-secondary hover:text-foreground rounded-lg',
|
| 15 |
+
link: 'text-primary underline-offset-4 hover:underline',
|
| 16 |
+
hero: 'bg-primary text-primary-foreground rounded-full px-6 py-3 text-base font-medium hover:bg-primary/90 transition-colors',
|
| 17 |
+
heroSecondary: 'liquid-glass text-foreground rounded-full px-6 py-3 text-base font-normal hover:bg-white/5 transition-colors',
|
| 18 |
+
},
|
| 19 |
+
size: {
|
| 20 |
+
default: 'h-9 px-4 py-2',
|
| 21 |
+
sm: 'h-8 rounded-md px-3 text-xs',
|
| 22 |
+
lg: 'h-10 rounded-md px-8',
|
| 23 |
+
icon: 'h-9 w-9',
|
| 24 |
+
},
|
| 25 |
+
},
|
| 26 |
+
defaultVariants: {
|
| 27 |
+
variant: 'default',
|
| 28 |
+
size: 'default',
|
| 29 |
+
},
|
| 30 |
+
}
|
| 31 |
+
);
|
| 32 |
+
|
| 33 |
+
export interface ButtonProps
|
| 34 |
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
| 35 |
+
VariantProps<typeof buttonVariants> {}
|
| 36 |
+
|
| 37 |
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
| 38 |
+
({ className, variant, size, ...props }, ref) => {
|
| 39 |
+
return (
|
| 40 |
+
<button
|
| 41 |
+
className={cn(buttonVariants({ variant, size, className }))}
|
| 42 |
+
ref={ref}
|
| 43 |
+
{...props}
|
| 44 |
+
/>
|
| 45 |
+
);
|
| 46 |
+
}
|
| 47 |
+
);
|
| 48 |
+
Button.displayName = 'Button';
|
| 49 |
+
|
| 50 |
+
export { Button, buttonVariants };
|
packages/client/src/components/ui/card.tsx
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import { cn } from '@/lib/utils';
|
| 3 |
+
|
| 4 |
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
| 5 |
+
({ className, ...props }, ref) => (
|
| 6 |
+
<div ref={ref} className={cn('rounded-xl border border-border bg-card text-card-foreground shadow', className)} {...props} />
|
| 7 |
+
)
|
| 8 |
+
);
|
| 9 |
+
Card.displayName = 'Card';
|
| 10 |
+
|
| 11 |
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
| 12 |
+
({ className, ...props }, ref) => (
|
| 13 |
+
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
| 14 |
+
)
|
| 15 |
+
);
|
| 16 |
+
CardHeader.displayName = 'CardHeader';
|
| 17 |
+
|
| 18 |
+
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
| 19 |
+
({ className, ...props }, ref) => (
|
| 20 |
+
<h3 ref={ref} className={cn('font-semibold leading-none tracking-tight', className)} {...props} />
|
| 21 |
+
)
|
| 22 |
+
);
|
| 23 |
+
CardTitle.displayName = 'CardTitle';
|
| 24 |
+
|
| 25 |
+
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
| 26 |
+
({ className, ...props }, ref) => (
|
| 27 |
+
<p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
| 28 |
+
)
|
| 29 |
+
);
|
| 30 |
+
CardDescription.displayName = 'CardDescription';
|
| 31 |
+
|
| 32 |
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
| 33 |
+
({ className, ...props }, ref) => (
|
| 34 |
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
| 35 |
+
)
|
| 36 |
+
);
|
| 37 |
+
CardContent.displayName = 'CardContent';
|
| 38 |
+
|
| 39 |
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
| 40 |
+
({ className, ...props }, ref) => (
|
| 41 |
+
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
| 42 |
+
)
|
| 43 |
+
);
|
| 44 |
+
CardFooter.displayName = 'CardFooter';
|
| 45 |
+
|
| 46 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
packages/client/src/components/ui/dialog.tsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
| 3 |
+
import { X } from 'lucide-react';
|
| 4 |
+
import { cn } from '@/lib/utils';
|
| 5 |
+
|
| 6 |
+
const Dialog = DialogPrimitive.Root;
|
| 7 |
+
const DialogTrigger = DialogPrimitive.Trigger;
|
| 8 |
+
const DialogPortal = DialogPrimitive.Portal;
|
| 9 |
+
const DialogClose = DialogPrimitive.Close;
|
| 10 |
+
|
| 11 |
+
const DialogOverlay = React.forwardRef<
|
| 12 |
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
| 13 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
| 14 |
+
>(({ className, ...props }, ref) => (
|
| 15 |
+
<DialogPrimitive.Overlay
|
| 16 |
+
ref={ref}
|
| 17 |
+
className={cn('fixed inset-0 z-50 bg-black/60 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', className)}
|
| 18 |
+
{...props}
|
| 19 |
+
/>
|
| 20 |
+
));
|
| 21 |
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
| 22 |
+
|
| 23 |
+
const DialogContent = React.forwardRef<
|
| 24 |
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
| 25 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
| 26 |
+
>(({ className, children, ...props }, ref) => (
|
| 27 |
+
<DialogPortal>
|
| 28 |
+
<DialogOverlay />
|
| 29 |
+
<DialogPrimitive.Content
|
| 30 |
+
ref={ref}
|
| 31 |
+
className={cn(
|
| 32 |
+
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-xl',
|
| 33 |
+
className
|
| 34 |
+
)}
|
| 35 |
+
{...props}
|
| 36 |
+
>
|
| 37 |
+
{children}
|
| 38 |
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
| 39 |
+
<X className="h-4 w-4" />
|
| 40 |
+
<span className="sr-only">Close</span>
|
| 41 |
+
</DialogPrimitive.Close>
|
| 42 |
+
</DialogPrimitive.Content>
|
| 43 |
+
</DialogPortal>
|
| 44 |
+
));
|
| 45 |
+
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
| 46 |
+
|
| 47 |
+
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
| 48 |
+
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
| 52 |
+
<div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
|
| 53 |
+
);
|
| 54 |
+
|
| 55 |
+
const DialogTitle = React.forwardRef<
|
| 56 |
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
| 57 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
| 58 |
+
>(({ className, ...props }, ref) => (
|
| 59 |
+
<DialogPrimitive.Title ref={ref} className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />
|
| 60 |
+
));
|
| 61 |
+
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
| 62 |
+
|
| 63 |
+
const DialogDescription = React.forwardRef<
|
| 64 |
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
| 65 |
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
| 66 |
+
>(({ className, ...props }, ref) => (
|
| 67 |
+
<DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
| 68 |
+
));
|
| 69 |
+
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
| 70 |
+
|
| 71 |
+
export { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription };
|
packages/client/src/components/ui/dropdown-menu.tsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
| 3 |
+
import { cn } from '@/lib/utils';
|
| 4 |
+
|
| 5 |
+
const DropdownMenu = DropdownMenuPrimitive.Root;
|
| 6 |
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
| 7 |
+
|
| 8 |
+
const DropdownMenuContent = React.forwardRef<
|
| 9 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
| 10 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
| 11 |
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
| 12 |
+
<DropdownMenuPrimitive.Portal>
|
| 13 |
+
<DropdownMenuPrimitive.Content
|
| 14 |
+
ref={ref}
|
| 15 |
+
sideOffset={sideOffset}
|
| 16 |
+
className={cn(
|
| 17 |
+
'z-50 min-w-[8rem] overflow-hidden rounded-lg border border-border bg-card p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
| 18 |
+
className
|
| 19 |
+
)}
|
| 20 |
+
{...props}
|
| 21 |
+
/>
|
| 22 |
+
</DropdownMenuPrimitive.Portal>
|
| 23 |
+
));
|
| 24 |
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
| 25 |
+
|
| 26 |
+
const DropdownMenuItem = React.forwardRef<
|
| 27 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
| 28 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
|
| 29 |
+
>(({ className, ...props }, ref) => (
|
| 30 |
+
<DropdownMenuPrimitive.Item
|
| 31 |
+
ref={ref}
|
| 32 |
+
className={cn(
|
| 33 |
+
'relative flex cursor-pointer select-none items-center rounded-md px-2 py-1.5 text-sm outline-none transition-colors focus:bg-secondary focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
| 34 |
+
className
|
| 35 |
+
)}
|
| 36 |
+
{...props}
|
| 37 |
+
/>
|
| 38 |
+
));
|
| 39 |
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
| 40 |
+
|
| 41 |
+
const DropdownMenuSeparator = React.forwardRef<
|
| 42 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
| 43 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
| 44 |
+
>(({ className, ...props }, ref) => (
|
| 45 |
+
<DropdownMenuPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-border', className)} {...props} />
|
| 46 |
+
));
|
| 47 |
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
| 48 |
+
|
| 49 |
+
const DropdownMenuLabel = React.forwardRef<
|
| 50 |
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
| 51 |
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
|
| 52 |
+
>(({ className, ...props }, ref) => (
|
| 53 |
+
<DropdownMenuPrimitive.Label ref={ref} className={cn('px-2 py-1.5 text-sm font-semibold', className)} {...props} />
|
| 54 |
+
));
|
| 55 |
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
| 56 |
+
|
| 57 |
+
export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel };
|
packages/client/src/components/ui/input.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from 'react';
|
| 2 |
+
import { cn } from '@/lib/utils';
|
| 3 |
+
|
| 4 |
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
| 5 |
+
|
| 6 |
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
| 7 |
+
({ className, type, ...props }, ref) => (
|
| 8 |
+
<input
|
| 9 |
+
type={type}
|
| 10 |
+
className={cn(
|
| 11 |
+
'flex h-9 w-full rounded-lg border border-border bg-secondary px-3 py-1 text-sm text-foreground shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50',
|
| 12 |
+
className
|
| 13 |
+
)}
|
| 14 |
+
ref={ref}
|
| 15 |
+
{...props}
|
| 16 |
+
/>
|
| 17 |
+
)
|
| 18 |
+
);
|
| 19 |
+
Input.displayName = 'Input';
|
| 20 |
+
|
| 21 |
+
export { Input };
|