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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)!
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
'<!doctype html>\n<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>' +
|
||||
html_escape(app.title) +
|
||||
'</title><link rel="stylesheet" href="/static/css/main.css"></head><body><div class="container"><h1>' +
|
||||
html_escape(app.title) +
|
||||
'</h1><p>Heroprompt server is running.</p></div><script src="/static/js/main.js"></script></body></html>'
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{.title}} - Heroprompt</title>
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="toolbar">
|
||||
<div class="title">{{.title}}</div>
|
||||
<span class="spacer"></span>
|
||||
<button id="toggleTheme" title="Toggle dark/light">🌓</button>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="compose">Compose</button>
|
||||
<button class="tab" data-tab="chat">Chat</button>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<section id="tab-compose" class="tab-pane active">
|
||||
<div class="layout">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<select id="workspaceSelect"></select>
|
||||
<button id="refreshWs">Refresh</button>
|
||||
<button id="wsCreateBtn" title="Create a new workspace">+</button>
|
||||
<button id="wsDetailsBtn" title="Workspace details">Details</button>
|
||||
<span class="spacer"></span>
|
||||
</div>
|
||||
<div class="sidebar-body">
|
||||
<div class="searchbar">
|
||||
<input id="search" placeholder="Search files..." />
|
||||
<div class="ws-info" id="wsInfo"></div>
|
||||
<button id="doSearch">Search</button>
|
||||
</div>
|
||||
<div id="tree"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="prompt">
|
||||
<h3>Prompt</h3>
|
||||
<textarea id="promptText" placeholder="Type your instructions here..."></textarea>
|
||||
<div class="actions"><button id="copyPrompt">Copy Prompt</button></div>
|
||||
</div>
|
||||
<div class="selection">
|
||||
<h3>Selected Files (<span id="selCount">0</span>) — Tokens: <span id="tokenCount">0</span></h3>
|
||||
<ul id="selected"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="tab-chat" class="tab-pane">
|
||||
<div class="chat">
|
||||
<div id="chatMessages" class="messages"></div>
|
||||
<div class="chat-input">
|
||||
<textarea id="chatInput" placeholder="Type a message..."></textarea>
|
||||
<button id="sendChat">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/main.js" defer></script>
|
||||
<!-- Modal: Create Workspace -->
|
||||
<div id="wsCreate" class="modal" aria-hidden="true">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h4>Create workspace</h4>
|
||||
<button id="wcClose" class="icon">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label for="wcName">Workspace name (optional)</label>
|
||||
<input id="wcName" placeholder="leave blank to auto-generate" />
|
||||
<small class="hint">If blank we will generate one for you</small>
|
||||
<label for="wcPath">Workspace path (required)</label>
|
||||
<input id="wcPath" placeholder="/full/path e.g. ~/code/github/project1" />
|
||||
<div id="wcError" class="error" role="alert"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="wcCancel">Cancel</button>
|
||||
<button id="wcCreate">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal: Workspace Details -->
|
||||
<div id="wsDetails" class="modal" aria-hidden="true">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h4>Workspace details</h4>
|
||||
<button id="wdClose" class="icon">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label for="wdName">Name</label>
|
||||
<input id="wdName" />
|
||||
<label for="wdPath">Path</label>
|
||||
<input id="wdPath" />
|
||||
<div id="wdError" class="error" role="alert"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="wdDelete">Delete</button>
|
||||
<span class="spacer"></span>
|
||||
<button id="wdCancel">Close</button>
|
||||
<button id="wdSave">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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()
|
||||
}
|
||||
21
lib/web/ui/chat/endpoints.v
Normal file
21
lib/web/ui/chat/endpoints.v
Normal file
@@ -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('</body>', '<div class="v-badge">Rendered by: chat</div></body>')
|
||||
return result
|
||||
}
|
||||
4
lib/web/ui/chat/utils.v
Normal file
4
lib/web/ui/chat/utils.v
Normal file
@@ -0,0 +1,4 @@
|
||||
module chat
|
||||
|
||||
// Placeholder for chat-specific utilities
|
||||
|
||||
@@ -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}')
|
||||
// }
|
||||
@@ -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 '
|
||||
// <!doctype html>
|
||||
// <html lang="en">
|
||||
// <head>
|
||||
// <meta charset="utf-8">
|
||||
// <meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
// <title>${app.title} - HeroScript Editor</title>
|
||||
// <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
// </head>
|
||||
// <body>
|
||||
// <div class="container mt-5">
|
||||
// <h1>HeroScript Editor</h1>
|
||||
// <p>HeroScript editor template not found. Please check the template files.</p>
|
||||
// <a href="/admin" class="btn btn-primary">Back to Admin</a>
|
||||
// </div>
|
||||
// </body>
|
||||
// </html>
|
||||
// '
|
||||
// }
|
||||
|
||||
// // Fallback Chat rendering method
|
||||
// fn (app &App) render_chat_fallback() string {
|
||||
// return '
|
||||
// <!doctype html>
|
||||
// <html lang="en">
|
||||
// <head>
|
||||
// <meta charset="utf-8">
|
||||
// <meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
// <title>${app.title} - Chat</title>
|
||||
// <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
// </head>
|
||||
// <body>
|
||||
// <div class="container mt-5">
|
||||
// <h1>Chat Assistant</h1>
|
||||
// <p>Chat template not found. Please check the template files.</p>
|
||||
// <a href="/admin" class="btn btn-primary">Back to Admin</a>
|
||||
// </div>
|
||||
// </body>
|
||||
// </html>
|
||||
// '
|
||||
// }
|
||||
|
||||
// // Fallback rendering method (inline template)
|
||||
// fn (app &App) render_admin_fallback(path string, heading string) string {
|
||||
// return '
|
||||
// <!doctype html>
|
||||
// <html lang="en">
|
||||
// <head>
|
||||
// <meta charset="utf-8">
|
||||
// <meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
// <title>${app.title}</title>
|
||||
// <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
// <style>
|
||||
// body { padding-top: 44px; }
|
||||
// .header {
|
||||
// height: 44px;
|
||||
// line-height: 44px;
|
||||
// font-size: 14px;
|
||||
// }
|
||||
// .sidebar {
|
||||
// position: fixed;
|
||||
// top: 44px;
|
||||
// bottom: 0;
|
||||
// left: 0;
|
||||
// width: 260px;
|
||||
// overflow-y: auto;
|
||||
// background: #f8f9fa;
|
||||
// border-right: 1px solid #e0e0e0;
|
||||
// }
|
||||
// .main {
|
||||
// margin-left: 260px;
|
||||
// padding: 16px;
|
||||
// }
|
||||
// .list-group-item {
|
||||
// border: 0;
|
||||
// padding: .35rem .75rem;
|
||||
// background: transparent;
|
||||
// }
|
||||
// .menu-leaf a {
|
||||
// color: #212529;
|
||||
// text-decoration: none;
|
||||
// }
|
||||
// .menu-toggle {
|
||||
// text-decoration: none;
|
||||
// color: #212529;
|
||||
// }
|
||||
// .menu-toggle .chev {
|
||||
// font-size: 10px;
|
||||
// opacity: .6;
|
||||
// }
|
||||
// .menu-section {
|
||||
// font-weight: 600;
|
||||
// color: #6c757d;
|
||||
// padding: .5rem .75rem;
|
||||
// }
|
||||
// </style>
|
||||
// </head>
|
||||
// <body>
|
||||
// <nav class="navbar navbar-dark bg-dark fixed-top header px-2">
|
||||
// <div class="d-flex w-100 align-items-center justify-content-between">
|
||||
// <div class="text-white fw-bold">${app.title}</div>
|
||||
// <div class="text-white-50">Admin</div>
|
||||
// </div>
|
||||
// </nav>
|
||||
|
||||
// <aside class="sidebar">
|
||||
// <div class="p-2">
|
||||
// <div class="menu-section">Navigation</div>
|
||||
// <div class="list-group list-group-flush">
|
||||
// ${menu_html(app.menu,
|
||||
// 0, 'm')}
|
||||
// </div>
|
||||
// </div>
|
||||
// </aside>
|
||||
|
||||
// <main class="main">
|
||||
// <div class="container-fluid">
|
||||
// <div class="d-flex align-items-center mb-3">
|
||||
// <h5 class="mb-0">${heading}</h5>
|
||||
// <span class="ms-2 text-muted small">/admin/${path}</span>
|
||||
// </div>
|
||||
// <div class="card">
|
||||
// <div class="card-body">
|
||||
// <p class="text-muted">This is a placeholder admin content area for: <code>/admin/${path}</code>.</p>
|
||||
// <p class="mb-0">Use the treeview on the left to navigate.</p>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// </main>
|
||||
|
||||
// <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
// </body>
|
||||
// </html>
|
||||
// '
|
||||
// }
|
||||
|
||||
// // 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 << '<div class="list-group-item">'
|
||||
// out << '<a class="menu-toggle d-flex align-items-center justify-content-between" data-bs-toggle="collapse" href="#${id}" role="button" aria-expanded="${if depth == 0 {
|
||||
// 'true'
|
||||
// } else {
|
||||
// 'false'
|
||||
// }}" aria-controls="${id}">'
|
||||
// out << '<span>${it.title}</span><span class="chev">›</span>'
|
||||
// out << '</a>'
|
||||
// out << '<div class="collapse ${if depth == 0 { 'show' } else { '' }}" id="${id}">'
|
||||
// out << '<div class="ms-2 mt-1">'
|
||||
// out << menu_html(it.children, depth + 1, id)
|
||||
// out << '</div>'
|
||||
// out << '</div>'
|
||||
// out << '</div>'
|
||||
// } else {
|
||||
// // leaf
|
||||
// out << '<div class="list-group-item menu-leaf"><a href="${if it.href.len > 0 {
|
||||
// it.href
|
||||
// } else {
|
||||
// '/admin'
|
||||
// }}">${it.title}</a></div>'
|
||||
// }
|
||||
// }
|
||||
// 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'
|
||||
// },
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
// }
|
||||
137
lib/web/ui/heroprompt/endpoints.v
Normal file
137
lib/web/ui/heroprompt/endpoints.v
Normal file
@@ -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)
|
||||
}
|
||||
21
lib/web/ui/heroprompt/page.v
Normal file
21
lib/web/ui/heroprompt/page.v
Normal file
@@ -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('</body>', '<div class="v-badge">Rendered by: heroprompt</div></body>')
|
||||
return result
|
||||
}
|
||||
3
lib/web/ui/heroprompt/utils.v
Normal file
3
lib/web/ui/heroprompt/utils.v
Normal file
@@ -0,0 +1,3 @@
|
||||
module ui
|
||||
|
||||
// Placeholder for heroprompt-specific utilities (if needed later)
|
||||
24
lib/web/ui/heroscript/endpoints.v
Normal file
24
lib/web/ui/heroscript/endpoints.v
Normal file
@@ -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('</body>', '<div class="v-badge">Rendered by: heroscript</div></body>')
|
||||
return result
|
||||
}
|
||||
4
lib/web/ui/heroscript/utils.v
Normal file
4
lib/web/ui/heroscript/utils.v
Normal file
@@ -0,0 +1,4 @@
|
||||
module heroscript
|
||||
|
||||
// Placeholder for heroscript-specific utilities
|
||||
|
||||
42
lib/web/ui/menu.v
Normal file
42
lib/web/ui/menu.v
Normal file
@@ -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' },
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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('</body>', '<div class="v-badge">Rendered by: heroprompt</div></body>')
|
||||
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<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - Heroprompt</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>Heroprompt</h1>\n\t\t<p>Heroprompt template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\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<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - HeroScript Editor</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>HeroScript Editor</h1>\n\t\t<p>HeroScript editor template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\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<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - Chat</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>Chat Assistant</h1>\n\t\t<p>Chat template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\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<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - HeroScript Editor</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>HeroScript Editor</h1>\n\t\t<p>HeroScript editor template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\n'
|
||||
// }
|
||||
|
||||
// fn render_chat_fallback(app &App) string {
|
||||
// return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - Chat</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>Chat Assistant</h1>\n\t\t<p>Chat template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\n'
|
||||
// }
|
||||
|
||||
// fn render_admin_fallback(app &App, path string, heading string) string {
|
||||
// return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title}</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">\n\t<style>body { padding-top: 44px; } .header { height: 44px; line-height: 44px; font-size: 14px; } .sidebar { position: fixed; top: 44px; bottom: 0; left: 0; width: 260px; overflow-y: auto; background: #f8f9fa; border-right: 1px solid #e0e0e0; } .main { margin-left: 260px; padding: 16px; } .list-group-item { border: 0; padding: .35rem .75rem; background: transparent; } .menu-leaf a { color: #212529; text-decoration: none; } .menu-toggle { text-decoration: none; color: #212529; } .menu-toggle .chev { font-size: 10px; opacity: .6; } .menu-section { font-weight: 600; color: #6c757d; padding: .5rem .75rem; }</style>\n</head>\n<body>\n\t<nav class="navbar navbar-dark bg-dark fixed-top header px-2">\n\t\t<div class="d-flex w-100 align-items-center justify-content-between">\n\t\t\t<div class="text-white fw-bold">${app.title}</div>\n\t\t\t<div class="text-white-50">Admin</div>\n\t\t</div>\n\t</nav>\n\n\t<aside class="sidebar">\n\t\t<div class="p-2">\n\t\t\t<div class="menu-section">Navigation</div>\n\t\t\t<div class="list-group list-group-flush">\n\t\t\t\t${menu_html(app.menu,
|
||||
// 0, 'm')}\n\t\t\t</div>\n\t\t</div>\n\t</aside>\n\n\t<main class="main">\n\t\t<div class="container-fluid">\n\t\t\t<div class="d-flex align-items-center mb-3">\n\t\t\t\t<h5 class="mb-0">${heading}</h5>\n\t\t\t\t<span class="ms-2 text-muted small">/admin/${path}</span>\n\t\t\t</div>\n\t\t\t<div class="card">\n\t\t\t\t<div class="card-body">\n\t\t\t\t\t<p class="text-muted">This is a placeholder admin content area for: <code>/admin/${path}</code>.</p>\n\t\t\t\t\t<p class="mb-0">Use the treeview on the left to navigate.</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n\n\t<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>\n</body>\n</html>\n'
|
||||
// }
|
||||
fn render_admin_fallback(app &App, path string, heading string) string {
|
||||
return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title}</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">\n\t<style>body { padding-top: 44px; } .header { height: 44px; line-height: 44px; font-size: 14px; } .sidebar { position: fixed; top: 44px; bottom: 0; left: 0; width: 260px; overflow-y: auto; background: #f8f9fa; border-right: 1px solid #e0e0e0; } .main { margin-left: 260px; padding: 16px; } .list-group-item { border: 0; padding: .35rem .75rem; background: transparent; } .menu-leaf a { color: #212529; text-decoration: none; } .menu-toggle { text-decoration: none; color: #212529; } .menu-toggle .chev { font-size: 10px; opacity: .6; } .menu-section { font-weight: 600; color: #6c757d; padding: .5rem .75rem; }</style>\n</head>\n<body>\n\t<nav class="navbar navbar-dark bg-dark fixed-top header px-2">\n\t\t<div class="d-flex w-100 align-items-center justify-content-between">\n\t\t\t<div class="text-white fw-bold">${app.title}</div>\n\t\t\t<div class="text-white-50">Admin</div>\n\t\t</div>\n\t</nav>\n\n\t<aside class="sidebar">\n\t\t<div class="p-2">\n\t\t\t<div class="menu-section">Navigation</div>\n\t\t\t<div class="list-group list-group-flush">\n\t\t\t\t${menu_html(app.menu,
|
||||
0, 'm')}\n\t\t\t</div>\n\t\t</div>\n\t</aside>\n\n\t<main class="main">\n\t\t<div class="container-fluid">\n\t\t\t<div class="d-flex align-items-center mb-3">\n\t\t\t\t<h5 class="mb-0">${heading}</h5>\n\t\t\t\t<span class="ms-2 text-muted small">/admin/${path}</span>\n\t\t\t</div>\n\t\t\t<div class="card">\n\t\t\t\t<div class="card-body">\n\t\t\t\t\t<p class="text-muted">This is a placeholder admin content area for: <code>/admin/${path}</code>.</p>\n\t\t\t\t\t<p class="mb-0">Use the treeview on the left to navigate.</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n\n\t<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>\n</body>\n</html>\n'
|
||||
}
|
||||
|
||||
@@ -45,3 +45,4 @@
|
||||
<script src="{{.js_theme_url}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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 << '<div class="item">'
|
||||
// out << '<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#${id}" role="button" aria-expanded="${if depth == 0 {
|
||||
// 'true'
|
||||
// } else {
|
||||
// 'false'
|
||||
// }}" aria-controls="${id}">'
|
||||
// out << '<span>${it.title}</span><span class="chev">›</span>'
|
||||
// out << '</a>'
|
||||
// out << '<div class="collapse ${if depth == 0 { 'show' } else { '' }}" id="${id}">'
|
||||
// out << '<div class="ms-2 mt-1">'
|
||||
// out << menu_html(it.children, depth + 1, id)
|
||||
// out << '</div>'
|
||||
// out << '</div>'
|
||||
// out << '</div>'
|
||||
// } else {
|
||||
// // leaf
|
||||
// out << '<a href="${it.href}" class="list-group-item list-group-item-action">${it.title}</a>'
|
||||
// }
|
||||
// }
|
||||
// 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 << '<div class="item">'
|
||||
out << '<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#${id}" role="button" aria-expanded="${if depth == 0 {
|
||||
'true'
|
||||
} else {
|
||||
'false'
|
||||
}}" aria-controls="${id}">'
|
||||
out << '<span>${it.title}</span><span class="chev">›</span>'
|
||||
out << '</a>'
|
||||
out << '<div class="collapse ${if depth == 0 { 'show' } else { '' }}" id="${id}">'
|
||||
out << '<div class="ms-2 mt-1">'
|
||||
out << menu_html(it.children, depth + 1, id)
|
||||
out << '</div>'
|
||||
out << '</div>'
|
||||
out << '</div>'
|
||||
} else {
|
||||
// leaf
|
||||
out << '<a href="${it.href}" class="list-group-item list-group-item-action">${it.title}</a>'
|
||||
}
|
||||
}
|
||||
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' }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user