diff --git a/aiprompts/herolib_advanced/ui console chalk.md b/aiprompts/herolib_advanced/ui console chalk.md index 5ebbbf88..0b267216 100644 --- a/aiprompts/herolib_advanced/ui console chalk.md +++ b/aiprompts/herolib_advanced/ui console chalk.md @@ -7,7 +7,7 @@ Chalk offers functions:- `console.color_fg(text string, color string)` - To chan Example: -```vlang +```v import freeflowuniverse.herolib.ui.console # basic usage diff --git a/aiprompts/herolib_core/core_params.md b/aiprompts/herolib_core/core_params.md index 8edc7a0a..36c86077 100644 --- a/aiprompts/herolib_core/core_params.md +++ b/aiprompts/herolib_core/core_params.md @@ -25,7 +25,7 @@ The parser supports various input formats: 4. **Comments**: `// this is a comment` (ignored during parsing) Example: -```vlang +```v text := "name:'John Doe' age:30 active:true // user details" params := paramsparser.new(text)! ``` diff --git a/aiprompts/herolib_core/core_ui_console.md b/aiprompts/herolib_core/core_ui_console.md index 8203c25b..4a623c5a 100644 --- a/aiprompts/herolib_core/core_ui_console.md +++ b/aiprompts/herolib_core/core_ui_console.md @@ -4,7 +4,7 @@ has mechanisms to print better to console, see the methods below import as -```vlang +```v import freeflowuniverse.herolib.ui.console ``` diff --git a/aiprompts/herolib_core/core_vshell.md b/aiprompts/herolib_core/core_vshell.md index ce06efec..9c6c6095 100644 --- a/aiprompts/herolib_core/core_vshell.md +++ b/aiprompts/herolib_core/core_vshell.md @@ -2,7 +2,7 @@ this is how we want example scripts to be, see the first line -```vlang +```v #!/usr/bin/env -S v -cg -gc none -cc tcc -d use_openssl -enable-globals run import freeflowuniverse.herolib... diff --git a/aiprompts/instructions_archive/models_from_v/complete.md b/aiprompts/instructions_archive/models_from_v/complete.md index b0babcb1..da56fd9f 100644 --- a/aiprompts/instructions_archive/models_from_v/complete.md +++ b/aiprompts/instructions_archive/models_from_v/complete.md @@ -926,7 +926,7 @@ The parser supports various input formats: 4. **Comments**: `// this is a comment` (ignored during parsing) Example: -```vlang +```v text := "name:'John Doe' age:30 active:true // user details" params := paramsparser.new(text)! ``` @@ -1278,7 +1278,7 @@ has mechanisms to print better to console, see the methods below import as -```vlang +```v import freeflowuniverse.herolib.ui.console ``` @@ -1481,7 +1481,7 @@ File: /Users/despiegk/code/github/freeflowuniverse/herolib/aiprompts/herolib_cor this is how we want example scripts to be, see the first line -```vlang +```v #!/usr/bin/env -S v -cg -gc none -cc tcc -d use_openssl -enable-globals run import freeflowuniverse.herolib... diff --git a/aiprompts/v_advanced/time instructions.md b/aiprompts/v_advanced/time instructions.md index 51552ebe..f84cd515 100644 --- a/aiprompts/v_advanced/time instructions.md +++ b/aiprompts/v_advanced/time instructions.md @@ -83,7 +83,7 @@ fn main() { } ``` -```vlang +```v module time diff --git a/cli/hero.v b/cli/hero.v index b32042ad..35facb7a 100644 --- a/cli/hero.v +++ b/cli/hero.v @@ -48,7 +48,7 @@ fn do() ! { mut cmd := Command{ name: 'hero' description: 'Your HERO toolset.' - version: '1.0.28' + version: '1.0.29' } // herocmds.cmd_run_add_flags(mut cmd) @@ -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) @@ -102,4 +102,4 @@ fn main() { // fn pre_func(cmd Command) ! { // herocmds.plbook_run(cmd)! -// } +// } \ No newline at end of file diff --git a/examples/threefold/grid/README.md b/examples/threefold/grid/README.md index 33f353a8..23c04fe9 100644 --- a/examples/threefold/grid/README.md +++ b/examples/threefold/grid/README.md @@ -6,7 +6,7 @@ To be able to run examples you need to install updated version of `griddriver`. Create some `griddriver_install.vsh` file containing following code: -```vlang +```v #!/usr/bin/env -S v -gc none -cc tcc -d use_openssl -enable-globals run import freeflowuniverse.herolib.installers.tfgrid.griddriver as griddriverinstaller diff --git a/examples/web/doctree/content/test.md b/examples/web/doctree/content/test.md index 00b22b0e..591dbadd 100644 --- a/examples/web/doctree/content/test.md +++ b/examples/web/doctree/content/test.md @@ -25,7 +25,7 @@ text in paragraph | February | $80 | | March | $420 | -```vlang +```v fn main(){ println('hello world') } diff --git a/examples/web/ui_demo.vsh b/examples/web/ui_demo.vsh index 50711018..10f395eb 100755 --- a/examples/web/ui_demo.vsh +++ b/examples/web/ui_demo.vsh @@ -1,13 +1,13 @@ #!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run -// import freeflowuniverse.herolib.web.ui +import freeflowuniverse.herolib.web.ui -// fn main() { -// println('Starting UI test server on port 8080...') -// println('Visit http://localhost:8080 to see the admin interface') +fn main() { + println('Starting UI test server on port 8080...') + println('Visit http://localhost:8080 to see the admin interface') -// ui.start( -// title: 'Test Admin Panel' -// port: 8080 -// )! -// } + ui.start( + title: 'Test Admin Panel' + port: 8080 + )! +} diff --git a/install_hero.sh b/install_hero.sh index 89ce3aa8..720f70dd 100755 --- a/install_hero.sh +++ b/install_hero.sh @@ -4,7 +4,7 @@ set -e os_name="$(uname -s)" arch_name="$(uname -m)" -version='1.0.28' +version='1.0.29' # Base URL for GitHub releases diff --git a/lib/clients/ipapi/readme.md b/lib/clients/ipapi/readme.md index 01288e93..e246cda7 100644 --- a/lib/clients/ipapi/readme.md +++ b/lib/clients/ipapi/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.clients. ipapi diff --git a/lib/clients/jina/readme.md b/lib/clients/jina/readme.md index cad966b1..987d4cbd 100644 --- a/lib/clients/jina/readme.md +++ b/lib/clients/jina/readme.md @@ -6,7 +6,7 @@ see https://jina.ai/ To get started -```vlang +```v import freeflowuniverse.herolib.clients. jina diff --git a/lib/clients/mailclient/readme.md b/lib/clients/mailclient/readme.md index d3862b36..fd44ffa9 100644 --- a/lib/clients/mailclient/readme.md +++ b/lib/clients/mailclient/readme.md @@ -3,7 +3,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.clients.mailclient diff --git a/lib/clients/openai/readme.md b/lib/clients/openai/readme.md index 68f7a5a8..1d6ecc70 100644 --- a/lib/clients/openai/readme.md +++ b/lib/clients/openai/readme.md @@ -2,7 +2,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.clients.openai import freeflowuniverse.herolib.core.playcmds diff --git a/lib/clients/runpod/readme.md b/lib/clients/runpod/readme.md index f04c665d..02d902a1 100644 --- a/lib/clients/runpod/readme.md +++ b/lib/clients/runpod/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v diff --git a/lib/clients/vastai/readme.md b/lib/clients/vastai/readme.md index 6c68d3da..a5f1308b 100644 --- a/lib/clients/vastai/readme.md +++ b/lib/clients/vastai/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.clients. vastai diff --git a/lib/clients/wireguard/readme.md b/lib/clients/wireguard/readme.md index 5fe40e9f..f571524c 100644 --- a/lib/clients/wireguard/readme.md +++ b/lib/clients/wireguard/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.clients. wireguard diff --git a/lib/core/generator/generic/templates/readme.md b/lib/core/generator/generic/templates/readme.md index d7e73a6c..17005bc5 100644 --- a/lib/core/generator/generic/templates/readme.md +++ b/lib/core/generator/generic/templates/readme.md @@ -4,7 +4,7 @@ ${args.title} To get started -```vlang +```v @if args.cat == .installer diff --git a/lib/core/herocmds/web.v b/lib/core/herocmds/web.v index 5ee52fd9..805c68f9 100644 --- a/lib/core/herocmds/web.v +++ b/lib/core/herocmds/web.v @@ -1,110 +1,110 @@ module herocmds -// import freeflowuniverse.herolib.ui.console -// import freeflowuniverse.herolib.web.ui -// import os -// import cli { Command, Flag } -// import time +import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.web.ui +import os +import cli { Command, Flag } +import time -// pub fn cmd_web(mut cmdroot Command) Command { -// mut cmd_run := Command{ -// name: 'web' -// description: 'Run the Heroprompt UI (located in lib/web/heroprompt).' -// required_args: 0 -// execute: cmd_web_execute -// } +pub fn cmd_web(mut cmdroot Command) Command { + mut cmd_run := Command{ + name: 'web' + description: 'Run the Heroprompt UI (located in lib/web/heroprompt).' + required_args: 0 + execute: cmd_web_execute + } -// cmd_run.add_flag(Flag{ -// flag: .bool -// required: false -// name: 'open' -// abbrev: 'o' -// description: 'Open the UI in the default browser after starting the server.' -// }) + cmd_run.add_flag(Flag{ + flag: .bool + required: false + name: 'open' + abbrev: 'o' + description: 'Open the UI in the default browser after starting the server.' + }) -// cmd_run.add_flag(Flag{ -// flag: .string -// required: false -// name: 'host' -// abbrev: 'h' -// description: 'Host to bind the server to (default: localhost).' -// }) + cmd_run.add_flag(Flag{ + flag: .string + required: false + name: 'host' + abbrev: 'h' + description: 'Host to bind the server to (default: localhost).' + }) -// cmd_run.add_flag(Flag{ -// flag: .int -// required: false -// name: 'port' -// abbrev: 'p' -// description: 'Port to bind the server to (default: 8080).' -// }) + cmd_run.add_flag(Flag{ + flag: .int + required: false + name: 'port' + abbrev: 'p' + description: 'Port to bind the server to (default: 8080).' + }) -// cmdroot.add_command(cmd_run) -// return cmdroot -// } + cmdroot.add_command(cmd_run) + return cmdroot +} -// fn cmd_web_execute(cmd Command) ! { -// // ---------- FLAGS ---------- -// mut open_ := cmd.flags.get_bool('open') or { false } -// mut host := cmd.flags.get_string('host') or { 'localhost' } -// mut port := cmd.flags.get_int('port') or { 8080 } +fn cmd_web_execute(cmd Command) ! { + // ---------- FLAGS ---------- + mut open_ := cmd.flags.get_bool('open') or { false } + mut host := cmd.flags.get_string('host') or { 'localhost' } + mut port := cmd.flags.get_int('port') or { 8080 } -// // Set defaults if not provided -// if host == '' { -// host = 'localhost' -// } -// if port == 0 { -// port = 8080 -// } + // Set defaults if not provided + if host == '' { + host = 'localhost' + } + if port == 0 { + port = 8080 + } -// console.print_header('Starting Heroprompt...') + console.print_header('Starting Heroprompt...') -// // Prepare arguments for the UI factory -// mut factory_args := ui.FactoryArgs{ -// title: 'Hero Admin Panel' -// host: host -// port: port -// } + // Prepare arguments for the UI factory + mut factory_args := ui.FactoryArgs{ + title: 'Hero Admin Panel' + host: host + port: port + } -// // ---------- START WEB SERVER ---------- -// console.print_header('Starting Heroprompt server...') + // ---------- START WEB SERVER ---------- + console.print_header('Starting Heroprompt server...') -// // Start the server in a separate thread to allow for browser opening -// spawn fn [factory_args] () { -// ui.start(factory_args) or { -// console.print_stderr('Failed to start Heroprompt server: ${err}') -// return -// } -// }() + // Start the server in a separate thread to allow for browser opening + spawn fn [factory_args] () { + ui.start(factory_args) or { + console.print_stderr('Failed to start Heroprompt server: ${err}') + return + } + }() -// // Give the server a moment to start -// time.sleep(2 * time.second) -// url := 'http://${factory_args.host}:${factory_args.port}' + // Give the server a moment to start + time.sleep(2 * time.second) + url := 'http://${factory_args.host}:${factory_args.port}' -// console.print_green('Heroprompt server is running on ${url}') + console.print_green('Heroprompt server is running on ${url}') -// if open_ { -// mut cmd_str := '' -// $if macos { -// cmd_str = 'open ${url}' -// } $else $if linux { -// cmd_str = 'xdg-open ${url}' -// } $else $if windows { -// cmd_str = 'start ${url}' -// } + if open_ { + mut cmd_str := '' + $if macos { + cmd_str = 'open ${url}' + } $else $if linux { + cmd_str = 'xdg-open ${url}' + } $else $if windows { + cmd_str = 'start ${url}' + } -// if cmd_str != '' { -// result := os.execute(cmd_str) -// if result.exit_code == 0 { -// console.print_green('Opened Heroprompt in default browser.') -// } else { -// console.print_stderr('Failed to open browser: ${result.output}') -// } -// } -// } + if cmd_str != '' { + result := os.execute(cmd_str) + if result.exit_code == 0 { + console.print_green('Opened Heroprompt in default browser.') + } else { + console.print_stderr('Failed to open browser: ${result.output}') + } + } + } -// // Keep the process alive while the server runs -// console.print_header('Press Ctrl+C to stop the server') -// for { -// time.sleep(1 * time.second) -// } -// } + // Keep the process alive while the server runs + console.print_header('Press Ctrl+C to stop the server') + for { + time.sleep(1 * time.second) + } +} diff --git a/lib/develop/codewalker/ignore.v b/lib/develop/codewalker/ignore.v index 432b5a01..1c4aca5e 100644 --- a/lib/develop/codewalker/ignore.v +++ b/lib/develop/codewalker/ignore.v @@ -8,7 +8,51 @@ module codewalker // - Lines starting with '#' are comments; empty lines ignored // No negation support for simplicity -const default_gitignore = '__pycache__/\n*.py[cod]\n*.so\n.Python\nbuild/\ndevelop-eggs/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n.env\n.venv\nvenv/\n.tox/\n.nox/\n.coverage\n.coveragerc\ncoverage.xml\n*.cover\n*.gem\n*.pyc\n.cache\n.pytest_cache/\n.mypy_cache/\n.hypothesis/\n' +const default_gitignore = ' +.git/ +.svn/ +.hg/ +.bzr/ +node_modules/ +__pycache__/ +*.py[cod] +*.so +.Python +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +.env +.venv +venv/ +.tox/ +.nox/ +.coverage +.coveragerc +coverage.xml +*.cover +*.gem +*.pyc +.cache +.pytest_cache/ +.mypy_cache/ +.hypothesis/ +.DS_Store +Thumbs.db +*.tmp +*.temp +*.log +' struct IgnoreRule { base string // relative dir from source root where the ignore file lives ('' means global) diff --git a/lib/develop/codewalker/tree.v b/lib/develop/codewalker/tree.v new file mode 100644 index 00000000..696bfe1d --- /dev/null +++ b/lib/develop/codewalker/tree.v @@ -0,0 +1,244 @@ +module codewalker + +import os + +// build_selected_tree renders a minimal tree of the given file paths. +// - files: absolute or relative file paths +// - base_root: if provided and files are absolute, the tree is rendered relative to this root +// The output marks files with a trailing " *" like the existing map convention. +pub fn build_selected_tree(files []string, base_root string) string { + mut rels := []string{} + for p in files { + mut rp := p + if base_root.len > 0 && rp.starts_with(base_root) { + rp = rp[base_root.len..] + if rp.len > 0 && rp.starts_with('/') { + rp = rp[1..] + } + } + rels << rp + } + rels.sort() + return tree_from_rel_paths(rels, '') +} + +fn tree_from_rel_paths(paths []string, prefix string) string { + mut out := '' + // group into directories and files at the current level + mut dir_children := map[string][]string{} + mut files := []string{} + for p in paths { + parts := p.split('/') + if parts.len <= 1 { + if p.len > 0 { + files << parts[0] + } + } else { + key := parts[0] + rest := parts[1..].join('/') + mut arr := dir_children[key] or { []string{} } + arr << rest + dir_children[key] = arr + } + } + mut dir_names := dir_children.keys() + dir_names.sort() + files.sort() + // render directories first, then files + for j, d in dir_names { + is_last_dir := j == dir_names.len - 1 + connector := if is_last_dir && files.len == 0 { '└── ' } else { '├── ' } + out += '${prefix}${connector}${d}\n' + child_prefix := if is_last_dir && files.len == 0 { + prefix + ' ' + } else { + prefix + '│ ' + } + out += tree_from_rel_paths(dir_children[d], child_prefix) + } + for i, f in files { + file_connector := if i == files.len - 1 { '└── ' } else { '├── ' } + out += '${prefix}${file_connector}${f} *\n' + } + return out +} + +// resolve_path resolves a relative path against a base path. +// If rel_path is absolute, returns it as-is. +// If rel_path is empty, returns base_path. +pub fn resolve_path(base_path string, rel_path string) string { + if rel_path.len == 0 { + return base_path + } + if os.is_abs_path(rel_path) { + return rel_path + } + return os.join_path(base_path, rel_path) +} + +pub struct DirItem { +pub: + name string + typ string +} + +// list_directory lists the contents of a directory. +// - base_path: workspace base path +// - rel_path: relative path from base (or absolute path) +// Returns a list of DirItem with name and type (file/directory). +pub fn list_directory(base_path string, rel_path string) ![]DirItem { + dir := resolve_path(base_path, rel_path) + if dir.len == 0 { + return error('base_path not set') + } + entries := os.ls(dir) or { return error('cannot list directory') } + mut out := []DirItem{} + for e in entries { + full := os.join_path(dir, e) + if os.is_dir(full) { + out << DirItem{ + name: e + typ: 'directory' + } + } else if os.is_file(full) { + out << DirItem{ + name: e + typ: 'file' + } + } + } + return out +} + +// list_directory_filtered lists the contents of a directory with ignore filtering applied. +// - base_path: workspace base path +// - rel_path: relative path from base (or absolute path) +// - ignore_matcher: IgnoreMatcher to filter out ignored files/directories +// Returns a list of DirItem with name and type (file/directory), filtered by ignore patterns. +pub fn list_directory_filtered(base_path string, rel_path string, ignore_matcher &IgnoreMatcher) ![]DirItem { + dir := resolve_path(base_path, rel_path) + if dir.len == 0 { + return error('base_path not set') + } + entries := os.ls(dir) or { return error('cannot list directory') } + mut out := []DirItem{} + for e in entries { + full := os.join_path(dir, e) + + // Calculate relative path from base_path for ignore checking + mut check_path := if rel_path.len > 0 { + if rel_path.ends_with('/') { rel_path + e } else { rel_path + '/' + e } + } else { + e + } + + // For directories, also check with trailing slash + is_directory := os.is_dir(full) + mut should_ignore := ignore_matcher.is_ignored(check_path) + if is_directory && !should_ignore { + // Also check directory pattern with trailing slash + should_ignore = ignore_matcher.is_ignored(check_path + '/') + } + + // Check if this entry should be ignored + if should_ignore { + continue + } + + if is_directory { + out << DirItem{ + name: e + typ: 'directory' + } + } else if os.is_file(full) { + out << DirItem{ + name: e + typ: 'file' + } + } + } + return out +} + +// list_files_recursive recursively lists all files in a directory +pub fn list_files_recursive(root string) []string { + mut out := []string{} + entries := os.ls(root) or { return out } + for e in entries { + fp := os.join_path(root, e) + if os.is_dir(fp) { + out << list_files_recursive(fp) + } else if os.is_file(fp) { + out << fp + } + } + return out +} + +// build_file_tree_fs builds a file system tree for given root directories +pub fn build_file_tree_fs(roots []string, prefix string) string { + mut out := '' + for i, root in roots { + if !os.is_dir(root) { + continue + } + connector := if i == roots.len - 1 { '└── ' } else { '├── ' } + out += '${prefix}${connector}${os.base(root)}\n' + child_prefix := if i == roots.len - 1 { prefix + ' ' } else { prefix + '│ ' } + // list children under root + entries := os.ls(root) or { []string{} } + // sort: dirs first then files + mut dirs := []string{} + mut files := []string{} + for e in entries { + fp := os.join_path(root, e) + if os.is_dir(fp) { + dirs << fp + } else if os.is_file(fp) { + files << fp + } + } + dirs.sort() + files.sort() + // files + for j, f in files { + file_connector := if j == files.len - 1 && dirs.len == 0 { + '└── ' + } else { + '├── ' + } + out += '${child_prefix}${file_connector}${os.base(f)} *\n' + } + // subdirectories + for j, d in dirs { + sub_connector := if j == dirs.len - 1 { '└── ' } else { '├── ' } + out += '${child_prefix}${sub_connector}${os.base(d)}\n' + sub_prefix := if j == dirs.len - 1 { + child_prefix + ' ' + } else { + child_prefix + '│ ' + } + out += build_file_tree_fs([d], sub_prefix) + } + } + return out +} + +// build_file_tree_selected builds a minimal tree that contains only the selected files. +// The tree is rendered relative to base_root when provided. +pub fn build_file_tree_selected(files []string, base_root string) string { + mut rels := []string{} + for fo in files { + mut rp := fo + if base_root.len > 0 && rp.starts_with(base_root) { + // make path relative to the base root + rp = rp[base_root.len..] + if rp.len > 0 && rp.starts_with('/') { + rp = rp[1..] + } + } + rels << rp + } + rels.sort() + return tree_from_rel_paths(rels, '') +} diff --git a/lib/develop/heroprompt/heroprompt_child.v b/lib/develop/heroprompt/heroprompt_child.v index d7b6466b..e57af3a7 100644 --- a/lib/develop/heroprompt/heroprompt_child.v +++ b/lib/develop/heroprompt/heroprompt_child.v @@ -5,9 +5,10 @@ import os pub struct HeropromptChild { pub mut: - content string - path pathlib.Path - name string + content string + path pathlib.Path + name string + include_tree bool // when true and this child is a dir, include full subtree in maps/contents } // Utility function to get file extension with special handling for common files diff --git a/lib/develop/heroprompt/heroprompt_workspace.v b/lib/develop/heroprompt/heroprompt_workspace.v index 6b47c5c8..95a6ac4b 100644 --- a/lib/develop/heroprompt/heroprompt_workspace.v +++ b/lib/develop/heroprompt/heroprompt_workspace.v @@ -39,12 +39,13 @@ pub fn (mut wsp Workspace) add_dir(args AddDirParams) !HeropromptChild { } mut ch := HeropromptChild{ - path: pathlib.Path{ + path: pathlib.Path{ path: abs_path cat: .dir exist: .yes } - name: name + name: name + include_tree: true } wsp.children << ch wsp.save()! @@ -220,55 +221,29 @@ pub: typ string @[json: 'type'] } -pub fn (wsp &Workspace) list() ![]ListItem { - mut dir := wsp.base_path - if dir.len == 0 { - return error('workspace base_path not set') - } - - if !os.is_abs_path(dir) { - dir = os.join_path(wsp.base_path, dir) - } - - entries := os.ls(dir) or { return error('cannot list directory') } +pub fn (wsp &Workspace) list_dir(rel_path string) ![]ListItem { + // Create an ignore matcher with default patterns + ignore_matcher := codewalker.gitignore_matcher_new() + items := codewalker.list_directory_filtered(wsp.base_path, rel_path, &ignore_matcher)! mut out := []ListItem{} - for e in entries { - full := os.join_path(dir, e) - if os.is_dir(full) { - out << ListItem{ - name: e - typ: 'directory' - } - } else if os.is_file(full) { - out << ListItem{ - name: e - typ: 'file' - } + for item in items { + out << ListItem{ + name: item.name + typ: item.typ } } return out } +pub fn (wsp &Workspace) list() ![]ListItem { + return wsp.list_dir('') +} + // Get the currently selected children (copy) pub fn (wsp Workspace) selected_children() []HeropromptChild { return wsp.children.clone() } -// Build utilities -fn list_files_recursive(root string) []string { - mut out := []string{} - entries := os.ls(root) or { return out } - for e in entries { - fp := os.join_path(root, e) - if os.is_dir(fp) { - out << list_files_recursive(fp) - } else if os.is_file(fp) { - out << fp - } - } - return out -} - // build_file_content generates formatted content for all selected files (and all files under selected dirs) fn (wsp Workspace) build_file_content() !string { mut content := '' @@ -295,7 +270,7 @@ fn (wsp Workspace) build_file_content() !string { } // files under selected directories, using CodeWalker for filtered traversal for ch in wsp.children { - if ch.path.cat == .dir { + if ch.path.cat == .dir && ch.include_tree { mut cw := codewalker.new(codewalker.CodeWalkerArgs{})! mut fm := cw.filemap_get(path: ch.path.path)! for rel, fc in fm.content { @@ -316,64 +291,6 @@ fn (wsp Workspace) build_file_content() !string { return content } -// Minimal tree builder for selected directories only; marks files with * -fn build_file_tree_fs(roots []HeropromptChild, prefix string) string { - mut out := '' - for i, root in roots { - if root.path.cat != .dir { - continue - } - connector := if i == roots.len - 1 { '└── ' } else { '├── ' } - out += '${prefix}${connector}${root.name}\n' - child_prefix := if i == roots.len - 1 { prefix + ' ' } else { prefix + '│ ' } - // list children under root - entries := os.ls(root.path.path) or { []string{} } - // sort: dirs first then files - mut dirs := []string{} - mut files := []string{} - for e in entries { - fp := os.join_path(root.path.path, e) - if os.is_dir(fp) { - dirs << fp - } else if os.is_file(fp) { - files << fp - } - } - dirs.sort() - files.sort() - // files - for j, f in files { - file_connector := if j == files.len - 1 && dirs.len == 0 { - '└── ' - } else { - '├── ' - } - out += '${child_prefix}${file_connector}${os.base(f)} *\n' - } - // subdirectories - for j, d in dirs { - sub_connector := if j == dirs.len - 1 { '└── ' } else { '├── ' } - out += '${child_prefix}${sub_connector}${os.base(d)}\n' - sub_prefix := if j == dirs.len - 1 { - child_prefix + ' ' - } else { - child_prefix + '│ ' - } - out += build_file_tree_fs([ - HeropromptChild{ - path: pathlib.Path{ - path: d - cat: .dir - exist: .yes - } - name: os.base(d) - }, - ], sub_prefix) - } - } - return out -} - pub struct HeropromptTmpPrompt { pub mut: user_instructions string @@ -392,42 +309,63 @@ fn (wsp Workspace) build_file_map() string { mut roots := []HeropromptChild{} mut files_only := []HeropromptChild{} for ch in wsp.children { - if ch.path.cat == .dir { - roots << ch + if ch.path.cat == .dir && ch.include_tree { + roots << ch // only include directories explicitly marked to include subtree } else if ch.path.cat == .file { files_only << ch } } - if roots.len > 0 { - base_path := roots[0].path.path - parent_path := if base_path.contains('/') { - base_path.split('/')[..base_path.split('/').len - 1].join('/') + if roots.len > 0 || files_only.len > 0 { + // derive a parent path for display + mut parent_path := '' + if roots.len > 0 { + base_path := roots[0].path.path + parent_path = if base_path.contains('/') { + base_path.split('/')[..base_path.split('/').len - 1].join('/') + } else { + base_path + } } else { - base_path + // no roots; show workspace base if set, else the parent of first file + parent_path = if wsp.base_path.len > 0 { + wsp.base_path + } else if files_only.len > 0 { + os.dir(files_only[0].path.path) + } else { + '' + } } // metadata mut total_files := 0 mut total_content_length := 0 mut file_extensions := map[string]int{} - // files under dirs - for r in roots { - for f in list_files_recursive(r.path.path) { - total_files++ - ext := get_file_extension(os.base(f)) - if ext.len > 0 { - file_extensions[ext] = file_extensions[ext] + 1 + // files under dirs (only when roots present) + if roots.len > 0 { + for r in roots { + for f in codewalker.list_files_recursive(r.path.path) { + total_files++ + ext := get_file_extension(os.base(f)) + if ext.len > 0 { + file_extensions[ext] = file_extensions[ext] + 1 + } + total_content_length += (os.read_file(f) or { '' }).len } - total_content_length += (os.read_file(f) or { '' }).len } } - // files only + // standalone files for fo in files_only { total_files++ ext := get_file_extension(fo.name) if ext.len > 0 { file_extensions[ext] = file_extensions[ext] + 1 } - total_content_length += fo.content.len + // if content not loaded, read length on demand + file_len := if fo.content.len == 0 { + (os.read_file(fo.path.path) or { '' }).len + } else { + fo.content.len + } + total_content_length += file_len } mut extensions_summary := '' for ext, count in file_extensions { @@ -442,10 +380,27 @@ fn (wsp Workspace) build_file_map() string { file_map += ' | Extensions: ${extensions_summary}' } file_map += '\n\n' - file_map += build_file_tree_fs(roots, '') - // list standalone files as well - for fo in files_only { - file_map += fo.path.path + ' *\n' + // Render selected structure + if roots.len > 0 { + mut root_paths := []string{} + for r in roots { + root_paths << r.path.path + } + file_map += codewalker.build_file_tree_fs(root_paths, '') + } + // If there are only standalone selected files (no selected dirs), + // build a minimal tree via codewalker relative to the workspace base. + if files_only.len > 0 && roots.len == 0 { + mut paths := []string{} + for fo in files_only { + paths << fo.path.path + } + file_map += codewalker.build_selected_tree(paths, wsp.base_path) + } else if files_only.len > 0 && roots.len > 0 { + // Keep listing absolute paths for standalone files when directories are also selected. + for fo in files_only { + file_map += fo.path.path + ' *\n' + } } } return file_map @@ -470,16 +425,15 @@ pub fn (wsp Workspace) prompt(args WorkspacePrompt) string { } // Save the workspace -fn (wsp &Workspace) save() !&Workspace { - mut tmp := wsp - tmp.updated = time.now() - tmp.is_saved = true - set(tmp)! +fn (mut wsp Workspace) save() !&Workspace { + wsp.updated = time.now() + wsp.is_saved = true + set(wsp)! return get(name: wsp.name)! } // Generate a random name for the workspace -fn generate_random_workspace_name() string { +pub fn generate_random_workspace_name() string { adjectives := [ 'brave', 'bright', diff --git a/lib/develop/heroprompt/readme.md b/lib/develop/heroprompt/readme.md index 8a7d70aa..2b38b0eb 100644 --- a/lib/develop/heroprompt/readme.md +++ b/lib/develop/heroprompt/readme.md @@ -1,30 +1,28 @@ # heroprompt - - To get started -```vlang +```v -import freeflowuniverse.herolib.clients. heroprompt +import freeflowuniverse.herolib.develop.heroprompt -mut client:= heroprompt.get()! +// Example Usage: -client... +// 1. Create a new workspace +mut workspace := heroprompt.new(name: 'my_workspace', path: os.getwd())! +// 2. Add a directory to the workspace +workspace.add_dir(path: './my_project_dir')! + +// 3. Add a file to the workspace +workspace.add_file(path: './my_project_dir/main.v')! + +// 4. Generate a prompt +user_instructions := 'Explain the code in main.v' +prompt_output := workspace.prompt(text: user_instructions) +println(prompt_output) ``` - -## example heroscript - -```hero -!!heroprompt.configure - secret: '...' - host: 'localhost' - port: 8888 -``` - - diff --git a/lib/develop/reprompt/templates/prompt_example.md b/lib/develop/reprompt/templates/prompt_example.md deleted file mode 100644 index 0675fea0..00000000 --- a/lib/develop/reprompt/templates/prompt_example.md +++ /dev/null @@ -1,1607 +0,0 @@ - -/Users/despiegk/code/github/freeflowuniverse/herolib -├── .gitignore -├── .vdocignore -├── compile.sh -├── CONTRIBUTING.md -├── doc.vsh -├── generate.vsh -├── herolib.code-workspace -├── install_hero.sh -├── install_herolib.vsh -├── install_v.sh -├── LICENSE -├── README.md -├── release_OLD.sh -├── release.vsh -├── specs.md -├── test_basic.vsh -└── test_runner.vsh -├── .github -│ └── workflows -│ ├── documentation.yml -│ ├── hero_build.yml -│ └── test.yml -├── aiprompts -│ └── herolib_start_here.md -│ ├── .openhands -│ │ └── setup.sh -│ ├── ai_instruct -│ │ ├── documentation_from_v_md.md -│ │ ├── documentation_from_v.md -│ │ ├── prompt_processing_instructions.md -│ │ ├── prompt_processing_openrpc_like.md -│ │ └── what_is_a_hero_twin.md -│ │ ├── models_from_v -│ ├── bizmodel -│ │ ├── bizmodel_cost.md -│ │ ├── bizmodel_funding.md -│ │ ├── bizmodel_generation_prompt.md -│ │ ├── bizmodel_hr.md -│ │ ├── bizmodel_revenue.md -│ │ └── costs.heroscript -│ ├── documentor -│ │ └── generate_v_doc_readable_md_files.md -│ ├── docusaurus -│ │ └── docusaurus_ebook_manual.md -│ ├── herolib_advanced -│ │ ├── advanced_paths.md -│ │ ├── builder.md -│ │ ├── cmdline_argument_parsing_example.vsh -│ │ ├── datatypes.md -│ │ ├── osal.md -│ │ ├── ourdb.md -│ │ ├── redis.md -│ │ ├── spreadsheet.md -│ │ └── ui console chalk.md -│ ├── herolib_core -│ │ ├── core_curdir_example.md -│ │ ├── core_globals.md -│ │ ├── core_heroscript_basics.md -│ │ ├── core_heroscript_playbook.md -│ │ ├── core_http_client.md -│ │ ├── core_osal.md -│ │ ├── core_ourtime.md -│ │ ├── core_params.md -│ │ ├── core_paths.md -│ │ ├── core_text.md -│ │ ├── core_ui_console.md -│ │ └── core_vshell.md -│ ├── v_advanced -│ │ ├── advanced_topics.md -│ │ ├── compress.md -│ │ ├── generics.md -│ │ ├── html_parser.md -│ │ ├── io.md -│ │ ├── net.md -│ │ ├── reflection.md -│ │ ├── regex.md -│ │ ├── smtp.md -│ │ └── time instructions.md -│ ├── v_core -│ │ └── v_manual.md -│ ├── v_veb_webserver -│ │ ├── veb_assets.md -│ │ ├── veb_auth.md -│ │ ├── veb_csrf.md -│ │ ├── veb_sse.md -│ │ ├── veb.md -│ │ └── vtemplates.md -├── cli -│ ├── .gitignore -│ ├── compile -│ ├── compile_upload.vsh -│ ├── compile_vdo.vsh -│ ├── compile.vsh -│ ├── hero.v -│ └── vdo.v -├── docker -│ └── docker_ubuntu_install.sh -│ ├── herolib -│ │ ├── .gitignore -│ │ ├── build.sh -│ │ ├── debug.sh -│ │ ├── docker-compose.yml -│ │ ├── Dockerfile -│ │ ├── export.sh -│ │ ├── README.md -│ │ ├── shell.sh -│ │ ├── ssh_init.sh -│ │ ├── ssh.sh -│ │ └── start.sh -│ │ ├── scripts -│ ├── postgresql -│ │ ├── docker-compose.yml -│ │ ├── readme.md -│ │ └── start.sh -├── examples -│ ├── README.md -│ └── sync_do.sh -│ ├── aiexamples -│ │ ├── groq.vsh -│ │ ├── jetconvertor.vsh -│ │ ├── jina.vsh -│ │ └── qdrant.vsh -│ ├── baobab -│ │ ├── generator -│ │ └── specification -│ ├── biztools -│ │ ├── bizmodel_complete.vsh -│ │ ├── bizmodel_export.vsh -│ │ ├── bizmodel_full.vsh -│ │ ├── bizmodel.vsh -│ │ ├── bizmodel1.vsh -│ │ ├── bizmodel2.vsh -│ │ ├── costs.vsh -│ │ ├── funding.vsh -│ │ ├── hr.vsh -│ │ └── notworking.md -│ │ ├── _archive -│ │ ├── bizmodel_docusaurus -│ │ ├── examples -│ ├── builder -│ │ ├── simple_ip4.vsh -│ │ ├── simple_ip6.vsh -│ │ └── simple.vsh -│ │ ├── remote_executor -│ ├── clients -│ │ ├── aiclient_example.vsh -│ │ ├── jina_example.vsh -│ │ ├── mail.vsh -│ │ ├── mycelium_rpc.vsh -│ │ ├── mycelium.vsh -│ │ ├── psql.vsh -│ │ └── zinit_rpc_example.vsh -│ ├── core -│ │ ├── agent_encoding.vsh -│ │ ├── generate.vsh -│ │ └── secrets_example.vsh -│ │ ├── base -│ │ ├── db -│ │ ├── dbfs -│ │ ├── openapi -│ │ ├── openrpc -│ │ ├── pathlib -│ ├── data -│ │ ├── .gitignore -│ │ ├── cache.vsh -│ │ ├── compress_gzip_example.vsh -│ │ ├── deduped_mycelium_master.vsh -│ │ ├── deduped_mycelium_worker.vsh -│ │ ├── encoder.vsh -│ │ ├── encrypt_decrypt.vsh -│ │ ├── graphdb.vsh -│ │ ├── heroencoder_example.vsh -│ │ ├── heroencoder_simple.vsh -│ │ ├── jsonexample.vsh -│ │ ├── ourdb_client.vsh -│ │ ├── ourdb_example.vsh -│ │ ├── ourdb_server.vsh -│ │ ├── radixtree.vsh -│ │ └── regex_example.vsh -│ │ ├── location -│ │ ├── ourdb_syncer -│ │ ├── params -│ │ ├── resp -│ ├── develop -│ │ ├── gittools -│ │ ├── ipapi -│ │ ├── juggler -│ │ ├── luadns -│ │ ├── openai -│ │ ├── runpod -│ │ ├── vastai -│ │ └── wireguard -│ ├── hero -│ │ └── alpine_example.vsh -│ │ ├── db -│ │ ├── generation -│ │ ├── openapi -│ ├── installers -│ │ ├── .gitignore -│ │ ├── cometbft.vsh -│ │ ├── conduit.vsh -│ │ ├── coredns.vsh -│ │ ├── hero_install.vsh -│ │ ├── installers.vsh -│ │ ├── traefik.vsh -│ │ └── youki.vsh -│ │ ├── db -│ │ ├── infra -│ │ ├── lang -│ │ ├── net -│ │ ├── sysadmintools -│ │ ├── threefold -│ │ ├── virt -│ ├── jobs -│ │ └── vfs_jobs_example.vsh -│ ├── lang -│ │ └── python -│ ├── mcp -│ │ ├── http_demo -│ │ ├── http_server -│ │ ├── inspector -│ │ └── simple_http -│ ├── osal -│ │ ├── .gitignore -│ │ ├── notifier.vsh -│ │ ├── startup_manager.vsh -│ │ ├── systemd.vsh -│ │ ├── tun.vsh -│ │ ├── ufw_play.vsh -│ │ └── ufw.vsh -│ │ ├── coredns -│ │ ├── download -│ │ ├── ping -│ │ ├── process -│ │ ├── rsync -│ │ ├── sandbox -│ │ ├── sshagent -│ │ ├── zinit -│ ├── schemas -│ │ ├── openapi -│ │ └── openrpc -│ ├── threefold -│ │ └── .gitignore -│ │ ├── grid -│ │ ├── gridproxy -│ │ ├── holochain -│ │ ├── solana -│ │ ├── tfgrid3deployer -│ ├── tools -│ │ ├── imagemagick -│ │ ├── tmux -│ │ └── vault -│ ├── ui -│ │ ├── flow1.v -│ │ └── silence.vsh -│ │ ├── console -│ │ ├── telegram -│ ├── vfs -│ │ └── vfs_db -│ ├── virt -│ │ ├── daguserver -│ │ ├── docker -│ │ ├── hetzner -│ │ ├── lima -│ │ ├── podman_buildah -│ │ ├── runc -│ │ └── windows -│ ├── web -│ │ ├── .gitignore -│ │ ├── docusaurus_example.vsh -│ │ ├── starllight_example.vsh -│ │ └── ui_demo.vsh -│ │ ├── doctree -│ │ ├── markdown_renderer -│ ├── webdav -│ │ ├── .gitignore -│ │ └── webdav_vfs.vsh -├── lib -│ ├── readme.md -│ └── v.mod -│ ├── ai -│ │ ├── escalayer -│ │ ├── mcp -│ │ └── utils -│ ├── baobab -│ │ └── README.md -│ │ ├── actor -│ │ ├── generator -│ │ ├── osis -│ │ ├── specification -│ │ ├── stage -│ ├── biz -│ │ ├── bizmodel -│ │ ├── investortool -│ │ ├── planner -│ │ └── spreadsheet -│ ├── builder -│ │ ├── bootstrapper.v -│ │ ├── builder_factory.v -│ │ ├── done.v -│ │ ├── executor_local_test.v -│ │ ├── executor_local.v -│ │ ├── executor_ssh_test.v -│ │ ├── executor_ssh.v -│ │ ├── executor.v -│ │ ├── model_package.v -│ │ ├── node_commands.v -│ │ ├── node_executor.v -│ │ ├── node_factory.v -│ │ ├── node.v -│ │ ├── nodedb_test.v -│ │ ├── portforward_lib.v -│ │ ├── readme.md -│ │ └── this_remote.v -│ ├── clients -│ │ ├── ipapi -│ │ ├── jina -│ │ ├── livekit -│ │ ├── mailclient -│ │ ├── meilisearch -│ │ ├── mycelium -│ │ ├── mycelium_rpc -│ │ ├── openai -│ │ ├── postgresql_client -│ │ ├── qdrant -│ │ ├── rclone -│ │ ├── runpod -│ │ ├── sendgrid -│ │ ├── vastai -│ │ ├── wireguard -│ │ ├── zerodb_client -│ │ ├── zinit -│ │ └── zinit_rpc -│ ├── code -│ │ └── generator -│ ├── conversiontools -│ │ └── tools.v -│ │ ├── docsorter -│ │ ├── imagemagick -│ │ ├── pdftotext -│ │ ├── text_extractor -│ ├── core -│ │ ├── interactive.v -│ │ ├── memdb_test.v -│ │ ├── memdb.v -│ │ ├── platform_test.v -│ │ ├── platform.v -│ │ ├── readme.md -│ │ ├── sudo_test.v -│ │ └── sudo.v -│ │ ├── base -│ │ ├── code -│ │ ├── generator -│ │ ├── herocmds -│ │ ├── httpconnection -│ │ ├── logger -│ │ ├── openrpc_remove -│ │ ├── pathlib -│ │ ├── playbook -│ │ ├── playcmds -│ │ ├── playmacros -│ │ ├── redisclient -│ │ ├── rootpath -│ │ ├── smartid -│ │ ├── texttools -│ │ ├── vexecutor -│ ├── crypt -│ │ └── crypt.v -│ │ ├── aes_symmetric -│ │ ├── crpgp -│ │ ├── ed25519 -│ │ ├── keychain -│ │ ├── keysafe -│ │ ├── openssl -│ │ ├── pgp -│ │ ├── secp256k1 -│ │ ├── secrets -│ ├── data -│ │ ├── cache -│ │ ├── currency -│ │ ├── dbfs -│ │ ├── dedupestor -│ │ ├── doctree -│ │ ├── encoder -│ │ ├── encoderhero -│ │ ├── flist -│ │ ├── gid -│ │ ├── graphdb -│ │ ├── ipaddress -│ │ ├── location -│ │ ├── markdown -│ │ ├── markdownparser2 -│ │ ├── markdownrenderer -│ │ ├── mnemonic -│ │ ├── models -│ │ ├── ourdb -│ │ ├── ourdb_syncer -│ │ ├── ourjson -│ │ ├── ourtime -│ │ ├── paramsparser -│ │ ├── radixtree -│ │ ├── resp -│ │ ├── serializers -│ │ ├── tst -│ │ ├── verasure -│ │ └── vstor -│ ├── dav -│ │ └── webdav -│ ├── develop -│ │ ├── gittools -│ │ ├── luadns -│ │ ├── performance -│ │ ├── sourcetree -│ │ ├── vscode -│ │ └── vscode_extensions -│ ├── hero -│ │ └── models -│ │ ├── db -│ ├── installers -│ │ ├── install_multi.v -│ │ └── upload.v -│ │ ├── base -│ │ ├── db -│ │ ├── develapps -│ │ ├── infra -│ │ ├── lang -│ │ ├── net -│ │ ├── sysadmintools -│ │ ├── threefold -│ │ ├── ulist -│ │ ├── virt -│ │ ├── web -│ ├── lang -│ │ ├── python -│ │ └── rust -│ ├── mcp -│ │ ├── backend_interface.v -│ │ ├── backend_memory.v -│ │ ├── factory.v -│ │ ├── generics.v -│ │ ├── handler_initialize_test.v -│ │ ├── handler_initialize.v -│ │ ├── handler_prompts.v -│ │ ├── handler_resources.v -│ │ ├── handler_tools.v -│ │ ├── model_configuration_test.v -│ │ ├── model_configuration.v -│ │ ├── model_error.v -│ │ ├── README.md -│ │ └── server.v -│ │ ├── baobab -│ │ ├── cmd -│ │ ├── mcpgen -│ │ ├── pugconvert -│ │ ├── rhai -│ │ ├── transport -│ │ ├── vcode -│ ├── osal -│ │ ├── core -│ │ ├── coredns -│ │ ├── hostsfile -│ │ ├── notifier -│ │ ├── osinstaller -│ │ ├── rsync -│ │ ├── screen -│ │ ├── sshagent -│ │ ├── startupmanager -│ │ ├── systemd -│ │ ├── tmux -│ │ ├── traefik -│ │ ├── tun -│ │ ├── ufw -│ │ └── zinit -│ ├── schemas -│ │ ├── jsonrpc -│ │ ├── jsonschema -│ │ ├── openapi -│ │ └── openrpc -│ ├── security -│ │ ├── authentication -│ │ └── jwt -│ ├── threefold -│ │ ├── grid3 -│ │ └── grid4 -│ ├── ui -│ │ ├── factory.v -│ │ └── readme.md -│ │ ├── console -│ │ ├── generic -│ │ ├── logger -│ │ ├── telegram -│ │ ├── template -│ │ ├── uimodel -│ ├── vfs -│ │ ├── interface.v -│ │ ├── metadata.v -│ │ └── README.md -│ │ ├── vfs_calendar -│ │ ├── vfs_contacts -│ │ ├── vfs_db -│ │ ├── vfs_local -│ │ ├── vfs_mail -│ │ ├── vfs_nested -│ ├── virt -│ │ ├── cloudhypervisor -│ │ ├── docker -│ │ ├── herocontainers -│ │ ├── hetzner -│ │ ├── lima -│ │ ├── qemu -│ │ ├── runc -│ │ └── utils -│ ├── web -│ │ ├── doctreeclient -│ │ ├── docusaurus -│ │ ├── echarts -│ │ ├── site -│ │ └── ui -├── libarchive -│ ├── installers -│ │ └── web -│ ├── rhai -│ │ ├── generate_rhai_example.v -│ │ ├── generate_wrapper_module.v -│ │ ├── register_functions.v -│ │ ├── register_types_test.v -│ │ ├── register_types.v -│ │ ├── rhai_test.v -│ │ ├── rhai.v -│ │ └── verify.v -│ │ ├── prompts -│ │ ├── templates -│ │ ├── testdata -│ └── starlight -│ ├── clean.v -│ ├── config.v -│ ├── factory.v -│ ├── model.v -│ ├── site_get.v -│ ├── site.v -│ ├── template.v -│ └── watcher.v -│ ├── templates -├── manual -│ ├── config.json -│ ├── create_tag.md -│ └── serve_wiki.sh -│ ├── best_practices -│ │ └── using_args_in_function.md -│ │ ├── osal -│ │ ├── scripts -│ ├── core -│ │ ├── base.md -│ │ ├── context_session_job.md -│ │ ├── context.md -│ │ ├── play.md -│ │ └── session.md -│ │ ├── concepts -│ ├── documentation -│ │ └── docextractor.md -├── research -│ └── globals -│ ├── globals_example_inplace.vsh -│ ├── globals_example_reference.vsh -│ ├── globals_example.vsh -│ └── ubuntu_partition.sh -├── vscodeplugin -│ ├── install_ubuntu.sh -│ ├── package.sh -│ └── readme.md -│ ├── heroscrypt-syntax -│ │ ├── heroscript-syntax-0.0.1.vsix -│ │ ├── language-configuration.json -│ │ └── package.json -│ │ ├── syntaxes - - - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/bizmodel.v -```v -module playcmds - -// import freeflowuniverse.herolib.core.playbook - -// fn git(mut actions playbook.Actions, action playbook.Action) ! { -// if action.name == 'init' { -// // means we support initialization afterwards -// c.bizmodel_init(mut actions, action)! -// } - -// // if action.name == 'get' { -// // mut gs := gittools.new()! -// // url := action.params.get('url')! -// // branch := action.params.get_default('branch', '')! -// // reset := action.params.get_default_false('reset')! -// // pull := action.params.get_default_false('pull')! -// // mut gr := gs.repo_get_from_url(url: url, branch: branch, pull: pull, reset: reset)! -// // } -// } - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/currency.v -```v -module playcmds - -// fn currency_actions(actions_ []playbook.Action) ! { -// mut actions2 := actions.filtersort(actions: actions_, actor: 'currency', book: '*')! -// if actions2.len == 0 { -// return -// } - -// mut cs := currency.new()! - -// for action in actions2 { -// // TODO: set the currencies -// if action.name == 'default_set' { -// cur := action.params.get('cur')! -// usdval := action.params.get_int('usdval')! -// cs.default_set(cur, usdval)! -// } -// } - -// // TODO: add the currency metainfo, do a test -// } - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/dagu.v -```v -module playcmds - -// import freeflowuniverse.herolib.installers.sysadmintools.daguserver - -// pub fn scheduler(heroscript string) ! { -// daguserver.play( -// heroscript: heroscript -// )! -// } - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/downloader.v -```v -module playcmds - -// import freeflowuniverse.herolib.core.playbook -// import freeflowuniverse.herolib.sysadmin.downloader - -// can start with sal, dal, ... the 2nd name is typicall the actor (or topic) -// do this function public and then it breaches out to detail functionality - -// pub fn sal_downloader(action playbook.Action) ! { -// match action.actor { -// 'downloader' { -// match action.name { -// 'get' { -// downloader_get(action: action)! -// } -// else { -// return error('actions not supported yet') -// } -// } -// } -// else { -// return error('actor not supported yet') -// } -// } -// } - -// fn downloader_get(args ActionExecArgs) ! { -// action := args.action -// // session:=args.action or {panic("no context")} //if we need it here -// mut name := action.params.get_default('name', '')! -// mut downloadpath := action.params.get_default('downloadpath', '')! -// mut url := action.params.get_default('url', '')! -// mut reset := action.params.get_default_false('reset') -// mut gitpull := action.params.get_default_false('gitpull') - -// mut minsize_kb := action.params.get_u32_default('minsize_kb', 0)! -// mut maxsize_kb := action.params.get_u32_default('maxsize_kb', 0)! - -// mut destlink := action.params.get_default_false('destlink') - -// mut dest := action.params.get_default('dest', '')! -// mut hash := action.params.get_default('hash', '')! -// mut metapath := action.params.get_default('metapath', '')! - -// mut meta := downloader.download( -// name: name -// downloadpath: downloadpath -// url: url -// reset: reset -// gitpull: gitpull -// minsize_kb: minsize_kb -// maxsize_kb: maxsize_kb -// destlink: destlink -// dest: dest -// hash: hash -// metapath: metapath -// // session:session // TODO IMPLEMENT (also optional) -// )! -// } - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/play_caddy.v -```v -module playcmds - -// import freeflowuniverse.herolib.installers.web.caddy as caddy_installer -// import freeflowuniverse.herolib.servers.caddy { CaddyFile } -// import freeflowuniverse.herolib.core.playbook -// import os -// // import net.urllib - -// pub fn play_caddy(mut plbook playbook.PlayBook) ! { -// play_caddy_basic(mut plbook)! -// play_caddy_configure(mut plbook)! -// } - -// pub fn play_caddy_configure(mut plbook playbook.PlayBook) ! { -// mut caddy_actions := plbook.find(filter: 'caddy_configure')! -// if caddy_actions.len == 0 { -// return -// } -// } - -// pub fn play_caddy_basic(mut plbook playbook.PlayBook) ! { -// caddy_actions := plbook.find(filter: 'caddy.')! -// if caddy_actions.len == 0 { -// return -// } - -// mut install_actions := plbook.find(filter: 'caddy.install')! - -// if install_actions.len > 0 { -// for install_action in install_actions { -// mut p := install_action.params -// xcaddy := p.get_default_false('xcaddy') -// file_path := p.get_default('file_path', '/etc/caddy')! -// file_url := p.get_default('file_url', '')! -// reset := p.get_default_false('reset') -// start := p.get_default_false('start') -// restart := p.get_default_false('restart') -// stop := p.get_default_false('stop') -// homedir := p.get_default('file_url', '')! -// plugins := p.get_list_default('plugins', []string{})! - -// caddy_installer.install( -// xcaddy: xcaddy -// file_path: file_path -// file_url: file_url -// reset: reset -// start: start -// restart: restart -// stop: stop -// homedir: homedir -// plugins: plugins -// )! -// } -// } - -// mut config_actions := plbook.find(filter: 'caddy.configure')! -// if config_actions.len > 0 { -// mut coderoot := '' -// mut reset := false -// mut pull := false - -// mut public_ip := '' - -// mut c := caddy.get('')! -// // that to me seems to be wrong, not generic enough -// if config_actions.len > 1 { -// return error('can only have 1 config action for books') -// } else if config_actions.len == 1 { -// mut p := config_actions[0].params -// path := p.get_default('path', '/etc/caddy')! -// url := p.get_default('url', '')! -// public_ip = p.get_default('public_ip', '')! -// c = caddy.configure('', homedir: path)! -// config_actions[0].done = true -// } - -// mut caddyfile := CaddyFile{} -// for mut action in plbook.find(filter: 'caddy.add_reverse_proxy')! { -// mut p := action.params -// mut from := p.get_default('from', '')! -// mut to := p.get_default('to', '')! - -// if from == '' || to == '' { -// return error('from & to cannot be empty') -// } - -// caddyfile.add_reverse_proxy( -// from: from -// to: to -// )! -// action.done = true -// } - -// for mut action in plbook.find(filter: 'caddy.add_file_server')! { -// mut p := action.params -// mut domain := p.get_default('domain', '')! -// mut root := p.get_default('root', '')! - -// if root.starts_with('~') { -// root = '${os.home_dir()}${root.trim_string_left('~')}' -// } - -// if domain == '' || root == '' { -// return error('domain & root cannot be empty') -// } - -// caddyfile.add_file_server( -// domain: domain -// root: root -// )! -// action.done = true -// } - -// for mut action in plbook.find(filter: 'caddy.add_basic_auth')! { -// mut p := action.params -// mut domain := p.get_default('domain', '')! -// mut username := p.get_default('username', '')! -// mut password := p.get_default('password', '')! - -// if domain == '' || username == '' || password == '' { -// return error('domain & root cannot be empty') -// } - -// caddyfile.add_basic_auth( -// domain: domain -// username: username -// password: password -// )! -// action.done = true -// } - -// for mut action in plbook.find(filter: 'caddy.generate')! { -// c.set_caddyfile(caddyfile)! -// action.done = true -// } - -// for mut action in plbook.find(filter: 'caddy.start')! { -// c.start()! -// action.done = true -// } -// c.reload()! -// } -// } - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/play_dagu_test.v -```v -module playcmds - -import freeflowuniverse.herolib.core.playbook - -const dagu_script = " -!!dagu.configure - instance: 'test' - username: 'admin' - password: 'testpassword' - -!!dagu.new_dag - name: 'test_dag' - -!!dagu.add_step - dag: 'test_dag' - name: 'hello_world' - command: 'echo hello world' - -!!dagu.add_step - dag: 'test_dag' - name: 'last_step' - command: 'echo last step' - - -" - -fn test_play_dagu() ! { - mut plbook := playbook.new(text: dagu_script)! - play_dagu(mut plbook)! - // panic('s') -} - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/play_dagu.v -```v -module playcmds - -// import freeflowuniverse.herolib.clients.daguclient -// import freeflowuniverse.herolib.installers.sysadmintools.daguserver -// import freeflowuniverse.herolib.installers.sysadmintools.daguserver -import freeflowuniverse.herolib.core.playbook -import freeflowuniverse.herolib.ui.console -import os - -// pub fn play_dagu(mut plbook playbook.PlayBook) ! { -// // dagu_actions := plbook.find(filter: 'dagu.')! -// // if dagu_actions.len == 0 { -// // return -// // } - -// // play_dagu_basic(mut plbook)! -// // play_dagu_configure(mut plbook)! -// } - -// // play_dagu plays the dagu play commands -// pub fn play_dagu_basic(mut plbook playbook.PlayBook) ! { -// // mut install_actions := plbook.find(filter: 'daguserver.configure')! - -// // if install_actions.len > 0 { -// // for install_action in install_actions { -// // mut p := install_action.params -// // panic("daguinstall play") -// // } -// // } - -// // dagu_actions := plbook.find(filter: 'daguserver.install')! -// // if dagu_actions.len > 0 { -// // panic("daguinstall play") -// // return -// // } - -// // mut config_actions := plbook.find(filter: 'dagu.configure')! -// // mut d := if config_actions.len > 1 { -// // return error('can only have 1 config action for dagu') -// // } else if config_actions.len == 1 { -// // mut p := config_actions[0].params -// // instance := p.get_default('instance', 'default')! -// // port := p.get_int_default('port', 8888)! -// // username := p.get_default('username', '')! -// // password := p.get_default('password', '')! -// // config_actions[0].done = true -// // mut server := daguserver.configure(instance, -// // port: port -// // username: username -// // password: password -// // )! -// // server.start()! -// // console.print_debug('Dagu server is running at http://localhost:${port}') -// // console.print_debug('Username: ${username} password: ${password}') - -// // // configure dagu client with server url and api secret -// // server_cfg := server.config()! -// // daguclient.get(instance, -// // url: 'http://localhost:${port}' -// // apisecret: server_cfg.secret -// // )! -// // } else { -// // mut server := daguserver.get('')! -// // server.start()! -// // daguclient.get('')! -// // } - -// // mut dags := map[string]DAG{} - -// // for mut action in plbook.find(filter: 'dagu.new_dag')! { -// // mut p := action.params -// // name := p.get_default('name', '')! -// // dags[name] = DAG{} -// // action.done = true -// // } - -// // for mut action in plbook.find(filter: 'dagu.add_step')! { -// // mut p := action.params -// // dag := p.get_default('dag', 'default')! -// // name := p.get_default('name', 'default')! -// // command := p.get_default('command', '')! -// // dags[dag].step_add( -// // nr: dags.len -// // name: name -// // command: command -// // )! -// // } - -// // for mut action in plbook.find(filter: 'dagu.run')! { -// // mut p := action.params -// // dag := p.get_default('dag', 'default')! -// // // d.new_dag(dags[dag])! -// // panic('to implement') -// // } -// } - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/play_juggler.v -```v -module playcmds - -import freeflowuniverse.herolib.data.doctree -import freeflowuniverse.herolib.ui.console -import freeflowuniverse.herolib.core.playbook -import freeflowuniverse.herolib.develop.juggler -import os - -pub fn play_juggler(mut plbook playbook.PlayBook) ! { - mut coderoot := '' - // mut install := false - mut reset := false - mut pull := false - - mut config_actions := plbook.find(filter: 'juggler.configure')! - - mut j := juggler.Juggler{} - - if config_actions.len > 1 { - return error('can only have 1 config action for juggler') - } else if config_actions.len == 1 { - mut p := config_actions[0].params - path := p.get_default('path', '/etc/juggler')! - url := p.get_default('url', '')! - username := p.get_default('username', '')! - password := p.get_default('password', '')! - port := p.get_int_default('port', 8000)! - - j = juggler.configure( - url: 'https://git.threefold.info/projectmycelium/itenv' - username: username - password: password - reset: true - )! - config_actions[0].done = true - } - - for mut action in plbook.find(filter: 'juggler.start')! { - j.start()! - action.done = true - } - - for mut action in plbook.find(filter: 'juggler.restart')! { - j.restart()! - action.done = true - } -} - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/play_publisher_test.v -```v -module playcmds - -import freeflowuniverse.herolib.core.playbook -import freeflowuniverse.herolib.core.playcmds -import freeflowuniverse.herolib.core.pathlib -import os - -fn test_play_publisher() { - mut p := pathlib.get_file(path: '/tmp/heroscript/do.hero', create: true)! - - s2 := " - -!!publisher.new_collection - url:'https://git.threefold.info/tfgrid/info_tfgrid/src/branch/main/collections' - reset: false - pull: true - - -!!book.define - name:'info_tfgrid' - summary_url:'https://git.threefold.info/tfgrid/info_tfgrid/src/branch/development/books/tech/SUMMARY.md' - title:'ThreeFold Technology' - collections: 'about,dashboard,farmers,library,partners_utilization,tech,p2p' - - -!!book.publish - name:'tech' - production: false -" - p.write(s2)! - - mut plbook := playbook.new(path: '/tmp/heroscript')! - playcmds.play_publisher(mut plbook)! -} - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/play_publisher.v -```v -module playcmds - -import freeflowuniverse.herolib.core.playbook -// import freeflowuniverse.herolib.hero.publishing - -// pub fn play_publisher(mut plbook playbook.PlayBook) ! { -// publishing.play(mut plbook)! -// } - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/play_threefold.v -```v -module playcmds - -import freeflowuniverse.herolib.core.playbook -// import freeflowuniverse.herolib.threefold.grid -// import freeflowuniverse.herolib.threefold.tfrobot -// import os - -// pub fn play_threefold(mut plbook playbook.PlayBook) ! { -// panic('fix tfrobot module') -// // mut config_actions := plbook.find(filter: 'threefold.configure')! - -// // mnemonics_ := os.getenv_opt('TFGRID_MNEMONIC') or { '' } -// // mut ssh_key := os.getenv_opt('SSH_KEY') or { '' } - -// // tfrobot.configure('play', network: 'main', mnemonics: mnemonics_)! - -// // mut robot := tfrobot.get('play')! - -// // if config_actions.len > 1 { -// // return error('can only have 1 config action for threefold') -// // } else if config_actions.len == 1 { -// // mut a := config_actions[0] -// // mut p := a.params -// // mut network := p.get_default('network', 'main')! -// // mnemonics := p.get_default('mnemonics', '')! -// // ssh_key = p.get_default('ssh_key', '')! - -// // network = network.to_lower() - -// // // mnemonics string -// // // network string = 'main' -// // tfrobot.configure('play', network: network, mnemonics: mnemonics)! - -// // robot = tfrobot.get('play')! - -// // config_actions[0].done = true -// // } -// // cfg := robot.config()! -// // if cfg.mnemonics == '' { -// // return error('TFGRID_MNEMONIC should be specified as env variable') -// // } - -// // if ssh_key == '' { -// // return error('SSHKey should be specified as env variable') -// // } - -// // panic('implement') - -// // for mut action in plbook.find(filter: 'threefold.deploy_vm')! { -// // mut p := action.params -// // deployment_name := p.get_default('deployment_name', 'deployment')! -// // name := p.get_default('name', 'vm')! -// // ssh_key := p.get_default('ssh_key', '')! -// // cores := p.get_int_default('cores', 1)! -// // memory := p.get_int_default('memory', 20)! -// // panic("implement") -// // action.done = true -// // } - -// // for mut action in plbook.find(filter: 'threefold.deploy_zdb')! { -// // panic("implement") -// // action.done = true -// // } -// } - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/_archive/play_zola.v -```v -module playcmds - -// import freeflowuniverse.herolib.ui.console -// import freeflowuniverse.herolib.web.zola -// import freeflowuniverse.herolib.core.playbook - -// struct WebsiteItem { -// mut: -// name string -// site ?&zola.ZolaSite -// } - -// pub fn play_zola(mut plbook playbook.PlayBook) ! { -// // mut coderoot := '' -// mut buildroot := '' -// mut publishroot := '' -// mut install := true -// mut reset := false - -// wsactions := plbook.find(filter: 'website.')! -// if wsactions.len == 0 { -// return -// } - -// mut config_actions := plbook.find(filter: 'websites:configure')! -// if config_actions.len > 1 { -// return error('can only have 1 config action for websites') -// } else if config_actions.len == 1 { -// mut p := config_actions[0].params -// buildroot = p.get_default('buildroot', '')! -// publishroot = p.get_default('publishroot', '')! -// // coderoot = p.get_default('coderoot', '')! -// install = p.get_default_true('install') -// reset = p.get_default_false('reset') -// config_actions[0].done = true -// } -// mut websites := zola.new( -// path_build: buildroot -// path_publish: publishroot -// install: install -// reset: reset -// )! - -// mut ws := WebsiteItem{} - -// for mut action in plbook.find(filter: 'website.')! { -// if action.name == 'define' { -// console.print_debug('website.define') -// mut p := action.params -// ws.name = p.get('name')! -// title := p.get_default('title', '')! -// description := p.get_default('description', '')! -// ws.site = websites.new(name: ws.name, title: title, description: description)! -// } else if action.name == 'template_add' { -// console.print_debug('website.template_add') -// mut p := action.params -// url := p.get_default('url', '')! -// mut site_ := ws.site or { -// return error("can't find website for template_add, should have been defined before with !!website.define") -// } - -// site_.template_add(url: url)! -// } else if action.name == 'content_add' { -// console.print_debug('website.content_add') -// mut p := action.params -// url := p.get_default('url', '')! -// mut site_ := ws.site or { -// return error("can't find website for content_add, should have been defined before with !!website.define") -// } - -// site_.content_add(url: url)! -// } else if action.name == 'doctree_add' { -// console.print_debug('website.doctree_add') -// mut p := action.params -// url := p.get_default('url', '')! -// pull := p.get_default_false('pull') -// mut site_ := ws.site or { -// return error("can't find website for doctree_add, should have been defined before with !!website.define") -// } - -// site_.doctree_add(url: url, pull: pull)! -// } else if action.name == 'post_add' { -// console.print_debug('website.post_add') -// mut p := action.params -// name := p.get_default('name', '')! -// collection := p.get_default('collection', '')! -// file := p.get_default('file', '')! -// page := p.get_default('page', '')! -// pointer := p.get_default('pointer', '')! -// mut site_ := ws.site or { -// return error("can't find website for doctree_add, should have been defined before with !!website.define") -// } - -// site_.post_add(name: name, collection: collection, file: file, pointer: pointer)! -// } else if action.name == 'blog_add' { -// console.print_debug('website.blog_add') -// mut p := action.params -// name := p.get_default('name', '')! -// collection := p.get_default('collection', '')! -// file := p.get_default('file', '')! -// page := p.get_default('page', '')! -// pointer := p.get_default('pointer', '')! -// mut site_ := ws.site or { -// return error("can't find website for doctree_add, should have been defined before with !!website.define") -// } - -// site_.blog_add(name: name)! -// } else if action.name == 'person_add' { -// console.print_debug('website.person_add') -// mut p := action.params -// name := p.get_default('name', '')! -// page := p.get_default('page', '')! -// collection := p.get_default('collection', '')! -// file := p.get_default('file', '')! -// pointer := p.get_default('pointer', '')! -// mut site_ := ws.site or { -// return error("can't find website for doctree_add, should have been defined before with !!website.define") -// } - -// site_.person_add( -// name: name -// collection: collection -// file: file -// page: page -// pointer: pointer -// )! -// } else if action.name == 'people_add' { -// console.print_debug('website.people_add') -// mut p := action.params -// name := p.get_default('name', '')! -// description := p.get_default('description', '')! -// sort_by_ := p.get_default('sort_by', '')! -// mut site_ := ws.site or { -// return error("can't find website for people_add, should have been defined before with !!website.define") -// } - -// sort_by := zola.SortBy.from(sort_by_)! -// site_.people_add( -// name: name -// title: p.get_default('title', '')! -// sort_by: sort_by -// description: description -// )! -// } else if action.name == 'blog_add' { -// console.print_debug('website.blog_add') -// mut p := action.params -// name := p.get_default('name', '')! -// description := p.get_default('description', '')! -// sort_by_ := p.get_default('sort_by', '')! -// mut site_ := ws.site or { -// return error("can't find website for people_add, should have been defined before with !!website.define") -// } - -// sort_by := zola.SortBy.from(sort_by_)! -// site_.blog_add( -// name: name -// title: p.get_default('title', '')! -// sort_by: sort_by -// description: description -// )! -// } else if action.name == 'news_add' { -// console.print_debug('website.news_add') -// mut p := action.params -// name := p.get_default('name', '')! -// collection := p.get_default('collection', '')! -// pointer := p.get_default('pointer', '')! -// file := p.get_default('file', '')! -// mut site_ := ws.site or { -// return error("can't find website for news_add, should have been defined before with !!website.define") -// } - -// site_.article_add(name: name, collection: collection, file: file, pointer: pointer)! -// } else if action.name == 'header_add' { -// console.print_debug('website.header_add') -// mut p := action.params -// template := p.get_default('template', '')! -// logo := p.get_default('logo', '')! -// mut site_ := ws.site or { -// return error("can't find website for doctree_add, should have been defined before with !!website.define") -// } - -// site_.header_add(template: template, logo: logo)! -// } else if action.name == 'header_link_add' { -// console.print_debug('website.header_link_add') -// mut p := action.params -// page := p.get_default('page', '')! -// label := p.get_default('label', '')! -// mut site_ := ws.site or { -// return error("can't find website for header_link_add, should have been defined before with !!website.define") -// } - -// site_.header_link_add(page: page, label: label)! -// } else if action.name == 'footer_add' { -// console.print_debug('website.footer_add') -// mut p := action.params -// template := p.get_default('template', '')! -// mut site_ := ws.site or { -// return error("can't find website for doctree_add, should have been defined before with !!website.define") -// } - -// site_.footer_add(template: template)! -// } else if action.name == 'page_add' { -// console.print_debug('website.page_add') -// mut p := action.params -// name := p.get_default('name', '')! -// collection := p.get_default('collection', '')! -// file := p.get_default('file', '')! -// homepage := p.get_default_false('homepage') -// mut site_ := ws.site or { -// return error("can't find website for doctree_add, should have been defined before with !!website.define") -// } - -// site_.page_add(name: name, collection: collection, file: file, homepage: homepage)! - -// // }else if action.name=="pull"{ -// // mut site_:=ws.site or { return error("can't find website for pull, should have been defined before with !!website.define")} -// // site_.pull()! -// } else if action.name == 'section_add' { -// console.print_debug('website.section_add') -// // mut p := action.params -// // name := p.get_default('name', '')! -// // // collection := p.get_default('collection', '')! -// // // file := p.get_default('file', '')! -// // // homepage := p.get_default_false('homepage') -// // mut site_ := ws.site or { -// // return error("can't find website for doctree_add, should have been defined before with !!website.define") -// // } - -// // site_.add_section(name: name)! - -// // }else if action.name=="pull"{ -// // mut site_:=ws.site or { return error("can't find website for pull, should have been defined before with !!website.define")} -// // site_.pull()! -// } else if action.name == 'generate' { -// mut site_ := ws.site or { -// return error("can't find website for generate, should have been defined before with !!website.define") -// } - -// site_.generate()! -// // site_.serve()! -// } else { -// return error("Cannot find right action for website. Found '${action.name}' which is a non understood action for !!website.") -// } -// action.done = true -// } -// } - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/factory.v -```v -module playcmds - -import freeflowuniverse.herolib.core.playbook { PlayBook } -import freeflowuniverse.herolib.data.doctree -import freeflowuniverse.herolib.biz.bizmodel -import freeflowuniverse.herolib.web.site -import freeflowuniverse.herolib.web.docusaurus -import freeflowuniverse.herolib.clients.openai - -// ------------------------------------------------------------------- -// run – entry point for all HeroScript play‑commands -// ------------------------------------------------------------------- - -@[params] -pub struct PlayArgs { -pub mut: - heroscript string - heroscript_path string - plbook ?PlayBook - reset bool -} - -pub fn run(args_ PlayArgs) ! { - mut args := args_ - mut plbook := args.plbook or { - playbook.new(text: args.heroscript, path: args.heroscript_path)! - } - - // Core actions - play_core(mut plbook)! - // Git actions - play_git(mut plbook)! - - // Business model (e.g. currency, bizmodel) - bizmodel.play(mut plbook)! - - // OpenAI client - openai.play(mut plbook)! - - // Website / docs - site.play(mut plbook)! - doctree.play(mut plbook)! - docusaurus.play(mut plbook)! - - // Ensure we did not leave any actions un‑processed - plbook.empty_check()! -} - -``` - -File: /Users/despiegk/code/github/freeflowuniverse/herolib/lib/core/playcmds/play_core.v -```v -module playcmds - -import freeflowuniverse.herolib.develop.gittools -import freeflowuniverse.herolib.core.playbook { PlayBook } -import freeflowuniverse.herolib.ui.console -import freeflowuniverse.herolib.core.texttools - -// ------------------------------------------------------------------- -// Core play‑command processing (context, session, env‑subst, etc) -// ------------------------------------------------------------------- - -fn play_core(mut plbook PlayBook) ! { - // ---------------------------------------------------------------- - // 1. Include handling (play include / echo) - // ---------------------------------------------------------------- - // Track included paths to prevent infinite recursion - mut included_paths := map[string]bool{} - - for action_ in plbook.find(filter: 'play.*')! { - if action_.name == 'include' { - console.print_debug('play run:${action_}') - mut action := *action_ - mut playrunpath := action.params.get_default('path', '')! - if playrunpath.len == 0 { - action.name = 'pull' - playrunpath = gittools.path( - path: action.params.get_default('path', '')! - git_url: action.params.get_default('git_url', '')! - git_reset: action.params.get_default_false('git_reset') - git_pull: action.params.get_default_false('git_pull') - )! - } - if playrunpath.len == 0 { - return error("can't run a heroscript didn't find url or path.") - } - - // Check for cycle detection - if playrunpath in included_paths { - console.print_debug('Skipping already included path: ${playrunpath}') - continue - } - - console.print_debug('play run path:${playrunpath}') - included_paths[playrunpath] = true - plbook.add(path: playrunpath)! - } - if action_.name == 'echo' { - content := action_.params.get_default('content', "didn't find content")! - console.print_header(content) - } - } - - // ---------------------------------------------------------------- - // 2. Session environment handling - // ---------------------------------------------------------------- - // Guard – make sure a session exists - mut session := plbook.session - - // !!session.env_set / env_set_once - for mut action in plbook.find(filter: 'session.')! { - mut p := action.params - match action.name { - 'env_set' { - key := p.get('key')! - val := p.get('val') or { p.get('value')! } - session.env_set(key, val)! - } - 'env_set_once' { - key := p.get('key')! - val := p.get('val') or { p.get('value')! } - // Use the dedicated “set‑once” method - session.env_set_once(key, val)! - } - else { /* ignore unknown sub‑action */ } - } - action.done = true - } - - // ---------------------------------------------------------------- - // 3. Template replacement in action parameters - // ---------------------------------------------------------------- - // Apply template replacement from session environment variables - if session.env.len > 0 { - // Create a map with name_fix applied to keys for template replacement - mut env_fixed := map[string]string{} - for key, value in session.env { - env_fixed[texttools.name_fix(key)] = value - } - - for mut action in plbook.actions { - if !action.done { - action.params.replace(env_fixed) - } - } - } - - for mut action in plbook.find(filter: 'core.coderoot_set')! { - mut p := action.params - if p.exists('coderoot') { - coderoot := p.get_path_create('coderoot')! - if session.context.config.coderoot != coderoot { - session.context.config.coderoot = coderoot - session.context.save()! - } - } else { - return error('coderoot needs to be specified') - } - action.done = true - } - - for mut action in plbook.find(filter: 'core.params_context_set')! { - mut p := action.params - mut context_params := session.context.params()! - for param in p.params { - context_params.set(param.key, param.value) - } - session.context.save()! - action.done = true - } - - for mut action in plbook.find(filter: 'core.params_session_set')! { - mut p := action.params - for param in p.params { - session.params.set(param.key, param.value) - } - session.save()! - action.done = true - } -} - -``` - - -these are my instructions what needs to be done with the attached code - -TODO… - diff --git a/lib/installers/db/cometbft/readme.md b/lib/installers/db/cometbft/readme.md index 00c6b32a..412a9a09 100644 --- a/lib/installers/db/cometbft/readme.md +++ b/lib/installers/db/cometbft/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.db.cometbft as cometbft_installer diff --git a/lib/installers/db/meilisearch_installer/readme.md b/lib/installers/db/meilisearch_installer/readme.md index 46a6e264..92e61d3b 100644 --- a/lib/installers/db/meilisearch_installer/readme.md +++ b/lib/installers/db/meilisearch_installer/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.db.meilisearch as meilisearchinstaller diff --git a/lib/installers/db/postgresql/readme.md b/lib/installers/db/postgresql/readme.md index 06a25bf5..ba45ab97 100644 --- a/lib/installers/db/postgresql/readme.md +++ b/lib/installers/db/postgresql/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v diff --git a/lib/installers/db/qdrant_installer/readme.md b/lib/installers/db/qdrant_installer/readme.md index 8e5f6b78..cc2e142f 100644 --- a/lib/installers/db/qdrant_installer/readme.md +++ b/lib/installers/db/qdrant_installer/readme.md @@ -4,7 +4,7 @@ Is a powerfull db for embedding for AI Agents. To get started -```vlang +```v import freeflowuniverse.herolib.installers.db.qdrant_installer diff --git a/lib/installers/infra/coredns/readme.md b/lib/installers/infra/coredns/readme.md index 15fa18a8..9c8b59ef 100644 --- a/lib/installers/infra/coredns/readme.md +++ b/lib/installers/infra/coredns/readme.md @@ -4,7 +4,7 @@ coredns To get started -```vlang +```v import freeflowuniverse.herolib.installers.infra.coredns as coredns_installer diff --git a/lib/installers/infra/gitea/readme.md b/lib/installers/infra/gitea/readme.md index 9dfb7153..9c14c56e 100644 --- a/lib/installers/infra/gitea/readme.md +++ b/lib/installers/infra/gitea/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.infra.gitea as gitea_installer diff --git a/lib/installers/infra/livekit/readme.md b/lib/installers/infra/livekit/readme.md index 61c79442..df226a99 100644 --- a/lib/installers/infra/livekit/readme.md +++ b/lib/installers/infra/livekit/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v diff --git a/lib/installers/infra/screen/readme.md b/lib/installers/infra/screen/readme.md index f8480f1f..8b13f311 100644 --- a/lib/installers/infra/screen/readme.md +++ b/lib/installers/infra/screen/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.screen as screen_installer diff --git a/lib/installers/infra/zinit_installer/readme.md b/lib/installers/infra/zinit_installer/readme.md index a5130131..a1994d3c 100644 --- a/lib/installers/infra/zinit_installer/readme.md +++ b/lib/installers/infra/zinit_installer/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something. zinit diff --git a/lib/installers/lang/golang/readme.md b/lib/installers/lang/golang/readme.md index 0d8346a1..ca110c61 100644 --- a/lib/installers/lang/golang/readme.md +++ b/lib/installers/lang/golang/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.lang.golang diff --git a/lib/installers/lang/nodejs/readme.md b/lib/installers/lang/nodejs/readme.md index 14f8478e..04585cd9 100644 --- a/lib/installers/lang/nodejs/readme.md +++ b/lib/installers/lang/nodejs/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.nodejs as nodejs_installer diff --git a/lib/installers/lang/python/readme.md b/lib/installers/lang/python/readme.md index f4046274..8b80bde6 100644 --- a/lib/installers/lang/python/readme.md +++ b/lib/installers/lang/python/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.python as python_installer diff --git a/lib/installers/lang/rust/readme.md b/lib/installers/lang/rust/readme.md index 1aa9a9a1..3b251986 100644 --- a/lib/installers/lang/rust/readme.md +++ b/lib/installers/lang/rust/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.rust as rust_installer diff --git a/lib/installers/net/wireguard_installer/readme.md b/lib/installers/net/wireguard_installer/readme.md index 73a58c61..8047b5c1 100644 --- a/lib/installers/net/wireguard_installer/readme.md +++ b/lib/installers/net/wireguard_installer/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.wireguard as wireguard_installer diff --git a/lib/installers/net/yggdrasil/readme.md b/lib/installers/net/yggdrasil/readme.md index 8da4e9ec..8241c696 100644 --- a/lib/installers/net/yggdrasil/readme.md +++ b/lib/installers/net/yggdrasil/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.yggdrasil as yggdrasil_installer diff --git a/lib/installers/sysadmintools/b2/readme.md b/lib/installers/sysadmintools/b2/readme.md index 68824662..514ee422 100644 --- a/lib/installers/sysadmintools/b2/readme.md +++ b/lib/installers/sysadmintools/b2/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.b2 as b2_installer diff --git a/lib/installers/sysadmintools/fungistor/readme.md b/lib/installers/sysadmintools/fungistor/readme.md index 731a2330..7f0d665e 100644 --- a/lib/installers/sysadmintools/fungistor/readme.md +++ b/lib/installers/sysadmintools/fungistor/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.fungistor as fungistor_installer diff --git a/lib/installers/sysadmintools/garage_s3/readme.md b/lib/installers/sysadmintools/garage_s3/readme.md index 04075960..bd280790 100644 --- a/lib/installers/sysadmintools/garage_s3/readme.md +++ b/lib/installers/sysadmintools/garage_s3/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v diff --git a/lib/installers/sysadmintools/grafana/readme.md b/lib/installers/sysadmintools/grafana/readme.md index 2be52a90..14d7b25f 100644 --- a/lib/installers/sysadmintools/grafana/readme.md +++ b/lib/installers/sysadmintools/grafana/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.grafana as grafana_installer diff --git a/lib/installers/sysadmintools/rclone/readme.md b/lib/installers/sysadmintools/rclone/readme.md index 09428d8a..99f5165c 100644 --- a/lib/installers/sysadmintools/rclone/readme.md +++ b/lib/installers/sysadmintools/rclone/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v diff --git a/lib/installers/sysadmintools/restic/readme.md b/lib/installers/sysadmintools/restic/readme.md index 61863bd2..0e8d2013 100644 --- a/lib/installers/sysadmintools/restic/readme.md +++ b/lib/installers/sysadmintools/restic/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.restic as restic_installer diff --git a/lib/installers/threefold/griddriver/readme.md b/lib/installers/threefold/griddriver/readme.md index 990b8c23..11dea35a 100644 --- a/lib/installers/threefold/griddriver/readme.md +++ b/lib/installers/threefold/griddriver/readme.md @@ -2,7 +2,7 @@ To use the installer: -```vlang +```v import freeflowuniverse.herolib.installers.threefold.griddriver fn main() { diff --git a/lib/installers/virt/cloudhypervisor/readme.md b/lib/installers/virt/cloudhypervisor/readme.md index 1d0d0ef7..698213a5 100644 --- a/lib/installers/virt/cloudhypervisor/readme.md +++ b/lib/installers/virt/cloudhypervisor/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something. cloudhypervisor diff --git a/lib/installers/virt/docker/readme.md b/lib/installers/virt/docker/readme.md index 8cb368a3..ac886ba7 100644 --- a/lib/installers/virt/docker/readme.md +++ b/lib/installers/virt/docker/readme.md @@ -2,7 +2,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.docker as docker_installer diff --git a/lib/installers/virt/pacman/readme.md b/lib/installers/virt/pacman/readme.md index 42007738..ecf9ab55 100644 --- a/lib/installers/virt/pacman/readme.md +++ b/lib/installers/virt/pacman/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.virt.pacman diff --git a/lib/installers/virt/podman/readme.md b/lib/installers/virt/podman/readme.md index 968205bd..f8de50a4 100644 --- a/lib/installers/virt/podman/readme.md +++ b/lib/installers/virt/podman/readme.md @@ -7,7 +7,7 @@ Podman is a lightweight container manager that allows users to manage and run co The following example demonstrates how to use the Podman installer in a VLang script. It checks if Podman is installed, removes it if found, or installs it if not. ### Example Code (VLang) -```vlang +```v import freeflowuniverse.herolib.installers.virt.podman as podman_installer mut podman := podman_installer.get()! diff --git a/lib/installers/virt/youki/readme.md b/lib/installers/virt/youki/readme.md index 0c20bfc6..781dcdf6 100644 --- a/lib/installers/virt/youki/readme.md +++ b/lib/installers/virt/youki/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.virt.youki diff --git a/lib/installers/web/bun/readme.md b/lib/installers/web/bun/readme.md index e756650f..2ca7cb7f 100644 --- a/lib/installers/web/bun/readme.md +++ b/lib/installers/web/bun/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.bun as bun_installer diff --git a/lib/installers/web/imagemagick/readme.md b/lib/installers/web/imagemagick/readme.md index ca621788..36cc4439 100644 --- a/lib/installers/web/imagemagick/readme.md +++ b/lib/installers/web/imagemagick/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v import freeflowuniverse.herolib.installers.something.imagemagick as imagemagick_installer diff --git a/lib/installers/web/tailwind/readme.md b/lib/installers/web/tailwind/readme.md index 098cefeb..3c798a16 100644 --- a/lib/installers/web/tailwind/readme.md +++ b/lib/installers/web/tailwind/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v diff --git a/lib/installers/web/tailwind4/readme.md b/lib/installers/web/tailwind4/readme.md index 13f5639a..a9e3dfb3 100644 --- a/lib/installers/web/tailwind4/readme.md +++ b/lib/installers/web/tailwind4/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v diff --git a/lib/installers/web/traefik/readme.md b/lib/installers/web/traefik/readme.md index 1409995f..dcf74b3d 100644 --- a/lib/installers/web/traefik/readme.md +++ b/lib/installers/web/traefik/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v diff --git a/lib/installers/web/zola/readme.md b/lib/installers/web/zola/readme.md index 2da105e2..5bb3a965 100644 --- a/lib/installers/web/zola/readme.md +++ b/lib/installers/web/zola/readme.md @@ -4,7 +4,7 @@ To get started -```vlang +```v diff --git a/lib/threefold/grid3/deployer/readme.md b/lib/threefold/grid3/deployer/readme.md index 1934c719..21363df6 100644 --- a/lib/threefold/grid3/deployer/readme.md +++ b/lib/threefold/grid3/deployer/readme.md @@ -2,7 +2,7 @@ To get started -```vlang +```v diff --git a/lib/web/heroprompt/endpoints.v b/lib/web/heroprompt/endpoints.v deleted file mode 100644 index a48a45ad..00000000 --- a/lib/web/heroprompt/endpoints.v +++ /dev/null @@ -1,315 +0,0 @@ -module heroprompt - -import veb -import os -import json -import time -import freeflowuniverse.herolib.develop.heroprompt as hp - -// Types for directory listing -struct DirItem { - name string - typ string @[json: 'type'] -} - -struct DirResp { - path string -mut: - items []DirItem -} - -// HTML routes -@['/heroprompt'; get] -pub fn (app &App) page_index(mut ctx Context) veb.Result { - return ctx.html(render_index(app)) -} - -// API routes (thin wrappers over develop.heroprompt) -@['/api/heroprompt/workspaces'; get] -pub fn (app &App) api_list(mut ctx Context) veb.Result { - mut names := []string{} - ws := hp.list(fromdb: true) or { []&hp.Workspace{} } - for w in ws { - names << w.name - } - ctx.set_content_type('application/json') - return ctx.text(json.encode(names)) -} - -@['/api/heroprompt/workspaces'; post] -pub fn (app &App) api_create(mut ctx Context) veb.Result { - name := ctx.form['name'] or { 'default' } - base_path_in := ctx.form['base_path'] or { '' } - if base_path_in.len == 0 { - return ctx.text('{"error":"base_path required"}') - } - mut base_path := base_path_in - // Expand tilde to user home - if base_path.starts_with('~') { - home := os.home_dir() - base_path = os.join_path(home, base_path.all_after('~')) - } - _ := hp.get(name: name, create: true, path: base_path) or { - return ctx.text('{"error":"create failed"}') - } - ctx.set_content_type('application/json') - return ctx.text(json.encode({ - 'name': name - 'base_path': base_path - })) -} - -@['/api/heroprompt/directory'; get] -pub fn (app &App) api_directory(mut ctx Context) veb.Result { - wsname := ctx.query['name'] or { 'default' } - path_q := ctx.query['path'] or { '' } - mut wsp := hp.get(name: wsname, create: false) or { - return ctx.text('{"error":"workspace not found"}') - } - // Use workspace list method; empty path means base_path - items_w := if path_q.len > 0 { wsp.list() or { - return ctx.text('{"error":"cannot list directory"}')} } else { wsp.list() or { - return ctx.text('{"error":"cannot list directory"}')} } - ctx.set_content_type('application/json') - mut resp := DirResp{ - path: if path_q.len > 0 { path_q } else { wsp.base_path } - } - for it in items_w { - resp.items << DirItem{ - name: it.name - typ: it.typ - } - } - return ctx.text(json.encode(resp)) -} - -// -------- File content endpoint -------- -struct FileResp { - language string - content string -} - -@['/api/heroprompt/file'; get] -pub fn (app &App) api_file(mut ctx Context) veb.Result { - wsname := ctx.query['name'] or { 'default' } - path_q := ctx.query['path'] or { '' } - if path_q.len == 0 { - return ctx.text('{"error":"path required"}') - } - mut base := '' - if wsp := hp.get(name: wsname, create: false) { - base = wsp.base_path - } - mut file_path := if !os.is_abs_path(path_q) && base.len > 0 { - os.join_path(base, path_q) - } else { - path_q - } - if !os.is_file(file_path) { - return ctx.text('{"error":"not a file"}') - } - // limit read to 1MB to avoid huge responses - max_size := i64(1_000_000) - sz := os.file_size(file_path) - if sz > max_size { - return ctx.text('{"error":"file too large"}') - } - content := os.read_file(file_path) or { return ctx.text('{"error":"failed to read"}') } - lang := detect_lang(file_path) - ctx.set_content_type('application/json') - return ctx.text(json.encode(FileResp{ language: lang, content: content })) -} - -fn detect_lang(path string) string { - ext := os.file_ext(path).trim_left('.') - return match ext.to_lower() { - 'v' { 'v' } - 'js' { 'javascript' } - 'ts' { 'typescript' } - 'py' { 'python' } - 'rs' { 'rust' } - 'go' { 'go' } - 'java' { 'java' } - 'c', 'h' { 'c' } - 'cpp', 'hpp', 'cc', 'hh' { 'cpp' } - 'sh', 'bash' { 'bash' } - 'json' { 'json' } - 'yaml', 'yml' { 'yaml' } - 'html', 'htm' { 'html' } - 'css' { 'css' } - 'md' { 'markdown' } - else { 'text' } - } -} - -// -------- Filename search endpoint -------- -struct SearchItem { - path string - typ string @[json: 'type'] -} - -@['/api/heroprompt/search'; get] -pub fn (app &App) api_search(mut ctx Context) veb.Result { - wsname := ctx.query['name'] or { 'default' } - q := ctx.query['q'] or { '' } - if q.len == 0 { - return ctx.text('{"error":"q required"}') - } - mut base := '' - if wsp := hp.get(name: wsname, create: false) { - base = wsp.base_path - } - if base.len == 0 { - return ctx.text('{"error":"workspace base_path not set"}') - } - max := (ctx.query['max'] or { '200' }).int() - mut results := []SearchItem{} - walk_search(base, q, max, mut results) - ctx.set_content_type('application/json') - return ctx.text(json.encode(results)) -} - -// Workspace details -@['/api/heroprompt/workspaces/:name'; get] -pub fn (app &App) api_workspace_get(mut ctx Context, name string) veb.Result { - wsp := hp.get(name: name, create: false) or { - return ctx.text('{"error":"workspace not found"}') - } - ctx.set_content_type('application/json') - return ctx.text(json.encode({ - 'name': wsp.name - 'base_path': wsp.base_path - })) -} - -@['/api/heroprompt/workspaces/:name'; delete] -pub fn (app &App) api_workspace_delete(mut ctx Context, name string) veb.Result { - wsp := hp.get(name: name, create: false) or { - return ctx.text('{"error":"workspace not found"}') - } - wsp.delete_workspace() or { return ctx.text('{"error":"delete failed"}') } - return ctx.text('{"ok":true}') -} - -@['/api/heroprompt/workspaces/:name'; patch] -pub fn (app &App) api_workspace_patch(mut ctx Context, name string) veb.Result { - wsp := hp.get(name: name, create: false) or { - return ctx.text('{"error":"workspace not found"}') - } - new_name := ctx.form['name'] or { '' } - mut base_path := ctx.form['base_path'] or { '' } - if base_path.len > 0 && base_path.starts_with('~') { - home := os.home_dir() - base_path = os.join_path(home, base_path.all_after('~')) - } - updated := wsp.update_workspace(name: new_name, base_path: base_path) or { - return ctx.text('{"error":"update failed"}') - } - ctx.set_content_type('application/json') - return ctx.text(json.encode({ - 'name': updated.name - 'base_path': updated.base_path - })) -} - -// -------- Path validation endpoint -------- -struct PathValidationResp { - is_abs bool - exists bool - is_dir bool - expanded string -} - -@['/api/heroprompt/validate_path'; get] -pub fn (app &App) api_validate_path(mut ctx Context) veb.Result { - p_in := ctx.query['path'] or { '' } - mut p := p_in - if p.starts_with('~') { - home := os.home_dir() - p = os.join_path(home, p.all_after('~')) - } - is_abs := if p != '' { os.is_abs_path(p) } else { false } - exists := if p != '' { os.exists(p) } else { false } - isdir := if exists { os.is_dir(p) } else { false } - ctx.set_content_type('application/json') - resp := PathValidationResp{ - is_abs: is_abs - exists: exists - is_dir: isdir - expanded: p - } - return ctx.text(json.encode(resp)) -} - -fn walk_search(root string, q string, max int, mut out []SearchItem) { - if out.len >= max { - return - } - entries := os.ls(root) or { return } - for e in entries { - if e in ['.git', 'node_modules', 'build', 'dist', '.v'] { - continue - } - p := os.join_path(root, e) - if os.is_dir(p) { - if out.len >= max { - return - } - if e.to_lower().contains(q.to_lower()) { - out << SearchItem{ - path: p - typ: 'directory' - } - } - walk_search(p, q, max, mut out) - } else if os.is_file(p) { - if e.to_lower().contains(q.to_lower()) { - out << SearchItem{ - path: p - typ: 'file' - } - } - } - if out.len >= max { - return - } - } -} - -// -------- Selection and prompt endpoints -------- -@['/api/heroprompt/workspaces/:name/files'; post] -pub fn (app &App) api_add_file(mut ctx Context, name string) veb.Result { - path := ctx.form['path'] or { '' } - if path.len == 0 { - return ctx.text('{"error":"path required"}') - } - mut wsp := hp.get(name: name, create: false) or { - return ctx.text('{"error":"workspace not found"}') - } - wsp.add_file(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') } - return ctx.text('{"ok":true}') -} - -@['/api/heroprompt/workspaces/:name/dirs'; post] -pub fn (app &App) api_add_dir(mut ctx Context, name string) veb.Result { - path := ctx.form['path'] or { '' } - if path.len == 0 { - return ctx.text('{"error":"path required"}') - } - mut wsp := hp.get(name: name, create: false) or { - return ctx.text('{"error":"workspace not found"}') - } - wsp.add_dir(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') } - return ctx.text('{"ok":true}') -} - -@['/api/heroprompt/workspaces/:name/prompt'; post] -pub fn (app &App) api_generate_prompt(mut ctx Context, name string) veb.Result { - text := ctx.form['text'] or { '' } - mut wsp := hp.get(name: name, create: false) or { - return ctx.text('{"error":"workspace not found"}') - } - prompt := wsp.prompt(text: text) - ctx.set_content_type('text/plain') - return ctx.text(prompt) -} diff --git a/lib/web/heroprompt/server.v b/lib/web/heroprompt/server.v deleted file mode 100644 index 8ba215cf..00000000 --- a/lib/web/heroprompt/server.v +++ /dev/null @@ -1,71 +0,0 @@ -module heroprompt - -import veb -import os - -// Public Context type for veb -pub struct Context { - veb.Context -} - -// Factory args for starting the server -@[params] -pub struct FactoryArgs { -pub mut: - host string = 'localhost' - port int = 8090 - title string = 'Heroprompt' -} - -// App holds server state and config -pub struct App { - veb.StaticHandler -pub mut: - title string - port int - base_path string // absolute path to this module directory -} - -// Create a new App instance (does not start the server) -pub fn new(args FactoryArgs) !&App { - base := os.dir(@FILE) - mut app := App{ - title: args.title - port: args.port - base_path: base - } - // Serve static assets from this module at /static - app.mount_static_folder_at(os.join_path(base, 'static'), '/static')! - return &app -} - -// Start the webserver (blocking) -pub fn start(args FactoryArgs) ! { - mut app := new(args)! - veb.run[App, Context](mut app, app.port) -} - -// Routes - -@['/'; get] -pub fn (app &App) index(mut ctx Context) veb.Result { - return ctx.html(render_index(app)) -} - -// Rendering helpers -fn render_index(app &App) string { - tpl := os.join_path(app.base_path, 'templates', 'index.html') - content := os.read_file(tpl) or { return render_index_fallback(app) } - return render_template(content, { - 'title': app.title - }) -} - -fn render_index_fallback(app &App) string { - return - '\n' + - html_escape(app.title) + - '

' + - html_escape(app.title) + - '

Heroprompt server is running.

' -} diff --git a/lib/web/heroprompt/static/css/main.css b/lib/web/heroprompt/static/css/main.css deleted file mode 100644 index 309e68c9..00000000 --- a/lib/web/heroprompt/static/css/main.css +++ /dev/null @@ -1,624 +0,0 @@ -:root { - --bg: #111827; - --text: #e5e7eb; - --panel: #0b1220; - --border: #1f2937; - --muted: #94a3b8; - --accent: #93c5fd; - --input: #0f172a; - --input-border: #334155; - --btn: #334155; - --btn-border: #475569; -} - -:root.light { - --bg: #f9fafb; - --text: #0f172a; - --panel: #ffffff; - --border: #e5e7eb; - --muted: #475569; - --accent: #2563eb; - --input: #ffffff; - --input-border: #cbd5e1; - --btn: #e5e7eb; - --btn-border: #cbd5e1; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; - margin: 0; - padding: 24px; - background: var(--bg); - color: var(--text); -} - -h1 { - font-size: 20px; - margin: 0 0 12px -} - -.container { - max-width: 1200px; - margin: 0 auto -} - -a { - color: var(--accent) -} - -.toolbar { - display: flex; - gap: 8px; - align-items: center; - margin-bottom: 12px -} - -.toolbar .spacer { - flex: 1 -} - -.toolbar input { - background: var(--input); - border: 1px solid var(--input-border); - color: var(--text); - padding: 6px 8px; - border-radius: 6px -} - -.toolbar button { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - padding: 6px 10px; - border-radius: 6px; - cursor: pointer -} - -.layout { - display: grid; - grid-template-columns: minmax(300px, 380px) 1fr; - gap: 16px; - min-height: 70vh -} - -.sidebar { - background: var(--panel); - border: 1px solid var(--border); - padding: 8px; - border-radius: 8px -} - -.sidebar-header { - display: flex; - gap: 8px; - row-gap: 6px; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; - flex-wrap: wrap -} - -.sidebar-header .spacer { - display: none -} - -.sidebar-header select { - min-width: 140px; - max-width: 100% -} - -@media (min-width: 900px) { - .sidebar-header { - flex-wrap: nowrap - } - - .sidebar-header .spacer { - display: block; - flex: 1 - } -} - -.ws-info { - font-size: 12px; - color: var(--muted); - margin-left: auto -} - -.sidebar-header label { - font-size: 12px; - color: var(--muted) -} - -.sidebar-header select { - background: var(--input); - border: 1px solid var(--input-border); - color: var(--text); - padding: 6px 8px; - border-radius: 6px -} - -.sidebar-header button { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - padding: 6px 10px; - border-radius: 6px; - cursor: pointer; - white-space: nowrap; - flex: 0 0 auto -} - -.sidebar-body { - .sidebar-header #wsCreateBtn { - width: 32px; - height: 32px; - padding: 0; - line-height: 30px; - text-align: center; - font-weight: bold - } - - .hint { - color: var(--muted); - font-size: 12px - } - - .prompt .actions { - margin-top: 8px - } - - .prompt .actions button { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - padding: 6px 10px; - border-radius: 6px; - cursor: pointer - } - - display: flex; - flex-direction: column; - gap: 8px -} - -.searchbar { - display: flex; - gap: 8px -} - -.searchbar input { - background: var(--input); - border: 1px solid var(--input-border); - color: var(--text); - padding: 6px 8px; - border-radius: 6px -} - -.searchbar button { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - padding: 6px 10px; - border-radius: 6px; - cursor: pointer -} - -.sidebar-header button { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - padding: 6px 10px; - border-radius: 6px; - cursor: pointer -} - -.main { - display: flex; - flex-direction: column; - - .prompt { - background: var(--panel); - border: 1px solid var(--border); - padding: 12px; - border-radius: 8px - } - - .prompt textarea { - width: 100%; - min-height: 120px; - background: var(--input); - color: var(--text); - border: 1px solid var(--input-border); - border-radius: 6px; - padding: 8px; - resize: vertical - } - - gap: 16px -} - -/* Tabs */ -.tabs { - display: flex; - justify-content: center; - gap: 8px; - margin-bottom: 8px -} - -.tab { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - padding: 6px 10px; - border-radius: 6px; - cursor: pointer -} - -.tab.active { - background: transparent; - border-color: var(--accent); - color: var(--accent) -} - -.tab-pane { - display: none; -} - -.tab-pane.active { - display: block; -} - -.subbar { - display: flex; - gap: 8px; - align-items: center; - margin: 8px 0 -} - -.subbar input { - background: var(--input); - border: 1px solid var(--input-border); - color: var(--text); - padding: 6px 8px; - border-radius: 6px -} - -.subbar button { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - padding: 6px 10px; - border-radius: 6px; - cursor: pointer -} - -.subbar input { - background: var(--input); - border: 1px solid var(--input-border); - color: var(--text); - padding: 6px 8px; - border-radius: 6px -} - -.subbar button { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - padding: 6px 10px; - border-radius: 6px; - cursor: pointer -} - - -/* Chat */ -.chat { - background: var(--panel); - border: 1px solid var(--border); - border-radius: 8px; - display: flex; - flex-direction: column; - height: 60vh -} - -.messages { - flex: 1; - padding: 12px; - overflow: auto; - display: flex; - flex-direction: column; - gap: 10px -} - -.message { - display: flex; - gap: 8px; -} - -.message .bubble { - max-width: 70%; - padding: 10px 12px; - border-radius: 12px; - border: 1px solid var(--border) -} - -.message.user .bubble { - background: rgba(99, 102, 241, 0.15); -} - -.message.ai .bubble { - background: rgba(16, 185, 129, 0.15); -} - -.chat-input { - border-top: 1px solid var(--border); - padding: 10px; - display: flex; - gap: 8px -} - -.chat-input textarea { - flex: 1; - background: var(--input); - border: 1px solid var(--input-border); - color: var(--text); - border-radius: 8px; - padding: 8px; - min-height: 60px -} - -.chat-input button { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - border-radius: 8px; - padding: 8px 12px; - cursor: pointer -} - -.preview { - background: var(--panel); - border: 1px solid var(--border); - padding: 8px; - border-radius: 6px; - min-height: 300px -} - -.prompt, -.selection { - background: var(--panel); - border: 1px solid var(--border); - padding: 12px; - border-radius: 6px -} - -.prompt textarea { - width: 100%; - min-height: 120px; - background: var(--input); - color: var(--text); - border: 1px solid var(--input-border); - border-radius: 6px; - padding: 8px; - resize: vertical; -} - -.prompt .actions { - margin-top: 8px; -} - -.prompt-output { - margin-top: 10px; -} - -.prompt-output pre { - white-space: pre-wrap; - background: var(--panel); - border: 1px solid var(--border); - padding: 8px; - border-radius: 6px; -} - -/* Collapsible tree styles */ -#tree ul { - list-style: none; - margin: 0; - padding-left: 12px; -} - -#tree li { - margin: 2px 0; -} - -#tree .dir { - color: var(--text); -} - -#tree .dir .dir-label .name { - color: var(--text); -} - -#tree .file a { - color: var(--text); - text-decoration: none; -} - -#tree .file a:hover { - text-decoration: underline; -} - -/* Modal */ -.modal[aria-hidden="true"] { - display: none -} - -.modal { - position: fixed; - inset: 0; - z-index: 1000; -} - -.modal-backdrop { - position: absolute; - inset: 0; - - background: rgba(0, 0, 0, 0.5) -} - -.modal .row { - display: flex; - gap: 12px -} - -.modal .col { - flex: 1; - display: flex; - flex-direction: column; - gap: 8px -} - -.modal .list { - list-style: none; - margin: 0; - padding: 0; - max-height: 40vh; - overflow: auto -} - -.modal .list li { - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; - padding: 6px 0; - border-bottom: 1px dashed var(--border) -} - -.modal .list .use { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - border-radius: 6px; - padding: 4px 8px; - cursor: pointer -} - -.modal-dialog { - position: relative; - margin: 8vh auto; - max-width: 520px; - background: var(--panel); - color: var(--text); - border: 1px solid var(--border); - border-radius: 10px; - padding: 12px; - z-index: 1 -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px -} - -.modal-header .icon { - background: transparent; - border: none; - color: var(--text); - cursor: pointer; - font-size: 18px -} - -.modal-body { - display: flex; - flex-direction: column; - gap: 8px -} - -.modal-body input { - background: var(--input); - color: var(--text); - border: 1px solid var(--input-border); - border-radius: 6px; - padding: 8px -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 12px -} - -.modal-footer button { - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - border-radius: 6px; - padding: 6px 10px; - cursor: pointer -} - -.error { - color: #ef4444; - font-size: 12px; - min-height: 16px -} - - -#tree .toggle { - appearance: none; - -webkit-appearance: none; - width: 0; - height: 0; - position: absolute; -} - -#tree .dir-label { - display: inline-flex; - align-items: center; - gap: 6px; - cursor: pointer; - user-select: none; -} - -#tree .dir .chev { - display: inline-block; - transform: rotate(-90deg); - transition: transform 0.15s ease; - width: 10px; - height: 10px; - border-right: 2px solid #93c5fd; - border-bottom: 2px solid #93c5fd; - margin-left: 2px; -} - -#tree .dir .toggle:checked+.dir-label .chev { - transform: rotate(0deg); -} - -#tree .children { - display: none; - margin-left: 16px; -} - -#tree .dir .toggle:checked~.children { - display: block; -} - -#tree button { - margin-left: 6px; - background: var(--btn); - border: 1px solid var(--btn-border); - color: var(--text); - padding: 2px 6px; - border-radius: 4px; - cursor: pointer; - font-size: 12px; -} - -#selected li { - display: flex; - justify-content: space-between; - gap: 8px -} \ No newline at end of file diff --git a/lib/web/heroprompt/static/js/main.js b/lib/web/heroprompt/static/js/main.js deleted file mode 100644 index 40669cd0..00000000 --- a/lib/web/heroprompt/static/js/main.js +++ /dev/null @@ -1,391 +0,0 @@ -console.log('Heroprompt UI loaded'); - -let currentWs = localStorage.getItem('heroprompt-current-ws') || 'default'; -let selected = []; - -const el = (id) => document.getElementById(id); - -async function api(url) { - try { const r = await fetch(url); return await r.json(); } - catch { return { error: 'request failed' }; } -} -async function post(url, data) { - const form = new FormData(); - Object.entries(data).forEach(([k, v]) => form.append(k, v)); - try { const r = await fetch(url, { method: 'POST', body: form }); return await r.json(); } - catch { return { error: 'request failed' }; } -} - -// Checkbox-based collapsible tree -let nodeId = 0; - -function renderTree(displayName, fullPath) { - const c = document.createElement('div'); - c.className = 'tree'; - const ul = document.createElement('ul'); - ul.className = 'tree-root'; - const root = buildDirNode(displayName, fullPath, true); - ul.appendChild(root); - c.appendChild(ul); - return c; -} - -function buildDirNode(name, fullPath, expanded = false) { - const li = document.createElement('li'); - li.className = 'dir'; - const id = `tn_${nodeId++}`; - - const toggle = document.createElement('input'); - toggle.type = 'checkbox'; - toggle.className = 'toggle'; - toggle.id = id; - if (expanded) toggle.checked = true; - - const label = document.createElement('label'); - label.htmlFor = id; - label.className = 'dir-label'; - const icon = document.createElement('span'); - icon.className = 'chev'; - const text = document.createElement('span'); - text.className = 'name'; - text.textContent = name; - label.appendChild(icon); - label.appendChild(text); - - const add = document.createElement('button'); - add.textContent = '+'; - add.title = 'Add directory to selection'; - add.onclick = () => addDirToSelection(fullPath); - - const children = document.createElement('ul'); - children.className = 'children'; - - toggle.addEventListener('change', async () => { - if (toggle.checked) { - if (!li.dataset.loaded) { - await loadChildren(fullPath, children); - li.dataset.loaded = '1'; - } - } - }); - - // Load immediately if expanded by default - if (expanded) { - setTimeout(async () => { - await loadChildren(fullPath, children); - li.dataset.loaded = '1'; - }, 0); - } - - li.appendChild(toggle); - li.appendChild(label); - li.appendChild(add); - li.appendChild(children); - return li; -} - -function createFileNode(name, fullPath) { - const li = document.createElement('li'); - li.className = 'file'; - const a = document.createElement('a'); - a.href = '#'; - a.textContent = name; - a.onclick = (e) => { e.preventDefault(); }; - const add = document.createElement('button'); - add.textContent = '+'; - add.title = 'Add file to selection'; - add.onclick = () => addFileToSelection(fullPath); - li.appendChild(a); - li.appendChild(add); - return li; -} - -async function loadChildren(parentPath, ul) { - const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`); - if (r.error) { ul.innerHTML = `
  • ${r.error}
  • `; return; } - ul.innerHTML = ''; - for (const it of r.items) { - const full = parentPath.endsWith('/') ? parentPath + it.name : parentPath + '/' + it.name; - if (it.type === 'directory') { - ul.appendChild(buildDirNode(it.name, full, false)); - } else { - ul.appendChild(createFileNode(it.name, full)); - } - } -} - -async function loadDir(p) { - el('tree').innerHTML = ''; - const display = p.split('/').filter(Boolean).slice(-1)[0] || p; - el('tree').appendChild(renderTree(display, p)); - updateSelectionList(); -} - -function updateSelectionList() { - el('selCount').textContent = String(selected.length); - const ul = el('selected'); - ul.innerHTML = ''; - for (const p of selected) { - const li = document.createElement('li'); - li.textContent = p; - const btn = document.createElement('button'); - btn.textContent = 'remove'; - btn.onclick = () => { selected = selected.filter(x => x !== p); updateSelectionList(); }; - li.appendChild(btn); - ul.appendChild(li); - } - // naive token estimator ~ 4 chars/token - const tokens = Math.ceil(selected.join('\n').length / 4); - el('tokenCount').textContent = String(Math.ceil(tokens)); -} - -function addToSelection(p) { - if (!selected.includes(p)) { selected.push(p); updateSelectionList(); } -} - - -async function addDirToSelection(p) { - const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/dirs`, { method: 'POST', body: new URLSearchParams({ path: p }) }); - const j = await r.json().catch(() => ({ error: 'request failed' })); - if (j && j.ok !== false && !j.error) { if (!selected.includes(p)) selected.push(p); updateSelectionList(); } -} - -async function addFileToSelection(p) { - if (selected.includes(p)) return; - const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/files`, { method: 'POST', body: new URLSearchParams({ path: p }) }); - const j = await r.json().catch(() => ({ error: 'request failed' })); - if (j && j.ok !== false && !j.error) { selected.push(p); updateSelectionList(); } -} - - -// Theme persistence and toggle -(function initTheme() { - const saved = localStorage.getItem('hero-theme'); - const root = document.documentElement; - if (saved === 'light') root.classList.add('light'); -})(); - -el('toggleTheme').onclick = () => { - const root = document.documentElement; - const isLight = root.classList.toggle('light'); - localStorage.setItem('hero-theme', isLight ? 'light' : 'dark'); -}; - -// Workspaces list + selector -async function reloadWorkspaces() { - const sel = document.getElementById('workspaceSelect'); - if (!sel) return; - sel.innerHTML = ''; - const names = await api('/api/heroprompt/workspaces').catch(() => []); - for (const n of names || []) { - const opt = document.createElement('option'); - opt.value = n; opt.textContent = n; - sel.appendChild(opt); - } - // ensure current ws name exists or select first - function updateWsInfo(info) { const box = document.getElementById('wsInfo'); if (!box) return; if (!info || info.error) { box.textContent = ''; return; } box.textContent = `${info.name} — ${info.base_path}`; } - - if ([...sel.options].some(o => o.value === currentWs)) sel.value = currentWs; - else if (sel.options.length > 0) sel.value = sel.options[0].value; -} -// On initial load: pick current or first workspace and load its base -(async function initWorkspace() { - const sel = document.getElementById('workspaceSelect'); - const names = await api('/api/heroprompt/workspaces').catch(() => []); - if (!names || names.length === 0) return; - if (!currentWs || !names.includes(currentWs)) { currentWs = names[0]; localStorage.setItem('heroprompt-current-ws', currentWs); } - if (sel) sel.value = currentWs; - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base) await loadDir(base); -})(); -// Create workspace modal wiring -const wcShow = () => { el('wcName').value = ''; el('wcPath').value = ''; el('wcError').textContent = ''; showModal('wsCreate'); }; -el('wsCreateBtn')?.addEventListener('click', wcShow); -el('wcClose')?.addEventListener('click', () => hideModal('wsCreate')); -el('wcCancel')?.addEventListener('click', () => hideModal('wsCreate')); - -el('wcCreate')?.addEventListener('click', async () => { - const name = el('wcName').value.trim(); - const path = el('wcPath').value.trim(); - if (!path) { el('wcError').textContent = 'Path is required.'; return; } - const formData = { base_path: path }; - if (name) formData.name = name; - const resp = await post('/api/heroprompt/workspaces', formData); - if (resp.error) { el('wcError').textContent = resp.error; return; } - currentWs = resp.name || currentWs; - localStorage.setItem('heroprompt-current-ws', currentWs); - await reloadWorkspaces(); - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base) await loadDir(base); - hideModal('wsCreate'); -}); -// Workspace details modal -el('wsDetailsBtn')?.addEventListener('click', async () => { - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - if (info && !info.error) { el('wdName').value = info.name || currentWs; el('wdPath').value = info.base_path || ''; el('wdError').textContent = ''; showModal('wsDetails'); } -}); - -el('wdClose')?.addEventListener('click', () => hideModal('wsDetails')); -el('wdCancel')?.addEventListener('click', () => hideModal('wsDetails')); - -el('wdSave')?.addEventListener('click', async () => { - const newName = el('wdName').value.trim(); - const newPath = el('wdPath').value.trim(); - // update via create semantics if name changed, or add an update endpoint later - const form = new FormData(); if (newName) form.append('name', newName); if (newPath) form.append('base_path', newPath); - const resp = await fetch('/api/heroprompt/workspaces', { method: 'POST', body: form }); - const j = await resp.json().catch(() => ({ error: 'request failed' })); - if (j.error) { el('wdError').textContent = j.error; return; } - currentWs = j.name || newName || currentWs; localStorage.setItem('heroprompt-current-ws', currentWs); - await reloadWorkspaces(); - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base) await loadDir(base); - hideModal('wsDetails'); -}); - -el('wdDelete')?.addEventListener('click', async () => { - // simple delete through factory delete via dedicated endpoint would be ideal; for now we can implement a delete endpoint later - const ok = confirm('Delete this workspace?'); if (!ok) return; - const r = await fetch(`/api/heroprompt/workspaces/${currentWs}`, { method: 'DELETE' }); - const j = await r.json().catch(() => ({})); - // ignore errors for now - await reloadWorkspaces(); - const sel = document.getElementById('workspaceSelect'); - currentWs = sel?.value || ''; - localStorage.setItem('heroprompt-current-ws', currentWs); - if (currentWs) { const info = await api(`/api/heroprompt/workspaces/${currentWs}`); const base = info?.base_path || ''; if (base) await loadDir(base); } - hideModal('wsDetails'); -}); - - - - -if (document.getElementById('workspaceSelect')) { - // Copy Prompt: generate on server using workspace.prompt and copy to clipboard - el('copyPrompt')?.addEventListener('click', async () => { - const text = el('promptText')?.value || ''; - try { - const r = await fetch(`/api/heroprompt/workspaces/${currentWs}/prompt`, { method: 'POST', body: new URLSearchParams({ text }) }); - const out = await r.text(); - await navigator.clipboard.writeText(out); - } catch (e) { - console.warn('copy prompt failed', e); - } - }); - - reloadWorkspaces(); - document.getElementById('workspaceSelect').addEventListener('change', async (e) => { - currentWs = e.target.value; - localStorage.setItem('heroprompt-current-ws', currentWs); - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base) await loadDir(base); - }); -} - -document.getElementById('refreshWs')?.addEventListener('click', async () => { - const info = await api(`/api/heroprompt/workspaces/${currentWs}`); - const base = info?.base_path || ''; - if (base) await loadDir(base); -}); -document.getElementById('openWsManage')?.addEventListener('click', async () => { - // populate manage list and open - const list = el('wmList'); const err = el('wmError'); if (!list) return; - err.textContent = ''; list.innerHTML = ''; - const names = await api('/api/heroprompt/workspaces').catch(() => []); - for (const n of names || []) { const li = document.createElement('li'); const s = document.createElement('span'); s.textContent = n; const b = document.createElement('button'); b.className = 'use'; b.textContent = 'Use'; b.onclick = async () => { currentWs = n; await reloadWorkspaces(); const info = await api(`/api/heroprompt/workspaces/${currentWs}`); const base = info?.base_path || ''; if (base) await loadDir(base); hideModal('wsManage'); }; li.appendChild(s); li.appendChild(b); list.appendChild(li); } - showModal('wsManage'); -}); - -// legacy setWs kept for backward compat - binds currentWs -el('setWs')?.addEventListener('click', async () => { - const base = el('basePath')?.value?.trim(); - if (!base) { alert('Enter base path'); return; } - const r = await post('/api/heroprompt/workspaces', { name: currentWs, base_path: base }); - if (r.error) { alert(r.error); return; } - await loadDir(base); -}); - -el('doSearch').onclick = async () => { - const q = el('search').value.trim(); - if (!q) return; - const r = await api(`/api/heroprompt/search?name=${currentWs}&q=${encodeURIComponent(q)}`); - if (r.error) { alert(r.error); return; } - const tree = el('tree'); - tree.innerHTML = '
    Search results:
    '; - const ul = document.createElement('ul'); - for (const it of r) { - const li = document.createElement('li'); - li.className = it.type; - const a = document.createElement('a'); - a.href = '#'; a.textContent = it.path; - a.onclick = async (e) => { - e.preventDefault(); - if (it.type === 'file') { - const rf = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(it.path)}`); - if (!rf.error) el('preview').textContent = rf.content; - } else { - await loadDir(it.path); - } - }; - const add = document.createElement('button'); - add.textContent = '+'; - add.title = 'Add to selection'; - add.onclick = () => addToSelection(it.path); - li.appendChild(a); - li.appendChild(add); - ul.appendChild(li); - } - tree.appendChild(ul); -}; - -// Tabs -function switchTab(id) { - for (const t of document.querySelectorAll('.tab')) t.classList.remove('active'); - for (const p of document.querySelectorAll('.tab-pane')) p.classList.remove('active'); - const btn = document.querySelector(`.tab[data-tab="${id}"]`); - const pane = document.getElementById(`tab-${id}`); - if (btn && pane) { - btn.classList.add('active'); - pane.classList.add('active'); - } -} - -for (const btn of document.querySelectorAll('.tab')) { - btn.addEventListener('click', () => switchTab(btn.dataset.tab)); -} - -// Chat (client-side mock for now) -el('sendChat').onclick = () => { - const input = el('chatInput'); - const text = input.value.trim(); - if (!text) return; - addChatMessage('user', text); - input.value = ''; - // Mock AI response - setTimeout(() => addChatMessage('ai', 'This is a placeholder AI response.'), 500); -}; - -function addChatMessage(role, text) { - const msg = document.createElement('div'); - msg.className = `message ${role}`; - const bubble = document.createElement('div'); - bubble.className = 'bubble'; - bubble.textContent = text; - msg.appendChild(bubble); - el('chatMessages').appendChild(msg); - el('chatMessages').scrollTop = el('chatMessages').scrollHeight; -} - -// Modal helpers -function showModal(id) { const m = el(id); if (!m) return; m.setAttribute('aria-hidden', 'false'); } -function hideModal(id) { const m = el(id); if (!m) return; m.setAttribute('aria-hidden', 'true'); el('wsError').textContent = ''; } - - - - - - diff --git a/lib/web/heroprompt/templates/index.html b/lib/web/heroprompt/templates/index.html deleted file mode 100644 index 8d5033de..00000000 --- a/lib/web/heroprompt/templates/index.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - {{.title}} - Heroprompt - - - - -
    -
    -
    {{.title}}
    - - -
    - -
    - - -
    -
    -
    -
    - -
    -
    -

    Prompt

    - -
    -
    -
    -

    Selected Files (0) — Tokens: 0

    -
      -
      -
      -
      -
      -
      -
      -
      -
      - - -
      -
      -
      -
      -
      - - - - - - \ No newline at end of file diff --git a/lib/web/heroprompt/utils.v b/lib/web/heroprompt/utils.v deleted file mode 100644 index e44fd628..00000000 --- a/lib/web/heroprompt/utils.v +++ /dev/null @@ -1,28 +0,0 @@ -module heroprompt - -import strings - -// Very small template renderer using {{.var}} replacement -pub fn render_template(tpl string, data map[string]string) string { - mut out := tpl - for k, v in data { - out = out.replace('{{.' + k + '}}', v) - } - return out -} - -// Minimal HTML escape -pub fn html_escape(s string) string { - mut b := strings.new_builder(s.len) - for ch in s { - match ch { - `&` { b.write_string('&') } - `<` { b.write_string('<') } - `>` { b.write_string('>') } - `"` { b.write_string('"') } - `'` { b.write_string(''') } - else { b.write_string(ch.str()) } - } - } - return b.str() -} diff --git a/lib/web/ui/README.md b/lib/web/ui/README.md new file mode 100644 index 00000000..45045fb3 --- /dev/null +++ b/lib/web/ui/README.md @@ -0,0 +1,25 @@ +# HeroPrompt Web UI + +A clean web interface for creating and managing AI prompts with file and workspace management. + +## Overview + +HeroPrompt provides a VS Code–style interface to browse files, organize workspaces, and generate AI prompts. It combines a modern UI with intelligent file handling and flexible prompt generation. + +## Features + +* 🎨 **Modern UI**: Light/dark themes, responsive layout, smooth animations +* 📁 **Workspaces**: Create, update, delete, and persist workspaces +* 🗂️ **File Explorer**: Tree view, filtering, expand/collapse, multi-select +* 🔍 **Preview**: Card-based file previews with metadata and syntax highlighting +* 🚀 **Prompt Generation**: Build structured AI prompts from selected files + +## Usage + +```bash +./cli/compile.vsh # Compile +./hero web # Run server +``` + +* Create a workspace → select files → preview → generate prompts +* Manage workspaces (create, update, delete) via UI diff --git a/lib/web/ui/chat_endpoints.v b/lib/web/ui/chat_endpoints.v new file mode 100644 index 00000000..41670468 --- /dev/null +++ b/lib/web/ui/chat_endpoints.v @@ -0,0 +1,20 @@ +module ui + +import os + +pub fn render_chat_alt(app &App) !string { + tpl := os.join_path(os.dir(@FILE), 'templates', 'chat.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_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') + // version banner + result = result.replace('', '
      Rendered by: chat
      ') + return result +} diff --git a/lib/web/ui/chat_utils.v b/lib/web/ui/chat_utils.v new file mode 100644 index 00000000..79ca78a8 --- /dev/null +++ b/lib/web/ui/chat_utils.v @@ -0,0 +1,3 @@ +module ui + +// Placeholder for chat-specific utilities diff --git a/lib/web/ui/endpoints.v b/lib/web/ui/endpoints.v deleted file mode 100644 index 91d694c8..00000000 --- a/lib/web/ui/endpoints.v +++ /dev/null @@ -1,244 +0,0 @@ -module ui - -// import veb -// import freeflowuniverse.herolib.develop.heroprompt -// import os -// import json - -// // Directory browsing and file read endpoints for Heroprompt.js compatibility -// struct DirItem { -// name string -// typ string @[json: 'type'] -// } - -// struct DirResp { -// path string -// mut: -// items []DirItem -// } - -// @['/api/heroprompt/directory'; get] -// pub fn (app &App) api_heroprompt_directory(mut ctx Context) veb.Result { -// // Optional workspace name, defaults to 'default' -// wsname := ctx.query['name'] or { 'default' } -// path_q := ctx.query['path'] or { '' } -// if path_q.len == 0 { -// return ctx.text('{"error":"path required"}') -// } -// // Try to resolve against workspace base_path if available, but do not require it -// mut base := '' -// if wsp := heroprompt.get(name: wsname, create: false) { -// base = wsp.base_path -// } -// // Resolve path: if absolute, use as-is; else join with base -// mut dir_path := path_q -// if !os.is_abs_path(dir_path) && base.len > 0 { -// dir_path = os.join_path(base, dir_path) -// } -// // List entries -// entries := os.ls(dir_path) or { return ctx.text('{"error":"cannot list directory"}') } -// mut items := []map[string]string{} -// for e in entries { -// full := os.join_path(dir_path, e) -// if os.is_dir(full) { -// items << { -// 'name': e -// 'type': 'directory' -// } -// } else if os.is_file(full) { -// items << { -// 'name': e -// 'type': 'file' -// } -// } -// } -// ctx.set_content_type('application/json') -// // Encode strongly typed JSON response -// mut resp := DirResp{ -// path: dir_path -// } -// for it in items { -// resp.items << DirItem{ -// name: it['name'] or { '' } -// typ: it['type'] or { '' } -// } -// } -// return ctx.text(json.encode(resp)) -// } - -// @['/api/heroprompt/file'; get] -// pub fn (app &App) api_heroprompt_file(mut ctx Context) veb.Result { -// wsname := ctx.query['name'] or { 'default' } -// path_q := ctx.query['path'] or { '' } -// if path_q.len == 0 { -// return ctx.text('{"error":"path required"}') -// } -// // Try to resolve against workspace base_path if available, but do not require it -// mut base := '' -// if wsp := heroprompt.get(name: wsname, create: false) { -// base = wsp.base_path -// } -// mut file_path := path_q -// if !os.is_abs_path(file_path) && base.len > 0 { -// file_path = os.join_path(base, file_path) -// } -// content := os.read_file(file_path) or { return ctx.text('{"error":"failed to read"}') } -// lang := detect_lang(file_path) -// ctx.set_content_type('application/json') -// return ctx.text(json.encode({ -// 'language': lang -// 'content': content -// })) -// } - -// fn detect_lang(path string) string { -// ext := os.file_ext(path).trim_left('.') -// return match ext.to_lower() { -// 'v' { 'v' } -// 'js' { 'javascript' } -// 'ts' { 'typescript' } -// 'py' { 'python' } -// 'rs' { 'rust' } -// 'go' { 'go' } -// 'java' { 'java' } -// 'c', 'h' { 'c' } -// 'cpp', 'hpp', 'cc', 'hh' { 'cpp' } -// 'sh', 'bash' { 'bash' } -// 'json' { 'json' } -// 'yaml', 'yml' { 'yaml' } -// 'html', 'htm' { 'html' } -// 'css' { 'css' } -// 'md' { 'markdown' } -// else { 'text' } -// } -// } - -// // Heroprompt API: list workspaces -// @['/api/heroprompt/workspaces'; get] -// pub fn (app &App) api_heroprompt_list(mut ctx Context) veb.Result { -// mut names := []string{} -// ws := heroprompt.list(fromdb: true) or { []&heroprompt.Workspace{} } -// for w in ws { -// names << w.name -// } -// ctx.set_content_type('application/json') -// return ctx.text(json.encode(names)) -// } - -// // Heroprompt API: create/get workspace -// @['/api/heroprompt/workspaces'; post] -// pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result { -// name := ctx.form['name'] or { '' } -// base_path := ctx.form['base_path'] or { '' } - -// if base_path.len == 0 { -// return ctx.text('{"error":"base_path required"}') -// } - -// mut wsp := heroprompt.get(name: name, create: true, path: base_path) or { -// return ctx.text('{"error":"create failed"}') -// } - -// ctx.set_content_type('application/json') -// return ctx.text(json.encode({ -// 'name': name -// 'base_path': base_path -// })) -// } - -// // Heroprompt API: add directory to workspace -// @['/api/heroprompt/workspaces/:name/dirs'; post] -// pub fn (app &App) api_heroprompt_add_dir(mut ctx Context, name string) veb.Result { -// path := ctx.form['path'] or { '' } -// if path.len == 0 { -// return ctx.text('{"error":"path required"}') -// } -// mut wsp := heroprompt.get(name: name, create: true) or { -// return ctx.text('{"error":"workspace not found"}') -// } -// wsp.add_dir(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') } -// ctx.set_content_type('application/json') -// return ctx.text('{"ok":true}') -// } - -// // Heroprompt API: add file to workspace -// @['/api/heroprompt/workspaces/:name/files'; post] -// pub fn (app &App) api_heroprompt_add_file(mut ctx Context, name string) veb.Result { -// path := ctx.form['path'] or { '' } -// if path.len == 0 { -// return ctx.text('{"error":"path required"}') -// } -// mut wsp := heroprompt.get(name: name, create: true) or { -// return ctx.text('{"error":"workspace not found"}') -// } -// wsp.add_file(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') } -// ctx.set_content_type('application/json') -// return ctx.text('{"ok":true}') -// } - -// // Heroprompt API: generate prompt -// @['/api/heroprompt/workspaces/:name/prompt'; post] -// pub fn (app &App) api_heroprompt_prompt(mut ctx Context, name string) veb.Result { -// text := ctx.form['text'] or { '' } -// mut wsp := heroprompt.get(name: name, create: false) or { -// return ctx.text('{"error":"workspace not found"}') -// } -// prompt := wsp.prompt(text: text) -// ctx.set_content_type('text/plain') -// return ctx.text(prompt) -// } - -// // Heroprompt API: get workspace details -// @['/api/heroprompt/workspaces/:name'; get] -// pub fn (app &App) api_heroprompt_get(mut ctx Context, name string) veb.Result { -// wsp := heroprompt.get(name: name, create: false) or { -// return ctx.text('{"error":"workspace not found"}') -// } -// mut children := []map[string]string{} -// for ch in wsp.children { -// children << { -// 'name': ch.name -// 'path': ch.path.path -// 'type': if ch.path.cat == .dir { 'directory' } else { 'file' } -// } -// } -// ctx.set_content_type('application/json') -// return ctx.text(json.encode({ -// 'name': wsp.name -// 'base_path': wsp.base_path -// 'children': json.encode(children) -// })) -// } - -// // Heroprompt API: delete workspace -// @['/api/heroprompt/workspaces/:name'; delete] -// pub fn (app &App) api_heroprompt_delete(mut ctx Context, name string) veb.Result { -// wsp := heroprompt.get(name: name, create: false) or { -// return ctx.text('{"error":"workspace not found"}') -// } -// wsp.delete_workspace() or { return ctx.text('{"error":"delete failed"}') } -// ctx.set_content_type('application/json') -// return ctx.text('{"ok":true}') -// } - -// // Heroprompt API: remove directory -// @['/api/heroprompt/workspaces/:name/dirs/remove'; post] -// pub fn (app &App) api_heroprompt_remove_dir(mut ctx Context, name string) veb.Result { -// path := ctx.form['path'] or { '' } -// mut wsp := heroprompt.get(name: name, create: false) or { -// return ctx.text('{"error":"workspace not found"}') -// } -// wsp.remove_dir(path: path, name: '') or { return ctx.text('{"error":"' + err.msg() + '"}') } -// return ctx.text('{"ok":true}') -// } - -// // Heroprompt API: remove file -// @['/api/heroprompt/workspaces/:name/files/remove'; post] -// pub fn (app &App) api_heroprompt_remove_file(mut ctx Context, name string) veb.Result { -// path := ctx.form['path'] or { '' } -// mut wsp := heroprompt.get(name: name, create: false) or { -// return ctx.text('{"error":"workspace not found"}') -// } -// wsp.remove_file(path: path, name: '') or { return ctx.text('{"error":"' + err.msg() + '"}') } -// return ctx.text('{"ok":true}') -// } diff --git a/lib/web/ui/factory.v b/lib/web/ui/factory.v deleted file mode 100644 index 384b1055..00000000 --- a/lib/web/ui/factory.v +++ /dev/null @@ -1,471 +0,0 @@ -module ui - -// import veb -// import os - -// // Public Context type for veb -// pub struct Context { -// veb.Context -// } - -// // Simple tree menu structure -// pub struct MenuItem { -// pub: -// title string -// href string -// children []MenuItem -// } - -// // Factory args -// @[params] -// pub struct WebArgs { -// pub mut: -// name string = 'default' -// host string = 'localhost' -// port int = 8080 -// title string = 'Admin' -// menu []MenuItem -// open bool -// } - -// // The App holds server state and config -// pub struct App { -// veb.StaticHandler -// pub mut: -// title string = 'default' -// menu []MenuItem -// port int = 7711 -// } - -// // Start the webserver (blocking) -// pub fn start(args WebArgs) ! { -// mut app := App{ -// title: args.title -// menu: args.menu -// port: args.port -// } -// veb.run[App, Context](mut app, app.port) -// } - -// // Routes - -// // Redirect root to /admin -// @['/'; get] -// pub fn (app &App) root(mut ctx Context) veb.Result { -// return ctx.redirect('/admin') -// } - -// // Admin home page -// @['/admin'; get] -// pub fn (app &App) admin_index(mut ctx Context) veb.Result { -// return ctx.html(app.render_admin('/', 'Welcome')) -// } - -// // HeroScript editor page -// @['/admin/heroscript'; get] -// pub fn (app &App) admin_heroscript(mut ctx Context) veb.Result { -// return ctx.html(app.render_heroscript()) -// } - -// // Chat page -// @['/admin/chat'; get] -// pub fn (app &App) admin_chat(mut ctx Context) veb.Result { -// return ctx.html(app.render_chat()) -// } - -// // Static CSS files -// @['/static/css/colors.css'; get] -// pub fn (app &App) serve_colors_css(mut ctx Context) veb.Result { -// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'colors.css') -// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') } -// ctx.set_content_type('text/css') -// return ctx.text(css_content) -// } - -// @['/static/css/main.css'; get] -// pub fn (app &App) serve_main_css(mut ctx Context) veb.Result { -// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'main.css') -// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') } -// ctx.set_content_type('text/css') -// return ctx.text(css_content) -// } - -// // Static JS files -// @['/static/js/theme.js'; get] -// pub fn (app &App) serve_theme_js(mut ctx Context) veb.Result { -// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'theme.js') -// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') } -// ctx.set_content_type('application/javascript') -// return ctx.text(js_content) -// } - -// @['/static/js/heroscript.js'; get] -// pub fn (app &App) serve_heroscript_js(mut ctx Context) veb.Result { -// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'heroscript.js') -// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') } -// ctx.set_content_type('application/javascript') -// return ctx.text(js_content) -// } - -// @['/static/js/chat.js'; get] -// pub fn (app &App) serve_chat_js(mut ctx Context) veb.Result { -// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'chat.js') -// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') } -// ctx.set_content_type('application/javascript') -// return ctx.text(js_content) -// } - -// @['/static/css/heroscript.css'; get] -// pub fn (app &App) serve_heroscript_css(mut ctx Context) veb.Result { -// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'heroscript.css') -// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') } -// ctx.set_content_type('text/css') -// return ctx.text(css_content) -// } - -// @['/static/css/chat.css'; get] -// pub fn (app &App) serve_chat_css(mut ctx Context) veb.Result { -// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'chat.css') -// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') } -// ctx.set_content_type('text/css') -// return ctx.text(css_content) -// } - -// // Catch-all content under /admin/* -// @['/admin/:path...'; get] -// pub fn (app &App) admin_section(mut ctx Context, path string) veb.Result { -// // Render current path in the main content -// return ctx.html(app.render_admin(path, 'Content')) -// } - -// // View rendering using external template - -// fn (app &App) render_admin(path string, heading string) string { -// // Get the template file path relative to the module -// template_path := os.join_path(os.dir(@FILE), 'templates', 'admin_layout.html') - -// // Read the template file -// template_content := os.read_file(template_path) or { -// // Fallback to inline template if file not found -// return app.render_admin_fallback(path, heading) -// } - -// // Generate menu HTML -// menu_content := menu_html(app.menu, 0, 'm') - -// // Simple template variable replacement -// mut result := template_content -// result = result.replace('{{.title}}', app.title) -// result = result.replace('{{.heading}}', heading) -// result = result.replace('{{.path}}', path) -// result = result.replace('{{.menu_html}}', menu_content) -// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css') -// result = result.replace('{{.css_main_url}}', '/static/css/main.css') -// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') - -// return result -// } - -// // HeroScript editor rendering using external template -// fn (app &App) render_heroscript() string { -// // Get the template file path relative to the module -// template_path := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.html') - -// // Read the template file -// template_content := os.read_file(template_path) or { -// // Fallback to basic template if file not found -// return app.render_heroscript_fallback() -// } - -// // Generate menu HTML -// menu_content := menu_html(app.menu, 0, 'm') - -// // Simple template variable replacement -// mut result := template_content -// result = result.replace('{{.title}}', app.title) -// result = result.replace('{{.menu_html}}', menu_content) -// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css') -// result = result.replace('{{.css_main_url}}', '/static/css/main.css') -// result = result.replace('{{.css_heroscript_url}}', '/static/css/heroscript.css') -// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') -// result = result.replace('{{.js_heroscript_url}}', '/static/js/heroscript.js') - -// return result -// } - -// // Chat rendering using external template -// fn (app &App) render_chat() string { -// // Get the template file path relative to the module -// template_path := os.join_path(os.dir(@FILE), 'templates', 'chat.html') - -// // Read the template file -// template_content := os.read_file(template_path) or { -// // Fallback to basic template if file not found -// return app.render_chat_fallback() -// } - -// // Generate menu HTML -// menu_content := menu_html(app.menu, 0, 'm') - -// // Simple template variable replacement -// mut result := template_content -// result = result.replace('{{.title}}', app.title) -// result = result.replace('{{.menu_html}}', menu_content) -// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css') -// result = result.replace('{{.css_main_url}}', '/static/css/main.css') -// result = result.replace('{{.css_chat_url}}', '/static/css/chat.css') -// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') -// result = result.replace('{{.js_chat_url}}', '/static/js/chat.js') - -// return result -// } - -// // Fallback HeroScript rendering method -// fn (app &App) render_heroscript_fallback() string { -// return ' -// -// -// -// -// -// ${app.title} - HeroScript Editor -// -// -// -//
      -//

      HeroScript Editor

      -//

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

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

      Chat Assistant

      -//

      Chat template not found. Please check the template files.

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

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

      -//

      Use the treeview on the left to navigate.

      -//
      -//
      -//
      -//
      - -// -// -// -// ' -// } - -// // Recursive menu renderer - -// fn menu_html(items []MenuItem, depth int, prefix string) string { -// mut out := []string{} -// for i, it in items { -// id := '${prefix}_${depth}_${i}' -// if it.children.len > 0 { -// // expandable group -// out << '
      ' -// out << '' -// out << '${it.title}' -// out << '' -// out << '
      ' -// out << '
      ' -// out << menu_html(it.children, depth + 1, id) -// out << '
      ' -// out << '
      ' -// out << '
      ' -// } else { -// // leaf -// out << '' -// } -// } -// return out.join('\n') -// } - -// // Default sample menu -// fn default_menu() []MenuItem { -// return [ -// MenuItem{ -// title: 'Dashboard' -// href: '/admin' -// }, -// MenuItem{ -// title: 'HeroScript' -// href: '/admin/heroscript' -// }, -// MenuItem{ -// title: 'Chat' -// href: '/admin/chat' -// }, -// MenuItem{ -// title: 'Users' -// children: [ -// MenuItem{ -// title: 'Overview' -// href: '/admin/users/overview' -// }, -// MenuItem{ -// title: 'Create' -// href: '/admin/users/create' -// }, -// MenuItem{ -// title: 'Roles' -// href: '/admin/users/roles' -// }, -// ] -// }, -// MenuItem{ -// title: 'Content' -// children: [ -// MenuItem{ -// title: 'Pages' -// href: '/admin/content/pages' -// }, -// MenuItem{ -// title: 'Media' -// href: '/admin/content/media' -// }, -// MenuItem{ -// title: 'Settings' -// children: [ -// MenuItem{ -// title: 'SEO' -// href: '/admin/content/settings/seo' -// }, -// MenuItem{ -// title: 'Themes' -// href: '/admin/content/settings/themes' -// }, -// ] -// }, -// ] -// }, -// MenuItem{ -// title: 'System' -// children: [ -// MenuItem{ -// title: 'Status' -// href: '/admin/system/status' -// }, -// MenuItem{ -// title: 'Logs' -// href: '/admin/system/logs' -// }, -// MenuItem{ -// title: 'Backups' -// href: '/admin/system/backups' -// }, -// ] -// }, -// ] -// } diff --git a/lib/web/ui/heroprompt_api.v b/lib/web/ui/heroprompt_api.v new file mode 100644 index 00000000..871e4f12 --- /dev/null +++ b/lib/web/ui/heroprompt_api.v @@ -0,0 +1,305 @@ +module ui + +import veb +import os +import json +import freeflowuniverse.herolib.develop.heroprompt as hp + +// Types +struct DirResp { + path string + items []hp.ListItem +} + +// Utility functions +fn expand_home_path(path string) string { + if path.starts_with('~') { + home := os.home_dir() + return os.join_path(home, path.all_after('~')) + } + return path +} + +fn json_error(message string) string { + return '{"error":"${message}"}' +} + +fn json_success() string { + return '{"ok":true}' +} + +// Recursive search function +fn search_directory(dir_path string, base_path string, query_lower string, mut results []map[string]string) { + entries := os.ls(dir_path) or { return } + + for entry in entries { + full_path := os.join_path(dir_path, entry) + + // Skip hidden files and common ignore patterns + if entry.starts_with('.') || entry == 'node_modules' || entry == 'target' + || entry == 'build' { + continue + } + + // Get relative path from workspace base + mut rel_path := full_path + if full_path.starts_with(base_path) { + rel_path = full_path[base_path.len..] + if rel_path.starts_with('/') { + rel_path = rel_path[1..] + } + } + + // Check if filename or path matches search query + if entry.to_lower().contains(query_lower) || rel_path.to_lower().contains(query_lower) { + results << { + 'name': entry + 'path': rel_path + 'full_path': full_path + 'type': if os.is_dir(full_path) { 'directory' } else { 'file' } + } + } + + // Recursively search subdirectories + if os.is_dir(full_path) { + search_directory(full_path, base_path, query_lower, mut results) + } + } +} + +// APIs +@['/api/heroprompt/workspaces'; get] +pub fn (app &App) api_heroprompt_list(mut ctx Context) veb.Result { + mut names := []string{} + ws := hp.list_workspaces_fromdb() 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_heroprompt_create(mut ctx Context) veb.Result { + name_input := ctx.form['name'] or { '' } + base_path_in := ctx.form['base_path'] or { '' } + if base_path_in.len == 0 { + return ctx.text(json_error('base_path required')) + } + + base_path := expand_home_path(base_path_in) + + // If no name provided, generate a random name + mut name := name_input.trim(' \t\n\r') + if name.len == 0 { + name = hp.generate_random_workspace_name() + } + + wsp := hp.get(name: name, create: true, path: base_path) or { + return ctx.text(json_error('create failed')) + } + ctx.set_content_type('application/json') + return ctx.text(json.encode({ + 'name': wsp.name + 'base_path': wsp.base_path + })) +} + +@['/api/heroprompt/workspaces/:name'; get] +pub fn (app &App) api_heroprompt_get(mut ctx Context, name string) veb.Result { + wsp := hp.get(name: name, create: false) or { + return ctx.text(json_error('workspace not found')) + } + ctx.set_content_type('application/json') + return ctx.text(json.encode({ + 'name': wsp.name + 'base_path': wsp.base_path + 'selected_files': wsp.selected_children().len.str() + })) +} + +@['/api/heroprompt/workspaces/:name'; put] +pub fn (app &App) api_heroprompt_update(mut ctx Context, name string) veb.Result { + wsp := hp.get(name: name, create: false) or { + return ctx.text(json_error('workspace not found')) + } + + new_name := ctx.form['name'] or { name } + new_base_path_in := ctx.form['base_path'] or { wsp.base_path } + new_base_path := expand_home_path(new_base_path_in) + + // Update the workspace using the update_workspace method + updated_wsp := wsp.update_workspace( + name: new_name + base_path: new_base_path + ) or { return ctx.text(json_error('failed to update workspace')) } + + ctx.set_content_type('application/json') + return ctx.text(json.encode({ + 'name': updated_wsp.name + 'base_path': updated_wsp.base_path + })) +} + +// Delete endpoint using POST (VEB framework compatibility) +@['/api/heroprompt/workspaces/:name/delete'; post] +pub fn (app &App) api_heroprompt_delete(mut ctx Context, name string) veb.Result { + wsp := hp.get(name: name, create: false) or { + return ctx.text(json_error('workspace not found')) + } + + // Delete the workspace + wsp.delete_workspace() or { return ctx.text(json_error('failed to delete workspace')) } + + ctx.set_content_type('application/json') + return ctx.text(json_success()) +} + +@['/api/heroprompt/directory'; get] +pub fn (app &App) api_heroprompt_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(json_error('workspace not found')) + } + items := wsp.list_dir(path_q) or { return ctx.text(json_error('cannot list directory')) } + ctx.set_content_type('application/json') + 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_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(json_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(json_error('not a file')) + } + content := os.read_file(file_path) or { return ctx.text(json_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_heroprompt_add_file(mut ctx Context, name string) veb.Result { + path := ctx.form['path'] or { '' } + if path.len == 0 { + return ctx.text(json_error('path required')) + } + mut wsp := hp.get(name: name, create: false) or { + return ctx.text(json_error('workspace not found')) + } + wsp.add_file(path: path) or { return ctx.text(json_error(err.msg())) } + return ctx.text(json_success()) +} + +@['/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(json_error('path required')) + } + mut wsp := hp.get(name: name, create: false) or { + return ctx.text(json_error('workspace not found')) + } + wsp.add_dir(path: path) or { return ctx.text(json_error(err.msg())) } + return ctx.text(json_success()) +} + +@['/api/heroprompt/workspaces/:name/prompt'; post] +pub fn (app &App) api_heroprompt_generate_prompt(mut ctx Context, name string) veb.Result { + text := ctx.form['text'] or { '' } + mut wsp := hp.get(name: name, create: false) or { + ctx.set_content_type('application/json') + return ctx.text(json_error('workspace not found')) + } + prompt := wsp.prompt(text: text) + ctx.set_content_type('text/plain') + return ctx.text(prompt) +} + +@['/api/heroprompt/workspaces/:name/selection'; post] +pub fn (app &App) api_heroprompt_sync_selection(mut ctx Context, name string) veb.Result { + paths_json := ctx.form['paths'] or { '[]' } + mut wsp := hp.get(name: name, create: false) or { + return ctx.text(json_error('workspace not found')) + } + + // Clear current selection + wsp.children.clear() + + // Parse paths and add them to workspace + paths := json.decode([]string, paths_json) or { + return ctx.text(json_error('invalid paths format')) + } + + for path in paths { + if os.is_file(path) { + wsp.add_file(path: path) or { + continue // Skip files that can't be added + } + } else if os.is_dir(path) { + wsp.add_dir(path: path) or { + continue // Skip directories that can't be added + } + } + } + + return ctx.text(json_success()) +} + +@['/api/heroprompt/workspaces/:name/search'; get] +pub fn (app &App) api_heroprompt_search(mut ctx Context, name string) veb.Result { + query := ctx.query['q'] or { '' } + if query.len == 0 { + return ctx.text(json_error('search query required')) + } + + wsp := hp.get(name: name, create: false) or { + return ctx.text(json_error('workspace not found')) + } + + // Simple recursive file search implementation + mut results := []map[string]string{} + query_lower := query.to_lower() + + // Recursive function to search files + search_directory(wsp.base_path, wsp.base_path, query_lower, mut results) + + ctx.set_content_type('application/json') + + // Manually build JSON response to avoid encoding issues + mut json_results := '[' + for i, result in results { + if i > 0 { + json_results += ',' + } + json_results += '{' + json_results += '"name":"${result['name']}",' + json_results += '"path":"${result['path']}",' + json_results += '"full_path":"${result['full_path']}",' + json_results += '"type":"${result['type']}"' + json_results += '}' + } + json_results += ']' + + response := '{"query":"${query}","results":${json_results},"count":"${results.len}"}' + return ctx.text(response) +} diff --git a/lib/web/ui/heroprompt_page.v b/lib/web/ui/heroprompt_page.v new file mode 100644 index 00000000..30b7435f --- /dev/null +++ b/lib/web/ui/heroprompt_page.v @@ -0,0 +1,21 @@ +module ui + +import os + +// Render the Heroprompt admin page using feature template +pub fn render_heroprompt_page(app &App) !string { + tpl := os.join_path(os.dir(@FILE), 'templates', 'heroprompt.html') + content := os.read_file(tpl)! + menu_content := menu_html(app.menu, 0, 'm') + mut result := content + result = result.replace('{{.title}}', app.title) + result = result.replace('{{.menu_html}}', menu_content) + result = result.replace('{{.css_colors_url}}', '/static/css/colors.css') + result = result.replace('{{.css_main_url}}', '/static/css/main.css') + result = result.replace('{{.css_heroprompt_url}}', '/static/css/heroprompt.css') + result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') + result = result.replace('{{.js_heroprompt_url}}', '/static/js/heroprompt.js') + // version banner + result = result.replace('', '
      Rendered by: heroprompt
      ') + return result +} diff --git a/lib/web/ui/heroprompt_utils.v b/lib/web/ui/heroprompt_utils.v new file mode 100644 index 00000000..a447cfa7 --- /dev/null +++ b/lib/web/ui/heroprompt_utils.v @@ -0,0 +1,3 @@ +module ui + +// Placeholder for heroprompt-specific utilities (if needed later) diff --git a/lib/web/ui/heroscript_endpoints.v b/lib/web/ui/heroscript_endpoints.v new file mode 100644 index 00000000..307aaa09 --- /dev/null +++ b/lib/web/ui/heroscript_endpoints.v @@ -0,0 +1,23 @@ +module ui + +import os + +// Render HeroScript page +pub fn render_heroscript_alt(app &App) !string { + tpl := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.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) + // 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/css/heroscript.css') + result = result.replace('{{.js_heroscript_url}}', '/static/js/heroscript.js') + // version banner + result = result.replace('', '
      Rendered by: heroscript
      ') + return result +} diff --git a/lib/web/ui/heroscript_utils.v b/lib/web/ui/heroscript_utils.v new file mode 100644 index 00000000..d8f35dc1 --- /dev/null +++ b/lib/web/ui/heroscript_utils.v @@ -0,0 +1,3 @@ +module ui + +// Placeholder for heroscript-specific utilities diff --git a/lib/web/ui/menu.v b/lib/web/ui/menu.v new file mode 100644 index 00000000..dddcbd60 --- /dev/null +++ b/lib/web/ui/menu.v @@ -0,0 +1,83 @@ +module ui + +// Default menu for the Admin UI. Used when no custom menu is provided. +pub fn get_default_menu() []MenuItem { + return [ + MenuItem{ + title: 'Dashboard' + href: '/admin' + }, + MenuItem{ + title: 'HeroScript' + href: '/admin/heroscript' + }, + MenuItem{ + title: 'Chat' + href: '/admin/chat' + }, + MenuItem{ + title: 'Heroprompt' + href: '/admin/heroprompt' + }, + MenuItem{ + title: 'Users' + children: [ + MenuItem{ + title: 'Overview' + href: '/admin/users/overview' + }, + MenuItem{ + title: 'Create' + href: '/admin/users/create' + }, + MenuItem{ + title: 'Roles' + href: '/admin/users/roles' + }, + ] + }, + MenuItem{ + title: 'Content' + children: [ + MenuItem{ + title: 'Pages' + href: '/admin/content/pages' + }, + MenuItem{ + title: 'Media' + href: '/admin/content/media' + }, + MenuItem{ + title: 'Settings' + children: [ + MenuItem{ + title: 'SEO' + href: '/admin/content/settings/seo' + }, + MenuItem{ + title: 'Themes' + href: '/admin/content/settings/themes' + }, + ] + }, + ] + }, + MenuItem{ + title: 'System' + children: [ + MenuItem{ + title: 'Status' + href: '/admin/system/status' + }, + MenuItem{ + title: 'Logs' + href: '/admin/system/logs' + }, + MenuItem{ + title: 'Backups' + href: '/admin/system/backups' + }, + ] + }, + ] +} diff --git a/lib/web/ui/server.v b/lib/web/ui/server.v index fa1dcec5..7d59788b 100644 --- a/lib/web/ui/server.v +++ b/lib/web/ui/server.v @@ -1,243 +1,209 @@ module ui -// import veb -// import os -// import net.http -// import json -// import freeflowuniverse.herolib.develop.heroprompt +import veb +import os +// 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 static folder + base := os.dir(@FILE) + 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) -// } +// 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) -// } +// Test API endpoint to verify routing works +@['/api/test'; get] +pub fn (app &App) api_test(mut ctx Context) veb.Result { + ctx.set_content_type('application/json') + return ctx.text('{"status":"ok","message":"API is working"}') +} -// @['/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) -// } +// 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 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_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 +} -// @['/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_heroprompt(app &App) string { + template_path := os.join_path(os.dir(@FILE), '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/css/heroprompt.css') + result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') + result = result.replace('{{.js_heroprompt_url}}', '/static/js/heroprompt.js') + // version banner + result = result.replace('', '
      Rendered by: heroprompt
      ') + 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) -// } +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 +} -// @['/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) -// } +// Fallbacks +fn render_heroprompt_fallback(app &App) string { + return '\n\n\n\n\t\n\t\n\t${app.title} - Heroprompt\n\t\n\n\n\t
      \n\t\t

      Heroprompt

      \n\t\t

      Heroprompt template not found. Please check the template files.

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

      HeroScript Editor

      \n\t\t

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

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

      Chat Assistant

      \n\t\t

      Chat template not found. Please check the template files.

      \n\t\tBack to Admin\n\t
      \n\n\n' +} -// // Pure functions for rendering templates -// fn render_admin(app &App, path string, heading string) string { -// template_path := os.join_path(os.dir(@FILE), 'templates', 'admin_layout.html') -// template_content := os.read_file(template_path) or { -// return render_admin_fallback(app, path, heading) -// } -// menu_content := menu_html(app.menu, 0, 'm') -// mut result := template_content -// result = result.replace('{{.title}}', app.title) -// result = result.replace('{{.heading}}', heading) -// result = result.replace('{{.path}}', path) -// result = result.replace('{{.menu_html}}', menu_content) -// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css') -// result = result.replace('{{.css_main_url}}', '/static/css/main.css') -// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') -// return result -// } - -// fn render_heroscript(app &App) string { -// template_path := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.html') -// template_content := os.read_file(template_path) or { return render_heroscript_fallback(app) } -// menu_content := menu_html(app.menu, 0, 'm') -// mut result := template_content -// result = result.replace('{{.title}}', app.title) -// result = result.replace('{{.menu_html}}', menu_content) -// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css') -// result = result.replace('{{.css_main_url}}', '/static/css/main.css') -// result = result.replace('{{.css_heroscript_url}}', '/static/css/heroscript.css') -// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') -// result = result.replace('{{.js_heroscript_url}}', '/static/js/heroscript.js') -// return result -// } - -// fn render_chat(app &App) string { -// template_path := os.join_path(os.dir(@FILE), 'templates', 'chat.html') -// template_content := os.read_file(template_path) or { return render_chat_fallback(app) } -// menu_content := menu_html(app.menu, 0, 'm') -// mut result := template_content -// result = result.replace('{{.title}}', app.title) -// result = result.replace('{{.menu_html}}', menu_content) -// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css') -// result = result.replace('{{.css_main_url}}', '/static/css/main.css') -// result = result.replace('{{.css_chat_url}}', '/static/css/chat.css') -// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js') -// result = result.replace('{{.js_chat_url}}', '/static/js/chat.js') -// return result -// } - -// // Fallbacks -// fn render_heroscript_fallback(app &App) string { -// return '\n\n\n\n\t\n\t\n\t${app.title} - HeroScript Editor\n\t\n\n\n\t
      \n\t\t

      HeroScript Editor

      \n\t\t

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

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

      Chat Assistant

      \n\t\t

      Chat template not found. Please check the template files.

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

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

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

      Use the treeview on the left to navigate.

      \n\t\t\t\t
      \n\t\t\t
      \n\t\t
      \n\t
      \n\n\t\n\n\n' -// } +fn render_admin_fallback(app &App, path string, heading string) string { + menu_content := menu_html(app.menu, 0, 'm') + return '\n\n\n\n\t\n\t\n\t${app.title}\n\t\n\t\n\n\n\t\n\n\t\n\n\t
      \n\t\t
      \n\t\t\t
      \n\t\t\t\t
      ${heading}
      \n\t\t\t\t/admin/${path}\n\t\t\t
      \n\t\t\t
      \n\t\t\t\t
      \n\t\t\t\t\t

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

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

      Use the treeview on the left to navigate.

      \n\t\t\t\t
      \n\t\t\t
      \n\t\t
      \n\t
      \n\n\t\n\n\n' +} diff --git a/lib/web/ui/templates/css/chat.css b/lib/web/ui/static/css/chat.css similarity index 100% rename from lib/web/ui/templates/css/chat.css rename to lib/web/ui/static/css/chat.css diff --git a/lib/web/ui/templates/css/colors.css b/lib/web/ui/static/css/colors.css similarity index 100% rename from lib/web/ui/templates/css/colors.css rename to lib/web/ui/static/css/colors.css diff --git a/lib/web/ui/static/css/heroprompt.css b/lib/web/ui/static/css/heroprompt.css new file mode 100644 index 00000000..8418b09f --- /dev/null +++ b/lib/web/ui/static/css/heroprompt.css @@ -0,0 +1,2705 @@ +/* Enhanced HeroPrompt UI - Clean VSCode-like Design */ + +/* Icon definitions using CSS pseudo-elements */ +.icon-collapse::before { + content: "⌄"; +} + +.icon-refresh::before { + content: "↻"; +} + +.icon-settings::before { + content: "⚙"; +} + + +.icon-close::before { + content: "✕"; +} + +.icon-folder-open::before { + content: "📂"; +} + +.icon-folder-closed::before { + content: "📁"; +} + +.icon-file::before { + content: "📄"; +} + +.icon-selection::before { + content: "☑"; +} + +.icon-prompt::before { + content: "✎"; +} + +.icon-chat::before { + content: "💬"; +} + +.icon-ai::before { + content: "🤖"; +} + +.icon-user::before { + content: "👤"; +} + +.icon-send::before { + content: "➤"; +} + +.icon-plus::before { + content: "+"; +} + +.icon-clear::before { + content: "🗑️"; +} + +.icon-export::before { + content: "📤"; +} + +.icon-attach::before { + content: "📎"; +} + +.icon-copy::before { + content: "📋"; +} + +.icon-regenerate::before { + content: "🔄"; +} + +.icon-edit::before { + content: "✏️"; +} + +.icon-template::before { + content: "📄"; +} + +.icon-save::before { + content: "💾"; +} + +.icon-token::before { + content: "🔢"; +} + +.icon-search::before { + content: "🔍"; +} + +.icon-generate::before { + content: "⚡"; +} + +.icon-empty::before { + content: "∅"; +} + +/* Base layout improvements */ +.main { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +/* Explorer Panel - Enhanced modern design */ +.explorer-panel { + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 12px; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.3s ease; +} + +.explorer-panel:hover { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); +} + +.explorer-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border-primary); + background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%); + backdrop-filter: blur(10px); +} + +.explorer-title { + font-size: 12px; + letter-spacing: 0.8px; + color: var(--text-secondary); + margin: 0; + font-weight: 700; + text-transform: uppercase; +} + +.explorer-actions { + display: flex; + gap: 6px; +} + +.btn-ghost { + background: transparent; + border: none; + color: var(--text-secondary); + padding: 6px 8px; + border-radius: 6px; + font-size: 12px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + position: relative; + overflow: hidden; +} + +.btn-ghost::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--link-color); + opacity: 0; + transition: opacity 0.2s ease; + border-radius: 6px; +} + +.btn-ghost:hover { + background-color: var(--menu-item-hover-bg); + color: var(--text-primary); + transform: translateY(-1px); +} + +.btn-ghost:hover::before { + opacity: 0.1; +} + +.btn-ghost:active { + transform: translateY(0); +} + +.workspace-selector { + margin-bottom: 16px; +} + +.modern-select { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + border-radius: 8px; + font-size: 13px; + padding: 8px 12px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.modern-select:focus { + border-color: var(--link-color); + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15), 0 4px 8px rgba(0, 0, 0, 0.1); + outline: none; + transform: translateY(-1px); +} + +.modern-select:hover { + border-color: var(--link-color); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.search-container { + position: relative; + margin-bottom: 16px; +} + +.search-icon { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + border-right: none; + border-radius: 8px 0 0 8px; + color: var(--text-secondary); + font-size: 12px; + padding: 8px 10px; + transition: all 0.2s ease; +} + +.modern-input { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + border-left: none; + border-right: none; + color: var(--text-primary); + font-size: 13px; + padding: 8px 12px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.modern-input:focus { + border-color: var(--link-color); + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15); + outline: none; +} + +.modern-input:focus+.search-clear, +.modern-input:focus~.search-icon { + border-color: var(--link-color); +} + +.search-clear { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + border-left: none; + border-radius: 0 8px 8px 0; + padding: 8px 10px; + transition: all 0.2s ease; + cursor: pointer; +} + +.search-clear:hover { + background-color: var(--bg-tertiary); + color: var(--text-primary); +} + +.explorer-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + padding: 8px 12px; +} + +.selection-controls { + padding: 8px 0; + border-bottom: 1px solid var(--border-primary); + margin-bottom: 8px; +} + +.selection-info .badge-selection { + background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-primary) 100%); + color: var(--text-primary); + font-size: 11px; + padding: 4px 8px; + border-radius: 12px; + border: 1px solid var(--border-primary); + font-weight: 600; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.selection-actions { + display: flex; + gap: 8px; +} + +.btn-xs { + font-size: 11px; + padding: 4px 8px; + border-radius: 6px; + transition: all 0.2s ease; +} + +.btn-xs:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Enhanced File Tree with smooth scrolling */ +.file-tree { + flex: 1; + overflow-y: auto; + padding: 8px 0; + font-size: 13px; + line-height: 1.3; + scroll-behavior: smooth; +} + +.file-tree::-webkit-scrollbar { + width: 8px; +} + +.file-tree::-webkit-scrollbar-track { + background: var(--bg-secondary); + border-radius: 4px; +} + +.file-tree::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, var(--border-primary) 0%, var(--text-secondary) 100%); + border-radius: 4px; + transition: background 0.2s ease; +} + +.file-tree::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, var(--text-secondary) 0%, var(--link-color) 100%); +} + +/* Smooth scrollbar for Firefox */ +.file-tree { + scrollbar-width: thin; + scrollbar-color: var(--border-primary) var(--bg-secondary); +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + color: var(--text-muted); + text-align: center; + animation: fadeIn 0.5s ease-in-out; +} + +.empty-state i { + font-size: 32px; + margin-bottom: 12px; + opacity: 0.6; + animation: pulse 2s infinite; +} + +.empty-state p { + margin: 0; + font-size: 14px; + font-weight: 500; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse { + + 0%, + 100% { + opacity: 0.6; + transform: scale(1); + } + + 50% { + opacity: 0.8; + transform: scale(1.05); + } +} + +/* Tree Items - Enhanced with smooth animations */ +.tree-item { + display: block; + margin: 0; + user-select: none; + overflow: hidden; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.tree-item-content { + display: flex; + align-items: center; + padding: 3px 8px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + min-height: 24px; + position: relative; +} + +.tree-item-content:hover { + background-color: var(--menu-item-hover-bg); + transform: translateX(2px); +} + +.tree-item-content:active { + transform: translateX(1px); + transition: transform 0.1s ease; +} + +.tree-checkbox { + margin: 0 8px 0 0; + cursor: pointer; + width: 14px; + height: 14px; + accent-color: var(--link-color); + transition: transform 0.2s ease; +} + +.tree-checkbox:hover { + transform: scale(1.1); +} + +.tree-expand-btn { + background: none; + border: none; + color: var(--text-secondary); + font-size: 12px; + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + margin-right: 4px; + border-radius: 3px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + font-weight: bold; +} + +.tree-expand-btn:hover { + background-color: var(--bg-tertiary); + color: var(--text-primary); + transform: scale(1.1); +} + +.tree-expand-btn:active { + transform: scale(0.95); +} + +.tree-expand-spacer { + width: 18px; + height: 18px; + margin-right: 4px; +} + +.tree-icon { + font-size: 14px; + margin-right: 8px; + width: 18px; + text-align: center; + transition: transform 0.2s ease; +} + +.tree-item-content:hover .tree-icon { + transform: scale(1.05); +} + +.tree-label { + flex: 1; + font-size: 13px; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + font-weight: 500; + transition: color 0.2s ease; +} + +.tree-item-content:hover .tree-label { + color: var(--link-color); +} + +/* File type specific styling */ +.tree-item[data-type="directory"] .tree-label { + font-weight: 600; +} + +.tree-item[data-type="file"] .tree-label { + font-weight: 400; + opacity: 0.9; +} + +/* Depth-based styling for better visual hierarchy */ +.tree-item[data-depth="0"] .tree-item-content { + font-weight: 600; + border-left: 3px solid transparent; +} + +.tree-item[data-depth="1"] .tree-item-content { + border-left: 2px solid var(--border-primary); +} + +.tree-item[data-depth="2"] .tree-item-content { + border-left: 1px solid var(--border-primary); +} + +/* Selected state */ +.tree-checkbox:checked+.tree-expand-btn+.tree-icon+.tree-label, +.tree-checkbox:checked+.tree-expand-spacer+.tree-icon+.tree-label { + color: var(--link-color); + font-weight: 600; +} + +.tree-checkbox:checked~* { + opacity: 1; +} + +/* Workspace Panel */ +.workspace-panel { + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 8px; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.workspace-header { + background-color: var(--bg-tertiary); + border-bottom: 1px solid var(--border-primary); + padding: 0; +} + +.tab-navigation { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; +} + +.modern-tabs { + display: flex; + gap: 2px; + border: none; +} + +.modern-tabs .nav-link { + background: transparent; + border: none; + color: var(--text-secondary); + padding: 8px 12px; + border-radius: 6px 6px 0 0; + font-size: 13px; + display: flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; + cursor: pointer; +} + +.modern-tabs .nav-link:hover { + background-color: var(--menu-item-hover-bg); + color: var(--text-primary); +} + +.modern-tabs .nav-link.active { + background-color: var(--bg-primary); + color: var(--text-primary); + border-bottom: 2px solid var(--link-color); +} + +.badge-count { + background-color: var(--bg-tertiary); + color: var(--text-primary); + font-size: 10px; + padding: 1px 4px; + border-radius: 8px; + min-width: 16px; + text-align: center; +} + +.workspace-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.token-counter { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: var(--text-secondary); +} + +.workspace-content { + flex: 1; + overflow: hidden; +} + +.tab-pane { + height: 100%; + overflow: hidden; + background-color: var(--bg-primary); +} + +/* Selection Workspace */ +.selection-workspace { + height: 100%; + display: flex; + flex-direction: column; + padding: 16px; +} + +.selection-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid var(--border-primary); +} + +.section-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.selection-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.selection-cards-container { + flex: 1; + display: flex; + flex-direction: column; + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 8px; + overflow: hidden; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background-color: var(--bg-tertiary); + border-bottom: 1px solid var(--border-primary); +} + +.panel-title { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.selection-cards { + flex: 1; + overflow-y: auto; + padding: 16px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 16px; + align-content: start; +} + +.selection-cards::-webkit-scrollbar { + width: 8px; +} + +.selection-cards::-webkit-scrollbar-track { + background: var(--bg-secondary); + border-radius: 4px; +} + +.selection-cards::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 4px; +} + +.selection-cards::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +.file-card { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + border-radius: 8px; + padding: 16px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + position: relative; + overflow: hidden; +} + +.file-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + border-color: var(--link-color); +} + +.file-card-header { + display: flex; + align-items: flex-start; + gap: 12px; + margin-bottom: 12px; +} + +.file-card-icon { + font-size: 24px; + flex-shrink: 0; + margin-top: 2px; +} + +.file-card-info { + flex: 1; + min-width: 0; +} + +.file-card-name { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 4px 0; + word-break: break-word; + line-height: 1.3; +} + +.file-card-path { + font-size: 11px; + color: var(--text-secondary); + margin: 0 0 8px 0; + word-break: break-all; + opacity: 0.8; +} + +.file-card-metadata { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; +} + +.metadata-item { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; + color: var(--text-secondary); + background-color: var(--bg-tertiary); + padding: 2px 6px; + border-radius: 4px; +} + +.metadata-item .icon { + font-size: 10px; +} + +.file-card-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.card-btn { + padding: 6px 12px; + font-size: 11px; + border-radius: 6px; + border: 1px solid var(--border-primary); + background-color: var(--bg-secondary); + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 4px; +} + +.card-btn:hover { + background-color: var(--bg-tertiary); + border-color: var(--link-color); + transform: translateY(-1px); +} + +.card-btn-primary { + background-color: var(--link-color); + color: white; + border-color: var(--link-color); +} + +.card-btn-primary:hover { + background-color: var(--link-hover-color); + border-color: var(--link-hover-color); +} + +.card-btn-danger { + background-color: transparent; + color: var(--danger-color); + border-color: var(--danger-color); +} + +.card-btn-danger:hover { + background-color: var(--danger-color); + color: white; +} + +.empty-selection-cards { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + color: var(--text-muted); + text-align: center; + animation: fadeIn 0.5s ease-in-out; + grid-column: 1 / -1; +} + +.empty-selection-cards i { + font-size: 32px; + margin-bottom: 12px; + opacity: 0.6; + animation: pulse 2s infinite; +} + +.empty-selection-cards p { + margin: 4px 0; + font-size: 14px; + font-weight: 500; +} + +.empty-selection-cards small { + font-size: 12px; + opacity: 0.7; +} + +/* File type specific icons and colors */ +.file-card[data-type="directory"] .file-card-icon { + color: #ffa500; +} + +.file-card[data-type="file"] .file-card-icon { + color: var(--text-secondary); +} + +.file-card[data-extension="js"] .file-card-icon, +.file-card[data-extension="ts"] .file-card-icon { + color: #f7df1e; +} + +.file-card[data-extension="css"] .file-card-icon { + color: #1572b6; +} + +.file-card[data-extension="html"] .file-card-icon { + color: #e34f26; +} + +.file-card[data-extension="json"] .file-card-icon { + color: #000000; +} + +.file-card[data-extension="md"] .file-card-icon { + color: #083fa1; +} + +.file-card[data-extension="v"] .file-card-icon { + color: #5d87bf; +} + +/* Prompt Workspace */ +.prompt-workspace { + height: 100%; + display: grid; + grid-template-rows: auto 1fr; + gap: 16px; + padding: 16px; + overflow: hidden; +} + +.prompt-editor, +.prompt-output { + display: flex; + flex-direction: column; + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 6px; + overflow: hidden; + min-height: 0; +} + +.prompt-editor { + max-height: 300px; +} + +.prompt-output { + flex: 1; + min-height: 200px; +} + +.editor-header, +.output-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background-color: var(--bg-tertiary); + border-bottom: 1px solid var(--border-primary); +} + +.editor-content, +.output-content { + flex: 1; + padding: 12px; + overflow: hidden; + min-height: 0; +} + +.modern-textarea { + width: 100%; + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + font-size: 13px; + line-height: 1.5; + padding: 12px; + border-radius: 4px; + resize: vertical; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.modern-textarea:focus { + border-color: var(--link-color); + box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2); + outline: none; +} + +.editor-footer { + padding: 12px; + background-color: var(--bg-tertiary); + border-top: 1px solid var(--border-primary); + display: flex; + gap: 8px; +} + +.prompt-result { + background-color: var(--bg-primary); + color: var(--text-primary); + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 11px; + line-height: 1.4; + white-space: pre-wrap; + padding: 12px; + overflow-y: auto; + height: 100%; + max-height: 100%; + border: 1px solid var(--border-primary); + border-radius: 4px; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.empty-output { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100px; + color: var(--text-muted); + text-align: center; +} + +.empty-output i { + font-size: 20px; + margin-bottom: 8px; + opacity: 0.5; +} + +.empty-output p { + margin: 0; + font-size: 13px; +} + +/* HeroPrompt-Style Chat Workspace */ +.chat-workspace { + height: 100%; + display: flex; + flex-direction: column; + background-color: var(--bg-primary); +} + +.chat-layout { + display: flex; + height: 100%; + overflow: hidden; +} + +/* Chat Sidebar */ +.chat-sidebar { + width: 280px; + background-color: var(--bg-secondary); + border-right: 1px solid var(--border-primary); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-sidebar-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border-primary); + background-color: var(--bg-tertiary); +} + +.sidebar-title { + font-size: 12px; + font-weight: 600; + color: var(--text-primary); + margin: 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.chat-list { + flex: 1; + overflow-y: auto; + padding: 8px; +} + +.chat-conversations { + display: flex; + flex-direction: column; + gap: 4px; +} + +.empty-chat-list { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + color: var(--text-muted); + text-align: center; + padding: 20px; +} + +.empty-chat-list i { + font-size: 24px; + margin-bottom: 8px; + opacity: 0.6; +} + +.empty-chat-list p { + margin: 4px 0; + font-size: 13px; + font-weight: 500; +} + +.empty-chat-list small { + font-size: 11px; + opacity: 0.7; +} + +.chat-conversation-item { + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid transparent; + background-color: var(--bg-primary); +} + +.chat-conversation-item:hover { + background-color: var(--menu-item-hover-bg); + border-color: var(--border-primary); +} + +.chat-conversation-item.active { + background-color: var(--link-color); + color: white; + border-color: var(--link-color); +} + +.conversation-title { + font-size: 13px; + font-weight: 500; + margin: 0 0 4px 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.conversation-preview { + font-size: 11px; + opacity: 0.7; + margin: 0 0 4px 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.conversation-meta { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 10px; + opacity: 0.6; +} + +.conversation-time { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; +} + +.conversation-actions { + display: flex; + gap: 2px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.chat-conversation-item:hover .conversation-actions { + opacity: 1; +} + +.conversation-action { + background: none; + border: none; + color: var(--text-secondary); + padding: 2px; + border-radius: 2px; + cursor: pointer; + font-size: 10px; + transition: all 0.2s ease; +} + +.conversation-action:hover { + background-color: var(--menu-item-hover-bg); + color: var(--text-primary); +} + +.chat-conversation-item.active .conversation-action { + color: rgba(255, 255, 255, 0.8); +} + +.chat-conversation-item.active .conversation-action:hover { + background-color: rgba(255, 255, 255, 0.1); + color: white; +} + +/* Chat Main Area */ +.chat-main { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border-primary); + background-color: var(--bg-tertiary); +} + +.chat-header-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.chat-status { + font-size: 11px; + color: var(--text-secondary); + font-weight: 500; + opacity: 0.8; +} + +.chat-status.typing { + color: var(--link-color); +} + +.chat-status.error { + color: var(--danger-color); +} + +.chat-actions { + display: flex; + gap: 4px; +} + +.chat-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-messages { + flex: 1; + overflow: hidden; + background-color: var(--bg-primary); +} + +.messages-container { + height: 100%; + overflow-y: auto; + padding: 16px; + scroll-behavior: smooth; +} + +.messages-container::-webkit-scrollbar { + width: 8px; +} + +.messages-container::-webkit-scrollbar-track { + background: var(--bg-secondary); + border-radius: 4px; +} + +.messages-container::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 4px; +} + +.messages-container::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +/* Welcome Message - HeroPrompt Style */ +.welcome-message { + display: flex; + gap: 12px; + max-width: 100%; + margin: 20px 0; + padding: 16px; + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + border-radius: 6px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.welcome-avatar { + flex-shrink: 0; + width: 32px; + height: 32px; + background-color: var(--link-color); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 14px; +} + +.welcome-content h4 { + margin: 0 0 8px 0; + color: var(--text-primary); + font-size: 14px; + font-weight: 600; +} + +.welcome-content p { + margin: 0 0 8px 0; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.4; +} + +.welcome-content ul { + margin: 0 0 12px 0; + padding-left: 16px; + color: var(--text-secondary); + font-size: 12px; + line-height: 1.5; +} + +.welcome-content li { + margin-bottom: 2px; +} + +.welcome-content small { + color: var(--text-muted); + font-size: 11px; + opacity: 0.8; +} + +/* Chat Messages - HeroPrompt Style */ +.chat-message { + display: flex; + gap: 8px; + margin-bottom: 16px; + max-width: 100%; + animation: fadeIn 0.2s ease-out; +} + +.chat-message.user { + flex-direction: row-reverse; +} + +.chat-message.user .message-content { + background-color: var(--link-color); + color: white; + margin-left: 40px; +} + +.chat-message.assistant .message-content { + background-color: var(--bg-secondary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + margin-right: 40px; +} + +.message-avatar { + flex-shrink: 0; + width: 24px; + height: 24px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; +} + +.message-avatar.user { + background-color: var(--link-color); + color: white; +} + +.message-avatar.assistant { + background-color: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-primary); +} + +.message-content { + flex: 1; + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; + line-height: 1.5; + word-wrap: break-word; + position: relative; +} + +.message-text { + margin: 0; + color: inherit; +} + +.message-text pre { + background-color: var(--bg-primary); + border: 1px solid var(--border-primary); + padding: 8px; + border-radius: 4px; + margin: 6px 0; + overflow-x: auto; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 11px; + line-height: 1.4; +} + +.message-text code { + background-color: var(--bg-tertiary); + padding: 1px 3px; + border-radius: 2px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 11px; + border: 1px solid var(--border-primary); +} + +.chat-message.user .message-text pre { + background-color: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); +} + +.chat-message.user .message-text code { + background-color: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.3); +} + +.message-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 6px; + font-size: 10px; + color: var(--text-muted); +} + +.message-time { + opacity: 0.7; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; +} + +.message-actions { + display: flex; + gap: 2px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.chat-message:hover .message-actions { + opacity: 1; +} + +.message-action { + background: none; + border: none; + color: var(--text-secondary); + padding: 2px 4px; + border-radius: 2px; + cursor: pointer; + font-size: 10px; + transition: all 0.2s ease; +} + +.message-action:hover { + background-color: var(--menu-item-hover-bg); + color: var(--text-primary); +} + +/* Typing Indicator - HeroPrompt Style */ +.typing-indicator { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background-color: var(--bg-secondary); + border-top: 1px solid var(--border-primary); + font-size: 11px; + color: var(--text-secondary); +} + +.typing-dots { + display: flex; + gap: 2px; +} + +.typing-dots span { + width: 4px; + height: 4px; + background-color: var(--link-color); + border-radius: 50%; + animation: typingDots 1.4s infinite ease-in-out; +} + +.typing-dots span:nth-child(1) { + animation-delay: -0.32s; +} + +.typing-dots span:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes typingDots { + + 0%, + 80%, + 100% { + transform: scale(0.8); + opacity: 0.5; + } + + 40% { + transform: scale(1); + opacity: 1; + } +} + +@keyframes messageSlideIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Chat Input - HeroPrompt Style */ +.chat-input-container { + background-color: var(--bg-primary); + border-top: 1px solid var(--border-primary); +} + +.chat-input { + padding: 12px 16px; + background-color: var(--bg-primary); +} + +.input-container { + display: flex; + gap: 8px; + align-items: flex-end; + background-color: var(--bg-secondary); + border: 2px solid var(--border-primary); + border-radius: 6px; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + padding: 4px; +} + +.input-container:focus-within { + border-color: var(--link-color); + box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15), 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.input-wrapper { + flex: 1; + position: relative; + background: transparent; + border: none; +} + +.chat-textarea { + width: 100%; + background: transparent; + border: none; + color: var(--text-primary); + font-size: 13px; + line-height: 1.4; + padding: 8px 12px; + resize: none; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + max-height: 120px; + min-height: 36px; + outline: none; +} + +.chat-textarea::placeholder { + color: var(--text-muted); +} + +.input-actions { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + display: flex; + gap: 2px; +} + +.btn-icon { + background: none; + border: none; + color: var(--text-secondary); + padding: 4px; + border-radius: 3px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s ease; +} + +.btn-icon:hover { + background-color: var(--menu-item-hover-bg); + color: var(--text-primary); +} + +.send-btn { + width: 36px; + height: 36px; + background-color: var(--link-color); + border: none; + border-radius: 4px; + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + transition: all 0.2s ease; +} + +.send-btn:hover:not(:disabled) { + background-color: var(--link-hover-color); + transform: translateY(-1px); +} + +.send-btn:disabled { + background-color: var(--bg-tertiary); + color: var(--text-muted); + cursor: not-allowed; +} + +.input-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 6px; + font-size: 10px; + color: var(--text-muted); +} + +.input-hint { + opacity: 0.7; +} + +.char-count { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; +} + +.char-count.warning { + color: var(--warning-color); +} + +.char-count.error { + color: var(--danger-color); +} + +/* Button styles */ +.btn { + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + padding: 6px 12px; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.btn-primary { + background-color: var(--link-color); + color: white; +} + +.btn-primary:hover { + background-color: var(--link-hover-color); +} + +.btn-secondary { + background-color: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-primary); +} + +.btn-secondary:hover { + background-color: var(--menu-item-hover-bg); +} + +.btn-sm { + font-size: 12px; + padding: 4px 8px; +} + +/* Loading and status states */ +.loading { + opacity: 0.6; + pointer-events: none; + position: relative; +} + +.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + border: 2px solid var(--border-primary); + border-top-color: var(--link-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.success-message { + color: var(--success-color); + background-color: rgba(25, 135, 84, 0.1); + border: 1px solid var(--success-color); + border-radius: 4px; + padding: 8px 12px; + font-size: 12px; +} + +.error-message { + color: var(--danger-color); + background-color: rgba(220, 53, 69, 0.1); + border: 1px solid var(--danger-color); + border-radius: 4px; + padding: 8px 12px; + font-size: 12px; +} + +/* Responsive design */ +@media (max-width: 768px) { + .selection-content { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } + + .prompt-workspace { + grid-template-rows: 1fr 1fr; + } + + .tab-navigation { + flex-direction: column; + gap: 8px; + align-items: stretch; + } + + .workspace-actions { + justify-content: center; + } +} + +/* High contrast mode */ +@media (prefers-contrast: high) { + + .tree-item-content:hover, + .btn-ghost:hover { + background-color: var(--text-primary); + color: var(--bg-primary); + } + + .btn-primary, + .btn-secondary { + border-width: 2px; + } +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + + .tree-expand-btn, + .btn, + .modern-tabs .nav-link, + .tree-item-content { + transition: none; + } + + .loading::after { + animation: none; + } +} + +/* Responsive design for card layout */ +@media (max-width: 768px) { + .selection-cards { + grid-template-columns: 1fr; + padding: 12px; + gap: 12px; + } + + .file-card { + padding: 12px; + } + + .file-card-header { + gap: 8px; + } + + .file-card-icon { + font-size: 20px; + } + + .file-card-name { + font-size: 13px; + } + + .file-card-metadata { + gap: 6px; + } + + .metadata-item { + font-size: 10px; + padding: 1px 4px; + } + + .file-card-actions { + gap: 6px; + } + + .card-btn { + padding: 4px 8px; + font-size: 10px; + } + + .prompt-workspace { + grid-template-rows: 1fr 1fr; + } + + .tab-navigation { + flex-direction: column; + gap: 8px; + align-items: stretch; + } + + .workspace-actions { + justify-content: center; + } +} + +@media (max-width: 480px) { + .selection-cards { + padding: 8px; + gap: 8px; + } + + .file-card { + padding: 8px; + } + + .file-card-metadata { + flex-direction: column; + gap: 4px; + } + + .file-card-actions { + flex-direction: column; + gap: 4px; + } + + .card-btn { + width: 100%; + justify-content: center; + } +} + +/* Enhanced File Preview Modal - VS Code Style */ +.file-preview-modal .modal-dialog { + max-width: 90vw; + width: 90vw; + height: 90vh; + margin: 5vh auto; +} + +.file-preview-modal .modal-content { + height: 100%; + display: flex; + flex-direction: column; +} + +.modal-content-dark { + background-color: var(--bg-primary) !important; + border: 1px solid var(--border-primary); + color: var(--text-primary); +} + +.modal-header-dark { + background-color: var(--bg-secondary) !important; + border-bottom: 1px solid var(--border-primary) !important; + color: var(--text-primary) !important; + padding: 12px 20px; +} + +.modal-title-container { + display: flex; + flex-direction: column; + gap: 4px; +} + +.modal-title-container .modal-title { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin: 0; + display: flex; + align-items: center; + gap: 8px; +} + +.modal-subtitle { + font-size: 12px; + color: var(--text-secondary); + opacity: 0.8; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; +} + +.btn-close-dark { + filter: invert(1); + opacity: 0.8; +} + +.btn-close-dark:hover { + opacity: 1; +} + +.modal-body-dark { + background-color: var(--bg-primary); + padding: 0; + flex: 1; + overflow: hidden; +} + +.modal-footer-dark { + background-color: var(--bg-secondary) !important; + border-top: 1px solid var(--border-primary) !important; + color: var(--text-primary) !important; + padding: 12px 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.file-info { + display: flex; + align-items: center; + gap: 16px; +} + +.file-stats { + font-size: 12px; + color: var(--text-secondary); + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; +} + +.modal-actions { + display: flex; + gap: 8px; +} + +.btn-dark { + background-color: var(--bg-tertiary); + border-color: var(--border-primary); + color: var(--text-primary); +} + +.btn-dark:hover { + background-color: var(--menu-item-hover-bg); + border-color: var(--border-secondary); + color: var(--text-primary); +} + +/* VS Code-like Code Editor */ +.code-preview-container { + height: 100%; + overflow: hidden; + background-color: var(--bg-primary); +} + +.code-editor { + display: flex; + height: 100%; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 13px; + line-height: 1.5; + background-color: var(--bg-primary); + position: relative; +} + +/* Synchronized scrolling container */ +.code-scroll-container { + display: flex; + height: 100%; + overflow: hidden; + background-color: var(--bg-primary); +} + +.line-numbers { + background-color: var(--bg-secondary); + border-right: 1px solid var(--border-primary); + padding: 16px 8px; + min-width: 60px; + text-align: right; + user-select: none; + color: var(--text-secondary); + font-size: 12px; + overflow: hidden; + flex-shrink: 0; + position: sticky; + top: 0; + height: fit-content; +} + +.line-number { + height: 19.5px; + /* Match line-height of code */ + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 8px; + transition: all 0.2s ease; +} + +.line-number:hover { + background-color: var(--menu-item-hover-bg); + color: var(--text-primary); +} + +.code-content { + flex: 1; + overflow: auto; + background-color: var(--bg-primary); + position: relative; +} + +.line-numbers-container { + overflow: hidden; + background-color: var(--bg-secondary); + border-right: 1px solid var(--border-primary); + min-width: 60px; + flex-shrink: 0; +} + +.line-numbers-scroll { + padding: 16px 8px; + text-align: right; + user-select: none; + color: var(--text-secondary); + font-size: 12px; + background-color: var(--bg-secondary); +} + +.code-content-container { + flex: 1; + overflow: auto; + background-color: var(--bg-primary); +} + +.code-text { + margin: 0; + padding: 16px 20px; + background-color: var(--bg-primary) !important; + color: var(--text-primary); + white-space: pre; + word-wrap: normal; + overflow-wrap: normal; + tab-size: 4; + -moz-tab-size: 4; + border: none; + outline: none; +} + +.code-text code { + background-color: transparent !important; + color: inherit; + padding: 0; + font-size: inherit; + font-family: inherit; + line-height: inherit; +} + +.code-content::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +.code-content::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +.code-content::-webkit-scrollbar-thumb { + background: var(--border-primary); + border-radius: 6px; + border: 2px solid var(--bg-secondary); +} + +.code-content::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +.code-content::-webkit-scrollbar-corner { + background: var(--bg-secondary); +} + +.code-text { + margin: 0; + padding: 16px 20px; + background: transparent; + color: var(--text-primary); + white-space: pre; + word-wrap: normal; + overflow-wrap: normal; + tab-size: 4; + -moz-tab-size: 4; +} + +.code-text code { + background: transparent; + color: inherit; + padding: 0; + font-size: inherit; + font-family: inherit; + line-height: inherit; +} + +/* Syntax highlighting hints (basic) */ +.code-text[data-language="js"] code, +.code-text[data-language="ts"] code { + color: #d4d4d4; +} + +.code-text[data-language="html"] code { + color: #d4d4d4; +} + +.code-text[data-language="css"] code { + color: #d4d4d4; +} + +.code-text[data-language="json"] code { + color: #d4d4d4; +} + +.code-text[data-language="md"] code { + color: #d4d4d4; +} + +.code-text[data-language="v"] code { + color: #d4d4d4; +} + +/* Loading state for modal */ +.code-preview-container .loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + font-size: 14px; + color: var(--text-secondary); +} + +/* Error state for modal */ +.code-preview-container .error-message { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--danger-color); + background-color: rgba(220, 53, 69, 0.1); + border: 1px solid var(--danger-color); + border-radius: 4px; + margin: 16px; + padding: 20px; + text-align: center; +} + +/* Responsive modal */ +@media (max-width: 768px) { + .file-preview-modal .modal-dialog { + max-width: 95vw; + width: 95vw; + height: 95vh; + margin: 2.5vh auto; + } + + .modal-header-dark { + padding: 8px 12px; + } + + .modal-footer-dark { + padding: 8px 12px; + flex-direction: column; + gap: 8px; + align-items: stretch; + } + + .modal-actions { + justify-content: center; + } + + .line-numbers { + min-width: 50px; + padding: 12px 6px; + } + + .code-text { + padding: 12px 16px; + font-size: 12px; + } + + .modal-title-container .modal-title { + font-size: 14px; + } + + .modal-subtitle { + font-size: 11px; + } +} + +@media (max-width: 480px) { + .line-numbers { + min-width: 40px; + padding: 8px 4px; + font-size: 11px; + } + + .code-text { + padding: 8px 12px; + font-size: 11px; + } + + .file-stats { + font-size: 11px; + } +} + +/* Modal Enhancements for Dark Theme */ +.modal-content { + border: none; + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + background-color: var(--bs-body-bg); + color: var(--bs-body-color); +} + +.modal-header { + border-bottom: 1px solid var(--bs-border-color) !important; + padding: 1.5rem; + background-color: var(--bs-body-bg) !important; + color: var(--bs-body-color) !important; +} + +.modal-body { + padding: 1.5rem; + background-color: var(--bs-body-bg); + color: var(--bs-body-color); +} + +.modal-footer { + border-top: 1px solid var(--bs-border-color) !important; + padding: 1rem 1.5rem; + background-color: var(--bs-body-bg) !important; + color: var(--bs-body-color) !important; +} + +/* Ensure modal backdrop is properly styled */ +.modal-backdrop { + background-color: rgba(0, 0, 0, 0.5) !important; +} + +/* Additional modal fixes */ +.modal { + --bs-modal-bg: var(--bs-body-bg); + --bs-modal-color: var(--bs-body-color); +} + +.modal-dialog { + --bs-modal-bg: var(--bs-body-bg); +} + +/* Workspace Management Modal */ +#workspaceManageModal .modal-content, +#workspaceDetailsModal .modal-content, +#confirmDeleteModal .modal-content { + background-color: var(--bs-body-bg); + color: var(--bs-body-color); +} + +#workspaceManageModal .form-control, +#workspaceDetailsModal .form-control { + background-color: var(--bs-body-bg); + border-color: var(--bs-border-color); + color: var(--bs-body-color); +} + +#workspaceManageModal .form-control:focus, +#workspaceDetailsModal .form-control:focus { + background-color: var(--bs-body-bg); + border-color: var(--bs-primary); + color: var(--bs-body-color); + box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25); +} + +#workspaceManageModal .btn, +#workspaceDetailsModal .btn, +#confirmDeleteModal .btn { + color: var(--bs-body-color); +} + +#workspaceManageModal .btn-primary, +#workspaceDetailsModal .btn-primary, +#confirmDeleteModal .btn-primary { + background-color: var(--bs-primary); + border-color: var(--bs-primary); + color: white; +} + +#workspaceManageModal .btn-danger, +#workspaceDetailsModal .btn-danger, +#confirmDeleteModal .btn-danger { + background-color: var(--bs-danger); + border-color: var(--bs-danger); + color: white; +} + +#workspaceManageModal .list-group-item, +#workspaceDetailsModal .list-group-item { + background-color: var(--bs-body-bg); + border-color: var(--bs-border-color); + color: var(--bs-body-color); +} + +#workspaceManageModal .list-group-item:hover, +#workspaceDetailsModal .list-group-item:hover { + background-color: var(--bs-secondary-bg); +} + +#workspaceManageModal .list-group-item.active, +#workspaceDetailsModal .list-group-item.active { + background-color: var(--bs-primary); + border-color: var(--bs-primary); + color: white; +} + +/* Mobile Responsive Design for Chat */ +@media (max-width: 768px) { + .chat-workspace { + padding: 0; + } + + .chat-layout { + flex-direction: column; + } + + .chat-sidebar { + width: 100%; + height: 200px; + border-right: none; + border-bottom: 1px solid var(--border-primary); + } + + .chat-sidebar-header { + padding: 8px 12px; + } + + .sidebar-title { + font-size: 11px; + } + + .chat-list { + padding: 4px; + } + + .chat-conversation-item { + padding: 6px 8px; + } + + .conversation-title { + font-size: 12px; + } + + .conversation-preview { + font-size: 10px; + } + + .conversation-meta { + font-size: 9px; + } + + .chat-main { + flex: 1; + } + + .chat-header { + padding: 12px 16px; + flex-direction: column; + gap: 8px; + align-items: stretch; + } + + .chat-header-info { + text-align: center; + } + + .chat-actions { + justify-content: center; + } + + .messages-container { + padding: 16px 12px; + } + + .chat-message { + gap: 8px; + margin-bottom: 16px; + } + + .chat-message.user .message-content { + margin-left: 40px; + } + + .chat-message.assistant .message-content { + margin-right: 40px; + } + + .message-avatar { + width: 28px; + height: 28px; + font-size: 12px; + } + + .message-content { + padding: 10px 12px; + font-size: 13px; + } + + .message-meta { + margin-top: 6px; + } + + .message-time { + font-size: 10px; + } + + .message-action { + padding: 3px; + font-size: 11px; + } + + .welcome-message { + margin: 20px auto; + padding: 16px; + gap: 12px; + } + + .welcome-avatar { + width: 40px; + height: 40px; + font-size: 16px; + } + + .welcome-content h4 { + font-size: 16px; + } + + .welcome-content p { + font-size: 13px; + } + + .welcome-content ul { + font-size: 12px; + } + + .chat-input { + padding: 12px 16px; + } + + .input-container { + gap: 8px; + } + + .chat-textarea { + padding: 10px 12px; + font-size: 13px; + } + + .send-btn { + width: 36px; + height: 36px; + border-radius: 8px; + font-size: 14px; + } + + .input-footer { + margin-top: 6px; + font-size: 10px; + } + + .typing-indicator { + padding: 8px 16px; + font-size: 12px; + } + + .typing-dots span { + width: 5px; + height: 5px; + } +} + +@media (max-width: 480px) { + .chat-sidebar { + height: 150px; + } + + .chat-sidebar-header { + padding: 6px 8px; + } + + .sidebar-title { + font-size: 10px; + } + + .chat-conversation-item { + padding: 4px 6px; + } + + .conversation-title { + font-size: 11px; + } + + .conversation-preview { + font-size: 9px; + } + + .empty-chat-list { + height: 100px; + padding: 10px; + } + + .empty-chat-list i { + font-size: 18px; + margin-bottom: 4px; + } + + .empty-chat-list p { + font-size: 11px; + } + + .empty-chat-list small { + font-size: 9px; + } +} + +@media (max-width: 480px) { + .chat-header { + padding: 8px 12px; + } + + .section-title { + font-size: 13px; + } + + .chat-status { + font-size: 11px; + } + + .messages-container { + padding: 12px 8px; + } + + .chat-message { + gap: 6px; + margin-bottom: 12px; + } + + .chat-message.user .message-content { + margin-left: 30px; + } + + .chat-message.assistant .message-content { + margin-right: 30px; + } + + .message-avatar { + width: 24px; + height: 24px; + font-size: 10px; + } + + .message-content { + padding: 8px 10px; + font-size: 12px; + border-radius: 8px; + } + + .welcome-message { + margin: 16px auto; + padding: 12px; + gap: 8px; + border-radius: 12px; + } + + .welcome-avatar { + width: 32px; + height: 32px; + font-size: 14px; + } + + .welcome-content h4 { + font-size: 14px; + margin-bottom: 8px; + } + + .welcome-content p { + font-size: 12px; + margin-bottom: 8px; + } + + .welcome-content ul { + font-size: 11px; + margin-bottom: 12px; + } + + .welcome-content small { + font-size: 10px; + } + + .chat-input { + padding: 8px 12px; + } + + .input-container { + gap: 6px; + } + + .chat-textarea { + padding: 8px 10px; + font-size: 12px; + min-height: 36px; + } + + .send-btn { + width: 32px; + height: 32px; + font-size: 12px; + } + + .input-footer { + margin-top: 4px; + font-size: 9px; + } + + .btn-icon { + padding: 4px; + font-size: 12px; + } +} + +/* Dark mode enhancements for chat */ +@media (prefers-color-scheme: dark) { + .chat-message.user .message-content { + background: linear-gradient(135deg, #0066cc 0%, #3d4ed8 100%); + } + + .message-text pre { + background-color: rgba(255, 255, 255, 0.1); + } + + .message-text code { + background-color: rgba(255, 255, 255, 0.1); + } +} + +/* High contrast mode for chat */ +@media (prefers-contrast: high) { + .chat-message.user .message-content { + background: #000; + color: #fff; + border: 2px solid #fff; + } + + .chat-message.assistant .message-content { + background: #fff; + color: #000; + border: 2px solid #000; + } + + .message-avatar { + border: 2px solid currentColor; + } + + .send-btn { + border: 2px solid #fff; + } +} + +/* Reduced motion for chat animations */ +@media (prefers-reduced-motion: reduce) { + .chat-message { + animation: none; + } + + .typing-dots span { + animation: none; + } + + .message-content, + .send-btn, + .input-wrapper { + transition: none; + } +} \ No newline at end of file diff --git a/lib/web/ui/templates/css/heroscript.css b/lib/web/ui/static/css/heroscript.css similarity index 100% rename from lib/web/ui/templates/css/heroscript.css rename to lib/web/ui/static/css/heroscript.css diff --git a/lib/web/ui/templates/css/main.css b/lib/web/ui/static/css/main.css similarity index 99% rename from lib/web/ui/templates/css/main.css rename to lib/web/ui/static/css/main.css index aa6f03e6..634a2a82 100644 --- a/lib/web/ui/templates/css/main.css +++ b/lib/web/ui/static/css/main.css @@ -192,15 +192,15 @@ code { transform: translateX(-100%); transition: transform 0.3s ease; } - + .sidebar.show { transform: translateX(0); } - + .main { margin-left: 0; } - + .mobile-menu-toggle { display: block; position: fixed; @@ -262,17 +262,18 @@ code { /* Print styles */ @media print { + .sidebar, .header, .theme-toggle { display: none !important; } - + .main { margin-left: 0 !important; padding: 0 !important; } - + body { padding-top: 0 !important; } diff --git a/lib/web/ui/templates/js/chat.js b/lib/web/ui/static/js/chat.js similarity index 100% rename from lib/web/ui/templates/js/chat.js rename to lib/web/ui/static/js/chat.js diff --git a/lib/web/ui/static/js/heroprompt.js b/lib/web/ui/static/js/heroprompt.js new file mode 100644 index 00000000..be44e253 --- /dev/null +++ b/lib/web/ui/static/js/heroprompt.js @@ -0,0 +1,1970 @@ +console.log('Enhanced HeroPrompt UI loaded'); + +// Global state +let currentWs = localStorage.getItem('heroprompt-current-ws') || 'default'; +let selected = new Set(); +let expandedDirs = new Set(); +let searchQuery = ''; + +// Utility functions +const el = (id) => document.getElementById(id); +const qs = (selector) => document.querySelector(selector); +const qsa = (selector) => document.querySelectorAll(selector); + +// File extension detection utility +const getFileExtension = (filename) => { + const parts = filename.split('.'); + return parts.length > 1 ? parts.pop().toLowerCase() : ''; +}; + +// File size formatting utility +const formatFileSize = (bytes) => { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +}; + +// Date formatting utility +const formatDate = (date) => { + const now = new Date(); + const diffTime = Math.abs(now - date); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + + if (diffDays === 1) return 'Yesterday'; + if (diffDays < 7) return `${diffDays} days ago`; + if (diffDays < 30) return `${Math.ceil(diffDays / 7)} weeks ago`; + return date.toLocaleDateString(); +}; + +// File icon mapping utility +const getFileIcon = (extension) => { + const iconMap = { + 'js': '📜', 'ts': '📜', 'html': '🌐', 'css': '🎨', 'json': '📋', + 'md': '📝', 'txt': '📄', 'v': '⚡', 'go': '🐹', 'py': '🐍', + 'java': '☕', 'cpp': '⚙️', 'c': '⚙️', 'rs': '🦀', 'php': '🐘', + 'rb': '💎', 'sh': '🐚', 'yml': '📄', 'yaml': '📄', 'xml': '📄', + 'svg': '🖼️', 'png': '🖼️', 'jpg': '🖼️', 'jpeg': '🖼️', 'gif': '🖼️', + 'pdf': '📕', 'zip': '📦', 'tar': '📦', 'gz': '📦' + }; + return iconMap[extension] || '📄'; +}; + +// API helpers +async function api(url) { + try { + const r = await fetch(url); + if (!r.ok) { + console.warn(`API call failed: ${url} - ${r.status}`); + return { error: `HTTP ${r.status}` }; + } + return await r.json(); + } catch (e) { + console.warn(`API call error: ${url}`, e); + return { error: 'request failed' }; + } +} + +async function post(url, data) { + const form = new FormData(); + Object.entries(data).forEach(([k, v]) => form.append(k, v)); + try { + const r = await fetch(url, { method: 'POST', body: form }); + if (!r.ok) { + console.warn(`POST failed: ${url} - ${r.status}`); + return { error: `HTTP ${r.status}` }; + } + return await r.json(); + } catch (e) { + console.warn(`POST error: ${url}`, e); + return { error: 'request failed' }; + } +} + +// Modal helpers +function showModal(id) { + const modalEl = el(id); + if (modalEl) { + const modal = new bootstrap.Modal(modalEl); + modal.show(); + } +} + +function hideModal(id) { + const modalEl = el(id); + if (modalEl) { + const modal = bootstrap.Modal.getInstance(modalEl); + if (modal) modal.hide(); + } +} + +// Tab management +function switchTab(tabName) { + // Update tab buttons + qsa('.tab').forEach(tab => { + tab.classList.remove('active'); + if (tab.getAttribute('data-tab') === tabName) { + tab.classList.add('active'); + } + }); + + // Update tab panes + qsa('.tab-pane').forEach(pane => { + pane.style.display = 'none'; + if (pane.id === `tab-${tabName}`) { + pane.style.display = 'block'; + } + }); +} + +// Enhanced file tree implementation with better spacing and reliability +class SimpleFileTree { + constructor(container) { + this.container = container; + this.loadedPaths = new Set(); + this.expandedDirs = new Set(); // Track expanded state locally + } + + createFileItem(item, path, depth = 0) { + const div = document.createElement('div'); + div.className = 'tree-item'; + div.dataset.path = path; + div.dataset.type = item.type; + div.dataset.depth = depth; + + const content = document.createElement('div'); + content.className = 'tree-item-content'; + // Use consistent 20px indentation per level + content.style.paddingLeft = `${depth * 20 + 8}px`; + + // Checkbox + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = 'tree-checkbox'; + checkbox.checked = selected.has(path); + checkbox.addEventListener('change', (e) => { + e.stopPropagation(); + if (checkbox.checked) { + selected.add(path); + } else { + selected.delete(path); + } + this.updateSelectionUI(); + }); + + // Expand/collapse button for directories + let expandBtn = null; + if (item.type === 'directory') { + expandBtn = document.createElement('button'); + expandBtn.className = 'tree-expand-btn'; + expandBtn.innerHTML = this.expandedDirs.has(path) ? '▼' : '▶'; + expandBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.toggleDirectory(path); + }); + } else { + // Spacer for files to align with directories + expandBtn = document.createElement('span'); + expandBtn.className = 'tree-expand-spacer'; + } + + // Icon + const icon = document.createElement('span'); + icon.className = 'tree-icon'; + icon.textContent = item.type === 'directory' ? + (this.expandedDirs.has(path) ? '📂' : '�') : '📄'; + + // Label + const label = document.createElement('span'); + label.className = 'tree-label'; + label.textContent = item.name; + label.addEventListener('click', (e) => { + e.stopPropagation(); + if (item.type === 'file') { + // Toggle file selection when clicking on file name + checkbox.checked = !checkbox.checked; + if (checkbox.checked) { + selected.add(path); + } else { + selected.delete(path); + } + this.updateSelectionUI(); + } else { + // Toggle directory expansion when clicking on directory name + this.toggleDirectory(path); + } + }); + + content.appendChild(checkbox); + content.appendChild(expandBtn); + content.appendChild(icon); + content.appendChild(label); + div.appendChild(content); + + return div; + } + + async toggleDirectory(dirPath) { + const isExpanded = this.expandedDirs.has(dirPath); + const dirElement = qs(`[data-path="${dirPath}"]`); + const expandBtn = dirElement?.querySelector('.tree-expand-btn'); + const icon = dirElement?.querySelector('.tree-icon'); + + if (isExpanded) { + // Collapse + this.expandedDirs.delete(dirPath); + if (expandBtn) expandBtn.innerHTML = '▶'; + if (icon) icon.textContent = '📁'; + this.removeChildren(dirPath); + // Remove from loaded paths so it can be reloaded when expanded again + this.loadedPaths.delete(dirPath); + } else { + // Expand + this.expandedDirs.add(dirPath); + if (expandBtn) expandBtn.innerHTML = '▼'; + if (icon) icon.textContent = '📂'; + await this.loadChildren(dirPath); + } + } + + removeChildren(parentPath) { + const items = qsa('.tree-item'); + const toRemove = []; + + items.forEach(item => { + const itemPath = item.dataset.path; + if (itemPath !== parentPath && itemPath.startsWith(parentPath + '/')) { + toRemove.push(item); + // Also remove from expanded dirs if it was expanded + this.expandedDirs.delete(itemPath); + this.loadedPaths.delete(itemPath); + } + }); + + // Remove elements with animation + toRemove.forEach(item => { + item.style.transition = 'opacity 0.2s ease, max-height 0.2s ease'; + item.style.opacity = '0'; + item.style.maxHeight = '0'; + setTimeout(() => item.remove(), 200); + }); + } + + async loadChildren(parentPath) { + // Always reload children to ensure fresh data + console.log('Loading children for:', parentPath); + const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(parentPath)}`); + + if (r.error) { + console.warn('Failed to load directory:', parentPath, r.error); + return; + } + + // Sort items: directories first, then files + const items = (r.items || []).sort((a, b) => { + if (a.type !== b.type) { + return a.type === 'directory' ? -1 : 1; + } + return a.name.localeCompare(b.name); + }); + + // Find the parent element + const parentElement = qs(`[data-path="${parentPath}"]`); + if (!parentElement) { + console.warn('Parent element not found for path:', parentPath); + return; + } + + const parentDepth = parseInt(parentElement.dataset.depth || '0'); + + // Create document fragment for efficient DOM manipulation + const fragment = document.createDocumentFragment(); + const childElements = []; + + // Create all child elements first + for (const item of items) { + const childPath = parentPath.endsWith('/') ? + parentPath + item.name : + parentPath + '/' + item.name; + + const childElement = this.createFileItem(item, childPath, parentDepth + 1); + + // Prepare for animation + childElement.style.opacity = '0'; + childElement.style.maxHeight = '0'; + childElement.style.transition = 'opacity 0.2s ease, max-height 0.2s ease'; + + fragment.appendChild(childElement); + childElements.push(childElement); + } + + // Insert all elements at once + parentElement.insertAdjacentElement('afterend', fragment.firstChild); + if (fragment.children.length > 1) { + let insertAfter = parentElement.nextElementSibling; + while (fragment.firstChild) { + insertAfter.insertAdjacentElement('afterend', fragment.firstChild); + insertAfter = insertAfter.nextElementSibling; + } + } + + // Trigger animations with staggered delay + childElements.forEach((element, index) => { + setTimeout(() => { + element.style.opacity = '1'; + element.style.maxHeight = '30px'; + }, index * 20 + 10); + }); + + this.loadedPaths.add(parentPath); + } + + getDepth(path) { + // Calculate depth based on forward slashes, but handle root paths better + if (!path || path === '/') return 0; + const cleanPath = path.replace(/^\/+|\/+$/g, ''); // Remove leading/trailing slashes + return cleanPath ? cleanPath.split('/').length - 1 : 0; + } + + async previewFile(filePath) { + const previewEl = el('preview'); + if (!previewEl) return; + + previewEl.innerHTML = '
      Loading...
      '; + + const r = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(filePath)}`); + + if (r.error) { + previewEl.innerHTML = `
      Error: ${r.error}
      `; + return; + } + + previewEl.textContent = r.content || 'No content'; + } + + updateSelectionUI() { + const selCountEl = el('selCount'); + const selCountTabEl = el('selCountTab'); + const tokenCountEl = el('tokenCount'); + const selectedCardsEl = el('selectedCards'); + + const count = selected.size; + + if (selCountEl) selCountEl.textContent = count.toString(); + if (selCountTabEl) selCountTabEl.textContent = count.toString(); + + // Update selection cards + if (selectedCardsEl) { + selectedCardsEl.innerHTML = ''; + + if (count === 0) { + selectedCardsEl.innerHTML = ` +
      + +

      No files selected

      + Use checkboxes in the explorer to select files and directories +
      + `; + } else { + Array.from(selected).forEach(path => { + const card = this.createFileCard(path); + selectedCardsEl.appendChild(card); + }); + } + } + + // Estimate token count (rough approximation) + const totalChars = Array.from(selected).join('\n').length; + const tokens = Math.ceil(totalChars / 4); + if (tokenCountEl) tokenCountEl.textContent = tokens.toString(); + } + + createFileCard(path) { + const card = document.createElement('div'); + card.className = 'file-card'; + + // Get file info + const fileName = path.split('/').pop(); + const extension = getFileExtension(fileName); + const isDirectory = this.isDirectory(path); + + card.dataset.type = isDirectory ? 'directory' : 'file'; + if (extension) { + card.dataset.extension = extension; + } + + // Get file stats (mock data for now - could be enhanced with real file stats) + const stats = this.getFileStats(path); + + card.innerHTML = ` +
      +
      + ${isDirectory ? '📁' : getFileIcon(extension)} +
      +
      +

      ${fileName}

      +

      ${path}

      +
      +
      +
      +
      + 📄 + ${isDirectory ? 'Directory' : 'File'} +
      + ${extension ? ` +
      + 🏷️ + ${extension.toUpperCase()} +
      + ` : ''} +
      + 📏 + ${stats.size} +
      +
      + 📅 + ${stats.modified} +
      +
      +
      + ${!isDirectory ? ` + + ` : ''} + +
      + `; + + return card; + } + + + isDirectory(path) { + // Check if path corresponds to a directory in the tree + const treeItem = qs(`[data-path="${path}"]`); + return treeItem && treeItem.dataset.type === 'directory'; + } + + getFileStats(path) { + // Mock file stats - in a real implementation, this would come from the API + return { + size: formatFileSize(Math.floor(Math.random() * 100000) + 1000), + modified: formatDate(new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000)) + }; + } + + async previewFileInModal(filePath) { + // Create and show modal for file preview + const modal = document.createElement('div'); + modal.className = 'modal fade file-preview-modal'; + modal.id = 'filePreviewModal'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + const bootstrapModal = new bootstrap.Modal(modal); + bootstrapModal.show(); + + // Load file content + const r = await api(`/api/heroprompt/file?name=${currentWs}&path=${encodeURIComponent(filePath)}`); + const contentEl = el('modalPreviewContent'); + + if (r.error) { + contentEl.innerHTML = `
      Error: ${r.error}
      `; + } else { + const content = r.content || 'No content'; + this.renderCodePreview(contentEl, content, filePath); + + // Update file stats + const statsEl = el('fileStats'); + if (statsEl) { + const lines = content.split('\n').length; + const chars = content.length; + const words = content.split(/\s+/).filter(w => w.length > 0).length; + statsEl.textContent = `${lines} lines, ${words} words, ${chars} characters`; + } + } + + // Clean up modal when closed + modal.addEventListener('hidden.bs.modal', () => { + modal.remove(); + }); + } + + renderCodePreview(container, content, filePath) { + const lines = content.split('\n'); + const extension = getFileExtension(filePath.split('/').pop()); + + // Create the code preview structure with synchronized scrolling + container.innerHTML = ` +
      +
      +
      + ${lines.map((_, index) => `
      ${index + 1}
      `).join('')} +
      +
      +
      +
      ${this.escapeHtml(content)}
      +
      +
      + `; + + // Set up synchronized scrolling + this.setupSynchronizedScrolling(container); + } + + setupSynchronizedScrolling(container) { + const lineNumbersContainer = container.querySelector('.line-numbers-container'); + const codeContentContainer = container.querySelector('.code-content-container'); + const lineNumbersScroll = container.querySelector('.line-numbers-scroll'); + + if (!lineNumbersContainer || !codeContentContainer || !lineNumbersScroll) { + return; + } + + // Synchronize scrolling between code content and line numbers + codeContentContainer.addEventListener('scroll', () => { + const scrollTop = codeContentContainer.scrollTop; + lineNumbersContainer.scrollTop = scrollTop; + }); + + // Optional: Allow scrolling from line numbers to affect code content + lineNumbersContainer.addEventListener('scroll', () => { + const scrollTop = lineNumbersContainer.scrollTop; + codeContentContainer.scrollTop = scrollTop; + }); + + // Ensure line numbers container can scroll + lineNumbersContainer.style.overflow = 'hidden'; + lineNumbersContainer.style.height = '100%'; + + // Make sure the line numbers scroll area matches the code content height + const codeText = container.querySelector('.code-text'); + if (codeText) { + const codeHeight = codeText.scrollHeight; + lineNumbersScroll.style.height = `${codeHeight}px`; + } + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + copyModalContent() { + const contentEl = el('modalPreviewContent'); + if (!contentEl) { + console.warn('Modal content element not found'); + return; + } + + const textContent = contentEl.textContent; + if (!textContent || textContent.trim().length === 0) { + console.warn('No content to copy'); + return; + } + + if (!navigator.clipboard) { + // Fallback for older browsers + this.fallbackCopyToClipboard(textContent); + return; + } + + navigator.clipboard.writeText(textContent).then(() => { + // Show success feedback + const originalContent = contentEl.innerHTML; + contentEl.innerHTML = '
      Content copied to clipboard!
      '; + setTimeout(() => { + contentEl.innerHTML = originalContent; + }, 2000); + }).catch(err => { + console.error('Failed to copy content:', err); + contentEl.innerHTML = '
      Failed to copy content
      '; + setTimeout(() => { + contentEl.innerHTML = originalContent; + }, 2000); + }); + } + + fallbackCopyToClipboard(text) { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + console.log('Fallback: Content copied to clipboard'); + } catch (err) { + console.error('Fallback: Failed to copy content', err); + } + + document.body.removeChild(textArea); + } + + removeFromSelection(path) { + selected.delete(path); + + // Update checkbox + const checkbox = qs(`[data-path="${path}"] .tree-checkbox`); + if (checkbox) { + checkbox.checked = false; + } + + this.updateSelectionUI(); + } + + selectAll() { + qsa('.tree-checkbox').forEach(checkbox => { + checkbox.checked = true; + const path = checkbox.closest('.tree-item').dataset.path; + selected.add(path); + }); + this.updateSelectionUI(); + } + + clearSelection() { + selected.clear(); + qsa('.tree-checkbox').forEach(checkbox => { + checkbox.checked = false; + }); + this.updateSelectionUI(); + } + + collapseAll() { + expandedDirs.clear(); + qsa('.tree-expand-btn').forEach(btn => { + btn.innerHTML = '▶'; + }); + // Remove all children except root level + qsa('.tree-item').forEach(item => { + const depth = parseInt(item.style.paddingLeft) / 16; + if (depth > 0) { + item.remove(); + } + }); + this.loadedPaths.clear(); + } + + async search(query) { + searchQuery = query.toLowerCase().trim(); + + if (!searchQuery) { + // Show all items when search is cleared + qsa('.tree-item').forEach(item => { + item.style.display = 'block'; + }); + return; + } + + try { + // Use the new search API to get all matching files across the workspace + const searchResults = await api(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/search?q=${encodeURIComponent(searchQuery)}`); + + if (searchResults.error) { + console.warn('Search failed:', searchResults.error); + // Fallback to local search + this.localSearch(query); + return; + } + + // Hide all current items + qsa('.tree-item').forEach(item => { + item.style.display = 'none'; + }); + + // Show matching items and expand their parent directories + const matchingPaths = new Set(); + searchResults.results.forEach(result => { + matchingPaths.add(result.path); + // Also add parent directory paths + const pathParts = result.path.split('/'); + for (let i = 1; i < pathParts.length; i++) { + const parentPath = pathParts.slice(0, i).join('/'); + if (parentPath) { + matchingPaths.add(parentPath); + } + } + }); + + // Show items that match or are parents of matches + // Get workspace info once + const workspaceInfo = await api(`/api/heroprompt/workspaces/${currentWs}`); + + qsa('.tree-item').forEach(item => { + const itemPath = item.dataset.path; + if (itemPath) { + // Get relative path from workspace base + let relPath = itemPath; + if (workspaceInfo && workspaceInfo.base_path && itemPath.startsWith(workspaceInfo.base_path)) { + relPath = itemPath.substring(workspaceInfo.base_path.length); + if (relPath.startsWith('/')) { + relPath = relPath.substring(1); + } + } + + if (matchingPaths.has(relPath) || relPath === '') { + item.style.display = 'block'; + // Auto-expand directories that contain matches + if (item.dataset.type === 'directory' && !this.expandedDirs.has(itemPath)) { + this.toggleDirectory(itemPath); + } + } + } + }); + + } catch (error) { + console.warn('Search API error:', error); + // Fallback to local search + this.localSearch(query); + } + } + + localSearch(query) { + const searchQuery = query.toLowerCase(); + qsa('.tree-item').forEach(item => { + const label = item.querySelector('.tree-label'); + if (label) { + const matches = !searchQuery || label.textContent.toLowerCase().includes(searchQuery); + item.style.display = matches ? 'block' : 'none'; + } + }); + } + + async render(workspacePath) { + this.container.innerHTML = '
      Loading workspace...
      '; + + const r = await api(`/api/heroprompt/directory?name=${currentWs}&path=${encodeURIComponent(workspacePath)}`); + + if (r.error) { + this.container.innerHTML = `
      ${r.error}
      `; + return; + } + + // Reset state + this.loadedPaths.clear(); + this.expandedDirs.clear(); + expandedDirs.clear(); + + // Sort items: directories first, then files + const items = (r.items || []).sort((a, b) => { + if (a.type !== b.type) { + return a.type === 'directory' ? -1 : 1; + } + return a.name.localeCompare(b.name); + }); + + // Create document fragment for efficient DOM manipulation + const fragment = document.createDocumentFragment(); + const elements = []; + + // Create all elements first + for (const item of items) { + const fullPath = workspacePath.endsWith('/') ? + workspacePath + item.name : + workspacePath + '/' + item.name; + + const element = this.createFileItem(item, fullPath, 0); + element.style.opacity = '0'; + element.style.transform = 'translateY(-10px)'; + element.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; + + fragment.appendChild(element); + elements.push(element); + } + + // Clear container and add all elements at once + this.container.innerHTML = ''; + this.container.appendChild(fragment); + + // Trigger staggered animations + elements.forEach((element, i) => { + setTimeout(() => { + element.style.opacity = '1'; + element.style.transform = 'translateY(0)'; + }, i * 50); + }); + + this.updateSelectionUI(); + } +} + +// Global tree instance +let fileTree = null; + +// Workspace management +async function reloadWorkspaces() { + const sel = el('workspaceSelect'); + if (!sel) return; + + sel.innerHTML = ''; + const names = await api('/api/heroprompt/workspaces'); + + sel.innerHTML = ''; + if (names.error || !Array.isArray(names)) { + sel.innerHTML = ''; + console.warn('Failed to load workspaces:', names); + return; + } + + for (const n of names) { + const opt = document.createElement('option'); + opt.value = n; + opt.textContent = n; + sel.appendChild(opt); + } + + if (names.includes(currentWs)) { + sel.value = currentWs; + } else if (names.length > 0) { + currentWs = names[0]; + sel.value = currentWs; + localStorage.setItem('heroprompt-current-ws', currentWs); + } +} + +async function initWorkspace() { + const names = await api('/api/heroprompt/workspaces'); + if (names.error || !Array.isArray(names) || names.length === 0) { + console.warn('No workspaces available'); + const treeEl = el('tree'); + if (treeEl) { + treeEl.innerHTML = ` +
      + +

      No workspaces available

      + Create one to get started +
      + `; + } + return; + } + + if (!currentWs || !names.includes(currentWs)) { + currentWs = names[0]; + localStorage.setItem('heroprompt-current-ws', currentWs); + } + + const sel = el('workspaceSelect'); + if (sel) sel.value = currentWs; + + const info = await api(`/api/heroprompt/workspaces/${currentWs}`); + const base = info?.base_path || ''; + if (base && fileTree) { + await fileTree.render(base); + } +} + +// Prompt generation +async function generatePrompt() { + const promptTextEl = el('promptText'); + const outputEl = el('promptOutput'); + + if (!outputEl) { + console.error('Prompt output element not found'); + return; + } + + if (!currentWs) { + outputEl.innerHTML = '
      No workspace selected. Please select a workspace first.
      '; + return; + } + + if (selected.size === 0) { + outputEl.innerHTML = '
      No files selected. Please select files first.
      '; + return; + } + + const promptText = promptTextEl?.value?.trim() || ''; + outputEl.innerHTML = '
      Generating prompt...
      '; + + try { + // sync selection to backend before generating + const paths = Array.from(selected); + const syncResult = await post(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/selection`, { + paths: JSON.stringify(paths) + }); + + if (syncResult.error) { + throw new Error(`Failed to sync selection: ${syncResult.error}`); + } + + const r = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(currentWs)}/prompt`, { + method: 'POST', + body: new URLSearchParams({ text: promptText }) + }); + + if (!r.ok) { + throw new Error(`HTTP ${r.status}: ${r.statusText}`); + } + + const result = await r.text(); + if (result.trim().length === 0) { + outputEl.innerHTML = '
      Generated prompt is empty
      '; + } else { + outputEl.textContent = result; + } + } catch (e) { + console.warn('Generate prompt failed', e); + outputEl.innerHTML = `
      Failed to generate prompt: ${e.message}
      `; + } +} + +async function copyPrompt() { + const outputEl = el('promptOutput'); + if (!outputEl) { + console.warn('Prompt output element not found'); + return; + } + + const text = outputEl.textContent; + if (!text || text.trim().length === 0 || text.includes('No files selected') || text.includes('Failed')) { + console.warn('No valid content to copy'); + return; + } + + if (!navigator.clipboard) { + // Fallback for older browsers + fallbackCopyToClipboard(text); + return; + } + + try { + await navigator.clipboard.writeText(text); + + // Show success feedback + const originalContent = outputEl.innerHTML; + outputEl.innerHTML = '
      Prompt copied to clipboard!
      '; + setTimeout(() => { + outputEl.innerHTML = originalContent; + }, 2000); + } catch (e) { + console.warn('Copy failed', e); + const originalContent = outputEl.innerHTML; + outputEl.innerHTML = '
      Failed to copy prompt
      '; + setTimeout(() => { + outputEl.innerHTML = originalContent; + }, 2000); + } +} + +// Global fallback function for clipboard operations +function fallbackCopyToClipboard(text) { + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.left = '-999999px'; + textArea.style.top = '-999999px'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + document.execCommand('copy'); + console.log('Fallback: Content copied to clipboard'); + } catch (err) { + console.error('Fallback: Failed to copy content', err); + } + + document.body.removeChild(textArea); +} + +// Confirmation modal helper +function showConfirmationModal(message, onConfirm) { + const messageEl = el('confirmDeleteMessage'); + const confirmBtn = el('confirmDeleteBtn'); + + if (messageEl) messageEl.textContent = message; + + // Remove any existing event listeners + const newConfirmBtn = confirmBtn.cloneNode(true); + confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn); + + // Add new event listener + newConfirmBtn.addEventListener('click', () => { + hideModal('confirmDeleteModal'); + onConfirm(); + }); + + showModal('confirmDeleteModal'); +} + +// Workspace management functions +async function deleteWorkspace(workspaceName) { + try { + const encodedName = encodeURIComponent(workspaceName); + const response = await fetch(`/api/heroprompt/workspaces/${encodedName}/delete`, { + method: 'POST' + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Delete failed:', response.status, errorText); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + // If we deleted the current workspace, switch to another one + if (workspaceName === currentWs) { + const names = await api('/api/heroprompt/workspaces'); + if (names && Array.isArray(names) && names.length > 0) { + currentWs = names[0]; + localStorage.setItem('heroprompt-current-ws', currentWs); + await reloadWorkspaces(); + const info = await api(`/api/heroprompt/workspaces/${currentWs}`); + const base = info?.base_path || ''; + if (base && fileTree) { + await fileTree.render(base); + } + } + } + + return { success: true }; + } catch (e) { + console.warn('Delete workspace failed', e); + return { error: 'Failed to delete workspace' }; + } +} + +async function updateWorkspace(workspaceName, newName, newPath) { + try { + const formData = new FormData(); + if (newName && newName !== workspaceName) { + formData.append('name', newName); + } + if (newPath) { + formData.append('base_path', newPath); + } + + const encodedName = encodeURIComponent(workspaceName); + const response = await fetch(`/api/heroprompt/workspaces/${encodedName}`, { + method: 'PUT', + body: formData + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Update failed:', response.status, errorText); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const result = await response.json(); + + // Update current workspace if it was renamed + if (workspaceName === currentWs && result.name && result.name !== workspaceName) { + currentWs = result.name; + localStorage.setItem('heroprompt-current-ws', currentWs); + } + + await reloadWorkspaces(); + const info = await api(`/api/heroprompt/workspaces/${currentWs}`); + const base = info?.base_path || ''; + if (base && fileTree) { + await fileTree.render(base); + } + + return result; + } catch (e) { + console.warn('Update workspace failed', e); + return { error: 'Failed to update workspace' }; + } +} + +// Initialize everything when DOM is ready +document.addEventListener('DOMContentLoaded', function () { + // Initialize file tree + const treeContainer = el('tree'); + if (treeContainer) { + fileTree = new SimpleFileTree(treeContainer); + } + + // Initialize workspaces + initWorkspace(); + reloadWorkspaces(); + + // Tab switching + qsa('.tab').forEach(tab => { + tab.addEventListener('click', function (e) { + e.preventDefault(); + const tabName = this.getAttribute('data-tab'); + switchTab(tabName); + }); + }); + + // Workspace selector + const workspaceSelect = el('workspaceSelect'); + if (workspaceSelect) { + workspaceSelect.addEventListener('change', async (e) => { + currentWs = e.target.value; + localStorage.setItem('heroprompt-current-ws', currentWs); + const info = await api(`/api/heroprompt/workspaces/${currentWs}`); + const base = info?.base_path || ''; + if (base && fileTree) { + await fileTree.render(base); + } + }); + } + + // Explorer controls + const collapseAllBtn = el('collapseAll'); + if (collapseAllBtn) { + collapseAllBtn.addEventListener('click', () => { + if (fileTree) fileTree.collapseAll(); + }); + } + + const refreshExplorerBtn = el('refreshExplorer'); + if (refreshExplorerBtn) { + refreshExplorerBtn.addEventListener('click', async () => { + const info = await api(`/api/heroprompt/workspaces/${currentWs}`); + const base = info?.base_path || ''; + if (base && fileTree) { + await fileTree.render(base); + } + }); + } + + const selectAllBtn = el('selectAll'); + if (selectAllBtn) { + selectAllBtn.addEventListener('click', () => { + if (fileTree) fileTree.selectAll(); + }); + } + + const clearSelectionBtn = el('clearSelection'); + if (clearSelectionBtn) { + clearSelectionBtn.addEventListener('click', () => { + if (fileTree) fileTree.clearSelection(); + }); + } + + const clearAllSelectionBtn = el('clearAllSelection'); + if (clearAllSelectionBtn) { + clearAllSelectionBtn.addEventListener('click', () => { + if (fileTree) fileTree.clearSelection(); + }); + } + + // Search functionality + const searchInput = el('search'); + const clearSearchBtn = el('clearSearch'); + + if (searchInput) { + searchInput.addEventListener('input', (e) => { + if (fileTree) { + fileTree.search(e.target.value); + } + }); + } + + if (clearSearchBtn) { + clearSearchBtn.addEventListener('click', () => { + if (searchInput) { + searchInput.value = ''; + if (fileTree) { + fileTree.search(''); + } + } + }); + } + + // Prompt generation + const generatePromptBtn = el('generatePrompt'); + if (generatePromptBtn) { + generatePromptBtn.addEventListener('click', generatePrompt); + } + + const copyPromptBtn = el('copyPrompt'); + if (copyPromptBtn) { + copyPromptBtn.addEventListener('click', copyPrompt); + } + + // Workspace creation modal + const wsCreateBtn = el('wsCreateBtn'); + if (wsCreateBtn) { + wsCreateBtn.addEventListener('click', () => { + const nameEl = el('wcName'); + const pathEl = el('wcPath'); + const errorEl = el('wcError'); + + if (nameEl) nameEl.value = ''; + if (pathEl) pathEl.value = ''; + if (errorEl) errorEl.textContent = ''; + + showModal('wsCreate'); + }); + } + + const wcCreateBtn = el('wcCreate'); + if (wcCreateBtn) { + wcCreateBtn.addEventListener('click', async () => { + const name = el('wcName')?.value?.trim() || ''; + const path = el('wcPath')?.value?.trim() || ''; + const errorEl = el('wcError'); + + if (!path) { + if (errorEl) errorEl.textContent = 'Path is required.'; + return; + } + + const formData = { base_path: path }; + if (name) formData.name = name; + + const resp = await post('/api/heroprompt/workspaces', formData); + if (resp.error) { + if (errorEl) errorEl.textContent = resp.error; + return; + } + + currentWs = resp.name || currentWs; + localStorage.setItem('heroprompt-current-ws', currentWs); + await reloadWorkspaces(); + + const info = await api(`/api/heroprompt/workspaces/${currentWs}`); + const base = info?.base_path || ''; + if (base && fileTree) { + await fileTree.render(base); + } + + hideModal('wsCreate'); + }); + } + + // Workspace details modal + const wsDetailsBtn = el('wsDetailsBtn'); + if (wsDetailsBtn) { + wsDetailsBtn.addEventListener('click', async () => { + const info = await api(`/api/heroprompt/workspaces/${currentWs}`); + if (info && !info.error) { + const nameEl = el('wdName'); + const pathEl = el('wdPath'); + const errorEl = el('wdError'); + + if (nameEl) nameEl.value = info.name || currentWs; + if (pathEl) pathEl.value = info.base_path || ''; + if (errorEl) errorEl.textContent = ''; + + showModal('wsDetails'); + } + }); + } + + // Workspace details update + const wdUpdateBtn = el('wdUpdate'); + if (wdUpdateBtn) { + wdUpdateBtn.addEventListener('click', async () => { + const name = el('wdName')?.value?.trim() || ''; + const path = el('wdPath')?.value?.trim() || ''; + const errorEl = el('wdError'); + + if (!path) { + if (errorEl) errorEl.textContent = 'Path is required.'; + return; + } + + const result = await updateWorkspace(currentWs, name, path); + if (result.error) { + if (errorEl) errorEl.textContent = result.error; + return; + } + + hideModal('wsDetails'); + }); + } + + // Workspace details delete + const wdDeleteBtn = el('wdDelete'); + if (wdDeleteBtn) { + wdDeleteBtn.addEventListener('click', async () => { + showConfirmationModal(`Are you sure you want to delete workspace "${currentWs}"?`, async () => { + const result = await deleteWorkspace(currentWs); + if (result.error) { + const errorEl = el('wdError'); + if (errorEl) errorEl.textContent = result.error; + return; + } + hideModal('wsDetails'); + }); + }); + } + + // Chat functionality + initChatInterface(); + + console.log('Enhanced HeroPrompt UI initialized'); +}); + +// Chat Interface Implementation +function initChatInterface() { + const chatInput = el('chatInput'); + const sendBtn = el('sendChat'); + const messagesContainer = el('chatMessages'); + const charCount = el('charCount'); + const chatStatus = el('chatStatus'); + const typingIndicator = el('typingIndicator'); + const newChatBtn = el('newChatBtn'); + const chatList = el('chatList'); + + let chatHistory = []; + let isTyping = false; + let conversations = JSON.parse(localStorage.getItem('heroprompt-conversations') || '[]'); + let currentConversationId = null; + + // Initialize chat input functionality + if (chatInput && sendBtn) { + // Auto-resize textarea + chatInput.addEventListener('input', function () { + this.style.height = 'auto'; + this.style.height = Math.min(this.scrollHeight, 120) + 'px'; + + // Update character count + if (charCount) { + const count = this.value.length; + charCount.textContent = count; + charCount.className = 'char-count'; + if (count > 2000) charCount.classList.add('warning'); + if (count > 4000) charCount.classList.add('error'); + } + + // Enable/disable send button + sendBtn.disabled = this.value.trim().length === 0; + }); + + // Handle Enter key + chatInput.addEventListener('keydown', function (e) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + if (!sendBtn.disabled) { + sendMessage(); + } + } + }); + + // Send button click + sendBtn.addEventListener('click', sendMessage); + } + + // Chat action buttons + const clearChatBtn = el('clearChat'); + const exportChatBtn = el('exportChat'); + + if (newChatBtn) { + newChatBtn.addEventListener('click', startNewChat); + } + + if (clearChatBtn) { + clearChatBtn.addEventListener('click', clearChat); + } + + if (exportChatBtn) { + exportChatBtn.addEventListener('click', exportChat); + } + + async function sendMessage() { + const message = chatInput.value.trim(); + if (!message || isTyping) return; + + // Add user message to chat + addMessage('user', message); + chatInput.value = ''; + chatInput.style.height = 'auto'; + sendBtn.disabled = true; + if (charCount) charCount.textContent = '0'; + + // Show typing indicator + showTypingIndicator(); + updateChatStatus('typing', 'AI is thinking...'); + + try { + // Simulate API call - replace with actual API endpoint + const response = await simulateAIResponse(message); + + // Hide typing indicator + hideTypingIndicator(); + + // Add AI response + addMessage('assistant', response); + updateChatStatus('ready', 'Ready'); + + } catch (error) { + hideTypingIndicator(); + addMessage('assistant', 'Sorry, I encountered an error. Please try again.'); + updateChatStatus('error', 'Error occurred'); + console.error('Chat error:', error); + } + } + + function addMessage(role, content) { + const messageDiv = document.createElement('div'); + messageDiv.className = `chat-message ${role}`; + + const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + messageDiv.innerHTML = ` +
      + +
      +
      +
      ${formatMessageContent(content)}
      +
      + ${timestamp} +
      + + ${role === 'assistant' ? ` + + ` : ''} +
      +
      +
      + `; + + messageDiv.id = messageId; + + // Remove welcome message if it exists + const welcomeMessage = messagesContainer.querySelector('.welcome-message'); + if (welcomeMessage) { + welcomeMessage.remove(); + } + + messagesContainer.appendChild(messageDiv); + + // Store in chat history + chatHistory.push({ + id: messageId, + role: role, + content: content, + timestamp: new Date().toISOString() + }); + + // Save to conversation + if (window.saveMessageToConversation) { + window.saveMessageToConversation(role, content); + } + + // Auto-scroll to bottom + scrollToBottom(); + } + + function formatMessageContent(content) { + // Basic markdown-like formatting + let formatted = content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/```([\s\S]*?)```/g, '
      $1
      ') + .replace(/\n/g, '
      '); + + return formatted; + } + + function showTypingIndicator() { + if (typingIndicator) { + typingIndicator.style.display = 'flex'; + isTyping = true; + } + } + + function hideTypingIndicator() { + if (typingIndicator) { + typingIndicator.style.display = 'none'; + isTyping = false; + } + } + + function updateChatStatus(type, message) { + if (chatStatus) { + chatStatus.textContent = message; + chatStatus.className = `chat-status ${type}`; + } + } + + function scrollToBottom() { + if (messagesContainer) { + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + } + + function startNewChat() { + clearChat(); + addMessage('assistant', 'Hello! I\'m ready to help you with your code. What would you like to know?'); + } + + function clearChat() { + chatHistory = []; + if (messagesContainer) { + messagesContainer.innerHTML = ` +
      +
      + +
      +
      +

      Welcome to AI Assistant

      +

      I'm here to help you with your code! You can:

      + + Select some files from the explorer and start chatting! +
      +
      + `; + } + updateChatStatus('ready', 'Ready'); + } + + function exportChat() { + if (chatHistory.length === 0) { + alert('No chat history to export'); + return; + } + + const exportData = { + timestamp: new Date().toISOString(), + messages: chatHistory, + workspace: currentWs, + selectedFiles: Array.from(selected) + }; + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `chat-export-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + // Simulate AI response - replace with actual API call + async function simulateAIResponse(userMessage) { + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000)); + + // Get context from selected files + const context = selected.size > 0 ? + `Based on your selected files (${Array.from(selected).join(', ')}), ` : ''; + + // Simple response generation - replace with actual AI API + const responses = [ + `${context}I can help you analyze and improve your code. What specific aspect would you like me to focus on?`, + `${context}I notice you're working with these files. Would you like me to review the code structure or suggest improvements?`, + `${context}I can help explain the code, identify potential issues, or suggest optimizations. What would you like to know?`, + `${context}Let me analyze your code and provide insights. Is there a particular functionality you'd like me to examine?` + ]; + + if (userMessage.toLowerCase().includes('error') || userMessage.toLowerCase().includes('bug')) { + return `${context}I can help you debug issues. Please share the specific error message or describe the unexpected behavior you're experiencing.`; + } + + if (userMessage.toLowerCase().includes('optimize') || userMessage.toLowerCase().includes('performance')) { + return `${context}For performance optimization, I can analyze your code for bottlenecks, suggest algorithmic improvements, and recommend best practices.`; + } + + if (userMessage.toLowerCase().includes('explain') || userMessage.toLowerCase().includes('how')) { + return `${context}I'd be happy to explain the code functionality. Which specific part would you like me to break down?`; + } + + return responses[Math.floor(Math.random() * responses.length)]; + } +} + +// Global helper function for message formatting +function formatMessageContent(content) { + // Basic markdown-like formatting + let formatted = content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/```([\s\S]*?)```/g, '
      $1
      ') + .replace(/\n/g, '
      '); + + return formatted; +} + +// Global functions for message actions +function copyMessage(messageId) { + const messageEl = document.getElementById(messageId); + if (!messageEl) return; + + const textEl = messageEl.querySelector('.message-text'); + if (!textEl) return; + + const text = textEl.textContent || textEl.innerText; + + if (navigator.clipboard) { + navigator.clipboard.writeText(text).then(() => { + showMessageFeedback(messageId, 'Copied!'); + }).catch(err => { + console.error('Copy failed:', err); + fallbackCopyToClipboard(text); + }); + } else { + fallbackCopyToClipboard(text); + } +} + +function regenerateMessage(messageId) { + const messageEl = document.getElementById(messageId); + if (!messageEl) return; + + // Find the previous user message + let prevMessage = messageEl.previousElementSibling; + while (prevMessage && !prevMessage.classList.contains('user')) { + prevMessage = prevMessage.previousElementSibling; + } + + if (prevMessage) { + const userText = prevMessage.querySelector('.message-text').textContent; + + // Remove the current AI message + messageEl.remove(); + + // Show typing indicator and regenerate + const typingIndicator = el('typingIndicator'); + if (typingIndicator) { + typingIndicator.style.display = 'flex'; + } + + // Simulate regeneration + setTimeout(async () => { + try { + const response = await simulateAIResponse(userText); + if (typingIndicator) { + typingIndicator.style.display = 'none'; + } + + // Create a new message manually + const messageDiv = document.createElement('div'); + messageDiv.className = 'chat-message assistant'; + const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const newMessageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + messageDiv.innerHTML = ` +
      + +
      +
      +
      ${formatMessageContent(response)}
      +
      + ${timestamp} +
      + + +
      +
      +
      + `; + + messageDiv.id = newMessageId; + const messagesContainer = el('chatMessages'); + if (messagesContainer) { + messagesContainer.appendChild(messageDiv); + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + } catch (error) { + if (typingIndicator) { + typingIndicator.style.display = 'none'; + } + console.error('Regeneration error:', error); + } + }, 1500); + } +} + +function showMessageFeedback(messageId, text) { + const messageEl = document.getElementById(messageId); + if (!messageEl) return; + + const actionsEl = messageEl.querySelector('.message-actions'); + if (!actionsEl) return; + + const originalHTML = actionsEl.innerHTML; + actionsEl.innerHTML = `${text}`; + + setTimeout(() => { + actionsEl.innerHTML = originalHTML; + }, 2000); +} + +// Chat List Management Functions +function initChatList() { + const chatList = el('chatList'); + const newChatBtn = el('newChatBtn'); + + if (!chatList) return; + + let conversations = JSON.parse(localStorage.getItem('heroprompt-conversations') || '[]'); + let currentConversationId = localStorage.getItem('heroprompt-current-conversation') || null; + + function renderChatList() { + if (conversations.length === 0) { + chatList.innerHTML = ` +
      + +

      No conversations yet

      + Start a new chat to begin +
      + `; + return; + } + + const conversationsHtml = conversations.map(conv => { + const isActive = conv.id === currentConversationId; + const preview = conv.messages.length > 0 ? + conv.messages[conv.messages.length - 1].content.substring(0, 50) + '...' : + 'New conversation'; + const time = new Date(conv.updatedAt).toLocaleDateString(); + + return ` +
      +
      ${conv.title}
      +
      ${preview}
      +
      + ${time} +
      + +
      +
      +
      + `; + }).join(''); + + chatList.innerHTML = `
      ${conversationsHtml}
      `; + + // Add click listeners to conversation items + chatList.querySelectorAll('.chat-conversation-item').forEach(item => { + item.addEventListener('click', (e) => { + if (!e.target.closest('.conversation-action')) { + const conversationId = item.dataset.conversationId; + loadConversation(conversationId); + } + }); + }); + } + + function createNewConversation() { + const newConversation = { + id: 'conv-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9), + title: `Chat ${conversations.length + 1}`, + messages: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + + conversations.unshift(newConversation); + localStorage.setItem('heroprompt-conversations', JSON.stringify(conversations)); + loadConversation(newConversation.id); + renderChatList(); + } + + function loadConversation(conversationId) { + currentConversationId = conversationId; + localStorage.setItem('heroprompt-current-conversation', conversationId); + + const messagesContainer = el('chatMessages'); + if (!messagesContainer) return; + + if (conversationId) { + const conversation = conversations.find(c => c.id === conversationId); + if (conversation && conversation.messages.length > 0) { + // Load existing conversation + messagesContainer.innerHTML = ''; + conversation.messages.forEach(message => { + addMessageToDOM(message.role, message.content, message.timestamp); + }); + } else { + // Show welcome message for empty conversation + showWelcomeMessage(); + } + } else { + // New conversation + showWelcomeMessage(); + } + + renderChatList(); // Update active state + scrollToBottom(); + } + + function showWelcomeMessage() { + const messagesContainer = el('chatMessages'); + if (!messagesContainer) return; + + messagesContainer.innerHTML = ` +
      +
      + +
      +
      +

      Welcome to AI Assistant

      +

      I'm here to help you with your code! You can:

      + + Select some files from the explorer and start chatting! +
      +
      + `; + } + + function addMessageToDOM(role, content, timestamp) { + const messagesContainer = el('chatMessages'); + if (!messagesContainer) return; + + const messageDiv = document.createElement('div'); + messageDiv.className = `chat-message ${role}`; + + const time = timestamp ? new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + messageDiv.innerHTML = ` +
      + +
      +
      +
      ${formatMessageContent(content)}
      +
      + ${time} +
      + + ${role === 'assistant' ? ` + + ` : ''} +
      +
      +
      + `; + + messageDiv.id = messageId; + messagesContainer.appendChild(messageDiv); + } + + function scrollToBottom() { + const messagesContainer = el('chatMessages'); + if (messagesContainer) { + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + } + + // Initialize + renderChatList(); + if (currentConversationId) { + loadConversation(currentConversationId); + } else { + showWelcomeMessage(); + } + + // Event listeners + if (newChatBtn) { + newChatBtn.addEventListener('click', createNewConversation); + } + + // Expose functions globally + window.loadConversation = loadConversation; + window.deleteConversation = function (conversationId) { + conversations = conversations.filter(c => c.id !== conversationId); + localStorage.setItem('heroprompt-conversations', JSON.stringify(conversations)); + + if (currentConversationId === conversationId) { + currentConversationId = null; + localStorage.removeItem('heroprompt-current-conversation'); + showWelcomeMessage(); + } + + renderChatList(); + }; + + window.saveMessageToConversation = function (role, content) { + if (!currentConversationId) { + createNewConversation(); + } + + const conversation = conversations.find(c => c.id === currentConversationId); + if (conversation) { + const message = { + role: role, + content: content, + timestamp: new Date().toISOString() + }; + + conversation.messages.push(message); + conversation.updatedAt = new Date().toISOString(); + + // Update title based on first user message + if (role === 'user' && conversation.title.startsWith('Chat ')) { + conversation.title = content.substring(0, 30) + '...'; + } + + localStorage.setItem('heroprompt-conversations', JSON.stringify(conversations)); + renderChatList(); + } + }; +} + +// Initialize chat list when DOM is ready +document.addEventListener('DOMContentLoaded', function () { + // Add a small delay to ensure other initialization is complete + setTimeout(() => { + initChatList(); + }, 100); +}); diff --git a/lib/web/ui/templates/js/heroscript.js b/lib/web/ui/static/js/heroscript.js similarity index 100% rename from lib/web/ui/templates/js/heroscript.js rename to lib/web/ui/static/js/heroscript.js diff --git a/lib/web/ui/templates/js/theme.js b/lib/web/ui/static/js/theme.js similarity index 100% rename from lib/web/ui/templates/js/theme.js rename to lib/web/ui/static/js/theme.js diff --git a/lib/web/ui/templates/admin_layout.html b/lib/web/ui/templates/admin/layout.html similarity index 99% rename from lib/web/ui/templates/admin_layout.html rename to lib/web/ui/templates/admin/layout.html index 66561001..d899c806 100644 --- a/lib/web/ui/templates/admin_layout.html +++ b/lib/web/ui/templates/admin/layout.html @@ -44,4 +44,5 @@ - \ No newline at end of file + + diff --git a/lib/web/ui/templates/css/heroprompt.css b/lib/web/ui/templates/css/heroprompt.css deleted file mode 100644 index 4694b6b7..00000000 --- a/lib/web/ui/templates/css/heroprompt.css +++ /dev/null @@ -1,267 +0,0 @@ -/* Heroprompt-specific styles */ - -.heroprompt-container { - display: flex; - gap: 1rem; - height: calc(100vh - 200px); -} - -.workspaces-panel { - flex: 0 0 280px; - min-width: 280px; -} - -.workspace-details { - flex: 1; - min-width: 0; -} - -.instructions-panel { - flex: 0 0 350px; - min-width: 350px; -} - -/* Workspace list styling */ -.workspace-item { - cursor: pointer; - transition: background-color 0.2s ease; - border: none !important; - padding: 0.75rem 1rem; -} - -.workspace-item:hover { - background-color: var(--bs-gray-100); -} - -[data-bs-theme="dark"] .workspace-item:hover { - background-color: var(--bs-gray-800); -} - -.workspace-item.active { - background-color: var(--bs-primary); - color: white; -} - -.workspace-item.active:hover { - background-color: var(--bs-primary); -} - -/* Directory and file styling */ -.directory-item { - border: 1px solid var(--bs-border-color); - border-radius: 0.375rem; - margin-bottom: 1rem; - background-color: var(--bs-body-bg); -} - -.directory-header { - background-color: var(--bs-gray-50); - border-bottom: 1px solid var(--bs-border-color); - padding: 0.75rem 1rem; - font-weight: 600; - border-radius: 0.375rem 0.375rem 0 0; -} - -[data-bs-theme="dark"] .directory-header { - background-color: var(--bs-gray-800); -} - -.file-list { - padding: 0.5rem; - max-height: 300px; - overflow-y: auto; -} - -.file-item { - display: flex; - align-items: center; - padding: 0.5rem; - border-radius: 0.25rem; - cursor: pointer; - transition: background-color 0.2s ease; - margin-bottom: 0.25rem; -} - -.file-item:hover { - background-color: var(--bs-gray-100); -} - -[data-bs-theme="dark"] .file-item:hover { - background-color: var(--bs-gray-700); -} - -.file-item.selected { - background-color: var(--bs-success-bg-subtle); - border: 1px solid var(--bs-success-border-subtle); -} - -.file-item input[type="checkbox"] { - margin-right: 0.5rem; -} - -.file-name { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.875rem; -} - -/* Empty state styling */ -.empty-state { - text-align: center; - padding: 2rem; - color: var(--bs-text-muted); -} - -.empty-state-icon { - font-size: 3rem; - margin-bottom: 1rem; - opacity: 0.5; -} - -/* Button styling */ -.btn-group-actions { - display: flex; - gap: 0.5rem; - align-items: center; -} - -/* Toast styling */ -.toast { - min-width: 300px; -} - -/* Instructions panel styling */ -.instructions-panel textarea { - resize: vertical; - min-height: 200px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.875rem; - line-height: 1.5; -} - -.instructions-panel .card-body { - padding: 1rem; -} - -/* Responsive design */ -@media (max-width: 1200px) { - .heroprompt-container { - flex-direction: column; - height: auto; - } - - .workspaces-panel, - .workspace-details, - .instructions-panel { - flex: none; - min-width: auto; - margin-bottom: 1rem; - } - - .instructions-panel { - order: 3; - } -} - -@media (max-width: 768px) { - .heroprompt-container { - flex-direction: column; - height: auto; - } - - .workspaces-panel, - .workspace-details, - .instructions-panel { - flex: none; - min-width: auto; - margin-bottom: 1rem; - } -} - -/* Selection counter */ -.selection-counter { - font-size: 0.875rem; - color: var(--bs-text-muted); - margin-left: 0.5rem; -} - -.selection-counter.has-selection { - color: var(--bs-success); - font-weight: 600; -} - -/* Directory path styling */ -.directory-path { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.75rem; - color: var(--bs-text-muted); - margin-top: 0.25rem; -} - -/* Workspace name input */ -.workspace-name-input { - border: 1px solid var(--bs-border-color); - border-radius: 0.25rem; - padding: 0.5rem; - width: 100%; - margin-bottom: 0.5rem; -} - -/* Loading state */ -.loading-spinner { - display: inline-block; - width: 1rem; - height: 1rem; - border: 2px solid var(--bs-border-color); - border-radius: 50%; - border-top-color: var(--bs-primary); - animation: spin 1s ease-in-out infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -/* File type icons */ -.file-icon { - margin-right: 0.5rem; - width: 16px; - text-align: center; -} - -.file-icon.file-md::before { - content: "📝"; -} - -.file-icon.file-v::before { - content: "⚡"; -} - -.file-icon.file-js::before { - content: "📜"; -} - -.file-icon.file-css::before { - content: "🎨"; -} - -.file-icon.file-html::before { - content: "🌐"; -} - -.file-icon.file-json::before { - content: "📋"; -} - -.file-icon.file-yaml::before { - content: "⚙️"; -} - -.file-icon.file-txt::before { - content: "📄"; -} - -.file-icon.file-default::before { - content: "📁"; -} \ No newline at end of file diff --git a/lib/web/ui/templates/heroprompt.html b/lib/web/ui/templates/heroprompt.html index 8345b825..23791b34 100644 --- a/lib/web/ui/templates/heroprompt.html +++ b/lib/web/ui/templates/heroprompt.html @@ -31,74 +31,283 @@
      -
      +
      Heroprompt
      - /admin/heroprompt +
      + +
      -
      - -
      -
      -
      -
      Workspaces
      - -
      -
      - - - -
      -
      -
      +
      + +
      +
      +
      +
      +
      Explorer
      +
      + + +
      +
      - -
      -
      -
      -
      - Select a workspace - -
      -
      - - +
      + +
      + +
      +
      + + + + + +
      -
      -
      -

      Select a workspace to view its directories and files.

      + +
      +
      +
      +
      + + 0 selected + +
      +
      +
      +
      +
      + +
      +
      + +

      Select a workspace to browse files

      +
      - -
      -
      -
      -
      User Instructions
      - + +
      +
      +
      +
      + +
      + + + 0 tokens + +
      +
      -
      - -
      - - These instructions will be included in the generated prompt along with the selected - files. - +- Analyze the code structure and identify potential improvements +- Add comprehensive error handling and validation +- Optimize performance and reduce complexity +- Add detailed documentation and comments +- Implement best practices and design patterns"> +
      + +
      + +
      +
      + Generated Prompt +
      + +
      +
      +
      +
      +
      + +

      Generated prompt will appear here

      +
      +
      +
      +
      +
      +
      + + +
      @@ -107,15 +316,94 @@ Example:
      - -
      -