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 - - - - -
-
-
{{.title}}
- - -
- -
- - -
-
-
-
- -
-
-

Prompt

- -
-
-
-

Selected Files (0) — Tokens: 0

-
    -
    -
    -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    -
    - - - - - - \ 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('', '
    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 -// -// -// -//
    -//

    HeroScript Editor

    -//

    HeroScript editor template not found. Please check the template files.

    -// Back to Admin -//
    -// -// -// ' -// } - -// // Fallback Chat rendering method -// fn (app &App) render_chat_fallback() string { -// return ' -// -// -// -// -// -// ${app.title} - Chat -// -// -// -//
    -//

    Chat Assistant

    -//

    Chat template not found. Please check the template files.

    -// Back to Admin -//
    -// -// -// ' -// } - -// // Fallback rendering method (inline template) -// fn (app &App) render_admin_fallback(path string, heading string) string { -// return ' -// -// -// -// -// -// ${app.title} -// -// -// -// -// - -// - -//
    -//
    -//
    -//
    ${heading}
    -// /admin/${path} -//
    -//
    -//
    -//

    This is a placeholder admin content area for: /admin/${path}.

    -//

    Use the treeview on the left to navigate.

    -//
    -//
    -//
    -//
    - -// -// -// -// ' -// } - -// // Recursive menu renderer - -// fn menu_html(items []MenuItem, depth int, prefix string) string { -// mut out := []string{} -// for i, it in items { -// id := '${prefix}_${depth}_${i}' -// if it.children.len > 0 { -// // expandable group -// out << '
    ' -// out << '' -// out << '${it.title}' -// out << '' -// out << '
    ' -// out << '
    ' -// out << menu_html(it.children, depth + 1, id) -// out << '
    ' -// out << '
    ' -// out << '
    ' -// } else { -// // leaf -// out << '' -// } -// } -// return out.join('\n') -// } - -// // Default sample menu -// fn default_menu() []MenuItem { -// return [ -// MenuItem{ -// title: 'Dashboard' -// href: '/admin' -// }, -// MenuItem{ -// title: 'HeroScript' -// href: '/admin/heroscript' -// }, -// MenuItem{ -// title: 'Chat' -// href: '/admin/chat' -// }, -// MenuItem{ -// title: 'Users' -// children: [ -// MenuItem{ -// title: 'Overview' -// href: '/admin/users/overview' -// }, -// MenuItem{ -// title: 'Create' -// href: '/admin/users/create' -// }, -// MenuItem{ -// title: 'Roles' -// href: '/admin/users/roles' -// }, -// ] -// }, -// MenuItem{ -// title: 'Content' -// children: [ -// MenuItem{ -// title: 'Pages' -// href: '/admin/content/pages' -// }, -// MenuItem{ -// title: 'Media' -// href: '/admin/content/media' -// }, -// MenuItem{ -// title: 'Settings' -// children: [ -// MenuItem{ -// title: 'SEO' -// href: '/admin/content/settings/seo' -// }, -// MenuItem{ -// title: 'Themes' -// href: '/admin/content/settings/themes' -// }, -// ] -// }, -// ] -// }, -// MenuItem{ -// title: 'System' -// children: [ -// MenuItem{ -// title: 'Status' -// href: '/admin/system/status' -// }, -// MenuItem{ -// title: 'Logs' -// href: '/admin/system/logs' -// }, -// MenuItem{ -// title: 'Backups' -// href: '/admin/system/backups' -// }, -// ] -// }, -// ] -// } diff --git a/lib/web/ui/heroprompt/endpoints.v b/lib/web/ui/heroprompt/endpoints.v new file mode 100644 index 00000000..2ceaa5b4 --- /dev/null +++ b/lib/web/ui/heroprompt/endpoints.v @@ -0,0 +1,137 @@ +module heroprompt + +import veb +import os +import json +import freeflowuniverse.herolib.develop.heroprompt as hp + +// Types +struct DirItem { + name string + typ string @[json: 'type'] +} + +struct DirResp { + path string + items []DirItem +} + +// APIs +@['/api/heroprompt/workspaces'; get] +pub fn api_heroprompt_list(mut ctx ui.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 + 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"}') + } + items_w := wsp.list() or { return ctx.text('{"error":"cannot list directory"}') } + ctx.set_content_type('application/json') + mut items := []DirItem{} + for it in items_w { + items << DirItem{ + name: it.name + typ: it.typ + } + } + return ctx.text(json.encode(DirResp{ + path: if path_q.len > 0 { path_q } else { wsp.base_path } + items: items + })) +} + +@['/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"}') + } + content := os.read_file(file_path) or { return ctx.text('{"error":"failed to read"}') } + ctx.set_content_type('application/json') + return ctx.text(json.encode({ + 'language': detect_lang(file_path) + 'content': content + })) +} + +@['/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/ui/heroprompt/page.v b/lib/web/ui/heroprompt/page.v new file mode 100644 index 00000000..2a70a2b5 --- /dev/null +++ b/lib/web/ui/heroprompt/page.v @@ -0,0 +1,21 @@ +module ui + +import os + +// Render the Heroprompt admin page using feature template +pub fn render_heroprompt_page(app &App) !string { + tpl := os.join_path(os.dir(@FILE), 'templates', 'heroprompt.html') + content := os.read_file(tpl)! + menu_content := 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_heroprompt_url}}', '/static/heroprompt/css/heroprompt.css') + result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') + result = result.replace('{{.js_heroprompt_url}}', '/static/heroprompt/js/heroprompt.js') + // version banner + result = result.replace('', '
    Rendered by: heroprompt
    ') + return result +} diff --git a/lib/web/ui/templates/css/heroprompt.css b/lib/web/ui/heroprompt/static/css/heroprompt.css similarity index 100% rename from lib/web/ui/templates/css/heroprompt.css rename to lib/web/ui/heroprompt/static/css/heroprompt.css diff --git a/lib/web/ui/templates/js/heroprompt.js b/lib/web/ui/heroprompt/static/js/heroprompt.js similarity index 100% rename from lib/web/ui/templates/js/heroprompt.js rename to lib/web/ui/heroprompt/static/js/heroprompt.js diff --git a/lib/web/ui/templates/heroprompt.html b/lib/web/ui/heroprompt/templates/heroprompt.html similarity index 100% rename from lib/web/ui/templates/heroprompt.html rename to lib/web/ui/heroprompt/templates/heroprompt.html diff --git a/lib/web/ui/heroprompt/utils.v b/lib/web/ui/heroprompt/utils.v new file mode 100644 index 00000000..a447cfa7 --- /dev/null +++ b/lib/web/ui/heroprompt/utils.v @@ -0,0 +1,3 @@ +module ui + +// Placeholder for heroprompt-specific utilities (if needed later) diff --git a/lib/web/ui/heroscript/endpoints.v b/lib/web/ui/heroscript/endpoints.v new file mode 100644 index 00000000..36713e0e --- /dev/null +++ b/lib/web/ui/heroscript/endpoints.v @@ -0,0 +1,24 @@ +module heroscript + +import os +import freeflowuniverse.herolib.web.ui + +// Render HeroScript page +pub fn render(app &ui.App) !string { + tpl := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.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) + // shared CSS/JS + 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') + // feature CSS/JS + result = result.replace('{{.css_heroscript_url}}', '/static/heroscript/css/heroscript.css') + result = result.replace('{{.js_heroscript_url}}', '/static/heroscript/js/heroscript.js') + // version banner + result = result.replace('', '
    Rendered by: heroscript
    ') + return result +} diff --git a/lib/web/ui/templates/css/heroscript.css b/lib/web/ui/heroscript/static/css/heroscript.css similarity index 100% rename from lib/web/ui/templates/css/heroscript.css rename to lib/web/ui/heroscript/static/css/heroscript.css diff --git a/lib/web/ui/templates/js/heroscript.js b/lib/web/ui/heroscript/static/js/heroscript.js similarity index 100% rename from lib/web/ui/templates/js/heroscript.js rename to lib/web/ui/heroscript/static/js/heroscript.js diff --git a/lib/web/ui/templates/heroscript_editor.html b/lib/web/ui/heroscript/templates/heroscript_editor.html similarity index 100% rename from lib/web/ui/templates/heroscript_editor.html rename to lib/web/ui/heroscript/templates/heroscript_editor.html diff --git a/lib/web/ui/heroscript/utils.v b/lib/web/ui/heroscript/utils.v new file mode 100644 index 00000000..e99cf686 --- /dev/null +++ b/lib/web/ui/heroscript/utils.v @@ -0,0 +1,4 @@ +module heroscript + +// Placeholder for heroscript-specific utilities + diff --git a/lib/web/ui/menu.v b/lib/web/ui/menu.v new file mode 100644 index 00000000..fc7cf966 --- /dev/null +++ b/lib/web/ui/menu.v @@ -0,0 +1,42 @@ +module ui + +// Default menu for the Admin UI. Used when no custom menu is provided. +pub fn get_default_menu() []MenuItem { + return [ + MenuItem{ title: 'Dashboard', href: '/admin' }, + MenuItem{ title: 'HeroScript', href: '/admin/heroscript' }, + MenuItem{ title: 'Chat', href: '/admin/chat' }, + MenuItem{ title: 'Heroprompt', href: '/admin/heroprompt' }, + MenuItem{ + title: 'Users' + children: [ + MenuItem{ title: 'Overview', href: '/admin/users/overview' }, + MenuItem{ title: 'Create', href: '/admin/users/create' }, + MenuItem{ title: 'Roles', href: '/admin/users/roles' }, + ] + }, + MenuItem{ + title: 'Content' + children: [ + MenuItem{ title: 'Pages', href: '/admin/content/pages' }, + MenuItem{ title: 'Media', href: '/admin/content/media' }, + MenuItem{ + title: 'Settings' + children: [ + MenuItem{ title: 'SEO', href: '/admin/content/settings/seo' }, + MenuItem{ title: 'Themes', href: '/admin/content/settings/themes' }, + ] + }, + ] + }, + MenuItem{ + title: 'System' + children: [ + MenuItem{ title: 'Status', href: '/admin/system/status' }, + MenuItem{ title: 'Logs', href: '/admin/system/logs' }, + MenuItem{ title: 'Backups', href: '/admin/system/backups' }, + ] + }, + ] +} + diff --git a/lib/web/ui/server.v b/lib/web/ui/server.v index fa1dcec5..4948ee34 100644 --- a/lib/web/ui/server.v +++ b/lib/web/ui/server.v @@ -1,243 +1,207 @@ module ui -// import veb -// import os -// import net.http -// import json -// import freeflowuniverse.herolib.develop.heroprompt +import veb +import os +import net.http +import json +// Feature endpoint files live in subdirectories but share the `ui` module, +// so no explicit imports are needed here. -// // Public Context type for veb -// pub struct Context { -// veb.Context -// } +// Public Context type for veb +pub struct Context { + veb.Context +} -// // Simple tree menu structure -// pub struct MenuItem { -// pub: -// title string -// href string -// children []MenuItem -// } +// Simple tree menu structure +pub struct MenuItem { +pub: + title string + href string + children []MenuItem +} -// // Factory args -// @[params] -// pub struct FactoryArgs { -// pub mut: -// name string = 'default' -// host string = 'localhost' -// port int = 8080 -// title string = 'Admin' -// menu []MenuItem -// open bool -// } +// Factory args +@[params] +pub struct FactoryArgs { +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 -// menu []MenuItem -// port int -// } +// The App holds server state and config +pub struct App { + veb.StaticHandler +pub mut: + title string + menu []MenuItem + port int +} -// pub fn new(args FactoryArgs) !&App { -// mut app := App{ -// title: args.title -// menu: args.menu -// port: args.port -// } -// return &app -// } +pub fn new(args FactoryArgs) !&App { + mut app := App{ + title: args.title + menu: if args.menu.len > 0 { args.menu } else { get_default_menu() } + port: args.port + } + // Mount shared and per-feature static folders + base := os.dir(@FILE) + app.mount_static_folder_at(os.join_path(base, 'static'), '/static')! + app.mount_static_folder_at(os.join_path(base, 'heroprompt', 'static'), '/static/heroprompt')! + app.mount_static_folder_at(os.join_path(base, 'heroscript', 'static'), '/static/heroscript')! + app.mount_static_folder_at(os.join_path(base, 'chat', 'static'), '/static/chat')! + return &app +} -// // Start the webserver (blocking) -// pub fn start(args FactoryArgs) ! { -// mut app := new(args)! -// veb.run[App, Context](mut app, app.port) -// } +// Start the webserver (blocking) +pub fn start(args FactoryArgs) ! { + mut app := new(args)! + veb.run[App, Context](mut app, app.port) +} -// // Routes +// Routes -// // Redirect root to /admin -// @['/'; get] -// pub fn (app &App) root(mut ctx Context) veb.Result { -// return ctx.redirect('/admin') -// } +// 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(render_admin(app, '/', 'Welcome')) -// } +// Serve shared static assets (colors.css, main.css, theme.js) +@['/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), 'static', '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) +} -// // HeroScript editor page -// @['/admin/heroscript'; get] -// pub fn (app &App) admin_heroscript(mut ctx Context) veb.Result { -// return ctx.html(render_heroscript(app)) -// } +@['/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), 'static', '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) +} -// // Chat page -// @['/admin/chat'; get] -// pub fn (app &App) admin_chat(mut ctx Context) veb.Result { -// return ctx.html(render_chat(app)) -// } +@['/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), 'static', '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) +} -// // Heroprompt page -// @['/admin/heroprompt'; get] -// pub fn (app &App) admin_heroprompt_page(mut ctx Context) veb.Result { -// template_path := os.join_path(os.dir(@FILE), 'templates', 'heroprompt.html') -// template_content := os.read_file(template_path) or { return ctx.text('template not found') } -// menu_content := menu_html(app.menu, 0, 'm') -// 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_heroprompt_url}}', '/static/css/heroprompt.css?v=2') -// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js?v=2') -// result = result.replace('{{.js_heroprompt_url}}', '/static/js/heroprompt.js?v=2') -// return ctx.html(result) -// } +// Admin home page +@['/admin'; get] +pub fn (app &App) admin_index(mut ctx Context) veb.Result { + return ctx.html(render_admin(app, '/', 'Welcome')) +} -// // Static Heroprompt assets -// @['/static/css/heroprompt.css'; get] -// pub fn (app &App) serve_heroprompt_css(mut ctx Context) veb.Result { -// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'heroprompt.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) -// } +// Feature routes registered here, using imported feature renderers -// @['/static/js/heroprompt.js'; get] -// pub fn (app &App) serve_heroprompt_js(mut ctx Context) veb.Result { -// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'heroprompt.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) -// } +// Catch-all content under /admin/* +@['/admin/:path...'; get] +pub fn (app &App) admin_section(mut ctx Context, path string) veb.Result { + // Route to specific feature renderers + match path { + 'heroprompt' { + return ctx.html(render_heroprompt(app)) + } + 'heroscript' { + return ctx.html(render_heroscript(app)) + } + 'chat' { + return ctx.html(render_chat(app)) + } + else { + return ctx.html(render_admin(app, path, 'Content')) + } + } +} -// // 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) -// } +// Pure functions for rendering templates +fn render_admin(app &App, path string, heading string) string { + template_path := os.join_path(os.dir(@FILE), 'templates', 'admin', 'layout.html') + template_content := os.read_file(template_path) or { + return render_admin_fallback(app, path, heading) + } + menu_content := menu_html(app.menu, 0, 'm') + 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 +} -// @['/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) -// } +fn render_heroscript(app &App) string { + template_path := os.join_path(os.dir(@FILE), 'heroscript', 'templates', 'heroscript_editor.html') + template_content := os.read_file(template_path) or { return render_heroscript_fallback(app) } + menu_content := menu_html(app.menu, 0, 'm') + 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/heroscript/css/heroscript.css') + result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') + result = result.replace('{{.js_heroscript_url}}', '/static/heroscript/js/heroscript.js') + return result +} -// // 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) -// } +fn render_heroprompt(app &App) string { + template_path := os.join_path(os.dir(@FILE), 'heroprompt', 'templates', 'heroprompt.html') + template_content := os.read_file(template_path) or { return render_heroprompt_fallback(app) } + menu_content := menu_html(app.menu, 0, 'm') + 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_heroprompt_url}}', '/static/heroprompt/css/heroprompt.css') + result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') + result = result.replace('{{.js_heroprompt_url}}', '/static/heroprompt/js/heroprompt.js') + // version banner + result = result.replace('', '
    Rendered by: heroprompt
    ') + return result +} -// @['/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) -// } +fn render_chat(app &App) string { + template_path := os.join_path(os.dir(@FILE), 'chat', 'templates', 'chat.html') + template_content := os.read_file(template_path) or { return render_chat_fallback(app) } + menu_content := menu_html(app.menu, 0, 'm') + 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/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') + return result +} -// @['/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) -// } +// Fallbacks +fn render_heroprompt_fallback(app &App) string { + return '\n\n\n\n\t\n\t\n\t${app.title} - Heroprompt\n\t\n\n\n\t
    \n\t\t

    Heroprompt

    \n\t\t

    Heroprompt template not found. Please check the template files.

    \n\t\tBack to Admin\n\t
    \n\n\n' +} -// @['/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) -// } +fn render_heroscript_fallback(app &App) string { + return '\n\n\n\n\t\n\t\n\t${app.title} - HeroScript Editor\n\t\n\n\n\t
    \n\t\t

    HeroScript Editor

    \n\t\t

    HeroScript editor template not found. Please check the template files.

    \n\t\tBack to Admin\n\t
    \n\n\n' +} -// @['/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) -// } +fn render_chat_fallback(app &App) string { + return '\n\n\n\n\t\n\t\n\t${app.title} - Chat\n\t\n\n\n\t
    \n\t\t

    Chat Assistant

    \n\t\t

    Chat template not found. Please check the template files.

    \n\t\tBack to Admin\n\t
    \n\n\n' +} -// // 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(render_admin(app, path, 'Content')) -// } - -// // Pure functions for rendering templates -// fn render_admin(app &App, path string, heading string) string { -// template_path := os.join_path(os.dir(@FILE), 'templates', 'admin_layout.html') -// template_content := os.read_file(template_path) or { -// return render_admin_fallback(app, path, heading) -// } -// menu_content := menu_html(app.menu, 0, 'm') -// 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 -// } - -// fn render_heroscript(app &App) string { -// template_path := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.html') -// template_content := os.read_file(template_path) or { return render_heroscript_fallback(app) } -// menu_content := menu_html(app.menu, 0, 'm') -// 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 -// } - -// fn render_chat(app &App) string { -// template_path := os.join_path(os.dir(@FILE), 'templates', 'chat.html') -// template_content := os.read_file(template_path) or { return render_chat_fallback(app) } -// menu_content := menu_html(app.menu, 0, 'm') -// 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 -// } - -// // Fallbacks -// fn render_heroscript_fallback(app &App) string { -// return '\n\n\n\n\t\n\t\n\t${app.title} - HeroScript Editor\n\t\n\n\n\t
    \n\t\t

    HeroScript Editor

    \n\t\t

    HeroScript editor template not found. Please check the template files.

    \n\t\tBack to Admin\n\t
    \n\n\n' -// } - -// fn render_chat_fallback(app &App) string { -// return '\n\n\n\n\t\n\t\n\t${app.title} - Chat\n\t\n\n\n\t
    \n\t\t

    Chat Assistant

    \n\t\t

    Chat template not found. Please check the template files.

    \n\t\tBack to Admin\n\t
    \n\n\n' -// } - -// fn render_admin_fallback(app &App, path string, heading string) string { -// return '\n\n\n\n\t\n\t\n\t${app.title}\n\t\n\t\n\n\n\t\n\n\t\n\n\t
    \n\t\t
    \n\t\t\t
    \n\t\t\t\t
    ${heading}
    \n\t\t\t\t/admin/${path}\n\t\t\t
    \n\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t

    This is a placeholder admin content area for: /admin/${path}.

    \n\t\t\t\t\t

    Use the treeview on the left to navigate.

    \n\t\t\t\t
    \n\t\t\t
    \n\t\t
    \n\t
    \n\n\t\n\n\n' -// } +fn render_admin_fallback(app &App, path string, heading string) string { + return '\n\n\n\n\t\n\t\n\t${app.title}\n\t\n\t\n\n\n\t\n\n\t\n\n\t
    \n\t\t
    \n\t\t\t
    \n\t\t\t\t
    ${heading}
    \n\t\t\t\t/admin/${path}\n\t\t\t
    \n\t\t\t
    \n\t\t\t\t
    \n\t\t\t\t\t

    This is a placeholder admin content area for: /admin/${path}.

    \n\t\t\t\t\t

    Use the treeview on the left to navigate.

    \n\t\t\t\t
    \n\t\t\t
    \n\t\t
    \n\t
    \n\n\t\n\n\n' +} diff --git a/lib/web/ui/templates/css/colors.css b/lib/web/ui/static/css/colors.css similarity index 100% rename from lib/web/ui/templates/css/colors.css rename to lib/web/ui/static/css/colors.css diff --git a/lib/web/ui/templates/css/main.css b/lib/web/ui/static/css/main.css similarity index 100% rename from lib/web/ui/templates/css/main.css rename to lib/web/ui/static/css/main.css diff --git a/lib/web/ui/templates/js/theme.js b/lib/web/ui/static/js/theme.js similarity index 100% rename from lib/web/ui/templates/js/theme.js rename to lib/web/ui/static/js/theme.js diff --git a/lib/web/ui/templates/admin_layout.html b/lib/web/ui/templates/admin/layout.html similarity index 99% rename from lib/web/ui/templates/admin_layout.html rename to lib/web/ui/templates/admin/layout.html index 66561001..d899c806 100644 --- a/lib/web/ui/templates/admin_layout.html +++ b/lib/web/ui/templates/admin/layout.html @@ -44,4 +44,5 @@ - \ No newline at end of file + + diff --git a/lib/web/ui/utils.v b/lib/web/ui/utils.v index e21fe0cc..407c47e8 100644 --- a/lib/web/ui/utils.v +++ b/lib/web/ui/utils.v @@ -1,30 +1,55 @@ module ui -// // Recursive menu renderer -// fn menu_html(items []MenuItem, depth int, prefix string) string { -// mut out := []string{} -// for i, it in items { -// id := '${prefix}_${depth}_${i}' -// if it.children.len > 0 { -// // expandable group -// out << '
    ' -// out << '' -// out << '${it.title}' -// out << '' -// out << '
    ' -// out << '
    ' -// out << menu_html(it.children, depth + 1, id) -// out << '
    ' -// out << '
    ' -// out << '
    ' -// } else { -// // leaf -// out << '${it.title}' -// } -// } -// return out.join('\n') -// } +import os + +// Recursive menu renderer +pub fn menu_html(items []MenuItem, depth int, prefix string) string { + mut out := []string{} + for i, it in items { + id := '${prefix}_${depth}_${i}' + if it.children.len > 0 { + // expandable group + out << '
    ' + out << '' + out << '${it.title}' + out << '' + out << '
    ' + out << '
    ' + out << menu_html(it.children, depth + 1, id) + out << '
    ' + out << '
    ' + out << '
    ' + } else { + // leaf + out << '${it.title}' + } + } + return out.join('\n') +} + +// Language detection utility for code files +pub 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' } + } +}