Browse Source

feat: add initial agent frontend

main
Diego Vester 2 months ago
parent
commit
eb98634463
  1. 74
      README.md
  2. 4
      e2e/tsconfig.json
  3. 8
      e2e/vue.spec.ts
  4. 1
      env.d.ts
  5. 38
      eslint.config.ts
  6. 13
      index.html
  7. 6914
      package-lock.json
  8. 54
      package.json
  9. 110
      playwright.config.ts
  10. BIN
      public/favicon.ico
  11. 397
      src/App.vue
  12. 11
      src/__tests__/App.spec.ts
  13. 12
      src/main.ts
  14. 8
      src/router/index.ts
  15. 12
      src/stores/counter.ts
  16. 12
      tsconfig.app.json
  17. 14
      tsconfig.json
  18. 19
      tsconfig.node.json
  19. 11
      tsconfig.vitest.json
  20. 20
      vite.config.ts
  21. 14
      vitest.config.ts

74
README.md

@ -1,3 +1,73 @@ @@ -1,3 +1,73 @@
# agent.corneruniverse.com
# agent
AI code agent for building websites
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```
### Run End-to-End Tests with [Playwright](https://playwright.dev)
```sh
# Install browsers for the first run
npx playwright install
# When testing on CI, must build the project first
npm run build
# Runs the end-to-end tests
npm run test:e2e
# Runs the tests only on Chromium
npm run test:e2e -- --project=chromium
# Runs the tests of a specific file
npm run test:e2e -- tests/example.spec.ts
# Runs the tests in debug mode
npm run test:e2e -- --debug
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

4
e2e/tsconfig.json

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": ["./**/*"]
}

8
e2e/vue.spec.ts

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
import { test, expect } from '@playwright/test';
// See here how to get started:
// https://playwright.dev/docs/intro
test('visits the app root url', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toHaveText('You did it!');
})

1
env.d.ts vendored

@ -0,0 +1 @@ @@ -0,0 +1 @@
/// <reference types="vite/client" />

38
eslint.config.ts

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginPlaywright from 'eslint-plugin-playwright'
import pluginVitest from '@vitest/eslint-plugin'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
import pluginOxlint from 'eslint-plugin-oxlint'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{vue,ts,mts,tsx}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
...pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
{
...pluginPlaywright.configs['flat/recommended'],
files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'],
},
{
...pluginVitest.configs.recommended,
files: ['src/**/__tests__/*'],
},
skipFormatting,
...pluginOxlint.configs['flat/recommended'],
)

13
index.html

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

6914
package-lock.json generated

File diff suppressed because it is too large Load Diff

54
package.json

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
{
"name": "agent",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"test:e2e": "playwright test",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache",
"format": "prettier --write --experimental-cli src/"
},
"dependencies": {
"pinia": "^3.0.4",
"vue": "^3.5.26",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@tsconfig/node24": "^24.0.3",
"@types/jsdom": "^27.0.0",
"@types/node": "^24.10.4",
"@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3",
"@vitest/eslint-plugin": "^1.6.5",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1",
"eslint": "^9.39.2",
"eslint-plugin-oxlint": "~1.38.0",
"eslint-plugin-playwright": "^2.4.0",
"eslint-plugin-vue": "~10.6.2",
"jiti": "^2.6.1",
"jsdom": "^27.4.0",
"npm-run-all2": "^8.0.4",
"oxlint": "~1.38.0",
"prettier": "3.7.4",
"typescript": "~5.9.3",
"vite": "beta",
"vite-plugin-vue-devtools": "^8.0.5",
"vitest": "^4.0.16",
"vue-tsc": "^3.2.2"
}
}

110
playwright.config.ts

@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
import process from 'node:process'
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.CI ? 'http://localhost:4173' : 'http://localhost:5173',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Only on CI systems run the tests headless */
headless: !!process.env.CI,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
webServer: {
/**
* Use the dev server by default for faster feedback loop.
* Use the preview server on CI for more realistic testing.
* Playwright will re-use the local server if there is already a dev-server running.
*/
command: process.env.CI ? 'npm run preview' : 'npm run dev',
port: process.env.CI ? 4173 : 5173,
reuseExistingServer: !process.env.CI,
},
})

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

397
src/App.vue

@ -0,0 +1,397 @@ @@ -0,0 +1,397 @@
<template>
<div id="app">
<header>
<h1>AI Code Agent</h1>
<div v-if="user" class="user-info">
{{ user.username }}
<button @click="logout">Logout</button>
</div>
</header>
<main>
<!-- Login -->
<div v-if="!user" class="login-section">
<p>Login with your Gitea account to get started.</p>
<a :href="apiUrl + '/api/auth/login'" class="btn">Login with Gitea</a>
</div>
<!-- Main Interface -->
<div v-else class="workspace">
<!-- Repo Selection -->
<div class="section">
<label>Repository</label>
<select v-model="selectedRepo" @change="clearState">
<option value="">Select a repository...</option>
<option v-for="repo in repos" :key="repo.name" :value="repo.name">
{{ repo.name }}
</option>
</select>
</div>
<!-- Prompt Input -->
<div v-if="selectedRepo" class="section">
<label>What would you like to change?</label>
<textarea
v-model="prompt"
placeholder="e.g., Add a contact form to the homepage with name, email, and message fields"
rows="4"
></textarea>
<button
@click="generate"
:disabled="generating || !prompt.trim()"
class="btn"
>
{{ generating ? "Generating..." : "Generate Changes" }}
</button>
</div>
<!-- Error Display -->
<div v-if="error" class="error">
{{ error }}
</div>
<!-- Generated Changes -->
<div v-if="operations" class="section">
<h2>Proposed Changes</h2>
<p class="summary">{{ operations.summary }}</p>
<div
v-for="op in operations.operations"
:key="op.path"
class="file-change"
>
<div class="file-header">
<span class="action" :class="op.action">{{ op.action }}</span>
<span class="path">{{ op.path }}</span>
</div>
<pre v-if="op.content" class="content">{{ op.content }}</pre>
</div>
<div class="actions">
<button @click="submit" :disabled="submitting" class="btn primary">
{{ submitting ? "Creating PR..." : "Create Pull Request" }}
</button>
<button @click="clearState" class="btn secondary">Discard</button>
</div>
</div>
<!-- PR Created -->
<div v-if="result" class="section success">
<h2>Pull Request Created!</h2>
<p>
<a :href="result.pr_url" target="_blank"
>View PR #{{ result.pr_number }} in Gitea</a
>
</p>
<p v-if="result.preview_url">
<a :href="result.preview_url" target="_blank"
>Preview: {{ result.preview_url }}</a
>
<br /><small>(Preview will be ready in ~30 seconds)</small>
</p>
</div>
</div>
</main>
</div>
</template>
<script>
export default {
data() {
return {
apiUrl: import.meta.env.VITE_API_URL || "",
user: null,
repos: [],
selectedRepo: "",
prompt: "",
generating: false,
operations: null,
submitting: false,
result: null,
error: null,
};
},
async mounted() {
await this.checkAuth();
if (this.user) {
await this.loadRepos();
}
},
methods: {
async checkAuth() {
try {
const res = await fetch(`${this.apiUrl}/api/auth/user`, {
credentials: "include",
});
if (res.ok) {
this.user = await res.json();
}
} catch (e) {
console.error("Auth check failed:", e);
}
},
async logout() {
await fetch(`${this.apiUrl}/api/auth/logout`, {
method: "POST",
credentials: "include",
});
this.user = null;
this.repos = [];
this.clearState();
},
async loadRepos() {
try {
const res = await fetch(`${this.apiUrl}/api/repos`, {
credentials: "include",
});
if (res.ok) {
this.repos = await res.json();
}
} catch (e) {
console.error("Failed to load repos:", e);
}
},
clearState() {
this.prompt = "";
this.operations = null;
this.result = null;
this.error = null;
},
async generate() {
this.generating = true;
this.error = null;
this.operations = null;
this.result = null;
try {
const res = await fetch(`${this.apiUrl}/api/generate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
repo: this.selectedRepo,
prompt: this.prompt,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Generation failed");
this.operations = data;
} catch (e) {
this.error = e.message;
} finally {
this.generating = false;
}
},
async submit() {
this.submitting = true;
this.error = null;
try {
const res = await fetch(`${this.apiUrl}/api/submit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
repo: this.selectedRepo,
operations: this.operations,
summary: this.operations.summary,
prompt: this.prompt,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Submit failed");
this.result = data;
this.operations = null;
} catch (e) {
this.error = e.message;
} finally {
this.submitting = false;
}
},
},
};
</script>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
}
#app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
h1 {
margin: 0;
font-size: 24px;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.section {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
label {
display: block;
font-weight: 600;
margin-bottom: 8px;
}
select,
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
margin-bottom: 15px;
}
textarea {
resize: vertical;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: #4a9eff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
text-decoration: none;
}
.btn:hover {
background: #3a8eef;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn.secondary {
background: #666;
}
.btn.primary {
background: #22c55e;
}
.error {
background: #fee;
color: #c00;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.success {
background: #efe;
border: 1px solid #afa;
}
.summary {
font-style: italic;
color: #666;
}
.file-change {
border: 1px solid #ddd;
border-radius: 4px;
margin: 15px 0;
overflow: hidden;
}
.file-header {
background: #f5f5f5;
padding: 10px;
display: flex;
gap: 10px;
align-items: center;
}
.action {
padding: 2px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.action.modify {
background: #fef3c7;
color: #92400e;
}
.action.create {
background: #d1fae5;
color: #065f46;
}
.action.delete {
background: #fee2e2;
color: #991b1b;
}
.path {
font-family: monospace;
font-size: 14px;
}
.content {
margin: 0;
padding: 15px;
overflow-x: auto;
background: #fafafa;
font-size: 13px;
max-height: 300px;
overflow-y: auto;
}
.actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.login-section {
text-align: center;
padding: 60px 20px;
background: white;
border-radius: 8px;
}
a {
color: #4a9eff;
}
</style>

11
src/__tests__/App.spec.ts

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import App from "../App.vue";
describe("App", () => {
it("mounts renders properly", () => {
const wrapper = mount(App);
expect(wrapper.text()).toContain("You did it!");
});
});

12
src/main.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount("#app");

8
src/router/index.ts

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
import { createRouter, createWebHistory } from "vue-router";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [],
});
export default router;

12
src/stores/counter.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});

12
tsconfig.app.json

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

14
tsconfig.json

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
}
]
}

19
tsconfig.node.json

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

11
tsconfig.vitest.json

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
{
"extends": "./tsconfig.app.json",
"include": ["src/**/__tests__/*", "env.d.ts"],
"exclude": [],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
"lib": [],
"types": ["node", "jsdom"]
}
}

20
vite.config.ts

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})

14
vitest.config.ts

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
)
Loading…
Cancel
Save