@agentlang/cli 0.6.6

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.
@@ -0,0 +1,1859 @@
1
+ var __asyncValues = (this && this.__asyncValues) || function (o) {
2
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
3
+ var m = o[Symbol.asyncIterator], i;
4
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
5
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
6
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
7
+ };
8
+ import { query, tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
9
+ import { z } from 'zod';
10
+ import fs from 'fs-extra';
11
+ import path from 'path';
12
+ import chalk from 'chalk';
13
+ import ora from 'ora';
14
+ /* eslint-disable no-console */
15
+ /**
16
+ * Analyzes the existing UI directory to determine if it exists and what's in it
17
+ */
18
+ async function analyzeExistingProject(projectDir) {
19
+ const analysis = {
20
+ exists: false,
21
+ isEmpty: true,
22
+ fileCount: 0,
23
+ hasPackageJson: false,
24
+ hasSourceFiles: false,
25
+ structure: '',
26
+ };
27
+ try {
28
+ // Check if directory exists
29
+ if (!(await fs.pathExists(projectDir))) {
30
+ return analysis;
31
+ }
32
+ analysis.exists = true;
33
+ // Get all files in the directory (excluding node_modules, etc.)
34
+ const files = [];
35
+ async function scanDirectory(dir, prefix = '') {
36
+ const entries = await fs.readdir(dir, { withFileTypes: true });
37
+ for (const entry of entries) {
38
+ // Skip common directories
39
+ if (['node_modules', '.git', 'dist', 'build', '.vscode'].includes(entry.name)) {
40
+ continue;
41
+ }
42
+ const fullPath = path.join(dir, entry.name);
43
+ const relativePath = path.relative(projectDir, fullPath);
44
+ files.push(relativePath);
45
+ if (entry.name === 'package.json') {
46
+ analysis.hasPackageJson = true;
47
+ }
48
+ if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts') || entry.name.endsWith('.jsx')) {
49
+ analysis.hasSourceFiles = true;
50
+ }
51
+ if (entry.isDirectory()) {
52
+ await scanDirectory(fullPath, `${prefix} `);
53
+ }
54
+ }
55
+ }
56
+ await scanDirectory(projectDir);
57
+ analysis.fileCount = files.length;
58
+ analysis.isEmpty = files.length === 0;
59
+ // Generate structure string (show first 20 files)
60
+ if (files.length > 0) {
61
+ const displayFiles = files.slice(0, 20).sort();
62
+ analysis.structure = displayFiles.join('\n');
63
+ if (files.length > 20) {
64
+ analysis.structure += `\n... and ${files.length - 20} more files`;
65
+ }
66
+ }
67
+ return analysis;
68
+ }
69
+ catch (error) {
70
+ console.log(chalk.yellow(` ⚠️ Error analyzing directory: ${error instanceof Error ? error.message : String(error)}`));
71
+ return analysis;
72
+ }
73
+ }
74
+ /* eslint-enable no-console */
75
+ /* eslint-disable no-console */
76
+ export async function generateUI(uiSpec, outputBaseDir, apiKey, shouldPush = false, userMessage) {
77
+ var _a, e_1, _b, _c;
78
+ const spinner = ora('Initializing UI generation...').start();
79
+ const startTime = Date.now();
80
+ try {
81
+ // Create output directory as 'ui' in the specified base directory
82
+ const projectDir = path.join(outputBaseDir, 'ui');
83
+ // Analyze existing project
84
+ spinner.text = 'Analyzing existing project...';
85
+ const projectAnalysis = await analyzeExistingProject(projectDir);
86
+ // Determine the generation mode
87
+ let mode;
88
+ if (userMessage) {
89
+ // User provided a message
90
+ if (projectAnalysis.exists && !projectAnalysis.isEmpty) {
91
+ mode = 'update'; // Update existing project based on user message
92
+ }
93
+ else {
94
+ mode = 'fresh'; // Generate fresh, then apply user message
95
+ }
96
+ }
97
+ else {
98
+ // No user message
99
+ if (projectAnalysis.exists && !projectAnalysis.isEmpty) {
100
+ mode = 'incremental'; // Add missing files based on spec
101
+ }
102
+ else {
103
+ mode = 'fresh'; // Fresh generation
104
+ }
105
+ }
106
+ // Display mode info on separate line
107
+ if (mode === 'fresh') {
108
+ spinner.text = `Creating new project: ${projectDir}`;
109
+ console.log(''); // Empty line for spacing
110
+ // Warn if directory exists with files but we're in fresh mode (shouldn't happen)
111
+ if (projectAnalysis.exists && projectAnalysis.fileCount > 0) {
112
+ console.log(chalk.yellow(` ⚠️ Warning: Directory exists with ${projectAnalysis.fileCount} files`));
113
+ console.log(chalk.yellow(' ⚠️ Switching to incremental mode to preserve existing files'));
114
+ mode = 'incremental';
115
+ }
116
+ else {
117
+ console.log(chalk.cyan(' 📦 Mode: Fresh generation'));
118
+ }
119
+ }
120
+ if (mode === 'incremental') {
121
+ spinner.succeed('Project analyzed');
122
+ console.log(''); // Empty line for spacing
123
+ console.log(chalk.cyan(' 🔄 Mode: Incremental update'));
124
+ console.log(chalk.gray(` 📂 Found existing project with ${projectAnalysis.fileCount} files`));
125
+ console.log(chalk.gray(' 📝 Will add missing files based on spec'));
126
+ spinner.start('Preparing incremental update...');
127
+ }
128
+ else if (mode === 'update') {
129
+ spinner.succeed('Project analyzed');
130
+ console.log(''); // Empty line for spacing
131
+ console.log(chalk.cyan(' ✏️ Mode: User-directed update'));
132
+ console.log(chalk.gray(` 📂 Found existing project with ${projectAnalysis.fileCount} files`));
133
+ console.log(chalk.gray(` 💬 User message: "${userMessage}"`));
134
+ // Check if request is vague
135
+ const vagueKeywords = ['fix', 'make sure', 'properly', 'work', 'working', 'issue', 'problem', 'error'];
136
+ const isVague = vagueKeywords.some(keyword => userMessage === null || userMessage === void 0 ? void 0 : userMessage.toLowerCase().includes(keyword));
137
+ if (isVague) {
138
+ console.log(chalk.yellow(' ⚠️ Note: Request is general - agent will first diagnose issues'));
139
+ }
140
+ spinner.start('Preparing update...');
141
+ }
142
+ await fs.ensureDir(projectDir);
143
+ // Track generated files and operations
144
+ const generatedFiles = [];
145
+ const filesCreated = [];
146
+ const directoriesCreated = [];
147
+ let lastFileCreated = '';
148
+ // Define tools for the agent using the correct API
149
+ const writeFile = tool('write_file', 'Write content to a file in the project directory', z.object({
150
+ file_path: z.string().describe('Relative path from project root (e.g., "src/App.tsx")'),
151
+ content: z.string().describe('The content to write to the file'),
152
+ }).shape, async (args) => {
153
+ const fullPath = path.join(projectDir, args.file_path);
154
+ // Ensure directory exists
155
+ await fs.ensureDir(path.dirname(fullPath));
156
+ // Write the file
157
+ await fs.writeFile(fullPath, args.content, 'utf-8');
158
+ // Track file creation
159
+ generatedFiles.push(args.file_path);
160
+ filesCreated.push(args.file_path);
161
+ lastFileCreated = args.file_path;
162
+ return {
163
+ content: [
164
+ {
165
+ type: 'text',
166
+ text: `Successfully wrote file: ${args.file_path}`,
167
+ },
168
+ ],
169
+ };
170
+ });
171
+ const createDirectory = tool('create_directory', 'Create a directory in the project', z.object({
172
+ dir_path: z.string().describe('Relative directory path from project root'),
173
+ }).shape, async (args) => {
174
+ const fullPath = path.join(projectDir, args.dir_path);
175
+ await fs.ensureDir(fullPath);
176
+ // Track silently
177
+ directoriesCreated.push(args.dir_path);
178
+ return {
179
+ content: [
180
+ {
181
+ type: 'text',
182
+ text: `Successfully created directory: ${args.dir_path}`,
183
+ },
184
+ ],
185
+ };
186
+ });
187
+ const listFiles = tool('list_files', 'List files that have been generated so far', z.object({}).shape, () => Promise.resolve({
188
+ content: [
189
+ {
190
+ type: 'text',
191
+ text: `Generated files:\n${generatedFiles.map(f => `- ${f}`).join('\n')}`,
192
+ },
193
+ ],
194
+ }));
195
+ // Create MCP server with our tools
196
+ const mcpServer = createSdkMcpServer({
197
+ name: 'ui-generator-tools',
198
+ version: '1.0.0',
199
+ tools: [writeFile, createDirectory, listFiles],
200
+ });
201
+ // Create the generation prompt
202
+ const prompt = createGenerationPrompt(uiSpec, projectDir, mode, projectAnalysis, userMessage);
203
+ // Configure SDK with API key
204
+ process.env.ANTHROPIC_API_KEY = apiKey;
205
+ // Change working directory to projectDir so Write tool creates files in the right place
206
+ const originalCwd = process.cwd();
207
+ process.chdir(projectDir);
208
+ // Start clean generation
209
+ console.log('');
210
+ spinner.start(chalk.cyan('Starting agent...'));
211
+ // Query Claude with our MCP server
212
+ const session = query({
213
+ prompt,
214
+ options: {
215
+ mcpServers: {
216
+ 'ui-generator-tools': mcpServer,
217
+ },
218
+ systemPrompt: {
219
+ type: 'preset',
220
+ preset: 'claude_code',
221
+ },
222
+ permissionMode: 'bypassPermissions', // Allow all tool operations without asking
223
+ // Allow all tools - agent can use MCP tools, Write, Read, Edit, Bash, etc.
224
+ },
225
+ });
226
+ // Helper function to count files in real-time
227
+ const countCurrentFiles = async () => {
228
+ try {
229
+ let count = 0;
230
+ const scan = async (dir) => {
231
+ const entries = await fs.readdir(dir, { withFileTypes: true });
232
+ for (const entry of entries) {
233
+ if (['node_modules', '.git', 'dist', 'build'].includes(entry.name))
234
+ continue;
235
+ if (entry.isDirectory()) {
236
+ await scan(path.join(dir, entry.name));
237
+ }
238
+ else {
239
+ count++;
240
+ }
241
+ }
242
+ };
243
+ await scan(projectDir);
244
+ return count;
245
+ }
246
+ catch (_a) {
247
+ return 0;
248
+ }
249
+ };
250
+ // Process messages from the agent
251
+ let toolCallCount = 0;
252
+ let lastProgressUpdate = Date.now();
253
+ let currentThinking = '';
254
+ let currentTool = '';
255
+ let cachedFileCount = 0;
256
+ let lastFileCountUpdate = Date.now();
257
+ let sessionSucceeded = false;
258
+ let sessionError;
259
+ const PROGRESS_UPDATE_INTERVAL = 10000; // Update every 10 seconds
260
+ const FILE_COUNT_UPDATE_INTERVAL = 2000; // Update file count every 2 seconds
261
+ try {
262
+ for (var _d = true, session_1 = __asyncValues(session), session_1_1; session_1_1 = await session_1.next(), _a = session_1_1.done, !_a; _d = true) {
263
+ _c = session_1_1.value;
264
+ _d = false;
265
+ const message = _c;
266
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
267
+ const now = Date.now();
268
+ if (message.type === 'assistant') {
269
+ // Extract text content and tool calls from assistant message
270
+ const content = message.message.content;
271
+ if (Array.isArray(content)) {
272
+ for (const block of content) {
273
+ if (block.type === 'text') {
274
+ // Extract thinking message
275
+ const text = block.text;
276
+ if (text.trim()) {
277
+ // Clean up the text: take first sentence or first 60 chars
278
+ const firstSentence = text.split(/[.!?]\s/)[0];
279
+ const cleaned = firstSentence.replace(/\n/g, ' ').slice(0, 60);
280
+ currentThinking = cleaned;
281
+ }
282
+ }
283
+ else if (block.type === 'tool_use') {
284
+ toolCallCount++;
285
+ // Extract tool name
286
+ const toolName = block.name;
287
+ currentTool = toolName;
288
+ }
289
+ }
290
+ }
291
+ // Update file count periodically (not on every message to avoid slowdown)
292
+ if (now - lastFileCountUpdate > FILE_COUNT_UPDATE_INTERVAL) {
293
+ cachedFileCount = await countCurrentFiles();
294
+ lastFileCountUpdate = now;
295
+ }
296
+ // Update spinner with clean progress info
297
+ let spinnerText = chalk.cyan(`Generating... ${cachedFileCount} files • ${elapsed}s`);
298
+ // Show current tool being used
299
+ if (currentTool) {
300
+ spinnerText += chalk.blue(` • Tool: ${currentTool}`);
301
+ }
302
+ // Show current thinking or last file created
303
+ if (currentThinking) {
304
+ spinnerText += chalk.gray(` • ${currentThinking}${currentThinking.length >= 60 ? '...' : ''}`);
305
+ }
306
+ else if (lastFileCreated) {
307
+ spinnerText += chalk.gray(` • ${lastFileCreated}`);
308
+ }
309
+ spinner.text = spinnerText;
310
+ // Show periodic progress updates (every 10 seconds)
311
+ if (now - lastProgressUpdate > PROGRESS_UPDATE_INTERVAL && cachedFileCount > 0) {
312
+ spinner.stop();
313
+ console.log(chalk.gray(` 📊 Progress: ${cachedFileCount} files created, ${toolCallCount} operations, ${elapsed}s elapsed`));
314
+ spinner.start(spinnerText);
315
+ lastProgressUpdate = now;
316
+ }
317
+ }
318
+ else if (message.type === 'result') {
319
+ // Final result
320
+ spinner.stop();
321
+ const finalElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
322
+ if (message.subtype === 'success') {
323
+ sessionSucceeded = true;
324
+ console.log(chalk.green('\n✅ Agent completed successfully'));
325
+ console.log(chalk.gray(` ⏱ Time: ${finalElapsed}s`));
326
+ console.log(chalk.gray(` 🔄 Turns: ${message.num_turns}`));
327
+ console.log(chalk.gray(` 🔧 Operations: ${toolCallCount}`));
328
+ console.log(chalk.gray(` 💰 Cost: $${message.total_cost_usd.toFixed(4)}`));
329
+ }
330
+ else {
331
+ sessionSucceeded = false;
332
+ sessionError = message.subtype;
333
+ console.log(chalk.yellow(`\n⚠️ Agent finished with status: ${message.subtype}`));
334
+ console.log(chalk.gray(` ⏱ Time: ${finalElapsed}s`));
335
+ // Check if agent did no work
336
+ if (toolCallCount === 0) {
337
+ console.log(chalk.yellow('\n⚠️ Warning: Agent completed but performed no operations.'));
338
+ console.log(chalk.gray(' This might indicate:'));
339
+ console.log(chalk.gray(' • The task description was unclear or too vague'));
340
+ console.log(chalk.gray(' • The agent thought no changes were needed'));
341
+ console.log(chalk.gray(' • An error occurred before tools could be used'));
342
+ }
343
+ }
344
+ }
345
+ }
346
+ }
347
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
348
+ finally {
349
+ try {
350
+ if (!_d && !_a && (_b = session_1.return)) await _b.call(session_1);
351
+ }
352
+ finally { if (e_1) throw e_1.error; }
353
+ }
354
+ // Check if session failed
355
+ if (!sessionSucceeded) {
356
+ throw new Error(`Agent session failed with status: ${sessionError || 'unknown'}. ` +
357
+ `The agent completed ${toolCallCount} operations before stopping.`);
358
+ }
359
+ // Restore original working directory
360
+ process.chdir(originalCwd);
361
+ // Count actual files generated in the ui/ directory
362
+ const actualFileCount = await countGeneratedFiles(projectDir);
363
+ console.log(chalk.green('\n✅ Generation complete!'));
364
+ console.log(chalk.green('\n📊 Summary:'));
365
+ console.log(chalk.gray(' • Files created: ') + chalk.white(actualFileCount));
366
+ console.log(chalk.gray(' • Time elapsed: ') + chalk.white(`${((Date.now() - startTime) / 1000).toFixed(1)}s`));
367
+ console.log(chalk.gray(' • Output location: ') + chalk.white(projectDir));
368
+ // Show sample files created (first 8)
369
+ if (filesCreated.length > 0) {
370
+ console.log(chalk.cyan('\n📄 Sample files created:'));
371
+ const sampleFiles = filesCreated.slice(0, 8);
372
+ sampleFiles.forEach(file => {
373
+ console.log(chalk.gray(` • ${file}`));
374
+ });
375
+ if (filesCreated.length > 8) {
376
+ console.log(chalk.gray(` ... and ${filesCreated.length - 8} more files`));
377
+ }
378
+ }
379
+ // Git operations if requested
380
+ if (shouldPush) {
381
+ console.log(''); // Add newline
382
+ await performGitOperations(projectDir, outputBaseDir, uiSpec.appInfo.title);
383
+ }
384
+ console.log(chalk.cyan('\n📝 Next steps:'));
385
+ console.log(chalk.white(` cd ${projectDir}`));
386
+ console.log(chalk.white(' npm install'));
387
+ console.log(chalk.white(' npm run build # Verify build succeeds'));
388
+ console.log(chalk.white(' npm run dev # Start development server'));
389
+ }
390
+ catch (error) {
391
+ spinner.fail('UI generation failed');
392
+ throw error;
393
+ }
394
+ }
395
+ /* eslint-enable no-console */
396
+ /**
397
+ * Count all files (recursively) in the generated project directory
398
+ */
399
+ async function countGeneratedFiles(projectDir) {
400
+ let count = 0;
401
+ async function countInDirectory(dir) {
402
+ try {
403
+ const entries = await fs.readdir(dir, { withFileTypes: true });
404
+ for (const entry of entries) {
405
+ const fullPath = path.join(dir, entry.name);
406
+ if (entry.isDirectory()) {
407
+ // Skip node_modules and other common directories
408
+ if (!['node_modules', '.git', 'dist', 'build', '.vscode'].includes(entry.name)) {
409
+ await countInDirectory(fullPath);
410
+ }
411
+ }
412
+ else if (entry.isFile()) {
413
+ count++;
414
+ }
415
+ }
416
+ }
417
+ catch (_a) {
418
+ // Ignore errors
419
+ }
420
+ }
421
+ await countInDirectory(projectDir);
422
+ return count;
423
+ }
424
+ /* eslint-disable no-console */
425
+ async function performGitOperations(projectDir, repoRoot, appTitle) {
426
+ const { exec } = await import('child_process');
427
+ const { promisify } = await import('util');
428
+ const execAsync = promisify(exec);
429
+ const gitSpinner = ora('Preparing git operations...').start();
430
+ try {
431
+ // Save original directory
432
+ const originalCwd = process.cwd();
433
+ // Change to repo root directory
434
+ process.chdir(repoRoot);
435
+ gitSpinner.text = 'Checking git status...';
436
+ // Check if it's a git repository
437
+ try {
438
+ await execAsync('git rev-parse --git-dir');
439
+ }
440
+ catch (_a) {
441
+ gitSpinner.fail('Not a git repository');
442
+ console.log(chalk.yellow(' ⚠️ Skipping git operations - not a git repository'));
443
+ process.chdir(originalCwd);
444
+ return;
445
+ }
446
+ // Check for uncommitted changes in ui/
447
+ gitSpinner.text = 'Checking for changes...';
448
+ const { stdout: statusOutput } = await execAsync('git status --porcelain ui/');
449
+ if (!statusOutput.trim()) {
450
+ gitSpinner.info('No changes to commit in ui/');
451
+ process.chdir(originalCwd);
452
+ return;
453
+ }
454
+ // Add all files in the ui directory
455
+ gitSpinner.text = 'Adding files to git...';
456
+ await execAsync('git add ui/');
457
+ gitSpinner.succeed('Added ui/ to git');
458
+ // Commit changes
459
+ gitSpinner.start('Committing changes...');
460
+ const commitMessage = `Add generated UI for ${appTitle}\n\n🤖 Generated with Agentlang CLI`;
461
+ await execAsync(`git commit -m "${commitMessage}"`);
462
+ gitSpinner.succeed('Committed changes');
463
+ // Check if remote exists
464
+ gitSpinner.start('Checking remote...');
465
+ try {
466
+ await execAsync('git remote get-url origin');
467
+ }
468
+ catch (_b) {
469
+ gitSpinner.warn('No remote repository configured');
470
+ console.log(chalk.yellow(' ⚠️ Skipping push - no remote configured'));
471
+ process.chdir(originalCwd);
472
+ return;
473
+ }
474
+ // Get current branch
475
+ const { stdout: branchOutput } = await execAsync('git branch --show-current');
476
+ const currentBranch = branchOutput.trim();
477
+ // Push to remote
478
+ gitSpinner.text = `Pushing to remote (${currentBranch})...`;
479
+ await execAsync(`git push origin ${currentBranch}`);
480
+ gitSpinner.succeed(`Pushed to remote (${currentBranch})`);
481
+ console.log(chalk.green('\n✅ Successfully committed and pushed UI changes'));
482
+ // Restore original directory
483
+ process.chdir(originalCwd);
484
+ }
485
+ catch (error) {
486
+ gitSpinner.fail('Git operations failed');
487
+ console.log(chalk.yellow('\n⚠️ Warning: Git operations encountered an error'));
488
+ if (error instanceof Error) {
489
+ // Extract the meaningful part of the error message
490
+ const errorMessage = error.message.split('\n')[0];
491
+ console.log(chalk.gray(` ${errorMessage}`));
492
+ }
493
+ console.log(chalk.yellow(' 💡 You may need to commit and push manually:'));
494
+ console.log(chalk.gray(' git add ui/'));
495
+ console.log(chalk.gray(` git commit -m "Add generated UI for ${appTitle}"`));
496
+ console.log(chalk.gray(' git push'));
497
+ }
498
+ }
499
+ /* eslint-enable no-console */
500
+ function createGenerationPrompt(uiSpec, projectDir, mode, projectAnalysis, userMessage) {
501
+ // Mode-specific instructions
502
+ let modeInstructions = '';
503
+ if (mode === 'fresh') {
504
+ modeInstructions = `
505
+ # MODE: Fresh Generation
506
+
507
+ You are creating a NEW React + TypeScript + Vite application from scratch.
508
+
509
+ ⚠️ CRITICAL: You MUST generate files. Do not skip file generation thinking the task is unclear.
510
+
511
+ Your task:
512
+ 1. Generate ALL required files for a complete working application
513
+ 2. Follow the template structure exactly as specified below
514
+ 3. Use the Write tool to create each file with its full content
515
+ 4. Do not stop until all files are created
516
+
517
+ ${userMessage ? `\n# ADDITIONAL REQUIREMENT\n\nAfter generating the complete base application, also implement this:\n${userMessage}` : ''}
518
+
519
+ IMPORTANT: Start creating files immediately. This is a fresh project with no existing files.`;
520
+ }
521
+ else if (mode === 'incremental') {
522
+ modeInstructions = `
523
+ # MODE: Incremental Update
524
+
525
+ An existing UI project was found at: ${projectDir}
526
+ Existing project has ${projectAnalysis.fileCount} files.
527
+
528
+ ⚠️ CRITICAL: You MUST take action. Do not complete without making changes.
529
+
530
+ Your task:
531
+ 1. Use Read tool to examine the existing project structure (start with package.json)
532
+ 2. Compare it with the UI spec below
533
+ 3. Identify MISSING files or features based on the spec
534
+ 4. Add ONLY the missing files/features using Write tool
535
+ 5. If files already exist and are complete, DO NOT regenerate them
536
+ 6. Update existing files ONLY if they're missing required features from the spec (use Edit tool)
537
+
538
+ Existing files (showing first 20):
539
+ \`\`\`
540
+ ${projectAnalysis.structure}
541
+ \`\`\`
542
+
543
+ IMPORTANT: Read the existing files first, then make necessary additions/updates. Do not complete without doing any work.`;
544
+ }
545
+ else if (mode === 'update') {
546
+ // Check if user message is vague/generic
547
+ const vagueKeywords = ['fix', 'make sure', 'properly', 'work', 'working', 'issue', 'problem', 'error'];
548
+ const isVagueRequest = vagueKeywords.some(keyword => userMessage === null || userMessage === void 0 ? void 0 : userMessage.toLowerCase().includes(keyword));
549
+ modeInstructions = `
550
+ # MODE: User-Directed Update
551
+
552
+ An existing UI project was found at: ${projectDir}
553
+ Existing project has ${projectAnalysis.fileCount} files.
554
+
555
+ # USER REQUEST:
556
+ ${userMessage}
557
+
558
+ Your task:
559
+ 1. Use Read tool to examine relevant existing files (start with package.json, key config files, and entry points)
560
+ 2. Understand the user's request: "${userMessage}"
561
+ ${isVagueRequest
562
+ ? `
563
+ 3. IMPORTANT: The user's request is general/vague. First DIAGNOSE issues:
564
+ - Read package.json to check dependencies and scripts
565
+ - Read configuration files (vite.config.ts, tsconfig.json, .env)
566
+ - Read main entry point (src/main.tsx, src/App.tsx)
567
+ - Look for common issues: missing dependencies, incorrect imports, configuration errors
568
+ - Check if the project structure matches the UI spec
569
+ - Identify any incomplete features or broken components
570
+ 4. Once you've identified specific issues, FIX them:
571
+ - Add missing dependencies to package.json
572
+ - Fix incorrect imports or paths
573
+ - Complete incomplete features based on the UI spec
574
+ - Fix configuration issues
575
+ - Ensure all required files exist and are properly implemented`
576
+ : `
577
+ 3. Make TARGETED changes to implement the request
578
+ 4. Modify existing files as needed using Edit tool
579
+ 5. Add new files only if necessary
580
+ 6. Test that your changes work with the existing codebase`}
581
+
582
+ Existing files (showing first 20):
583
+ \`\`\`
584
+ ${projectAnalysis.structure}
585
+ \`\`\`
586
+
587
+ ${isVagueRequest ? 'Start by reading and diagnosing, then fix the issues you find.' : "Focus on the user's specific request. Be surgical - only change what's necessary."}`;
588
+ }
589
+ return `
590
+ You are a UI generation agent for creating React + TypeScript + Tailwind applications from AgentLang specs.
591
+
592
+ ${modeInstructions}
593
+
594
+ # UI SPEC
595
+ \`\`\`json
596
+ ${JSON.stringify(uiSpec, null, 2)}
597
+ \`\`\`
598
+
599
+ # GOAL
600
+ Generate a complete, working, polished React admin UI that looks deployment-ready.
601
+
602
+ # APPLICATION STRUCTURE
603
+
604
+ **Navigation (Sidebar):**
605
+ - **Home Button** → Dashboard page
606
+ - **Entities Section** → List of all entities
607
+ * Click entity → Entity list page
608
+ * Click instance → Entity detail page (shows relationships if any)
609
+ - **Workflows Section** → List of all workflows
610
+ * Click workflow → Opens workflow dialog
611
+
612
+ **Dashboard:**
613
+ - Stat cards (entity counts)
614
+ - Quick Actions (workflow cards)
615
+ - Recent activity/status
616
+
617
+ **Entity Detail Page (when entity has relationships):**
618
+ - Entity details at top
619
+ - Embedded relationship tables below
620
+ - Each relationship table has Search + Create button
621
+
622
+ **Chatbot Bubble (floating, bottom-right):**
623
+ - Click to open chat panel
624
+ - Agent dropdown INSIDE chat panel header
625
+ - Chat interface with message history
626
+
627
+ # PHASE 1: GENERATE ALL FILES
628
+
629
+ ## Step 1: Project Setup
630
+ 1. Create package.json with dependencies:
631
+ - react, react-dom, react-router-dom
632
+ - typescript, @types/react, @types/react-dom
633
+ - vite, @vitejs/plugin-react
634
+ - tailwindcss, postcss, autoprefixer
635
+ - @iconify/react, formik, yup
636
+
637
+ 2. Create configuration files:
638
+ - tsconfig.json (strict mode)
639
+ - vite.config.ts (standard React setup)
640
+ - tailwind.config.js (with content paths)
641
+ - postcss.config.js
642
+ - index.html
643
+
644
+ 3. Create **.env** file:
645
+ \`\`\`env
646
+ # Backend API URL (AgentLang server)
647
+ VITE_BACKEND_URL=http://localhost:8080/
648
+
649
+ # Mock data mode - set to true by default so app works without backend
650
+ VITE_USE_MOCK_DATA=true
651
+
652
+ # Agent chat is backend-powered - no LLM keys in frontend!
653
+ # Keys are configured in backend only
654
+ \`\`\`
655
+
656
+ Also create **.env.example** with same structure (without sensitive values).
657
+
658
+ ## Step 2: API Client Setup
659
+
660
+ Create **src/api/client.ts**:
661
+ \`\`\`typescript
662
+ const API_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080';
663
+ const USE_MOCK = import.meta.env.VITE_USE_MOCK_DATA === 'true';
664
+
665
+ export const apiClient = {
666
+ baseURL: API_URL,
667
+ useMock: USE_MOCK
668
+ };
669
+ \`\`\`
670
+
671
+ Create **src/api/endpoints.ts** - AgentLang API patterns:
672
+ \`\`\`typescript
673
+ // Authentication endpoints (special)
674
+ export const authEndpoints = {
675
+ login: '/agentlang.auth/login', // POST { email, password }
676
+ signUp: '/agentlang.auth/signUp', // POST { email, password, name }
677
+ forgotPassword: '/agentlang.auth/forgotPassword' // POST { email }
678
+ };
679
+
680
+ // Entity CRUD endpoints (dynamic)
681
+ export const entityEndpoints = {
682
+ list: (model: string, entity: string) => \`/\${model}/\${entity}\`, // GET
683
+ get: (model: string, entity: string, id: string) => \`/\${model}/\${entity}/\${id}\`, // GET
684
+ create: (model: string, entity: string) => \`/\${model}/\${entity}\`, // POST
685
+ update: (model: string, entity: string, id: string) => \`/\${model}/\${entity}/\${id}\`, // PUT
686
+ delete: (model: string, entity: string, id: string) => \`/\${model}/\${entity}/\${id}\` // DELETE
687
+ };
688
+
689
+ // Workflow endpoints (dynamic)
690
+ export const workflowEndpoints = {
691
+ execute: (model: string, workflow: string) => \`/\${model}/\${workflow}\` // POST
692
+ };
693
+
694
+ // Agent chat endpoint
695
+ export const agentEndpoints = {
696
+ chat: (agentName: string) => \`/agents/\${agentName}/chat\` // POST (backend handles LLM)
697
+ };
698
+ \`\`\`
699
+
700
+ **Examples:**
701
+ - Create customer: \`POST /CarDealership/Customer\` with \`{ name: "John", contactDetails: "john@email.com" }\`
702
+ - Get all dealers: \`GET /CarDealership/Dealer\`
703
+ - Execute workflow: \`POST /CarDealership/ProcessSale\` with workflow inputs
704
+ - Login: \`POST /agentlang.auth/login\` with \`{ email: "...", password: "..." }\`
705
+
706
+ ## Step 3: Mock Data Layer
707
+
708
+ Create **src/data/mockData.ts**:
709
+ \`\`\`typescript
710
+ // Generate mock data for ALL entities in the spec
711
+ export const mockData = {
712
+ 'ModelName/EntityName': [
713
+ { id: '1', field1: 'value', field2: 'value', ... },
714
+ { id: '2', ...},
715
+ // At least 3-5 records per entity
716
+ ],
717
+ // ... all other entities
718
+ };
719
+
720
+ // Mock users for authentication (CRITICAL for login/signup to work!)
721
+ export const mockUsers = [
722
+ {
723
+ id: '1',
724
+ email: 'admin@example.com',
725
+ password: 'admin123',
726
+ name: 'Admin User',
727
+ role: 'admin',
728
+ token: 'mock-token-admin-123'
729
+ },
730
+ {
731
+ id: '2',
732
+ email: 'user@example.com',
733
+ password: 'user123',
734
+ name: 'Demo User',
735
+ role: 'user',
736
+ token: 'mock-token-user-456'
737
+ }
738
+ ];
739
+
740
+ // Mock API following AgentLang patterns
741
+ export const mockApi = {
742
+ // Authentication endpoints
743
+ async login(email: string, password: string) {
744
+ await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network
745
+ const user = mockUsers.find(u => u.email === email && u.password === password);
746
+ if (!user) return { error: 'Invalid credentials', status: 'error' };
747
+ return {
748
+ data: {
749
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
750
+ token: user.token
751
+ },
752
+ status: 'success'
753
+ };
754
+ },
755
+
756
+ async signUp(email: string, password: string, name: string) {
757
+ await new Promise(resolve => setTimeout(resolve, 500));
758
+ // Check if user already exists
759
+ if (mockUsers.find(u => u.email === email)) {
760
+ return { error: 'User already exists', status: 'error' };
761
+ }
762
+ const newUser = {
763
+ id: Date.now().toString(),
764
+ email,
765
+ password,
766
+ name,
767
+ role: 'user',
768
+ token: \`mock-token-\${Date.now()}\`
769
+ };
770
+ mockUsers.push(newUser);
771
+ return {
772
+ data: {
773
+ user: { id: newUser.id, email: newUser.email, name: newUser.name, role: newUser.role },
774
+ token: newUser.token
775
+ },
776
+ status: 'success'
777
+ };
778
+ },
779
+
780
+ async forgotPassword(email: string) {
781
+ await new Promise(resolve => setTimeout(resolve, 500));
782
+ const user = mockUsers.find(u => u.email === email);
783
+ if (!user) return { error: 'User not found', status: 'error' };
784
+ return { data: { message: 'Password reset email sent' }, status: 'success' };
785
+ },
786
+
787
+ // Entity CRUD endpoints
788
+ async list(model: string, entity: string) {
789
+ const key = \`\${model}/\${entity}\`;
790
+ await new Promise(resolve => setTimeout(resolve, 300));
791
+ return { data: mockData[key] || [], status: 'success' };
792
+ },
793
+
794
+ async get(model: string, entity: string, id: string) {
795
+ const key = \`\${model}/\${entity}\`;
796
+ await new Promise(resolve => setTimeout(resolve, 200));
797
+ const item = (mockData[key] || []).find(i => i.id === id);
798
+ if (!item) return { error: 'Not found', status: 'error' };
799
+ return { data: item, status: 'success' };
800
+ },
801
+
802
+ async create(model: string, entity: string, data: any) {
803
+ const key = \`\${model}/\${entity}\`;
804
+ await new Promise(resolve => setTimeout(resolve, 400));
805
+ const newItem = { id: Date.now().toString(), ...data };
806
+ if (!mockData[key]) mockData[key] = [];
807
+ mockData[key].push(newItem);
808
+ return { data: newItem, status: 'success' };
809
+ },
810
+
811
+ async update(model: string, entity: string, id: string, data: any) {
812
+ const key = \`\${model}/\${entity}\`;
813
+ await new Promise(resolve => setTimeout(resolve, 400));
814
+ const index = (mockData[key] || []).findIndex(i => i.id === id);
815
+ if (index === -1) return { error: 'Not found', status: 'error' };
816
+ mockData[key][index] = { ...mockData[key][index], ...data };
817
+ return { data: mockData[key][index], status: 'success' };
818
+ },
819
+
820
+ async delete(model: string, entity: string, id: string) {
821
+ const key = \`\${model}/\${entity}\`;
822
+ await new Promise(resolve => setTimeout(resolve, 300));
823
+ mockData[key] = (mockData[key] || []).filter(i => i.id !== id);
824
+ return { status: 'success' };
825
+ },
826
+
827
+ // Workflow execution endpoint
828
+ async executeWorkflow(model: string, workflowName: string, inputs: any) {
829
+ await new Promise(resolve => setTimeout(resolve, 500));
830
+ console.log(\`Executing workflow: \${model}/\${workflowName}\`, inputs);
831
+ // Mock successful execution
832
+ return { status: 'success', data: { message: 'Workflow executed successfully' } };
833
+ }
834
+ };
835
+ \`\`\`
836
+
837
+ ## Step 4: Core Utilities
838
+
839
+ Create **src/data/uiSpec.ts** - export the UI spec
840
+
841
+ Create **src/utils/specParser.ts**:
842
+ \`\`\`typescript
843
+ export function getFormSpec(entityName: string) {
844
+ return spec[\`\${entityName}.ui.form\`];
845
+ }
846
+ export function getDashboardSpec(entityName: string) {
847
+ return spec[\`\${entityName}.ui.dashboard\`];
848
+ }
849
+ export function getInstanceSpec(entityName: string) {
850
+ return spec[\`\${entityName}.ui.instance\`];
851
+ }
852
+ export function getChildRelationships(parentEntity: string) {
853
+ return spec.relationships?.filter(r => r.parent === parentEntity) || [];
854
+ }
855
+ \`\`\`
856
+
857
+ Create **src/utils/workflowParser.ts**:
858
+ \`\`\`typescript
859
+ export function getWorkflows(spec: any) {
860
+ return (spec.workflows || []).map(name => ({
861
+ name,
862
+ displayName: spec[name].displayName,
863
+ description: spec[name].description,
864
+ icon: spec[name].icon,
865
+ ui: spec[\`\${name}.ui\`],
866
+ inputs: spec[\`\${name}.inputs\`] || {}
867
+ }));
868
+ }
869
+ \`\`\`
870
+
871
+ Create **src/hooks/useEntityData.ts** - CRUD hook with defensive patterns:
872
+ \`\`\`typescript
873
+ export function useEntityData(model: string, entity: string) {
874
+ const [data, setData] = useState<any[]>([]);
875
+ const [loading, setLoading] = useState(false);
876
+ const [error, setError] = useState<string | null>(null);
877
+
878
+ const fetchData = async () => {
879
+ setLoading(true);
880
+ try {
881
+ const useMock = import.meta.env.VITE_USE_MOCK_DATA === 'true';
882
+ const result = useMock
883
+ ? await mockApi.list(model, entity)
884
+ : await fetch(\`\${API_URL}/\${model}/\${entity}\`).then(r => r.json());
885
+
886
+ // DEFENSIVE: Always ensure array
887
+ setData(Array.isArray(result.data) ? result.data : []);
888
+ } catch (err) {
889
+ setError(err.message);
890
+ setData([]); // DEFENSIVE: Empty array on error
891
+ } finally {
892
+ setLoading(false);
893
+ }
894
+ };
895
+
896
+ return {
897
+ data: Array.isArray(data) ? data : [], // DEFENSIVE: Always return array
898
+ loading,
899
+ error,
900
+ fetchData
901
+ };
902
+ }
903
+ \`\`\`
904
+
905
+ ## Step 5: Dynamic Components
906
+
907
+ Create **src/components/dynamic/DynamicTable.tsx** - Reusable table with row actions:
908
+
909
+ **CRITICAL - Must include Actions column:**
910
+ \`\`\`typescript
911
+ export function DynamicTable({ data, spec, onRowClick, onCreateClick, onEdit, onDelete, showCreateButton = true }: Props) {
912
+ // DEFENSIVE: Always validate array first
913
+ const safeData = Array.isArray(data) ? data : [];
914
+
915
+ const [searchTerm, setSearchTerm] = useState('');
916
+ const [currentPage, setCurrentPage] = useState(1);
917
+ const [pageSize, setPageSize] = useState(25);
918
+
919
+ // DEFENSIVE: Filter safely
920
+ const filteredData = useMemo(() => {
921
+ if (!searchTerm) return safeData;
922
+ return safeData.filter(item =>
923
+ Object.values(item || {}).some(v =>
924
+ String(v || '').toLowerCase().includes(searchTerm.toLowerCase())
925
+ )
926
+ );
927
+ }, [safeData, searchTerm]);
928
+
929
+ // DEFENSIVE: Always check before spreading
930
+ const sortedData = useMemo(() => {
931
+ if (!Array.isArray(filteredData)) return [];
932
+ return [...filteredData];
933
+ }, [filteredData]);
934
+
935
+ const paginatedData = useMemo(() => {
936
+ if (!Array.isArray(sortedData)) return [];
937
+ const start = (currentPage - 1) * pageSize;
938
+ return sortedData.slice(start, start + pageSize);
939
+ }, [sortedData, currentPage, pageSize]);
940
+
941
+ if (safeData.length === 0) {
942
+ return (
943
+ <div className="text-center py-12 bg-gray-50 rounded-lg">
944
+ <Icon icon="mdi:database-off" className="text-4xl text-gray-400 mb-2" />
945
+ <p className="text-gray-600">No data available</p>
946
+ </div>
947
+ );
948
+ }
949
+
950
+ return (
951
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200">
952
+ {/* Header with Search + Create Button TOGETHER on right */}
953
+ <div className="flex justify-between items-center p-4 border-b border-gray-200">
954
+ <h2 className="text-xl font-semibold text-gray-900">{spec.title}</h2>
955
+
956
+ {/* Search and Create together on the right */}
957
+ <div className="flex gap-3 items-center">
958
+ <div className="relative">
959
+ <Icon icon="mdi:magnify" className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
960
+ <input
961
+ type="text"
962
+ placeholder="Search..."
963
+ value={searchTerm}
964
+ onChange={(e) => setSearchTerm(e.target.value)}
965
+ className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg w-64 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
966
+ />
967
+ </div>
968
+ {showCreateButton && (
969
+ <button
970
+ onClick={onCreateClick}
971
+ className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors shadow-sm"
972
+ >
973
+ <Icon icon="mdi:plus" />
974
+ <span>Create</span>
975
+ </button>
976
+ )}
977
+ </div>
978
+ </div>
979
+
980
+ {/* Table */}
981
+ <div className="overflow-x-auto">
982
+ <table className="w-full">
983
+ <thead className="bg-gray-50 border-b border-gray-200">
984
+ <tr>
985
+ {spec.columns.map(col => (
986
+ <th key={col.key} className="px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider">
987
+ {col.label}
988
+ </th>
989
+ ))}
990
+ {/* CRITICAL: Actions column header */}
991
+ <th className="px-6 py-3 text-right text-xs font-semibold text-gray-700 uppercase tracking-wider">
992
+ Actions
993
+ </th>
994
+ </tr>
995
+ </thead>
996
+ <tbody className="divide-y divide-gray-200">
997
+ {paginatedData.map((row, idx) => (
998
+ <tr key={row.id || idx} className="hover:bg-gray-50 transition-colors">
999
+ {spec.columns.map(col => (
1000
+ <td
1001
+ key={col.key}
1002
+ onClick={() => onRowClick(row)}
1003
+ className="px-6 py-4 text-sm text-gray-900 cursor-pointer"
1004
+ >
1005
+ {row[col.key]}
1006
+ </td>
1007
+ ))}
1008
+ {/* CRITICAL: Actions column with Edit and Delete buttons */}
1009
+ <td className="px-6 py-4 text-right whitespace-nowrap">
1010
+ <div className="flex justify-end gap-2">
1011
+ <button
1012
+ onClick={(e) => {
1013
+ e.stopPropagation();
1014
+ onEdit(row);
1015
+ }}
1016
+ className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
1017
+ title="Edit"
1018
+ >
1019
+ <Icon icon="mdi:pencil" className="text-lg" />
1020
+ </button>
1021
+ <button
1022
+ onClick={(e) => {
1023
+ e.stopPropagation();
1024
+ onDelete(row);
1025
+ }}
1026
+ className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
1027
+ title="Delete"
1028
+ >
1029
+ <Icon icon="mdi:delete" className="text-lg" />
1030
+ </button>
1031
+ </div>
1032
+ </td>
1033
+ </tr>
1034
+ ))}
1035
+ </tbody>
1036
+ </table>
1037
+ </div>
1038
+
1039
+ {/* Pagination */}
1040
+ <div className="flex justify-between items-center px-6 py-4 border-t border-gray-200 bg-gray-50">
1041
+ <div className="text-sm text-gray-700">
1042
+ Showing <span className="font-medium">{Math.min((currentPage - 1) * pageSize + 1, sortedData.length)}</span> to{' '}
1043
+ <span className="font-medium">{Math.min(currentPage * pageSize, sortedData.length)}</span> of{' '}
1044
+ <span className="font-medium">{sortedData.length}</span> results
1045
+ </div>
1046
+ <div className="flex gap-2">
1047
+ <button
1048
+ onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
1049
+ disabled={currentPage === 1}
1050
+ className="px-3 py-1 border border-gray-300 rounded-md hover:bg-white disabled:opacity-50 disabled:cursor-not-allowed"
1051
+ >
1052
+ Previous
1053
+ </button>
1054
+ <button
1055
+ onClick={() => setCurrentPage(p => p + 1)}
1056
+ disabled={currentPage * pageSize >= sortedData.length}
1057
+ className="px-3 py-1 border border-gray-300 rounded-md hover:bg-white disabled:opacity-50 disabled:cursor-not-allowed"
1058
+ >
1059
+ Next
1060
+ </button>
1061
+ </div>
1062
+ </div>
1063
+ </div>
1064
+ );
1065
+ }
1066
+ \`\`\`
1067
+
1068
+ **CRITICAL Requirements:**
1069
+ 1. **Header Layout**: Title on left, Search + Create button on right (TOGETHER)
1070
+ 2. **Actions Column**: MUST have "Actions" column at the end with Edit and Delete icon buttons
1071
+ 3. **Edit Button**: Blue pencil icon (mdi:pencil), opens edit form dialog
1072
+ 4. **Delete Button**: Red trash icon (mdi:delete), shows confirmation dialog then deletes
1073
+ 5. **Stop Propagation**: Action buttons must call \`e.stopPropagation()\` to prevent row click
1074
+ 6. **Search Input**: Has search icon, placeholder "Search...", 256px width
1075
+ 7. **Empty State**: Show nice empty state with icon and message
1076
+ 8. **Pagination**: Show count and prev/next buttons
1077
+ 9. **Defensive**: Always check \`Array.isArray(data)\` before operations
1078
+
1079
+ Create **src/components/dynamic/DynamicForm.tsx** - Standard form with Formik
1080
+ Create **src/components/dynamic/DynamicCard.tsx** - Card layout for entity details
1081
+ Create **src/components/dynamic/ComponentResolver.tsx** - Resolves spec references to components
1082
+
1083
+ ## Step 6: Entity Components
1084
+
1085
+ Create **src/components/entity/EntityList.tsx** - Uses DynamicTable to show entities
1086
+
1087
+ Create **src/components/entity/EntityDetail.tsx** - Detail page with embedded relationship tables:
1088
+
1089
+ **CRITICAL - Structure:**
1090
+ \`\`\`typescript
1091
+ export function EntityDetail() {
1092
+ const { model, entity, id } = useParams();
1093
+ const { data: item, loading } = useEntityDetail(model, entity, id);
1094
+
1095
+ // Get child relationships for this entity
1096
+ const relationships = getChildRelationships(\`\${model}/\${entity}\`);
1097
+
1098
+ return (
1099
+ <div className="space-y-6 p-6">
1100
+ {/* Entity Instance Details Card */}
1101
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
1102
+ <h2 className="text-2xl font-bold text-gray-900 mb-4">{entity} Details</h2>
1103
+ <DynamicCard data={item} spec={getInstanceSpec(\`\${model}/\${entity}\`)} />
1104
+ </div>
1105
+
1106
+ {/* Child Relationships - EMBEDDED TABLES */}
1107
+ {relationships.map(rel => {
1108
+ const childData = useRelationshipData(rel, item.id);
1109
+ return (
1110
+ <div key={rel.name} className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
1111
+ <h3 className="text-xl font-semibold text-gray-900 mb-4">{rel.displayName}</h3>
1112
+
1113
+ {/* CRITICAL: DynamicTable with Create button */}
1114
+ <DynamicTable
1115
+ data={childData} {/* NEVER JSON.stringify! Must be array */}
1116
+ spec={getDashboardSpec(rel.child)}
1117
+ onRowClick={(child) => navigate(\`/\${rel.child}/\${child.id}\`)}
1118
+ onCreateClick={() => openCreateDialog(rel.child)}
1119
+ showCreateButton={true} {/* CRITICAL: Show create button beside search */}
1120
+ />
1121
+ </div>
1122
+ );
1123
+ })}
1124
+ </div>
1125
+ );
1126
+ }
1127
+ \`\`\`
1128
+
1129
+ **CRITICAL Requirements:**
1130
+ 1. **Entity Details**: Show in card at top with field labels and values
1131
+ 2. **Relationship Tables**: Each relationship shown as DynamicTable (NOT JSON)
1132
+ 3. **Create Button**: MUST appear beside search input in EACH relationship table
1133
+ - Position: Top-right, next to search input
1134
+ - Label: "Add {RelationshipName}" or "Create"
1135
+ - Opens form dialog to create new child record
1136
+ 4. **Click Behavior**: Clicking row navigates to child detail page
1137
+ 5. **Spacing**: Each section in separate white card with border
1138
+ 6. **No Relationships?**: If entity has no relationships, only show details card
1139
+
1140
+ ## Step 7: Workflows
1141
+
1142
+ Create **src/components/workflows/WorkflowDialog.tsx** - Modal dialog for workflow execution:
1143
+
1144
+ **CRITICAL - Must read form fields from spec:**
1145
+ \`\`\`typescript
1146
+ interface WorkflowDialogProps {
1147
+ workflow: WorkflowInfo; // Has name, displayName, description, icon, inputs
1148
+ onClose: () => void;
1149
+ }
1150
+
1151
+ export function WorkflowDialog({ workflow, onClose }: WorkflowDialogProps) {
1152
+ const [formData, setFormData] = useState({});
1153
+ const [loading, setLoading] = useState(false);
1154
+ const [error, setError] = useState(null);
1155
+
1156
+ // CRITICAL: Read input fields from spec["WorkflowName.inputs"]
1157
+ const inputFields = workflow.inputs || {};
1158
+ const fieldNames = Object.keys(inputFields);
1159
+
1160
+ const handleSubmit = async (e) => {
1161
+ e.preventDefault();
1162
+ setLoading(true);
1163
+
1164
+ try {
1165
+ // Extract model and workflow name
1166
+ const [model, workflowName] = workflow.name.split('/');
1167
+
1168
+ // Submit to workflow endpoint
1169
+ const useMock = import.meta.env.VITE_USE_MOCK_DATA === 'true';
1170
+ const result = useMock
1171
+ ? await mockApi.executeWorkflow(model, workflowName, formData)
1172
+ : await fetch(\`\${API_URL}/\${model}/\${workflowName}\`, {
1173
+ method: 'POST',
1174
+ headers: { 'Content-Type': 'application/json' },
1175
+ body: JSON.stringify(formData)
1176
+ }).then(r => r.json());
1177
+
1178
+ if (result.status === 'success') {
1179
+ // Show success toast
1180
+ toast.success('Workflow executed successfully!');
1181
+ onClose();
1182
+ } else {
1183
+ setError(result.error || 'Workflow execution failed');
1184
+ }
1185
+ } catch (err) {
1186
+ setError(err.message);
1187
+ } finally {
1188
+ setLoading(false);
1189
+ }
1190
+ };
1191
+
1192
+ return (
1193
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
1194
+ <div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
1195
+ {/* Header */}
1196
+ <div className="flex justify-between items-center p-6 border-b">
1197
+ <div className="flex items-center gap-3">
1198
+ <Icon icon={workflow.icon || 'mdi:lightning-bolt'} className="text-3xl text-blue-600" />
1199
+ <div>
1200
+ <h2 className="text-2xl font-bold text-gray-900">{workflow.displayName}</h2>
1201
+ <p className="text-sm text-gray-600">{workflow.description}</p>
1202
+ </div>
1203
+ </div>
1204
+ <button onClick={onClose} className="text-gray-400 hover:text-gray-600">
1205
+ <Icon icon="mdi:close" className="text-2xl" />
1206
+ </button>
1207
+ </div>
1208
+
1209
+ {/* Form */}
1210
+ <form onSubmit={handleSubmit} className="p-6">
1211
+ <div className="space-y-4">
1212
+ {fieldNames.map(fieldName => {
1213
+ const field = inputFields[fieldName];
1214
+ return (
1215
+ <div key={fieldName}>
1216
+ <label className="block text-sm font-medium text-gray-700 mb-2">
1217
+ {field.label || fieldName}
1218
+ {field.required && <span className="text-red-600">*</span>}
1219
+ </label>
1220
+
1221
+ {/* Render input based on field type */}
1222
+ {field.inputType === 'select' || field.dataSource ? (
1223
+ <select
1224
+ required={field.required}
1225
+ value={formData[fieldName] || ''}
1226
+ onChange={(e) => setFormData({ ...formData, [fieldName]: e.target.value })}
1227
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
1228
+ >
1229
+ <option value="">Select...</option>
1230
+ {/* Options from field.dataSource or field.options */}
1231
+ </select>
1232
+ ) : field.inputType === 'textarea' ? (
1233
+ <textarea
1234
+ required={field.required}
1235
+ value={formData[fieldName] || ''}
1236
+ onChange={(e) => setFormData({ ...formData, [fieldName]: e.target.value })}
1237
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
1238
+ rows={3}
1239
+ />
1240
+ ) : (
1241
+ <input
1242
+ type={field.inputType || 'text'}
1243
+ required={field.required}
1244
+ value={formData[fieldName] || ''}
1245
+ onChange={(e) => setFormData({ ...formData, [fieldName]: e.target.value })}
1246
+ placeholder={field.placeholder}
1247
+ className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
1248
+ />
1249
+ )}
1250
+
1251
+ {field.helpText && (
1252
+ <p className="text-xs text-gray-500 mt-1">{field.helpText}</p>
1253
+ )}
1254
+ </div>
1255
+ );
1256
+ })}
1257
+ </div>
1258
+
1259
+ {error && (
1260
+ <div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-600 text-sm">
1261
+ {error}
1262
+ </div>
1263
+ )}
1264
+
1265
+ {/* Actions */}
1266
+ <div className="flex justify-end gap-3 mt-6">
1267
+ <button
1268
+ type="button"
1269
+ onClick={onClose}
1270
+ className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
1271
+ >
1272
+ Cancel
1273
+ </button>
1274
+ <button
1275
+ type="submit"
1276
+ disabled={loading}
1277
+ className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2"
1278
+ >
1279
+ {loading && <Icon icon="mdi:loading" className="animate-spin" />}
1280
+ Execute
1281
+ </button>
1282
+ </div>
1283
+ </form>
1284
+ </div>
1285
+ </div>
1286
+ );
1287
+ }
1288
+ \`\`\`
1289
+
1290
+ **CRITICAL Requirements:**
1291
+ 1. **Read inputs from spec**: workflow.inputs contains all form fields from spec["\${workflowName}.inputs"]
1292
+ 2. **Dynamic form fields**: Render inputs based on field.inputType (text, select, textarea, number, etc.)
1293
+ 3. **Required fields**: Show red asterisk, enforce with HTML5 required attribute
1294
+ 4. **Field types**:
1295
+ - \`inputType: 'text'\` → text input
1296
+ - \`inputType: 'select'\` or \`dataSource\` → dropdown select
1297
+ - \`inputType: 'textarea'\` → textarea
1298
+ - \`inputType: 'number'\` → number input
1299
+ 5. **Submit**: POST to \`/:model/:workflowName\` with form data
1300
+ 6. **Show errors**: Display error message if execution fails
1301
+ 7. **Success**: Show success toast and close dialog
1302
+ 8. **Loading state**: Disable submit button, show spinner while executing
1303
+
1304
+ Create **src/components/dashboard/QuickActions.tsx** - Workflow cards for dashboard:
1305
+ \`\`\`typescript
1306
+ import { getWorkflows } from '@/utils/workflowParser';
1307
+ import { uiSpec } from '@/data/uiSpec';
1308
+ import { Icon } from '@iconify/react';
1309
+ import { useState } from 'react';
1310
+ import { WorkflowDialog } from '@/components/workflows/WorkflowDialog';
1311
+
1312
+ export function QuickActions() {
1313
+ // Get workflows that should show on dashboard
1314
+ const workflows = getWorkflows(uiSpec).filter(w => w.ui?.showOnDashboard === true);
1315
+ const [selectedWorkflow, setSelectedWorkflow] = useState(null);
1316
+
1317
+ if (workflows.length === 0) {
1318
+ return (
1319
+ <div className="text-center py-8 text-gray-500">
1320
+ <p>No quick actions configured</p>
1321
+ </div>
1322
+ );
1323
+ }
1324
+
1325
+ return (
1326
+ <>
1327
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1328
+ {workflows.map(workflow => (
1329
+ <button
1330
+ key={workflow.name}
1331
+ onClick={() => setSelectedWorkflow(workflow)}
1332
+ className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:shadow-md transition-all text-left bg-white"
1333
+ >
1334
+ <div className="flex items-start gap-4">
1335
+ <div className="bg-blue-50 p-3 rounded-lg">
1336
+ <Icon icon={workflow.icon || 'mdi:lightning-bolt'} className="text-3xl text-blue-600" />
1337
+ </div>
1338
+ <div className="flex-1">
1339
+ <h3 className="font-semibold text-gray-900 mb-1">{workflow.displayName}</h3>
1340
+ <p className="text-sm text-gray-600 line-clamp-2">{workflow.description}</p>
1341
+ </div>
1342
+ </div>
1343
+ </button>
1344
+ ))}
1345
+ </div>
1346
+
1347
+ {/* Workflow Dialog */}
1348
+ {selectedWorkflow && (
1349
+ <WorkflowDialog
1350
+ workflow={selectedWorkflow}
1351
+ onClose={() => setSelectedWorkflow(null)}
1352
+ />
1353
+ )}
1354
+ </>
1355
+ );
1356
+ }
1357
+ \`\`\`
1358
+
1359
+ **Requirements:**
1360
+ - Filter workflows where \`showOnDashboard === true\`
1361
+ - Grid layout: 3 columns on desktop, 2 on tablet, 1 on mobile
1362
+ - Each card: Icon + displayName + description
1363
+ - Click opens WorkflowDialog
1364
+ - Hover effect: border color change + shadow
1365
+ - Empty state if no workflows configured
1366
+
1367
+ ## Step 8: Dashboard Page (CRITICAL - Polished & Deployment-Ready)
1368
+
1369
+ Create **src/pages/Dashboard.tsx** - Professional, polished dashboard:
1370
+ \`\`\`typescript
1371
+ export function Dashboard() {
1372
+ const entities = Object.keys(mockData);
1373
+
1374
+ return (
1375
+ <div className="space-y-8 p-6">
1376
+ {/* Welcome Header */}
1377
+ <div>
1378
+ <h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
1379
+ <p className="text-gray-600 mt-1">Welcome back! Here's an overview of your system.</p>
1380
+ </div>
1381
+
1382
+ {/* Stat Cards - Show ALL entity counts with icons */}
1383
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
1384
+ {entities.map(entityKey => {
1385
+ const count = Array.isArray(mockData[entityKey]) ? mockData[entityKey].length : 0;
1386
+ const entityName = entityKey.split('/')[1];
1387
+ return (
1388
+ <div key={entityKey} className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow p-6 border border-gray-200">
1389
+ <div className="flex items-center justify-between">
1390
+ <div>
1391
+ <p className="text-sm font-medium text-gray-600">{entityName}</p>
1392
+ <p className="text-3xl font-bold text-gray-900 mt-2">{count}</p>
1393
+ <p className="text-xs text-gray-500 mt-1">Total records</p>
1394
+ </div>
1395
+ <div className="bg-blue-50 p-3 rounded-lg">
1396
+ <Icon icon="mdi:database" className="text-3xl text-blue-600" />
1397
+ </div>
1398
+ </div>
1399
+ </div>
1400
+ );
1401
+ })}
1402
+ </div>
1403
+
1404
+ {/* Workflow Quick Actions Section */}
1405
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
1406
+ <div className="mb-6">
1407
+ <h2 className="text-xl font-semibold text-gray-900">Quick Actions</h2>
1408
+ <p className="text-sm text-gray-600 mt-1">Execute workflows directly from the dashboard</p>
1409
+ </div>
1410
+ <QuickActions />
1411
+ </div>
1412
+
1413
+ {/* Recent Activity Section (Optional) */}
1414
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1415
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
1416
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h3>
1417
+ <div className="space-y-3">
1418
+ <div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
1419
+ <Icon icon="mdi:check-circle" className="text-green-600 text-xl" />
1420
+ <div>
1421
+ <p className="text-sm font-medium text-gray-900">System ready</p>
1422
+ <p className="text-xs text-gray-600">All services operational</p>
1423
+ </div>
1424
+ </div>
1425
+ </div>
1426
+ </div>
1427
+
1428
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
1429
+ <h3 className="text-lg font-semibold text-gray-900 mb-4">System Status</h3>
1430
+ <div className="space-y-3">
1431
+ <div className="flex justify-between items-center">
1432
+ <span className="text-sm text-gray-600">Database</span>
1433
+ <span className="text-sm font-medium text-green-600">Connected</span>
1434
+ </div>
1435
+ <div className="flex justify-between items-center">
1436
+ <span className="text-sm text-gray-600">Mock Mode</span>
1437
+ <span className="text-sm font-medium text-blue-600">Active</span>
1438
+ </div>
1439
+ </div>
1440
+ </div>
1441
+ </div>
1442
+ </div>
1443
+ );
1444
+ }
1445
+ \`\`\`
1446
+
1447
+ **Dashboard Requirements:**
1448
+ - **Professional Header**: Welcome message with page title
1449
+ - **Stat Cards**: Show ALL entities with:
1450
+ * Entity count in large, bold text
1451
+ * Icon in colored background circle
1452
+ * "Total records" label
1453
+ * Hover effect with shadow transition
1454
+ - **Quick Actions Section**:
1455
+ * White card with border
1456
+ * Section header with description
1457
+ * Grid of workflow cards (3-4 per row)
1458
+ - **Recent Activity/Status**: Optional cards showing system info
1459
+ - **Spacing**: Generous spacing (32px between sections)
1460
+ - **Colors**: Professional palette (gray-900 text, blue-600 accents, subtle borders)
1461
+ - **Polish**: Shadows, borders, hover effects, icons in colored backgrounds
1462
+
1463
+ ## Step 9: Layout & Navigation
1464
+
1465
+ Create **src/components/layout/Sidebar.tsx** - Toggleable sidebar with clear structure:
1466
+
1467
+ **Structure (top to bottom):**
1468
+ 1. **App Title/Logo** at top
1469
+ 2. **User Menu** (at top):
1470
+ - User avatar/icon (mdi:account-circle)
1471
+ - User name from auth context
1472
+ - Dropdown menu on click:
1473
+ * "Profile" → Opens profile settings dialog
1474
+ * "Logout" → Clears auth token, redirects to login
1475
+ 3. **Home Button** - Links to dashboard (\`/\`) with \`mdi:home\` icon
1476
+ 4. **Entities Section**:
1477
+ - Section header: "Entities"
1478
+ - List ALL entities from spec
1479
+ - Each entity: icon + name, links to \`/entity-list/:model/:entity\`
1480
+ - When clicked, shows entity list view
1481
+ - Clicking an entity instance → shows detail page with relationships
1482
+ 5. **Workflows Section** (separate from entities):
1483
+ - Section header: "Workflows"
1484
+ - List ALL workflows from spec.workflows
1485
+ - **CRITICAL**: Each workflow is a CLICKABLE button that opens WorkflowDialog
1486
+ - Each workflow: icon + displayName
1487
+ - onClick handler: Opens WorkflowDialog component with workflow data
1488
+ - Same workflows as dashboard Quick Actions
1489
+
1490
+ **Behavior:**
1491
+ - **Hamburger toggle** (mdi:menu icon) at top - collapses/expands sidebar
1492
+ - **State persisted**: Save collapsed state to localStorage
1493
+ - **Mobile**: Hidden by default on mobile (< 768px), overlay when opened
1494
+ - **Desktop**: Side-by-side with content, collapsible
1495
+ - **Animation**: Smooth 200-300ms transition
1496
+
1497
+ **Example Structure:**
1498
+ \`\`\`
1499
+ ┌─────────────────┐
1500
+ │ App Name │
1501
+ │ 👤 John Doe ▾ │ ← User menu (Profile/Logout)
1502
+ ├─────────────────┤
1503
+ │ 🏠 Home │ ← Links to dashboard
1504
+ ├─────────────────┤
1505
+ │ ENTITIES │
1506
+ │ 📦 Customers │
1507
+ │ 📦 Orders │
1508
+ │ 📦 Products │
1509
+ ├─────────────────┤
1510
+ │ WORKFLOWS │
1511
+ │ ⚡ Create Order │ ← CLICKABLE button, opens WorkflowDialog
1512
+ │ ⚡ Process Sale │ ← CLICKABLE button, opens WorkflowDialog
1513
+ └─────────────────┘
1514
+ \`\`\`
1515
+
1516
+ **User Profile Dialog:**
1517
+ - Modal/dialog that opens when "Profile" clicked
1518
+ - Shows user info: name, email, role
1519
+ - Allows editing name
1520
+ - "Save" and "Cancel" buttons
1521
+
1522
+ **Logout:**
1523
+ - Clear localStorage token
1524
+ - Clear any auth context state
1525
+ - Redirect to \`/login\` page
1526
+
1527
+ Create **src/components/layout/ChatbotBubble.tsx** - Floating chat with agent selection:
1528
+
1529
+ **CRITICAL - Agent Selection Inside Bubble:**
1530
+ - **Fixed position**: bottom-right corner (20px from right, 20px from bottom)
1531
+ - **z-index**: 9999 (above all content)
1532
+ - **Closed state**: Small circular button with chat icon (mdi:message)
1533
+ - **Opened state**: Expands to chat panel (400px width, 600px height)
1534
+ - **Agent Dropdown**: INSIDE the chat panel header
1535
+ * Dropdown to select agent from spec.agents array
1536
+ * Label: "Talk to:" or "Select Agent:"
1537
+ * Default to first agent
1538
+ - **Chat Interface**:
1539
+ * Messages area (scrollable)
1540
+ * Input field at bottom
1541
+ * Send button
1542
+ * Backend-powered: POST to \`/agents/:agentName/chat\` with message
1543
+ - **Close button**: X button in chat panel header
1544
+
1545
+ **DO NOT** put agent selection in sidebar - it belongs in the chatbot bubble!
1546
+
1547
+ Create **src/components/ErrorBoundary.tsx** - Error boundary component
1548
+
1549
+ Create **src/components/auth/Login.tsx** - Modern login page with social auth:
1550
+
1551
+ **Layout Structure:**
1552
+ 1. **NO SIDEBAR** on auth pages
1553
+ 2. **Centered card** design (max-width: 400px) with logo/app name at top
1554
+ 3. **Social Sign-In Buttons** (top section):
1555
+ - Google button: White background, Google logo, "Continue with Google"
1556
+ - GitHub button: Dark background, GitHub logo, "Continue with GitHub"
1557
+ - Microsoft button: White background, Microsoft logo, "Continue with Microsoft"
1558
+ - Each button full-width, proper brand colors
1559
+ - Click shows toast: "Social sign-in coming soon!" (not implemented yet)
1560
+ 4. **Divider**: Horizontal line with "or" text in center
1561
+ 5. **Email/Password Form**:
1562
+ - Email input with icon
1563
+ - Password input with show/hide toggle
1564
+ - "Remember me" checkbox
1565
+ - "Forgot password?" link
1566
+ 6. **Sign In Button**: Primary button, full-width
1567
+ 7. **Mock credentials**: Small text below: "Demo: admin@example.com / admin123"
1568
+ 8. **Sign Up Link**: "Don't have an account? Sign up" at bottom
1569
+
1570
+ **Code Example:**
1571
+ \`\`\`tsx
1572
+ <div className="space-y-4">
1573
+ {/* Social Auth Buttons */}
1574
+ <button
1575
+ onClick={() => toast.info('Google sign-in coming soon!')}
1576
+ className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
1577
+ >
1578
+ <Icon icon="mdi:google" className="text-xl" />
1579
+ <span className="font-medium">Continue with Google</span>
1580
+ </button>
1581
+
1582
+ <button
1583
+ onClick={() => toast.info('GitHub sign-in coming soon!')}
1584
+ className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition-colors"
1585
+ >
1586
+ <Icon icon="mdi:github" className="text-xl" />
1587
+ <span className="font-medium">Continue with GitHub</span>
1588
+ </button>
1589
+
1590
+ {/* Divider */}
1591
+ <div className="relative my-6">
1592
+ <div className="absolute inset-0 flex items-center">
1593
+ <div className="w-full border-t border-gray-300"></div>
1594
+ </div>
1595
+ <div className="relative flex justify-center text-sm">
1596
+ <span className="px-2 bg-white text-gray-500">or continue with email</span>
1597
+ </div>
1598
+ </div>
1599
+
1600
+ {/* Email/Password Form */}
1601
+ <form onSubmit={handleEmailLogin}>
1602
+ {/* Email and password inputs */}
1603
+ </form>
1604
+ </div>
1605
+ \`\`\`
1606
+
1607
+ **CRITICAL Requirements:**
1608
+ - **Social buttons**: Google (white), GitHub (dark), Microsoft (white) - use \`mdi:google\`, \`mdi:github\`, \`mdi:microsoft\`
1609
+ - **Click behavior**: Show toast "Coming soon!" - NOT implemented yet
1610
+ - **Divider**: "or continue with email" between social and email form
1611
+ - **Email login**: Must call \`mockApi.login(email, password)\` on submit
1612
+ - **Handle response**: Check \`result.status === 'success'\`, store token, navigate to dashboard
1613
+ - **Show errors**: Display \`result.error\` if login fails
1614
+ - **Mock credentials**: Display "Demo: admin@example.com / admin123" below form
1615
+
1616
+ Create **src/components/auth/SignUp.tsx** - Modern signup page with social auth:
1617
+
1618
+ **Layout Structure:**
1619
+ 1. **Similar design** to Login page
1620
+ 2. **Social Sign-Up Buttons** (same as login):
1621
+ - Google, GitHub, Microsoft buttons
1622
+ - Click shows toast: "Social sign-up coming soon!"
1623
+ 3. **Divider**: "or sign up with email"
1624
+ 4. **Form fields**: Name, Email, Password, Confirm Password
1625
+ 5. **Terms checkbox**: "I agree to Terms of Service and Privacy Policy"
1626
+ 6. **Sign Up Button**: Primary button, full-width
1627
+ 7. **Login Link**: "Already have an account? Sign in" at bottom
1628
+
1629
+ **CRITICAL Requirements:**
1630
+ - **Social buttons**: Same styling as login page
1631
+ - **Form validation**: Check password match, email format, terms accepted
1632
+ - **Email signup**: Must call \`mockApi.signUp(email, password, name)\` on submit
1633
+ - **Handle response**: Check \`result.status === 'success'\`, store token, navigate to dashboard
1634
+ - **Show errors**: Display \`result.error\` if signup fails (e.g., "User already exists")
1635
+
1636
+ ## Step 10: Main App
1637
+
1638
+ Create **src/App.tsx**:
1639
+ - Setup React Router with entity routes
1640
+ - Include Sidebar, ChatbotBubble in layout
1641
+ - Wrap with ErrorBoundary in main.tsx
1642
+
1643
+ Create **src/main.tsx**:
1644
+ \`\`\`typescript
1645
+ ReactDOM.createRoot(document.getElementById('root')!).render(
1646
+ <React.StrictMode>
1647
+ <ErrorBoundary>
1648
+ <App />
1649
+ </ErrorBoundary>
1650
+ </React.StrictMode>
1651
+ );
1652
+ \`\`\`
1653
+
1654
+ Create **src/index.css** - Tailwind imports
1655
+
1656
+ # UI POLISH & MINIMALISM GUIDELINES
1657
+
1658
+ ⚠️ **CRITICAL**: The generated UI must be polished, professional, and deployment-ready.
1659
+
1660
+ ## Visual Polish
1661
+
1662
+ **Clean Modern Aesthetic:**
1663
+ - **Whitespace**: Generous spacing between sections (24-32px)
1664
+ - **Borders**: Subtle borders (\`border-gray-200\`) or shadows instead of heavy lines
1665
+ - **Corners**: Consistent border-radius (\`rounded-lg\` for cards, \`rounded-md\` for buttons/inputs)
1666
+ - **Shadows**: Subtle shadows for depth
1667
+ * Cards: \`shadow\` or \`shadow-sm\`
1668
+ * Hover: \`hover:shadow-md\`
1669
+ * Modals: \`shadow-xl\`
1670
+ - **Colors**: Limited, consistent palette
1671
+ * Primary: \`bg-blue-500\`, \`text-blue-600\`
1672
+ * Success: \`bg-green-500\`, \`text-green-600\`
1673
+ * Danger: \`bg-red-500\`, \`text-red-600\`
1674
+ * Neutral: \`text-gray-600\`, \`bg-gray-50\`
1675
+
1676
+ **Typography:**
1677
+ - **Font sizes**:
1678
+ * h1: \`text-3xl\` (page titles)
1679
+ * h2: \`text-xl\` (section titles)
1680
+ * h3: \`text-lg\` (card titles)
1681
+ * body: \`text-base\` (content)
1682
+ * small: \`text-sm\` (meta info)
1683
+ - **Line height**: Use Tailwind defaults (\`leading-normal\`, \`leading-relaxed\`)
1684
+ - **Contrast**: Ensure readable text (dark text on light backgrounds)
1685
+
1686
+ **Smooth Interactions:**
1687
+ - **Transitions**: \`transition-all duration-200\` on interactive elements
1688
+ - **Hover effects**: Background darken, shadow changes
1689
+ * Buttons: \`hover:bg-blue-600\`, \`hover:shadow-md\`
1690
+ * Table rows: \`hover:bg-gray-50\`
1691
+ - **Loading states**:
1692
+ * Disable buttons + show spinner during submit
1693
+ * Skeleton screens for content loading
1694
+ - **Animations**: Subtle, purposeful (not distracting)
1695
+
1696
+ ## Minimalism & Clutter Reduction
1697
+
1698
+ **NO Unnecessary Elements:**
1699
+ - **One primary action** per section
1700
+ - **Don't add features** not in spec (no "Export", "Print", "Share" unless specified)
1701
+ - **Hide advanced features** in dropdowns/"More" menus
1702
+ - **Progressive disclosure**: Show simple interface first
1703
+
1704
+ **Clean Tables:**
1705
+ - **Essential columns only**: 4-6 most important columns
1706
+ - **Icon actions**: Use icon buttons for row actions (edit, delete, view)
1707
+ - **Bulk actions**: Only show when rows selected
1708
+ - **Compact filters**: Collapse into "Filters" button if many
1709
+
1710
+ **Simple Forms:**
1711
+ - **Group related fields** visually
1712
+ - **Single column** layout (easier to scan)
1713
+ - **Inline validation**: Show errors after field blur
1714
+ - **Help text**: Use placeholder text or tooltips
1715
+
1716
+ ## Consistency
1717
+
1718
+ **Component Patterns:**
1719
+ - **Same spacing** everywhere (use \`space-y-4\`, \`gap-4\`, \`p-6\` consistently)
1720
+ - **Same colors**: Stick to the palette above
1721
+ - **Same patterns**: If one table has search at top-right, ALL tables should
1722
+
1723
+ **Icon Usage:**
1724
+ - **Consistent icon set**: ONLY Material Design Icons via \`@iconify/react\`
1725
+ - **Consistent sizes**: \`text-xl\` for buttons, \`text-2xl\` for features, \`text-base\` inline
1726
+ - **Standard icons**:
1727
+ * Create: \`mdi:plus\`
1728
+ * Edit: \`mdi:pencil\`
1729
+ * Delete: \`mdi:delete\`
1730
+ * Search: \`mdi:magnify\`
1731
+ * Filter: \`mdi:filter-variant\`
1732
+ * Menu: \`mdi:menu\`
1733
+ * Close: \`mdi:close\`
1734
+ * Check: \`mdi:check\`
1735
+ * Alert: \`mdi:alert-circle\`
1736
+
1737
+ ## Mobile Responsiveness
1738
+
1739
+ **Responsive Patterns:**
1740
+ - **Tables**: Use responsive grid classes (\`grid-cols-1 md:grid-cols-2 lg:grid-cols-4\`)
1741
+ - **Sidebars**: Overlay on mobile (\`< 768px\`), side-by-side on desktop
1742
+ - **Forms**: Full-width inputs on mobile
1743
+ - **Touch targets**: Min 44px height for buttons (\`py-2\` or \`py-3\`)
1744
+
1745
+ ## Component-Specific Polish
1746
+
1747
+ **Buttons:**
1748
+ - ✅ Hover states: \`hover:bg-blue-600\`, \`hover:shadow-md\`
1749
+ - ✅ Disabled state: \`disabled:opacity-50\`, \`disabled:cursor-not-allowed\`
1750
+ - ✅ Loading state: Show spinner + disable
1751
+
1752
+ **Inputs:**
1753
+ - ✅ Focus state: \`focus:ring-2\`, \`focus:ring-blue-500\`, \`focus:border-blue-500\`
1754
+ - ✅ Error state: \`border-red-500\`, error message below
1755
+ - ✅ Disabled state: \`bg-gray-100\`, \`cursor-not-allowed\`
1756
+
1757
+ **Tables:**
1758
+ - ✅ Hover rows: \`hover:bg-gray-50\`
1759
+ - ✅ Clickable rows: \`cursor-pointer\`
1760
+ - ✅ Empty state: "No data available" message
1761
+ - ✅ Loading: Skeleton or spinner
1762
+
1763
+ **Modals:**
1764
+ - ✅ Smooth fade-in: \`transition-opacity\`
1765
+ - ✅ Backdrop: \`bg-black bg-opacity-50\`
1766
+ - ✅ Click outside to close
1767
+ - ✅ Escape key to close
1768
+
1769
+ **Remember:**
1770
+ - **Quality over quantity**: Better to have fewer, well-polished features
1771
+ - **Minimalism**: When in doubt, leave it out
1772
+ - **Consistency**: Every page should feel like part of the same app
1773
+ - **Professional**: Should look like a commercial SaaS product
1774
+
1775
+ # PHASE 2: VERIFY & FIX
1776
+
1777
+ After generating ALL files above:
1778
+
1779
+ 1. Run \`npm install\`
1780
+
1781
+ 2. Run \`tsc --noEmit\`
1782
+ - Fix any TypeScript errors
1783
+ - Common fixes: add missing imports, fix type annotations
1784
+
1785
+ 3. Run \`npm run build\`
1786
+ - Fix any build errors
1787
+ - Common fixes: resolve import paths, add missing dependencies
1788
+
1789
+ 4. Check for common issues:
1790
+ - **"not iterable" errors?** → Add \`Array.isArray()\` checks in DynamicTable
1791
+ - **Relationship tables showing JSON?** → Ensure EntityDetail uses DynamicTable component
1792
+ - **Missing search in tables?** → Verify DynamicTable has search input + create button together
1793
+ - **Missing mock data?** → Add all entities to mockData.ts
1794
+ - **TypeScript errors?** → Fix type annotations and imports
1795
+
1796
+ 5. Fix all issues found, then run \`npm run build\` again
1797
+
1798
+ 6. **DO NOT run \`npm run dev\`** - Only verify build succeeds
1799
+
1800
+ # CRITICAL RULES - READ BEFORE STARTING
1801
+
1802
+ ## Navigation & Layout
1803
+ 1. **Sidebar Structure**:
1804
+ - User menu at top (avatar + name + dropdown with Profile/Logout)
1805
+ - Home button (links to dashboard)
1806
+ - Entities section (only entities, NOT workflows)
1807
+ - **Workflows section (CLICKABLE buttons that open WorkflowDialog)**
1808
+ - Toggleable with localStorage persistence
1809
+ 2. **Dashboard**: Stat cards + Quick Actions (workflows) + status cards
1810
+ 3. **Entity Detail**: Show details card + embedded relationship tables (NOT JSON)
1811
+ 4. **Chatbot**: Agent selection dropdown INSIDE chatbot panel, not in sidebar
1812
+
1813
+ ## Tables & Data
1814
+ 5. **All tables MUST have**:
1815
+ - Search input + Create button (TOGETHER on right)
1816
+ - **Actions column with Edit and Delete icon buttons for EACH row**
1817
+ - Pagination
1818
+ 6. **Table Actions**:
1819
+ - Edit button: Blue pencil icon, opens edit form
1820
+ - Delete button: Red trash icon, shows confirmation then deletes
1821
+ - Use \`e.stopPropagation()\` on action buttons to prevent row click
1822
+ 7. **Relationship tables**: MUST have Create button beside search in each table
1823
+ 8. **Never use JSON.stringify**: Always render relationships as DynamicTable
1824
+ 9. **Always validate arrays**: \`Array.isArray(data) ? data : []\` before operations
1825
+ 10. **Mock data**: Create 3-5 realistic records for ALL entities
1826
+
1827
+ ## Workflows
1828
+ 11. **Sidebar workflows**: MUST be CLICKABLE buttons that open WorkflowDialog
1829
+ 12. **WorkflowDialog MUST read**: \`workflow.inputs\` from spec["\${workflowName}.inputs"]
1830
+ 13. **Dynamic form fields**: Render inputs based on \`inputType\` (text, select, textarea, number)
1831
+ 14. **Submit workflow**: POST to \`/:model/:workflowName\` with form data
1832
+
1833
+ ## User Management
1834
+ 15. **User menu**: Show in sidebar at top with avatar, name, dropdown
1835
+ 16. **Logout**: Clear localStorage token, redirect to /login
1836
+ 17. **Profile**: Opens dialog to edit user name, email (read-only), role (read-only)
1837
+
1838
+ ## Authentication
1839
+ 18. **Social auth buttons**: Show Google, GitHub, Microsoft buttons (UI only, not functional yet)
1840
+ 19. **Social button click**: Show toast "Coming soon!" - functionality to be added later
1841
+ 20. **Email login/signup MUST call**: \`mockApi.login()\` and \`mockApi.signUp()\`
1842
+ 21. **Show mock credentials**: Display "Demo: admin@example.com / admin123" on login page
1843
+ 22. **Handle responses**: Check \`result.status === 'success'\`, show errors
1844
+ 23. **Divider**: "or continue with email" between social buttons and email form
1845
+
1846
+ ## Styling & Polish
1847
+ 24. **Tailwind only**: No inline styles, no CSS-in-JS
1848
+ 25. **Professional polish**: Borders, shadows, hover effects, spacing (24-32px)
1849
+ 26. **Consistent colors**: Blue-600 primary, gray-900 text, gray-50 backgrounds
1850
+ 27. **Icons**: Only Material Design Icons (mdi:*) via @iconify/react
1851
+
1852
+ ## Build Requirements
1853
+ 28. **Build must succeed**: Fix all errors until \`npm run build\` passes
1854
+ 29. **NO dev server**: Only verify with \`tsc --noEmit\` and \`npm run build\`
1855
+ 30. **Workflows from spec**: Read from spec.workflows array, access metadata with spec[workflowName]
1856
+
1857
+ START NOW! Generate all files, then verify and fix any issues.`;
1858
+ }
1859
+ //# sourceMappingURL=uiGenerator.js.map