Merge pull request #118 from freeflowuniverse/development_heroprompt
Development heroprompt
This commit is contained in:
@@ -7,7 +7,7 @@ Chalk offers functions:- `console.color_fg(text string, color string)` - To chan
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
# basic usage
|
# basic usage
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ The parser supports various input formats:
|
|||||||
4. **Comments**: `// this is a comment` (ignored during parsing)
|
4. **Comments**: `// this is a comment` (ignored during parsing)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```vlang
|
```v
|
||||||
text := "name:'John Doe' age:30 active:true // user details"
|
text := "name:'John Doe' age:30 active:true // user details"
|
||||||
params := paramsparser.new(text)!
|
params := paramsparser.new(text)!
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ has mechanisms to print better to console, see the methods below
|
|||||||
|
|
||||||
import as
|
import as
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
this is how we want example scripts to be, see the first line
|
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
|
#!/usr/bin/env -S v -cg -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
import freeflowuniverse.herolib...
|
import freeflowuniverse.herolib...
|
||||||
|
|||||||
@@ -926,7 +926,7 @@ The parser supports various input formats:
|
|||||||
4. **Comments**: `// this is a comment` (ignored during parsing)
|
4. **Comments**: `// this is a comment` (ignored during parsing)
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```vlang
|
```v
|
||||||
text := "name:'John Doe' age:30 active:true // user details"
|
text := "name:'John Doe' age:30 active:true // user details"
|
||||||
params := paramsparser.new(text)!
|
params := paramsparser.new(text)!
|
||||||
```
|
```
|
||||||
@@ -1278,7 +1278,7 @@ has mechanisms to print better to console, see the methods below
|
|||||||
|
|
||||||
import as
|
import as
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
import freeflowuniverse.herolib.ui.console
|
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
|
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
|
#!/usr/bin/env -S v -cg -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
import freeflowuniverse.herolib...
|
import freeflowuniverse.herolib...
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
module time
|
module time
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ fn do() ! {
|
|||||||
mut cmd := Command{
|
mut cmd := Command{
|
||||||
name: 'hero'
|
name: 'hero'
|
||||||
description: 'Your HERO toolset.'
|
description: 'Your HERO toolset.'
|
||||||
version: '1.0.28'
|
version: '1.0.29'
|
||||||
}
|
}
|
||||||
|
|
||||||
// herocmds.cmd_run_add_flags(mut cmd)
|
// herocmds.cmd_run_add_flags(mut cmd)
|
||||||
@@ -85,7 +85,7 @@ fn do() ! {
|
|||||||
herocmds.cmd_git(mut cmd)
|
herocmds.cmd_git(mut cmd)
|
||||||
herocmds.cmd_generator(mut cmd)
|
herocmds.cmd_generator(mut cmd)
|
||||||
herocmds.cmd_docusaurus(mut cmd)
|
herocmds.cmd_docusaurus(mut cmd)
|
||||||
// herocmds.cmd_web(mut cmd)
|
herocmds.cmd_web(mut cmd)
|
||||||
|
|
||||||
cmd.setup()
|
cmd.setup()
|
||||||
cmd.parse(os.args)
|
cmd.parse(os.args)
|
||||||
|
|||||||
@@ -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:
|
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
|
#!/usr/bin/env -S v -gc none -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.tfgrid.griddriver as griddriverinstaller
|
import freeflowuniverse.herolib.installers.tfgrid.griddriver as griddriverinstaller
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ text in paragraph
|
|||||||
| February | $80 |
|
| February | $80 |
|
||||||
| March | $420 |
|
| March | $420 |
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
fn main(){
|
fn main(){
|
||||||
println('hello world')
|
println('hello world')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
|
#!/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() {
|
fn main() {
|
||||||
// println('Starting UI test server on port 8080...')
|
println('Starting UI test server on port 8080...')
|
||||||
// println('Visit http://localhost:8080 to see the admin interface')
|
println('Visit http://localhost:8080 to see the admin interface')
|
||||||
|
|
||||||
// ui.start(
|
ui.start(
|
||||||
// title: 'Test Admin Panel'
|
title: 'Test Admin Panel'
|
||||||
// port: 8080
|
port: 8080
|
||||||
// )!
|
)!
|
||||||
// }
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -e
|
|||||||
|
|
||||||
os_name="$(uname -s)"
|
os_name="$(uname -s)"
|
||||||
arch_name="$(uname -m)"
|
arch_name="$(uname -m)"
|
||||||
version='1.0.28'
|
version='1.0.29'
|
||||||
|
|
||||||
|
|
||||||
# Base URL for GitHub releases
|
# Base URL for GitHub releases
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.clients. ipapi
|
import freeflowuniverse.herolib.clients. ipapi
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ see https://jina.ai/
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.clients. jina
|
import freeflowuniverse.herolib.clients. jina
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
import freeflowuniverse.herolib.clients.mailclient
|
import freeflowuniverse.herolib.clients.mailclient
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
import freeflowuniverse.herolib.clients.openai
|
import freeflowuniverse.herolib.clients.openai
|
||||||
import freeflowuniverse.herolib.core.playcmds
|
import freeflowuniverse.herolib.core.playcmds
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.clients. vastai
|
import freeflowuniverse.herolib.clients. vastai
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.clients. wireguard
|
import freeflowuniverse.herolib.clients. wireguard
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ ${args.title}
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
@if args.cat == .installer
|
@if args.cat == .installer
|
||||||
|
|
||||||
|
|||||||
@@ -1,110 +1,110 @@
|
|||||||
module herocmds
|
module herocmds
|
||||||
|
|
||||||
// import freeflowuniverse.herolib.ui.console
|
import freeflowuniverse.herolib.ui.console
|
||||||
// import freeflowuniverse.herolib.web.ui
|
import freeflowuniverse.herolib.web.ui
|
||||||
// import os
|
import os
|
||||||
// import cli { Command, Flag }
|
import cli { Command, Flag }
|
||||||
// import time
|
import time
|
||||||
|
|
||||||
// pub fn cmd_web(mut cmdroot Command) Command {
|
pub fn cmd_web(mut cmdroot Command) Command {
|
||||||
// mut cmd_run := Command{
|
mut cmd_run := Command{
|
||||||
// name: 'web'
|
name: 'web'
|
||||||
// description: 'Run the Heroprompt UI (located in lib/web/heroprompt).'
|
description: 'Run the Heroprompt UI (located in lib/web/heroprompt).'
|
||||||
// required_args: 0
|
required_args: 0
|
||||||
// execute: cmd_web_execute
|
execute: cmd_web_execute
|
||||||
// }
|
}
|
||||||
|
|
||||||
// cmd_run.add_flag(Flag{
|
cmd_run.add_flag(Flag{
|
||||||
// flag: .bool
|
flag: .bool
|
||||||
// required: false
|
required: false
|
||||||
// name: 'open'
|
name: 'open'
|
||||||
// abbrev: 'o'
|
abbrev: 'o'
|
||||||
// description: 'Open the UI in the default browser after starting the server.'
|
description: 'Open the UI in the default browser after starting the server.'
|
||||||
// })
|
})
|
||||||
|
|
||||||
// cmd_run.add_flag(Flag{
|
cmd_run.add_flag(Flag{
|
||||||
// flag: .string
|
flag: .string
|
||||||
// required: false
|
required: false
|
||||||
// name: 'host'
|
name: 'host'
|
||||||
// abbrev: 'h'
|
abbrev: 'h'
|
||||||
// description: 'Host to bind the server to (default: localhost).'
|
description: 'Host to bind the server to (default: localhost).'
|
||||||
// })
|
})
|
||||||
|
|
||||||
// cmd_run.add_flag(Flag{
|
cmd_run.add_flag(Flag{
|
||||||
// flag: .int
|
flag: .int
|
||||||
// required: false
|
required: false
|
||||||
// name: 'port'
|
name: 'port'
|
||||||
// abbrev: 'p'
|
abbrev: 'p'
|
||||||
// description: 'Port to bind the server to (default: 8080).'
|
description: 'Port to bind the server to (default: 8080).'
|
||||||
// })
|
})
|
||||||
|
|
||||||
// cmdroot.add_command(cmd_run)
|
cmdroot.add_command(cmd_run)
|
||||||
// return cmdroot
|
return cmdroot
|
||||||
// }
|
}
|
||||||
|
|
||||||
// fn cmd_web_execute(cmd Command) ! {
|
fn cmd_web_execute(cmd Command) ! {
|
||||||
// // ---------- FLAGS ----------
|
// ---------- FLAGS ----------
|
||||||
// mut open_ := cmd.flags.get_bool('open') or { false }
|
mut open_ := cmd.flags.get_bool('open') or { false }
|
||||||
// mut host := cmd.flags.get_string('host') or { 'localhost' }
|
mut host := cmd.flags.get_string('host') or { 'localhost' }
|
||||||
// mut port := cmd.flags.get_int('port') or { 8080 }
|
mut port := cmd.flags.get_int('port') or { 8080 }
|
||||||
|
|
||||||
// // Set defaults if not provided
|
// Set defaults if not provided
|
||||||
// if host == '' {
|
if host == '' {
|
||||||
// host = 'localhost'
|
host = 'localhost'
|
||||||
// }
|
}
|
||||||
// if port == 0 {
|
if port == 0 {
|
||||||
// port = 8080
|
port = 8080
|
||||||
// }
|
}
|
||||||
|
|
||||||
// console.print_header('Starting Heroprompt...')
|
console.print_header('Starting Heroprompt...')
|
||||||
|
|
||||||
// // Prepare arguments for the UI factory
|
// Prepare arguments for the UI factory
|
||||||
// mut factory_args := ui.FactoryArgs{
|
mut factory_args := ui.FactoryArgs{
|
||||||
// title: 'Hero Admin Panel'
|
title: 'Hero Admin Panel'
|
||||||
// host: host
|
host: host
|
||||||
// port: port
|
port: port
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // ---------- START WEB SERVER ----------
|
// ---------- START WEB SERVER ----------
|
||||||
// console.print_header('Starting Heroprompt server...')
|
console.print_header('Starting Heroprompt server...')
|
||||||
|
|
||||||
// // Start the server in a separate thread to allow for browser opening
|
// Start the server in a separate thread to allow for browser opening
|
||||||
// spawn fn [factory_args] () {
|
spawn fn [factory_args] () {
|
||||||
// ui.start(factory_args) or {
|
ui.start(factory_args) or {
|
||||||
// console.print_stderr('Failed to start Heroprompt server: ${err}')
|
console.print_stderr('Failed to start Heroprompt server: ${err}')
|
||||||
// return
|
return
|
||||||
// }
|
}
|
||||||
// }()
|
}()
|
||||||
|
|
||||||
// // Give the server a moment to start
|
// Give the server a moment to start
|
||||||
// time.sleep(2 * time.second)
|
time.sleep(2 * time.second)
|
||||||
// url := 'http://${factory_args.host}:${factory_args.port}'
|
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_ {
|
if open_ {
|
||||||
// mut cmd_str := ''
|
mut cmd_str := ''
|
||||||
// $if macos {
|
$if macos {
|
||||||
// cmd_str = 'open ${url}'
|
cmd_str = 'open ${url}'
|
||||||
// } $else $if linux {
|
} $else $if linux {
|
||||||
// cmd_str = 'xdg-open ${url}'
|
cmd_str = 'xdg-open ${url}'
|
||||||
// } $else $if windows {
|
} $else $if windows {
|
||||||
// cmd_str = 'start ${url}'
|
cmd_str = 'start ${url}'
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if cmd_str != '' {
|
if cmd_str != '' {
|
||||||
// result := os.execute(cmd_str)
|
result := os.execute(cmd_str)
|
||||||
// if result.exit_code == 0 {
|
if result.exit_code == 0 {
|
||||||
// console.print_green('Opened Heroprompt in default browser.')
|
console.print_green('Opened Heroprompt in default browser.')
|
||||||
// } else {
|
} else {
|
||||||
// console.print_stderr('Failed to open browser: ${result.output}')
|
console.print_stderr('Failed to open browser: ${result.output}')
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Keep the process alive while the server runs
|
// Keep the process alive while the server runs
|
||||||
// console.print_header('Press Ctrl+C to stop the server')
|
console.print_header('Press Ctrl+C to stop the server')
|
||||||
// for {
|
for {
|
||||||
// time.sleep(1 * time.second)
|
time.sleep(1 * time.second)
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|||||||
@@ -8,7 +8,51 @@ module codewalker
|
|||||||
// - Lines starting with '#' are comments; empty lines ignored
|
// - Lines starting with '#' are comments; empty lines ignored
|
||||||
// No negation support for simplicity
|
// 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 {
|
struct IgnoreRule {
|
||||||
base string // relative dir from source root where the ignore file lives ('' means global)
|
base string // relative dir from source root where the ignore file lives ('' means global)
|
||||||
|
|||||||
244
lib/develop/codewalker/tree.v
Normal file
244
lib/develop/codewalker/tree.v
Normal file
@@ -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, '')
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ pub mut:
|
|||||||
content string
|
content string
|
||||||
path pathlib.Path
|
path pathlib.Path
|
||||||
name string
|
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
|
// Utility function to get file extension with special handling for common files
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ pub fn (mut wsp Workspace) add_dir(args AddDirParams) !HeropromptChild {
|
|||||||
exist: .yes
|
exist: .yes
|
||||||
}
|
}
|
||||||
name: name
|
name: name
|
||||||
|
include_tree: true
|
||||||
}
|
}
|
||||||
wsp.children << ch
|
wsp.children << ch
|
||||||
wsp.save()!
|
wsp.save()!
|
||||||
@@ -220,55 +221,29 @@ pub:
|
|||||||
typ string @[json: 'type']
|
typ string @[json: 'type']
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn (wsp &Workspace) list() ![]ListItem {
|
pub fn (wsp &Workspace) list_dir(rel_path string) ![]ListItem {
|
||||||
mut dir := wsp.base_path
|
// Create an ignore matcher with default patterns
|
||||||
if dir.len == 0 {
|
ignore_matcher := codewalker.gitignore_matcher_new()
|
||||||
return error('workspace base_path not set')
|
items := codewalker.list_directory_filtered(wsp.base_path, rel_path, &ignore_matcher)!
|
||||||
}
|
|
||||||
|
|
||||||
if !os.is_abs_path(dir) {
|
|
||||||
dir = os.join_path(wsp.base_path, dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
entries := os.ls(dir) or { return error('cannot list directory') }
|
|
||||||
mut out := []ListItem{}
|
mut out := []ListItem{}
|
||||||
for e in entries {
|
for item in items {
|
||||||
full := os.join_path(dir, e)
|
|
||||||
if os.is_dir(full) {
|
|
||||||
out << ListItem{
|
out << ListItem{
|
||||||
name: e
|
name: item.name
|
||||||
typ: 'directory'
|
typ: item.typ
|
||||||
}
|
|
||||||
} else if os.is_file(full) {
|
|
||||||
out << ListItem{
|
|
||||||
name: e
|
|
||||||
typ: 'file'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn (wsp &Workspace) list() ![]ListItem {
|
||||||
|
return wsp.list_dir('')
|
||||||
|
}
|
||||||
|
|
||||||
// Get the currently selected children (copy)
|
// Get the currently selected children (copy)
|
||||||
pub fn (wsp Workspace) selected_children() []HeropromptChild {
|
pub fn (wsp Workspace) selected_children() []HeropromptChild {
|
||||||
return wsp.children.clone()
|
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)
|
// build_file_content generates formatted content for all selected files (and all files under selected dirs)
|
||||||
fn (wsp Workspace) build_file_content() !string {
|
fn (wsp Workspace) build_file_content() !string {
|
||||||
mut content := ''
|
mut content := ''
|
||||||
@@ -295,7 +270,7 @@ fn (wsp Workspace) build_file_content() !string {
|
|||||||
}
|
}
|
||||||
// files under selected directories, using CodeWalker for filtered traversal
|
// files under selected directories, using CodeWalker for filtered traversal
|
||||||
for ch in wsp.children {
|
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 cw := codewalker.new(codewalker.CodeWalkerArgs{})!
|
||||||
mut fm := cw.filemap_get(path: ch.path.path)!
|
mut fm := cw.filemap_get(path: ch.path.path)!
|
||||||
for rel, fc in fm.content {
|
for rel, fc in fm.content {
|
||||||
@@ -316,64 +291,6 @@ fn (wsp Workspace) build_file_content() !string {
|
|||||||
return content
|
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 struct HeropromptTmpPrompt {
|
||||||
pub mut:
|
pub mut:
|
||||||
user_instructions string
|
user_instructions string
|
||||||
@@ -392,26 +309,40 @@ fn (wsp Workspace) build_file_map() string {
|
|||||||
mut roots := []HeropromptChild{}
|
mut roots := []HeropromptChild{}
|
||||||
mut files_only := []HeropromptChild{}
|
mut files_only := []HeropromptChild{}
|
||||||
for ch in wsp.children {
|
for ch in wsp.children {
|
||||||
if ch.path.cat == .dir {
|
if ch.path.cat == .dir && ch.include_tree {
|
||||||
roots << ch
|
roots << ch // only include directories explicitly marked to include subtree
|
||||||
} else if ch.path.cat == .file {
|
} else if ch.path.cat == .file {
|
||||||
files_only << ch
|
files_only << ch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if roots.len > 0 || files_only.len > 0 {
|
||||||
|
// derive a parent path for display
|
||||||
|
mut parent_path := ''
|
||||||
if roots.len > 0 {
|
if roots.len > 0 {
|
||||||
base_path := roots[0].path.path
|
base_path := roots[0].path.path
|
||||||
parent_path := if base_path.contains('/') {
|
parent_path = if base_path.contains('/') {
|
||||||
base_path.split('/')[..base_path.split('/').len - 1].join('/')
|
base_path.split('/')[..base_path.split('/').len - 1].join('/')
|
||||||
} else {
|
} else {
|
||||||
base_path
|
base_path
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 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
|
// metadata
|
||||||
mut total_files := 0
|
mut total_files := 0
|
||||||
mut total_content_length := 0
|
mut total_content_length := 0
|
||||||
mut file_extensions := map[string]int{}
|
mut file_extensions := map[string]int{}
|
||||||
// files under dirs
|
// files under dirs (only when roots present)
|
||||||
|
if roots.len > 0 {
|
||||||
for r in roots {
|
for r in roots {
|
||||||
for f in list_files_recursive(r.path.path) {
|
for f in codewalker.list_files_recursive(r.path.path) {
|
||||||
total_files++
|
total_files++
|
||||||
ext := get_file_extension(os.base(f))
|
ext := get_file_extension(os.base(f))
|
||||||
if ext.len > 0 {
|
if ext.len > 0 {
|
||||||
@@ -420,14 +351,21 @@ fn (wsp Workspace) build_file_map() string {
|
|||||||
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 {
|
for fo in files_only {
|
||||||
total_files++
|
total_files++
|
||||||
ext := get_file_extension(fo.name)
|
ext := get_file_extension(fo.name)
|
||||||
if ext.len > 0 {
|
if ext.len > 0 {
|
||||||
file_extensions[ext] = file_extensions[ext] + 1
|
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 := ''
|
mut extensions_summary := ''
|
||||||
for ext, count in file_extensions {
|
for ext, count in file_extensions {
|
||||||
@@ -442,12 +380,29 @@ fn (wsp Workspace) build_file_map() string {
|
|||||||
file_map += ' | Extensions: ${extensions_summary}'
|
file_map += ' | Extensions: ${extensions_summary}'
|
||||||
}
|
}
|
||||||
file_map += '\n\n'
|
file_map += '\n\n'
|
||||||
file_map += build_file_tree_fs(roots, '')
|
// Render selected structure
|
||||||
// list standalone files as well
|
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 {
|
for fo in files_only {
|
||||||
file_map += fo.path.path + ' *\n'
|
file_map += fo.path.path + ' *\n'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return file_map
|
return file_map
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,16 +425,15 @@ pub fn (wsp Workspace) prompt(args WorkspacePrompt) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save the workspace
|
// Save the workspace
|
||||||
fn (wsp &Workspace) save() !&Workspace {
|
fn (mut wsp Workspace) save() !&Workspace {
|
||||||
mut tmp := wsp
|
wsp.updated = time.now()
|
||||||
tmp.updated = time.now()
|
wsp.is_saved = true
|
||||||
tmp.is_saved = true
|
set(wsp)!
|
||||||
set(tmp)!
|
|
||||||
return get(name: wsp.name)!
|
return get(name: wsp.name)!
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a random name for the workspace
|
// Generate a random name for the workspace
|
||||||
fn generate_random_workspace_name() string {
|
pub fn generate_random_workspace_name() string {
|
||||||
adjectives := [
|
adjectives := [
|
||||||
'brave',
|
'brave',
|
||||||
'bright',
|
'bright',
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
# heroprompt
|
# heroprompt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
To get started
|
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
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.db.cometbft as cometbft_installer
|
import freeflowuniverse.herolib.installers.db.cometbft as cometbft_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.db.meilisearch as meilisearchinstaller
|
import freeflowuniverse.herolib.installers.db.meilisearch as meilisearchinstaller
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Is a powerfull db for embedding for AI Agents.
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.db.qdrant_installer
|
import freeflowuniverse.herolib.installers.db.qdrant_installer
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ coredns
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.infra.coredns as coredns_installer
|
import freeflowuniverse.herolib.installers.infra.coredns as coredns_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.infra.gitea as gitea_installer
|
import freeflowuniverse.herolib.installers.infra.gitea as gitea_installer
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.screen as screen_installer
|
import freeflowuniverse.herolib.installers.something.screen as screen_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something. zinit
|
import freeflowuniverse.herolib.installers.something. zinit
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.lang.golang
|
import freeflowuniverse.herolib.installers.lang.golang
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.nodejs as nodejs_installer
|
import freeflowuniverse.herolib.installers.something.nodejs as nodejs_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.python as python_installer
|
import freeflowuniverse.herolib.installers.something.python as python_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.rust as rust_installer
|
import freeflowuniverse.herolib.installers.something.rust as rust_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.wireguard as wireguard_installer
|
import freeflowuniverse.herolib.installers.something.wireguard as wireguard_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.yggdrasil as yggdrasil_installer
|
import freeflowuniverse.herolib.installers.something.yggdrasil as yggdrasil_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.b2 as b2_installer
|
import freeflowuniverse.herolib.installers.something.b2 as b2_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.fungistor as fungistor_installer
|
import freeflowuniverse.herolib.installers.something.fungistor as fungistor_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.grafana as grafana_installer
|
import freeflowuniverse.herolib.installers.something.grafana as grafana_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.restic as restic_installer
|
import freeflowuniverse.herolib.installers.something.restic as restic_installer
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
To use the installer:
|
To use the installer:
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
import freeflowuniverse.herolib.installers.threefold.griddriver
|
import freeflowuniverse.herolib.installers.threefold.griddriver
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something. cloudhypervisor
|
import freeflowuniverse.herolib.installers.something. cloudhypervisor
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.docker as docker_installer
|
import freeflowuniverse.herolib.installers.something.docker as docker_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.virt.pacman
|
import freeflowuniverse.herolib.installers.virt.pacman
|
||||||
|
|||||||
@@ -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.
|
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)
|
### Example Code (VLang)
|
||||||
```vlang
|
```v
|
||||||
import freeflowuniverse.herolib.installers.virt.podman as podman_installer
|
import freeflowuniverse.herolib.installers.virt.podman as podman_installer
|
||||||
|
|
||||||
mut podman := podman_installer.get()!
|
mut podman := podman_installer.get()!
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.virt.youki
|
import freeflowuniverse.herolib.installers.virt.youki
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.bun as bun_installer
|
import freeflowuniverse.herolib.installers.something.bun as bun_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.imagemagick as imagemagick_installer
|
import freeflowuniverse.herolib.installers.something.imagemagick as imagemagick_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,315 +0,0 @@
|
|||||||
module heroprompt
|
|
||||||
|
|
||||||
import veb
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import freeflowuniverse.herolib.develop.heroprompt as hp
|
|
||||||
|
|
||||||
// Types for directory listing
|
|
||||||
struct DirItem {
|
|
||||||
name string
|
|
||||||
typ string @[json: 'type']
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DirResp {
|
|
||||||
path string
|
|
||||||
mut:
|
|
||||||
items []DirItem
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTML routes
|
|
||||||
@['/heroprompt'; get]
|
|
||||||
pub fn (app &App) page_index(mut ctx Context) veb.Result {
|
|
||||||
return ctx.html(render_index(app))
|
|
||||||
}
|
|
||||||
|
|
||||||
// API routes (thin wrappers over develop.heroprompt)
|
|
||||||
@['/api/heroprompt/workspaces'; get]
|
|
||||||
pub fn (app &App) api_list(mut ctx Context) veb.Result {
|
|
||||||
mut names := []string{}
|
|
||||||
ws := hp.list(fromdb: true) or { []&hp.Workspace{} }
|
|
||||||
for w in ws {
|
|
||||||
names << w.name
|
|
||||||
}
|
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
return ctx.text(json.encode(names))
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/workspaces'; post]
|
|
||||||
pub fn (app &App) api_create(mut ctx Context) veb.Result {
|
|
||||||
name := ctx.form['name'] or { 'default' }
|
|
||||||
base_path_in := ctx.form['base_path'] or { '' }
|
|
||||||
if base_path_in.len == 0 {
|
|
||||||
return ctx.text('{"error":"base_path required"}')
|
|
||||||
}
|
|
||||||
mut base_path := base_path_in
|
|
||||||
// Expand tilde to user home
|
|
||||||
if base_path.starts_with('~') {
|
|
||||||
home := os.home_dir()
|
|
||||||
base_path = os.join_path(home, base_path.all_after('~'))
|
|
||||||
}
|
|
||||||
_ := hp.get(name: name, create: true, path: base_path) or {
|
|
||||||
return ctx.text('{"error":"create failed"}')
|
|
||||||
}
|
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
return ctx.text(json.encode({
|
|
||||||
'name': name
|
|
||||||
'base_path': base_path
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/directory'; get]
|
|
||||||
pub fn (app &App) api_directory(mut ctx Context) veb.Result {
|
|
||||||
wsname := ctx.query['name'] or { 'default' }
|
|
||||||
path_q := ctx.query['path'] or { '' }
|
|
||||||
mut wsp := hp.get(name: wsname, create: false) or {
|
|
||||||
return ctx.text('{"error":"workspace not found"}')
|
|
||||||
}
|
|
||||||
// Use workspace list method; empty path means base_path
|
|
||||||
items_w := if path_q.len > 0 { wsp.list() or {
|
|
||||||
return ctx.text('{"error":"cannot list directory"}')} } else { wsp.list() or {
|
|
||||||
return ctx.text('{"error":"cannot list directory"}')} }
|
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
mut resp := DirResp{
|
|
||||||
path: if path_q.len > 0 { path_q } else { wsp.base_path }
|
|
||||||
}
|
|
||||||
for it in items_w {
|
|
||||||
resp.items << DirItem{
|
|
||||||
name: it.name
|
|
||||||
typ: it.typ
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctx.text(json.encode(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- File content endpoint --------
|
|
||||||
struct FileResp {
|
|
||||||
language string
|
|
||||||
content string
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/file'; get]
|
|
||||||
pub fn (app &App) api_file(mut ctx Context) veb.Result {
|
|
||||||
wsname := ctx.query['name'] or { 'default' }
|
|
||||||
path_q := ctx.query['path'] or { '' }
|
|
||||||
if path_q.len == 0 {
|
|
||||||
return ctx.text('{"error":"path required"}')
|
|
||||||
}
|
|
||||||
mut base := ''
|
|
||||||
if wsp := hp.get(name: wsname, create: false) {
|
|
||||||
base = wsp.base_path
|
|
||||||
}
|
|
||||||
mut file_path := if !os.is_abs_path(path_q) && base.len > 0 {
|
|
||||||
os.join_path(base, path_q)
|
|
||||||
} else {
|
|
||||||
path_q
|
|
||||||
}
|
|
||||||
if !os.is_file(file_path) {
|
|
||||||
return ctx.text('{"error":"not a file"}')
|
|
||||||
}
|
|
||||||
// limit read to 1MB to avoid huge responses
|
|
||||||
max_size := i64(1_000_000)
|
|
||||||
sz := os.file_size(file_path)
|
|
||||||
if sz > max_size {
|
|
||||||
return ctx.text('{"error":"file too large"}')
|
|
||||||
}
|
|
||||||
content := os.read_file(file_path) or { return ctx.text('{"error":"failed to read"}') }
|
|
||||||
lang := detect_lang(file_path)
|
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
return ctx.text(json.encode(FileResp{ language: lang, content: content }))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn detect_lang(path string) string {
|
|
||||||
ext := os.file_ext(path).trim_left('.')
|
|
||||||
return match ext.to_lower() {
|
|
||||||
'v' { 'v' }
|
|
||||||
'js' { 'javascript' }
|
|
||||||
'ts' { 'typescript' }
|
|
||||||
'py' { 'python' }
|
|
||||||
'rs' { 'rust' }
|
|
||||||
'go' { 'go' }
|
|
||||||
'java' { 'java' }
|
|
||||||
'c', 'h' { 'c' }
|
|
||||||
'cpp', 'hpp', 'cc', 'hh' { 'cpp' }
|
|
||||||
'sh', 'bash' { 'bash' }
|
|
||||||
'json' { 'json' }
|
|
||||||
'yaml', 'yml' { 'yaml' }
|
|
||||||
'html', 'htm' { 'html' }
|
|
||||||
'css' { 'css' }
|
|
||||||
'md' { 'markdown' }
|
|
||||||
else { 'text' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- Filename search endpoint --------
|
|
||||||
struct SearchItem {
|
|
||||||
path string
|
|
||||||
typ string @[json: 'type']
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/search'; get]
|
|
||||||
pub fn (app &App) api_search(mut ctx Context) veb.Result {
|
|
||||||
wsname := ctx.query['name'] or { 'default' }
|
|
||||||
q := ctx.query['q'] or { '' }
|
|
||||||
if q.len == 0 {
|
|
||||||
return ctx.text('{"error":"q required"}')
|
|
||||||
}
|
|
||||||
mut base := ''
|
|
||||||
if wsp := hp.get(name: wsname, create: false) {
|
|
||||||
base = wsp.base_path
|
|
||||||
}
|
|
||||||
if base.len == 0 {
|
|
||||||
return ctx.text('{"error":"workspace base_path not set"}')
|
|
||||||
}
|
|
||||||
max := (ctx.query['max'] or { '200' }).int()
|
|
||||||
mut results := []SearchItem{}
|
|
||||||
walk_search(base, q, max, mut results)
|
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
return ctx.text(json.encode(results))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workspace details
|
|
||||||
@['/api/heroprompt/workspaces/:name'; get]
|
|
||||||
pub fn (app &App) api_workspace_get(mut ctx Context, name string) veb.Result {
|
|
||||||
wsp := hp.get(name: name, create: false) or {
|
|
||||||
return ctx.text('{"error":"workspace not found"}')
|
|
||||||
}
|
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
return ctx.text(json.encode({
|
|
||||||
'name': wsp.name
|
|
||||||
'base_path': wsp.base_path
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/workspaces/:name'; delete]
|
|
||||||
pub fn (app &App) api_workspace_delete(mut ctx Context, name string) veb.Result {
|
|
||||||
wsp := hp.get(name: name, create: false) or {
|
|
||||||
return ctx.text('{"error":"workspace not found"}')
|
|
||||||
}
|
|
||||||
wsp.delete_workspace() or { return ctx.text('{"error":"delete failed"}') }
|
|
||||||
return ctx.text('{"ok":true}')
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/workspaces/:name'; patch]
|
|
||||||
pub fn (app &App) api_workspace_patch(mut ctx Context, name string) veb.Result {
|
|
||||||
wsp := hp.get(name: name, create: false) or {
|
|
||||||
return ctx.text('{"error":"workspace not found"}')
|
|
||||||
}
|
|
||||||
new_name := ctx.form['name'] or { '' }
|
|
||||||
mut base_path := ctx.form['base_path'] or { '' }
|
|
||||||
if base_path.len > 0 && base_path.starts_with('~') {
|
|
||||||
home := os.home_dir()
|
|
||||||
base_path = os.join_path(home, base_path.all_after('~'))
|
|
||||||
}
|
|
||||||
updated := wsp.update_workspace(name: new_name, base_path: base_path) or {
|
|
||||||
return ctx.text('{"error":"update failed"}')
|
|
||||||
}
|
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
return ctx.text(json.encode({
|
|
||||||
'name': updated.name
|
|
||||||
'base_path': updated.base_path
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- Path validation endpoint --------
|
|
||||||
struct PathValidationResp {
|
|
||||||
is_abs bool
|
|
||||||
exists bool
|
|
||||||
is_dir bool
|
|
||||||
expanded string
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/validate_path'; get]
|
|
||||||
pub fn (app &App) api_validate_path(mut ctx Context) veb.Result {
|
|
||||||
p_in := ctx.query['path'] or { '' }
|
|
||||||
mut p := p_in
|
|
||||||
if p.starts_with('~') {
|
|
||||||
home := os.home_dir()
|
|
||||||
p = os.join_path(home, p.all_after('~'))
|
|
||||||
}
|
|
||||||
is_abs := if p != '' { os.is_abs_path(p) } else { false }
|
|
||||||
exists := if p != '' { os.exists(p) } else { false }
|
|
||||||
isdir := if exists { os.is_dir(p) } else { false }
|
|
||||||
ctx.set_content_type('application/json')
|
|
||||||
resp := PathValidationResp{
|
|
||||||
is_abs: is_abs
|
|
||||||
exists: exists
|
|
||||||
is_dir: isdir
|
|
||||||
expanded: p
|
|
||||||
}
|
|
||||||
return ctx.text(json.encode(resp))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn walk_search(root string, q string, max int, mut out []SearchItem) {
|
|
||||||
if out.len >= max {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
entries := os.ls(root) or { return }
|
|
||||||
for e in entries {
|
|
||||||
if e in ['.git', 'node_modules', 'build', 'dist', '.v'] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
p := os.join_path(root, e)
|
|
||||||
if os.is_dir(p) {
|
|
||||||
if out.len >= max {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if e.to_lower().contains(q.to_lower()) {
|
|
||||||
out << SearchItem{
|
|
||||||
path: p
|
|
||||||
typ: 'directory'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
walk_search(p, q, max, mut out)
|
|
||||||
} else if os.is_file(p) {
|
|
||||||
if e.to_lower().contains(q.to_lower()) {
|
|
||||||
out << SearchItem{
|
|
||||||
path: p
|
|
||||||
typ: 'file'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if out.len >= max {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------- Selection and prompt endpoints --------
|
|
||||||
@['/api/heroprompt/workspaces/:name/files'; post]
|
|
||||||
pub fn (app &App) api_add_file(mut ctx Context, name string) veb.Result {
|
|
||||||
path := ctx.form['path'] or { '' }
|
|
||||||
if path.len == 0 {
|
|
||||||
return ctx.text('{"error":"path required"}')
|
|
||||||
}
|
|
||||||
mut wsp := hp.get(name: name, create: false) or {
|
|
||||||
return ctx.text('{"error":"workspace not found"}')
|
|
||||||
}
|
|
||||||
wsp.add_file(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') }
|
|
||||||
return ctx.text('{"ok":true}')
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/workspaces/:name/dirs'; post]
|
|
||||||
pub fn (app &App) api_add_dir(mut ctx Context, name string) veb.Result {
|
|
||||||
path := ctx.form['path'] or { '' }
|
|
||||||
if path.len == 0 {
|
|
||||||
return ctx.text('{"error":"path required"}')
|
|
||||||
}
|
|
||||||
mut wsp := hp.get(name: name, create: false) or {
|
|
||||||
return ctx.text('{"error":"workspace not found"}')
|
|
||||||
}
|
|
||||||
wsp.add_dir(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') }
|
|
||||||
return ctx.text('{"ok":true}')
|
|
||||||
}
|
|
||||||
|
|
||||||
@['/api/heroprompt/workspaces/:name/prompt'; post]
|
|
||||||
pub fn (app &App) api_generate_prompt(mut ctx Context, name string) veb.Result {
|
|
||||||
text := ctx.form['text'] or { '' }
|
|
||||||
mut wsp := hp.get(name: name, create: false) or {
|
|
||||||
return ctx.text('{"error":"workspace not found"}')
|
|
||||||
}
|
|
||||||
prompt := wsp.prompt(text: text)
|
|
||||||
ctx.set_content_type('text/plain')
|
|
||||||
return ctx.text(prompt)
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
module heroprompt
|
|
||||||
|
|
||||||
import veb
|
|
||||||
import os
|
|
||||||
|
|
||||||
// Public Context type for veb
|
|
||||||
pub struct Context {
|
|
||||||
veb.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// Factory args for starting the server
|
|
||||||
@[params]
|
|
||||||
pub struct FactoryArgs {
|
|
||||||
pub mut:
|
|
||||||
host string = 'localhost'
|
|
||||||
port int = 8090
|
|
||||||
title string = 'Heroprompt'
|
|
||||||
}
|
|
||||||
|
|
||||||
// App holds server state and config
|
|
||||||
pub struct App {
|
|
||||||
veb.StaticHandler
|
|
||||||
pub mut:
|
|
||||||
title string
|
|
||||||
port int
|
|
||||||
base_path string // absolute path to this module directory
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new App instance (does not start the server)
|
|
||||||
pub fn new(args FactoryArgs) !&App {
|
|
||||||
base := os.dir(@FILE)
|
|
||||||
mut app := App{
|
|
||||||
title: args.title
|
|
||||||
port: args.port
|
|
||||||
base_path: base
|
|
||||||
}
|
|
||||||
// Serve static assets from this module at /static
|
|
||||||
app.mount_static_folder_at(os.join_path(base, 'static'), '/static')!
|
|
||||||
return &app
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the webserver (blocking)
|
|
||||||
pub fn start(args FactoryArgs) ! {
|
|
||||||
mut app := new(args)!
|
|
||||||
veb.run[App, Context](mut app, app.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
|
|
||||||
@['/'; get]
|
|
||||||
pub fn (app &App) index(mut ctx Context) veb.Result {
|
|
||||||
return ctx.html(render_index(app))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rendering helpers
|
|
||||||
fn render_index(app &App) string {
|
|
||||||
tpl := os.join_path(app.base_path, 'templates', 'index.html')
|
|
||||||
content := os.read_file(tpl) or { return render_index_fallback(app) }
|
|
||||||
return render_template(content, {
|
|
||||||
'title': app.title
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_index_fallback(app &App) string {
|
|
||||||
return
|
|
||||||
'<!doctype html>\n<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>' +
|
|
||||||
html_escape(app.title) +
|
|
||||||
'</title><link rel="stylesheet" href="/static/css/main.css"></head><body><div class="container"><h1>' +
|
|
||||||
html_escape(app.title) +
|
|
||||||
'</h1><p>Heroprompt server is running.</p></div><script src="/static/js/main.js"></script></body></html>'
|
|
||||||
}
|
|
||||||
@@ -1,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
|
|
||||||
}
|
|
||||||
@@ -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 = `<li class="err">${r.error}</li>`; 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 = '<div>Search results:</div>';
|
|
||||||
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 = ''; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>{{.title}} - Heroprompt</title>
|
|
||||||
<link rel="stylesheet" href="/static/css/main.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="toolbar">
|
|
||||||
<div class="title">{{.title}}</div>
|
|
||||||
<span class="spacer"></span>
|
|
||||||
<button id="toggleTheme" title="Toggle dark/light">🌓</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tabs">
|
|
||||||
<button class="tab active" data-tab="compose">Compose</button>
|
|
||||||
<button class="tab" data-tab="chat">Chat</button>
|
|
||||||
</div>
|
|
||||||
<div class="tab-content">
|
|
||||||
<section id="tab-compose" class="tab-pane active">
|
|
||||||
<div class="layout">
|
|
||||||
<div class="sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<select id="workspaceSelect"></select>
|
|
||||||
<button id="refreshWs">Refresh</button>
|
|
||||||
<button id="wsCreateBtn" title="Create a new workspace">+</button>
|
|
||||||
<button id="wsDetailsBtn" title="Workspace details">Details</button>
|
|
||||||
<span class="spacer"></span>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-body">
|
|
||||||
<div class="searchbar">
|
|
||||||
<input id="search" placeholder="Search files..." />
|
|
||||||
<div class="ws-info" id="wsInfo"></div>
|
|
||||||
<button id="doSearch">Search</button>
|
|
||||||
</div>
|
|
||||||
<div id="tree"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="main">
|
|
||||||
<div class="prompt">
|
|
||||||
<h3>Prompt</h3>
|
|
||||||
<textarea id="promptText" placeholder="Type your instructions here..."></textarea>
|
|
||||||
<div class="actions"><button id="copyPrompt">Copy Prompt</button></div>
|
|
||||||
</div>
|
|
||||||
<div class="selection">
|
|
||||||
<h3>Selected Files (<span id="selCount">0</span>) — Tokens: <span id="tokenCount">0</span></h3>
|
|
||||||
<ul id="selected"></ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="tab-chat" class="tab-pane">
|
|
||||||
<div class="chat">
|
|
||||||
<div id="chatMessages" class="messages"></div>
|
|
||||||
<div class="chat-input">
|
|
||||||
<textarea id="chatInput" placeholder="Type a message..."></textarea>
|
|
||||||
<button id="sendChat">Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="/static/js/main.js" defer></script>
|
|
||||||
<!-- Modal: Create Workspace -->
|
|
||||||
<div id="wsCreate" class="modal" aria-hidden="true">
|
|
||||||
<div class="modal-backdrop"></div>
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h4>Create workspace</h4>
|
|
||||||
<button id="wcClose" class="icon">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<label for="wcName">Workspace name (optional)</label>
|
|
||||||
<input id="wcName" placeholder="leave blank to auto-generate" />
|
|
||||||
<small class="hint">If blank we will generate one for you</small>
|
|
||||||
<label for="wcPath">Workspace path (required)</label>
|
|
||||||
<input id="wcPath" placeholder="/full/path e.g. ~/code/github/project1" />
|
|
||||||
<div id="wcError" class="error" role="alert"></div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="wcCancel">Cancel</button>
|
|
||||||
<button id="wcCreate">Create</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Modal: Workspace Details -->
|
|
||||||
<div id="wsDetails" class="modal" aria-hidden="true">
|
|
||||||
<div class="modal-backdrop"></div>
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h4>Workspace details</h4>
|
|
||||||
<button id="wdClose" class="icon">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<label for="wdName">Name</label>
|
|
||||||
<input id="wdName" />
|
|
||||||
<label for="wdPath">Path</label>
|
|
||||||
<input id="wdPath" />
|
|
||||||
<div id="wdError" class="error" role="alert"></div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="wdDelete">Delete</button>
|
|
||||||
<span class="spacer"></span>
|
|
||||||
<button id="wdCancel">Close</button>
|
|
||||||
<button id="wdSave">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
module heroprompt
|
|
||||||
|
|
||||||
import strings
|
|
||||||
|
|
||||||
// Very small template renderer using {{.var}} replacement
|
|
||||||
pub fn render_template(tpl string, data map[string]string) string {
|
|
||||||
mut out := tpl
|
|
||||||
for k, v in data {
|
|
||||||
out = out.replace('{{.' + k + '}}', v)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimal HTML escape
|
|
||||||
pub fn html_escape(s string) string {
|
|
||||||
mut b := strings.new_builder(s.len)
|
|
||||||
for ch in s {
|
|
||||||
match ch {
|
|
||||||
`&` { b.write_string('&') }
|
|
||||||
`<` { b.write_string('<') }
|
|
||||||
`>` { b.write_string('>') }
|
|
||||||
`"` { b.write_string('"') }
|
|
||||||
`'` { b.write_string(''') }
|
|
||||||
else { b.write_string(ch.str()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return b.str()
|
|
||||||
}
|
|
||||||
25
lib/web/ui/README.md
Normal file
25
lib/web/ui/README.md
Normal file
@@ -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
|
||||||
20
lib/web/ui/chat_endpoints.v
Normal file
20
lib/web/ui/chat_endpoints.v
Normal file
@@ -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('</body>', '<div class="v-badge">Rendered by: chat</div></body>')
|
||||||
|
return result
|
||||||
|
}
|
||||||
3
lib/web/ui/chat_utils.v
Normal file
3
lib/web/ui/chat_utils.v
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module ui
|
||||||
|
|
||||||
|
// Placeholder for chat-specific utilities
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
module ui
|
|
||||||
|
|
||||||
// import veb
|
|
||||||
// import freeflowuniverse.herolib.develop.heroprompt
|
|
||||||
// import os
|
|
||||||
// import json
|
|
||||||
|
|
||||||
// // Directory browsing and file read endpoints for Heroprompt.js compatibility
|
|
||||||
// struct DirItem {
|
|
||||||
// name string
|
|
||||||
// typ string @[json: 'type']
|
|
||||||
// }
|
|
||||||
|
|
||||||
// struct DirResp {
|
|
||||||
// path string
|
|
||||||
// mut:
|
|
||||||
// items []DirItem
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @['/api/heroprompt/directory'; get]
|
|
||||||
// pub fn (app &App) api_heroprompt_directory(mut ctx Context) veb.Result {
|
|
||||||
// // Optional workspace name, defaults to 'default'
|
|
||||||
// wsname := ctx.query['name'] or { 'default' }
|
|
||||||
// path_q := ctx.query['path'] or { '' }
|
|
||||||
// if path_q.len == 0 {
|
|
||||||
// return ctx.text('{"error":"path required"}')
|
|
||||||
// }
|
|
||||||
// // Try to resolve against workspace base_path if available, but do not require it
|
|
||||||
// mut base := ''
|
|
||||||
// if wsp := heroprompt.get(name: wsname, create: false) {
|
|
||||||
// base = wsp.base_path
|
|
||||||
// }
|
|
||||||
// // Resolve path: if absolute, use as-is; else join with base
|
|
||||||
// mut dir_path := path_q
|
|
||||||
// if !os.is_abs_path(dir_path) && base.len > 0 {
|
|
||||||
// dir_path = os.join_path(base, dir_path)
|
|
||||||
// }
|
|
||||||
// // List entries
|
|
||||||
// entries := os.ls(dir_path) or { return ctx.text('{"error":"cannot list directory"}') }
|
|
||||||
// mut items := []map[string]string{}
|
|
||||||
// for e in entries {
|
|
||||||
// full := os.join_path(dir_path, e)
|
|
||||||
// if os.is_dir(full) {
|
|
||||||
// items << {
|
|
||||||
// 'name': e
|
|
||||||
// 'type': 'directory'
|
|
||||||
// }
|
|
||||||
// } else if os.is_file(full) {
|
|
||||||
// items << {
|
|
||||||
// 'name': e
|
|
||||||
// 'type': 'file'
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ctx.set_content_type('application/json')
|
|
||||||
// // Encode strongly typed JSON response
|
|
||||||
// mut resp := DirResp{
|
|
||||||
// path: dir_path
|
|
||||||
// }
|
|
||||||
// for it in items {
|
|
||||||
// resp.items << DirItem{
|
|
||||||
// name: it['name'] or { '' }
|
|
||||||
// typ: it['type'] or { '' }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return ctx.text(json.encode(resp))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @['/api/heroprompt/file'; get]
|
|
||||||
// pub fn (app &App) api_heroprompt_file(mut ctx Context) veb.Result {
|
|
||||||
// wsname := ctx.query['name'] or { 'default' }
|
|
||||||
// path_q := ctx.query['path'] or { '' }
|
|
||||||
// if path_q.len == 0 {
|
|
||||||
// return ctx.text('{"error":"path required"}')
|
|
||||||
// }
|
|
||||||
// // Try to resolve against workspace base_path if available, but do not require it
|
|
||||||
// mut base := ''
|
|
||||||
// if wsp := heroprompt.get(name: wsname, create: false) {
|
|
||||||
// base = wsp.base_path
|
|
||||||
// }
|
|
||||||
// mut file_path := path_q
|
|
||||||
// if !os.is_abs_path(file_path) && base.len > 0 {
|
|
||||||
// file_path = os.join_path(base, file_path)
|
|
||||||
// }
|
|
||||||
// content := os.read_file(file_path) or { return ctx.text('{"error":"failed to read"}') }
|
|
||||||
// lang := detect_lang(file_path)
|
|
||||||
// ctx.set_content_type('application/json')
|
|
||||||
// return ctx.text(json.encode({
|
|
||||||
// 'language': lang
|
|
||||||
// 'content': content
|
|
||||||
// }))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn detect_lang(path string) string {
|
|
||||||
// ext := os.file_ext(path).trim_left('.')
|
|
||||||
// return match ext.to_lower() {
|
|
||||||
// 'v' { 'v' }
|
|
||||||
// 'js' { 'javascript' }
|
|
||||||
// 'ts' { 'typescript' }
|
|
||||||
// 'py' { 'python' }
|
|
||||||
// 'rs' { 'rust' }
|
|
||||||
// 'go' { 'go' }
|
|
||||||
// 'java' { 'java' }
|
|
||||||
// 'c', 'h' { 'c' }
|
|
||||||
// 'cpp', 'hpp', 'cc', 'hh' { 'cpp' }
|
|
||||||
// 'sh', 'bash' { 'bash' }
|
|
||||||
// 'json' { 'json' }
|
|
||||||
// 'yaml', 'yml' { 'yaml' }
|
|
||||||
// 'html', 'htm' { 'html' }
|
|
||||||
// 'css' { 'css' }
|
|
||||||
// 'md' { 'markdown' }
|
|
||||||
// else { 'text' }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heroprompt API: list workspaces
|
|
||||||
// @['/api/heroprompt/workspaces'; get]
|
|
||||||
// pub fn (app &App) api_heroprompt_list(mut ctx Context) veb.Result {
|
|
||||||
// mut names := []string{}
|
|
||||||
// ws := heroprompt.list(fromdb: true) or { []&heroprompt.Workspace{} }
|
|
||||||
// for w in ws {
|
|
||||||
// names << w.name
|
|
||||||
// }
|
|
||||||
// ctx.set_content_type('application/json')
|
|
||||||
// return ctx.text(json.encode(names))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heroprompt API: create/get workspace
|
|
||||||
// @['/api/heroprompt/workspaces'; post]
|
|
||||||
// pub fn (app &App) api_heroprompt_create(mut ctx Context) veb.Result {
|
|
||||||
// name := ctx.form['name'] or { '' }
|
|
||||||
// base_path := ctx.form['base_path'] or { '' }
|
|
||||||
|
|
||||||
// if base_path.len == 0 {
|
|
||||||
// return ctx.text('{"error":"base_path required"}')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// mut wsp := heroprompt.get(name: name, create: true, path: base_path) or {
|
|
||||||
// return ctx.text('{"error":"create failed"}')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ctx.set_content_type('application/json')
|
|
||||||
// return ctx.text(json.encode({
|
|
||||||
// 'name': name
|
|
||||||
// 'base_path': base_path
|
|
||||||
// }))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heroprompt API: add directory to workspace
|
|
||||||
// @['/api/heroprompt/workspaces/:name/dirs'; post]
|
|
||||||
// pub fn (app &App) api_heroprompt_add_dir(mut ctx Context, name string) veb.Result {
|
|
||||||
// path := ctx.form['path'] or { '' }
|
|
||||||
// if path.len == 0 {
|
|
||||||
// return ctx.text('{"error":"path required"}')
|
|
||||||
// }
|
|
||||||
// mut wsp := heroprompt.get(name: name, create: true) or {
|
|
||||||
// return ctx.text('{"error":"workspace not found"}')
|
|
||||||
// }
|
|
||||||
// wsp.add_dir(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') }
|
|
||||||
// ctx.set_content_type('application/json')
|
|
||||||
// return ctx.text('{"ok":true}')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heroprompt API: add file to workspace
|
|
||||||
// @['/api/heroprompt/workspaces/:name/files'; post]
|
|
||||||
// pub fn (app &App) api_heroprompt_add_file(mut ctx Context, name string) veb.Result {
|
|
||||||
// path := ctx.form['path'] or { '' }
|
|
||||||
// if path.len == 0 {
|
|
||||||
// return ctx.text('{"error":"path required"}')
|
|
||||||
// }
|
|
||||||
// mut wsp := heroprompt.get(name: name, create: true) or {
|
|
||||||
// return ctx.text('{"error":"workspace not found"}')
|
|
||||||
// }
|
|
||||||
// wsp.add_file(path: path) or { return ctx.text('{"error":"' + err.msg() + '"}') }
|
|
||||||
// ctx.set_content_type('application/json')
|
|
||||||
// return ctx.text('{"ok":true}')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heroprompt API: generate prompt
|
|
||||||
// @['/api/heroprompt/workspaces/:name/prompt'; post]
|
|
||||||
// pub fn (app &App) api_heroprompt_prompt(mut ctx Context, name string) veb.Result {
|
|
||||||
// text := ctx.form['text'] or { '' }
|
|
||||||
// mut wsp := heroprompt.get(name: name, create: false) or {
|
|
||||||
// return ctx.text('{"error":"workspace not found"}')
|
|
||||||
// }
|
|
||||||
// prompt := wsp.prompt(text: text)
|
|
||||||
// ctx.set_content_type('text/plain')
|
|
||||||
// return ctx.text(prompt)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heroprompt API: get workspace details
|
|
||||||
// @['/api/heroprompt/workspaces/:name'; get]
|
|
||||||
// pub fn (app &App) api_heroprompt_get(mut ctx Context, name string) veb.Result {
|
|
||||||
// wsp := heroprompt.get(name: name, create: false) or {
|
|
||||||
// return ctx.text('{"error":"workspace not found"}')
|
|
||||||
// }
|
|
||||||
// mut children := []map[string]string{}
|
|
||||||
// for ch in wsp.children {
|
|
||||||
// children << {
|
|
||||||
// 'name': ch.name
|
|
||||||
// 'path': ch.path.path
|
|
||||||
// 'type': if ch.path.cat == .dir { 'directory' } else { 'file' }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ctx.set_content_type('application/json')
|
|
||||||
// return ctx.text(json.encode({
|
|
||||||
// 'name': wsp.name
|
|
||||||
// 'base_path': wsp.base_path
|
|
||||||
// 'children': json.encode(children)
|
|
||||||
// }))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heroprompt API: delete workspace
|
|
||||||
// @['/api/heroprompt/workspaces/:name'; delete]
|
|
||||||
// pub fn (app &App) api_heroprompt_delete(mut ctx Context, name string) veb.Result {
|
|
||||||
// wsp := heroprompt.get(name: name, create: false) or {
|
|
||||||
// return ctx.text('{"error":"workspace not found"}')
|
|
||||||
// }
|
|
||||||
// wsp.delete_workspace() or { return ctx.text('{"error":"delete failed"}') }
|
|
||||||
// ctx.set_content_type('application/json')
|
|
||||||
// return ctx.text('{"ok":true}')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heroprompt API: remove directory
|
|
||||||
// @['/api/heroprompt/workspaces/:name/dirs/remove'; post]
|
|
||||||
// pub fn (app &App) api_heroprompt_remove_dir(mut ctx Context, name string) veb.Result {
|
|
||||||
// path := ctx.form['path'] or { '' }
|
|
||||||
// mut wsp := heroprompt.get(name: name, create: false) or {
|
|
||||||
// return ctx.text('{"error":"workspace not found"}')
|
|
||||||
// }
|
|
||||||
// wsp.remove_dir(path: path, name: '') or { return ctx.text('{"error":"' + err.msg() + '"}') }
|
|
||||||
// return ctx.text('{"ok":true}')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Heroprompt API: remove file
|
|
||||||
// @['/api/heroprompt/workspaces/:name/files/remove'; post]
|
|
||||||
// pub fn (app &App) api_heroprompt_remove_file(mut ctx Context, name string) veb.Result {
|
|
||||||
// path := ctx.form['path'] or { '' }
|
|
||||||
// mut wsp := heroprompt.get(name: name, create: false) or {
|
|
||||||
// return ctx.text('{"error":"workspace not found"}')
|
|
||||||
// }
|
|
||||||
// wsp.remove_file(path: path, name: '') or { return ctx.text('{"error":"' + err.msg() + '"}') }
|
|
||||||
// return ctx.text('{"ok":true}')
|
|
||||||
// }
|
|
||||||
@@ -1,471 +0,0 @@
|
|||||||
module ui
|
|
||||||
|
|
||||||
// import veb
|
|
||||||
// import os
|
|
||||||
|
|
||||||
// // Public Context type for veb
|
|
||||||
// pub struct Context {
|
|
||||||
// veb.Context
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Simple tree menu structure
|
|
||||||
// pub struct MenuItem {
|
|
||||||
// pub:
|
|
||||||
// title string
|
|
||||||
// href string
|
|
||||||
// children []MenuItem
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Factory args
|
|
||||||
// @[params]
|
|
||||||
// pub struct WebArgs {
|
|
||||||
// pub mut:
|
|
||||||
// name string = 'default'
|
|
||||||
// host string = 'localhost'
|
|
||||||
// port int = 8080
|
|
||||||
// title string = 'Admin'
|
|
||||||
// menu []MenuItem
|
|
||||||
// open bool
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // The App holds server state and config
|
|
||||||
// pub struct App {
|
|
||||||
// veb.StaticHandler
|
|
||||||
// pub mut:
|
|
||||||
// title string = 'default'
|
|
||||||
// menu []MenuItem
|
|
||||||
// port int = 7711
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Start the webserver (blocking)
|
|
||||||
// pub fn start(args WebArgs) ! {
|
|
||||||
// mut app := App{
|
|
||||||
// title: args.title
|
|
||||||
// menu: args.menu
|
|
||||||
// port: args.port
|
|
||||||
// }
|
|
||||||
// veb.run[App, Context](mut app, app.port)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Routes
|
|
||||||
|
|
||||||
// // Redirect root to /admin
|
|
||||||
// @['/'; get]
|
|
||||||
// pub fn (app &App) root(mut ctx Context) veb.Result {
|
|
||||||
// return ctx.redirect('/admin')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Admin home page
|
|
||||||
// @['/admin'; get]
|
|
||||||
// pub fn (app &App) admin_index(mut ctx Context) veb.Result {
|
|
||||||
// return ctx.html(app.render_admin('/', 'Welcome'))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // HeroScript editor page
|
|
||||||
// @['/admin/heroscript'; get]
|
|
||||||
// pub fn (app &App) admin_heroscript(mut ctx Context) veb.Result {
|
|
||||||
// return ctx.html(app.render_heroscript())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Chat page
|
|
||||||
// @['/admin/chat'; get]
|
|
||||||
// pub fn (app &App) admin_chat(mut ctx Context) veb.Result {
|
|
||||||
// return ctx.html(app.render_chat())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Static CSS files
|
|
||||||
// @['/static/css/colors.css'; get]
|
|
||||||
// pub fn (app &App) serve_colors_css(mut ctx Context) veb.Result {
|
|
||||||
// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'colors.css')
|
|
||||||
// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
|
|
||||||
// ctx.set_content_type('text/css')
|
|
||||||
// return ctx.text(css_content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @['/static/css/main.css'; get]
|
|
||||||
// pub fn (app &App) serve_main_css(mut ctx Context) veb.Result {
|
|
||||||
// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'main.css')
|
|
||||||
// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
|
|
||||||
// ctx.set_content_type('text/css')
|
|
||||||
// return ctx.text(css_content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Static JS files
|
|
||||||
// @['/static/js/theme.js'; get]
|
|
||||||
// pub fn (app &App) serve_theme_js(mut ctx Context) veb.Result {
|
|
||||||
// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'theme.js')
|
|
||||||
// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
|
|
||||||
// ctx.set_content_type('application/javascript')
|
|
||||||
// return ctx.text(js_content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @['/static/js/heroscript.js'; get]
|
|
||||||
// pub fn (app &App) serve_heroscript_js(mut ctx Context) veb.Result {
|
|
||||||
// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'heroscript.js')
|
|
||||||
// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
|
|
||||||
// ctx.set_content_type('application/javascript')
|
|
||||||
// return ctx.text(js_content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @['/static/js/chat.js'; get]
|
|
||||||
// pub fn (app &App) serve_chat_js(mut ctx Context) veb.Result {
|
|
||||||
// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'chat.js')
|
|
||||||
// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
|
|
||||||
// ctx.set_content_type('application/javascript')
|
|
||||||
// return ctx.text(js_content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @['/static/css/heroscript.css'; get]
|
|
||||||
// pub fn (app &App) serve_heroscript_css(mut ctx Context) veb.Result {
|
|
||||||
// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'heroscript.css')
|
|
||||||
// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
|
|
||||||
// ctx.set_content_type('text/css')
|
|
||||||
// return ctx.text(css_content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @['/static/css/chat.css'; get]
|
|
||||||
// pub fn (app &App) serve_chat_css(mut ctx Context) veb.Result {
|
|
||||||
// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'chat.css')
|
|
||||||
// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
|
|
||||||
// ctx.set_content_type('text/css')
|
|
||||||
// return ctx.text(css_content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Catch-all content under /admin/*
|
|
||||||
// @['/admin/:path...'; get]
|
|
||||||
// pub fn (app &App) admin_section(mut ctx Context, path string) veb.Result {
|
|
||||||
// // Render current path in the main content
|
|
||||||
// return ctx.html(app.render_admin(path, 'Content'))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // View rendering using external template
|
|
||||||
|
|
||||||
// fn (app &App) render_admin(path string, heading string) string {
|
|
||||||
// // Get the template file path relative to the module
|
|
||||||
// template_path := os.join_path(os.dir(@FILE), 'templates', 'admin_layout.html')
|
|
||||||
|
|
||||||
// // Read the template file
|
|
||||||
// template_content := os.read_file(template_path) or {
|
|
||||||
// // Fallback to inline template if file not found
|
|
||||||
// return app.render_admin_fallback(path, heading)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Generate menu HTML
|
|
||||||
// menu_content := menu_html(app.menu, 0, 'm')
|
|
||||||
|
|
||||||
// // Simple template variable replacement
|
|
||||||
// mut result := template_content
|
|
||||||
// result = result.replace('{{.title}}', app.title)
|
|
||||||
// result = result.replace('{{.heading}}', heading)
|
|
||||||
// result = result.replace('{{.path}}', path)
|
|
||||||
// result = result.replace('{{.menu_html}}', menu_content)
|
|
||||||
// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
|
|
||||||
// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
|
|
||||||
// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
|
|
||||||
|
|
||||||
// return result
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // HeroScript editor rendering using external template
|
|
||||||
// fn (app &App) render_heroscript() string {
|
|
||||||
// // Get the template file path relative to the module
|
|
||||||
// template_path := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.html')
|
|
||||||
|
|
||||||
// // Read the template file
|
|
||||||
// template_content := os.read_file(template_path) or {
|
|
||||||
// // Fallback to basic template if file not found
|
|
||||||
// return app.render_heroscript_fallback()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Generate menu HTML
|
|
||||||
// menu_content := menu_html(app.menu, 0, 'm')
|
|
||||||
|
|
||||||
// // Simple template variable replacement
|
|
||||||
// mut result := template_content
|
|
||||||
// result = result.replace('{{.title}}', app.title)
|
|
||||||
// result = result.replace('{{.menu_html}}', menu_content)
|
|
||||||
// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
|
|
||||||
// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
|
|
||||||
// result = result.replace('{{.css_heroscript_url}}', '/static/css/heroscript.css')
|
|
||||||
// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
|
|
||||||
// result = result.replace('{{.js_heroscript_url}}', '/static/js/heroscript.js')
|
|
||||||
|
|
||||||
// return result
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Chat rendering using external template
|
|
||||||
// fn (app &App) render_chat() string {
|
|
||||||
// // Get the template file path relative to the module
|
|
||||||
// template_path := os.join_path(os.dir(@FILE), 'templates', 'chat.html')
|
|
||||||
|
|
||||||
// // Read the template file
|
|
||||||
// template_content := os.read_file(template_path) or {
|
|
||||||
// // Fallback to basic template if file not found
|
|
||||||
// return app.render_chat_fallback()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Generate menu HTML
|
|
||||||
// menu_content := menu_html(app.menu, 0, 'm')
|
|
||||||
|
|
||||||
// // Simple template variable replacement
|
|
||||||
// mut result := template_content
|
|
||||||
// result = result.replace('{{.title}}', app.title)
|
|
||||||
// result = result.replace('{{.menu_html}}', menu_content)
|
|
||||||
// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
|
|
||||||
// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
|
|
||||||
// result = result.replace('{{.css_chat_url}}', '/static/css/chat.css')
|
|
||||||
// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
|
|
||||||
// result = result.replace('{{.js_chat_url}}', '/static/js/chat.js')
|
|
||||||
|
|
||||||
// return result
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Fallback HeroScript rendering method
|
|
||||||
// fn (app &App) render_heroscript_fallback() string {
|
|
||||||
// return '
|
|
||||||
// <!doctype html>
|
|
||||||
// <html lang="en">
|
|
||||||
// <head>
|
|
||||||
// <meta charset="utf-8">
|
|
||||||
// <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
// <title>${app.title} - HeroScript Editor</title>
|
|
||||||
// <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
// </head>
|
|
||||||
// <body>
|
|
||||||
// <div class="container mt-5">
|
|
||||||
// <h1>HeroScript Editor</h1>
|
|
||||||
// <p>HeroScript editor template not found. Please check the template files.</p>
|
|
||||||
// <a href="/admin" class="btn btn-primary">Back to Admin</a>
|
|
||||||
// </div>
|
|
||||||
// </body>
|
|
||||||
// </html>
|
|
||||||
// '
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Fallback Chat rendering method
|
|
||||||
// fn (app &App) render_chat_fallback() string {
|
|
||||||
// return '
|
|
||||||
// <!doctype html>
|
|
||||||
// <html lang="en">
|
|
||||||
// <head>
|
|
||||||
// <meta charset="utf-8">
|
|
||||||
// <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
// <title>${app.title} - Chat</title>
|
|
||||||
// <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
// </head>
|
|
||||||
// <body>
|
|
||||||
// <div class="container mt-5">
|
|
||||||
// <h1>Chat Assistant</h1>
|
|
||||||
// <p>Chat template not found. Please check the template files.</p>
|
|
||||||
// <a href="/admin" class="btn btn-primary">Back to Admin</a>
|
|
||||||
// </div>
|
|
||||||
// </body>
|
|
||||||
// </html>
|
|
||||||
// '
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Fallback rendering method (inline template)
|
|
||||||
// fn (app &App) render_admin_fallback(path string, heading string) string {
|
|
||||||
// return '
|
|
||||||
// <!doctype html>
|
|
||||||
// <html lang="en">
|
|
||||||
// <head>
|
|
||||||
// <meta charset="utf-8">
|
|
||||||
// <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
// <title>${app.title}</title>
|
|
||||||
// <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
|
||||||
// <style>
|
|
||||||
// body { padding-top: 44px; }
|
|
||||||
// .header {
|
|
||||||
// height: 44px;
|
|
||||||
// line-height: 44px;
|
|
||||||
// font-size: 14px;
|
|
||||||
// }
|
|
||||||
// .sidebar {
|
|
||||||
// position: fixed;
|
|
||||||
// top: 44px;
|
|
||||||
// bottom: 0;
|
|
||||||
// left: 0;
|
|
||||||
// width: 260px;
|
|
||||||
// overflow-y: auto;
|
|
||||||
// background: #f8f9fa;
|
|
||||||
// border-right: 1px solid #e0e0e0;
|
|
||||||
// }
|
|
||||||
// .main {
|
|
||||||
// margin-left: 260px;
|
|
||||||
// padding: 16px;
|
|
||||||
// }
|
|
||||||
// .list-group-item {
|
|
||||||
// border: 0;
|
|
||||||
// padding: .35rem .75rem;
|
|
||||||
// background: transparent;
|
|
||||||
// }
|
|
||||||
// .menu-leaf a {
|
|
||||||
// color: #212529;
|
|
||||||
// text-decoration: none;
|
|
||||||
// }
|
|
||||||
// .menu-toggle {
|
|
||||||
// text-decoration: none;
|
|
||||||
// color: #212529;
|
|
||||||
// }
|
|
||||||
// .menu-toggle .chev {
|
|
||||||
// font-size: 10px;
|
|
||||||
// opacity: .6;
|
|
||||||
// }
|
|
||||||
// .menu-section {
|
|
||||||
// font-weight: 600;
|
|
||||||
// color: #6c757d;
|
|
||||||
// padding: .5rem .75rem;
|
|
||||||
// }
|
|
||||||
// </style>
|
|
||||||
// </head>
|
|
||||||
// <body>
|
|
||||||
// <nav class="navbar navbar-dark bg-dark fixed-top header px-2">
|
|
||||||
// <div class="d-flex w-100 align-items-center justify-content-between">
|
|
||||||
// <div class="text-white fw-bold">${app.title}</div>
|
|
||||||
// <div class="text-white-50">Admin</div>
|
|
||||||
// </div>
|
|
||||||
// </nav>
|
|
||||||
|
|
||||||
// <aside class="sidebar">
|
|
||||||
// <div class="p-2">
|
|
||||||
// <div class="menu-section">Navigation</div>
|
|
||||||
// <div class="list-group list-group-flush">
|
|
||||||
// ${menu_html(app.menu,
|
|
||||||
// 0, 'm')}
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </aside>
|
|
||||||
|
|
||||||
// <main class="main">
|
|
||||||
// <div class="container-fluid">
|
|
||||||
// <div class="d-flex align-items-center mb-3">
|
|
||||||
// <h5 class="mb-0">${heading}</h5>
|
|
||||||
// <span class="ms-2 text-muted small">/admin/${path}</span>
|
|
||||||
// </div>
|
|
||||||
// <div class="card">
|
|
||||||
// <div class="card-body">
|
|
||||||
// <p class="text-muted">This is a placeholder admin content area for: <code>/admin/${path}</code>.</p>
|
|
||||||
// <p class="mb-0">Use the treeview on the left to navigate.</p>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </main>
|
|
||||||
|
|
||||||
// <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
|
||||||
// </body>
|
|
||||||
// </html>
|
|
||||||
// '
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Recursive menu renderer
|
|
||||||
|
|
||||||
// fn menu_html(items []MenuItem, depth int, prefix string) string {
|
|
||||||
// mut out := []string{}
|
|
||||||
// for i, it in items {
|
|
||||||
// id := '${prefix}_${depth}_${i}'
|
|
||||||
// if it.children.len > 0 {
|
|
||||||
// // expandable group
|
|
||||||
// out << '<div class="list-group-item">'
|
|
||||||
// out << '<a class="menu-toggle d-flex align-items-center justify-content-between" data-bs-toggle="collapse" href="#${id}" role="button" aria-expanded="${if depth == 0 {
|
|
||||||
// 'true'
|
|
||||||
// } else {
|
|
||||||
// 'false'
|
|
||||||
// }}" aria-controls="${id}">'
|
|
||||||
// out << '<span>${it.title}</span><span class="chev">›</span>'
|
|
||||||
// out << '</a>'
|
|
||||||
// out << '<div class="collapse ${if depth == 0 { 'show' } else { '' }}" id="${id}">'
|
|
||||||
// out << '<div class="ms-2 mt-1">'
|
|
||||||
// out << menu_html(it.children, depth + 1, id)
|
|
||||||
// out << '</div>'
|
|
||||||
// out << '</div>'
|
|
||||||
// out << '</div>'
|
|
||||||
// } else {
|
|
||||||
// // leaf
|
|
||||||
// out << '<div class="list-group-item menu-leaf"><a href="${if it.href.len > 0 {
|
|
||||||
// it.href
|
|
||||||
// } else {
|
|
||||||
// '/admin'
|
|
||||||
// }}">${it.title}</a></div>'
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return out.join('\n')
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Default sample menu
|
|
||||||
// fn default_menu() []MenuItem {
|
|
||||||
// return [
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Dashboard'
|
|
||||||
// href: '/admin'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'HeroScript'
|
|
||||||
// href: '/admin/heroscript'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Chat'
|
|
||||||
// href: '/admin/chat'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Users'
|
|
||||||
// children: [
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Overview'
|
|
||||||
// href: '/admin/users/overview'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Create'
|
|
||||||
// href: '/admin/users/create'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Roles'
|
|
||||||
// href: '/admin/users/roles'
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Content'
|
|
||||||
// children: [
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Pages'
|
|
||||||
// href: '/admin/content/pages'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Media'
|
|
||||||
// href: '/admin/content/media'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Settings'
|
|
||||||
// children: [
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'SEO'
|
|
||||||
// href: '/admin/content/settings/seo'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Themes'
|
|
||||||
// href: '/admin/content/settings/themes'
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'System'
|
|
||||||
// children: [
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Status'
|
|
||||||
// href: '/admin/system/status'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Logs'
|
|
||||||
// href: '/admin/system/logs'
|
|
||||||
// },
|
|
||||||
// MenuItem{
|
|
||||||
// title: 'Backups'
|
|
||||||
// href: '/admin/system/backups'
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
305
lib/web/ui/heroprompt_api.v
Normal file
305
lib/web/ui/heroprompt_api.v
Normal file
@@ -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)
|
||||||
|
}
|
||||||
21
lib/web/ui/heroprompt_page.v
Normal file
21
lib/web/ui/heroprompt_page.v
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module ui
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
// Render the Heroprompt admin page using feature template
|
||||||
|
pub fn render_heroprompt_page(app &App) !string {
|
||||||
|
tpl := os.join_path(os.dir(@FILE), 'templates', 'heroprompt.html')
|
||||||
|
content := os.read_file(tpl)!
|
||||||
|
menu_content := menu_html(app.menu, 0, 'm')
|
||||||
|
mut result := content
|
||||||
|
result = result.replace('{{.title}}', app.title)
|
||||||
|
result = result.replace('{{.menu_html}}', menu_content)
|
||||||
|
result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
|
||||||
|
result = result.replace('{{.css_main_url}}', '/static/css/main.css')
|
||||||
|
result = result.replace('{{.css_heroprompt_url}}', '/static/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('</body>', '<div class="v-badge">Rendered by: heroprompt</div></body>')
|
||||||
|
return result
|
||||||
|
}
|
||||||
3
lib/web/ui/heroprompt_utils.v
Normal file
3
lib/web/ui/heroprompt_utils.v
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module ui
|
||||||
|
|
||||||
|
// Placeholder for heroprompt-specific utilities (if needed later)
|
||||||
23
lib/web/ui/heroscript_endpoints.v
Normal file
23
lib/web/ui/heroscript_endpoints.v
Normal file
@@ -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('</body>', '<div class="v-badge">Rendered by: heroscript</div></body>')
|
||||||
|
return result
|
||||||
|
}
|
||||||
3
lib/web/ui/heroscript_utils.v
Normal file
3
lib/web/ui/heroscript_utils.v
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module ui
|
||||||
|
|
||||||
|
// Placeholder for heroscript-specific utilities
|
||||||
83
lib/web/ui/menu.v
Normal file
83
lib/web/ui/menu.v
Normal file
@@ -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'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,243 +1,209 @@
|
|||||||
module ui
|
module ui
|
||||||
|
|
||||||
// import veb
|
import veb
|
||||||
// import os
|
import os
|
||||||
// import net.http
|
// Feature endpoint files live in subdirectories but share the `ui` module,
|
||||||
// import json
|
// so no explicit imports are needed here.
|
||||||
// import freeflowuniverse.herolib.develop.heroprompt
|
|
||||||
|
|
||||||
// // Public Context type for veb
|
// Public Context type for veb
|
||||||
// pub struct Context {
|
pub struct Context {
|
||||||
// veb.Context
|
veb.Context
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Simple tree menu structure
|
// Simple tree menu structure
|
||||||
// pub struct MenuItem {
|
pub struct MenuItem {
|
||||||
// pub:
|
pub:
|
||||||
// title string
|
title string
|
||||||
// href string
|
href string
|
||||||
// children []MenuItem
|
children []MenuItem
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Factory args
|
// Factory args
|
||||||
// @[params]
|
@[params]
|
||||||
// pub struct FactoryArgs {
|
pub struct FactoryArgs {
|
||||||
// pub mut:
|
pub mut:
|
||||||
// name string = 'default'
|
name string = 'default'
|
||||||
// host string = 'localhost'
|
host string = 'localhost'
|
||||||
// port int = 8080
|
port int = 8080
|
||||||
// title string = 'Admin'
|
title string = 'Admin'
|
||||||
// menu []MenuItem
|
menu []MenuItem
|
||||||
// open bool
|
open bool
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // The App holds server state and config
|
// The App holds server state and config
|
||||||
// pub struct App {
|
pub struct App {
|
||||||
// veb.StaticHandler
|
veb.StaticHandler
|
||||||
// pub mut:
|
pub mut:
|
||||||
// title string
|
title string
|
||||||
// menu []MenuItem
|
menu []MenuItem
|
||||||
// port int
|
port int
|
||||||
// }
|
}
|
||||||
|
|
||||||
// pub fn new(args FactoryArgs) !&App {
|
pub fn new(args FactoryArgs) !&App {
|
||||||
// mut app := App{
|
mut app := App{
|
||||||
// title: args.title
|
title: args.title
|
||||||
// menu: args.menu
|
menu: if args.menu.len > 0 { args.menu } else { get_default_menu() }
|
||||||
// port: args.port
|
port: args.port
|
||||||
// }
|
}
|
||||||
// return &app
|
// 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)
|
// Start the webserver (blocking)
|
||||||
// pub fn start(args FactoryArgs) ! {
|
pub fn start(args FactoryArgs) ! {
|
||||||
// mut app := new(args)!
|
mut app := new(args)!
|
||||||
// veb.run[App, Context](mut app, app.port)
|
veb.run[App, Context](mut app, app.port)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Routes
|
// Routes
|
||||||
|
|
||||||
// // Redirect root to /admin
|
// Redirect root to /admin
|
||||||
// @['/'; get]
|
@['/'; get]
|
||||||
// pub fn (app &App) root(mut ctx Context) veb.Result {
|
pub fn (app &App) root(mut ctx Context) veb.Result {
|
||||||
// return ctx.redirect('/admin')
|
return ctx.redirect('/admin')
|
||||||
// }
|
}
|
||||||
|
|
||||||
// // Admin home page
|
// Serve shared static assets (colors.css, main.css, theme.js)
|
||||||
// @['/admin'; get]
|
@['/static/css/colors.css'; get]
|
||||||
// pub fn (app &App) admin_index(mut ctx Context) veb.Result {
|
pub fn (app &App) serve_colors_css(mut ctx Context) veb.Result {
|
||||||
// return ctx.html(render_admin(app, '/', 'Welcome'))
|
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
|
@['/static/css/main.css'; get]
|
||||||
// @['/admin/heroscript'; get]
|
pub fn (app &App) serve_main_css(mut ctx Context) veb.Result {
|
||||||
// pub fn (app &App) admin_heroscript(mut ctx Context) veb.Result {
|
css_path := os.join_path(os.dir(@FILE), 'static', 'css', 'main.css')
|
||||||
// return ctx.html(render_heroscript(app))
|
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
|
@['/static/js/theme.js'; get]
|
||||||
// @['/admin/chat'; get]
|
pub fn (app &App) serve_theme_js(mut ctx Context) veb.Result {
|
||||||
// pub fn (app &App) admin_chat(mut ctx Context) veb.Result {
|
js_path := os.join_path(os.dir(@FILE), 'static', 'js', 'theme.js')
|
||||||
// return ctx.html(render_chat(app))
|
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 home page
|
||||||
// @['/admin/heroprompt'; get]
|
@['/admin'; get]
|
||||||
// pub fn (app &App) admin_heroprompt_page(mut ctx Context) veb.Result {
|
pub fn (app &App) admin_index(mut ctx Context) veb.Result {
|
||||||
// template_path := os.join_path(os.dir(@FILE), 'templates', 'heroprompt.html')
|
return ctx.html(render_admin(app, '/', 'Welcome'))
|
||||||
// template_content := os.read_file(template_path) or { return ctx.text('template not found') }
|
}
|
||||||
// menu_content := menu_html(app.menu, 0, 'm')
|
|
||||||
// mut result := template_content
|
|
||||||
// result = result.replace('{{.title}}', app.title)
|
|
||||||
// result = result.replace('{{.menu_html}}', menu_content)
|
|
||||||
// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
|
|
||||||
// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
|
|
||||||
// result = result.replace('{{.css_heroprompt_url}}', '/static/css/heroprompt.css?v=2')
|
|
||||||
// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js?v=2')
|
|
||||||
// result = result.replace('{{.js_heroprompt_url}}', '/static/js/heroprompt.js?v=2')
|
|
||||||
// return ctx.html(result)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Static Heroprompt assets
|
// Feature routes registered here, using imported feature renderers
|
||||||
// @['/static/css/heroprompt.css'; get]
|
|
||||||
// pub fn (app &App) serve_heroprompt_css(mut ctx Context) veb.Result {
|
|
||||||
// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'heroprompt.css')
|
|
||||||
// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
|
|
||||||
// ctx.set_content_type('text/css')
|
|
||||||
// return ctx.text(css_content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @['/static/js/heroprompt.js'; get]
|
// Catch-all content under /admin/*
|
||||||
// pub fn (app &App) serve_heroprompt_js(mut ctx Context) veb.Result {
|
@['/admin/:path...'; get]
|
||||||
// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'heroprompt.js')
|
pub fn (app &App) admin_section(mut ctx Context, path string) veb.Result {
|
||||||
// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
|
// Route to specific feature renderers
|
||||||
// ctx.set_content_type('application/javascript')
|
match path {
|
||||||
// return ctx.text(js_content)
|
'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
|
// Test API endpoint to verify routing works
|
||||||
// @['/static/css/colors.css'; get]
|
@['/api/test'; get]
|
||||||
// pub fn (app &App) serve_colors_css(mut ctx Context) veb.Result {
|
pub fn (app &App) api_test(mut ctx Context) veb.Result {
|
||||||
// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'colors.css')
|
ctx.set_content_type('application/json')
|
||||||
// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
|
return ctx.text('{"status":"ok","message":"API is working"}')
|
||||||
// ctx.set_content_type('text/css')
|
}
|
||||||
// return ctx.text(css_content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @['/static/css/main.css'; get]
|
// Pure functions for rendering templates
|
||||||
// pub fn (app &App) serve_main_css(mut ctx Context) veb.Result {
|
fn render_admin(app &App, path string, heading string) string {
|
||||||
// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'main.css')
|
template_path := os.join_path(os.dir(@FILE), 'templates', 'admin', 'layout.html')
|
||||||
// css_content := os.read_file(css_path) or { return ctx.text('/* CSS file not found */') }
|
template_content := os.read_file(template_path) or {
|
||||||
// ctx.set_content_type('text/css')
|
return render_admin_fallback(app, path, heading)
|
||||||
// return ctx.text(css_content)
|
}
|
||||||
// }
|
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
|
fn render_heroscript(app &App) string {
|
||||||
// @['/static/js/theme.js'; get]
|
template_path := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.html')
|
||||||
// pub fn (app &App) serve_theme_js(mut ctx Context) veb.Result {
|
template_content := os.read_file(template_path) or { return render_heroscript_fallback(app) }
|
||||||
// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'theme.js')
|
menu_content := menu_html(app.menu, 0, 'm')
|
||||||
// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
|
mut result := template_content
|
||||||
// ctx.set_content_type('application/javascript')
|
result = result.replace('{{.title}}', app.title)
|
||||||
// return ctx.text(js_content)
|
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]
|
fn render_heroprompt(app &App) string {
|
||||||
// pub fn (app &App) serve_heroscript_js(mut ctx Context) veb.Result {
|
template_path := os.join_path(os.dir(@FILE), 'templates', 'heroprompt.html')
|
||||||
// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'heroscript.js')
|
template_content := os.read_file(template_path) or { return render_heroprompt_fallback(app) }
|
||||||
// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
|
menu_content := menu_html(app.menu, 0, 'm')
|
||||||
// ctx.set_content_type('application/javascript')
|
mut result := template_content
|
||||||
// return ctx.text(js_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('</body>', '<div class="v-badge">Rendered by: heroprompt</div></body>')
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// @['/static/js/chat.js'; get]
|
fn render_chat(app &App) string {
|
||||||
// pub fn (app &App) serve_chat_js(mut ctx Context) veb.Result {
|
template_path := os.join_path(os.dir(@FILE), 'templates', 'chat.html')
|
||||||
// js_path := os.join_path(os.dir(@FILE), 'templates', 'js', 'chat.js')
|
template_content := os.read_file(template_path) or { return render_chat_fallback(app) }
|
||||||
// js_content := os.read_file(js_path) or { return ctx.text('/* JS file not found */') }
|
menu_content := menu_html(app.menu, 0, 'm')
|
||||||
// ctx.set_content_type('application/javascript')
|
mut result := template_content
|
||||||
// return ctx.text(js_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]
|
// Fallbacks
|
||||||
// pub fn (app &App) serve_heroscript_css(mut ctx Context) veb.Result {
|
fn render_heroprompt_fallback(app &App) string {
|
||||||
// css_path := os.join_path(os.dir(@FILE), 'templates', 'css', 'heroscript.css')
|
return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - Heroprompt</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>Heroprompt</h1>\n\t\t<p>Heroprompt template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\n'
|
||||||
// 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]
|
fn render_heroscript_fallback(app &App) string {
|
||||||
// pub fn (app &App) serve_chat_css(mut ctx Context) veb.Result {
|
return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - HeroScript Editor</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>HeroScript Editor</h1>\n\t\t<p>HeroScript editor template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\n'
|
||||||
// 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/*
|
fn render_chat_fallback(app &App) string {
|
||||||
// @['/admin/:path...'; get]
|
return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - Chat</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>Chat Assistant</h1>\n\t\t<p>Chat template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\n'
|
||||||
// pub fn (app &App) admin_section(mut ctx Context, path string) veb.Result {
|
}
|
||||||
// // Render current path in the main content
|
|
||||||
// return ctx.html(render_admin(app, path, 'Content'))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Pure functions for rendering templates
|
fn render_admin_fallback(app &App, path string, heading string) string {
|
||||||
// fn render_admin(app &App, path string, heading string) string {
|
menu_content := menu_html(app.menu, 0, 'm')
|
||||||
// template_path := os.join_path(os.dir(@FILE), 'templates', 'admin_layout.html')
|
return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title}</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">\n\t<style>body { padding-top: 44px; } .header { height: 44px; line-height: 44px; font-size: 14px; } .sidebar { position: fixed; top: 44px; bottom: 0; left: 0; width: 260px; overflow-y: auto; background: #f8f9fa; border-right: 1px solid #e0e0e0; } .main { margin-left: 260px; padding: 16px; } .list-group-item { border: 0; padding: .35rem .75rem; background: transparent; } .menu-leaf a { color: #212529; text-decoration: none; } .menu-toggle { text-decoration: none; color: #212529; } .menu-toggle .chev { font-size: 10px; opacity: .6; } .menu-section { font-weight: 600; color: #6c757d; padding: .5rem .75rem; }</style>\n</head>\n<body>\n\t<nav class="navbar navbar-dark bg-dark fixed-top header px-2">\n\t\t<div class="d-flex w-100 align-items-center justify-content-between">\n\t\t\t<div class="text-white fw-bold">${app.title}</div>\n\t\t\t<div class="text-white-50">Admin</div>\n\t\t</div>\n\t</nav>\n\n\t<aside class="sidebar">\n\t\t<div class="p-2">\n\t\t\t<div class="menu-section">Navigation</div>\n\t\t\t<div class="list-group list-group-flush">\n\t\t\t\t${menu_content}\n\t\t\t</div>\n\t\t</div>\n\t</aside>\n\n\t<main class="main">\n\t\t<div class="container-fluid">\n\t\t\t<div class="d-flex align-items-center mb-3">\n\t\t\t\t<h5 class="mb-0">${heading}</h5>\n\t\t\t\t<span class="ms-2 text-muted small">/admin/${path}</span>\n\t\t\t</div>\n\t\t\t<div class="card">\n\t\t\t\t<div class="card-body">\n\t\t\t\t\t<p class="text-muted">This is a placeholder admin content area for: <code>/admin/${path}</code>.</p>\n\t\t\t\t\t<p class="mb-0">Use the treeview on the left to navigate.</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n\n\t<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>\n</body>\n</html>\n'
|
||||||
// template_content := os.read_file(template_path) or {
|
}
|
||||||
// return render_admin_fallback(app, path, heading)
|
|
||||||
// }
|
|
||||||
// menu_content := menu_html(app.menu, 0, 'm')
|
|
||||||
// mut result := template_content
|
|
||||||
// result = result.replace('{{.title}}', app.title)
|
|
||||||
// result = result.replace('{{.heading}}', heading)
|
|
||||||
// result = result.replace('{{.path}}', path)
|
|
||||||
// result = result.replace('{{.menu_html}}', menu_content)
|
|
||||||
// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
|
|
||||||
// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
|
|
||||||
// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
|
|
||||||
// return result
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn render_heroscript(app &App) string {
|
|
||||||
// template_path := os.join_path(os.dir(@FILE), 'templates', 'heroscript_editor.html')
|
|
||||||
// template_content := os.read_file(template_path) or { return render_heroscript_fallback(app) }
|
|
||||||
// menu_content := menu_html(app.menu, 0, 'm')
|
|
||||||
// mut result := template_content
|
|
||||||
// result = result.replace('{{.title}}', app.title)
|
|
||||||
// result = result.replace('{{.menu_html}}', menu_content)
|
|
||||||
// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
|
|
||||||
// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
|
|
||||||
// result = result.replace('{{.css_heroscript_url}}', '/static/css/heroscript.css')
|
|
||||||
// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
|
|
||||||
// result = result.replace('{{.js_heroscript_url}}', '/static/js/heroscript.js')
|
|
||||||
// return result
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn render_chat(app &App) string {
|
|
||||||
// template_path := os.join_path(os.dir(@FILE), 'templates', 'chat.html')
|
|
||||||
// template_content := os.read_file(template_path) or { return render_chat_fallback(app) }
|
|
||||||
// menu_content := menu_html(app.menu, 0, 'm')
|
|
||||||
// mut result := template_content
|
|
||||||
// result = result.replace('{{.title}}', app.title)
|
|
||||||
// result = result.replace('{{.menu_html}}', menu_content)
|
|
||||||
// result = result.replace('{{.css_colors_url}}', '/static/css/colors.css')
|
|
||||||
// result = result.replace('{{.css_main_url}}', '/static/css/main.css')
|
|
||||||
// result = result.replace('{{.css_chat_url}}', '/static/css/chat.css')
|
|
||||||
// result = result.replace('{{.js_theme_url}}', '/static/js/theme.js')
|
|
||||||
// result = result.replace('{{.js_chat_url}}', '/static/js/chat.js')
|
|
||||||
// return result
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Fallbacks
|
|
||||||
// fn render_heroscript_fallback(app &App) string {
|
|
||||||
// return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - HeroScript Editor</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>HeroScript Editor</h1>\n\t\t<p>HeroScript editor template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\n'
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn render_chat_fallback(app &App) string {
|
|
||||||
// return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title} - Chat</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">\n</head>\n<body>\n\t<div class="container mt-5">\n\t\t<h1>Chat Assistant</h1>\n\t\t<p>Chat template not found. Please check the template files.</p>\n\t\t<a href="/admin" class="btn btn-primary">Back to Admin</a>\n\t</div>\n</body>\n</html>\n'
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn render_admin_fallback(app &App, path string, heading string) string {
|
|
||||||
// return '\n<!doctype html>\n<html lang="en">\n<head>\n\t<meta charset="utf-8">\n\t<meta name="viewport" content="width=device-width, initial-scale=1">\n\t<title>${app.title}</title>\n\t<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">\n\t<style>body { padding-top: 44px; } .header { height: 44px; line-height: 44px; font-size: 14px; } .sidebar { position: fixed; top: 44px; bottom: 0; left: 0; width: 260px; overflow-y: auto; background: #f8f9fa; border-right: 1px solid #e0e0e0; } .main { margin-left: 260px; padding: 16px; } .list-group-item { border: 0; padding: .35rem .75rem; background: transparent; } .menu-leaf a { color: #212529; text-decoration: none; } .menu-toggle { text-decoration: none; color: #212529; } .menu-toggle .chev { font-size: 10px; opacity: .6; } .menu-section { font-weight: 600; color: #6c757d; padding: .5rem .75rem; }</style>\n</head>\n<body>\n\t<nav class="navbar navbar-dark bg-dark fixed-top header px-2">\n\t\t<div class="d-flex w-100 align-items-center justify-content-between">\n\t\t\t<div class="text-white fw-bold">${app.title}</div>\n\t\t\t<div class="text-white-50">Admin</div>\n\t\t</div>\n\t</nav>\n\n\t<aside class="sidebar">\n\t\t<div class="p-2">\n\t\t\t<div class="menu-section">Navigation</div>\n\t\t\t<div class="list-group list-group-flush">\n\t\t\t\t${menu_html(app.menu,
|
|
||||||
// 0, 'm')}\n\t\t\t</div>\n\t\t</div>\n\t</aside>\n\n\t<main class="main">\n\t\t<div class="container-fluid">\n\t\t\t<div class="d-flex align-items-center mb-3">\n\t\t\t\t<h5 class="mb-0">${heading}</h5>\n\t\t\t\t<span class="ms-2 text-muted small">/admin/${path}</span>\n\t\t\t</div>\n\t\t\t<div class="card">\n\t\t\t\t<div class="card-body">\n\t\t\t\t\t<p class="text-muted">This is a placeholder admin content area for: <code>/admin/${path}</code>.</p>\n\t\t\t\t\t<p class="mb-0">Use the treeview on the left to navigate.</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</main>\n\n\t<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>\n</body>\n</html>\n'
|
|
||||||
// }
|
|
||||||
|
|||||||
2705
lib/web/ui/static/css/heroprompt.css
Normal file
2705
lib/web/ui/static/css/heroprompt.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -262,6 +262,7 @@ code {
|
|||||||
|
|
||||||
/* Print styles */
|
/* Print styles */
|
||||||
@media print {
|
@media print {
|
||||||
|
|
||||||
.sidebar,
|
.sidebar,
|
||||||
.header,
|
.header,
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
1970
lib/web/ui/static/js/heroprompt.js
Normal file
1970
lib/web/ui/static/js/heroprompt.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -45,3 +45,4 @@
|
|||||||
<script src="{{.js_theme_url}}"></script>
|
<script src="{{.js_theme_url}}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
@@ -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: "📁";
|
|
||||||
}
|
|
||||||
@@ -31,74 +31,283 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid h-100">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<h5 class="mb-0">Heroprompt</h5>
|
<h5 class="mb-0">Heroprompt</h5>
|
||||||
<span class="ms-2 text-muted small">/admin/heroprompt</span>
|
<div class="ms-auto">
|
||||||
|
<button id="wsCreateBtn" class="btn btn-primary btn-sm">
|
||||||
|
+ New Workspace
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="heroprompt-container">
|
<div class="row h-100">
|
||||||
<!-- Left Column: Workspaces -->
|
<!-- Left Panel: Enhanced File Explorer -->
|
||||||
<div class="workspaces-panel">
|
<div class="col-md-4 h-100">
|
||||||
<div class="card">
|
<div class="explorer-panel h-100">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="explorer-header">
|
||||||
<h6 class="mb-0">Workspaces</h6>
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<button id="create-workspace" class="btn btn-sm btn-primary">New</button>
|
<h6 class="mb-0 text-uppercase fw-bold explorer-title">Explorer</h6>
|
||||||
|
<div class="explorer-actions">
|
||||||
|
<button id="refreshExplorer" class="btn btn-lg btn-ghost me-2"
|
||||||
|
title="Refresh Workspace">
|
||||||
|
<i class="icon-refresh"></i>
|
||||||
|
</button>
|
||||||
|
<button id="wsDetailsBtn" class="btn btn-lg btn-ghost" title="Workspace Details">
|
||||||
|
<i class="icon-settings"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<label for="workspace-select" class="form-label small text-muted">Select workspace</label>
|
|
||||||
<select id="workspace-select" class="form-select form-select-sm mb-2"></select>
|
<div class="workspace-selector mb-3">
|
||||||
<button id="delete-workspace" class="btn btn-sm btn-outline-danger"
|
<select id="workspaceSelect" class="form-select form-select-sm modern-select"></select>
|
||||||
style="display:none;">Delete</button>
|
</div>
|
||||||
|
|
||||||
|
<div class="search-container mb-3">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text search-icon">
|
||||||
|
<i class="icon-search"></i>
|
||||||
|
</span>
|
||||||
|
<input id="search" class="form-control modern-input"
|
||||||
|
placeholder="Search files and folders...">
|
||||||
|
<button id="clearSearch" class="btn btn-ghost search-clear" title="Clear search">
|
||||||
|
<i class="icon-close"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Middle Column: Workspace Details -->
|
<div class="explorer-content">
|
||||||
<div class="workspace-details">
|
<div class="selection-controls mb-2">
|
||||||
<div class="card">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="selection-info">
|
||||||
<h6 class="mb-0">
|
<span class="badge badge-selection">
|
||||||
<span id="current-workspace-name">Select a workspace</span>
|
<span id="selCount">0</span> selected
|
||||||
<button id="delete-workspace" class="btn btn-sm btn-outline-danger ms-2"
|
</span>
|
||||||
style="display: none;">Delete</button>
|
</div>
|
||||||
</h6>
|
<div class="selection-actions">
|
||||||
<div>
|
|
||||||
<button id="add-directory" class="btn btn-sm btn-secondary me-2"
|
|
||||||
style="display: none;">Add Directory</button>
|
|
||||||
<button id="copy-selection" class="btn btn-sm btn-success" style="display: none;">Copy
|
|
||||||
Selection</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<div id="workspace-content">
|
|
||||||
<p class="text-muted">Select a workspace to view its directories and files.</p>
|
<div id="tree" class="file-tree">
|
||||||
|
<div class="empty-state">
|
||||||
|
<i class="icon-folder-open"></i>
|
||||||
|
<p>Select a workspace to browse files</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column: User Instructions -->
|
<!-- Right Panel: Enhanced Workspace -->
|
||||||
<div class="instructions-panel">
|
<div class="col-md-8 h-100">
|
||||||
<div class="card">
|
<div class="workspace-panel h-100">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="workspace-header">
|
||||||
<h6 class="mb-0">User Instructions</h6>
|
<div class="tab-navigation">
|
||||||
<button id="clear-instructions" class="btn btn-sm btn-outline-secondary">Clear</button>
|
<div class="nav nav-tabs modern-tabs" id="mainTabs">
|
||||||
|
<button class="nav-link active tab" data-tab="selection">
|
||||||
|
<i class="icon-selection"></i>
|
||||||
|
<span>Selection</span>
|
||||||
|
<span class="badge badge-count" id="selCountTab">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-link tab" data-tab="prompt">
|
||||||
|
<i class="icon-prompt"></i>
|
||||||
|
<span>Prompt</span>
|
||||||
|
</button>
|
||||||
|
<button class="nav-link tab" data-tab="chat">
|
||||||
|
<i class="icon-chat"></i>
|
||||||
|
<span>AI Chat</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="workspace-actions">
|
||||||
<textarea id="user-instructions" class="form-control" rows="10" placeholder="Enter your instructions for what needs to be done with the selected code...
|
<span class="token-counter">
|
||||||
|
<i class="icon-token"></i>
|
||||||
|
<span id="tokenCount">0</span> tokens
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="workspace-content">
|
||||||
|
<!-- Selection Tab -->
|
||||||
|
<div id="tab-selection" class="tab-pane active">
|
||||||
|
<div class="selection-workspace">
|
||||||
|
<div class="selection-header">
|
||||||
|
<h6 class="section-title">Selected Files & Directories</h6>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selection-content">
|
||||||
|
<div class="selection-cards-container">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">Selected Files & Directories</span>
|
||||||
|
</div>
|
||||||
|
<div id="selectedCards" class="selection-cards">
|
||||||
|
<div class="empty-selection-cards">
|
||||||
|
<i class="icon-empty"></i>
|
||||||
|
<p>No files selected</p>
|
||||||
|
<small>Use checkboxes in the explorer to select files and
|
||||||
|
directories</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prompt Tab -->
|
||||||
|
<div id="tab-prompt" class="tab-pane" style="display: none;">
|
||||||
|
<div class="prompt-workspace">
|
||||||
|
<div class="prompt-editor">
|
||||||
|
<div class="editor-header">
|
||||||
|
<h6 class="section-title">Prompt Instructions</h6>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<button id="loadTemplate" class="btn btn-sm btn-ghost"
|
||||||
|
title="Load Template">
|
||||||
|
<i class="icon-template"></i>
|
||||||
|
</button>
|
||||||
|
<button id="saveTemplate" class="btn btn-sm btn-ghost"
|
||||||
|
title="Save Template">
|
||||||
|
<i class="icon-save"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-content">
|
||||||
|
<textarea id="promptText" class="modern-textarea" rows="8" placeholder="Enter your instructions for what needs to be done with the selected code...
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
- Analyze the code structure
|
- Analyze the code structure and identify potential improvements
|
||||||
- Identify potential improvements
|
- Add comprehensive error handling and validation
|
||||||
- Add error handling
|
- Optimize performance and reduce complexity
|
||||||
- Optimize performance
|
- Add detailed documentation and comments
|
||||||
- Add documentation"></textarea>
|
- Implement best practices and design patterns"></textarea>
|
||||||
<div class="mt-3">
|
</div>
|
||||||
<small class="text-muted">
|
<div class="editor-footer">
|
||||||
These instructions will be included in the generated prompt along with the selected
|
<button id="generatePrompt" class="btn btn-primary">
|
||||||
files.
|
<i class="icon-generate"></i>
|
||||||
</small>
|
Generate Prompt
|
||||||
|
</button>
|
||||||
|
<button id="copyPrompt" class="btn btn-secondary">
|
||||||
|
<i class="icon-copy"></i>
|
||||||
|
Copy to Clipboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="prompt-output">
|
||||||
|
<div class="output-header">
|
||||||
|
<span class="panel-title">Generated Prompt</span>
|
||||||
|
<div class="output-actions">
|
||||||
|
<button id="copyOutput" class="btn btn-xs btn-ghost"
|
||||||
|
title="Copy Output">
|
||||||
|
<i class="icon-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="output-content">
|
||||||
|
<div id="promptOutput" class="prompt-result">
|
||||||
|
<div class="empty-output">
|
||||||
|
<i class="icon-prompt"></i>
|
||||||
|
<p>Generated prompt will appear here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat Tab -->
|
||||||
|
<div id="tab-chat" class="tab-pane" style="display: none;">
|
||||||
|
<div class="chat-workspace">
|
||||||
|
<div class="chat-layout">
|
||||||
|
<!-- Chat Sidebar -->
|
||||||
|
<div class="chat-sidebar">
|
||||||
|
<div class="chat-sidebar-header">
|
||||||
|
<h6 class="sidebar-title">Conversations</h6>
|
||||||
|
<button id="newChatBtn" class="btn btn-sm btn-ghost" title="New Chat">
|
||||||
|
<i class="icon-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="chat-list">
|
||||||
|
<div id="chatList" class="chat-conversations">
|
||||||
|
<div class="empty-chat-list">
|
||||||
|
<i class="icon-chat"></i>
|
||||||
|
<p>No conversations yet</p>
|
||||||
|
<small>Start a new chat to begin</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chat Main Area -->
|
||||||
|
<div class="chat-main">
|
||||||
|
<div class="chat-header">
|
||||||
|
<div class="chat-header-info">
|
||||||
|
<h6 class="section-title">AI Assistant</h6>
|
||||||
|
<span id="chatStatus" class="chat-status">Ready</span>
|
||||||
|
</div>
|
||||||
|
<div class="chat-actions">
|
||||||
|
<button id="clearChat" class="btn btn-sm btn-ghost"
|
||||||
|
title="Clear Chat">
|
||||||
|
<i class="icon-clear"></i>
|
||||||
|
</button>
|
||||||
|
<button id="exportChat" class="btn btn-sm btn-ghost"
|
||||||
|
title="Export Chat">
|
||||||
|
<i class="icon-export"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-content">
|
||||||
|
<div class="chat-messages">
|
||||||
|
<div id="chatMessages" class="messages-container">
|
||||||
|
<div class="welcome-message">
|
||||||
|
<div class="welcome-avatar">
|
||||||
|
<i class="icon-ai"></i>
|
||||||
|
</div>
|
||||||
|
<div class="welcome-content">
|
||||||
|
<h4>Welcome to AI Assistant</h4>
|
||||||
|
<p>I'm here to help you with your code! You can:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Ask questions about your selected files</li>
|
||||||
|
<li>Request code explanations and improvements</li>
|
||||||
|
<li>Get suggestions for best practices</li>
|
||||||
|
<li>Debug issues and optimize performance</li>
|
||||||
|
</ul>
|
||||||
|
<small>Select some files from the explorer and start
|
||||||
|
chatting!</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="typingIndicator" class="typing-indicator"
|
||||||
|
style="display: none;">
|
||||||
|
<span>AI is typing</span>
|
||||||
|
<div class="typing-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-input">
|
||||||
|
<div class="input-container">
|
||||||
|
<textarea id="chatInput" class="chat-textarea" rows="2"
|
||||||
|
placeholder="Ask about your code, request explanations, or get suggestions..."></textarea>
|
||||||
|
<button id="sendChat" class="btn btn-primary send-btn">
|
||||||
|
<i class="icon-send"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="input-footer">
|
||||||
|
<span class="input-hint">Press Enter to send, Shift+Enter for
|
||||||
|
new line</span>
|
||||||
|
<span id="charCount" class="char-count">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,15 +316,94 @@ Example:
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Toast for notifications -->
|
<!-- Modals -->
|
||||||
<div class="toast-container position-fixed bottom-0 end-0 p-3">
|
<div class="modal fade" id="wsCreate" tabindex="-1">
|
||||||
<div id="notification-toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
|
<div class="modal-dialog">
|
||||||
<div class="toast-header">
|
<div class="modal-content">
|
||||||
<strong class="me-auto">Heroprompt</strong>
|
<div class="modal-header">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
<h5 class="modal-title">Create Workspace</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" id="wcClose"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wcName" class="form-label">Workspace Name (optional)</label>
|
||||||
|
<input type="text" class="form-control" id="wcName" placeholder="Enter workspace name">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wcPath" class="form-label">Base Path (required)</label>
|
||||||
|
<input type="text" class="form-control" id="wcPath" placeholder="Enter base directory path">
|
||||||
|
</div>
|
||||||
|
<div id="wcError" class="text-danger small"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
|
||||||
|
id="wcCancel">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="wcCreate">Create</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="wsDetails" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Workspace Details</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" id="wdClose"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wdName" class="form-label">Workspace Name</label>
|
||||||
|
<input type="text" class="form-control" id="wdName">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wdPath" class="form-label">Base Path</label>
|
||||||
|
<input type="text" class="form-control" id="wdPath">
|
||||||
|
</div>
|
||||||
|
<div id="wdError" class="text-danger small"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" id="wdDelete">Delete</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
|
||||||
|
id="wdCancel">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="wdUpdate">Update</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="wsManage" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Manage Workspaces</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="wmError" class="text-danger small mb-3"></div>
|
||||||
|
<div class="list-group" id="wmList">
|
||||||
|
<div class="text-muted">Loading workspaces...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirmation Modal -->
|
||||||
|
<div class="modal fade" id="confirmDeleteModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Confirm Delete</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="confirmDeleteMessage">Are you sure you want to delete this workspace?</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toast-body">
|
|
||||||
<!-- Toast message will be set by JavaScript -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,760 +0,0 @@
|
|||||||
/**
|
|
||||||
* Heroprompt - Client-side workspace and file selection management
|
|
||||||
* Updated to work with V backend API and support subdirectories
|
|
||||||
*/
|
|
||||||
|
|
||||||
class Heroprompt {
|
|
||||||
constructor() {
|
|
||||||
// Backend-integrated state (no localStorage)
|
|
||||||
this.currentWorkspace = '';
|
|
||||||
this.workspaces = [];
|
|
||||||
this.selectedFiles = new Set();
|
|
||||||
this.selectedDirs = new Set();
|
|
||||||
|
|
||||||
this.initializeUI();
|
|
||||||
this.bindEvents();
|
|
||||||
// Load workspaces from backend and render
|
|
||||||
this.refreshWorkspaces();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data management
|
|
||||||
loadData() {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem(this.storageKey);
|
|
||||||
if (stored) {
|
|
||||||
return JSON.parse(stored);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to load heroprompt data:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
workspaces: {
|
|
||||||
default: { dirs: [] }
|
|
||||||
},
|
|
||||||
current: 'default'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
saveData() {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to save heroprompt data:', e);
|
|
||||||
this.showToast('Failed to save data', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// API calls to V backend
|
|
||||||
async fetchDirectory(path) {
|
|
||||||
try {
|
|
||||||
const qs = new URLSearchParams({ name: this.currentWorkspace || 'default', path }).toString();
|
|
||||||
const response = await fetch(`/api/heroprompt/directory?${qs}`);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to fetch directory:', e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchFileContent(path) {
|
|
||||||
try {
|
|
||||||
const qs = new URLSearchParams({ name: this.currentWorkspace || 'default', path }).toString();
|
|
||||||
const response = await fetch(`/api/heroprompt/file?${qs}`);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP ${response.status}`);
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to fetch file:', e);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workspace management
|
|
||||||
createWorkspace(name) {
|
|
||||||
if (!name || name.trim() === '') {
|
|
||||||
this.showToast('Workspace name cannot be empty', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
name = name.trim();
|
|
||||||
if (this.data.workspaces[name]) {
|
|
||||||
this.showToast('Workspace already exists', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data.workspaces[name] = { dirs: [] };
|
|
||||||
this.data.current = name;
|
|
||||||
this.currentWorkspace = name;
|
|
||||||
this.saveData();
|
|
||||||
this.render();
|
|
||||||
this.showToast(`Workspace "${name}" created`, 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteWorkspace(name) {
|
|
||||||
if (name === 'default') {
|
|
||||||
this.showToast('Cannot delete default workspace', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirm(`Are you sure you want to delete workspace "${name}"?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.data.workspaces[name];
|
|
||||||
|
|
||||||
if (this.currentWorkspace === name) {
|
|
||||||
this.currentWorkspace = 'default';
|
|
||||||
this.data.current = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveData();
|
|
||||||
this.render();
|
|
||||||
this.showToast(`Workspace "${name}" deleted`, 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
selectWorkspace(name) {
|
|
||||||
if (!this.data.workspaces[name]) {
|
|
||||||
this.showToast('Workspace not found', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentWorkspace = name;
|
|
||||||
this.data.current = name;
|
|
||||||
this.saveData();
|
|
||||||
this.render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directory management
|
|
||||||
async addDirectory() {
|
|
||||||
// Try to use the File System Access API if available
|
|
||||||
if ('showDirectoryPicker' in window) {
|
|
||||||
await this.addDirectoryWithPicker();
|
|
||||||
} else {
|
|
||||||
this.addDirectoryWithPrompt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Backend API helpers
|
|
||||||
async apiListWorkspaces() {
|
|
||||||
const res = await fetch('/api/heroprompt/workspaces');
|
|
||||||
if (!res.ok) throw new Error('Failed to list workspaces');
|
|
||||||
return await res.json(); // array of names
|
|
||||||
}
|
|
||||||
|
|
||||||
async apiCreateWorkspace(name, base_path = '') {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('name', name);
|
|
||||||
if (base_path) form.append('base_path', base_path);
|
|
||||||
const res = await fetch('/api/heroprompt/workspaces', { method: 'POST', body: form });
|
|
||||||
if (!res.ok) throw new Error('Failed to create workspace');
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async apiDeleteWorkspace(name) {
|
|
||||||
const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
||||||
if (!res.ok) throw new Error('Failed to delete workspace');
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWorkspaces(selectName = '') {
|
|
||||||
try {
|
|
||||||
this.workspaces = await this.apiListWorkspaces();
|
|
||||||
if (this.workspaces.length > 0) {
|
|
||||||
this.currentWorkspace = selectName || this.currentWorkspace || this.workspaces[0];
|
|
||||||
} else {
|
|
||||||
this.currentWorkspace = '';
|
|
||||||
}
|
|
||||||
await this.render();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
this.showToast('Failed to load workspaces', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async apiGetWorkspace(name) {
|
|
||||||
const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(name)}`);
|
|
||||||
if (!res.ok) throw new Error('Failed to get workspace');
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async apiAddDir(path) {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('path', path);
|
|
||||||
const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(this.currentWorkspace)}/dirs`, {
|
|
||||||
method: 'POST', body: form
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error('Failed to add directory');
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async apiRemoveDir(path) {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('path', path);
|
|
||||||
const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(this.currentWorkspace)}/dirs/remove`, {
|
|
||||||
method: 'POST', body: form
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error('Failed to remove directory');
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async apiAddFile(path) {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('path', path);
|
|
||||||
const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(this.currentWorkspace)}/files`, {
|
|
||||||
method: 'POST', body: form
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error('Failed to add file');
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async apiRemoveFile(path) {
|
|
||||||
const form = new FormData();
|
|
||||||
form.append('path', path);
|
|
||||||
const res = await fetch(`/api/heroprompt/workspaces/${encodeURIComponent(this.currentWorkspace)}/files/remove`, {
|
|
||||||
method: 'POST', body: form
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error('Failed to remove file');
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadCurrentWorkspaceDetails() {
|
|
||||||
if (!this.currentWorkspace) {
|
|
||||||
this.currentDetails = null;
|
|
||||||
this.selectedFiles = new Set();
|
|
||||||
this.selectedDirs = new Set();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await this.apiGetWorkspace(this.currentWorkspace);
|
|
||||||
this.currentDetails = data;
|
|
||||||
const files = new Set();
|
|
||||||
const dirs = new Set();
|
|
||||||
for (const ch of (data.children || [])) {
|
|
||||||
if (ch.type === 'file') files.add(ch.path);
|
|
||||||
if (ch.type === 'directory') dirs.add(ch.path);
|
|
||||||
}
|
|
||||||
this.selectedFiles = files;
|
|
||||||
this.selectedDirs = dirs;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async addDirectoryWithPicker() {
|
|
||||||
try {
|
|
||||||
const dirHandle = await window.showDirectoryPicker();
|
|
||||||
|
|
||||||
// For File System Access API, we need to read the directory contents directly
|
|
||||||
// since we can't pass the handle to the backend
|
|
||||||
const files = [];
|
|
||||||
const subdirs = [];
|
|
||||||
|
|
||||||
for await (const [name, handle] of dirHandle.entries()) {
|
|
||||||
if (handle.kind === 'file') {
|
|
||||||
files.push(name);
|
|
||||||
} else if (handle.kind === 'directory') {
|
|
||||||
subdirs.push(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedDir = {
|
|
||||||
path: dirHandle.name, // Use the directory name as path for now
|
|
||||||
files: files.sort(),
|
|
||||||
subdirs: subdirs.sort(),
|
|
||||||
selected: []
|
|
||||||
};
|
|
||||||
|
|
||||||
this.data.workspaces[this.currentWorkspace].dirs.push(processedDir);
|
|
||||||
this.saveData();
|
|
||||||
this.render();
|
|
||||||
this.showToast(`Directory "${dirHandle.name}" added`, 'success');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
if (e.name !== 'AbortError') {
|
|
||||||
console.error('Directory picker error:', e);
|
|
||||||
this.showToast('Failed to access directory', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async processDirectoryHandle(dirHandle, basePath = '') {
|
|
||||||
// This method is no longer used with the File System Access API approach
|
|
||||||
// but kept for compatibility
|
|
||||||
try {
|
|
||||||
const fullPath = basePath ? `${basePath}/${dirHandle.name}` : dirHandle.name;
|
|
||||||
const dirData = await this.fetchDirectory(fullPath);
|
|
||||||
|
|
||||||
// Check if we got an error response
|
|
||||||
if (dirData.error) {
|
|
||||||
throw new Error(dirData.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the directory structure from the backend
|
|
||||||
const processedDir = {
|
|
||||||
path: dirData.path,
|
|
||||||
files: [],
|
|
||||||
subdirs: [],
|
|
||||||
selected: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Separate files and directories
|
|
||||||
if (dirData.items && Array.isArray(dirData.items)) {
|
|
||||||
for (const item of dirData.items) {
|
|
||||||
if (item.type === 'file') {
|
|
||||||
processedDir.files.push(item.name);
|
|
||||||
} else if (item.type === 'directory') {
|
|
||||||
processedDir.subdirs.push(item.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data.workspaces[this.currentWorkspace].dirs.push(processedDir);
|
|
||||||
this.saveData();
|
|
||||||
this.render();
|
|
||||||
this.showToast(`Directory "${dirHandle.name}" added`, 'success');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to process directory:', e);
|
|
||||||
this.showToast('Failed to process directory', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async addDirectoryWithPrompt() {
|
|
||||||
const path = prompt('Enter directory path:');
|
|
||||||
if (!path || path.trim() === '') return;
|
|
||||||
try {
|
|
||||||
await this.apiAddDir(path.trim());
|
|
||||||
await this.render();
|
|
||||||
this.showToast(`Directory "${path}" added`, 'success');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to add directory:', e);
|
|
||||||
this.showToast(`Failed to add directory: ${e.message}`, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeDirectory(path) {
|
|
||||||
if (!confirm('Are you sure you want to remove this directory?')) return;
|
|
||||||
try {
|
|
||||||
await this.apiRemoveDir(path);
|
|
||||||
await this.render();
|
|
||||||
this.showToast('Directory removed', 'success');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to remove directory:', e);
|
|
||||||
this.showToast('Failed to remove directory', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// File selection management (backend-synced)
|
|
||||||
async toggleFileSelection(filePath) {
|
|
||||||
try {
|
|
||||||
if (this.selectedFiles.has(filePath)) {
|
|
||||||
await this.apiRemoveFile(filePath);
|
|
||||||
this.selectedFiles.delete(filePath);
|
|
||||||
} else {
|
|
||||||
await this.apiAddFile(filePath);
|
|
||||||
this.selectedFiles.add(filePath);
|
|
||||||
}
|
|
||||||
await this.renderWorkspaceDetails();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to toggle file selection:', e);
|
|
||||||
this.showToast('Failed to toggle file selection', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAllFiles(workspaceName, dirIndex) {
|
|
||||||
const dir = this.data.workspaces[workspaceName].dirs[dirIndex];
|
|
||||||
dir.selected = [...dir.files];
|
|
||||||
this.saveData();
|
|
||||||
this.renderWorkspaceDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
deselectAllFiles(workspaceName, dirIndex) {
|
|
||||||
const dir = this.data.workspaces[workspaceName].dirs[dirIndex];
|
|
||||||
dir.selected = [];
|
|
||||||
this.saveData();
|
|
||||||
this.renderWorkspaceDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate file tree structure
|
|
||||||
generateFileTree(dirPath, files, subdirs) {
|
|
||||||
const lines = [];
|
|
||||||
const dirName = dirPath.split('/').pop() || dirPath;
|
|
||||||
|
|
||||||
lines.push(`${dirPath}`);
|
|
||||||
|
|
||||||
// Add files
|
|
||||||
files.forEach((file, index) => {
|
|
||||||
const isLast = index === files.length - 1 && subdirs.length === 0;
|
|
||||||
lines.push(`${isLast ? '└──' : '├──'} ${file}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add subdirectories (placeholder for now)
|
|
||||||
subdirs.forEach((subdir, index) => {
|
|
||||||
const isLast = index === subdirs.length - 1;
|
|
||||||
lines.push(`${isLast ? '└──' : '├──'} ${subdir}/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clipboard functionality with new format
|
|
||||||
async copySelection() {
|
|
||||||
if (!this.currentWorkspace) {
|
|
||||||
this.showToast('Select a workspace first', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userInstructions = document.getElementById('user-instructions').value.trim();
|
|
||||||
if (!userInstructions) {
|
|
||||||
this.showToast('Please enter user instructions', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasSelection = false;
|
|
||||||
const output = [];
|
|
||||||
|
|
||||||
// Add user instructions
|
|
||||||
output.push('<user_instructions>');
|
|
||||||
output.push(userInstructions);
|
|
||||||
output.push('</user_instructions>');
|
|
||||||
output.push('');
|
|
||||||
|
|
||||||
// Generate file map
|
|
||||||
output.push('<file_map>');
|
|
||||||
const dirSet = new Set(Array.from(this.selectedFiles).map(p => p.split('/').slice(0, -1).join('/')));
|
|
||||||
for (const dirPath of dirSet) {
|
|
||||||
const data = await this.fetchDirectory(dirPath);
|
|
||||||
const files = (data.items || []).filter(it => it.type === 'file').map(it => it.name);
|
|
||||||
const subdirs = (data.items || []).filter(it => it.type === 'directory').map(it => it.name);
|
|
||||||
const fileTree = this.generateFileTree(dirPath, files, subdirs);
|
|
||||||
output.push(fileTree);
|
|
||||||
}
|
|
||||||
output.push('</file_map>');
|
|
||||||
output.push('');
|
|
||||||
|
|
||||||
if (this.selectedFiles.size === 0) {
|
|
||||||
this.showToast('No files selected', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate file contents
|
|
||||||
output.push('<file_contents>');
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const filePath of Array.from(this.selectedFiles)) {
|
|
||||||
try {
|
|
||||||
const fileData = await this.fetchFileContent(filePath);
|
|
||||||
output.push(`File: ${filePath}`);
|
|
||||||
output.push('');
|
|
||||||
output.push(`\`\`\`${fileData.language}`);
|
|
||||||
output.push(fileData.content);
|
|
||||||
output.push('```');
|
|
||||||
output.push('');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed to fetch file ${filePath}:`, e);
|
|
||||||
output.push(`File: ${filePath}`);
|
|
||||||
output.push('');
|
|
||||||
output.push('```text');
|
|
||||||
output.push(`Error: Failed to read file - ${e.message}`);
|
|
||||||
output.push('```');
|
|
||||||
output.push('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error fetching file contents:', e);
|
|
||||||
this.showToast('Error fetching file contents', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
output.push('</file_contents>');
|
|
||||||
|
|
||||||
const text = output.join('\n');
|
|
||||||
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
this.showToast('Selection copied to clipboard', 'success');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Clipboard error:', e);
|
|
||||||
this.fallbackCopyToClipboard(text);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.fallbackCopyToClipboard(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
this.showToast('Selection copied to clipboard', 'success');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Fallback copy failed:', e);
|
|
||||||
this.showToast('Failed to copy to clipboard', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI Management
|
|
||||||
initializeUI() {
|
|
||||||
// Cache DOM elements
|
|
||||||
this.elements = {
|
|
||||||
workspaceSelect: document.getElementById('workspace-select'),
|
|
||||||
workspaceContent: document.getElementById('workspace-content'),
|
|
||||||
currentWorkspaceName: document.getElementById('current-workspace-name'),
|
|
||||||
createWorkspaceBtn: document.getElementById('create-workspace'),
|
|
||||||
deleteWorkspaceBtn: document.getElementById('delete-workspace'),
|
|
||||||
addDirectoryBtn: document.getElementById('add-directory'),
|
|
||||||
copySelectionBtn: document.getElementById('copy-selection'),
|
|
||||||
clearInstructionsBtn: document.getElementById('clear-instructions'),
|
|
||||||
userInstructions: document.getElementById('user-instructions'),
|
|
||||||
toast: document.getElementById('notification-toast')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
// Workspace management
|
|
||||||
this.elements.createWorkspaceBtn.addEventListener('click', async () => {
|
|
||||||
const name = prompt('Enter workspace name:');
|
|
||||||
if (!name) return;
|
|
||||||
const base_path = prompt('Enter base path for this workspace (optional):') || '';
|
|
||||||
try {
|
|
||||||
await this.apiCreateWorkspace(name.trim(), base_path.trim());
|
|
||||||
await this.refreshWorkspaces(name.trim());
|
|
||||||
this.showToast(`Workspace "${name}" created`, 'success');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
this.showToast('Failed to create workspace', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.elements.deleteWorkspaceBtn.addEventListener('click', async () => {
|
|
||||||
if (!this.currentWorkspace) return;
|
|
||||||
if (!confirm(`Are you sure you want to delete workspace "${this.currentWorkspace}"?`)) return;
|
|
||||||
try {
|
|
||||||
await this.apiDeleteWorkspace(this.currentWorkspace);
|
|
||||||
await this.refreshWorkspaces();
|
|
||||||
this.showToast('Workspace deleted', 'success');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
this.showToast('Failed to delete workspace', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.elements.workspaceSelect.addEventListener('change', async () => {
|
|
||||||
this.currentWorkspace = this.elements.workspaceSelect.value;
|
|
||||||
await this.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Directory management
|
|
||||||
this.elements.addDirectoryBtn.addEventListener('click', () => {
|
|
||||||
this.addDirectory();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Copy selection
|
|
||||||
this.elements.copySelectionBtn.addEventListener('click', () => {
|
|
||||||
this.copySelection();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear instructions
|
|
||||||
this.elements.clearInstructionsBtn.addEventListener('click', () => {
|
|
||||||
this.elements.userInstructions.value = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.renderWorkspaceSelect();
|
|
||||||
this.renderWorkspaceDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderWorkspaceSelect() {
|
|
||||||
const names = this.workspaces;
|
|
||||||
const options = names.map(n => `<option value="${n}" ${n === this.currentWorkspace ? 'selected' : ''}>${n}</option>`).join('');
|
|
||||||
this.elements.workspaceSelect.innerHTML = options;
|
|
||||||
this.elements.deleteWorkspaceBtn.style.display = this.currentWorkspace && this.currentWorkspace !== 'default' ? 'inline-block' : 'none';
|
|
||||||
this.elements.currentWorkspaceName.textContent = this.currentWorkspace || 'Select a workspace';
|
|
||||||
}
|
|
||||||
|
|
||||||
renderWorkspaceDetails() {
|
|
||||||
const workspace = this.data.workspaces[this.currentWorkspace];
|
|
||||||
|
|
||||||
// Update header
|
|
||||||
this.elements.currentWorkspaceName.textContent = this.currentWorkspace;
|
|
||||||
|
|
||||||
// Show/hide buttons based on selection
|
|
||||||
const hasWorkspace = !!this.currentWorkspace;
|
|
||||||
this.elements.deleteWorkspaceBtn.style.display = hasWorkspace && this.currentWorkspace !== 'default' ? 'inline-block' : 'none';
|
|
||||||
this.elements.addDirectoryBtn.style.display = hasWorkspace ? 'inline-block' : 'none';
|
|
||||||
this.elements.copySelectionBtn.style.display = hasWorkspace ? 'inline-block' : 'none';
|
|
||||||
|
|
||||||
if (!workspace) {
|
|
||||||
this.elements.workspaceContent.innerHTML = '<p class="text-muted">Select a workspace to view its directories and files.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workspace.dirs.length === 0) {
|
|
||||||
this.elements.workspaceContent.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<div class="empty-state-icon">📁</div>
|
|
||||||
<p>No directories added to this workspace.</p>
|
|
||||||
<p class="text-muted">Click "Add Directory" to get started.</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load current workspace details from backend
|
|
||||||
try {
|
|
||||||
await this.loadCurrentWorkspaceDetails();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
this.elements.workspaceContent.innerHTML = '<p class="text-muted">Failed to load workspace details.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirs = Array.from(this.selectedDirs);
|
|
||||||
if (dirs.length === 0) {
|
|
||||||
this.elements.workspaceContent.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<div class="empty-state-icon">📁</div>
|
|
||||||
<p>No directories added to this workspace.</p>
|
|
||||||
<p class="text-muted">Click "Add Directory" to get started.</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render directories as expandable tree explorers
|
|
||||||
this.elements.workspaceContent.innerHTML = dirs.map((dirPath, dirIndex) => `
|
|
||||||
<div class="directory-item">
|
|
||||||
<div class="directory-header">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<strong>${this.getDirectoryName(dirPath)}</strong>
|
|
||||||
<div class="directory-path">${dirPath}</div>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group-actions">
|
|
||||||
<button class="btn btn-sm btn-outline-danger" onclick="heroprompt.removeDirectory('${dirPath}')">Remove</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="file-tree" id="file-tree-${dirIndex}"></div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// Populate tree nodes asynchronously
|
|
||||||
dirs.forEach((dirPath, dirIndex) => {
|
|
||||||
const container = document.getElementById(`file-tree-${dirIndex}`);
|
|
||||||
this.renderDirNode(dirPath, dirIndex, container, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Render a directory node with lazy subdir loading
|
|
||||||
async renderDirNode(dirPath, dirIndex, container, level) {
|
|
||||||
try {
|
|
||||||
// Fetch directory listing from backend
|
|
||||||
const data = await this.fetchDirectory(dirPath);
|
|
||||||
const items = data.items || [];
|
|
||||||
|
|
||||||
// Separate directories and files
|
|
||||||
const dirs = items.filter(it => it.type === 'directory').map(it => it.name);
|
|
||||||
const files = items.filter(it => it.type === 'file').map(it => it.name);
|
|
||||||
|
|
||||||
// Build HTML
|
|
||||||
const indent = ' '.repeat(level * 2);
|
|
||||||
const list = [];
|
|
||||||
|
|
||||||
dirs.forEach(sub => {
|
|
||||||
const subPath = `${dirPath}/${sub}`;
|
|
||||||
const nodeId = `dir-node-${dirIndex}-${level}-${sub.replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
||||||
list.push(`
|
|
||||||
<div class="file-item">
|
|
||||||
<span class="file-icon">📁</span>
|
|
||||||
<a href="#" class="toggle" data-target="${nodeId}" data-path="${subPath}">${indent}${sub}</a>
|
|
||||||
<div id="${nodeId}" class="children" style="display:none; margin-left:12px;"></div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
files.forEach(file => {
|
|
||||||
const absPath = `${dirPath}/${file}`;
|
|
||||||
const isSel = this.selectedFiles.has(absPath);
|
|
||||||
list.push(`
|
|
||||||
<div class="file-item ${isSel ? 'selected' : ''}" onclick="heroprompt.toggleFileSelection('${absPath}')">
|
|
||||||
<input type="checkbox" ${isSel ? 'checked' : ''} onclick="event.stopPropagation()">
|
|
||||||
<span class="file-icon ${this.getFileIconClass(file)}"></span>
|
|
||||||
<span class="file-name">${indent}${file}</span>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
container.innerHTML = list.join('');
|
|
||||||
|
|
||||||
// Bind toggles for lazy load
|
|
||||||
container.querySelectorAll('a.toggle').forEach(a => {
|
|
||||||
a.addEventListener('click', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const targetId = a.getAttribute('data-target');
|
|
||||||
const path = a.getAttribute('data-path');
|
|
||||||
const target = document.getElementById(targetId);
|
|
||||||
if (target.getAttribute('data-loaded') !== '1') {
|
|
||||||
await this.renderDirNode(path, dirIndex, target, level + 1);
|
|
||||||
target.setAttribute('data-loaded', '1');
|
|
||||||
}
|
|
||||||
target.style.display = (target.style.display === 'none') ? 'block' : 'none';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to render directory node:', e);
|
|
||||||
container.innerHTML = `<div class="text-muted small">Failed to load ${dirPath}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDirectoryName(path) {
|
|
||||||
return path.split('/').pop() || path.split('\\').pop() || path;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileIconClass(fileName) {
|
|
||||||
const ext = fileName.split('.').pop().toLowerCase();
|
|
||||||
return `file-${ext}` || 'file-default';
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast(message, type = 'info') {
|
|
||||||
const toast = this.elements.toast;
|
|
||||||
const toastBody = toast.querySelector('.toast-body');
|
|
||||||
|
|
||||||
toastBody.textContent = message;
|
|
||||||
|
|
||||||
// Set toast color based on type
|
|
||||||
toast.className = 'toast';
|
|
||||||
if (type === 'success') {
|
|
||||||
toast.classList.add('text-bg-success');
|
|
||||||
} else if (type === 'error') {
|
|
||||||
toast.classList.add('text-bg-danger');
|
|
||||||
} else {
|
|
||||||
toast.classList.add('text-bg-info');
|
|
||||||
}
|
|
||||||
|
|
||||||
const bsToast = new bootstrap.Toast(toast);
|
|
||||||
bsToast.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize when DOM is loaded
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
window.heroprompt = new Heroprompt();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Export for potential external use
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = Heroprompt;
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,55 @@
|
|||||||
module ui
|
module ui
|
||||||
|
|
||||||
// // Recursive menu renderer
|
import os
|
||||||
// fn menu_html(items []MenuItem, depth int, prefix string) string {
|
|
||||||
// mut out := []string{}
|
// Recursive menu renderer
|
||||||
// for i, it in items {
|
pub fn menu_html(items []MenuItem, depth int, prefix string) string {
|
||||||
// id := '${prefix}_${depth}_${i}'
|
mut out := []string{}
|
||||||
// if it.children.len > 0 {
|
for i, it in items {
|
||||||
// // expandable group
|
id := '${prefix}_${depth}_${i}'
|
||||||
// out << '<div class="item">'
|
if it.children.len > 0 {
|
||||||
// out << '<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#${id}" role="button" aria-expanded="${if depth == 0 {
|
// expandable group
|
||||||
// 'true'
|
out << '<div class="item">'
|
||||||
// } else {
|
out << '<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#${id}" role="button" aria-expanded="${if depth == 0 {
|
||||||
// 'false'
|
'true'
|
||||||
// }}" aria-controls="${id}">'
|
} else {
|
||||||
// out << '<span>${it.title}</span><span class="chev">›</span>'
|
'false'
|
||||||
// out << '</a>'
|
}}" aria-controls="${id}">'
|
||||||
// out << '<div class="collapse ${if depth == 0 { 'show' } else { '' }}" id="${id}">'
|
out << '<span>${it.title}</span><span class="chev">›</span>'
|
||||||
// out << '<div class="ms-2 mt-1">'
|
out << '</a>'
|
||||||
// out << menu_html(it.children, depth + 1, id)
|
out << '<div class="collapse ${if depth == 0 { 'show' } else { '' }}" id="${id}">'
|
||||||
// out << '</div>'
|
out << '<div class="ms-2 mt-1">'
|
||||||
// out << '</div>'
|
out << menu_html(it.children, depth + 1, id)
|
||||||
// out << '</div>'
|
out << '</div>'
|
||||||
// } else {
|
out << '</div>'
|
||||||
// // leaf
|
out << '</div>'
|
||||||
// out << '<a href="${it.href}" class="list-group-item list-group-item-action">${it.title}</a>'
|
} else {
|
||||||
// }
|
// leaf
|
||||||
// }
|
out << '<a href="${it.href}" class="list-group-item list-group-item-action">${it.title}</a>'
|
||||||
// return out.join('\n')
|
}
|
||||||
// }
|
}
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Language detection utility for code files
|
||||||
|
pub fn detect_lang(path string) string {
|
||||||
|
ext := os.file_ext(path).trim_left('.')
|
||||||
|
return match ext.to_lower() {
|
||||||
|
'v' { 'v' }
|
||||||
|
'js' { 'javascript' }
|
||||||
|
'ts' { 'typescript' }
|
||||||
|
'py' { 'python' }
|
||||||
|
'rs' { 'rust' }
|
||||||
|
'go' { 'go' }
|
||||||
|
'java' { 'java' }
|
||||||
|
'c', 'h' { 'c' }
|
||||||
|
'cpp', 'hpp', 'cc', 'hh' { 'cpp' }
|
||||||
|
'sh', 'bash' { 'bash' }
|
||||||
|
'json' { 'json' }
|
||||||
|
'yaml', 'yml' { 'yaml' }
|
||||||
|
'html', 'htm' { 'html' }
|
||||||
|
'css' { 'css' }
|
||||||
|
'md' { 'markdown' }
|
||||||
|
else { 'text' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ dify
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
import freeflowuniverse.herolib.installers.something.dify as dify_installer
|
import freeflowuniverse.herolib.installers.something.dify as dify_installer
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
To get started
|
To get started
|
||||||
|
|
||||||
```vlang
|
```v
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user