summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md13
-rw-r--r--bot.go62
-rw-r--r--main.go217
-rw-r--r--session.go1
-rw-r--r--storage/memory.go4
-rw-r--r--storage/storage.go19
-rw-r--r--tools.go106
-rw-r--r--tui.go311
8 files changed, 468 insertions, 265 deletions
diff --git a/README.md b/README.md
index f7c63bf..cfbe3a5 100644
--- a/README.md
+++ b/README.md
@@ -8,13 +8,19 @@
- edit message? (including from bot); +
- ability to copy message; +
- menu with old chats (chat files); +
-- fullscreen textarea option (for long prompt);
- tab to switch selection between textview and textarea (input and chat); +
- basic tools: memorize and recall;
- stop stream from the bot; +
- sqlitedb instead of chatfiles; +
+- define tools and sys prompt for them to be used; +
+- add system prompt without tools (for mistral); +
+- option to switch between predefined sys prompts; +
- sqlite for the bot memory;
-- option to switch between predefined sys prompts;
+- fullscreen textarea option (bothersome to implement);
+- 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; +
@@ -23,3 +29,6 @@
- Tab is needed to copy paste text into textarea box, use shift+tab to switch focus; (changed tp pgup) +
- delete last msg: can have unexpected behavior (deletes what appears to be two messages if last bot msg was not generated (should only delete icon in that case));
- 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 replaces old ones in db;
diff --git a/bot.go b/bot.go
index 66303a2..d4c86f0 100644
--- a/bot.go
+++ b/bot.go
@@ -23,41 +23,32 @@ var httpClient = http.Client{
var (
logger *slog.Logger
- APIURL = "http://localhost:8080/v1/chat/completions"
- DB = map[string]map[string]any{}
userRole = "user"
assistantRole = "assistant"
toolRole = "tool"
assistantIcon = "<🤖>: "
userIcon = "<user>: "
- historyDir = "./history/"
- // TODO: pass as an cli arg
+ // TODO: pass as an cli arg or have config
+ APIURL = "http://localhost:8080/v1/chat/completions"
+ logFileName = "log.txt"
showSystemMsgs bool
+ chunkLimit = 1000
activeChatName string
chunkChan = make(chan string, 10)
streamDone = make(chan bool, 1)
chatBody *models.ChatBody
- store storage.ChatHistory
+ store storage.FullRepo
defaultFirstMsg = "Hello! What can I do for you?"
defaultStarter = []models.MessagesStory{
{Role: "system", Content: systemMsg},
{Role: assistantRole, Content: defaultFirstMsg},
}
- interruptResp = false
+ defaultStarterBytes = []byte{}
+ interruptResp = false
)
// ====
-func getUserInput(userPrompt string) string {
- fmt.Printf(userPrompt)
- reader := bufio.NewReader(os.Stdin)
- line, err := reader.ReadString('\n')
- if err != nil {
- panic(err) // think about it
- }
- return line
-}
-
func formMsg(chatBody *models.ChatBody, newMsg, role string) io.Reader {
if newMsg != "" { // otherwise let the bot continue
newMsg := models.MessagesStory{Role: role, Content: newMsg}
@@ -65,7 +56,8 @@ func formMsg(chatBody *models.ChatBody, newMsg, role string) io.Reader {
}
data, err := json.Marshal(chatBody)
if err != nil {
- panic(err)
+ logger.Error("failed to form a msg", "error", err)
+ return nil
}
return bytes.NewReader(data)
}
@@ -89,14 +81,15 @@ func sendMsgToLLM(body io.Reader) (any, error) {
break
}
llmchunk := models.LLMRespChunk{}
- if counter > 2000 {
+ if counter > chunkLimit {
+ logger.Warn("response hit chunk limit", "limit", chunkLimit)
streamDone <- true
break
}
line, err := reader.ReadBytes('\n')
if err != nil {
streamDone <- true
- panic(err)
+ logger.Error("error reading response body", "error", err)
}
// logger.Info("linecheck", "line", string(line), "len", len(line), "counter", counter)
if len(line) <= 1 {
@@ -128,6 +121,9 @@ func sendMsgToLLM(body io.Reader) (any, error) {
func chatRound(userMsg, role string, tv *tview.TextView) {
botRespMode = true
reader := formMsg(chatBody, userMsg, role)
+ if reader == nil {
+ return // any notification in that case?
+ }
go sendMsgToLLM(reader)
fmt.Fprintf(tv, fmt.Sprintf("(%d) ", len(chatBody.Messages)))
fmt.Fprintf(tv, assistantIcon)
@@ -159,18 +155,22 @@ out:
}
func findCall(msg string, tv *tview.TextView) {
- prefix := "__tool_call__\n"
- suffix := "\n__tool_call__"
+ // prefix := "__tool_call__\n"
+ // suffix := "\n__tool_call__"
+ // if !strings.HasPrefix(msg, prefix) ||
+ // !strings.HasSuffix(msg, suffix) {
+ // return
+ // }
+ // jsStr := strings.TrimSuffix(strings.TrimPrefix(msg, prefix), suffix)
fc := models.FuncCall{}
- if !strings.HasPrefix(msg, prefix) ||
- !strings.HasSuffix(msg, suffix) {
+ jsStr := toolCallRE.FindString(msg)
+ if jsStr == "" {
+ // tool call not found
return
}
- jsStr := strings.TrimSuffix(strings.TrimPrefix(msg, prefix), suffix)
if err := json.Unmarshal([]byte(jsStr), &fc); err != nil {
logger.Error("failed to unmarshal tool call", "error", err)
return
- // panic(err)
}
// call a func
f, ok := fnMap[fc.Name]
@@ -229,13 +229,15 @@ func textSliceToChat(chat []string) []models.MessagesStory {
}
func init() {
- file, err := os.OpenFile("log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ file, err := os.OpenFile(logFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
- panic(err)
+ logger.Error("failed to open log file", "error", err, "filename", logFileName)
+ return
}
- // create dir if does not exist
- if err := os.MkdirAll(historyDir, os.ModePerm); err != nil {
- panic(err)
+ defaultStarterBytes, err = json.Marshal(defaultStarter)
+ if err != nil {
+ logger.Error("failed to marshal defaultStarter", "error", err)
+ return
}
logger = slog.New(slog.NewTextHandler(file, nil))
store = storage.NewProviderSQL("test.db", logger)
diff --git a/main.go b/main.go
index 1dc387a..46cb42c 100644
--- a/main.go
+++ b/main.go
@@ -1,13 +1,8 @@
package main
import (
- "fmt"
- "path"
- "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,216 +25,10 @@ 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 {
- panic(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))
- activeChatName = path.Join(historyDir, fmt.Sprintf("%d_chat.json", time.Now().Unix()))
- 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
- }
- })
- 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 {
- // TODO: add notification that text was copied
- 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 := listHistoryFiles(historyDir)
- fList, err := loadHistoryChats()
- if err != nil {
- panic(err)
- }
- 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
- }
- // 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 {
- panic(err)
+ logger.Error("failed to start tview app", "error", err)
+ return
}
}
diff --git a/session.go b/session.go
index 23d725d..7c035fa 100644
--- a/session.go
+++ b/session.go
@@ -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 67b8dd8..c863799 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -8,12 +8,18 @@ import (
"github.com/jmoiron/sqlx"
)
+type FullRepo interface {
+ ChatHistory
+ Memories
+}
+
type ChatHistory interface {
ListChats() ([]models.Chat, error)
GetChatByID(id uint32) (*models.Chat, error)
GetLastChat() (*models.Chat, error)
UpsertChat(chat *models.Chat) (*models.Chat, error)
RemoveChat(id uint32) error
+ ChatGetMaxID() (uint32, error)
}
type ProviderSQL struct {
@@ -61,12 +67,19 @@ func (p ProviderSQL) RemoveChat(id uint32) error {
return err
}
-func NewProviderSQL(dbPath string, logger *slog.Logger) ChatHistory {
+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 {
- panic(err)
+ logger.Error("failed to open db connection", "error", err)
+ return nil
}
- // get SQLite version
p := ProviderSQL{db: db, logger: logger}
p.Migrate()
return p
diff --git a/tools.go b/tools.go
index 82f07b3..b752790 100644
--- a/tools.go
+++ b/tools.go
@@ -1,59 +1,133 @@
package main
+import (
+ "elefant/models"
+ "encoding/json"
+ "regexp"
+ "time"
+)
+
var (
// TODO: form that message based on existing funcs
- systemMsg = `You're a helpful assistant.
+ basicSysMsg = `Large Language Model that helps user with any of his requests.`
+ toolCallRE = regexp.MustCompile(`__tool_call__\s*([\s\S]*?)__tool_call__`)
+ toolSysMsg = `You're a helpful assistant.
# Tools
You can do functions call if needed.
Your current tools:
<tools>
+[
+{
+"name":"recall",
+"args": "topic",
+"when_to_use": "when asked about topic that user previously asked to memorise"
+},
{
-"name":"get_id",
-"args": "username"
+"name":"memorise",
+"args": ["topic", "info"],
+"when_to_use": "when asked to memorise something"
+},
+{
+"name":"recall_topics",
+"args": null,
+"when_to_use": "once in a while"
}
+]
</tools>
To make a function call return a json object within __tool_call__ tags;
Example:
__tool_call__
{
-"name":"get_id",
+"name":"recall",
"args": "Adam"
}
__tool_call__
-When making function call avoid typing anything else. 'tool' user will respond with the results of the call.
+When done right, tool call will be delivered to the 'tool' agent. 'tool' agent will respond with the results of the call.
After that you are free to respond to the user.
`
+ systemMsg = toolSysMsg
+ sysMap = map[string]string{"basic_sys": basicSysMsg, "tool_sys": toolSysMsg}
+ sysLabels = []string{"cancel", "basic_sys", "tool_sys"}
)
-func memorize(topic, info string) {
- //
+/*
+consider cases:
+- append mode (treat it like a journal appendix)
+- replace mode (new info/mind invalidates old ones)
+also:
+- some writing can be done without consideration of previous data;
+- others do;
+*/
+func memorise(args ...string) []byte {
+ agent := assistantRole
+ if len(args) < 2 {
+ logger.Warn("not enough args to call memorise tool")
+ return nil
+ }
+ memory := &models.Memory{
+ Agent: agent,
+ Topic: args[0],
+ Mind: args[1],
+ UpdatedAt: time.Now(),
+ }
+ store.Memorise(memory)
+ return nil
}
-func recall(topic string) string {
- //
- return ""
+func recall(args ...string) []byte {
+ agent := assistantRole
+ if len(args) < 1 {
+ logger.Warn("not enough args to call recall tool")
+ return nil
+ }
+ mind, err := store.Recall(agent, args[0])
+ if err != nil {
+ logger.Error("failed to use tool", "error", err, "args", args)
+ return nil
+ }
+ return []byte(mind)
}
-func recallTopics() []string {
- return []string{}
+func recallTopics(args ...string) []byte {
+ agent := assistantRole
+ topics, err := store.RecallTopics(agent)
+ if err != nil {
+ logger.Error("failed to use tool", "error", err, "args", args)
+ return nil
+ }
+ data, err := json.Marshal(topics)
+ if err != nil {
+ logger.Error("failed to use tool", "error", err, "args", args)
+ return nil
+ }
+ return data
}
func fullMemoryLoad() {}
// predifine funcs
-func getUserDetails(id ...string) map[string]any {
+func getUserDetails(args ...string) []byte {
// db query
// return DB[id[0]]
- return map[string]any{
+ m := map[string]any{
"username": "fm11",
"id": 24983,
"reputation": 911,
"balance": 214.73,
}
+ data, err := json.Marshal(m)
+ if err != nil {
+ logger.Error("failed to use tool", "error", err, "args", args)
+ return nil
+ }
+ return data
}
-type fnSig func(...string) map[string]any
+type fnSig func(...string) []byte
var fnMap = map[string]fnSig{
- "get_id": getUserDetails,
+ "get_id": getUserDetails,
+ "recall": recall,
+ "recall_topics": recallTopics,
+ "memorise": memorise,
}
diff --git a/tui.go b/tui.go
new file mode 100644
index 0000000..ba12e6d
--- /dev/null
+++ b/tui.go
@@ -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
+ })
+}