Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: CI

on:
push:
branches:
- main
- 'APE-*'
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
run: npm ci

- name: Run lint
run: npm run lint

- name: Build application
run: npm run build

deploy:
runs-on: ubuntu-latest
needs: ci
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
run: npm ci

- name: Deploy to Vercel
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: |
npm install -g vercel@latest

if [ "${GITHUB_REF_NAME}" = "main" ]; then
PROD_FLAG="--prod"
else
PROD_FLAG=""
fi

# Let Vercel build remotely (same as Git integration). Local `vercel build`
# typechecks server/ with Node16 rules and fails on extensionless imports.
vercel deploy $PROD_FLAG --token="$VERCEL_TOKEN"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.vercel
.env*
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Edit a landing page through conversation with an AI agent. Chat text streams on the left (JSON blocks are hidden in the thread); the preview switches to the proposed page when the reply finishes, then **Apply** commits it for the next turn.

**Live demo:** _add your Vercel URL after deploy_
**Repository:** _add your GitHub URL_
**Repository:** https://github.com/olegpy/ai-page-editor

## Stack

Expand Down Expand Up @@ -64,11 +64,33 @@ Production: api/chat.ts → server/chat.ts → OpenAI
- `vite.config.ts` middleware does **not** run in production.
- Env: local `.env` loaded by `vite/apiDevPlugin.ts`; on Vercel set `OPENAI_API_KEY` in the dashboard.

## CI

GitHub Actions runs on every push and pull request to `main`:

- `npm ci`
- `npm run lint`
- `npm run build`

Workflow: [`.github/workflows/ci.yml`](.github/workflows/ci.yml)

## Deploy (Vercel)

1. Push to GitHub and import the project in [Vercel](https://vercel.com).
2. Add environment variable: `OPENAI_API_KEY`.
3. Deploy. Routes under `api/` run as serverless functions; the SPA is served from `dist/`.
1. Push `main` to GitHub (CI should pass).
2. [Import the repo](https://vercel.com/new) in Vercel (Git integration).
3. Vercel reads [`vercel.json`](vercel.json):
- **Build:** `npm run build` → output `dist`
- **API:** `api/chat.ts` → serverless `POST /api/chat`
- **SPA:** all non-`/api` routes → `index.html`
4. **Environment variables** (Production + Preview):

| Name | Value |
|------|--------|
| `OPENAI_API_KEY` | Your OpenAI API key (server only; never `VITE_*`) |

5. Deploy, then paste the production URL into **Live demo** above.

`vite/` dev middleware is not used on Vercel — only `api/chat.ts` + the static build.

## Scripts

Expand Down
2 changes: 1 addition & 1 deletion api/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CHAT_REQUEST_FAILED, createChatResponse, HttpStatus } from '../server'
import { CHAT_REQUEST_FAILED, createChatResponse, HttpStatus } from '../server/index.js'

export async function POST(request: Request) {
try {
Expand Down
15 changes: 15 additions & 0 deletions api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"],
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"esModuleInterop": true,
"verbatimModuleSyntax": true
},
"include": ["./**/*.ts", "../server/**/*.ts", "../src/landing/contentSchema.ts", "../src/landing/types.ts"]
}
8 changes: 4 additions & 4 deletions server/chat.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createOpenAI } from '@ai-sdk/openai'
import { convertToModelMessages, streamText, type UIMessage } from 'ai'
import { getChatPrompt } from './chatPrompt'
import { chatRequestSchema, type ChatRequest } from './chatRequest'
import { HttpStatus } from './httpStatus'
import { getChatPrompt } from './chatPrompt.js'
import { chatRequestSchema, type ChatRequest } from './chatRequest.js'
import { HttpStatus } from './httpStatus.js'
import {
CHAT_REQUEST_FAILED,
INVALID_CHAT_REQUEST,
OPENAI_API_KEY_MISSING,
} from './messages'
} from './messages.js'

export type { ChatRequest }

Expand Down
2 changes: 1 addition & 1 deletion server/chatPrompt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { LandingPageContent } from '../src/landing/types'
import type { LandingPageContent } from '../src/landing/types.js'

/** System prompt for POST /api/chat: current page JSON, rules, and expected reply format. */
export function getChatPrompt(pageContent: LandingPageContent): string {
Expand Down
2 changes: 1 addition & 1 deletion server/chatRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod'
import { landingPageContentSchema } from '../src/landing/contentSchema'
import { landingPageContentSchema } from '../src/landing/contentSchema.js'

const textPartSchema = z.object({
type: z.literal('text'),
Expand Down
10 changes: 5 additions & 5 deletions server/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export { createChatResponse, type ChatRequest } from './chat'
export { getChatPrompt } from './chatPrompt'
export { chatRequestSchema } from './chatRequest'
export { createChatResponse, type ChatRequest } from './chat.js'
export { getChatPrompt } from './chatPrompt.js'
export { chatRequestSchema } from './chatRequest.js'
export {
CHAT_REQUEST_FAILED,
INVALID_CHAT_REQUEST,
OPENAI_API_KEY_MISSING,
} from './messages'
export { HttpStatus, type HttpStatusCode } from './httpStatus'
} from './messages.js'
export { HttpStatus, type HttpStatusCode } from './httpStatus.js'
12 changes: 11 additions & 1 deletion vercel.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
{
"rewrites": [{ "source": "/((?!api/).*)", "destination": "/index.html" }]
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "vite",
"buildCommand": "npm run build",
"outputDirectory": "dist",
"installCommand": "npm ci",
"rewrites": [{ "source": "/((?!api/).*)", "destination": "/index.html" }],
"functions": {
"api/chat.ts": {
"maxDuration": 60
}
}
}
Loading