Feat: rename user through props

This commit is contained in:
Grail Finder
2025-06-11 11:36:24 +03:00
parent 8902893f6e
commit c2da07ddc3
6 changed files with 139 additions and 81 deletions

2
bot.go
View File

@@ -437,6 +437,7 @@ func removeThinking(chatBody *models.ChatBody) {
func applyCharCard(cc *models.CharCard) { func applyCharCard(cc *models.CharCard) {
cfg.AssistantRole = cc.Role cfg.AssistantRole = cc.Role
// FIXME: remove
// Initialize Cluedo if enabled and matching role // Initialize Cluedo if enabled and matching role
if cfg.EnableCluedo && cc.Role == "CluedoPlayer" { if cfg.EnableCluedo && cc.Role == "CluedoPlayer" {
playerOrder = []string{cfg.UserRole, cfg.AssistantRole, cfg.CluedoRole2} playerOrder = []string{cfg.UserRole, cfg.AssistantRole, cfg.CluedoRole2}
@@ -444,6 +445,7 @@ func applyCharCard(cc *models.CharCard) {
} }
history, err := loadAgentsLastChat(cfg.AssistantRole) history, err := loadAgentsLastChat(cfg.AssistantRole)
if err != nil { 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) logger.Warn("failed to load last agent chat;", "agent", cc.Role, "err", err)
history = []models.RoleMsg{ history = []models.RoleMsg{
{Role: "system", Content: cc.SysPrompt}, {Role: "system", Content: cc.SysPrompt},

View File

@@ -9,7 +9,6 @@ import (
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
"regexp"
"strings" "strings"
"time" "time"
@@ -23,7 +22,7 @@ var (
TTSTextChan = make(chan string, 10000) TTSTextChan = make(chan string, 10000)
TTSFlushChan = make(chan bool, 1) TTSFlushChan = make(chan bool, 1)
TTSDoneChan = make(chan bool, 1) TTSDoneChan = make(chan bool, 1)
endsWithPunctuation = regexp.MustCompile(`[;.!?]$`) // endsWithPunctuation = regexp.MustCompile(`[;.!?]$`)
) )
type Orator interface { type Orator interface {

View File

@@ -12,7 +12,7 @@ var (
botRespMode = false botRespMode = false
editMode = false editMode = false
selectedIndex = int(-1) 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{} focusSwitcher = map[tview.Primitive]tview.Primitive{}
) )

View File

@@ -1,8 +1,8 @@
package models package models
import ( import (
"gf-lt/config"
"fmt" "fmt"
"gf-lt/config"
"strings" "strings"
) )
@@ -76,6 +76,27 @@ type ChatBody struct {
Messages []RoleMsg `json:"messages"` 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 { type ChatToolsBody struct {
Model string `json:"model"` Model string `json:"model"`
Messages []RoleMsg `json:"messages"` Messages []RoleMsg `json:"messages"`

106
tables.go
View File

@@ -267,59 +267,59 @@ func makeRAGTable(fileList []string) *tview.Flex {
return ragflex return ragflex
} }
func makeLoadedRAGTable(fileList []string) *tview.Table { // func makeLoadedRAGTable(fileList []string) *tview.Table {
actions := []string{"delete"} // actions := []string{"delete"}
rows, cols := len(fileList), len(actions)+1 // rows, cols := len(fileList), len(actions)+1
fileTable := tview.NewTable(). // fileTable := tview.NewTable().
SetBorders(true) // SetBorders(true)
for r := 0; r < rows; r++ { // for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ { // for c := 0; c < cols; c++ {
color := tcell.ColorWhite // color := tcell.ColorWhite
if c < 1 { // if c < 1 {
fileTable.SetCell(r, c, // fileTable.SetCell(r, c,
tview.NewTableCell(fileList[r]). // tview.NewTableCell(fileList[r]).
SetTextColor(color). // SetTextColor(color).
SetAlign(tview.AlignCenter)) // SetAlign(tview.AlignCenter))
} else { // } else {
fileTable.SetCell(r, c, // fileTable.SetCell(r, c,
tview.NewTableCell(actions[c-1]). // tview.NewTableCell(actions[c-1]).
SetTextColor(color). // SetTextColor(color).
SetAlign(tview.AlignCenter)) // SetAlign(tview.AlignCenter))
} // }
} // }
} // }
fileTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { // fileTable.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 { // if key == tcell.KeyEsc || key == tcell.KeyF1 {
pages.RemovePage(RAGPage) // pages.RemovePage(RAGPage)
return // return
} // }
if key == tcell.KeyEnter { // if key == tcell.KeyEnter {
fileTable.SetSelectable(true, true) // fileTable.SetSelectable(true, true)
} // }
}).SetSelectedFunc(func(row int, column int) { // }).SetSelectedFunc(func(row int, column int) {
defer pages.RemovePage(RAGPage) // defer pages.RemovePage(RAGPage)
tc := fileTable.GetCell(row, column) // tc := fileTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed) // tc.SetTextColor(tcell.ColorRed)
fileTable.SetSelectable(false, false) // fileTable.SetSelectable(false, false)
fpath := fileList[row] // fpath := fileList[row]
// notification := fmt.Sprintf("chat: %s; action: %s", fpath, tc.Text) // // notification := fmt.Sprintf("chat: %s; action: %s", fpath, tc.Text)
switch tc.Text { // switch tc.Text {
case "delete": // case "delete":
if err := ragger.RemoveFile(fpath); err != nil { // if err := ragger.RemoveFile(fpath); err != nil {
logger.Error("failed to delete file", "filename", fpath, "error", err) // logger.Error("failed to delete file", "filename", fpath, "error", err)
return // return
} // }
if err := notifyUser("chat deleted", fpath+" was deleted"); err != nil { // if err := notifyUser("chat deleted", fpath+" was deleted"); err != nil {
logger.Error("failed to send notification", "error", err) // logger.Error("failed to send notification", "error", err)
} // }
return // return
default: // default:
// pages.RemovePage(RAGPage) // // pages.RemovePage(RAGPage)
return // return
} // }
}) // })
return fileTable // return fileTable
} // }
func makeAgentTable(agentList []string) *tview.Table { func makeAgentTable(agentList []string) *tview.Table {
actions := []string{"load"} actions := []string{"load"}

76
tui.go
View File

@@ -43,6 +43,7 @@ var (
codeBlockPage = "codeBlockPage" codeBlockPage = "codeBlockPage"
imgPage = "imgPage" imgPage = "imgPage"
// help text // help text
// [yellow]F10[white]: manage loaded rag files (that already in vector db)
helpText = ` helpText = `
[yellow]Esc[white]: send msg [yellow]Esc[white]: send msg
[yellow]PgUp/Down[white]: switch focus between input and chat widgets [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]F7[white]: copy last msg to clipboard (linux xclip)
[yellow]F8[white]: copy n 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]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]F11[white]: import chat file
[yellow]F12[white]: show this help page [yellow]F12[white]: show this help page
[yellow]Ctrl+w[white]: resume generation on the last msg [yellow]Ctrl+w[white]: resume generation on the last msg
@@ -65,11 +65,12 @@ var (
[yellow]Ctrl+c[white]: close programm [yellow]Ctrl+c[white]: close programm
[yellow]Ctrl+p[white]: props edit form (min-p, dry, etc.) [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+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+t[white]: remove thinking (<think>) and tool messages from context (delete from chat)
[yellow]Ctrl+l[white]: update connected model name (llamacpp) [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+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+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 Press Enter to go back
` `
@@ -144,7 +145,7 @@ func updateStatusLine() {
if asr != nil { if asr != nil {
isRecording = asr.IsRecording() 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) { func initSysCards() ([]string, error) {
@@ -166,6 +167,36 @@ func initSysCards() ([]string, error) {
return labels, nil 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() { func startNewChat() {
id, err := store.ChatGetMaxID() id, err := store.ChatGetMaxID()
if err != nil { if err != nil {
@@ -218,6 +249,11 @@ func makePropsForm(props map[string]float32) *tview.Form {
}).AddDropDown("Select a model: ", []string{chatBody.Model, "deepseek-chat", "deepseek-reasoner"}, 0, }).AddDropDown("Select a model: ", []string{chatBody.Model, "deepseek-chat", "deepseek-reasoner"}, 0,
func(option string, optionIndex int) { func(option string, optionIndex int) {
chatBody.Model = option 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() { AddButton("Quit", func() {
pages.RemovePage(propsPage) pages.RemovePage(propsPage)
@@ -545,23 +581,23 @@ func init() {
// updateStatusLine() // updateStatusLine()
return nil return nil
} }
if event.Key() == tcell.KeyF10 { // if event.Key() == tcell.KeyF10 {
// list rag loaded in db // // list rag loaded in db
loadedFiles, err := ragger.ListLoaded() // loadedFiles, err := ragger.ListLoaded()
if err != nil { // if err != nil {
logger.Error("failed to list regfiles in db", "error", err) // logger.Error("failed to list regfiles in db", "error", err)
return nil // return nil
} // }
if len(loadedFiles) == 0 { // if len(loadedFiles) == 0 {
if err := notifyUser("loaded RAG", "no files in db"); err != nil { // if err := notifyUser("loaded RAG", "no files in db"); err != nil {
logger.Error("failed to send notification", "error", err) // logger.Error("failed to send notification", "error", err)
} // }
return nil // return nil
} // }
dbRAGTable := makeLoadedRAGTable(loadedFiles) // dbRAGTable := makeLoadedRAGTable(loadedFiles)
pages.AddPage(RAGPage, dbRAGTable, true, true) // pages.AddPage(RAGPage, dbRAGTable, true, true)
return nil // return nil
} // }
if event.Key() == tcell.KeyF11 { if event.Key() == tcell.KeyF11 {
// read files in chat_exports // read files in chat_exports
dirname := "chat_exports" dirname := "chat_exports"