diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | main.go | 249 | ||||
-rw-r--r-- | session.go | 1 | ||||
-rw-r--r-- | storage/memory.go | 4 | ||||
-rw-r--r-- | storage/storage.go | 8 | ||||
-rw-r--r-- | tui.go | 311 |
6 files changed, 327 insertions, 249 deletions
@@ -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; @@ -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 { @@ -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 } diff --git a/storage/memory.go b/storage/memory.go index a7bf8cc..088ce1c 100644 --- a/storage/memory.go +++ b/storage/memory.go @@ -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 diff --git a/storage/storage.go b/storage/storage.go index 08cd81a..c863799 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -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 { @@ -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 + }) +} |