Feat: rename chat

This commit is contained in:
Grail Finder
2024-11-24 17:39:52 +03:00
parent 6625a8103b
commit 5dfb558476
6 changed files with 327 additions and 249 deletions

View File

@@ -20,6 +20,7 @@
- consider adding use /completion of llamacpp, since openai endpoint clearly has template|format issues;
- change temp, min-p and other params from tui;
- help page with all key bindings;
- rename current chat;
### FIX:
- bot responding (or haninging) blocks everything; +
@@ -30,4 +31,4 @@
- empty input to continue bot msg gens new msg index and bot icon;
- sometimes bots put additional info around the tool call, have a regexp to match tool call; +
- remove all panics from code; +
- new chat is not saved in db;
- new chat replaces old ones in db;

249
main.go
View File

@@ -1,13 +1,8 @@
package main
import (
"elefant/models"
"fmt"
"strconv"
"time"
"unicode"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
@@ -16,7 +11,7 @@ var (
editMode = false
botMsg = "no"
selectedIndex = int(-1)
indexLine = "Esc: send msg; PgUp/Down: switch focus; F1: manage chats; F2: regen last; F3:delete last msg; F4: edit msg; F5: toggle system; F6: interrupt bot resp; Row: [yellow]%d[white], Column: [yellow]%d; bot resp mode: %v"
indexLine = "Esc: send msg; PgUp/Down: switch focus; F1: manage chats; F2: regen last; F3:delete last msg; F4: edit msg; F5: toggle system; F6: interrupt bot resp; bot resp mode: %v; current chat: %s"
focusSwitcher = map[tview.Primitive]tview.Primitive{}
)
@@ -30,248 +25,6 @@ func isASCII(s string) bool {
}
func main() {
app := tview.NewApplication()
pages := tview.NewPages()
textArea := tview.NewTextArea().
SetPlaceholder("Type your prompt...")
textArea.SetBorder(true).SetTitle("input")
textView := tview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetChangedFunc(func() {
app.Draw()
})
textView.SetBorder(true).SetTitle("chat")
focusSwitcher[textArea] = textView
focusSwitcher[textView] = textArea
position := tview.NewTextView().
SetDynamicColors(true).
SetTextAlign(tview.AlignCenter)
flex := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(textView, 0, 40, false).
AddItem(textArea, 0, 10, true).
AddItem(position, 0, 1, false)
updateStatusLine := func() {
fromRow, fromColumn, toRow, toColumn := textArea.GetCursor()
if fromRow == toRow && fromColumn == toColumn {
position.SetText(fmt.Sprintf(indexLine, fromRow, fromColumn, botRespMode))
} else {
position.SetText(fmt.Sprintf("Esc: send msg; PgUp/Down: switch focus; F1: manage chats; F2: regen last; F3:delete last msg; F4: edit msg; F5: toggle system; F6: interrupt bot resp; Row: [yellow]%d[white], Column: [yellow]%d[white] - [red]To[white] Row: [yellow]%d[white], To Column: [yellow]%d; bot resp mode: %v", fromRow, fromColumn, toRow, toColumn, botRespMode))
}
}
chatOpts := []string{"cancel", "new"}
fList, err := loadHistoryChats()
if err != nil {
logger.Error("failed to load chat history", "error", err)
}
chatOpts = append(chatOpts, fList...)
chatActModal := tview.NewModal().
SetText("Chat actions:").
AddButtons(chatOpts).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
switch buttonLabel {
case "new":
// set chat body
chatBody.Messages = defaultStarter
textView.SetText(chatToText(showSystemMsgs))
newChat := &models.Chat{
Name: fmt.Sprintf("%v_%v", "new", time.Now().Unix()),
Msgs: string(defaultStarterBytes),
}
// activeChatName = path.Join(historyDir, fmt.Sprintf("%d_chat.json", time.Now().Unix()))
activeChatName = newChat.Name
chatMap[newChat.Name] = newChat
pages.RemovePage("history")
return
// set text
case "cancel":
pages.RemovePage("history")
return
default:
fn := buttonLabel
history, err := loadHistoryChat(fn)
if err != nil {
logger.Error("failed to read history file", "filename", fn)
pages.RemovePage("history")
return
}
chatBody.Messages = history
textView.SetText(chatToText(showSystemMsgs))
activeChatName = fn
pages.RemovePage("history")
return
}
})
sysModal := tview.NewModal().
SetText("Switch sys msg:").
AddButtons(sysLabels).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
switch buttonLabel {
case "cancel":
pages.RemovePage("sys")
return
default:
sysMsg, ok := sysMap[buttonLabel]
if !ok {
logger.Warn("no such sys msg", "name", buttonLabel)
pages.RemovePage("sys")
return
}
chatBody.Messages[0].Content = sysMsg
// replace textview
textView.SetText(chatToText(showSystemMsgs))
pages.RemovePage("sys")
}
})
editArea := tview.NewTextArea().
SetPlaceholder("Replace msg...")
editArea.SetBorder(true).SetTitle("input")
editArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape && editMode {
editedMsg := editArea.GetText()
if editedMsg == "" {
notifyUser("edit", "no edit provided")
pages.RemovePage("editArea")
editMode = false
return nil
}
chatBody.Messages[selectedIndex].Content = editedMsg
// change textarea
textView.SetText(chatToText(showSystemMsgs))
pages.RemovePage("editArea")
editMode = false
return nil
}
return event
})
indexPickWindow := tview.NewInputField().
SetLabel("Enter a msg index: ").
SetFieldWidth(4).
SetAcceptanceFunc(tview.InputFieldInteger).
SetDoneFunc(func(key tcell.Key) {
pages.RemovePage("getIndex")
return
})
indexPickWindow.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
si := indexPickWindow.GetText()
selectedIndex, err = strconv.Atoi(si)
if err != nil {
logger.Error("failed to convert provided index", "error", err, "si", si)
}
if len(chatBody.Messages) <= selectedIndex && selectedIndex < 0 {
logger.Warn("chosen index is out of bounds", "index", selectedIndex)
return nil
}
m := chatBody.Messages[selectedIndex]
if editMode && event.Key() == tcell.KeyEnter {
pages.AddPage("editArea", editArea, true, true)
editArea.SetText(m.Content, true)
}
if !editMode && event.Key() == tcell.KeyEnter {
copyToClipboard(m.Content)
notification := fmt.Sprintf("msg '%s' was copied to the clipboard", m.Content[:30])
notifyUser("copied", notification)
}
return event
})
//
textArea.SetMovedFunc(updateStatusLine)
updateStatusLine()
textView.SetText(chatToText(showSystemMsgs))
textView.ScrollToEnd()
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyF1 {
fList, err := loadHistoryChats()
if err != nil {
logger.Error("failed to load chat history", "error", err)
return nil
}
chatOpts = append(chatOpts, fList...)
pages.AddPage("history", chatActModal, true, true)
return nil
}
if event.Key() == tcell.KeyF2 {
// regen last msg
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
textView.SetText(chatToText(showSystemMsgs))
go chatRound("", userRole, textView)
return nil
}
if event.Key() == tcell.KeyF3 {
// delete last msg
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
textView.SetText(chatToText(showSystemMsgs))
botRespMode = false // hmmm; is that correct?
return nil
}
if event.Key() == tcell.KeyF4 {
// edit msg
editMode = true
pages.AddPage("getIndex", indexPickWindow, true, true)
return nil
}
if event.Key() == tcell.KeyF5 {
// switch showSystemMsgs
showSystemMsgs = !showSystemMsgs
textView.SetText(chatToText(showSystemMsgs))
}
if event.Key() == tcell.KeyF6 {
interruptResp = true
botRespMode = false
return nil
}
if event.Key() == tcell.KeyF7 {
// copy msg to clipboard
editMode = false
m := chatBody.Messages[len(chatBody.Messages)-1]
copyToClipboard(m.Content)
notification := fmt.Sprintf("msg '%s' was copied to the clipboard", m.Content[:30])
notifyUser("copied", notification)
return nil
}
if event.Key() == tcell.KeyF8 {
// copy msg to clipboard
editMode = false
pages.AddPage("getIndex", indexPickWindow, true, true)
return nil
}
if event.Key() == tcell.KeyCtrlE {
textArea.SetText("pressed ctrl+e", true)
return nil
}
if event.Key() == tcell.KeyCtrlS {
// switch sys prompt
pages.AddPage("sys", sysModal, true, true)
return nil
}
// cannot send msg in editMode or botRespMode
if event.Key() == tcell.KeyEscape && !editMode && !botRespMode {
fromRow, fromColumn, _, _ := textArea.GetCursor()
position.SetText(fmt.Sprintf(indexLine, fromRow, fromColumn, botRespMode))
// read all text into buffer
msgText := textArea.GetText()
if msgText != "" {
fmt.Fprintf(textView, "\n(%d) <user>: %s\n", len(chatBody.Messages), msgText)
textArea.SetText("", true)
textView.ScrollToEnd()
}
// update statue line
go chatRound(msgText, userRole, textView)
return nil
}
if event.Key() == tcell.KeyPgUp || event.Key() == tcell.KeyPgDn {
currentF := app.GetFocus()
app.SetFocus(focusSwitcher[currentF])
return nil
}
if isASCII(string(event.Rune())) && !botRespMode {
// botRespMode = false
// fromRow, fromColumn, _, _ := textArea.GetCursor()
// position.SetText(fmt.Sprintf(indexLine, fromRow, fromColumn, botRespMode))
return event
}
return event
})
pages.AddPage("main", flex, true, true)
if err := app.SetRoot(pages,
true).EnableMouse(true).Run(); err != nil {

View File

@@ -37,6 +37,7 @@ func updateStorageChat(name string, msgs []models.MessagesStory) error {
return err
}
chat.UpdatedAt = time.Now()
// if new chat will create id
_, err = store.UpsertChat(chat)
return err
}

View File

@@ -12,12 +12,14 @@ func (p ProviderSQL) Memorise(m *models.Memory) (*models.Memory, error) {
query := "INSERT INTO memories (agent, topic, mind) VALUES (:agent, :topic, :mind) RETURNING *;"
stmt, err := p.db.PrepareNamed(query)
if err != nil {
p.logger.Error("failed to prepare stmt", "query", query, "error", err)
return nil, err
}
defer stmt.Close()
var memory models.Memory
err = stmt.Get(&memory, m)
if err != nil {
p.logger.Error("failed to insert memory", "query", query, "error", err)
return nil, err
}
return &memory, nil
@@ -28,6 +30,7 @@ func (p ProviderSQL) Recall(agent, topic string) (string, error) {
var mind string
err := p.db.Get(&mind, query, agent, topic)
if err != nil {
p.logger.Error("failed to get memory", "query", query, "error", err)
return "", err
}
return mind, nil
@@ -38,6 +41,7 @@ func (p ProviderSQL) RecallTopics(agent string) ([]string, error) {
var topics []string
err := p.db.Select(&topics, query, agent)
if err != nil {
p.logger.Error("failed to get topics", "query", query, "error", err)
return nil, err
}
return topics, nil

View File

@@ -19,6 +19,7 @@ type ChatHistory interface {
GetLastChat() (*models.Chat, error)
UpsertChat(chat *models.Chat) (*models.Chat, error)
RemoveChat(id uint32) error
ChatGetMaxID() (uint32, error)
}
type ProviderSQL struct {
@@ -66,6 +67,13 @@ func (p ProviderSQL) RemoveChat(id uint32) error {
return err
}
func (p ProviderSQL) ChatGetMaxID() (uint32, error) {
query := "SELECT MAX(id) FROM chats;"
var id uint32
err := p.db.Get(&id, query)
return id, err
}
func NewProviderSQL(dbPath string, logger *slog.Logger) FullRepo {
db, err := sqlx.Open("sqlite", dbPath)
if err != nil {

311
tui.go Normal file
View File

@@ -0,0 +1,311 @@
package main
import (
"elefant/models"
"fmt"
"strconv"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
var (
app *tview.Application
pages *tview.Pages
textArea *tview.TextArea
editArea *tview.TextArea
textView *tview.TextView
position *tview.TextView
flex *tview.Flex
chatActModal *tview.Modal
sysModal *tview.Modal
indexPickWindow *tview.InputField
renameWindow *tview.InputField
)
func init() {
app = tview.NewApplication()
pages = tview.NewPages()
textArea = tview.NewTextArea().
SetPlaceholder("Type your prompt...")
textArea.SetBorder(true).SetTitle("input")
textView = tview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetChangedFunc(func() {
app.Draw()
})
textView.SetBorder(true).SetTitle("chat")
focusSwitcher[textArea] = textView
focusSwitcher[textView] = textArea
position = tview.NewTextView().
SetDynamicColors(true).
SetTextAlign(tview.AlignCenter)
flex = tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(textView, 0, 40, false).
AddItem(textArea, 0, 10, true).
AddItem(position, 0, 1, false)
updateStatusLine := func() {
fromRow, fromColumn, toRow, toColumn := textArea.GetCursor()
if fromRow == toRow && fromColumn == toColumn {
position.SetText(fmt.Sprintf(indexLine, botRespMode, activeChatName))
} else {
position.SetText(fmt.Sprintf("Esc: send msg; PgUp/Down: switch focus; F1: manage chats; F2: regen last; F3:delete last msg; F4: edit msg; F5: toggle system; F6: interrupt bot resp; Row: [yellow]%d[white], Column: [yellow]%d[white] - [red]To[white] Row: [yellow]%d[white], To Column: [yellow]%d; bot resp mode: %v", fromRow, fromColumn, toRow, toColumn, botRespMode))
}
}
chatOpts := []string{"cancel", "new", "rename current"}
chatList, err := loadHistoryChats()
if err != nil {
logger.Error("failed to load chat history", "error", err)
chatList = []string{}
}
chatActModal := tview.NewModal().
SetText("Chat actions:").
AddButtons(append(chatOpts, chatList...)).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
switch buttonLabel {
case "new":
id, err := store.ChatGetMaxID()
if err != nil {
logger.Error("failed to get chat id", "error", err)
}
// set chat body
chatBody.Messages = defaultStarter
textView.SetText(chatToText(showSystemMsgs))
newChat := &models.Chat{
ID: id,
Name: fmt.Sprintf("%v_%v", "new", time.Now().Unix()),
Msgs: string(defaultStarterBytes),
}
// activeChatName = path.Join(historyDir, fmt.Sprintf("%d_chat.json", time.Now().Unix()))
activeChatName = newChat.Name
chatMap[newChat.Name] = newChat
pages.RemovePage("history")
return
// set text
case "cancel":
pages.RemovePage("history")
return
case "rename current":
// add input field
pages.RemovePage("history")
pages.AddPage("renameW", renameWindow, true, true)
return
default:
fn := buttonLabel
history, err := loadHistoryChat(fn)
if err != nil {
logger.Error("failed to read history file", "chat", fn)
pages.RemovePage("history")
return
}
chatBody.Messages = history
textView.SetText(chatToText(showSystemMsgs))
activeChatName = fn
pages.RemovePage("history")
return
}
})
sysModal = tview.NewModal().
SetText("Switch sys msg:").
AddButtons(sysLabels).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
switch buttonLabel {
case "cancel":
pages.RemovePage("sys")
return
default:
sysMsg, ok := sysMap[buttonLabel]
if !ok {
logger.Warn("no such sys msg", "name", buttonLabel)
pages.RemovePage("sys")
return
}
chatBody.Messages[0].Content = sysMsg
// replace textview
textView.SetText(chatToText(showSystemMsgs))
pages.RemovePage("sys")
}
})
editArea = tview.NewTextArea().
SetPlaceholder("Replace msg...")
editArea.SetBorder(true).SetTitle("input")
editArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape && editMode {
editedMsg := editArea.GetText()
if editedMsg == "" {
notifyUser("edit", "no edit provided")
pages.RemovePage("editArea")
editMode = false
return nil
}
chatBody.Messages[selectedIndex].Content = editedMsg
// change textarea
textView.SetText(chatToText(showSystemMsgs))
pages.RemovePage("editArea")
editMode = false
return nil
}
return event
})
indexPickWindow = tview.NewInputField().
SetLabel("Enter a msg index: ").
SetFieldWidth(4).
SetAcceptanceFunc(tview.InputFieldInteger).
SetDoneFunc(func(key tcell.Key) {
pages.RemovePage("getIndex")
return
})
indexPickWindow.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
si := indexPickWindow.GetText()
selectedIndex, err = strconv.Atoi(si)
if err != nil {
logger.Error("failed to convert provided index", "error", err, "si", si)
}
if len(chatBody.Messages) <= selectedIndex && selectedIndex < 0 {
logger.Warn("chosen index is out of bounds", "index", selectedIndex)
return nil
}
m := chatBody.Messages[selectedIndex]
if editMode && event.Key() == tcell.KeyEnter {
pages.AddPage("editArea", editArea, true, true)
editArea.SetText(m.Content, true)
}
if !editMode && event.Key() == tcell.KeyEnter {
copyToClipboard(m.Content)
notification := fmt.Sprintf("msg '%s' was copied to the clipboard", m.Content[:30])
notifyUser("copied", notification)
}
return event
})
//
renameWindow = tview.NewInputField().
SetLabel("Enter a msg index: ").
SetFieldWidth(20).
SetAcceptanceFunc(tview.InputFieldMaxLength(100)).
SetDoneFunc(func(key tcell.Key) {
pages.RemovePage("renameW")
return
})
renameWindow.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEnter {
nname := renameWindow.GetText()
if nname == "" {
return event
}
currentChat := chatMap[activeChatName]
delete(chatMap, activeChatName)
currentChat.Name = nname
activeChatName = nname
chatMap[activeChatName] = currentChat
_, err := store.UpsertChat(currentChat)
if err != nil {
logger.Error("failed to upsert chat", "error", err, "chat", currentChat)
}
notification := fmt.Sprintf("renamed chat to '%s'", activeChatName)
notifyUser("renamed", notification)
}
return event
})
//
textArea.SetMovedFunc(updateStatusLine)
updateStatusLine()
textView.SetText(chatToText(showSystemMsgs))
textView.ScrollToEnd()
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyF1 {
chatList, err := loadHistoryChats()
if err != nil {
logger.Error("failed to load chat history", "error", err)
return nil
}
chatOpts := append(chatOpts, chatList...)
chatActModal.ClearButtons()
chatActModal.AddButtons(chatOpts)
pages.AddPage("history", chatActModal, true, true)
return nil
}
if event.Key() == tcell.KeyF2 {
// regen last msg
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
textView.SetText(chatToText(showSystemMsgs))
go chatRound("", userRole, textView)
return nil
}
if event.Key() == tcell.KeyF3 {
// delete last msg
chatBody.Messages = chatBody.Messages[:len(chatBody.Messages)-1]
textView.SetText(chatToText(showSystemMsgs))
botRespMode = false // hmmm; is that correct?
return nil
}
if event.Key() == tcell.KeyF4 {
// edit msg
editMode = true
pages.AddPage("getIndex", indexPickWindow, true, true)
return nil
}
if event.Key() == tcell.KeyF5 {
// switch showSystemMsgs
showSystemMsgs = !showSystemMsgs
textView.SetText(chatToText(showSystemMsgs))
}
if event.Key() == tcell.KeyF6 {
interruptResp = true
botRespMode = false
return nil
}
if event.Key() == tcell.KeyF7 {
// copy msg to clipboard
editMode = false
m := chatBody.Messages[len(chatBody.Messages)-1]
copyToClipboard(m.Content)
notification := fmt.Sprintf("msg '%s' was copied to the clipboard", m.Content[:30])
notifyUser("copied", notification)
return nil
}
if event.Key() == tcell.KeyF8 {
// copy msg to clipboard
editMode = false
pages.AddPage("getIndex", indexPickWindow, true, true)
return nil
}
if event.Key() == tcell.KeyCtrlE {
textArea.SetText("pressed ctrl+e", true)
return nil
}
if event.Key() == tcell.KeyCtrlS {
// switch sys prompt
pages.AddPage("sys", sysModal, true, true)
return nil
}
// cannot send msg in editMode or botRespMode
if event.Key() == tcell.KeyEscape && !editMode && !botRespMode {
fromRow, fromColumn, _, _ := textArea.GetCursor()
position.SetText(fmt.Sprintf(indexLine, fromRow, fromColumn, botRespMode))
// read all text into buffer
msgText := textArea.GetText()
if msgText != "" {
fmt.Fprintf(textView, "\n(%d) <user>: %s\n", len(chatBody.Messages), msgText)
textArea.SetText("", true)
textView.ScrollToEnd()
}
// update statue line
go chatRound(msgText, userRole, textView)
return nil
}
if event.Key() == tcell.KeyPgUp || event.Key() == tcell.KeyPgDn {
currentF := app.GetFocus()
app.SetFocus(focusSwitcher[currentF])
return nil
}
if isASCII(string(event.Rune())) && !botRespMode {
// botRespMode = false
// fromRow, fromColumn, _, _ := textArea.GetCursor()
// position.SetText(fmt.Sprintf(indexLine, fromRow, fromColumn, botRespMode))
return event
}
return event
})
}