diff options
| -rw-r--r-- | helpfuncs.go | 216 | ||||
| -rw-r--r-- | main.go | 12 | ||||
| -rw-r--r-- | tui.go | 222 |
3 files changed, 225 insertions, 225 deletions
diff --git a/helpfuncs.go b/helpfuncs.go new file mode 100644 index 0000000..f238cd4 --- /dev/null +++ b/helpfuncs.go @@ -0,0 +1,216 @@ +package main + +import ( + "fmt" + "gf-lt/models" + "gf-lt/pngmeta" + "image" + "os" + "path" + "strings" +) + +func colorText() { + text := textView.GetText(false) + quoteReplacer := strings.NewReplacer( + `”`, `"`, + `“`, `"`, + `“`, `"`, + `”`, `"`, + `**`, `*`, + ) + text = quoteReplacer.Replace(text) + // Step 1: Extract code blocks and replace them with unique placeholders + var codeBlocks []string + placeholder := "__CODE_BLOCK_%d__" + counter := 0 + // thinking + var thinkBlocks []string + placeholderThink := "__THINK_BLOCK_%d__" + counterThink := 0 + // Replace code blocks with placeholders and store their styled versions + text = codeBlockRE.ReplaceAllStringFunc(text, func(match string) string { + // Style the code block and store it + styled := fmt.Sprintf("[red::i]%s[-:-:-]", match) + codeBlocks = append(codeBlocks, styled) + // Generate a unique placeholder (e.g., "__CODE_BLOCK_0__") + id := fmt.Sprintf(placeholder, counter) + counter++ + return id + }) + text = thinkRE.ReplaceAllStringFunc(text, func(match string) string { + // Style the code block and store it + styled := fmt.Sprintf("[red::i]%s[-:-:-]", match) + thinkBlocks = append(thinkBlocks, styled) + // Generate a unique placeholder (e.g., "__CODE_BLOCK_0__") + id := fmt.Sprintf(placeholderThink, counterThink) + counterThink++ + return id + }) + // Step 2: Apply other regex styles to the non-code parts + text = quotesRE.ReplaceAllString(text, `[orange::-]$1[-:-:-]`) + text = starRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`) + // text = thinkRE.ReplaceAllString(text, `[yellow::i]$1[-:-:-]`) + // Step 3: Restore the styled code blocks from placeholders + for i, cb := range codeBlocks { + text = strings.Replace(text, fmt.Sprintf(placeholder, i), cb, 1) + } + logger.Debug("thinking debug", "blocks", thinkBlocks) + for i, tb := range thinkBlocks { + text = strings.Replace(text, fmt.Sprintf(placeholderThink, i), tb, 1) + } + textView.SetText(text) +} + +func updateStatusLine() { + position.SetText(makeStatusLine()) + helpView.SetText(fmt.Sprintf(helpText, makeStatusLine())) +} + +func initSysCards() ([]string, error) { + labels := []string{} + labels = append(labels, sysLabels...) + cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole, logger) + if err != nil { + logger.Error("failed to read sys dir", "error", err) + return nil, err + } + for _, cc := range cards { + if cc.Role == "" { + logger.Warn("empty role", "file", cc.FilePath) + continue + } + sysMap[cc.Role] = cc + labels = append(labels, cc.Role) + } + return labels, nil +} + +func startNewChat() { + id, err := store.ChatGetMaxID() + if err != nil { + logger.Error("failed to get chat id", "error", err) + } + if ok := charToStart(cfg.AssistantRole); !ok { + logger.Warn("no such sys msg", "name", cfg.AssistantRole) + } + // set chat body + chatBody.Messages = chatBody.Messages[:2] + textView.SetText(chatToText(cfg.ShowSys)) + newChat := &models.Chat{ + ID: id + 1, + Name: fmt.Sprintf("%d_%s", id+1, cfg.AssistantRole), + Msgs: string(defaultStarterBytes), + Agent: cfg.AssistantRole, + } + activeChatName = newChat.Name + chatMap[newChat.Name] = newChat + updateStatusLine() + colorText() +} + +func renameUser(oldname, newname string) { + if oldname == "" { + // not provided; deduce who user is + // INFO: if user not yet spoke, it is hard to replace mentions in sysprompt and first message about thme + roles := chatBody.ListRoles() + for _, role := range roles { + if role == cfg.AssistantRole { + continue + } + if role == cfg.ToolRole { + continue + } + if role == "system" { + continue + } + oldname = role + break + } + if oldname == "" { + // still + logger.Warn("fn: renameUser; failed to find old name", "newname", newname) + return + } + } + viewText := textView.GetText(false) + viewText = strings.ReplaceAll(viewText, oldname, newname) + chatBody.Rename(oldname, newname) + textView.SetText(viewText) +} + +func setLogLevel(sl string) { + switch sl { + case "Debug": + logLevel.Set(-4) + case "Info": + logLevel.Set(0) + case "Warn": + logLevel.Set(4) + } +} + +func listRolesWithUser() []string { + roles := chatBody.ListRoles() + if !strInSlice(cfg.UserRole, roles) { + roles = append(roles, cfg.UserRole) + } + return roles +} + +func loadImage() { + filepath := defaultImage + cc, ok := sysMap[cfg.AssistantRole] + if ok { + if strings.HasSuffix(cc.FilePath, ".png") { + filepath = cc.FilePath + } + } + file, err := os.Open(filepath) + if err != nil { + panic(err) + } + defer file.Close() + img, _, err := image.Decode(file) + if err != nil { + panic(err) + } + imgView.SetImage(img) +} + +func strInSlice(s string, sl []string) bool { + for _, el := range sl { + if strings.EqualFold(s, el) { + return true + } + } + return false +} + +func makeStatusLine() string { + isRecording := false + if asr != nil { + isRecording = asr.IsRecording() + } + persona := cfg.UserRole + if cfg.WriteNextMsgAs != "" { + persona = cfg.WriteNextMsgAs + } + botPersona := cfg.AssistantRole + if cfg.WriteNextMsgAsCompletionAgent != "" { + botPersona = cfg.WriteNextMsgAsCompletionAgent + } + // Add image attachment info to status line + var imageInfo string + if imageAttachmentPath != "" { + // Get just the filename from the path + imageName := path.Base(imageAttachmentPath) + imageInfo = fmt.Sprintf(" | attached img: [orange:-:b]%s[-:-:-]", imageName) + } else { + imageInfo = "" + } + statusLine := fmt.Sprintf(indexLineCompletion, botRespMode, cfg.AssistantRole, activeChatName, + cfg.ToolUse, chatBody.Model, cfg.SkipLLMResp, cfg.CurrentAPI, cfg.ThinkUse, logLevel.Level(), + isRecording, persona, botPersona, injectRole) + return statusLine + imageInfo +} @@ -9,13 +9,13 @@ import ( ) var ( - botRespMode = false - editMode = false - injectRole = true - selectedIndex = int(-1) - currentAPIIndex = 0 // Index to track current API in ApiLinks slice + botRespMode = false + editMode = false + injectRole = true + selectedIndex = int(-1) + currentAPIIndex = 0 // Index to track current API in ApiLinks slice // indexLine = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10)\nAPI_URL: [orange:-:b]%s[-:-:-] (ctrl+v) | ThinkUse: [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q)" - indexLineCompletion = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10)\nAPI_URL: [orange:-:b]%s[-:-:-] (ctrl+v) | ThinkUse: [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | Bot will write as [orange:-:b]%s[-:-:-] (ctrl+x) | role_inject [orange:-:b]%v[-:-:-]" + indexLineCompletion = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | card's char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l) | skip LLM resp: [orange:-:b]%v[-:-:-] (F10)\nAPI_URL: [orange:-:b]%s[-:-:-] (ctrl+v) | Insert <think>: [orange:-:b]%v[-:-:-] (ctrl+p) | Log Level: [orange:-:b]%v[-:-:-] (ctrl+p) | Recording: [orange:-:b]%v[-:-:-] (ctrl+r) | Writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | Bot will write as [orange:-:b]%s[-:-:-] (ctrl+x) | role_inject [orange:-:b]%v[-:-:-]" focusSwitcher = map[tview.Primitive]tview.Primitive{} ) @@ -4,7 +4,6 @@ import ( "fmt" "gf-lt/extra" "gf-lt/models" - "gf-lt/pngmeta" "image" _ "image/jpeg" _ "image/png" @@ -66,7 +65,7 @@ var ( [yellow]Ctrl+e[white]: export chat to json file [yellow]Ctrl+c[white]: close programm [yellow]Ctrl+n[white]: start a new chat -[yellow]Ctrl+o[white]: open file picker +[yellow]Ctrl+o[white]: open image file picker [yellow]Ctrl+p[white]: props edit form (min-p, dry, etc.) [yellow]Ctrl+v[white]: switch between /completion and /chat api (if provided in config) [yellow]Ctrl+r[white]: start/stop recording from your microphone (needs stt server) @@ -79,7 +78,6 @@ var ( [yellow]Ctrl+y[white]: list loaded RAG files (view and manage loaded files) [yellow]Ctrl+q[white]: cycle through mentioned chars in chat, to pick persona to send next msg as [yellow]Ctrl+x[white]: cycle through mentioned chars in chat, to pick persona to send next msg as (for llm) -RAG Window: [yellow]x[white]: close window | [yellow]Enter[white]: select action %s @@ -141,206 +139,6 @@ Press Enter to go back } ) -func loadImage() { - filepath := defaultImage - cc, ok := sysMap[cfg.AssistantRole] - if ok { - if strings.HasSuffix(cc.FilePath, ".png") { - filepath = cc.FilePath - } - } - file, err := os.Open(filepath) - if err != nil { - panic(err) - } - defer file.Close() - img, _, err := image.Decode(file) - if err != nil { - panic(err) - } - imgView.SetImage(img) -} - -func strInSlice(s string, sl []string) bool { - for _, el := range sl { - if strings.EqualFold(s, el) { - return true - } - } - return false -} - -func colorText() { - text := textView.GetText(false) - quoteReplacer := strings.NewReplacer( - `”`, `"`, - `“`, `"`, - `“`, `"`, - `”`, `"`, - `**`, `*`, - ) - text = quoteReplacer.Replace(text) - // Step 1: Extract code blocks and replace them with unique placeholders - var codeBlocks []string - placeholder := "__CODE_BLOCK_%d__" - counter := 0 - // thinking - var thinkBlocks []string - placeholderThink := "__THINK_BLOCK_%d__" - counterThink := 0 - // Replace code blocks with placeholders and store their styled versions - text = codeBlockRE.ReplaceAllStringFunc(text, func(match string) string { - // Style the code block and store it - styled := fmt.Sprintf("[red::i]%s[-:-:-]", match) - codeBlocks = append(codeBlocks, styled) - // Generate a unique placeholder (e.g., "__CODE_BLOCK_0__") - id := fmt.Sprintf(placeholder, counter) - counter++ - return id - }) - text = thinkRE.ReplaceAllStringFunc(text, func(match string) string { - // Style the code block and store it - styled := fmt.Sprintf("[red::i]%s[-:-:-]", match) - thinkBlocks = append(thinkBlocks, styled) - // Generate a unique placeholder (e.g., "__CODE_BLOCK_0__") - id := fmt.Sprintf(placeholderThink, counterThink) - counterThink++ - return id - }) - // Step 2: Apply other regex styles to the non-code parts - text = quotesRE.ReplaceAllString(text, `[orange::-]$1[-:-:-]`) - text = starRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`) - // text = thinkRE.ReplaceAllString(text, `[yellow::i]$1[-:-:-]`) - // Step 3: Restore the styled code blocks from placeholders - for i, cb := range codeBlocks { - text = strings.Replace(text, fmt.Sprintf(placeholder, i), cb, 1) - } - logger.Debug("thinking debug", "blocks", thinkBlocks) - for i, tb := range thinkBlocks { - text = strings.Replace(text, fmt.Sprintf(placeholderThink, i), tb, 1) - } - textView.SetText(text) -} - -func makeStatusLine() string { - isRecording := false - if asr != nil { - isRecording = asr.IsRecording() - } - persona := cfg.UserRole - if cfg.WriteNextMsgAs != "" { - persona = cfg.WriteNextMsgAs - } - botPersona := cfg.AssistantRole - if cfg.WriteNextMsgAsCompletionAgent != "" { - botPersona = cfg.WriteNextMsgAsCompletionAgent - } - - // Add image attachment info to status line - var imageInfo string - if imageAttachmentPath != "" { - // Get just the filename from the path - imageName := path.Base(imageAttachmentPath) - imageInfo = fmt.Sprintf(" | attached img: [orange:-:b]%s[-:-:-]", imageName) - } else { - imageInfo = "" - } - - statusLine := fmt.Sprintf(indexLineCompletion, botRespMode, cfg.AssistantRole, activeChatName, - cfg.ToolUse, chatBody.Model, cfg.SkipLLMResp, cfg.CurrentAPI, cfg.ThinkUse, logLevel.Level(), - isRecording, persona, botPersona, injectRole) - - return statusLine + imageInfo -} - -func updateStatusLine() { - position.SetText(makeStatusLine()) - helpView.SetText(fmt.Sprintf(helpText, makeStatusLine())) -} - -func initSysCards() ([]string, error) { - labels := []string{} - labels = append(labels, sysLabels...) - cards, err := pngmeta.ReadDirCards(cfg.SysDir, cfg.UserRole, logger) - if err != nil { - logger.Error("failed to read sys dir", "error", err) - return nil, err - } - for _, cc := range cards { - if cc.Role == "" { - logger.Warn("empty role", "file", cc.FilePath) - continue - } - sysMap[cc.Role] = cc - labels = append(labels, cc.Role) - } - return labels, nil -} - -func renameUser(oldname, newname string) { - if oldname == "" { - // not provided; deduce who user is - // INFO: if user not yet spoke, it is hard to replace mentions in sysprompt and first message about thme - roles := chatBody.ListRoles() - for _, role := range roles { - if role == cfg.AssistantRole { - continue - } - if role == cfg.ToolRole { - continue - } - if role == "system" { - continue - } - oldname = role - break - } - if oldname == "" { - // still - logger.Warn("fn: renameUser; failed to find old name", "newname", newname) - return - } - } - viewText := textView.GetText(false) - viewText = strings.ReplaceAll(viewText, oldname, newname) - chatBody.Rename(oldname, newname) - textView.SetText(viewText) -} - -func startNewChat() { - id, err := store.ChatGetMaxID() - if err != nil { - logger.Error("failed to get chat id", "error", err) - } - if ok := charToStart(cfg.AssistantRole); !ok { - logger.Warn("no such sys msg", "name", cfg.AssistantRole) - } - // set chat body - chatBody.Messages = chatBody.Messages[:2] - textView.SetText(chatToText(cfg.ShowSys)) - newChat := &models.Chat{ - ID: id + 1, - Name: fmt.Sprintf("%d_%s", id+1, cfg.AssistantRole), - Msgs: string(defaultStarterBytes), - Agent: cfg.AssistantRole, - } - activeChatName = newChat.Name - chatMap[newChat.Name] = newChat - updateStatusLine() - colorText() -} - -func setLogLevel(sl string) { - switch sl { - case "Debug": - logLevel.Set(-4) - case "Info": - logLevel.Set(0) - case "Warn": - logLevel.Set(4) - } -} - func makePropsForm(props map[string]float32) *tview.Form { // https://github.com/rivo/tview/commit/0a18dea458148770d212d348f656988df75ff341 // no way to close a form by a key press; a shame. @@ -363,7 +161,7 @@ func makePropsForm(props map[string]float32) *tview.Form { }).AddDropDown("Select a model: ", modelList, 0, func(option string, optionIndex int) { chatBody.Model = option - }).AddDropDown("Write next message as: ", chatBody.ListRoles(), 0, + }).AddDropDown("Write next message as: ", listRolesWithUser(), 0, func(option string, optionIndex int) { cfg.WriteNextMsgAs = option }).AddInputField("new char to write msg as: ", "", 32, tview.InputFieldMaxLength(32), @@ -885,14 +683,7 @@ func init() { if cfg.WriteNextMsgAs != "" { persona = cfg.WriteNextMsgAs } - roles := chatBody.ListRoles() - if len(roles) == 0 { - logger.Warn("empty roles in chat") - return nil - } - if !strInSlice(cfg.UserRole, roles) { - roles = append(roles, cfg.UserRole) - } + roles := listRolesWithUser() logger.Info("list roles", "roles", roles) for i, role := range roles { if strings.EqualFold(role, persona) { @@ -1046,10 +837,3 @@ func init() { return event }) } - -// UpdateImageAttachmentStatus updates the UI to reflect the current image attachment status -func UpdateImageAttachmentStatus(imagePath string) { - // The image attachment status is now shown in the main status line - // Just update the status line to reflect the current image attachment - updateStatusLine() -} |
