Files
herolib/lib/develop/heroprompt/heroprompt_workspace.v
Mahmoud-Emad 854eb9972b feat: integrate Heroprompt UI and backend
- Replace generic UI with dedicated Heroprompt web interface
- Implement new Heroprompt-specific backend APIs
- Develop client-side logic for file browsing and selection
- Enhance workspace configuration and management capabilities
- Remove deprecated generic UI modules and code
2025-08-21 10:49:02 +03:00

527 lines
12 KiB
V
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module heroprompt
import rand
import time
import os
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.develop.codewalker
// Selection API
@[params]
pub struct AddDirParams {
pub mut:
path string @[required]
}
@[params]
pub struct AddFileParams {
pub mut:
path string @[required]
}
// add a directory to the selection (no recursion stored; recursion is done on-demand)
pub fn (mut wsp Workspace) add_dir(args AddDirParams) !HeropromptChild {
if args.path.len == 0 {
return error('the directory path is required')
}
if !os.exists(args.path) || !os.is_dir(args.path) {
return error('path is not an existing directory: ${args.path}')
}
abs_path := os.real_path(args.path)
name := os.base(abs_path)
for child in wsp.children {
if child.name == name {
return error('another directory with the same name already exists: ${name}')
}
}
mut ch := HeropromptChild{
path: pathlib.Path{
path: abs_path
cat: .dir
exist: .yes
}
name: name
}
wsp.children << ch
wsp.save()!
return ch
}
// add a file to the selection
pub fn (mut wsp Workspace) add_file(args AddFileParams) !HeropromptChild {
if args.path.len == 0 {
return error('The file path is required')
}
if !os.exists(args.path) || !os.is_file(args.path) {
return error('Path is not an existing file: ${args.path}')
}
abs_path := os.real_path(args.path)
name := os.base(abs_path)
for child in wsp.children {
if child.path.cat == .file && child.name == name {
return error('another file with the same name already exists: ${name}')
}
if child.path.cat == .dir && child.name == name {
return error('${name}: is a directory, cannot add file with same name')
}
}
content := os.read_file(abs_path) or { '' }
mut ch := HeropromptChild{
path: pathlib.Path{
path: abs_path
cat: .file
exist: .yes
}
name: name
content: content
}
wsp.children << ch
wsp.save()!
return ch
}
// Removal API
@[params]
pub struct RemoveParams {
pub mut:
path string
name string
}
// Remove a directory from the selection (by absolute path or name)
pub fn (mut wsp Workspace) remove_dir(args RemoveParams) ! {
if args.path.len == 0 && args.name.len == 0 {
return error('either path or name is required to remove a directory')
}
mut idxs := []int{}
for i, ch in wsp.children {
if ch.path.cat != .dir {
continue
}
if args.path.len > 0 && os.real_path(args.path) == ch.path.path {
idxs << i
continue
}
if args.name.len > 0 && args.name == ch.name {
idxs << i
}
}
if idxs.len == 0 {
return error('no matching directory found to remove')
}
// remove from end to start to keep indices valid
idxs.sort(a > b)
for i in idxs {
wsp.children.delete(i)
}
wsp.save()!
}
// Remove a file from the selection (by absolute path or name)
pub fn (mut wsp Workspace) remove_file(args RemoveParams) ! {
if args.path.len == 0 && args.name.len == 0 {
return error('either path or name is required to remove a file')
}
mut idxs := []int{}
for i, ch in wsp.children {
if ch.path.cat != .file {
continue
}
if args.path.len > 0 && os.real_path(args.path) == ch.path.path {
idxs << i
continue
}
if args.name.len > 0 && args.name == ch.name {
idxs << i
}
}
if idxs.len == 0 {
return error('no matching file found to remove')
}
idxs.sort(a > b)
for i in idxs {
wsp.children.delete(i)
}
wsp.save()!
}
// Delete this workspace from the store
pub fn (wsp &Workspace) delete_workspace() ! {
delete(name: wsp.name)!
}
// Update this workspace (name and/or base_path)
@[params]
pub struct UpdateParams {
pub mut:
name string
base_path string
}
pub fn (wsp &Workspace) update_workspace(args UpdateParams) !&Workspace {
mut updated := Workspace{
name: if args.name.len > 0 { args.name } else { wsp.name }
base_path: if args.base_path.len > 0 { args.base_path } else { wsp.base_path }
children: wsp.children
created: wsp.created
updated: time.now()
is_saved: true
}
// if name changed, delete old key first
if updated.name != wsp.name {
delete(name: wsp.name)!
}
set(updated)!
return get(name: updated.name)!
}
// @[params]
// pub struct UpdateParams {
// pub mut:
// name string
// base_path string
// // Update only the name and the base path for now
// }
// // Delete this workspace from the store
// pub fn (wsp &Workspace) update_workspace(args_ UpdateParams) ! {
// delete(name: wsp.name)!
// }
// List workspaces (wrapper over factory list)
pub fn list_workspaces() ![]&Workspace {
return list(fromdb: false)!
}
pub fn list_workspaces_fromdb() ![]&Workspace {
return list(fromdb: true)!
}
// List entries in a directory relative to this workspace base or absolute
@[params]
pub struct ListArgs {
pub mut:
path string // if empty, will use workspace.base_path
}
pub struct ListItem {
pub:
name string
typ string @[json: 'type']
}
pub fn (wsp &Workspace) list() ![]ListItem {
mut dir := wsp.base_path
if dir.len == 0 {
return error('workspace base_path not set')
}
if !os.is_abs_path(dir) {
dir = os.join_path(wsp.base_path, dir)
}
entries := os.ls(dir) or { return error('cannot list directory') }
mut out := []ListItem{}
for e in entries {
full := os.join_path(dir, e)
if os.is_dir(full) {
out << ListItem{
name: e
typ: 'directory'
}
} else if os.is_file(full) {
out << ListItem{
name: e
typ: 'file'
}
}
}
return out
}
// Get the currently selected children (copy)
pub fn (wsp Workspace) selected_children() []HeropromptChild {
return wsp.children.clone()
}
// Build utilities
fn list_files_recursive(root string) []string {
mut out := []string{}
entries := os.ls(root) or { return out }
for e in entries {
fp := os.join_path(root, e)
if os.is_dir(fp) {
out << list_files_recursive(fp)
} else if os.is_file(fp) {
out << fp
}
}
return out
}
// build_file_content generates formatted content for all selected files (and all files under selected dirs)
fn (wsp Workspace) build_file_content() !string {
mut content := ''
// files selected directly
for ch in wsp.children {
if ch.path.cat == .file {
if content.len > 0 {
content += '\n\n'
}
content += '${ch.path.path}\n'
ext := get_file_extension(ch.name)
if ch.content.len == 0 {
// read on demand
ch_content := os.read_file(ch.path.path) or { '' }
if ch_content.len == 0 {
content += '(Empty file)\n'
} else {
content += '```' + ext + '\n' + ch_content + '\n```'
}
} else {
content += '```' + ext + '\n' + ch.content + '\n```'
}
}
}
// files under selected directories, using CodeWalker for filtered traversal
for ch in wsp.children {
if ch.path.cat == .dir {
mut cw := codewalker.new(codewalker.CodeWalkerArgs{})!
mut fm := cw.filemap_get(path: ch.path.path)!
for rel, fc in fm.content {
if content.len > 0 {
content += '\n\n'
}
abs := os.join_path(ch.path.path, rel)
content += abs + '\n'
ext := get_file_extension(os.base(abs))
if fc.len == 0 {
content += '(Empty file)\n'
} else {
content += '```' + ext + '\n' + fc + '\n```'
}
}
}
}
return content
}
// Minimal tree builder for selected directories only; marks files with *
fn build_file_tree_fs(roots []HeropromptChild, prefix string) string {
mut out := ''
for i, root in roots {
if root.path.cat != .dir {
continue
}
connector := if i == roots.len - 1 { ' ' } else { ' ' }
out += '${prefix}${connector}${root.name}\n'
child_prefix := if i == roots.len - 1 { prefix + ' ' } else { prefix + ' ' }
// list children under root
entries := os.ls(root.path.path) or { []string{} }
// sort: dirs first then files
mut dirs := []string{}
mut files := []string{}
for e in entries {
fp := os.join_path(root.path.path, e)
if os.is_dir(fp) {
dirs << fp
} else if os.is_file(fp) {
files << fp
}
}
dirs.sort()
files.sort()
// files
for j, f in files {
file_connector := if j == files.len - 1 && dirs.len == 0 {
' '
} else {
' '
}
out += '${child_prefix}${file_connector}${os.base(f)} *\n'
}
// subdirectories
for j, d in dirs {
sub_connector := if j == dirs.len - 1 { ' ' } else { ' ' }
out += '${child_prefix}${sub_connector}${os.base(d)}\n'
sub_prefix := if j == dirs.len - 1 {
child_prefix + ' '
} else {
child_prefix + ' '
}
out += build_file_tree_fs([
HeropromptChild{
path: pathlib.Path{
path: d
cat: .dir
exist: .yes
}
name: os.base(d)
},
], sub_prefix)
}
}
return out
}
pub struct HeropromptTmpPrompt {
pub mut:
user_instructions string
file_map string
file_contents string
}
fn (wsp Workspace) build_user_instructions(text string) string {
return text
}
// build_file_map creates a complete file map with base path and metadata
fn (wsp Workspace) build_file_map() string {
mut file_map := ''
// roots are selected directories
mut roots := []HeropromptChild{}
mut files_only := []HeropromptChild{}
for ch in wsp.children {
if ch.path.cat == .dir {
roots << ch
} else if ch.path.cat == .file {
files_only << ch
}
}
if roots.len > 0 {
base_path := roots[0].path.path
parent_path := if base_path.contains('/') {
base_path.split('/')[..base_path.split('/').len - 1].join('/')
} else {
base_path
}
// metadata
mut total_files := 0
mut total_content_length := 0
mut file_extensions := map[string]int{}
// files under dirs
for r in roots {
for f in list_files_recursive(r.path.path) {
total_files++
ext := get_file_extension(os.base(f))
if ext.len > 0 {
file_extensions[ext] = file_extensions[ext] + 1
}
total_content_length += (os.read_file(f) or { '' }).len
}
}
// files only
for fo in files_only {
total_files++
ext := get_file_extension(fo.name)
if ext.len > 0 {
file_extensions[ext] = file_extensions[ext] + 1
}
total_content_length += fo.content.len
}
mut extensions_summary := ''
for ext, count in file_extensions {
if extensions_summary.len > 0 {
extensions_summary += ', '
}
extensions_summary += '${ext}(${count})'
}
file_map = '${parent_path}\n'
file_map += '# Selected Files: ${total_files} | Total Content: ${total_content_length} chars'
if extensions_summary.len > 0 {
file_map += ' | Extensions: ${extensions_summary}'
}
file_map += '\n\n'
file_map += build_file_tree_fs(roots, '')
// list standalone files as well
for fo in files_only {
file_map += fo.path.path + ' *\n'
}
}
return file_map
}
pub struct WorkspacePrompt {
pub mut:
text string
}
pub fn (wsp Workspace) prompt(args WorkspacePrompt) string {
user_instructions := wsp.build_user_instructions(args.text)
file_map := wsp.build_file_map()
file_contents := wsp.build_file_content() or { '(Error building file contents)' }
prompt := HeropromptTmpPrompt{
user_instructions: user_instructions
file_map: file_map
file_contents: file_contents
}
reprompt := $tmpl('./templates/prompt.template')
return reprompt
}
// Save the workspace
fn (wsp &Workspace) save() !&Workspace {
mut tmp := wsp
tmp.updated = time.now()
tmp.is_saved = true
set(tmp)!
return get(name: wsp.name)!
}
// Generate a random name for the workspace
fn generate_random_workspace_name() string {
adjectives := [
'brave',
'bright',
'clever',
'swift',
'noble',
'mighty',
'fearless',
'bold',
'wise',
'epic',
'valiant',
'fierce',
'legendary',
'heroic',
'dynamic',
]
nouns := [
'forge',
'script',
'ocean',
'phoenix',
'atlas',
'quest',
'shield',
'dragon',
'code',
'summit',
'path',
'realm',
'spark',
'anvil',
'saga',
]
// Seed randomness with time
rand.seed([u32(time.now().unix()), u32(time.now().nanosecond)])
adj := adjectives[rand.intn(adjectives.len) or { 0 }]
noun := nouns[rand.intn(nouns.len) or { 0 }]
number := rand.intn(100) or { 0 } // 099
return '${adj}_${noun}_${number}'
}