diff options
-rw-r--r-- | bot.go | 2 | ||||
-rw-r--r-- | extra/tts.go | 9 | ||||
-rw-r--r-- | main.go | 2 | ||||
-rw-r--r-- | models/models.go | 23 | ||||
-rw-r--r-- | tables.go | 106 | ||||
-rw-r--r-- | tui.go | 78 |
6 files changed, 139 insertions, 81 deletions
@@ -437,6 +437,7 @@ func removeThinking(chatBody *models.ChatBody) { func applyCharCard(cc *models.CharCard) { cfg.AssistantRole = cc.Role + // FIXME: remove // Initialize Cluedo if enabled and matching role if cfg.EnableCluedo && cc.Role == "CluedoPlayer" { playerOrder = []string{cfg.UserRole, cfg.AssistantRole, cfg.CluedoRole2} @@ -444,6 +445,7 @@ func applyCharCard(cc *models.CharCard) { } history, err := loadAgentsLastChat(cfg.AssistantRole) if err != nil { + // TODO: too much action for err != nil; loadAgentsLastChat needs to be split up logger.Warn("failed to load last agent chat;", "agent", cc.Role, "err", err) history = []models.RoleMsg{ {Role: "system", Content: cc.SysPrompt}, diff --git a/extra/tts.go b/extra/tts.go index 6cdecd4..31e6887 100644 --- a/extra/tts.go +++ b/extra/tts.go @@ -9,7 +9,6 @@ import ( "io" "log/slog" "net/http" - "regexp" "strings" "time" @@ -20,10 +19,10 @@ import ( ) var ( - TTSTextChan = make(chan string, 10000) - TTSFlushChan = make(chan bool, 1) - TTSDoneChan = make(chan bool, 1) - endsWithPunctuation = regexp.MustCompile(`[;.!?]$`) + TTSTextChan = make(chan string, 10000) + TTSFlushChan = make(chan bool, 1) + TTSDoneChan = make(chan bool, 1) + // endsWithPunctuation = regexp.MustCompile(`[;.!?]$`) ) type Orator interface { @@ -12,7 +12,7 @@ var ( botRespMode = false editMode = false selectedIndex = int(-1) - indexLine = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | RAGEnabled: [orange:-:b]%v[-:-:-] (F11) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l)\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)" + indexLine = "F12 to show keys help | bot resp mode: [orange:-:b]%v[-:-:-] (F6) | char: [orange:-:b]%s[-:-:-] (ctrl+s) | chat: [orange:-:b]%s[-:-:-] (F1) | toolUseAdviced: [orange:-:b]%v[-:-:-] (ctrl+k) | model: [orange:-:b]%s[-:-:-] (ctrl+l)\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)" focusSwitcher = map[tview.Primitive]tview.Primitive{} ) diff --git a/models/models.go b/models/models.go index 2e2ef34..918e35e 100644 --- a/models/models.go +++ b/models/models.go @@ -1,8 +1,8 @@ package models import ( - "gf-lt/config" "fmt" + "gf-lt/config" "strings" ) @@ -76,6 +76,27 @@ type ChatBody struct { Messages []RoleMsg `json:"messages"` } +func (cb *ChatBody) Rename(oldname, newname string) { + for i, m := range cb.Messages { + cb.Messages[i].Content = strings.ReplaceAll(m.Content, oldname, newname) + cb.Messages[i].Role = strings.ReplaceAll(m.Role, oldname, newname) + } +} + +func (cb *ChatBody) ListRoles() []string { + namesMap := make(map[string]struct{}) + for _, m := range cb.Messages { + namesMap[m.Role] = struct{}{} + } + resp := make([]string, len(namesMap)) + i := 0 + for k := range namesMap { + resp[i] = k + i++ + } + return resp +} + type ChatToolsBody struct { Model string `json:"model"` Messages []RoleMsg `json:"messages"` @@ -267,59 +267,59 @@ func makeRAGTable(fileList []string) *tview.Flex { return ragflex } -func makeLoadedRAGTable(fileList []string) *tview.Table { - actions := []string{"delete"} - rows, cols := len(fileList), len(actions)+1 - fileTable := tview.NewTable(). - SetBorders(true) - for r := 0; r < rows; r++ { - for c := 0; c < cols; c++ { - color := tcell.ColorWhite - if c < 1 { - fileTable.SetCell(r, c, - tview.NewTableCell(fileList[r]). - SetTextColor(color). - SetAlign(tview.AlignCenter)) - } else { - fileTable.SetCell(r, c, - tview.NewTableCell(actions[c-1]). - SetTextColor(color). - SetAlign(tview.AlignCenter)) - } - } - } - fileTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { - if key == tcell.KeyEsc || key == tcell.KeyF1 { - pages.RemovePage(RAGPage) - return - } - if key == tcell.KeyEnter { - fileTable.SetSelectable(true, true) - } - }).SetSelectedFunc(func(row int, column int) { - defer pages.RemovePage(RAGPage) - tc := fileTable.GetCell(row, column) - tc.SetTextColor(tcell.ColorRed) - fileTable.SetSelectable(false, false) - fpath := fileList[row] - // notification := fmt.Sprintf("chat: %s; action: %s", fpath, tc.Text) - switch tc.Text { - case "delete": - if err := ragger.RemoveFile(fpath); err != nil { - logger.Error("failed to delete file", "filename", fpath, "error", err) - return - } - if err := notifyUser("chat deleted", fpath+" was deleted"); err != nil { - logger.Error("failed to send notification", "error", err) - } - return - default: - // pages.RemovePage(RAGPage) - return - } - }) - return fileTable -} +// func makeLoadedRAGTable(fileList []string) *tview.Table { +// actions := []string{"delete"} +// rows, cols := len(fileList), len(actions)+1 +// fileTable := tview.NewTable(). +// SetBorders(true) +// for r := 0; r < rows; r++ { +// for c := 0; c < cols; c++ { +// color := tcell.ColorWhite +// if c < 1 { +// fileTable.SetCell(r, c, +// tview.NewTableCell(fileList[r]). +// SetTextColor(color). +// SetAlign(tview.AlignCenter)) +// } else { +// fileTable.SetCell(r, c, +// tview.NewTableCell(actions[c-1]). +// SetTextColor(color). +// SetAlign(tview.AlignCenter)) +// } +// } +// } +// fileTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { +// if key == tcell.KeyEsc || key == tcell.KeyF1 { +// pages.RemovePage(RAGPage) +// return +// } +// if key == tcell.KeyEnter { +// fileTable.SetSelectable(true, true) +// } +// }).SetSelectedFunc(func(row int, column int) { +// defer pages.RemovePage(RAGPage) +// tc := fileTable.GetCell(row, column) +// tc.SetTextColor(tcell.ColorRed) +// fileTable.SetSelectable(false, false) +// fpath := fileList[row] +// // notification := fmt.Sprintf("chat: %s; action: %s", fpath, tc.Text) +// switch tc.Text { +// case "delete": +// if err := ragger.RemoveFile(fpath); err != nil { +// logger.Error("failed to delete file", "filename", fpath, "error", err) +// return +// } +// if err := notifyUser("chat deleted", fpath+" was deleted"); err != nil { +// logger.Error("failed to send notification", "error", err) +// } +// return +// default: +// // pages.RemovePage(RAGPage) +// return +// } +// }) +// return fileTable +// } func makeAgentTable(agentList []string) *tview.Table { actions := []string{"load"} @@ -43,6 +43,7 @@ var ( codeBlockPage = "codeBlockPage" imgPage = "imgPage" // help text + // [yellow]F10[white]: manage loaded rag files (that already in vector db) helpText = ` [yellow]Esc[white]: send msg [yellow]PgUp/Down[white]: switch focus between input and chat widgets @@ -55,7 +56,6 @@ var ( [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]: import chat file [yellow]F12[white]: show this help page [yellow]Ctrl+w[white]: resume generation on the last msg @@ -65,11 +65,12 @@ var ( [yellow]Ctrl+c[white]: close programm [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]: menu of files that can be loaded in vector db (RAG) +[yellow]Ctrl+r[white]: start/stop recording from your microphone (needs stt server) [yellow]Ctrl+t[white]: remove thinking (<think>) and tool messages from context (delete from chat) [yellow]Ctrl+l[white]: update connected model name (llamacpp) [yellow]Ctrl+k[white]: switch tool use (recommend tool use to llm after user msg) [yellow]Ctrl+j[white]: if chat agent is char.png will show the image; then any key to return +[yellow]Ctrl+a[white]: interrupt tts (needs tts server) Press Enter to go back ` @@ -144,7 +145,7 @@ func updateStatusLine() { if asr != nil { isRecording = asr.IsRecording() } - position.SetText(fmt.Sprintf(indexLine, botRespMode, cfg.AssistantRole, activeChatName, cfg.RAGEnabled, cfg.ToolUse, chatBody.Model, cfg.CurrentAPI, cfg.ThinkUse, logLevel.Level(), isRecording)) + position.SetText(fmt.Sprintf(indexLine, botRespMode, cfg.AssistantRole, activeChatName, cfg.ToolUse, chatBody.Model, cfg.CurrentAPI, cfg.ThinkUse, logLevel.Level(), isRecording)) } func initSysCards() ([]string, error) { @@ -166,6 +167,36 @@ func initSysCards() ([]string, error) { 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 { @@ -218,7 +249,12 @@ func makePropsForm(props map[string]float32) *tview.Form { }).AddDropDown("Select a model: ", []string{chatBody.Model, "deepseek-chat", "deepseek-reasoner"}, 0, func(option string, optionIndex int) { chatBody.Model = option - }). + }).AddInputField("username: ", cfg.UserRole, 32, tview.InputFieldMaxLength(32), func(text string) { + if text != "" { + renameUser(cfg.UserRole, text) + cfg.UserRole = text + } + }). AddButton("Quit", func() { pages.RemovePage(propsPage) }) @@ -545,23 +581,23 @@ func init() { // updateStatusLine() return nil } - if event.Key() == tcell.KeyF10 { - // list rag loaded in db - loadedFiles, err := ragger.ListLoaded() - if err != nil { - logger.Error("failed to list regfiles in db", "error", err) - return nil - } - if len(loadedFiles) == 0 { - if err := notifyUser("loaded RAG", "no files in db"); err != nil { - logger.Error("failed to send notification", "error", err) - } - return nil - } - dbRAGTable := makeLoadedRAGTable(loadedFiles) - pages.AddPage(RAGPage, dbRAGTable, true, true) - return nil - } + // if event.Key() == tcell.KeyF10 { + // // list rag loaded in db + // loadedFiles, err := ragger.ListLoaded() + // if err != nil { + // logger.Error("failed to list regfiles in db", "error", err) + // return nil + // } + // if len(loadedFiles) == 0 { + // if err := notifyUser("loaded RAG", "no files in db"); err != nil { + // logger.Error("failed to send notification", "error", err) + // } + // return nil + // } + // dbRAGTable := makeLoadedRAGTable(loadedFiles) + // pages.AddPage(RAGPage, dbRAGTable, true, true) + // return nil + // } if event.Key() == tcell.KeyF11 { // read files in chat_exports dirname := "chat_exports" |