From 68dd957421a1a420db6c4a8c2f21be734f6ef6b5 Mon Sep 17 00:00:00 2001
From: Mahmoud-Emad
Date: Thu, 21 Aug 2025 18:28:17 +0300
Subject: [PATCH] feat: add modular web UI features
- Enable `web` command to start UI server
- Centralize web server setup and static serving
- Implement modular UI for chat and script editor
- Refactor Heroprompt UI into its own module
- Introduce dynamic theme switching and mobile menu
---
cli/hero.v | 2 +-
examples/web/ui_demo.vsh | 18 +-
lib/core/herocmds/web.v | 184 +++----
lib/web/heroprompt/endpoints.v | 315 ------------
lib/web/heroprompt/server.v | 71 ---
lib/web/heroprompt/templates/index.html | 115 -----
lib/web/heroprompt/utils.v | 28 --
lib/web/ui/chat/endpoints.v | 21 +
.../{templates => chat/static}/css/chat.css | 0
.../ui/{templates => chat/static}/js/chat.js | 0
lib/web/ui/{ => chat}/templates/chat.html | 0
lib/web/ui/chat/utils.v | 4 +
lib/web/ui/endpoints.v | 244 ---------
lib/web/ui/factory.v | 471 ------------------
lib/web/ui/heroprompt/endpoints.v | 137 +++++
lib/web/ui/heroprompt/page.v | 21 +
.../static}/css/heroprompt.css | 0
.../static}/js/heroprompt.js | 0
.../templates/heroprompt.html | 0
lib/web/ui/heroprompt/utils.v | 3 +
lib/web/ui/heroscript/endpoints.v | 24 +
.../static}/css/heroscript.css | 0
.../static}/js/heroscript.js | 0
.../templates/heroscript_editor.html | 0
lib/web/ui/heroscript/utils.v | 4 +
lib/web/ui/menu.v | 42 ++
lib/web/ui/server.v | 402 +++++++--------
.../ui/{templates => static}/css/colors.css | 0
lib/web/ui/{templates => static}/css/main.css | 0
lib/web/ui/{templates => static}/js/theme.js | 0
.../{admin_layout.html => admin/layout.html} | 3 +-
lib/web/ui/utils.v | 81 +--
32 files changed, 596 insertions(+), 1594 deletions(-)
delete mode 100644 lib/web/heroprompt/endpoints.v
delete mode 100644 lib/web/heroprompt/server.v
delete mode 100644 lib/web/heroprompt/templates/index.html
delete mode 100644 lib/web/heroprompt/utils.v
create mode 100644 lib/web/ui/chat/endpoints.v
rename lib/web/ui/{templates => chat/static}/css/chat.css (100%)
rename lib/web/ui/{templates => chat/static}/js/chat.js (100%)
rename lib/web/ui/{ => chat}/templates/chat.html (100%)
create mode 100644 lib/web/ui/chat/utils.v
delete mode 100644 lib/web/ui/endpoints.v
delete mode 100644 lib/web/ui/factory.v
create mode 100644 lib/web/ui/heroprompt/endpoints.v
create mode 100644 lib/web/ui/heroprompt/page.v
rename lib/web/ui/{templates => heroprompt/static}/css/heroprompt.css (100%)
rename lib/web/ui/{templates => heroprompt/static}/js/heroprompt.js (100%)
rename lib/web/ui/{ => heroprompt}/templates/heroprompt.html (100%)
create mode 100644 lib/web/ui/heroprompt/utils.v
create mode 100644 lib/web/ui/heroscript/endpoints.v
rename lib/web/ui/{templates => heroscript/static}/css/heroscript.css (100%)
rename lib/web/ui/{templates => heroscript/static}/js/heroscript.js (100%)
rename lib/web/ui/{ => heroscript}/templates/heroscript_editor.html (100%)
create mode 100644 lib/web/ui/heroscript/utils.v
create mode 100644 lib/web/ui/menu.v
rename lib/web/ui/{templates => static}/css/colors.css (100%)
rename lib/web/ui/{templates => static}/css/main.css (100%)
rename lib/web/ui/{templates => static}/js/theme.js (100%)
rename lib/web/ui/templates/{admin_layout.html => admin/layout.html} (99%)
diff --git a/cli/hero.v b/cli/hero.v
index b32042ad..4e26b040 100644
--- a/cli/hero.v
+++ b/cli/hero.v
@@ -85,7 +85,7 @@ fn do() ! {
herocmds.cmd_git(mut cmd)
herocmds.cmd_generator(mut cmd)
herocmds.cmd_docusaurus(mut cmd)
- // herocmds.cmd_web(mut cmd)
+ herocmds.cmd_web(mut cmd)
cmd.setup()
cmd.parse(os.args)
diff --git a/examples/web/ui_demo.vsh b/examples/web/ui_demo.vsh
index 50711018..10f395eb 100755
--- a/examples/web/ui_demo.vsh
+++ b/examples/web/ui_demo.vsh
@@ -1,13 +1,13 @@
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
-// import freeflowuniverse.herolib.web.ui
+import freeflowuniverse.herolib.web.ui
-// fn main() {
-// println('Starting UI test server on port 8080...')
-// println('Visit http://localhost:8080 to see the admin interface')
+fn main() {
+ println('Starting UI test server on port 8080...')
+ println('Visit http://localhost:8080 to see the admin interface')
-// ui.start(
-// title: 'Test Admin Panel'
-// port: 8080
-// )!
-// }
+ ui.start(
+ title: 'Test Admin Panel'
+ port: 8080
+ )!
+}
diff --git a/lib/core/herocmds/web.v b/lib/core/herocmds/web.v
index 5ee52fd9..805c68f9 100644
--- a/lib/core/herocmds/web.v
+++ b/lib/core/herocmds/web.v
@@ -1,110 +1,110 @@
module herocmds
-// import freeflowuniverse.herolib.ui.console
-// import freeflowuniverse.herolib.web.ui
-// import os
-// import cli { Command, Flag }
-// import time
+import freeflowuniverse.herolib.ui.console
+import freeflowuniverse.herolib.web.ui
+import os
+import cli { Command, Flag }
+import time
-// pub fn cmd_web(mut cmdroot Command) Command {
-// mut cmd_run := Command{
-// name: 'web'
-// description: 'Run the Heroprompt UI (located in lib/web/heroprompt).'
-// required_args: 0
-// execute: cmd_web_execute
-// }
+pub fn cmd_web(mut cmdroot Command) Command {
+ mut cmd_run := Command{
+ name: 'web'
+ description: 'Run the Heroprompt UI (located in lib/web/heroprompt).'
+ required_args: 0
+ execute: cmd_web_execute
+ }
-// cmd_run.add_flag(Flag{
-// flag: .bool
-// required: false
-// name: 'open'
-// abbrev: 'o'
-// description: 'Open the UI in the default browser after starting the server.'
-// })
+ cmd_run.add_flag(Flag{
+ flag: .bool
+ required: false
+ name: 'open'
+ abbrev: 'o'
+ description: 'Open the UI in the default browser after starting the server.'
+ })
-// cmd_run.add_flag(Flag{
-// flag: .string
-// required: false
-// name: 'host'
-// abbrev: 'h'
-// description: 'Host to bind the server to (default: localhost).'
-// })
+ cmd_run.add_flag(Flag{
+ flag: .string
+ required: false
+ name: 'host'
+ abbrev: 'h'
+ description: 'Host to bind the server to (default: localhost).'
+ })
-// cmd_run.add_flag(Flag{
-// flag: .int
-// required: false
-// name: 'port'
-// abbrev: 'p'
-// description: 'Port to bind the server to (default: 8080).'
-// })
+ cmd_run.add_flag(Flag{
+ flag: .int
+ required: false
+ name: 'port'
+ abbrev: 'p'
+ description: 'Port to bind the server to (default: 8080).'
+ })
-// cmdroot.add_command(cmd_run)
-// return cmdroot
-// }
+ cmdroot.add_command(cmd_run)
+ return cmdroot
+}
-// fn cmd_web_execute(cmd Command) ! {
-// // ---------- FLAGS ----------
-// mut open_ := cmd.flags.get_bool('open') or { false }
-// mut host := cmd.flags.get_string('host') or { 'localhost' }
-// mut port := cmd.flags.get_int('port') or { 8080 }
+fn cmd_web_execute(cmd Command) ! {
+ // ---------- FLAGS ----------
+ mut open_ := cmd.flags.get_bool('open') or { false }
+ mut host := cmd.flags.get_string('host') or { 'localhost' }
+ mut port := cmd.flags.get_int('port') or { 8080 }
-// // Set defaults if not provided
-// if host == '' {
-// host = 'localhost'
-// }
-// if port == 0 {
-// port = 8080
-// }
+ // Set defaults if not provided
+ if host == '' {
+ host = 'localhost'
+ }
+ if port == 0 {
+ port = 8080
+ }
-// console.print_header('Starting Heroprompt...')
+ console.print_header('Starting Heroprompt...')
-// // Prepare arguments for the UI factory
-// mut factory_args := ui.FactoryArgs{
-// title: 'Hero Admin Panel'
-// host: host
-// port: port
-// }
+ // Prepare arguments for the UI factory
+ mut factory_args := ui.FactoryArgs{
+ title: 'Hero Admin Panel'
+ host: host
+ port: port
+ }
-// // ---------- START WEB SERVER ----------
-// console.print_header('Starting Heroprompt server...')
+ // ---------- START WEB SERVER ----------
+ console.print_header('Starting Heroprompt server...')
-// // Start the server in a separate thread to allow for browser opening
-// spawn fn [factory_args] () {
-// ui.start(factory_args) or {
-// console.print_stderr('Failed to start Heroprompt server: ${err}')
-// return
-// }
-// }()
+ // Start the server in a separate thread to allow for browser opening
+ spawn fn [factory_args] () {
+ ui.start(factory_args) or {
+ console.print_stderr('Failed to start Heroprompt server: ${err}')
+ return
+ }
+ }()
-// // Give the server a moment to start
-// time.sleep(2 * time.second)
-// url := 'http://${factory_args.host}:${factory_args.port}'
+ // Give the server a moment to start
+ time.sleep(2 * time.second)
+ url := 'http://${factory_args.host}:${factory_args.port}'
-// console.print_green('Heroprompt server is running on ${url}')
+ console.print_green('Heroprompt server is running on ${url}')
-// if open_ {
-// mut cmd_str := ''
-// $if macos {
-// cmd_str = 'open ${url}'
-// } $else $if linux {
-// cmd_str = 'xdg-open ${url}'
-// } $else $if windows {
-// cmd_str = 'start ${url}'
-// }
+ if open_ {
+ mut cmd_str := ''
+ $if macos {
+ cmd_str = 'open ${url}'
+ } $else $if linux {
+ cmd_str = 'xdg-open ${url}'
+ } $else $if windows {
+ cmd_str = 'start ${url}'
+ }
-// if cmd_str != '' {
-// result := os.execute(cmd_str)
-// if result.exit_code == 0 {
-// console.print_green('Opened Heroprompt in default browser.')
-// } else {
-// console.print_stderr('Failed to open browser: ${result.output}')
-// }
-// }
-// }
+ if cmd_str != '' {
+ result := os.execute(cmd_str)
+ if result.exit_code == 0 {
+ console.print_green('Opened Heroprompt in default browser.')
+ } else {
+ console.print_stderr('Failed to open browser: ${result.output}')
+ }
+ }
+ }
-// // Keep the process alive while the server runs
-// console.print_header('Press Ctrl+C to stop the server')
-// for {
-// time.sleep(1 * time.second)
-// }
-// }
+ // Keep the process alive while the server runs
+ console.print_header('Press Ctrl+C to stop the server')
+ for {
+ time.sleep(1 * time.second)
+ }
+}
diff --git a/lib/web/heroprompt/endpoints.v b/lib/web/heroprompt/endpoints.v
deleted file mode 100644
index a48a45ad..00000000
--- a/lib/web/heroprompt/endpoints.v
+++ /dev/null
@@ -1,315 +0,0 @@
-module heroprompt
-
-import veb
-import os
-import json
-import time
-import freeflowuniverse.herolib.develop.heroprompt as hp
-
-// Types for directory listing
-struct DirItem {
- name string
- typ string @[json: 'type']
-}
-
-struct DirResp {
- path string
-mut:
- items []DirItem
-}
-
-// HTML routes
-@['/heroprompt'; get]
-pub fn (app &App) page_index(mut ctx Context) veb.Result {
- return ctx.html(render_index(app))
-}
-
-// API routes (thin wrappers over develop.heroprompt)
-@['/api/heroprompt/workspaces'; get]
-pub fn (app &App) api_list(mut ctx Context) veb.Result {
- mut names := []string{}
- ws := hp.list(fromdb: true) or { []&hp.Workspace{} }
- for w in ws {
- names << w.name
- }
- ctx.set_content_type('application/json')
- return ctx.text(json.encode(names))
-}
-
-@['/api/heroprompt/workspaces'; post]
-pub fn (app &App) api_create(mut ctx Context) veb.Result {
- name := ctx.form['name'] or { 'default' }
- base_path_in := ctx.form['base_path'] or { '' }
- if base_path_in.len == 0 {
- return ctx.text('{"error":"base_path required"}')
- }
- mut base_path := base_path_in
- // Expand tilde to user home
- if base_path.starts_with('~') {
- home := os.home_dir()
- base_path = os.join_path(home, base_path.all_after('~'))
- }
- _ := hp.get(name: name, create: true, path: base_path) or {
- return ctx.text('{"error":"create failed"}')
- }
- ctx.set_content_type('application/json')
- return ctx.text(json.encode({
- 'name': name
- 'base_path': base_path
- }))
-}
-
-@['/api/heroprompt/directory'; get]
-pub fn (app &App) api_directory(mut ctx Context) veb.Result {
- wsname := ctx.query['name'] or { 'default' }
- path_q := ctx.query['path'] or { '' }
- mut wsp := hp.get(name: wsname, create: false) or {
- return ctx.text('{"error":"workspace not found"}')
- }
- // Use workspace list method; empty path means base_path
- items_w := if path_q.len > 0 { wsp.list() or {
- return ctx.text('{"error":"cannot list directory"}')} } else { wsp.list() or {
- return ctx.text('{"error":"cannot list directory"}')} }
- ctx.set_content_type('application/json')
- mut resp := DirResp{
- path: if path_q.len > 0 { path_q } else { wsp.base_path }
- }
- for it in items_w {
- resp.items << DirItem{
- name: it.name
- typ: it.typ
- }
- }
- return ctx.text(json.encode(resp))
-}
-
-// -------- File content endpoint --------
-struct FileResp {
- language string
- content string
-}
-
-@['/api/heroprompt/file'; get]
-pub fn (app &App) api_file(mut ctx Context) veb.Result {
- wsname := ctx.query['name'] or { 'default' }
- path_q := ctx.query['path'] or { '' }
- if path_q.len == 0 {
- return ctx.text('{"error":"path required"}')
- }
- mut base := ''
- if wsp := hp.get(name: wsname, create: false) {
- base = wsp.base_path
- }
- mut file_path := if !os.is_abs_path(path_q) && base.len > 0 {
- os.join_path(base, path_q)
- } else {
- path_q
- }
- if !os.is_file(file_path) {
- return ctx.text('{"error":"not a file"}')
- }
- // limit read to 1MB to avoid huge responses
- max_size := i64(1_000_000)
- sz := os.file_size(file_path)
- if sz > max_size {
- return ctx.text('{"error":"file too large"}')
- }
- content := os.read_file(file_path) or { return ctx.text('{"error":"failed to read"}') }
- lang := detect_lang(file_path)
- ctx.set_content_type('application/json')
- return ctx.text(json.encode(FileResp{ language: lang, content: content }))
-}
-
-fn detect_lang(path string) string {
- ext := os.file_ext(path).trim_left('.')
- return match ext.to_lower() {
- 'v' { 'v' }
- 'js' { 'javascript' }
- 'ts' { 'typescript' }
- 'py' { 'python' }
- 'rs' { 'rust' }
- 'go' { 'go' }
- 'java' { 'java' }
- 'c', 'h' { 'c' }
- 'cpp', 'hpp', 'cc', 'hh' { 'cpp' }
- 'sh', 'bash' { 'bash' }
- 'json' { 'json' }
- 'yaml', 'yml' { 'yaml' }
- 'html', 'htm' { 'html' }
- 'css' { 'css' }
- 'md' { 'markdown' }
- else { 'text' }
- }
-}
-
-// -------- Filename search endpoint --------
-struct SearchItem {
- path string
- typ string @[json: 'type']
-}
-
-@['/api/heroprompt/search'; get]
-pub fn (app &App) api_search(mut ctx Context) veb.Result {
- wsname := ctx.query['name'] or { 'default' }
- q := ctx.query['q'] or { '' }
- if q.len == 0 {
- return ctx.text('{"error":"q required"}')
- }
- mut base := ''
- if wsp := hp.get(name: wsname, create: false) {
- base = wsp.base_path
- }
- if base.len == 0 {
- return ctx.text('{"error":"workspace base_path not set"}')
- }
- max := (ctx.query['max'] or { '200' }).int()
- mut results := []SearchItem{}
- walk_search(base, q, max, mut results)
- ctx.set_content_type('application/json')
- return ctx.text(json.encode(results))
-}
-
-// Workspace details
-@['/api/heroprompt/workspaces/:name'; get]
-pub fn (app &App) api_workspace_get(mut ctx Context, name string) veb.Result {
- wsp := hp.get(name: name, create: false) or {
- return ctx.text('{"error":"workspace not found"}')
- }
- ctx.set_content_type('application/json')
- return ctx.text(json.encode({
- 'name': wsp.name
- 'base_path': wsp.base_path
- }))
-}
-
-@['/api/heroprompt/workspaces/:name'; delete]
-pub fn (app &App) api_workspace_delete(mut ctx Context, name string) veb.Result {
- wsp := hp.get(name: name, create: false) or {
- return ctx.text('{"error":"workspace not found"}')
- }
- wsp.delete_workspace() or { return ctx.text('{"error":"delete failed"}') }
- return ctx.text('{"ok":true}')
-}
-
-@['/api/heroprompt/workspaces/:name'; patch]
-pub fn (app &App) api_workspace_patch(mut ctx Context, name string) veb.Result {
- wsp := hp.get(name: name, create: false) or {
- return ctx.text('{"error":"workspace not found"}')
- }
- new_name := ctx.form['name'] or { '' }
- mut base_path := ctx.form['base_path'] or { '' }
- if base_path.len > 0 && base_path.starts_with('~') {
- home := os.home_dir()
- base_path = os.join_path(home, base_path.all_after('~'))
- }
- updated := wsp.update_workspace(name: new_name, base_path: base_path) or {
- return ctx.text('{"error":"update failed"}')
- }
- ctx.set_content_type('application/json')
- return ctx.text(json.encode({
- 'name': updated.name
- 'base_path': updated.base_path
- }))
-}
-
-// -------- Path validation endpoint --------
-struct PathValidationResp {
- is_abs bool
- exists bool
- is_dir bool
- expanded string
-}
-
-@['/api/heroprompt/validate_path'; get]
-pub fn (app &App) api_validate_path(mut ctx Context) veb.Result {
- p_in := ctx.query['path'] or { '' }
- mut p := p_in
- if p.starts_with('~') {
- home := os.home_dir()
- p = os.join_path(home, p.all_after('~'))
- }
- is_abs := if p != '' { os.is_abs_path(p) } else { false }
- exists := if p != '' { os.exists(p) } else { false }
- isdir := if exists { os.is_dir(p) } else { false }
- ctx.set_content_type('application/json')
- resp := PathValidationResp{
- is_abs: is_abs
- exists: exists
- is_dir: isdir
- expanded: p
- }
- return ctx.text(json.encode(resp))
-}
-
-fn walk_search(root string, q string, max int, mut out []SearchItem) {
- if out.len >= max {
- return
- }
- entries := os.ls(root) or { return }
- for e in entries {
- if e in ['.git', 'node_modules', 'build', 'dist', '.v'] {
- continue
- }
- p := os.join_path(root, e)
- if os.is_dir(p) {
- if out.len >= max {
- return
- }
- if e.to_lower().contains(q.to_lower()) {
- out << SearchItem{
- path: p
- typ: 'directory'
- }
- }
- walk_search(p, q, max, mut out)
- } else if os.is_file(p) {
- if e.to_lower().contains(q.to_lower()) {
- out << SearchItem{
- path: p
- typ: 'file'
- }
- }
- }
- if out.len >= max {
- return
- }
- }
-}
-
-// -------- Selection and prompt endpoints --------
-@['/api/heroprompt/workspaces/:name/files'; post]
-pub fn (app &App) api_add_file(mut ctx Context, name string) veb.Result {
- path := ctx.form['path'] or { '' }
- if path.len == 0 {
- return ctx.text('{"error":"path required"}')
- }
- mut wsp := hp.get(name: name, create: false) or {
- return ctx.text('{"error":"workspace not found"}')
- }
- wsp.add_file(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') }
- return ctx.text('{"ok":true}')
-}
-
-@['/api/heroprompt/workspaces/:name/dirs'; post]
-pub fn (app &App) api_add_dir(mut ctx Context, name string) veb.Result {
- path := ctx.form['path'] or { '' }
- if path.len == 0 {
- return ctx.text('{"error":"path required"}')
- }
- mut wsp := hp.get(name: name, create: false) or {
- return ctx.text('{"error":"workspace not found"}')
- }
- wsp.add_dir(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') }
- return ctx.text('{"ok":true}')
-}
-
-@['/api/heroprompt/workspaces/:name/prompt'; post]
-pub fn (app &App) api_generate_prompt(mut ctx Context, name string) veb.Result {
- text := ctx.form['text'] or { '' }
- mut wsp := hp.get(name: name, create: false) or {
- return ctx.text('{"error":"workspace not found"}')
- }
- prompt := wsp.prompt(text: text)
- ctx.set_content_type('text/plain')
- return ctx.text(prompt)
-}
diff --git a/lib/web/heroprompt/server.v b/lib/web/heroprompt/server.v
deleted file mode 100644
index 8ba215cf..00000000
--- a/lib/web/heroprompt/server.v
+++ /dev/null
@@ -1,71 +0,0 @@
-module heroprompt
-
-import veb
-import os
-
-// Public Context type for veb
-pub struct Context {
- veb.Context
-}
-
-// Factory args for starting the server
-@[params]
-pub struct FactoryArgs {
-pub mut:
- host string = 'localhost'
- port int = 8090
- title string = 'Heroprompt'
-}
-
-// App holds server state and config
-pub struct App {
- veb.StaticHandler
-pub mut:
- title string
- port int
- base_path string // absolute path to this module directory
-}
-
-// Create a new App instance (does not start the server)
-pub fn new(args FactoryArgs) !&App {
- base := os.dir(@FILE)
- mut app := App{
- title: args.title
- port: args.port
- base_path: base
- }
- // Serve static assets from this module at /static
- app.mount_static_folder_at(os.join_path(base, 'static'), '/static')!
- return &app
-}
-
-// Start the webserver (blocking)
-pub fn start(args FactoryArgs) ! {
- mut app := new(args)!
- veb.run[App, Context](mut app, app.port)
-}
-
-// Routes
-
-@['/'; get]
-pub fn (app &App) index(mut ctx Context) veb.Result {
- return ctx.html(render_index(app))
-}
-
-// Rendering helpers
-fn render_index(app &App) string {
- tpl := os.join_path(app.base_path, 'templates', 'index.html')
- content := os.read_file(tpl) or { return render_index_fallback(app) }
- return render_template(content, {
- 'title': app.title
- })
-}
-
-fn render_index_fallback(app &App) string {
- return
- '\n' +
- html_escape(app.title) +
- '' +
- html_escape(app.title) +
- '
Heroprompt server is running.
'
-}
diff --git a/lib/web/heroprompt/templates/index.html b/lib/web/heroprompt/templates/index.html
deleted file mode 100644
index 8d5033de..00000000
--- a/lib/web/heroprompt/templates/index.html
+++ /dev/null
@@ -1,115 +0,0 @@
-
-
-
-
-
-
- {{.title}} - Heroprompt
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Prompt
-
-
-
-
-
Selected Files (0) — Tokens: 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
If blank we will generate one for you
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/lib/web/heroprompt/utils.v b/lib/web/heroprompt/utils.v
deleted file mode 100644
index e44fd628..00000000
--- a/lib/web/heroprompt/utils.v
+++ /dev/null
@@ -1,28 +0,0 @@
-module heroprompt
-
-import strings
-
-// Very small template renderer using {{.var}} replacement
-pub fn render_template(tpl string, data map[string]string) string {
- mut out := tpl
- for k, v in data {
- out = out.replace('{{.' + k + '}}', v)
- }
- return out
-}
-
-// Minimal HTML escape
-pub fn html_escape(s string) string {
- mut b := strings.new_builder(s.len)
- for ch in s {
- match ch {
- `&` { b.write_string('&') }
- `<` { b.write_string('<') }
- `>` { b.write_string('>') }
- `"` { b.write_string('"') }
- `'` { b.write_string(''') }
- else { b.write_string(ch.str()) }
- }
- }
- return b.str()
-}
diff --git a/lib/web/ui/chat/endpoints.v b/lib/web/ui/chat/endpoints.v
new file mode 100644
index 00000000..952ca743
--- /dev/null
+++ b/lib/web/ui/chat/endpoints.v
@@ -0,0 +1,21 @@
+module chat
+
+import os
+import freeflowuniverse.herolib.web.ui
+
+pub fn render(app &ui.App) !string {
+ tpl := os.join_path(os.dir(@FILE), 'templates', 'chat.html')
+ content := os.read_file(tpl)!
+ menu_content := ui.menu_html(app.menu, 0, 'm')
+ mut result := content
+ result = result.replace('{{.title}}', app.title)
+ result = result.replace('{{.menu_html}}', menu_content)
+ result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
+ result = result.replace('{{.css_main_url}}', '/static/css/main.css')
+ result = result.replace('{{.css_chat_url}}', '/static/chat/css/chat.css')
+ result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
+ result = result.replace('{{.js_chat_url}}', '/static/chat/js/chat.js')
+ // version banner
+ result = result.replace('
-//
-//
HeroScript Editor
-//
HeroScript editor template not found. Please check the template files.
-//
Back to Admin
-//
-// ', '
Rendered by: chat
')
+ return result
+}
diff --git a/lib/web/ui/templates/css/chat.css b/lib/web/ui/chat/static/css/chat.css
similarity index 100%
rename from lib/web/ui/templates/css/chat.css
rename to lib/web/ui/chat/static/css/chat.css
diff --git a/lib/web/ui/templates/js/chat.js b/lib/web/ui/chat/static/js/chat.js
similarity index 100%
rename from lib/web/ui/templates/js/chat.js
rename to lib/web/ui/chat/static/js/chat.js
diff --git a/lib/web/ui/templates/chat.html b/lib/web/ui/chat/templates/chat.html
similarity index 100%
rename from lib/web/ui/templates/chat.html
rename to lib/web/ui/chat/templates/chat.html
diff --git a/lib/web/ui/chat/utils.v b/lib/web/ui/chat/utils.v
new file mode 100644
index 00000000..127b3671
--- /dev/null
+++ b/lib/web/ui/chat/utils.v
@@ -0,0 +1,4 @@
+module chat
+
+// Placeholder for chat-specific utilities
+
diff --git a/lib/web/ui/endpoints.v b/lib/web/ui/endpoints.v
deleted file mode 100644
index 91d694c8..00000000
--- a/lib/web/ui/endpoints.v
+++ /dev/null
@@ -1,244 +0,0 @@
-module ui
-
-// import veb
-// import freeflowuniverse.herolib.develop.heroprompt
-// import os
-// import json
-
-// // Directory browsing and file read endpoints for Heroprompt.js compatibility
-// struct DirItem {
-// name string
-// typ string @[json: 'type']
-// }
-
-// struct DirResp {
-// path string
-// mut:
-// items []DirItem
-// }
-
-// @['/api/heroprompt/directory'; get]
-// pub fn (app &App) api_heroprompt_directory(mut ctx Context) veb.Result {
-// // Optional workspace name, defaults to 'default'
-// wsname := ctx.query['name'] or { 'default' }
-// path_q := ctx.query['path'] or { '' }
-// if path_q.len == 0 {
-// return ctx.text('{"error":"path required"}')
-// }
-// // Try to resolve against workspace base_path if available, but do not require it
-// mut base := ''
-// if wsp := heroprompt.get(name: wsname, create: false) {
-// base = wsp.base_path
-// }
-// // Resolve path: if absolute, use as-is; else join with base
-// mut dir_path := path_q
-// if !os.is_abs_path(dir_path) && base.len > 0 {
-// dir_path = os.join_path(base, dir_path)
-// }
-// // List entries
-// entries := os.ls(dir_path) or { return ctx.text('{"error":"cannot list directory"}') }
-// mut items := []map[string]string{}
-// for e in entries {
-// full := os.join_path(dir_path, e)
-// if os.is_dir(full) {
-// items << {
-// 'name': e
-// 'type': 'directory'
-// }
-// } else if os.is_file(full) {
-// items << {
-// 'name': e
-// 'type': 'file'
-// }
-// }
-// }
-// ctx.set_content_type('application/json')
-// // Encode strongly typed JSON response
-// mut resp := DirResp{
-// path: dir_path
-// }
-// for it in items {
-// resp.items << DirItem{
-// name: it['name'] or { '' }
-// typ: it['type'] or { '' }
-// }
-// }
-// return ctx.text(json.encode(resp))
-// }
-
-// @['/api/heroprompt/file'; get]
-// pub fn (app &App) api_heroprompt_file(mut ctx Context) veb.Result {
-// wsname := ctx.query['name'] or { 'default' }
-// path_q := ctx.query['path'] or { '' }
-// if path_q.len == 0 {
-// return ctx.text('{"error":"path required"}')
-// }
-// // Try to resolve against workspace base_path if available, but do not require it
-// mut base := ''
-// if wsp := heroprompt.get(name: wsname, create: false) {
-// base = wsp.base_path
-// }
-// mut file_path := path_q
-// if !os.is_abs_path(file_path) && base.len > 0 {
-// file_path = os.join_path(base, file_path)
-// }
-// content := os.read_file(file_path) or { return ctx.text('{"error":"failed to read"}') }
-// lang := detect_lang(file_path)
-// ctx.set_content_type('application/json')
-// return ctx.text(json.encode({
-// 'language': lang
-// 'content': content
-// }))
-// }
-
-// fn detect_lang(path string) string {
-// ext := os.file_ext(path).trim_left('.')
-// return match ext.to_lower() {
-// 'v' { 'v' }
-// 'js' { 'javascript' }
-// 'ts' { 'typescript' }
-// 'py' { 'python' }
-// 'rs' { 'rust' }
-// 'go' { 'go' }
-// 'java' { 'java' }
-// 'c', 'h' { 'c' }
-// 'cpp', 'hpp', 'cc', 'hh' { 'cpp' }
-// 'sh', 'bash' { 'bash' }
-// 'json' { 'json' }
-// 'yaml', 'yml' { 'yaml' }
-// 'html', 'htm' { 'html' }
-// 'css' { 'css' }
-// 'md' { 'markdown' }
-// else { 'text' }
-// }
-// }
-
-// // Heroprompt API: list workspaces
-// @['/api/heroprompt/workspaces'; get]
-// pub fn (app &App) api_heroprompt_list(mut ctx Context) veb.Result {
-// mut names := []string{}
-// ws := heroprompt.list(fromdb: true) or { []&heroprompt.Workspace{} }
-// for w in ws {
-// names << w.name
-// }
-// ctx.set_content_type('application/json')
-// return ctx.text(json.encode(names))
-// }
-
-// // Heroprompt API: create/get workspace
-// @['/api/heroprompt/workspaces'; post]
-// pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result {
-// name := ctx.form['name'] or { '' }
-// base_path := ctx.form['base_path'] or { '' }
-
-// if base_path.len == 0 {
-// return ctx.text('{"error":"base_path required"}')
-// }
-
-// mut wsp := heroprompt.get(name: name, create: true, path: base_path) or {
-// return ctx.text('{"error":"create failed"}')
-// }
-
-// ctx.set_content_type('application/json')
-// return ctx.text(json.encode({
-// 'name': name
-// 'base_path': base_path
-// }))
-// }
-
-// // Heroprompt API: add directory to workspace
-// @['/api/heroprompt/workspaces/:name/dirs'; post]
-// pub fn (app &App) api_heroprompt_add_dir(mut ctx Context, name string) veb.Result {
-// path := ctx.form['path'] or { '' }
-// if path.len == 0 {
-// return ctx.text('{"error":"path required"}')
-// }
-// mut wsp := heroprompt.get(name: name, create: true) or {
-// return ctx.text('{"error":"workspace not found"}')
-// }
-// wsp.add_dir(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') }
-// ctx.set_content_type('application/json')
-// return ctx.text('{"ok":true}')
-// }
-
-// // Heroprompt API: add file to workspace
-// @['/api/heroprompt/workspaces/:name/files'; post]
-// pub fn (app &App) api_heroprompt_add_file(mut ctx Context, name string) veb.Result {
-// path := ctx.form['path'] or { '' }
-// if path.len == 0 {
-// return ctx.text('{"error":"path required"}')
-// }
-// mut wsp := heroprompt.get(name: name, create: true) or {
-// return ctx.text('{"error":"workspace not found"}')
-// }
-// wsp.add_file(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') }
-// ctx.set_content_type('application/json')
-// return ctx.text('{"ok":true}')
-// }
-
-// // Heroprompt API: generate prompt
-// @['/api/heroprompt/workspaces/:name/prompt'; post]
-// pub fn (app &App) api_heroprompt_prompt(mut ctx Context, name string) veb.Result {
-// text := ctx.form['text'] or { '' }
-// mut wsp := heroprompt.get(name: name, create: false) or {
-// return ctx.text('{"error":"workspace not found"}')
-// }
-// prompt := wsp.prompt(text: text)
-// ctx.set_content_type('text/plain')
-// return ctx.text(prompt)
-// }
-
-// // Heroprompt API: get workspace details
-// @['/api/heroprompt/workspaces/:name'; get]
-// pub fn (app &App) api_heroprompt_get(mut ctx Context, name string) veb.Result {
-// wsp := heroprompt.get(name: name, create: false) or {
-// return ctx.text('{"error":"workspace not found"}')
-// }
-// mut children := []map[string]string{}
-// for ch in wsp.children {
-// children << {
-// 'name': ch.name
-// 'path': ch.path.path
-// 'type': if ch.path.cat == .dir { 'directory' } else { 'file' }
-// }
-// }
-// ctx.set_content_type('application/json')
-// return ctx.text(json.encode({
-// 'name': wsp.name
-// 'base_path': wsp.base_path
-// 'children': json.encode(children)
-// }))
-// }
-
-// // Heroprompt API: delete workspace
-// @['/api/heroprompt/workspaces/:name'; delete]
-// pub fn (app &App) api_heroprompt_delete(mut ctx Context, name string) veb.Result {
-// wsp := heroprompt.get(name: name, create: false) or {
-// return ctx.text('{"error":"workspace not found"}')
-// }
-// wsp.delete_workspace() or { return ctx.text('{"error":"delete failed"}') }
-// ctx.set_content_type('application/json')
-// return ctx.text('{"ok":true}')
-// }
-
-// // Heroprompt API: remove directory
-// @['/api/heroprompt/workspaces/:name/dirs/remove'; post]
-// pub fn (app &App) api_heroprompt_remove_dir(mut ctx Context, name string) veb.Result {
-// path := ctx.form['path'] or { '' }
-// mut wsp := heroprompt.get(name: name, create: false) or {
-// return ctx.text('{"error":"workspace not found"}')
-// }
-// wsp.remove_dir(path: path, name: '') or { return ctx.text('{"error":"' + err.msg() + '"}') }
-// return ctx.text('{"ok":true}')
-// }
-
-// // Heroprompt API: remove file
-// @['/api/heroprompt/workspaces/:name/files/remove'; post]
-// pub fn (app &App) api_heroprompt_remove_file(mut ctx Context, name string) veb.Result {
-// path := ctx.form['path'] or { '' }
-// mut wsp := heroprompt.get(name: name, create: false) or {
-// return ctx.text('{"error":"workspace not found"}')
-// }
-// wsp.remove_file(path: path, name: '') or { return ctx.text('{"error":"' + err.msg() + '"}') }
-// return ctx.text('{"ok":true}')
-// }
diff --git a/lib/web/ui/factory.v b/lib/web/ui/factory.v
deleted file mode 100644
index 384b1055..00000000
--- a/lib/web/ui/factory.v
+++ /dev/null
@@ -1,471 +0,0 @@
-module ui
-
-// import veb
-// import os
-
-// // Public Context type for veb
-// pub struct Context {
-// veb.Context
-// }
-
-// // Simple tree menu structure
-// pub struct MenuItem {
-// pub:
-// title string
-// href string
-// children []MenuItem
-// }
-
-// // Factory args
-// @[params]
-// pub struct WebArgs {
-// pub mut:
-// name string = 'default'
-// host string = 'localhost'
-// port int = 8080
-// title string = 'Admin'
-// menu []MenuItem
-// open bool
-// }
-
-// // The App holds server state and config
-// pub struct App {
-// veb.StaticHandler
-// pub mut:
-// title string = 'default'
-// menu []MenuItem
-// port int = 7711
-// }
-
-// // Start the webserver (blocking)
-// pub fn start(args WebArgs) ! {
-// mut app := App{
-// title: args.title
-// menu: args.menu
-// port: args.port
-// }
-// veb.run[App, Context](mut app, app.port)
-// }
-
-// // Routes
-
-// // Redirect root to /admin
-// @['/'; get]
-// pub fn (app &App) root(mut ctx Context) veb.Result {
-// return ctx.redirect('/admin')
-// }
-
-// // Admin home page
-// @['/admin'; get]
-// pub fn (app &App) admin_index(mut ctx Context) veb.Result {
-// return ctx.html(app.render_admin('/', 'Welcome'))
-// }
-
-// // HeroScript editor page
-// @['/admin/heroscript'; get]
-// pub fn (app &App) admin_heroscript(mut ctx Context) veb.Result {
-// return ctx.html(app.render_heroscript())
-// }
-
-// // Chat page
-// @['/admin/chat'; get]
-// pub fn (app &App) admin_chat(mut ctx Context) veb.Result {
-// return ctx.html(app.render_chat())
-// }
-
-// // Static CSS files
-// @['/static/css/colors.css'; get]
-// pub fn (app &App) serve_colors_css(mut ctx Context) veb.Result {
-// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'colors.css')
-// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
-// ctx.set_content_type('text/css')
-// return ctx.text(css_content)
-// }
-
-// @['/static/css/main.css'; get]
-// pub fn (app &App) serve_main_css(mut ctx Context) veb.Result {
-// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'main.css')
-// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
-// ctx.set_content_type('text/css')
-// return ctx.text(css_content)
-// }
-
-// // Static JS files
-// @['/static/js/theme.js'; get]
-// pub fn (app &App) serve_theme_js(mut ctx Context) veb.Result {
-// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'theme.js')
-// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
-// ctx.set_content_type('application/javascript')
-// return ctx.text(js_content)
-// }
-
-// @['/static/js/heroscript.js'; get]
-// pub fn (app &App) serve_heroscript_js(mut ctx Context) veb.Result {
-// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'heroscript.js')
-// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
-// ctx.set_content_type('application/javascript')
-// return ctx.text(js_content)
-// }
-
-// @['/static/js/chat.js'; get]
-// pub fn (app &App) serve_chat_js(mut ctx Context) veb.Result {
-// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'chat.js')
-// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
-// ctx.set_content_type('application/javascript')
-// return ctx.text(js_content)
-// }
-
-// @['/static/css/heroscript.css'; get]
-// pub fn (app &App) serve_heroscript_css(mut ctx Context) veb.Result {
-// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'heroscript.css')
-// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
-// ctx.set_content_type('text/css')
-// return ctx.text(css_content)
-// }
-
-// @['/static/css/chat.css'; get]
-// pub fn (app &App) serve_chat_css(mut ctx Context) veb.Result {
-// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'chat.css')
-// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
-// ctx.set_content_type('text/css')
-// return ctx.text(css_content)
-// }
-
-// // Catch-all content under /admin/*
-// @['/admin/:path...'; get]
-// pub fn (app &App) admin_section(mut ctx Context, path string) veb.Result {
-// // Render current path in the main content
-// return ctx.html(app.render_admin(path, 'Content'))
-// }
-
-// // View rendering using external template
-
-// fn (app &App) render_admin(path string, heading string) string {
-// // Get the template file path relative to the module
-// template_path := os.join_path(os.dir(@FILE), 'templates', 'admin_layout.html')
-
-// // Read the template file
-// template_content := os.read_file(template_path) or {
-// // Fallback to inline template if file not found
-// return app.render_admin_fallback(path, heading)
-// }
-
-// // Generate menu HTML
-// menu_content := menu_html(app.menu, 0, 'm')
-
-// // Simple template variable replacement
-// mut result := template_content
-// result = result.replace('{{.title}}', app.title)
-// result = result.replace('{{.heading}}', heading)
-// result = result.replace('{{.path}}', path)
-// result = result.replace('{{.menu_html}}', menu_content)
-// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
-// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
-// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
-
-// return result
-// }
-
-// // HeroScript editor rendering using external template
-// fn (app &App) render_heroscript() string {
-// // Get the template file path relative to the module
-// template_path := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.html')
-
-// // Read the template file
-// template_content := os.read_file(template_path) or {
-// // Fallback to basic template if file not found
-// return app.render_heroscript_fallback()
-// }
-
-// // Generate menu HTML
-// menu_content := menu_html(app.menu, 0, 'm')
-
-// // Simple template variable replacement
-// mut result := template_content
-// result = result.replace('{{.title}}', app.title)
-// result = result.replace('{{.menu_html}}', menu_content)
-// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
-// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
-// result = result.replace('{{.css_heroscript_url}}', '/static/css/heroscript.css')
-// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
-// result = result.replace('{{.js_heroscript_url}}', '/static/js/heroscript.js')
-
-// return result
-// }
-
-// // Chat rendering using external template
-// fn (app &App) render_chat() string {
-// // Get the template file path relative to the module
-// template_path := os.join_path(os.dir(@FILE), 'templates', 'chat.html')
-
-// // Read the template file
-// template_content := os.read_file(template_path) or {
-// // Fallback to basic template if file not found
-// return app.render_chat_fallback()
-// }
-
-// // Generate menu HTML
-// menu_content := menu_html(app.menu, 0, 'm')
-
-// // Simple template variable replacement
-// mut result := template_content
-// result = result.replace('{{.title}}', app.title)
-// result = result.replace('{{.menu_html}}', menu_content)
-// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
-// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
-// result = result.replace('{{.css_chat_url}}', '/static/css/chat.css')
-// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
-// result = result.replace('{{.js_chat_url}}', '/static/js/chat.js')
-
-// return result
-// }
-
-// // Fallback HeroScript rendering method
-// fn (app &App) render_heroscript_fallback() string {
-// return '
-//
-//
-//
-//
-//
-//
${app.title} - HeroScript Editor
-//
-//
-//
-//