From cec10210c284a17cd341b48ee205bcfd278205bd Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Wed, 3 Dec 2025 13:29:57 +0300 Subject: Feat: props table instead of form --- bot.go | 18 ++++ helpfuncs.go | 12 ++- props_table.go | 292 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tui.go | 4 +- 4 files changed, 321 insertions(+), 5 deletions(-) create mode 100644 props_table.go diff --git a/bot.go b/bot.go index 66786ca..bd1ce3f 100644 --- a/bot.go +++ b/bot.go @@ -32,6 +32,8 @@ var ( cfg *config.Config logger *slog.Logger logLevel = new(slog.LevelVar) +) +var ( activeChatName string chunkChan = make(chan string, 10) openAIToolChan = make(chan string, 10) @@ -65,6 +67,22 @@ var ( } ) +// GetLogLevel returns the current log level as a string +func GetLogLevel() string { + level := logLevel.Level() + switch level { + case slog.LevelDebug: + return "Debug" + case slog.LevelInfo: + return "Info" + case slog.LevelWarn: + return "Warn" + default: + // For any other values, return "Info" as default + return "Info" + } +} + func createClient(connectTimeout time.Duration) *http.Client { // Custom transport with connection timeout transport := &http.Transport{ diff --git a/helpfuncs.go b/helpfuncs.go index d73befe..c9f2a0d 100644 --- a/helpfuncs.go +++ b/helpfuncs.go @@ -152,10 +152,16 @@ func setLogLevel(sl string) { func listRolesWithUser() []string { roles := chatBody.ListRoles() - if !strInSlice(cfg.UserRole, roles) { - roles = append(roles, cfg.UserRole) + // Remove user role if it exists in the list (to avoid duplicates and ensure it's at position 0) + filteredRoles := make([]string, 0, len(roles)) + for _, role := range roles { + if role != cfg.UserRole { + filteredRoles = append(filteredRoles, role) + } } - return roles + // Prepend user role to the beginning of the list + result := append([]string{cfg.UserRole}, filteredRoles...) + return result } func loadImage() { diff --git a/props_table.go b/props_table.go new file mode 100644 index 0000000..c3dd7b2 --- /dev/null +++ b/props_table.go @@ -0,0 +1,292 @@ +package main + +import ( + "fmt" + "slices" + "strconv" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// Define constants for cell types +const ( + CellTypeCheckbox = "checkbox" + CellTypeDropdown = "dropdown" + CellTypeInput = "input" + CellTypeHeader = "header" +) + +// CellData holds additional data for each cell +type CellData struct { + Type string + Options []string + OnChange interface{} +} + +// makePropsTable creates a table-based alternative to the props form +// This allows for better key bindings and immediate effect of changes +func makePropsTable(props map[string]float32) *tview.Table { + // Create a new table + table := tview.NewTable(). + SetBorders(true). + SetSelectable(true, false) // Allow row selection but not column selection + + table.SetTitle("Properties Configuration (Press 'x' to exit)"). + SetTitleAlign(tview.AlignLeft) + + row := 0 + + // Add a header or note row + headerCell := tview.NewTableCell("Props for llamacpp completion call"). + SetTextColor(tcell.ColorYellow). + SetAlign(tview.AlignLeft). + SetSelectable(false) + table.SetCell(row, 0, headerCell) + table.SetCell(row, 1, + tview.NewTableCell(""). + SetTextColor(tcell.ColorYellow). + SetSelectable(false)) + row++ + + // Store cell data for later use in selection functions + cellData := make(map[string]*CellData) + + // Helper function to add a checkbox-like row + addCheckboxRow := func(label string, initialValue bool, onChange func(bool)) { + table.SetCell(row, 0, + tview.NewTableCell(label). + SetTextColor(tcell.ColorWhite). + SetAlign(tview.AlignLeft). + SetSelectable(false)) + + valueText := "No" + if initialValue { + valueText = "Yes" + } + + valueCell := tview.NewTableCell(valueText). + SetTextColor(tcell.ColorGreen). + SetAlign(tview.AlignCenter) + table.SetCell(row, 1, valueCell) + + // Store cell data + cellID := fmt.Sprintf("checkbox_%d", row) + cellData[cellID] = &CellData{ + Type: CellTypeCheckbox, + OnChange: onChange, + } + row++ + } + + // Helper function to add a dropdown-like row + addDropdownRow := func(label string, options []string, initialValue string, onChange func(string)) { + table.SetCell(row, 0, + tview.NewTableCell(label). + SetTextColor(tcell.ColorWhite). + SetAlign(tview.AlignLeft). + SetSelectable(false)) + + valueCell := tview.NewTableCell(initialValue). + SetTextColor(tcell.ColorGreen). + SetAlign(tview.AlignCenter) + table.SetCell(row, 1, valueCell) + + // Store cell data + cellID := fmt.Sprintf("dropdown_%d", row) + cellData[cellID] = &CellData{ + Type: CellTypeDropdown, + Options: options, + OnChange: onChange, + } + row++ + } + + // Helper function to add an input field row + addInputRow := func(label string, initialValue string, onChange func(string)) { + table.SetCell(row, 0, + tview.NewTableCell(label). + SetTextColor(tcell.ColorWhite). + SetAlign(tview.AlignLeft). + SetSelectable(false)) + + valueCell := tview.NewTableCell(initialValue). + SetTextColor(tcell.ColorGreen). + SetAlign(tview.AlignCenter) + table.SetCell(row, 1, valueCell) + + // Store cell data + cellID := fmt.Sprintf("input_%d", row) + cellData[cellID] = &CellData{ + Type: CellTypeInput, + OnChange: onChange, + } + row++ + } + + // Add checkboxes + addCheckboxRow("Insert \U000F0E88 (/completion only)", cfg.ThinkUse, func(checked bool) { + cfg.ThinkUse = checked + }) + + addCheckboxRow("RAG use", cfg.RAGEnabled, func(checked bool) { + cfg.RAGEnabled = checked + }) + + addCheckboxRow("Inject role", injectRole, func(checked bool) { + injectRole = checked + }) + + // Add dropdowns + logLevels := []string{"Debug", "Info", "Warn"} + addDropdownRow("Set log level", logLevels, GetLogLevel(), func(option string) { + setLogLevel(option) + }) + + // Prepare API links dropdown - insert current API at the beginning + apiLinks := slices.Insert(cfg.ApiLinks, 0, cfg.CurrentAPI) + addDropdownRow("Select an api", apiLinks, cfg.CurrentAPI, func(option string) { + cfg.CurrentAPI = option + }) + + // Prepare model list dropdown + modelList := []string{chatBody.Model, "deepseek-chat", "deepseek-reasoner"} + modelList = append(modelList, ORFreeModels...) + addDropdownRow("Select a model", modelList, chatBody.Model, func(option string) { + chatBody.Model = option + }) + + // Role selection dropdown + addDropdownRow("Write next message as", listRolesWithUser(), cfg.WriteNextMsgAs, func(option string) { + cfg.WriteNextMsgAs = option + }) + + // Add input fields + addInputRow("New char to write msg as", "", func(text string) { + if text != "" { + cfg.WriteNextMsgAs = text + } + }) + + addInputRow("Username", cfg.UserRole, func(text string) { + if text != "" { + renameUser(cfg.UserRole, text) + cfg.UserRole = text + } + }) + + // Add property fields (the float32 values) + for propName, value := range props { + propName := propName // capture loop variable for closure + propValue := fmt.Sprintf("%v", value) + addInputRow(propName, propValue, func(text string) { + if val, err := strconv.ParseFloat(text, 32); err == nil { + props[propName] = float32(val) + } + }) + } + + // Set selection function to handle dropdown-like behavior + table.SetSelectedFunc(func(selectedRow, selectedCol int) { + // Only handle selection on the value column (column 1) + if selectedCol != 1 { + // If user selects the label column, move to the value column + if table.GetRowCount() > selectedRow && table.GetColumnCount() > 1 { + table.Select(selectedRow, 1) + } + return + } + + // Get the cell and its corresponding data + cell := table.GetCell(selectedRow, selectedCol) + cellID := fmt.Sprintf("checkbox_%d", selectedRow) + + // Check if it's a checkbox + if cellData[cellID] != nil && cellData[cellID].Type == CellTypeCheckbox { + data := cellData[cellID] + if onChange, ok := data.OnChange.(func(bool)); ok { + // Toggle the checkbox value + newValue := cell.Text == "No" + onChange(newValue) + if newValue { + cell.SetText("Yes") + } else { + cell.SetText("No") + } + } + return + } + + // Check for dropdown + dropdownCellID := fmt.Sprintf("dropdown_%d", selectedRow) + if cellData[dropdownCellID] != nil && cellData[dropdownCellID].Type == CellTypeDropdown { + data := cellData[dropdownCellID] + if onChange, ok := data.OnChange.(func(string)); ok && data.Options != nil { + // Find current option and cycle to next + currentValue := cell.Text + currentIndex := -1 + for i, opt := range data.Options { + if opt == currentValue { + currentIndex = i + break + } + } + + // Move to next option (cycle back to 0 if at end) + nextIndex := (currentIndex + 1) % len(data.Options) + newValue := data.Options[nextIndex] + + onChange(newValue) + cell.SetText(newValue) + } + return + } + + // Handle input fields by creating an input modal on selection + inputCellID := fmt.Sprintf("input_%d", selectedRow) + if cellData[inputCellID] != nil && cellData[inputCellID].Type == CellTypeInput { + data := cellData[inputCellID] + if onChange, ok := data.OnChange.(func(string)); ok { + // Create an input modal + currentValue := cell.Text + inputFld := tview.NewInputField() + inputFld.SetLabel("Edit value: ") + inputFld.SetText(currentValue) + inputFld.SetDoneFunc(func(key tcell.Key) { + if key == tcell.KeyEnter { + newText := inputFld.GetText() + onChange(newText) + cell.SetText(newText) // Update the table cell + } + pages.RemovePage("editModal") + }) + + // Create a simple modal with the input field + modalFlex := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(tview.NewBox(), 0, 1, false). // Spacer + AddItem(tview.NewFlex(). + AddItem(tview.NewBox(), 0, 1, false). // Spacer + AddItem(inputFld, 30, 1, true). // Input field + AddItem(tview.NewBox(), 0, 1, false), // Spacer + 0, 1, true). + AddItem(tview.NewBox(), 0, 1, false) // Spacer + + // Add modal page and make it visible + pages.AddPage("editModal", modalFlex, true, true) + } + return + } + }) + + // Set input capture to handle 'x' key for exiting + table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyRune && event.Rune() == 'x' { + pages.RemovePage(propsPage) + return nil + } + return event + }) + + return table +} \ No newline at end of file diff --git a/tui.go b/tui.go index 65cd2d0..819b3f8 100644 --- a/tui.go +++ b/tui.go @@ -912,8 +912,8 @@ func init() { return nil } if event.Key() == tcell.KeyCtrlP { - propsForm := makePropsForm(defaultLCPProps) - pages.AddPage(propsPage, propsForm, true, true) + propsTable := makePropsTable(defaultLCPProps) + pages.AddPage(propsPage, propsTable, true, true) return nil } if event.Key() == tcell.KeyCtrlN { -- cgit v1.2.3