From 7ca188dcdc9e98e3222a05a160006c76c1b84862 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Sun, 2 Feb 2025 13:25:24 +0300 Subject: Feat: code block copy; model update; [WIP:broke tables] --- README.md | 5 +++-- tables.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tools.go | 12 +++++++----- tui.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 126 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 759d018..fe4aca1 100644 --- a/README.md +++ b/README.md @@ -35,13 +35,13 @@ - DRY; + - keybind to switch between openai and llamacpp endpoints (chat vs completion); + ======= +- separate messages that are stored and chat and send to the bot, i.e. option to omit tool calls and thinking (there might be a point where they are no longer needed in ctx); + +- option to remove from chat history; + - char card is the sys message, but how about giving tools to char that does not have it? - lets say we have two (or more) agents with the same name across multiple chats. These agents go and ask db for topics they memorised. Now they can access topics that aren't meant for them. (so memory should have an option: shareable; that indicates if that memory can be shared across chats); - server mode: no tui but api calls with the func calling, rag, other middleware; - boolean flag to use/not use tools. I see it as a msg from a tool to an llm "Hey, it might be good idea to use me!"; - connection to a model status; (need to be tied to some event, perhaps its own shortcut even) -- separate messages that are stored and chat and send to the bot, i.e. option to omit tool calls and thinking (there might be a point where they are no longer needed in ctx); -- option to remove from chat history; ### FIX: - bot responding (or hanging) blocks everything; + @@ -69,3 +69,4 @@ - model info shold be an event and show disconnect status when fails; - message editing broke ( runtime error: index out of range [-1]); out of index - remove icons for agents/user; use only : +- table selection does not work; diff --git a/tables.go b/tables.go index 0b3ef01..35a2e60 100644 --- a/tables.go +++ b/tables.go @@ -358,3 +358,65 @@ func makeAgentTable(agentList []string) *tview.Table { }) return chatActTable } + +func makeCodeBlockTable(codeBlocks []string) *tview.Table { + actions := []string{"copy"} + rows, cols := len(codeBlocks), len(actions)+1 + table := tview.NewTable(). + SetBorders(true) + logger.Info("creating codeblock table", "len#", len(codeBlocks), "data", codeBlocks) + for r := 0; r < rows; r++ { + for c := 0; c < cols; c++ { + color := tcell.ColorWhite + previewLen := 30 + if len(codeBlocks[r]) < 30 { + previewLen = len(codeBlocks[r]) + } + if c < 1 { + table.SetCell(r, c, + tview.NewTableCell(codeBlocks[r][:previewLen]). + SetTextColor(color). + SetAlign(tview.AlignCenter)) + } else { + table.SetCell(r, c, + tview.NewTableCell(actions[c-1]). + SetTextColor(color). + SetAlign(tview.AlignCenter)) + } + } + } + logger.Info("filled table", "len#", len(codeBlocks), "data", codeBlocks) + table.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { + if key == tcell.KeyEsc || key == tcell.KeyF1 { + pages.RemovePage(agentPage) + return + } + if key == tcell.KeyEnter { + table.SetSelectable(true, true) + } + }).SetSelectedFunc(func(row int, column int) { + tc := table.GetCell(row, column) + tc.SetTextColor(tcell.ColorRed) + table.SetSelectable(false, false) + selected := codeBlocks[row] + // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) + switch tc.Text { + case "copy": + if err := copyToClipboard(selected); err != nil { + if err := notifyUser("error", err.Error()); err != nil { + logger.Error("failed to send notification", "error", err) + } + } + if err := notifyUser("copied", selected); err != nil { + logger.Error("failed to send notification", "error", err) + } + pages.RemovePage(codeBlockPage) + app.SetFocus(textArea) + return + default: + pages.RemovePage(codeBlockPage) + return + } + }) + return table +} diff --git a/tools.go b/tools.go index 8fda0ab..94c1b16 100644 --- a/tools.go +++ b/tools.go @@ -9,11 +9,13 @@ import ( ) var ( - toolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`) - quotesRE = regexp.MustCompile(`(".*?")`) - starRE = regexp.MustCompile(`(\*.*?\*)`) - thinkRE = regexp.MustCompile(`(.*?)`) - // codeBlokRE = regexp.MustCompile(`(\x60\x60\x60.*?\x60\x60\x60)`) + toolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`) + quotesRE = regexp.MustCompile(`(".*?")`) + starRE = regexp.MustCompile(`(\*.*?\*)`) + thinkRE = regexp.MustCompile(`(.*?)`) + codeBlockRE = regexp.MustCompile(`(?s)\x60{3}(?:.*?)\n(.*?)\n\s*\x60{3}\s*`) + // codeBlockRE = regexp.MustCompile("```\s*([\s\S]*?)```") + // codeBlockRE = regexp.MustCompile(`(\x60\x60\x60.*?\x60\x60\x60)`) basicSysMsg = `Large Language Model that helps user with any of his requests.` toolSysMsg = `You're a helpful assistant. # Tools diff --git a/tui.go b/tui.go index dc8cdb5..f3d6f61 100644 --- a/tui.go +++ b/tui.go @@ -37,6 +37,7 @@ var ( RAGPage = "RAGPage " longStatusPage = "longStatusPage" propsPage = "propsPage" + codeBlockPage = "codeBlockPage" // help text helpText = ` [yellow]Esc[white]: send msg @@ -49,6 +50,7 @@ var ( [yellow]F6[white]: interrupt bot resp [yellow]F7[white]: copy last msg to clipboard (linux xclip) [yellow]F8[white]: copy n msg to clipboard (linux xclip) +[yellow]F9[white]: table to copy from; with all code blocks [yellow]F10[white]: manage loaded rag files (that already in vector db) [yellow]F11[white]: switch RAGEnabled boolean [yellow]F12[white]: show this help page @@ -65,7 +67,7 @@ Press Enter to go back ` ) -// // code block colors get interrupted by " & * +// code block colors get interrupted by " & * // func codeBlockColor(text string) string { // fi := strings.Index(text, "```") // if fi < 0 { @@ -78,14 +80,41 @@ Press Enter to go back // return strings.Replace(text, "```", "```[blue:black:i]", 1) // } +// func colorText() { +// // INFO: is there a better way to markdown? +// text := textView.GetText(false) +// text = quotesRE.ReplaceAllString(text, `[orange::-]$1[-:-:-]`) +// text = starRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`) +// text = thinkRE.ReplaceAllString(text, `[turquoise::i]$1[-:-:-]`) +// text = codeBlockRE.ReplaceAllString(text, "[blue::i]```\n$1\n```\n[-:-:-]") +// // text = codeBlockRE.ReplaceAllString(text, "[blue:black:i]```\n$1\n```\n[-:-:-]") +// textView.SetText(text) +// } + func colorText() { - // INFO: is there a better way to markdown? text := textView.GetText(false) - text = quotesRE.ReplaceAllString(text, `[orange:-:-]$1[-:-:-]`) + // Step 1: Extract code blocks and replace them with unique placeholders + var codeBlocks []string + placeholder := "__CODE_BLOCK_%d__" + counter := 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("[brown::i]%s[-:-:-]", match) + codeBlocks = append(codeBlocks, styled) + // Generate a unique placeholder (e.g., "__CODE_BLOCK_0__") + id := fmt.Sprintf(placeholder, counter) + counter++ + 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, `[turquoise::i]$1[-:-:-]`) - // cb := codeBlockColor(cq) - // cb := codeBlockRE.ReplaceAllString(cq, `[blue:black: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) + } textView.SetText(text) } @@ -428,6 +457,21 @@ func init() { pages.AddPage(indexPage, indexPickWindow, true, true) return nil } + if event.Key() == tcell.KeyF9 { + // table of codeblocks to copy + text := textView.GetText(false) + cb := codeBlockRE.FindAllString(text, -1) + if len(cb) == 0 { + if err := notifyUser("notify", "no code blocks in chat"); err != nil { + logger.Error("failed to send notification", "error", err) + } + return nil + } + table := makeCodeBlockTable(cb) + pages.AddPage(codeBlockPage, table, true, true) + // updateStatusLine() + return nil + } if event.Key() == tcell.KeyF10 { // list rag loaded in db loadedFiles, err := ragger.ListLoaded() @@ -476,6 +520,11 @@ func init() { startNewChat() return nil } + if event.Key() == tcell.KeyCtrlM { + fetchModelName() + updateStatusLine() + return nil + } if event.Key() == tcell.KeyCtrlT { // clear context // remove tools and thinking -- cgit v1.2.3