Merge branch 'development_heroprompt' of github.com:freeflowuniverse/herolib into development_heroprompt

This commit is contained in:
2025-08-21 12:11:11 +02:00
5 changed files with 901 additions and 901 deletions

View File

@@ -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
// )!
// }

View File

@@ -1,244 +1,244 @@
module ui
import veb
import freeflowuniverse.herolib.develop.heroprompt
import os
import json
// 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']
}
// // 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
}
// 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/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
}))
}
// @['/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' }
}
}
// 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: 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 { '' }
// // 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"}')
}
// 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"}')
}
// 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
}))
}
// 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 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: 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: 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: 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: 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 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}')
}
// // 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}')
// }

View File

@@ -1,471 +1,471 @@
module ui
import veb
import os
// import veb
// import os
// 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 WebArgs {
pub mut:
name string = 'default'
host string = 'localhost'
port int = 8080
title string = 'Admin'
menu []MenuItem
open bool
}
// // 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
}
// // 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)
}
// // 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
// // 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(app.render_admin('/', 'Welcome'))
}
// // 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())
}
// // 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())
}
// // 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 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/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 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/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/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/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)
}
// @['/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'))
}
// // 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
// // 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')
// 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)
}
// // 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')
// // 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')
// // 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
}
// 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')
// // 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()
}
// // 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')
// // 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')
// // 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
}
// 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')
// // 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()
}
// // 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')
// // 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')
// // 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
}
// 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 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 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>
// // 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>
// <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>
// <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>
'
}
// <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
// // 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">&rsaquo;</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')
}
// 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">&rsaquo;</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'
},
]
},
]
}
// // 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'
// },
// ]
// },
// ]
// }

View File

@@ -1,243 +1,243 @@
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
// import freeflowuniverse.herolib.develop.heroprompt
// 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: args.menu
// port: args.port
// }
// 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'))
}
// // Admin home page
// @['/admin'; get]
// pub fn (app &App) admin_index(mut ctx Context) veb.Result {
// return ctx.html(render_admin(app, '/', 'Welcome'))
// }
// HeroScript editor page
@['/admin/heroscript'; get]
pub fn (app &App) admin_heroscript(mut ctx Context) veb.Result {
return ctx.html(render_heroscript(app))
}
// // HeroScript editor page
// @['/admin/heroscript'; get]
// pub fn (app &App) admin_heroscript(mut ctx Context) veb.Result {
// return ctx.html(render_heroscript(app))
// }
// Chat page
@['/admin/chat'; get]
pub fn (app &App) admin_chat(mut ctx Context) veb.Result {
return ctx.html(render_chat(app))
}
// // Chat page
// @['/admin/chat'; get]
// pub fn (app &App) admin_chat(mut ctx Context) veb.Result {
// return ctx.html(render_chat(app))
// }
// 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)
}
// // 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)
// }
// 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)
}
// // 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)
// }
@['/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)
}
// @['/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)
// }
// 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 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/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 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/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/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/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)
}
// @['/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(render_admin(app, path, '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(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
}
// // 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_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
}
// 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'
}
// // 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_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'
// }

View File

@@ -1,30 +1,30 @@
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">&rsaquo;</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')
}
// // 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">&rsaquo;</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')
// }