ugly-app 0.1.41 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,9 +6,9 @@ A full-stack TypeScript framework for building production-ready web applications
6
6
 
7
7
  - **Server**: Express + WebSocket with type-safe RPC and Zod validation
8
8
  - **Client**: React + Vite with typed routing, lazy pages, and popup management
9
- - **Database**: MongoDB with typed collections, indexes, migrations, and live document tracking
9
+ - **Database**: MongoDB with typed collections, dot-notation updates, indexes, migrations, and live document tracking
10
10
  - **Auth**: JWT + HttpOnly cookies, ugly.bot OAuth out of the box, extensible via `AuthProvider`
11
- - **AI**: Text generation (Together, Claude, OpenAI, Google, Groq, Fireworks) + image generation (Together, FAL, Google, Wavespeed)
11
+ - **AI**: Text generation (Together, Claude, OpenAI, Google, Groq, Fireworks) + image generation (Together, FAL, Google, Wavespeed) + embeddings + STT/TTS
12
12
  - **Storage**: Cloudflare R2 / AWS S3 with presigned uploads
13
13
  - **CLI**: `ugly-app` commands for dev, build, deploy, migrations, logs, and auth utilities
14
14
 
@@ -29,7 +29,14 @@ npm run dev
29
29
  Entry point for the server. Creates an Express + WebSocket server with typed RPC, auth, and all framework services.
30
30
 
31
31
  ```typescript
32
- import { createApp, createUserHelper, type CallHandlers, type RequestHandlers, type HandlerContext } from 'ugly-app';
32
+ import {
33
+ createApp,
34
+ createUserHelper,
35
+ getFeedbackHandlers,
36
+ type CallHandlers,
37
+ type RequestHandlers,
38
+ type HandlerContext,
39
+ } from 'ugly-app';
33
40
  import { dbDefaults } from 'ugly-app/shared';
34
41
  import { functions, requests } from '../shared/api';
35
42
  import { collections } from '../shared/collections';
@@ -39,6 +46,7 @@ import type { User } from '../shared/types';
39
46
  const userHelper = createUserHelper<User>(collections.user);
40
47
 
41
48
  const calls = {
49
+ ...getFeedbackHandlers(maintainBotUserId),
42
50
  myFunction: async (ctx: HandlerContext, input) => {
43
51
  return { greeting: `Hello, ${input.name}` };
44
52
  },
@@ -135,7 +143,7 @@ export const requests = defineRequests({
135
143
 
136
144
  - `fn({ input, output })` — defines a mutation (write operation). `input` and `output` are Zod schemas.
137
145
  - `req({ input, output })` — defines a query (read operation).
138
- - `defineFunctions()` / `defineRequests()` — identity wrappers that preserve types.
146
+ - `defineFunctions()` / `defineRequests()` — identity wrappers that preserve types. `defineCalls` is an alias for `defineFunctions`.
139
147
  - `z` is re-exported from Zod for convenience.
140
148
 
141
149
  ### Collections (`shared/collections.ts`)
@@ -156,11 +164,14 @@ export const collections = defineCollections({
156
164
  });
157
165
  ```
158
166
 
159
- Collection meta fields:
160
- - `cache` — enable in-memory caching
161
- - `trackable` — allow real-time `trackDoc`/`trackDocs` subscriptions
162
- - `public` — accessible without auth
163
- - `parent` — parent collection name for cascade deletes (or `null`)
167
+ Each collection definition has:
168
+ - `type` — phantom field for TypeScript type inference (never read at runtime)
169
+ - `meta` — runtime metadata:
170
+ - `cache` — enable in-memory caching
171
+ - `trackable` — allow real-time `trackDoc`/`trackDocs` subscriptions
172
+ - `public` — accessible without auth
173
+ - `parent` — parent collection name for cascade deletes (or `null`)
174
+ - `onDelete?` — optional callback invoked on document deletion
164
175
 
165
176
  All documents extend `DBObject`: `{ id: string, version: number, created: Date, updated: Date }`.
166
177
 
@@ -274,6 +285,10 @@ Wraps your app with context for `useApp()`. Provides user info, socket access, p
274
285
  | `splashDone(step)` | Mark a splash screen step as complete |
275
286
  | `localizer(key, params?)` | Localize a string key |
276
287
 
288
+ `useAppOptional()` returns the same context or `null` if outside `<AppProvider>`.
289
+
290
+ `useLocalizer()` returns the localizer function (falls back to identity if outside provider).
291
+
277
292
  ### Router
278
293
 
279
294
  #### Setup (`client/router.ts`)
@@ -307,7 +322,18 @@ export const allPages = {
307
322
  ```
308
323
 
309
324
  - **`lazyPage(factory)`** — lazy-imports a default-exported React component. The component receives route params as props.
310
- - **`lazyPageLoader(factory)`** — lazy-imports an async loader function `(params) => Promise<ReactElement>` for routes that need data fetching before render. The loader file is the chunk boundary.
325
+ - **`lazyPageLoader(factory)`** — lazy-imports an async loader function `(params) => Promise<ReactElement>` for routes that need data fetching before render. The loader file is the chunk boundary — it can statically import its page component.
326
+
327
+ **`lazyPageLoader` example:**
328
+
329
+ ```typescript
330
+ // pages/SlowPageLoader.tsx
331
+ import SlowPage from './SlowPage'; // static import OK — same chunk
332
+ export default async function PageLoader({ id }: { id: string }) {
333
+ const data = await fetchSlowData(id);
334
+ return <SlowPage {...data} />;
335
+ }
336
+ ```
311
337
 
312
338
  #### Navigation with `useRouter()`
313
339
 
@@ -346,10 +372,12 @@ handle.hide(); // dismiss programmatically
346
372
  ```
347
373
 
348
374
  Popup modes:
349
- - **`block`** — dark backdrop (40% opacity), clicking backdrop does NOT dismiss
375
+ - **`block`** (default) — dark backdrop (40% opacity), clicking backdrop does NOT dismiss
350
376
  - **`transient`** — light backdrop (20% opacity), clicking backdrop dismisses
351
377
  - **`contextMenu`** — same as transient, intended for menus and pickers
352
378
 
379
+ `renderLayer` receives `{ content, spring, hide }` — `spring` is an animated value from 0 to 1, `hide` is a function to close the popup.
380
+
353
381
  #### `Link` component
354
382
 
355
383
  ```tsx
@@ -415,14 +443,14 @@ configurator.setAuth({
415
443
  await ctx.db.setDoc(collections.note, doc);
416
444
  await ctx.db.setDoc(collections.note, doc, { skipIfExists: true });
417
445
 
418
- // Partial update — only specified fields
446
+ // Partial update — only specified fields (supports dot-notation paths)
419
447
  await ctx.db.setDocFields(collections.note, id, { title: 'New title' });
420
448
 
421
449
  // Partial update — returns null if document doesn't exist (no error)
422
450
  const doc = await ctx.db.setDocFieldsOrIgnore(collections.note, id, { title });
423
451
 
424
- // Partial update — creates the document if it doesn't exist
425
- await ctx.db.setDocFieldsOrCreate(collections.note, id, { title });
452
+ // Partial update — creates the document if it doesn't exist (obj = default doc for insert)
453
+ await ctx.db.setDocFieldsOrCreate(collections.note, id, { title }, defaultDoc);
426
454
 
427
455
  // MongoDB update operators ($inc, $addToSet, $pull, $unset, $set)
428
456
  await ctx.db.setDocOp(collections.note, id, { $inc: { views: 1 } });
@@ -439,6 +467,10 @@ const notes = await ctx.db.getDocs(collections.note, { userId }, { sort: { creat
439
467
  const results = await ctx.db.getQuery<MyResult>('note', pipeline, { skip, limit });
440
468
  const count = await ctx.db.getQueryCount('note', pipeline);
441
469
  const raw = await ctx.db.getQueryRaw<T>('note', pipeline);
470
+
471
+ // Dynamic/untyped access (when collection name is a runtime string)
472
+ const doc = await ctx.db.rawGetDoc(collectionName, id);
473
+ const docs = await ctx.db.rawGetDocs(collectionName, filter);
442
474
  ```
443
475
 
444
476
  ### Deleting
@@ -460,12 +492,13 @@ const key = ctx.db.cacheKey('prefix', id); // generate a cache key
460
492
  ### Helpers
461
493
 
462
494
  ```typescript
463
- import { dbDefaults, createUserHelper } from 'ugly-app';
495
+ import { createUserHelper } from 'ugly-app';
496
+ import { dbDefaults } from 'ugly-app/shared';
464
497
 
465
498
  // dbDefaults() returns { version: 1, created: new Date(), updated: new Date() }
466
499
  const doc = { id: newId(), ...dbDefaults(), title: 'Hello' };
467
500
 
468
- // createUserHelper — typed user CRUD
501
+ // createUserHelper — typed user CRUD with get, set, update methods
469
502
  const userHelper = createUserHelper<User>(collections.user);
470
503
  const user = await userHelper.get(db, userId);
471
504
  await userHelper.set(db, { id: userId, ...dbDefaults(), email });
@@ -485,6 +518,8 @@ export const dbIndexes = defineDbIndexes({
485
518
  });
486
519
  ```
487
520
 
521
+ Index types supported: standard (`IndexDef`), text search (`SearchIndexDef`), and vector/embedding (`VectorIndexDef` with cosine, euclidean, or dotProduct similarity).
522
+
488
523
  Run `npm run db:init` to create/update indexes.
489
524
 
490
525
  ---
@@ -538,18 +573,48 @@ import { createImageGenClient } from 'ugly-app';
538
573
  const imageGen = createImageGenClient();
539
574
  ```
540
575
 
576
+ ### Embeddings
577
+
578
+ ```typescript
579
+ import { createEmbeddingClient, cosineSimilarity } from 'ugly-app';
580
+ const embeddings = createEmbeddingClient();
581
+ const similarity = cosineSimilarity(vectorA, vectorB);
582
+ ```
583
+
584
+ ### Speech-to-text / Text-to-speech
585
+
586
+ ```typescript
587
+ // Server-side providers
588
+ import { registerSTTProvider, groqWhisperSTTProvider, loadVADModel } from 'ugly-app';
589
+ import { registerTTSProvider, azureTTSProvider } from 'ugly-app';
590
+
591
+ // Client-side hooks
592
+ import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
593
+ ```
594
+
541
595
  ### Custom providers
542
596
 
543
597
  ```typescript
544
- import { registerTextGenProvider, registerImageGenProvider } from 'ugly-app';
598
+ import { registerTextGenProvider, registerImageGenProvider, registerEmbeddingProvider } from 'ugly-app';
545
599
 
546
600
  registerTextGenProvider('myProvider', myTextGenImplementation);
547
601
  registerImageGenProvider('myProvider', myImageGenImplementation);
602
+ registerEmbeddingProvider('myProvider', myEmbeddingImplementation);
548
603
  ```
549
604
 
550
- ### AI proxy
605
+ ### Client-side AI calls
551
606
 
552
- The client accesses AI via `POST /ai/request` (same-origin, avoids CORS). The server verifies the user JWT and forwards to `https://ugly.bot` using the `UGLY_BOT_TOKEN` env var.
607
+ The client can call AI through the server proxy without managing tokens directly:
608
+
609
+ ```typescript
610
+ import { callTextGen, callJsonGen, callImageGen } from 'ugly-app/client';
611
+
612
+ const text = await callTextGen(messages, options);
613
+ const json = await callJsonGen(schema, messages);
614
+ const image = await callImageGen(prompt, options);
615
+ ```
616
+
617
+ These call `POST /ai/request` (same-origin, avoids CORS). The server verifies the user JWT and forwards to `https://ugly.bot` using the `UGLY_BOT_TOKEN` env var.
553
618
 
554
619
  ---
555
620
 
@@ -569,7 +634,7 @@ const url = storage.url('public', destKey);
569
634
  const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
570
635
  ```
571
636
 
572
- Buckets: `'public'` and `'temp'`.
637
+ Buckets: `'public'` and `'temp'`. Supports Cloudflare R2 (production) or MinIO (dev).
573
638
 
574
639
  Static build-time assets go in `client/public/`. Never hardcode `/asset/...` paths — use the `buildId` from `shared/Build.ts`.
575
640
 
@@ -607,7 +672,7 @@ sub(); // unsubscribe
607
672
  ### Redis
608
673
 
609
674
  ```typescript
610
- import { getRedisClient } from 'ugly-app';
675
+ import { getRedisClient, redisGet, redisSet, redisDel, redisPublish, redisSubscribe } from 'ugly-app';
611
676
  const redis = getRedisClient();
612
677
  ```
613
678
 
@@ -622,31 +687,63 @@ configurator.setWorkerQueue(queue);
622
687
  await enqueueTask('taskName', payload);
623
688
  ```
624
689
 
625
- ### Rate limiting
690
+ ### Billing
626
691
 
627
692
  ```typescript
628
- // Inside a handler always call before expensive operations
629
- await ctx.rateLimit.check();
693
+ import { initBillingGateway, getBillingGateway } from 'ugly-app';
694
+
695
+ await initBillingGateway({ /* config */ });
696
+ const billing = getBillingGateway();
630
697
  ```
631
698
 
632
- ### Embeddings
699
+ ### Client error capture
633
700
 
634
701
  ```typescript
635
- import { createEmbeddingClient, registerEmbeddingProvider } from 'ugly-app';
636
- const embeddings = createEmbeddingClient();
702
+ import { captureClientError, initClientLogger } from 'ugly-app/client';
703
+
704
+ initClientLogger(); // call once at startup — captures unhandled errors
705
+ captureClientError(error); // manually report an error to POST /api/client-error
637
706
  ```
638
707
 
639
- ### Billing
708
+ ### Feedback
640
709
 
641
710
  ```typescript
642
- import { initBillingGateway, getBillingGateway } from 'ugly-app';
711
+ import { FeedbackButton, setFeedbackContext, clearFeedbackContext } from 'ugly-app/client';
643
712
 
644
- await initBillingGateway({ /* config */ });
645
- const billing = getBillingGateway();
713
+ // Render the built-in feedback button (bottom-right, always at [data-id="feedback-button"])
714
+ <FeedbackButton />
715
+
716
+ // Set contextual data attached to feedback submissions
717
+ setFeedbackContext({ page: 'editor', noteId: '123' });
718
+ clearFeedbackContext();
719
+ ```
720
+
721
+ Server-side, `getFeedbackHandlers(maintainBotUserId)` provides the RPC handlers for submitting and managing feedback. User feedback history is available at `GET /my_feedback` (requires auth cookie, returns markdown).
722
+
723
+ ### Rate limiting
724
+
725
+ ```typescript
726
+ // Inside a handler — always call before expensive operations
727
+ await ctx.rateLimit.check();
646
728
  ```
647
729
 
648
730
  ---
649
731
 
732
+ ## Built-in endpoints
733
+
734
+ | Endpoint | Description |
735
+ |----------|-------------|
736
+ | `GET /health` | Health check — returns `{ status: 'ok', timestamp }` |
737
+ | `POST /auth/verify` | Exchange OAuth code for a session cookie |
738
+ | `POST /auth/logout` | Clear the auth cookie |
739
+ | `GET /auth/token` | Refresh and return the current token |
740
+ | `GET /auth/url` | Get the OAuth popup URL |
741
+ | `POST /ai/request` | AI proxy — forwards to ugly.bot (requires auth) |
742
+ | `POST /api/client-error` | Client-side error capture |
743
+ | `GET /my_feedback` | User feedback history (markdown, requires auth) |
744
+
745
+ ---
746
+
650
747
  ## Migrations
651
748
 
652
749
  Never change a collection field type without writing a migration:
@@ -691,6 +788,7 @@ Run with `npm run db:migrate`. Use `npm run db:migrate -- --status` to preview p
691
788
  | Variable | Description |
692
789
  |----------|-------------|
693
790
  | `JWT_SECRET` | **Required** — signs auth tokens |
791
+ | `JWT_EXPIRY_SECONDS` | Token lifetime (optional) |
694
792
  | `MONGODB_URI` | MongoDB connection string |
695
793
  | `PORT` | Server port (default: 3000) |
696
794
  | `NODE_ENV` | `development` or `production` |
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"AA8DA,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4DxE"}
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"AAyFA,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4DxE"}
@@ -38,7 +38,33 @@ function generateEnv(examplePath, projectName) {
38
38
  })
39
39
  .join('\n');
40
40
  }
41
+ /** Locate the `code` binary — checks PATH then known macOS app bundle locations. */
42
+ function findCodeCli() {
43
+ try {
44
+ execSync('code --version', { stdio: 'ignore' });
45
+ return 'code';
46
+ }
47
+ catch { /* not in PATH */ }
48
+ const candidates = process.platform === 'win32'
49
+ ? [
50
+ path.join(process.env['LOCALAPPDATA'] ?? '', 'Programs\\Microsoft VS Code\\bin\\code.cmd'),
51
+ path.join(process.env['ProgramFiles'] ?? '', 'Microsoft VS Code\\bin\\code.cmd'),
52
+ path.join(process.env['ProgramFiles(x86)'] ?? '', 'Microsoft VS Code\\bin\\code.cmd'),
53
+ ]
54
+ : [
55
+ '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
56
+ path.join(os.homedir(), 'Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'),
57
+ ];
58
+ for (const p of candidates) {
59
+ if (fs.existsSync(p))
60
+ return `"${p}"`;
61
+ }
62
+ return null;
63
+ }
41
64
  async function installVsixExtension() {
65
+ const codeCli = findCodeCli();
66
+ if (!codeCli)
67
+ return; // VSCode not found — skip silently
42
68
  const url = 'https://ugly.bot/asset/ugly-bot.vsix';
43
69
  const vsixPath = path.join(os.tmpdir(), 'ugly-bot.vsix');
44
70
  try {
@@ -48,7 +74,7 @@ async function installVsixExtension() {
48
74
  throw new Error(`HTTP ${res.status}`);
49
75
  const buf = await res.arrayBuffer();
50
76
  await fs.writeFile(vsixPath, Buffer.from(buf));
51
- execSync(`code --install-extension "${vsixPath}"`, { stdio: 'inherit' });
77
+ execSync(`${codeCli} --install-extension "${vsixPath}"`, { stdio: 'inherit' });
52
78
  console.log('[ugly-app] VSCode extension installed.');
53
79
  }
54
80
  catch {
@@ -75,6 +101,8 @@ export async function scaffoldProject(projectName) {
75
101
  // Generate .env from .env.example with a real JWT_SECRET and optional keys commented out
76
102
  const envContent = generateEnv(path.join(destDir, '.env.example'), projectName);
77
103
  await fs.writeFile(path.join(destDir, '.env'), envContent, 'utf-8');
104
+ console.log('[ugly-app] Installing dependencies...');
105
+ execSync('npm install', { cwd: destDir, stdio: 'inherit' });
78
106
  console.log('[ugly-app] Initialising git repository...');
79
107
  execSync('git init', { cwd: destDir, stdio: 'inherit' });
80
108
  execSync('git add .', { cwd: destDir, stdio: 'inherit' });
@@ -82,8 +110,6 @@ export async function scaffoldProject(projectName) {
82
110
  cwd: destDir,
83
111
  stdio: 'inherit',
84
112
  });
85
- console.log('[ugly-app] Installing dependencies...');
86
- execSync('npm install', { cwd: destDir, stdio: 'inherit' });
87
113
  const portStart = 3000;
88
114
  const projectId = nanoid(10).toLowerCase();
89
115
  writeUglyAppConfig(destDir, { projectId, portStart });
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/cli/upgrade.ts"],"names":[],"mappings":"AA2BA,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAgIhD"}
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/cli/upgrade.ts"],"names":[],"mappings":"AA4BA,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAgIhD"}
@@ -15,6 +15,7 @@ const CONFIG_FILES = [
15
15
  'CLAUDE.md',
16
16
  'playwright.config.ts',
17
17
  'client/index.html',
18
+ '.vscode/settings.json',
18
19
  ];
19
20
  // Files stored under a different name in templates (npm strips dotfiles from packages)
20
21
  const RENAMED_FILES = [
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../src/cli/upgrade.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../src/cli/upgrade.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-app",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "type": "module",
5
5
  "main": "./dist/server/index.js",
6
6
  "exports": {
@@ -4,7 +4,6 @@ import {
4
4
  Card,
5
5
  Input,
6
6
  PageLayout,
7
- Text,
8
7
  callImageGen,
9
8
  callTextGen,
10
9
  } from 'ugly-app/client';
@@ -12,83 +11,102 @@ import type { ImageGenModel, TextGenModel } from 'ugly-app/shared';
12
11
  import { imageGenModels, textGenModels } from 'ugly-app/shared';
13
12
 
14
13
  type Mode = 'text' | 'image';
14
+ interface LogEntry { ts: number; msg: string; kind: 'info' | 'ok' | 'err' }
15
+
16
+ function fmt(ms: number): string {
17
+ return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
18
+ }
15
19
 
16
20
  export default function AITestPage(): React.ReactElement {
17
21
  const [mode, setMode] = useState<Mode>('text');
18
22
  const [prompt, setPrompt] = useState('');
19
23
  const [textModel, setTextModel] = useState<TextGenModel>(textGenModels[0]!);
20
- const [imageModel, setImageModel] = useState<ImageGenModel>(
21
- imageGenModels[0]!,
22
- );
24
+ const [imageModel, setImageModel] = useState<ImageGenModel>(imageGenModels[0]!);
23
25
  const [result, setResult] = useState('');
24
26
  const [imageUrl, setImageUrl] = useState('');
25
27
  const [loading, setLoading] = useState(false);
26
- const [error, setError] = useState('');
28
+ const [logs, setLogs] = useState<LogEntry[]>([]);
29
+
30
+ function addLog(msg: string, kind: LogEntry['kind'] = 'info'): number {
31
+ const ts = Date.now();
32
+ setLogs((prev) => [...prev, { ts, msg, kind }]);
33
+ return ts;
34
+ }
27
35
 
28
36
  async function handleRun(): Promise<void> {
29
37
  if (!prompt.trim()) return;
30
38
  setLoading(true);
31
- setError('');
32
39
  setResult('');
33
40
  setImageUrl('');
41
+ setLogs([]);
42
+
43
+ const started = Date.now();
44
+ const model = mode === 'text' ? textModel : imageModel;
45
+ addLog(`Using model: ${model}`);
46
+ addLog(`Sending ${mode} request…`);
47
+
34
48
  try {
35
49
  if (mode === 'text') {
36
50
  const text = await callTextGen({
37
51
  model: textModel,
38
52
  messages: [{ role: 'user', content: prompt }],
39
53
  });
54
+ const elapsed = Date.now() - started;
55
+ addLog(`Done in ${fmt(elapsed)} — ${text.length} chars`, 'ok');
40
56
  setResult(text);
41
57
  } else {
42
58
  const url = await callImageGen({ model: imageModel, prompt });
59
+ const elapsed = Date.now() - started;
60
+ addLog(`Done in ${fmt(elapsed)}`, 'ok');
43
61
  setImageUrl(url);
44
62
  }
45
63
  } catch (err) {
46
- setError(err instanceof Error ? err.message : String(err));
64
+ const msg = err instanceof Error ? err.message : String(err);
65
+ addLog(msg, 'err');
47
66
  } finally {
48
67
  setLoading(false);
49
68
  }
50
69
  }
51
70
 
52
71
  const model = mode === 'text' ? textModel : imageModel;
53
- const models: readonly string[] =
54
- mode === 'text' ? textGenModels : imageGenModels;
72
+ const models: readonly string[] = mode === 'text' ? textGenModels : imageGenModels;
55
73
 
56
74
  return (
57
75
  <PageLayout
58
76
  header={
59
- <div className="px-6 py-4">
60
- <a href="/" className="text-blue-600">
77
+ <div className="px-6 py-4 border-b border-gray-200 dark:border-gray-800">
78
+ <a href="/" className="text-sm text-gray-500 hover:text-gray-900 dark:hover:text-gray-100 transition-colors">
61
79
  ← Home
62
80
  </a>
63
81
  </div>
64
82
  }
65
83
  >
66
- <div className="max-w-2xl mx-auto">
67
- <Text size="xl" weight="bold">
68
- AI Model Test
69
- </Text>
70
-
71
- <div className="mt-4 flex gap-2">
72
- <Button
73
- variant={mode === 'text' ? 'primary' : 'secondary'}
74
- onClick={() => setMode('text')}
75
- >
76
- Text
77
- </Button>
78
- <Button
79
- variant={mode === 'image' ? 'primary' : 'secondary'}
80
- onClick={() => setMode('image')}
81
- >
82
- Image
83
- </Button>
84
+ <div className="max-w-2xl mx-auto px-4 py-6">
85
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-6">AI Test</h1>
86
+
87
+ {/* Mode toggle */}
88
+ <div className="flex gap-1 p-1 bg-gray-100 dark:bg-gray-800 rounded-lg w-fit mb-6">
89
+ {(['text', 'image'] as Mode[]).map((m) => (
90
+ <button
91
+ key={m}
92
+ onClick={() => setMode(m)}
93
+ className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors capitalize ${
94
+ mode === m
95
+ ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm'
96
+ : 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
97
+ }`}
98
+ >
99
+ {m}
100
+ </button>
101
+ ))}
84
102
  </div>
85
103
 
86
- <Card className="mt-4">
104
+ <Card className="mb-4">
87
105
  <div className="space-y-4">
88
106
  <div>
89
- <Text size="sm" weight="medium" className="mb-1">
107
+ <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
90
108
  Model
91
- </Text>
109
+ </label>
92
110
  <select
93
111
  value={model}
94
112
  onChange={(e) =>
@@ -96,12 +114,10 @@ export default function AITestPage(): React.ReactElement {
96
114
  ? setTextModel(e.target.value as TextGenModel)
97
115
  : setImageModel(e.target.value as ImageGenModel)
98
116
  }
99
- className="w-full border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 text-sm bg-white dark:bg-gray-800"
117
+ className="w-full border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-2 text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
100
118
  >
101
119
  {models.map((m) => (
102
- <option key={m} value={m}>
103
- {m}
104
- </option>
120
+ <option key={m} value={m}>{m}</option>
105
121
  ))}
106
122
  </select>
107
123
  </div>
@@ -110,42 +126,61 @@ export default function AITestPage(): React.ReactElement {
110
126
  label="Prompt"
111
127
  value={prompt}
112
128
  onChange={setPrompt}
113
- placeholder={
114
- mode === 'text' ? 'Ask something...' : 'Describe an image...'
115
- }
129
+ placeholder={mode === 'text' ? 'Ask something…' : 'Describe an image…'}
116
130
  />
117
131
 
118
- <Button onClick={handleRun} disabled={loading || !prompt.trim()}>
119
- {loading ? 'Running...' : 'Run'}
132
+ <Button
133
+ onClick={handleRun}
134
+ disabled={loading || !prompt.trim()}
135
+ className="w-full"
136
+ >
137
+ {loading ? 'Running…' : 'Run'}
120
138
  </Button>
121
139
  </div>
122
140
  </Card>
123
141
 
124
- {error && (
125
- <Card className="mt-4 border-red-300">
126
- <Text size="sm" className="text-red-600">
127
- {error}
128
- </Text>
129
- </Card>
142
+ {/* Log panel — always visible once a run starts */}
143
+ {logs.length > 0 && (
144
+ <div className="mb-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-3 font-mono text-xs space-y-1">
145
+ {logs.map((entry, i) => (
146
+ <div
147
+ key={i}
148
+ className={
149
+ entry.kind === 'err'
150
+ ? 'text-red-500'
151
+ : entry.kind === 'ok'
152
+ ? 'text-green-600 dark:text-green-400'
153
+ : 'text-gray-500 dark:text-gray-400'
154
+ }
155
+ >
156
+ {entry.kind === 'err' ? '✗' : entry.kind === 'ok' ? '✓' : '·'} {entry.msg}
157
+ </div>
158
+ ))}
159
+ {loading && (
160
+ <div className="text-blue-500 animate-pulse">· waiting…</div>
161
+ )}
162
+ </div>
130
163
  )}
131
164
 
165
+ {/* Text result */}
132
166
  {result && (
133
- <Card className="mt-4">
134
- <Text size="sm" weight="medium" className="mb-2">
167
+ <Card>
168
+ <p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
135
169
  Result
136
- </Text>
137
- <pre className="text-sm whitespace-pre-wrap break-words bg-gray-50 dark:bg-gray-900 p-3 rounded">
170
+ </p>
171
+ <pre className="text-sm whitespace-pre-wrap break-words text-gray-800 dark:text-gray-200 leading-relaxed">
138
172
  {result}
139
173
  </pre>
140
174
  </Card>
141
175
  )}
142
176
 
177
+ {/* Image result */}
143
178
  {imageUrl && (
144
- <Card className="mt-4">
145
- <Text size="sm" weight="medium" className="mb-2">
179
+ <Card>
180
+ <p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
146
181
  Result
147
- </Text>
148
- <img src={imageUrl} alt="Generated" className="w-full rounded" />
182
+ </p>
183
+ <img src={imageUrl} alt="Generated" className="w-full rounded-lg" />
149
184
  </Card>
150
185
  )}
151
186
  </div>
@@ -1,6 +1,27 @@
1
1
  import React from 'react';
2
- import { Button, Card, LoginPopup, PageLayout, Text, useApp } from 'ugly-app/client';
3
- import { useRouter } from '../router';
2
+ import { Button, Card, PageLayout, Text, useApp } from 'ugly-app/client';
3
+
4
+ function openLogin(): void {
5
+ window.open(
6
+ `https://ugly.bot/oauth?origin=${encodeURIComponent(window.location.origin)}`,
7
+ 'ugly-bot-login',
8
+ `width=480,height=640,left=${Math.round(window.screenX + (window.outerWidth - 480) / 2)},top=${Math.round(window.screenY + (window.outerHeight - 640) / 2)}`,
9
+ );
10
+ function onMessage(event: MessageEvent): void {
11
+ if (event.origin !== 'https://ugly.bot') return;
12
+ const data = event.data as { type?: string; code?: string } | null;
13
+ if (!data || data.type !== 'ugly-bot-oauth' || !data.code) return;
14
+ window.removeEventListener('message', onMessage);
15
+ void fetch('/auth/verify', {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ body: JSON.stringify({ code: data.code }),
19
+ }).then((res) => {
20
+ if (res.ok) window.location.reload();
21
+ });
22
+ }
23
+ window.addEventListener('message', onMessage);
24
+ }
4
25
 
5
26
  function AuthDemoAuthenticated(): React.ReactElement {
6
27
  const app = useApp();
@@ -13,29 +34,21 @@ function AuthDemoAuthenticated(): React.ReactElement {
13
34
  return (
14
35
  <PageLayout
15
36
  header={
16
- <div className="px-6 py-4">
17
- <a href="/" className="text-blue-600">
37
+ <div className="px-6 py-4 border-b border-gray-200 dark:border-gray-800">
38
+ <a href="/" className="text-sm text-gray-500 hover:text-gray-900 dark:hover:text-gray-100 transition-colors">
18
39
  ← Home
19
40
  </a>
20
41
  </div>
21
42
  }
22
43
  >
23
- <div className="max-w-md mx-auto">
24
- <Text size="xl" weight="bold">
25
- Auth Demo
26
- </Text>
27
- <Card className="mt-4">
28
- <Text weight="bold">Logged In</Text>
29
- <pre className="mt-2 text-sm bg-gray-100 dark:bg-gray-900 p-3 rounded overflow-auto">
30
- {JSON.stringify(
31
- {
32
- userId: app.userId,
33
- email: app.user.email,
34
- phone: app.user.phone,
35
- },
36
- null,
37
- 2,
38
- )}
44
+ <div className="max-w-md mx-auto px-4 py-6">
45
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4">Auth Demo</h1>
46
+ <Card>
47
+ <p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
48
+ Logged in
49
+ </p>
50
+ <pre className="text-sm bg-gray-50 dark:bg-gray-900 p-3 rounded-lg overflow-auto text-gray-800 dark:text-gray-200">
51
+ {JSON.stringify({ userId: app.userId, email: app.user.email, phone: app.user.phone }, null, 2)}
39
52
  </pre>
40
53
  <Button variant="secondary" onClick={handleLogout} className="mt-4">
41
54
  Logout
@@ -47,36 +60,21 @@ function AuthDemoAuthenticated(): React.ReactElement {
47
60
  }
48
61
 
49
62
  function AuthDemoUnauthenticated(): React.ReactElement {
50
- const { openPopup } = useRouter();
51
-
52
- function handleLogin(): void {
53
- openPopup(
54
- <LoginPopup
55
- inline
56
-
57
- onSuccess={() => { window.location.reload(); }}
58
- />,
59
- { mode: 'transient' },
60
- );
61
- }
62
-
63
63
  return (
64
64
  <PageLayout
65
65
  header={
66
- <div className="px-6 py-4">
67
- <a href="/" className="text-blue-600">
66
+ <div className="px-6 py-4 border-b border-gray-200 dark:border-gray-800">
67
+ <a href="/" className="text-sm text-gray-500 hover:text-gray-900 dark:hover:text-gray-100 transition-colors">
68
68
  ← Home
69
69
  </a>
70
70
  </div>
71
71
  }
72
72
  >
73
- <div className="max-w-md mx-auto">
74
- <Text size="xl" weight="bold">
75
- Auth Demo
76
- </Text>
77
- <Card className="mt-4">
78
- <Text>You are not logged in.</Text>
79
- <Button onClick={handleLogin} className="mt-4">
73
+ <div className="max-w-md mx-auto px-4 py-6">
74
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-4">Auth Demo</h1>
75
+ <Card>
76
+ <Text className="text-gray-600 dark:text-gray-400">You are not logged in.</Text>
77
+ <Button onClick={openLogin} className="mt-4">
80
78
  Login with ugly.bot
81
79
  </Button>
82
80
  </Card>
@@ -86,8 +84,7 @@ function AuthDemoUnauthenticated(): React.ReactElement {
86
84
  }
87
85
 
88
86
  export default function AuthDemoPage(): React.ReactElement {
89
- const isLoggedIn = !!(window as unknown as { __AUTH_TOKEN__?: string })
90
- .__AUTH_TOKEN__;
87
+ const isLoggedIn = !!(window as unknown as { __AUTH_TOKEN__?: string }).__AUTH_TOKEN__;
91
88
  if (isLoggedIn) return <AuthDemoAuthenticated />;
92
89
  return <AuthDemoUnauthenticated />;
93
90
  }
@@ -1,6 +1,27 @@
1
1
  import React from 'react';
2
- import { Button, Card, LoginPopup, PageLayout, Text, useApp } from 'ugly-app/client';
3
- import { useRouter } from '../router';
2
+ import { Button, Card, PageLayout, Text, useApp } from 'ugly-app/client';
3
+
4
+ function openLogin(): void {
5
+ window.open(
6
+ `https://ugly.bot/oauth?origin=${encodeURIComponent(window.location.origin)}`,
7
+ 'ugly-bot-login',
8
+ `width=480,height=640,left=${Math.round(window.screenX + (window.outerWidth - 480) / 2)},top=${Math.round(window.screenY + (window.outerHeight - 640) / 2)}`,
9
+ );
10
+ function onMessage(event: MessageEvent): void {
11
+ if (event.origin !== 'https://ugly.bot') return;
12
+ const data = event.data as { type?: string; code?: string } | null;
13
+ if (!data || data.type !== 'ugly-bot-oauth' || !data.code) return;
14
+ window.removeEventListener('message', onMessage);
15
+ void fetch('/auth/verify', {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ body: JSON.stringify({ code: data.code }),
19
+ }).then((res) => {
20
+ if (res.ok) window.location.reload();
21
+ });
22
+ }
23
+ window.addEventListener('message', onMessage);
24
+ }
4
25
 
5
26
  function HomePageAuthenticated(): React.ReactElement {
6
27
  const app = useApp();
@@ -13,10 +34,8 @@ function HomePageAuthenticated(): React.ReactElement {
13
34
  return (
14
35
  <PageLayout
15
36
  header={
16
- <div className="flex items-center justify-between px-6 py-4">
17
- <Text size="lg" weight="bold">
18
- My App
19
- </Text>
37
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
38
+ <span className="text-base font-semibold text-gray-900 dark:text-gray-100">My App</span>
20
39
  <Button variant="secondary" onClick={handleLogout}>
21
40
  Logout
22
41
  </Button>
@@ -29,27 +48,12 @@ function HomePageAuthenticated(): React.ReactElement {
29
48
  }
30
49
 
31
50
  function HomePageUnauthenticated(): React.ReactElement {
32
- const { openPopup } = useRouter();
33
-
34
- function handleLogin(): void {
35
- openPopup(
36
- <LoginPopup
37
- inline
38
-
39
- onSuccess={() => { window.location.reload(); }}
40
- />,
41
- { mode: 'transient' },
42
- );
43
- }
44
-
45
51
  return (
46
52
  <PageLayout
47
53
  header={
48
- <div className="flex items-center justify-between px-6 py-4">
49
- <Text size="lg" weight="bold">
50
- My App
51
- </Text>
52
- <Button variant="primary" onClick={handleLogin}>
54
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800">
55
+ <span className="text-base font-semibold text-gray-900 dark:text-gray-100">My App</span>
56
+ <Button variant="primary" onClick={openLogin}>
53
57
  Login
54
58
  </Button>
55
59
  </div>
@@ -60,69 +64,42 @@ function HomePageUnauthenticated(): React.ReactElement {
60
64
  );
61
65
  }
62
66
 
63
- function HomePageBody({
64
- userId,
65
- }: {
66
- userId: string | null;
67
- }): React.ReactElement {
67
+ function HomePageBody({ userId }: { userId: string | null }): React.ReactElement {
68
68
  return (
69
- <div className="max-w-2xl mx-auto">
70
- <Card>
71
- <Text size="xl" weight="bold">
72
- Welcome
69
+ <div className="max-w-2xl mx-auto px-4 py-6">
70
+ <Card className="mb-6">
71
+ <Text size="xl" weight="bold">Welcome</Text>
72
+ <Text className="mt-1 text-gray-500 dark:text-gray-400">
73
+ {userId ? `Logged in as: ${userId}` : 'This app was built with ugly-app.'}
73
74
  </Text>
74
- {userId ? (
75
- <Text className="mt-2 text-gray-600 dark:text-gray-400">
76
- Logged in as: {userId}
77
- </Text>
78
- ) : (
79
- <Text className="mt-2 text-gray-600 dark:text-gray-400">
80
- This app was built with ugly-app.
81
- </Text>
82
- )}
83
75
  </Card>
84
76
 
85
- <div className="mt-6 grid gap-4">
86
- <a href="/auth-demo" className="block">
87
- <Card className="hover:shadow-md transition-shadow cursor-pointer">
88
- <Text weight="medium">Auth Demo →</Text>
89
- <Text size="sm" className="text-gray-500 mt-1">
90
- See login/logout and user info
91
- </Text>
92
- </Card>
93
- </a>
94
- <a href="/user/example-user-id" className="block">
95
- <Card className="hover:shadow-md transition-shadow cursor-pointer">
96
- <Text weight="medium">Path Params Demo →</Text>
97
- <Text size="sm" className="text-gray-500 mt-1">
98
- URL: /user/:userId
99
- </Text>
100
- </Card>
101
- </a>
102
- <a href="/search?q=hello" className="block">
103
- <Card className="hover:shadow-md transition-shadow cursor-pointer">
104
- <Text weight="medium">Query Params Demo →</Text>
105
- <Text size="sm" className="text-gray-500 mt-1">
106
- URL: /search?q=...
107
- </Text>
108
- </Card>
109
- </a>
110
- <a href="/ai-test" className="block">
111
- <Card className="hover:shadow-md transition-shadow cursor-pointer">
112
- <Text weight="medium">AI Model Test →</Text>
113
- <Text size="sm" className="text-gray-500 mt-1">
114
- Test text & image generation models
115
- </Text>
116
- </Card>
117
- </a>
77
+ <div className="grid gap-3">
78
+ {[
79
+ { href: '/auth-demo', label: 'Auth Demo', desc: 'Login, logout, and user info' },
80
+ { href: '/user/example-user-id', label: 'Path Params Demo', desc: 'URL: /user/:userId' },
81
+ { href: '/search?q=hello', label: 'Query Params Demo', desc: 'URL: /search?q=...' },
82
+ { href: '/ai-test', label: 'AI Test', desc: 'Text & image generation models' },
83
+ ].map(({ href, label, desc }) => (
84
+ <a key={href} href={href} className="block group">
85
+ <Card className="transition-shadow group-hover:shadow-md">
86
+ <div className="flex items-center justify-between">
87
+ <div>
88
+ <Text weight="medium">{label}</Text>
89
+ <Text size="sm" className="text-gray-500 mt-0.5">{desc}</Text>
90
+ </div>
91
+ <span className="text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">→</span>
92
+ </div>
93
+ </Card>
94
+ </a>
95
+ ))}
118
96
  </div>
119
97
  </div>
120
98
  );
121
99
  }
122
100
 
123
101
  export default function HomePage(): React.ReactElement {
124
- const isLoggedIn = !!(window as unknown as { __AUTH_TOKEN__?: string })
125
- .__AUTH_TOKEN__;
102
+ const isLoggedIn = !!(window as unknown as { __AUTH_TOKEN__?: string }).__AUTH_TOKEN__;
126
103
  if (isLoggedIn) return <HomePageAuthenticated />;
127
104
  return <HomePageUnauthenticated />;
128
105
  }
@@ -2,3 +2,4 @@ node_modules/
2
2
  dist/
3
3
  .DS_Store
4
4
  .env
5
+ logs/