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
This commit is contained in:
Mahmoud-Emad
2025-08-21 10:49:02 +03:00
parent 3988548fe9
commit 854eb9972b
16 changed files with 2493 additions and 1128 deletions

View File

@@ -16,13 +16,15 @@ __global (
pub struct ArgsGet {
pub mut:
name string = 'default'
path string
fromdb bool // will load from filesystem
create bool // default will not create if not exist
}
pub fn new(args ArgsGet) !&Workspace {
mut obj := Workspace{
name: args.name
name: args.name
base_path: args.path
}
set(obj)!
return get(name: args.name)!
@@ -120,13 +122,28 @@ pub fn play(mut plbook PlayBook) ! {
if !plbook.exists(filter: 'heroprompt.') {
return
}
mut install_actions := plbook.find(filter: 'heroprompt.configure')!
if install_actions.len > 0 {
for install_action in install_actions {
heroscript := install_action.heroscript()
mut obj2 := heroscript_loads(heroscript)!
set(obj2)!
}
// 1) Configure workspaces
mut cfg_actions := plbook.find(filter: 'heroprompt.configure')!
for cfg_action in cfg_actions {
heroscript := cfg_action.heroscript()
mut obj := heroscript_loads(heroscript)!
set(obj)!
}
// 2) Add directories
for action in plbook.find(filter: 'heroprompt.add_dir')! {
mut p := action.params
wsname := p.get_default('name', heroprompt_default)!
mut wsp := get(name: wsname)!
path := p.get('path') or { return error("heroprompt.add_dir requires 'path'") }
wsp.add_dir(path: path)!
}
// 3) Add files
for action in plbook.find(filter: 'heroprompt.add_file')! {
mut p := action.params
wsname := p.get_default('name', heroprompt_default)!
mut wsp := get(name: wsname)!
path := p.get('path') or { return error("heroprompt.add_file requires 'path'") }
wsp.add_file(path: path)!
}
}

View File

@@ -1,7 +1,7 @@
module heroprompt
import freeflowuniverse.herolib.data.encoderhero
import time
import freeflowuniverse.herolib.core.playbook
pub const version = '0.0.0'
const singleton = false
@@ -28,8 +28,23 @@ fn obj_init(mycfg_ Workspace) !Workspace {
/////////////NORMALLY NO NEED TO TOUCH
pub fn heroscript_loads(heroscript string) !Workspace {
// TODO: go from heroscript to object
//load playbook, and manually get the params out of the actions & fill in the object
$dbg;
return obj
mut pb := playbook.new(text: heroscript)!
// Accept either define or configure; prefer define if present
mut action_name := 'heroprompt.define'
if !pb.exists_once(filter: action_name) {
action_name = 'heroprompt.configure'
if !pb.exists_once(filter: action_name) {
return error("heroprompt: missing 'heroprompt.define' or 'heroprompt.configure' action")
}
}
mut action := pb.get(filter: action_name)!
mut p := action.params
return Workspace{
name: p.get_default('name', 'default')!
base_path: p.get_default('base_path', '')!
created: time.now()
updated: time.now()
children: []HeropromptChild{}
}
}

View File

@@ -6,175 +6,6 @@ import os
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.develop.codewalker
fn (wsp &Workspace) save() !&Workspace {
mut tmp := wsp
tmp.updated = time.now()
tmp.is_saved = true
set(tmp)!
return get(name: wsp.name)!
}
// // WorkspaceItem represents a file or directory in the workspace tree
// pub struct WorkspaceItem {
// pub mut:
// name string // Item name (file or directory name)
// path string // Full path to the item
// is_directory bool // True if this is a directory
// is_file bool // True if this is a file
// size i64 // File size in bytes (0 for directories)
// extension string // File extension (empty for directories)
// children []WorkspaceItem // Child items (for directories)
// is_expanded bool // Whether directory is expanded in UI
// is_selected bool // Whether this item is selected for prompts
// depth int // Depth level in the tree (0 = root)
// }
// // WorkspaceList represents the complete hierarchical listing of a workspace
// pub struct WorkspaceList {
// pub mut:
// root_path string // Root path of the workspace
// items []WorkspaceItem // Top-level items in the workspace
// total_files int // Total number of files
// total_dirs int // Total number of directories
// }
// // list returns the complete hierarchical structure of the workspace
// pub fn (wsp Workspace) list() WorkspaceList {
// mut result := WorkspaceList{
// root_path: wsp.base_path
// }
// if wsp.base_path.len == 0 || !os.exists(wsp.base_path) {
// return result
// }
// // Build the complete tree structure (ALL files and directories)
// result.items = wsp.build_workspace_tree(wsp.base_path, 0)
// wsp.calculate_totals(result.items, mut result)
// // Mark selected items
// wsp.mark_selected_items(mut result.items)
// return result
// }
// // build_workspace_tree recursively builds the workspace tree structure
// fn (wsp Workspace) build_workspace_tree(path string, depth int) []WorkspaceItem {
// mut items := []WorkspaceItem{}
// entries := os.ls(path) or { return items }
// for entry in entries {
// full_path := os.join_path(path, entry)
// if os.is_dir(full_path) {
// mut dir_item := WorkspaceItem{
// name: entry
// path: full_path
// is_directory: true
// is_file: false
// size: 0
// extension: ''
// is_expanded: false
// is_selected: false
// depth: depth
// }
// // Recursively get children
// dir_item.children = wsp.build_workspace_tree(full_path, depth + 1)
// items << dir_item
// } else if os.is_file(full_path) {
// file_info := os.stat(full_path) or { continue }
// extension := get_file_extension(entry)
// file_item := WorkspaceItem{
// name: entry
// path: full_path
// is_directory: false
// is_file: true
// size: file_info.size
// extension: extension
// children: []
// is_expanded: false
// is_selected: false
// depth: depth
// }
// items << file_item
// }
// }
// // Sort: directories first, then files, both alphabetically
// items.sort_with_compare(fn (a &WorkspaceItem, b &WorkspaceItem) int {
// if a.is_directory && !b.is_directory {
// return -1
// }
// if !a.is_directory && b.is_directory {
// return 1
// }
// if a.name < b.name {
// return -1
// }
// if a.name > b.name {
// return 1
// }
// return 0
// })
// return items
// }
// // calculate_totals counts total files and directories in the workspace
// fn (wsp Workspace) calculate_totals(items []WorkspaceItem, mut result WorkspaceList) {
// for item in items {
// if item.is_directory {
// result.total_dirs++
// wsp.calculate_totals(item.children, mut result)
// } else {
// result.total_files++
// }
// }
// }
// // mark_selected_items marks which items are currently selected for prompts
// fn (wsp Workspace) mark_selected_items(mut items []WorkspaceItem) {
// for mut item in items {
// // Check if this item is selected by comparing paths
// item.is_selected = wsp.is_item_selected(item.path)
// // Recursively mark children
// if item.is_directory && item.children.len > 0 {
// wsp.mark_selected_items(mut item.children)
// }
// }
// }
// // is_item_selected checks if a specific path is selected in the workspace
// fn (wsp Workspace) is_item_selected(path string) bool {
// dirs := wsp.children.filter(fn (item &HeropromptChild) bool {
// return item.path.cat == .dir
// })
// for dir in dirs {
// if dir.path.path == path {
// return true
// }
// files := dir.children.filter(fn (item &HeropromptChild) bool {
// return item.path.cat == .file
// })
// for file in files {
// if file.path.path == path {
// return true
// }
// }
// child_dirs := dir.children.filter(fn (item &HeropromptChild) bool {
// return item.path.cat == .dir
// })
// if wsp.is_path_in_selected_dirs(path, child_dirs) {
// return true
// }
// }
// return false
// }
// Selection API
@[params]
pub struct AddDirParams {
@@ -329,6 +160,44 @@ 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)!
@@ -338,6 +207,48 @@ 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()
@@ -558,316 +469,16 @@ pub fn (wsp Workspace) prompt(args WorkspacePrompt) string {
return reprompt
}
// // is_path_in_selected_dirs recursively checks subdirectories for selected items
// fn (wsp Workspace) is_path_in_selected_dirs(path string, dirs []&HeropromptChild) bool {
// for dir in dirs {
// if dir.path.cat != .dir {
// continue
// }
// if dir.path.path == path {
// return true
// }
// files := dir.children.filter(fn (item &HeropromptChild) bool {
// return item.path.cat == .file
// })
// for file in files {
// if file.path.path == path {
// return true
// }
// }
// child_dirs := dir.children.filter(fn (item &HeropromptChild) bool {
// return item.path.cat == .dir
// })
// if wsp.is_path_in_selected_dirs(path, child_dirs) {
// return true
// }
// }
// return false
// }
// 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)!
}
// @[params]
// pub struct AddDirParams {
// pub mut:
// path string @[required]
// select_all bool
// }
// pub fn (mut wsp Workspace) add_dir(args_ AddDirParams) !&HeropromptChild {
// if args_.path.len == 0 {
// return error('The dir path is required')
// }
// if !os.exists(args_.path) {
// return error('The provided path does not exists')
// }
// abs_path := os.real_path(args_.path)
// parts := abs_path.split(os.path_separator)
// dir_name := parts[parts.len - 1]
// mut added := &HeropromptChild{
// path: pathlib.Path{
// path: abs_path
// cat: .dir
// exist: .yes
// }
// name: dir_name
// }
// if args_.select_all {
// added.select_all_files_and_dirs(abs_path)
// }
// wsp.children << added
// return added
// }
// // Metadata structures for selected files and directories
// struct SelectedFilesMetadata {
// content_length int // File content length in characters
// extension string // File extension
// name string // File name
// path string // Full file path
// }
// struct SelectedDirsMetadata {
// name string // Directory name
// selected_files []SelectedFilesMetadata // Files in this directory
// }
// struct WorkspaceGetSelected {
// pub mut:
// dirs []SelectedDirsMetadata // All directories with their selected files
// }
// pub fn (wsp Workspace) get_selected() WorkspaceGetSelected {
// mut result := WorkspaceGetSelected{}
// for dir in wsp.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .dir
// }) {
// mut files := []SelectedFilesMetadata{}
// for file in dir.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .file
// }) {
// files << SelectedFilesMetadata{
// content_length: file.content.len
// extension: get_file_extension(file.name)
// name: file.name
// path: file.path.path
// }
// }
// result.dirs << SelectedDirsMetadata{
// name: dir.name
// selected_files: files
// }
// }
// return result
// }
// pub struct WorkspacePrompt {
// pub mut:
// text string
// }
// pub fn (wsp Workspace) prompt(args WorkspacePrompt) string {
// prompt := wsp.build_prompt(args.text)
// return prompt
// }
// // Placeholder function for future needs, in case we need to highlight the user_instructions block with some addtional messages
// fn (wsp Workspace) build_user_instructions(text string) string {
// return text
// }
// // build_file_tree creates a tree-like representation of directories and files
// fn build_file_tree(dirs []&HeropromptChild, prefix string) string {
// mut out := ''
// for i, dir in dirs {
// if dir.path.cat != .dir {
// continue
// }
// // Determine the correct tree connector
// connector := if i == dirs.len - 1 { ' ' } else { ' ' }
// // Directory name
// out += '${prefix}${connector}${dir.name}\n'
// // Calculate new prefix for children
// child_prefix := if i == dirs.len - 1 { prefix + ' ' } else { prefix + ' ' }
// // Total children (files + subdirs)
// files := dir.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .file
// })
// subdirs := dir.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .dir
// })
// total_children := files.len + subdirs.len
// // Files in this directory
// for j, file in files {
// file_connector := if j == total_children - 1 { ' ' } else { ' ' }
// out += '${child_prefix}${file_connector}${file.name} *\n'
// }
// // Recurse into subdirectories
// for j, sub_dir in subdirs {
// sub_connector := if files.len + j == total_children - 1 {
// ' '
// } else {
// ' '
// }
// out += '${child_prefix}${sub_connector}${sub_dir.name}\n'
// sub_prefix := if files.len + j == total_children - 1 {
// child_prefix + ' '
// } else {
// child_prefix + ' '
// }
// // Build content for this subdirectory directly without calling build_file_map again
// sub_files := sub_dir.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .file
// })
// sub_subdirs := sub_dir.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .dir
// })
// sub_total_children := sub_files.len + sub_subdirs.len
// for k, sub_file in sub_files {
// sub_file_connector := if k == sub_total_children - 1 {
// ' '
// } else {
// ' '
// }
// out += '${sub_prefix}${sub_file_connector}${sub_file.name} *\n'
// }
// if sub_subdirs.len > 0 {
// out += build_file_tree(sub_subdirs, sub_prefix)
// }
// }
// }
// return out
// }
// // build_file_content generates formatted content for all selected files
// fn (wsp Workspace) build_file_content() string {
// mut content := ''
// for dir in wsp.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .dir
// }) {
// for file in dir.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .file
// }) {
// if content.len > 0 {
// content += '\n\n'
// }
// content += '${file.path.path}\n'
// extension := get_file_extension(file.name)
// if file.content.len == 0 {
// content += '(Empty file)\n'
// } else {
// content += '```${extension}\n'
// content += file.content
// content += '\n```'
// }
// }
// content += wsp.build_dir_file_content(dir.children)
// }
// return content
// }
// // build_dir_file_content recursively processes subdirectories
// fn (wsp Workspace) build_dir_file_content(dirs []&HeropromptChild) string {
// mut content := ''
// for dir in dirs {
// if dir.path.cat != .dir {
// continue
// }
// for file in dir.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .file
// }) {
// if content.len > 0 {
// content += '\n\n'
// }
// content += '${file.path.path}\n'
// extension := get_file_extension(file.name)
// if file.content.len == 0 {
// content += '(Empty file)\n'
// } else {
// content += '```${extension}\n'
// content += file.content
// content += '\n```'
// }
// }
// let_subdirs := dir.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .dir
// })
// if let_subdirs.len > 0 {
// content += wsp.build_dir_file_content(let_subdirs)
// }
// }
// return content
// }
// pub struct HeropromptTmpPrompt {
// pub mut:
// user_instructions string
// file_map string
// file_contents string
// }
// // build_prompt generates the final prompt with metadata and file tree
// fn (wsp Workspace) build_prompt(text string) string {
// user_instructions := wsp.build_user_instructions(text)
// file_map := wsp.build_file_map()
// file_contents := wsp.build_file_content()
// prompt := HeropromptTmpPrompt{
// user_instructions: user_instructions
// file_map: file_map
// file_contents: file_contents
// }
// reprompt := $tmpl('./templates/prompt.template')
// return reprompt
// }
// // build_file_map creates a complete file map with base path and metadata
// fn (wsp Workspace) build_file_map() string {
// mut file_map := ''
// // Consider only top-level directories as roots
// mut roots := wsp.children.filter(fn (c &HeropromptChild) bool {
// return c.path.cat == .dir
// })
// 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
// }
// selected_metadata := wsp.get_selected()
// mut total_files := 0
// mut total_content_length := 0
// mut file_extensions := map[string]int{}
// for dir_meta in selected_metadata.dirs {
// total_files += dir_meta.selected_files.len
// for file_meta in dir_meta.selected_files {
// total_content_length += file_meta.content_length
// if file_meta.extension.len > 0 {
// file_extensions[file_meta.extension] = file_extensions[file_meta.extension] + 1
// }
// }
// }
// 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(roots, '')
// }
// return file_map
// }
/// Generate a random name for the workspace
// Generate a random name for the workspace
fn generate_random_workspace_name() string {
adjectives := [
'brave',