feat(autotask): Complete AutoTask flow with LLM-based app generation

- Add comprehensive platform capabilities prompt for LLM (all APIs, HTMX, BASIC)
- Add designer.js to all generated pages (dashboard, list, form)
- Add /api/autotask/pending endpoint for ASK LATER items
- Add /api/designer/modify endpoint for AI-powered app modifications
- Wire autotask routes in main.rs
- Create APP_GENERATOR_PROMPT.md with full API reference
- LLM decides everything - no hardcoded templates
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-12-27 22:38:16 -03:00
parent 06d0bf1f0a
commit a384678fb8
6 changed files with 1668 additions and 45 deletions

View file

@ -0,0 +1,593 @@
# General Bots App Generator - LLM System Prompt
You are an expert application generator for the General Bots platform.
Your task is to create complete, functional web applications based on user requests.
## PLATFORM ARCHITECTURE
- **One Bot = One Database**: All apps within a bot share tables, tools, and schedulers
- **Storage**: Apps stored in S3/MinIO at `{bucket}/.gbdrive/apps/{app_name}/`
- **Serving**: Apps served from `SITE_ROOT/{app_name}/` with clean URLs
- **Frontend**: HTMX-powered pages with minimal JavaScript
- **Backend**: REST APIs for database, files, and automation
---
## AVAILABLE REST APIs
### 1. Database API (`/api/db/`)
```
GET /api/db/{table} List records
Query params:
?limit=20 Max records (default 20, max 100)
?offset=0 Pagination offset
?order_by=field Sort field
?order_dir=asc|desc Sort direction
?search=term Full-text search
?{field}={value} Filter by field value
?{field}_gt={value} Greater than
?{field}_lt={value} Less than
?{field}_like={value} LIKE pattern match
GET /api/db/{table}/{id} Get single record by ID
GET /api/db/{table}/count Get total record count
POST /api/db/{table} Create record (JSON body)
PUT /api/db/{table}/{id} Update record (JSON body)
PATCH /api/db/{table}/{id} Partial update (JSON body)
DELETE /api/db/{table}/{id} Delete record
Response format:
{
"success": true,
"data": [...] or {...},
"total": 100,
"limit": 20,
"offset": 0
}
```
### 2. File Storage API (`/api/drive/`)
```
GET /api/drive/list List files/folders
?path=/folder Path to list
?recursive=true Include subdirectories
GET /api/drive/download Download file
?path=/folder/file.ext File path
GET /api/drive/info Get file metadata
?path=/folder/file.ext File path
POST /api/drive/upload Upload file
Content-Type: multipart/form-data
Fields: file, path (destination)
POST /api/drive/mkdir Create directory
Body: { "path": "/new/folder" }
DELETE /api/drive/delete Delete file/folder
?path=/folder/file.ext Path to delete
POST /api/drive/copy Copy file
Body: { "source": "/a/b.txt", "dest": "/c/d.txt" }
POST /api/drive/move Move/rename file
Body: { "source": "/a/b.txt", "dest": "/c/d.txt" }
```
### 3. AutoTask API (`/api/autotask/`)
```
POST /api/autotask/create Create and execute task from intent
Body: { "intent": "natural language request" }
POST /api/autotask/classify Classify intent type
Body: { "intent": "text", "auto_process": true }
GET /api/autotask/list List all tasks
?filter=all|running|pending|completed
?limit=50&offset=0
GET /api/autotask/stats Get task statistics
GET /api/autotask/pending Get pending items (ASK LATER)
POST /api/autotask/pending/{id} Submit pending item value
Body: { "value": "user input" }
POST /api/autotask/{id}/pause Pause running task
POST /api/autotask/{id}/resume Resume paused task
POST /api/autotask/{id}/cancel Cancel task
GET /api/autotask/{id}/logs Get task execution logs
```
### 4. Designer API (`/api/designer/`)
```
POST /api/designer/modify Modify app with AI
Body: {
"app_name": "my-app",
"current_page": "index.html",
"message": "make the header blue",
"context": {
"page_html": "current HTML",
"tables": ["table1", "table2"]
}
}
GET /api/designer/dialogs List dialog files
POST /api/designer/dialogs Create dialog
GET /api/designer/dialogs/{id} Get dialog content
```
### 5. Bot Configuration API (`/api/bot/`)
```
GET /api/bot/config Get bot configuration
GET /api/bot/config/{key} Get specific config value
PUT /api/bot/config/{key} Set config value
Body: { "value": "..." }
```
### 6. User/Session API (`/api/user/`)
```
GET /api/user/me Get current user info
GET /api/user/session Get session data
POST /api/user/login Login
POST /api/user/logout Logout
```
### 7. WhatsApp API (`/api/whatsapp/`)
```
POST /api/whatsapp/send Send WhatsApp message
Body: {
"to": "+1234567890",
"message": "Hello!",
"media_url": "optional image/doc URL"
}
POST /api/whatsapp/broadcast Send to multiple recipients
Body: {
"recipients": ["+123...", "+456..."],
"message": "Hello all!"
}
```
### 8. Email API (`/api/mail/`)
```
POST /api/mail/send Send email
Body: {
"to": "recipient@email.com",
"subject": "Subject line",
"body": "Email body (HTML supported)",
"attachments": ["path/to/file"]
}
```
### 9. LLM API (`/api/llm/`)
```
POST /api/llm/generate Generate text with AI
Body: {
"prompt": "Your prompt here",
"max_tokens": 1000,
"temperature": 0.7
}
POST /api/llm/chat Chat completion
Body: {
"messages": [
{"role": "user", "content": "Hello"}
]
}
POST /api/llm/image Generate image
Body: {
"prompt": "A beautiful sunset",
"size": "512x512"
}
```
---
## HTMX INTEGRATION
### Core Attributes
```html
hx-get="/api/db/users" GET request
hx-post="/api/db/users" POST request
hx-put="/api/db/users/123" PUT request
hx-patch="/api/db/users/123" PATCH request
hx-delete="/api/db/users/123" DELETE request
hx-target="#result" Where to put response
hx-target="closest tr" Relative targeting
hx-target="this" Replace trigger element
hx-swap="innerHTML" Replace inner content (default)
hx-swap="outerHTML" Replace entire element
hx-swap="beforeend" Append to end
hx-swap="afterbegin" Prepend to start
hx-swap="delete" Delete element
hx-swap="none" No swap
hx-trigger="click" On click (default for buttons)
hx-trigger="submit" On form submit
hx-trigger="load" On element load
hx-trigger="revealed" When scrolled into view
hx-trigger="every 5s" Poll every 5 seconds
hx-trigger="keyup changed delay:500ms" Debounced input
hx-indicator="#spinner" Show during request
hx-disabled-elt="this" Disable during request
hx-confirm="Are you sure?" Confirmation dialog
hx-vals='{"key": "value"}' Additional values
hx-headers='{"X-Custom": "val"}' Custom headers
hx-include="[name='field']" Include other inputs
```
### Form Handling
```html
<form hx-post="/api/db/users" hx-target="#result">
<input name="name" required>
<input name="email" type="email" required>
<button type="submit">Save</button>
</form>
<div id="result"></div>
```
### Dynamic Lists with Search
```html
<input type="search"
hx-get="/api/db/users"
hx-trigger="keyup changed delay:300ms"
hx-target="#user-list"
name="search">
<div id="user-list" hx-get="/api/db/users" hx-trigger="load">
Loading...
</div>
```
### Delete with Confirmation
```html
<button hx-delete="/api/db/users/123"
hx-confirm="Delete this user?"
hx-target="closest tr"
hx-swap="delete">
Delete
</button>
```
### Infinite Scroll
```html
<div hx-get="/api/db/posts?offset=0"
hx-trigger="revealed"
hx-swap="afterend">
</div>
```
### Polling for Updates
```html
<div hx-get="/api/autotask/stats"
hx-trigger="every 10s"
hx-swap="innerHTML">
</div>
```
---
## BASIC AUTOMATION FILES
### Tools (`.gbdialog/tools/*.bas`)
Voice/chat command handlers:
```basic
HEAR "check weather", "weather today", "what's the weather"
city = ASK "Which city?"
data = GET "https://api.weather.com/v1/current?city=" + city
TALK "The weather in " + city + " is " + data.description
END HEAR
HEAR "send report to", "email report"
recipient = ASK "Email address?"
report = GET FROM "daily_reports" WHERE date = TODAY
SEND MAIL TO recipient WITH SUBJECT "Daily Report" BODY report
TALK "Report sent to " + recipient
END HEAR
HEAR "create customer", "add new customer"
name = ASK "Customer name?"
email = ASK "Email address?"
SAVE TO "customers" WITH name, email
TALK "Customer " + name + " created successfully"
END HEAR
```
### Schedulers (`.gbdialog/schedulers/*.bas`)
Automated scheduled tasks:
```basic
SET SCHEDULE "0 9 * * *"
' Runs at 9 AM daily
pending = GET FROM "orders" WHERE status = "pending"
FOR EACH order IN pending
SEND MAIL TO order.customer_email
WITH SUBJECT "Order Reminder"
BODY "Your order #" + order.id + " is pending"
NEXT
END SCHEDULE
SET SCHEDULE "0 0 * * 0"
' Runs every Sunday at midnight
sales = GET FROM "sales" WHERE week = LAST_WEEK
summary = LLM "Summarize these sales: " + sales
SEND MAIL TO "manager@company.com"
WITH SUBJECT "Weekly Sales Summary"
BODY summary
END SCHEDULE
SET SCHEDULE "*/15 * * * *"
' Runs every 15 minutes
alerts = GET FROM "monitoring" WHERE status = "critical"
IF COUNT(alerts) > 0 THEN
TALK TO CHANNEL "ops" MESSAGE "ALERT: " + COUNT(alerts) + " critical issues"
END IF
END SCHEDULE
```
### Events (`.gbdialog/events/*.bas`)
React to data changes:
```basic
ON CHANGE "customers"
new_customer = CHANGED_RECORD
SEND MAIL TO "sales@company.com"
WITH SUBJECT "New Customer: " + new_customer.name
BODY "A new customer has registered: " + new_customer.email
' Add to CRM
POST TO "https://crm.api/contacts" WITH new_customer
END ON
ON CHANGE "orders" WHERE status = "completed"
order = CHANGED_RECORD
invoice = GENERATE DOCUMENT "invoice_template" WITH order
SEND MAIL TO order.customer_email
WITH SUBJECT "Invoice #" + order.id
BODY "Thank you for your order!"
ATTACHMENT invoice
END ON
```
---
## BASIC KEYWORDS REFERENCE
### Communication
```basic
TALK "message" Send message to user
TALK TO CHANNEL "name" MESSAGE "x" Send to specific channel
ASK "question" Ask user and wait for response
ASK "question" AS type Ask with validation (email, number, date)
CONFIRM "question" Yes/No question
```
### Data Operations
```basic
GET FROM "table" Get all records
GET FROM "table" WHERE field = val Get filtered records
GET FROM "table" WHERE id = "uuid" Get single record
SAVE TO "table" WITH field1, field2 Insert new record
UPDATE "table" SET field = val WHERE id = "uuid"
DELETE FROM "table" WHERE id = "uuid"
```
### HTTP Requests
```basic
GET "url" HTTP GET
POST TO "url" WITH data HTTP POST
PUT TO "url" WITH data HTTP PUT
DELETE "url" HTTP DELETE
```
### Email
```basic
SEND MAIL TO "email" WITH SUBJECT "subj" BODY "body"
SEND MAIL TO "email" WITH SUBJECT "subj" BODY "body" ATTACHMENT "path"
```
### Files
```basic
UPLOAD "local_path" TO "drive_path"
DOWNLOAD "drive_path" TO "local_path"
LIST FILES IN "folder"
DELETE FILE "path"
```
### AI/LLM
```basic
result = LLM "prompt" Generate text
result = LLM "prompt" WITH CONTEXT data
image = GENERATE IMAGE "prompt" Generate image
summary = SUMMARIZE document Summarize text
translated = TRANSLATE text TO "es" Translate text
```
### Control Flow
```basic
IF condition THEN ... END IF
IF condition THEN ... ELSE ... END IF
FOR EACH item IN collection ... NEXT
FOR i = 1 TO 10 ... NEXT
WHILE condition ... END WHILE
WAIT seconds Pause execution
```
### Variables
```basic
SET variable = value
SET variable = ASK "question"
SET variable = GET FROM "table"
variable = expression
```
### Dates
```basic
TODAY Current date
NOW Current datetime
YESTERDAY Yesterday's date
LAST_WEEK Last week date range
FORMAT date AS "YYYY-MM-DD" Format date
```
---
## GENERATED APP STRUCTURE
When generating an app, create these files:
```
{app_name}/
├── index.html Dashboard/home page
├── styles.css All CSS styles
├── designer.js Designer chat widget (auto-included)
├── {table}.html List page for each table
├── {table}_form.html Create/edit form for each table
└── app.js Optional custom JavaScript
```
### Required HTML Head
Every HTML page MUST include:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Title</title>
<link rel="stylesheet" href="styles.css">
<script src="/js/vendor/htmx.min.js"></script>
<script src="designer.js" defer></script>
</head>
```
---
## RESPONSE FORMAT
When generating an app, respond with JSON:
```json
{
"name": "app-name-lowercase-dashes",
"description": "What this app does",
"domain": "custom|healthcare|sales|inventory|booking|etc",
"tables": [
{
"name": "table_name",
"fields": [
{"name": "id", "type": "guid", "nullable": false},
{"name": "created_at", "type": "datetime", "nullable": false},
{"name": "updated_at", "type": "datetime", "nullable": false},
{"name": "field_name", "type": "string", "nullable": true}
]
}
],
"pages": [
{
"filename": "index.html",
"title": "Dashboard",
"html": "complete HTML document"
}
],
"tools": [
{
"name": "tool_name",
"triggers": ["phrase1", "phrase2"],
"basic_code": "BASIC code"
}
],
"schedulers": [
{
"name": "scheduler_name",
"schedule": "0 9 * * *",
"basic_code": "BASIC code"
}
],
"css": "complete CSS styles",
"custom_js": "optional JavaScript"
}
```
### Field Types
- `guid` - UUID primary key
- `string` - VARCHAR(255)
- `text` - TEXT (long content)
- `integer` - INT
- `decimal` - DECIMAL(10,2)
- `boolean` - BOOLEAN
- `date` - DATE
- `datetime` - TIMESTAMP
- `json` - JSONB
---
## EXAMPLES
### Simple Calculator (No Database)
User: "Create a pink calculator"
Response: Beautiful calculator UI with pink theme, working JavaScript calculations, no tables needed.
### CRM Application (With Database)
User: "Create a CRM for managing customers and deals"
Response:
- Tables: customers, deals, activities, notes
- Pages: Dashboard with stats, customer list/form, deal pipeline, activity log
- Tools: "add customer", "log activity"
- CSS: Professional business theme
### Booking System
User: "Create appointment booking for a dental clinic"
Response:
- Tables: patients, dentists, appointments, services
- Pages: Calendar view, patient list, appointment form
- Schedulers: Daily reminder emails, weekly availability report
- Tools: "book appointment", "check availability"
---
## IMPORTANT RULES
1. **Always use HTMX** for API calls - NO fetch() or XMLHttpRequest in HTML
2. **Include designer.js** in all pages for AI modification capability
3. **Make it beautiful** - Use modern CSS, proper spacing, nice colors
4. **Make it functional** - All buttons should work, forms should submit
5. **Use the APIs** - Connect to /api/db/ for data operations
6. **Be complete** - Generate all necessary pages, not just stubs
7. **Match the request** - If user wants pink, make it pink
8. **Tables are optional** - Simple tools don't need database tables

View file

@ -119,11 +119,8 @@ impl AppGenerator {
let pages = self.generate_htmx_pages(&structure)?;
trace!("Generated {} pages", pages.len());
// Get bot name for S3 bucket
let bot_name = self.get_bot_name(session.bot_id)?;
let bucket_name = format!("{}.gbai", bot_name.to_lowercase());
// Write to S3 drive: {bucket}/.gbdrive/apps/{app_name}/
let drive_app_path = format!(".gbdrive/apps/{}", structure.name);
for page in &pages {
@ -132,6 +129,14 @@ impl AppGenerator {
.await?;
}
let designer_js = self.generate_designer_js();
self.write_to_drive(
&bucket_name,
&format!("{}/designer.js", drive_app_path),
&designer_js,
)
.await?;
let css_content = self.generate_app_css();
self.write_to_drive(
&bucket_name,
@ -140,7 +145,6 @@ impl AppGenerator {
)
.await?;
// Tools go to {bucket}/.gbdialog/tools/
let tools = self.generate_tools(&structure)?;
for tool in &tools {
let tool_path = format!(".gbdialog/tools/{}", tool.filename);
@ -148,7 +152,6 @@ impl AppGenerator {
.await?;
}
// Schedulers go to {bucket}/.gbdialog/schedulers/
let schedulers = self.generate_schedulers(&structure)?;
for scheduler in &schedulers {
let scheduler_path = format!(".gbdialog/schedulers/{}", scheduler.filename);
@ -156,7 +159,6 @@ impl AppGenerator {
.await?;
}
// Sync app to SITE_ROOT for serving
self.sync_app_to_site_root(&bucket_name, &structure.name, session.bot_id)
.await?;
@ -177,40 +179,130 @@ impl AppGenerator {
})
}
/// Use LLM to analyze app requirements and generate structure
fn get_platform_capabilities_prompt(&self) -> &'static str {
r##"
GENERAL BOTS APP GENERATOR - PLATFORM CAPABILITIES
AVAILABLE REST APIs:
DATABASE API (/api/db/):
- GET /api/db/TABLE - List records (query: limit, offset, order_by, order_dir, search, field=value)
- GET /api/db/TABLE/ID - Get single record
- GET /api/db/TABLE/count - Get record count
- POST /api/db/TABLE - Create record (JSON body)
- PUT /api/db/TABLE/ID - Update record (JSON body)
- DELETE /api/db/TABLE/ID - Delete record
FILE STORAGE API (/api/drive/):
- GET /api/drive/list?path=/folder - List files
- GET /api/drive/download?path=/file.ext - Download file
- POST /api/drive/upload - Upload (multipart: file, path)
- DELETE /api/drive/delete?path=/file.ext - Delete file
AUTOTASK API (/api/autotask/):
- POST /api/autotask/create - Create task {"intent": "..."}
- GET /api/autotask/list - List tasks
- GET /api/autotask/stats - Get statistics
- GET /api/autotask/pending - Get pending items
DESIGNER API (/api/designer/):
- POST /api/designer/modify - AI modify app {"app_name", "current_page", "message"}
COMMUNICATION APIs:
- POST /api/mail/send - {"to", "subject", "body"}
- POST /api/whatsapp/send - {"to": "+123...", "message"}
- POST /api/llm/generate - {"prompt", "max_tokens"}
- POST /api/llm/image - {"prompt", "size"}
HTMX ATTRIBUTES:
- hx-get, hx-post, hx-put, hx-delete - HTTP methods
- hx-target="#id" - Where to put response
- hx-swap="innerHTML|outerHTML|beforeend|delete" - How to insert
- hx-trigger="click|submit|load|every 5s|keyup changed delay:300ms" - When to fire
- hx-indicator="#spinner" - Loading indicator
- hx-confirm="Are you sure?" - Confirmation dialog
BASIC AUTOMATION (.bas files):
Tools (.gbdialog/tools/*.bas):
HEAR "phrase1", "phrase2"
result = GET FROM "table"
TALK "Response: " + result
END HEAR
Schedulers (.gbdialog/schedulers/*.bas):
SET SCHEDULE "0 9 * * *"
data = GET FROM "reports"
SEND MAIL TO "email" WITH SUBJECT "Daily" BODY data
END SCHEDULE
BASIC KEYWORDS:
- TALK "message" - Send message
- ASK "question" - Get user input
- GET FROM "table" WHERE field=val - Query database
- SAVE TO "table" WITH field1, field2 - Insert record
- SEND MAIL TO "x" WITH SUBJECT "y" BODY "z"
- result = LLM "prompt" - AI text generation
- image = GENERATE IMAGE "prompt" - AI image generation
REQUIRED HTML HEAD (all pages must include):
- link rel="stylesheet" href="styles.css"
- script src="/js/vendor/htmx.min.js"
- script src="designer.js" defer
FIELD TYPES: guid, string, text, integer, decimal, boolean, date, datetime, json
RULES:
1. Always use HTMX for API calls - NO fetch() in HTML
2. Include designer.js in ALL pages
3. Make it beautiful and fully functional
4. Tables are optional - simple apps (calculator, timer) dont need them
"##
}
async fn analyze_app_requirements_with_llm(
&self,
intent: &str,
) -> Result<AppStructure, Box<dyn std::error::Error + Send + Sync>> {
let capabilities = self.get_platform_capabilities_prompt();
let prompt = format!(
r#"Analyze this user request and design an application structure.
r#"You are an expert app generator for General Bots platform.
User Request: "{intent}"
{capabilities}
Generate a JSON response with the application structure:
USER REQUEST: "{intent}"
Generate a complete application. For simple apps (calculator, timer, game), you can use empty tables array.
For data apps (CRM, inventory), design appropriate tables.
Respond with JSON:
{{
"name": "short_app_name (lowercase, no spaces)",
"description": "Brief description of the app",
"domain": "industry domain (healthcare, sales, inventory, booking, etc.)",
"name": "app-name-lowercase-dashes",
"description": "What this app does",
"domain": "custom|healthcare|sales|inventory|booking|etc",
"tables": [
{{
"name": "table_name",
"fields": [
{{"name": "field_name", "type": "string|integer|decimal|boolean|date|datetime|text|guid", "nullable": true/false, "reference": "other_table or null"}}
{{"name": "id", "type": "guid", "nullable": false}},
{{"name": "field_name", "type": "string|integer|decimal|boolean|date|datetime|text", "nullable": true, "reference": "other_table or null"}}
]
}}
],
"features": ["crud", "search", "dashboard", "reports", "etc"]
"features": ["list of features"],
"custom_html": "OPTIONAL: For non-CRUD apps like calculators, provide complete HTML here",
"custom_css": "OPTIONAL: Custom CSS styles",
"custom_js": "OPTIONAL: Custom JavaScript"
}}
Guidelines:
- Every table should have id (guid), created_at (datetime), updated_at (datetime)
- Use snake_case for table and field names
- Include relationships between tables using _id suffix fields
- Design 2-5 tables based on the request complexity
- Include relevant fields for the domain
IMPORTANT:
- For simple tools (calculator, converter, timer): use custom_html/css/js, tables can be empty []
- For data apps (CRM, booking): design tables, custom_* fields are optional
- Always include id, created_at, updated_at in tables
- Make it beautiful and fully functional
Respond ONLY with valid JSON."#
Respond with valid JSON only."#
);
let response = self.call_llm(&prompt).await?;
@ -1002,6 +1094,7 @@ Respond ONLY with valid JSON."#
let _ = writeln!(html, " <title>{}</title>", structure.name);
let _ = writeln!(html, " <link rel=\"stylesheet\" href=\"styles.css\">");
let _ = writeln!(html, " <script src=\"/js/vendor/htmx.min.js\"></script>");
let _ = writeln!(html, " <script src=\"designer.js\" defer></script>");
let _ = writeln!(html, "</head>");
let _ = writeln!(html, "<body>");
let _ = writeln!(html, " <header class=\"app-header\">");
@ -1048,6 +1141,7 @@ Respond ONLY with valid JSON."#
let _ = writeln!(html, " <title>{table_name} - List</title>");
let _ = writeln!(html, " <link rel=\"stylesheet\" href=\"styles.css\">");
let _ = writeln!(html, " <script src=\"/js/vendor/htmx.min.js\"></script>");
let _ = writeln!(html, " <script src=\"designer.js\" defer></script>");
let _ = writeln!(html, "</head>");
let _ = writeln!(html, "<body>");
let _ = writeln!(html, " <header class=\"page-header\">");
@ -1103,10 +1197,11 @@ Respond ONLY with valid JSON."#
let _ = writeln!(html, " <title>{table_name} - Form</title>");
let _ = writeln!(html, " <link rel=\"stylesheet\" href=\"styles.css\">");
let _ = writeln!(html, " <script src=\"/js/vendor/htmx.min.js\"></script>");
let _ = writeln!(html, " <script src=\"designer.js\" defer></script>");
let _ = writeln!(html, "</head>");
let _ = writeln!(html, "<body>");
let _ = writeln!(html, " <header class=\"page-header\">");
let _ = writeln!(html, " <h1>Add {table_name}</h1>");
let _ = writeln!(html, " <h1>New {table_name}</h1>");
let _ = writeln!(
html,
" <a href=\"{table_name}.html\" class=\"btn\">Back to List</a>"
@ -1201,7 +1296,6 @@ input[type="search"] { width: 100%; max-width: 300px; padding: 0.5rem; border: 1
&self,
_structure: &AppStructure,
) -> Result<Vec<GeneratedScript>, Box<dyn std::error::Error + Send + Sync>> {
// LLM generates actual tool content based on app requirements
Ok(Vec::new())
}
@ -1209,11 +1303,92 @@ input[type="search"] { width: 100%; max-width: 300px; padding: 0.5rem; border: 1
&self,
_structure: &AppStructure,
) -> Result<Vec<GeneratedScript>, Box<dyn std::error::Error + Send + Sync>> {
// LLM generates actual scheduler content based on app requirements
Ok(Vec::new())
}
/// Get site path from config
fn generate_designer_js(&self) -> String {
r#"(function() {
const style = document.createElement('style');
style.textContent = `
.designer-btn { position: fixed; bottom: 20px; right: 20px; width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; cursor: pointer; box-shadow: 0 4px 20px rgba(102,126,234,0.4); font-size: 24px; z-index: 9999; transition: transform 0.2s, box-shadow 0.2s; }
.designer-btn:hover { transform: scale(1.1); box-shadow: 0 6px 30px rgba(102,126,234,0.6); }
.designer-panel { position: fixed; bottom: 90px; right: 20px; width: 380px; max-height: 500px; background: white; border-radius: 16px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); z-index: 9998; display: none; flex-direction: column; overflow: hidden; }
.designer-panel.open { display: flex; }
.designer-header { padding: 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: 600; display: flex; justify-content: space-between; align-items: center; }
.designer-close { background: none; border: none; color: white; font-size: 20px; cursor: pointer; }
.designer-messages { flex: 1; overflow-y: auto; padding: 16px; max-height: 300px; }
.designer-msg { margin: 8px 0; padding: 10px 14px; border-radius: 12px; max-width: 85%; }
.designer-msg.user { background: #667eea; color: white; margin-left: auto; }
.designer-msg.ai { background: #f0f0f0; color: #333; }
.designer-input { display: flex; padding: 12px; border-top: 1px solid #eee; gap: 8px; }
.designer-input input { flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 20px; outline: none; }
.designer-input button { padding: 10px 16px; background: #667eea; color: white; border: none; border-radius: 20px; cursor: pointer; }
`;
document.head.appendChild(style);
const btn = document.createElement('button');
btn.className = 'designer-btn';
btn.innerHTML = '🎨';
btn.title = 'Designer AI';
document.body.appendChild(btn);
const panel = document.createElement('div');
panel.className = 'designer-panel';
panel.innerHTML = `
<div class="designer-header">
<span>🎨 Designer AI</span>
<button class="designer-close">×</button>
</div>
<div class="designer-messages">
<div class="designer-msg ai">Hi! I can help you modify this app. What would you like to change?</div>
</div>
<div class="designer-input">
<input type="text" placeholder="e.g., Make the header blue..." />
<button>Send</button>
</div>
`;
document.body.appendChild(panel);
btn.onclick = () => panel.classList.toggle('open');
panel.querySelector('.designer-close').onclick = () => panel.classList.remove('open');
const input = panel.querySelector('input');
const sendBtn = panel.querySelector('.designer-input button');
const messages = panel.querySelector('.designer-messages');
const appName = window.location.pathname.split('/')[2] || 'app';
const currentPage = window.location.pathname.split('/').pop() || 'index.html';
async function sendMessage() {
const msg = input.value.trim();
if (!msg) return;
messages.innerHTML += `<div class="designer-msg user">${msg}</div>`;
input.value = '';
messages.scrollTop = messages.scrollHeight;
try {
const res = await fetch('/api/designer/modify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ app_name: appName, current_page: currentPage, message: msg })
});
const data = await res.json();
messages.innerHTML += `<div class="designer-msg ai">${data.message || 'Done!'}</div>`;
if (data.success && data.changes && data.changes.length > 0) {
setTimeout(() => location.reload(), 1000);
}
} catch (e) {
messages.innerHTML += `<div class="designer-msg ai">Sorry, something went wrong. Try again.</div>`;
}
messages.scrollTop = messages.scrollHeight;
}
sendBtn.onclick = sendMessage;
input.onkeypress = (e) => { if (e.key === 'Enter') sendMessage(); };
})();"#.to_string()
}
fn get_site_path(&self) -> String {
self.state
.config

View file

@ -12,6 +12,9 @@ use axum::{
Json,
};
use chrono::Utc;
use diesel::prelude::*;
use diesel::sql_query;
use diesel::sql_types::{Text, Uuid as DieselUuid};
use log::{error, info, trace};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
@ -54,6 +57,33 @@ pub struct IntentResultResponse {
pub next_steps: Vec<String>,
}
/// Request for one-click create and execute
#[derive(Debug, Deserialize)]
pub struct CreateAndExecuteRequest {
pub intent: String,
}
/// Response for create and execute - simple status updates
#[derive(Debug, Serialize)]
pub struct CreateAndExecuteResponse {
pub success: bool,
pub task_id: String,
pub status: String,
pub message: String,
pub app_url: Option<String>,
pub created_resources: Vec<CreatedResourceResponse>,
pub pending_items: Vec<PendingItemResponse>,
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct PendingItemResponse {
pub id: String,
pub label: String,
pub config_key: String,
pub reason: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CreatedResourceResponse {
pub resource_type: String,
@ -259,6 +289,105 @@ pub struct RecommendationResponse {
pub action: Option<String>,
}
/// Create and execute in one call - no dialogs, just do it
/// POST /api/autotask/create
pub async fn create_and_execute_handler(
State(state): State<Arc<AppState>>,
Json(request): Json<CreateAndExecuteRequest>,
) -> impl IntoResponse {
info!(
"Create and execute: {}",
&request.intent[..request.intent.len().min(100)]
);
let session = match get_current_session(&state) {
Ok(s) => s,
Err(e) => {
return (
StatusCode::UNAUTHORIZED,
Json(CreateAndExecuteResponse {
success: false,
task_id: String::new(),
status: "error".to_string(),
message: format!("Authentication error: {}", e),
app_url: None,
created_resources: Vec::new(),
pending_items: Vec::new(),
error: Some(e.to_string()),
}),
);
}
};
// Create task record first
let task_id = Uuid::new_v4();
if let Err(e) = create_task_record(&state, task_id, &session, &request.intent) {
error!("Failed to create task record: {}", e);
}
// Update status to running
let _ = update_task_status_db(&state, task_id, "running", None);
// Use IntentClassifier to classify and process
let classifier = IntentClassifier::new(Arc::clone(&state));
match classifier
.classify_and_process(&request.intent, &session)
.await
{
Ok(result) => {
let status = if result.success {
"completed"
} else {
"failed"
};
let _ = update_task_status_db(&state, task_id, status, result.error.as_deref());
// Get any pending items (ASK LATER)
let pending_items = get_pending_items_for_bot(&state, session.bot_id);
(
StatusCode::OK,
Json(CreateAndExecuteResponse {
success: result.success,
task_id: task_id.to_string(),
status: status.to_string(),
message: result.message,
app_url: result.app_url,
created_resources: result
.created_resources
.into_iter()
.map(|r| CreatedResourceResponse {
resource_type: r.resource_type,
name: r.name,
path: r.path,
})
.collect(),
pending_items,
error: result.error,
}),
)
}
Err(e) => {
let _ = update_task_status_db(&state, task_id, "failed", Some(&e.to_string()));
error!("Failed to process intent: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(CreateAndExecuteResponse {
success: false,
task_id: task_id.to_string(),
status: "failed".to_string(),
message: "Failed to process request".to_string(),
app_url: None,
created_resources: Vec::new(),
pending_items: Vec::new(),
error: Some(e.to_string()),
}),
)
}
}
}
/// Classify and optionally process an intent
/// POST /api/autotask/classify
pub async fn classify_intent_handler(
@ -1244,30 +1373,162 @@ fn start_task_execution(
}
fn list_auto_tasks(
_state: &Arc<AppState>,
_filter: &str,
_limit: i32,
_offset: i32,
state: &Arc<AppState>,
filter: &str,
limit: i32,
offset: i32,
) -> Result<Vec<AutoTask>, Box<dyn std::error::Error + Send + Sync>> {
Ok(Vec::new())
let mut conn = state.conn.get()?;
let status_filter = match filter {
"running" => Some("running"),
"pending" => Some("pending"),
"completed" => Some("completed"),
"failed" => Some("failed"),
_ => None,
};
#[derive(QueryableByName)]
struct TaskRow {
#[diesel(sql_type = Text)]
id: String,
#[diesel(sql_type = Text)]
title: String,
#[diesel(sql_type = Text)]
intent: String,
#[diesel(sql_type = Text)]
status: String,
#[diesel(sql_type = diesel::sql_types::Float8)]
progress: f64,
}
let query = if let Some(status) = status_filter {
format!(
"SELECT id::text, title, intent, status, progress FROM auto_tasks WHERE status = '{}' ORDER BY created_at DESC LIMIT {} OFFSET {}",
status, limit, offset
)
} else {
format!(
"SELECT id::text, title, intent, status, progress FROM auto_tasks ORDER BY created_at DESC LIMIT {} OFFSET {}",
limit, offset
)
};
let rows: Vec<TaskRow> = sql_query(&query).get_results(&mut conn).unwrap_or_default();
Ok(rows
.into_iter()
.map(|r| AutoTask {
id: r.id,
title: r.title,
intent: r.intent,
status: match r.status.as_str() {
"running" => AutoTaskStatus::Running,
"completed" => AutoTaskStatus::Completed,
"failed" => AutoTaskStatus::Failed,
"paused" => AutoTaskStatus::Paused,
"cancelled" => AutoTaskStatus::Cancelled,
_ => AutoTaskStatus::Draft,
},
mode: ExecutionMode::FullyAutomatic,
priority: TaskPriority::Medium,
plan_id: None,
basic_program: None,
current_step: 0,
total_steps: 0,
progress: r.progress,
step_results: Vec::new(),
pending_decisions: Vec::new(),
pending_approvals: Vec::new(),
risk_summary: None,
resource_usage: Default::default(),
error: None,
rollback_state: None,
session_id: String::new(),
bot_id: String::new(),
created_by: String::new(),
assigned_to: String::new(),
schedule: None,
tags: Vec::new(),
parent_task_id: None,
subtask_ids: Vec::new(),
depends_on: Vec::new(),
dependents: Vec::new(),
mcp_servers: Vec::new(),
external_apis: Vec::new(),
created_at: Utc::now(),
updated_at: Utc::now(),
started_at: None,
completed_at: None,
estimated_completion: None,
})
.collect())
}
fn get_auto_task_stats(
_state: &Arc<AppState>,
state: &Arc<AppState>,
) -> Result<AutoTaskStatsResponse, Box<dyn std::error::Error + Send + Sync>> {
let mut conn = state.conn.get()?;
#[derive(QueryableByName)]
struct CountRow {
#[diesel(sql_type = diesel::sql_types::BigInt)]
count: i64,
}
let total: i64 = sql_query("SELECT COUNT(*) as count FROM auto_tasks")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let running: i64 =
sql_query("SELECT COUNT(*) as count FROM auto_tasks WHERE status = 'running'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let pending: i64 =
sql_query("SELECT COUNT(*) as count FROM auto_tasks WHERE status = 'pending'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let completed: i64 =
sql_query("SELECT COUNT(*) as count FROM auto_tasks WHERE status = 'completed'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let failed: i64 = sql_query("SELECT COUNT(*) as count FROM auto_tasks WHERE status = 'failed'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let pending_approval: i64 =
sql_query("SELECT COUNT(*) as count FROM task_approvals WHERE status = 'pending'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
let pending_decision: i64 =
sql_query("SELECT COUNT(*) as count FROM task_decisions WHERE status = 'pending'")
.get_result::<CountRow>(&mut conn)
.map(|r| r.count)
.unwrap_or(0);
Ok(AutoTaskStatsResponse {
total: 0,
running: 0,
pending: 0,
completed: 0,
failed: 0,
pending_approval: 0,
pending_decision: 0,
total: total as i32,
running: running as i32,
pending: pending as i32,
completed: completed as i32,
failed: failed as i32,
pending_approval: pending_approval as i32,
pending_decision: pending_decision as i32,
})
}
fn update_task_status(
_state: &Arc<AppState>,
state: &Arc<AppState>,
task_id: &str,
status: AutoTaskStatus,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@ -1275,9 +1536,126 @@ fn update_task_status(
"Updating task status task_id={} status={:?}",
task_id, status
);
let status_str = match status {
AutoTaskStatus::Running => "running",
AutoTaskStatus::Completed => "completed",
AutoTaskStatus::Failed => "failed",
AutoTaskStatus::Paused => "paused",
AutoTaskStatus::Cancelled => "cancelled",
_ => "pending",
};
let mut conn = state.conn.get()?;
sql_query("UPDATE auto_tasks SET status = $1, updated_at = NOW() WHERE id = $2::uuid")
.bind::<Text, _>(status_str)
.bind::<Text, _>(task_id)
.execute(&mut conn)?;
Ok(())
}
/// Create task record in database
fn create_task_record(
state: &Arc<AppState>,
task_id: Uuid,
session: &crate::shared::models::UserSession,
intent: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut conn = state.conn.get()?;
let title = if intent.len() > 100 {
format!("{}...", &intent[..97])
} else {
intent.to_string()
};
sql_query(
"INSERT INTO auto_tasks (id, bot_id, session_id, title, intent, status, execution_mode, priority, progress, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, 'pending', 'autonomous', 'normal', 0.0, NOW(), NOW())"
)
.bind::<DieselUuid, _>(task_id)
.bind::<DieselUuid, _>(session.bot_id)
.bind::<DieselUuid, _>(session.id)
.bind::<Text, _>(&title)
.bind::<Text, _>(intent)
.execute(&mut conn)?;
Ok(())
}
/// Update task status in database
fn update_task_status_db(
state: &Arc<AppState>,
task_id: Uuid,
status: &str,
error: Option<&str>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut conn = state.conn.get()?;
if let Some(err) = error {
sql_query(
"UPDATE auto_tasks SET status = $1, error = $2, updated_at = NOW() WHERE id = $3",
)
.bind::<Text, _>(status)
.bind::<Text, _>(err)
.bind::<DieselUuid, _>(task_id)
.execute(&mut conn)?;
} else {
let completed_at = if status == "completed" || status == "failed" {
", completed_at = NOW()"
} else {
""
};
let query = format!(
"UPDATE auto_tasks SET status = $1, updated_at = NOW(){} WHERE id = $2",
completed_at
);
sql_query(&query)
.bind::<Text, _>(status)
.bind::<DieselUuid, _>(task_id)
.execute(&mut conn)?;
}
Ok(())
}
/// Get pending items (ASK LATER) for a bot
fn get_pending_items_for_bot(state: &Arc<AppState>, bot_id: Uuid) -> Vec<PendingItemResponse> {
let mut conn = match state.conn.get() {
Ok(c) => c,
Err(_) => return Vec::new(),
};
#[derive(QueryableByName)]
struct PendingRow {
#[diesel(sql_type = Text)]
id: String,
#[diesel(sql_type = Text)]
field_label: String,
#[diesel(sql_type = Text)]
config_key: String,
#[diesel(sql_type = diesel::sql_types::Nullable<Text>)]
reason: Option<String>,
}
let rows: Vec<PendingRow> = sql_query(
"SELECT id::text, field_label, config_key, reason FROM pending_info WHERE bot_id = $1 AND is_filled = false"
)
.bind::<DieselUuid, _>(bot_id)
.get_results(&mut conn)
.unwrap_or_default();
rows.into_iter()
.map(|r| PendingItemResponse {
id: r.id,
label: r.field_label,
config_key: r.config_key,
reason: r.reason,
})
.collect()
}
fn simulate_task_execution(
_state: &Arc<AppState>,
safety_layer: &SafetyLayer,
@ -1485,6 +1863,112 @@ fn apply_recommendation(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
info!("Applying recommendation: {}", rec_id);
// TODO: Implement recommendation application logic
// This would modify the execution plan based on the recommendation
Ok(())
}
#[derive(Debug, Serialize)]
pub struct PendingItemsResponse {
pub items: Vec<PendingItemResponse>,
pub count: usize,
}
pub async fn get_pending_items_handler(State(state): State<Arc<AppState>>) -> impl IntoResponse {
info!("Getting pending items");
let session = match get_current_session(&state) {
Ok(s) => s,
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(PendingItemsResponse {
items: Vec::new(),
count: 0,
}),
)
.into_response();
}
};
let items = get_pending_items_for_bot(&state, session.bot_id);
(
StatusCode::OK,
Json(PendingItemsResponse {
count: items.len(),
items,
}),
)
.into_response()
}
#[derive(Debug, Deserialize)]
pub struct SubmitPendingItemRequest {
pub value: String,
}
pub async fn submit_pending_item_handler(
State(state): State<Arc<AppState>>,
Path(item_id): Path<String>,
Json(request): Json<SubmitPendingItemRequest>,
) -> impl IntoResponse {
info!("Submitting pending item {item_id}: {}", request.value);
let session = match get_current_session(&state) {
Ok(s) => s,
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(serde_json::json!({
"success": false,
"error": "Authentication required"
})),
)
.into_response();
}
};
match resolve_pending_item(&state, &item_id, &request.value, session.bot_id) {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({
"success": true,
"message": "Pending item resolved"
})),
)
.into_response(),
Err(e) => {
error!("Failed to resolve pending item {item_id}: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"success": false,
"error": e.to_string()
})),
)
.into_response()
}
}
}
fn resolve_pending_item(
state: &Arc<AppState>,
item_id: &str,
value: &str,
bot_id: Uuid,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut conn = state.conn.get()?;
let item_uuid = Uuid::parse_str(item_id)?;
sql_query(
"UPDATE pending_info SET resolved = true, resolved_value = $1, resolved_at = NOW()
WHERE id = $2 AND bot_id = $3",
)
.bind::<Text, _>(value)
.bind::<DieselUuid, _>(item_uuid)
.bind::<DieselUuid, _>(bot_id)
.execute(&mut conn)?;
info!("Resolved pending item {item_id} with value: {value}");
Ok(())
}

View file

@ -102,16 +102,18 @@ pub use safety_layer::{AuditEntry, ConstraintCheckResult, SafetyLayer, Simulatio
pub use autotask_api::{
apply_recommendation_handler, cancel_task_handler, classify_intent_handler,
compile_intent_handler, execute_plan_handler, execute_task_handler, get_approvals_handler,
get_decisions_handler, get_stats_handler, get_task_logs_handler, list_tasks_handler,
pause_task_handler, resume_task_handler, simulate_plan_handler, simulate_task_handler,
submit_approval_handler, submit_decision_handler,
compile_intent_handler, create_and_execute_handler, execute_plan_handler, execute_task_handler,
get_approvals_handler, get_decisions_handler, get_pending_items_handler, get_stats_handler,
get_task_logs_handler, list_tasks_handler, pause_task_handler, resume_task_handler,
simulate_plan_handler, simulate_task_handler, submit_approval_handler, submit_decision_handler,
submit_pending_item_handler,
};
pub fn configure_autotask_routes() -> axum::Router<std::sync::Arc<crate::shared::state::AppState>> {
use axum::routing::{get, post};
axum::Router::new()
.route("/api/autotask/create", post(create_and_execute_handler))
.route("/api/autotask/classify", post(classify_intent_handler))
.route("/api/autotask/compile", post(compile_intent_handler))
.route("/api/autotask/execute", post(execute_plan_handler))
@ -150,6 +152,11 @@ pub fn configure_autotask_routes() -> axum::Router<std::sync::Arc<crate::shared:
"/api/autotask/recommendations/:rec_id/apply",
post(apply_recommendation_handler),
)
.route("/api/autotask/pending", get(get_pending_items_handler))
.route(
"/api/autotask/pending/:item_id",
post(submit_pending_item_handler),
)
}
pub fn get_all_keywords() -> Vec<String> {

View file

@ -77,6 +77,7 @@ pub fn configure_designer_routes() -> Router<Arc<AppState>> {
get(handle_list_dialogs).post(handle_create_dialog),
)
.route("/api/designer/dialogs/{id}", get(handle_get_dialog))
.route("/api/designer/modify", post(handle_designer_modify))
}
pub async fn handle_list_files(State(state): State<Arc<AppState>>) -> impl IntoResponse {
@ -742,3 +743,365 @@ fn html_escape(s: &str) -> String {
.replace('"', "&quot;")
.replace('\'', "&#39;")
}
#[derive(Debug, Clone, Deserialize)]
pub struct DesignerModifyRequest {
pub app_name: String,
pub current_page: Option<String>,
pub message: String,
pub context: Option<DesignerContext>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DesignerContext {
pub page_html: Option<String>,
pub tables: Option<Vec<String>>,
pub recent_changes: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DesignerModifyResponse {
pub success: bool,
pub message: String,
pub changes: Vec<DesignerChange>,
pub suggestions: Vec<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DesignerChange {
pub change_type: String,
pub file_path: String,
pub description: String,
pub preview: Option<String>,
}
pub async fn handle_designer_modify(
State(state): State<Arc<AppState>>,
Json(request): Json<DesignerModifyRequest>,
) -> impl IntoResponse {
let app = &request.app_name;
let msg_preview = &request.message[..request.message.len().min(100)];
log::info!("Designer modify request for app '{app}': {msg_preview}");
let session = match get_designer_session(&state) {
Ok(s) => s,
Err(e) => {
return (
axum::http::StatusCode::UNAUTHORIZED,
Json(DesignerModifyResponse {
success: false,
message: "Authentication required".to_string(),
changes: Vec::new(),
suggestions: Vec::new(),
error: Some(e.to_string()),
}),
);
}
};
match process_designer_modification(&state, &request, &session).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)),
Err(e) => {
log::error!("Designer modification failed: {e}");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(DesignerModifyResponse {
success: false,
message: "Failed to process modification".to_string(),
changes: Vec::new(),
suggestions: Vec::new(),
error: Some(e.to_string()),
}),
)
}
}
}
fn get_designer_session(
state: &AppState,
) -> Result<crate::shared::models::UserSession, Box<dyn std::error::Error + Send + Sync>> {
use crate::shared::models::schema::bots::dsl::*;
use crate::shared::models::UserSession;
let mut conn = state.conn.get()?;
let bot_result: Result<(Uuid, String), _> = bots.select((id, name)).first(&mut conn);
match bot_result {
Ok((bot_id_val, _bot_name_val)) => Ok(UserSession {
id: Uuid::new_v4(),
user_id: Uuid::nil(),
bot_id: bot_id_val,
title: "designer".to_string(),
context_data: serde_json::json!({}),
current_tool: None,
created_at: Utc::now(),
updated_at: Utc::now(),
}),
Err(_) => Err("No bot found for designer session".into()),
}
}
async fn process_designer_modification(
state: &AppState,
request: &DesignerModifyRequest,
session: &crate::shared::models::UserSession,
) -> Result<DesignerModifyResponse, Box<dyn std::error::Error + Send + Sync>> {
let prompt = build_designer_prompt(request);
let llm_response = call_designer_llm(state, &prompt).await?;
let (changes, message, suggestions) =
parse_and_apply_changes(state, request, &llm_response, session).await?;
Ok(DesignerModifyResponse {
success: true,
message,
changes,
suggestions,
error: None,
})
}
fn build_designer_prompt(request: &DesignerModifyRequest) -> String {
let context_info = request
.context
.as_ref()
.map(|ctx| {
let mut info = String::new();
if let Some(ref html) = ctx.page_html {
info.push_str(&format!(
"\nCurrent page HTML (first 500 chars):\n{}\n",
&html[..html.len().min(500)]
));
}
if let Some(ref tables) = ctx.tables {
info.push_str(&format!("\nAvailable tables: {}\n", tables.join(", ")));
}
info
})
.unwrap_or_default();
format!(
r#"You are a Designer AI assistant helping modify an HTMX-based application.
App Name: {}
Current Page: {}
{}
User Request: "{}"
Analyze the request and respond with JSON describing the changes needed:
{{
"understanding": "brief description of what user wants",
"changes": [
{{
"type": "modify_html|add_field|remove_field|add_table|modify_style|add_page",
"file": "filename.html or styles.css",
"description": "what this change does",
"code": "the new/modified code snippet"
}}
],
"message": "friendly response to user explaining what was done",
"suggestions": ["optional follow-up suggestions"]
}}
Guidelines:
- Use HTMX attributes (hx-get, hx-post, hx-target, hx-swap, hx-trigger)
- Keep styling minimal and consistent
- API endpoints follow pattern: /api/db/{{table_name}}
- Forms should use hx-post for submissions
- Lists should use hx-get with pagination
Respond with valid JSON only."#,
request.app_name,
request.current_page.as_deref().unwrap_or("index.html"),
context_info,
request.message
)
}
async fn call_designer_llm(
_state: &AppState,
prompt: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let llm_url = std::env::var("LLM_URL").unwrap_or_else(|_| "http://localhost:11434".to_string());
let llm_model = std::env::var("LLM_MODEL").unwrap_or_else(|_| "llama3.2".to_string());
let client = reqwest::Client::new();
let response = client
.post(format!("{}/api/generate", llm_url))
.json(&serde_json::json!({
"model": llm_model,
"prompt": prompt,
"stream": false,
"options": {
"temperature": 0.3,
"num_predict": 2000
}
}))
.send()
.await?;
if !response.status().is_success() {
let status = response.status();
return Err(format!("LLM request failed: {status}").into());
}
let result: serde_json::Value = response.json().await?;
let response_text = result["response"].as_str().unwrap_or("{}").to_string();
let json_text = if response_text.contains("```json") {
response_text
.split("```json")
.nth(1)
.and_then(|s| s.split("```").next())
.unwrap_or(&response_text)
.trim()
.to_string()
} else if response_text.contains("```") {
response_text
.split("```")
.nth(1)
.unwrap_or(&response_text)
.trim()
.to_string()
} else {
response_text
};
Ok(json_text)
}
async fn parse_and_apply_changes(
state: &AppState,
request: &DesignerModifyRequest,
llm_response: &str,
session: &crate::shared::models::UserSession,
) -> Result<(Vec<DesignerChange>, String, Vec<String>), Box<dyn std::error::Error + Send + Sync>> {
#[derive(Deserialize)]
struct LlmChangeResponse {
understanding: Option<String>,
changes: Option<Vec<LlmChange>>,
message: Option<String>,
suggestions: Option<Vec<String>>,
}
#[derive(Deserialize)]
struct LlmChange {
#[serde(rename = "type")]
change_type: String,
file: String,
description: String,
code: Option<String>,
}
let parsed: LlmChangeResponse = serde_json::from_str(llm_response).unwrap_or(LlmChangeResponse {
understanding: Some("Could not parse LLM response".to_string()),
changes: None,
message: Some("I understood your request but encountered an issue processing it. Could you try rephrasing?".to_string()),
suggestions: Some(vec!["Try being more specific".to_string()]),
});
let mut applied_changes = Vec::new();
if let Some(changes) = parsed.changes {
for change in changes {
if let Some(ref code) = change.code {
match apply_file_change(state, &request.app_name, &change.file, code, session).await
{
Ok(()) => {
applied_changes.push(DesignerChange {
change_type: change.change_type,
file_path: change.file,
description: change.description,
preview: Some(code[..code.len().min(200)].to_string()),
});
}
Err(e) => {
let file = &change.file;
log::warn!("Failed to apply change to {file}: {e}");
}
}
}
}
}
let message = parsed.message.unwrap_or_else(|| {
if applied_changes.is_empty() {
"I couldn't make any changes. Could you provide more details?".to_string()
} else {
format!(
"Done! I made {} change(s) to your app.",
applied_changes.len()
)
}
});
let suggestions = parsed.suggestions.unwrap_or_default();
Ok((applied_changes, message, suggestions))
}
async fn apply_file_change(
state: &AppState,
app_name: &str,
file_name: &str,
content: &str,
session: &crate::shared::models::UserSession,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use crate::shared::models::schema::bots::dsl::*;
let mut conn = state.conn.get()?;
let bot_name_val: String = bots
.filter(id.eq(session.bot_id))
.select(name)
.first(&mut conn)?;
let bucket_name = format!("{}.gbai", bot_name_val.to_lowercase());
let file_path = format!(".gbdrive/apps/{app_name}/{file_name}");
if let Some(ref s3_client) = state.drive {
use aws_sdk_s3::primitives::ByteStream;
s3_client
.put_object()
.bucket(&bucket_name)
.key(&file_path)
.body(ByteStream::from(content.as_bytes().to_vec()))
.content_type(get_content_type(file_name))
.send()
.await?;
log::info!("Designer updated file: s3://{bucket_name}/{file_path}");
let site_path = state
.config
.as_ref()
.map(|c| c.site_path.clone())
.unwrap_or_else(|| "./botserver-stack/sites".to_string());
let local_path = format!("{site_path}/{app_name}/{file_name}");
if let Some(parent) = std::path::Path::new(&local_path).parent() {
let _ = std::fs::create_dir_all(parent);
}
std::fs::write(&local_path, content)?;
log::info!("Designer synced to local: {local_path}");
}
Ok(())
}
fn get_content_type(filename: &str) -> &'static str {
if filename.ends_with(".html") {
"text/html"
} else if filename.ends_with(".css") {
"text/css"
} else if filename.ends_with(".js") {
"application/javascript"
} else if filename.ends_with(".json") {
"application/json"
} else {
"text/plain"
}
}

View file

@ -212,6 +212,7 @@ async fn run_axum_server(
api_router = api_router.merge(botserver::designer::configure_designer_routes());
api_router = api_router.merge(botserver::basic::keywords::configure_db_routes());
api_router = api_router.merge(botserver::basic::keywords::configure_app_server_routes());
api_router = api_router.merge(botserver::basic::keywords::configure_autotask_routes());
#[cfg(feature = "whatsapp")]
{