E5K7 commited on
Commit
c2c8c8d
·
0 Parent(s):

Initial commit: Rebranded to GLMPilot and migrated to GLM-5 API

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +6 -0
  2. .env.example +18 -0
  3. .gitignore +11 -0
  4. ARCHITECTURE.md +73 -0
  5. Dockerfile +38 -0
  6. README.md +174 -0
  7. docker-compose.yml +44 -0
  8. package-lock.json +0 -0
  9. package.json +22 -0
  10. packages/client/README.md +26 -0
  11. packages/client/index.html +14 -0
  12. packages/client/package.json +47 -0
  13. packages/client/postcss.config.js +6 -0
  14. packages/client/src/App.tsx +20 -0
  15. packages/client/src/components/ai/AIChatPanel.tsx +141 -0
  16. packages/client/src/components/ai/AgentResultsPanel.tsx +134 -0
  17. packages/client/src/components/ai/MarkdownMessage.tsx +43 -0
  18. packages/client/src/components/editor/CodeEditor.tsx +126 -0
  19. packages/client/src/components/editor/EditorTabs.tsx +46 -0
  20. packages/client/src/components/editor/MultiFileEditor.tsx +30 -0
  21. packages/client/src/components/explorer/FileTree.tsx +108 -0
  22. packages/client/src/components/github/RepoImporter.tsx +74 -0
  23. packages/client/src/components/ide/EnvironmentSelector.tsx +52 -0
  24. packages/client/src/components/landing/CTAFooterWrapper.tsx +27 -0
  25. packages/client/src/components/landing/CTASection.tsx +25 -0
  26. packages/client/src/components/landing/ChessSection.tsx +52 -0
  27. packages/client/src/components/landing/FeaturesSection.tsx +79 -0
  28. packages/client/src/components/landing/FooterSection.tsx +59 -0
  29. packages/client/src/components/landing/HLSVideo.tsx +45 -0
  30. packages/client/src/components/landing/HeroSection.tsx +69 -0
  31. packages/client/src/components/landing/LandingPage.tsx +19 -0
  32. packages/client/src/components/landing/MarqueeRow.tsx +22 -0
  33. packages/client/src/components/landing/Navbar.tsx +36 -0
  34. packages/client/src/components/landing/NumbersSection.tsx +46 -0
  35. packages/client/src/components/landing/ReverseChessSection.tsx +56 -0
  36. packages/client/src/components/landing/SectionBadge.tsx +20 -0
  37. packages/client/src/components/landing/TestimonialsSection.tsx +56 -0
  38. packages/client/src/components/layout/IDEShell.tsx +98 -0
  39. packages/client/src/components/layout/PanelLayout.tsx +67 -0
  40. packages/client/src/components/layout/Sidebar.tsx +95 -0
  41. packages/client/src/components/layout/StatusBar.tsx +64 -0
  42. packages/client/src/components/layout/TopBar.tsx +103 -0
  43. packages/client/src/components/preview/LivePreview.tsx +124 -0
  44. packages/client/src/components/terminal/TerminalPanel.tsx +189 -0
  45. packages/client/src/components/ui/badge.tsx +32 -0
  46. packages/client/src/components/ui/button.tsx +50 -0
  47. packages/client/src/components/ui/card.tsx +46 -0
  48. packages/client/src/components/ui/dialog.tsx +71 -0
  49. packages/client/src/components/ui/dropdown-menu.tsx +57 -0
  50. 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 };