5 Commits

Author SHA1 Message Date
Grail Finder
b386c1181f Fix (rag): epub load 2026-02-25 14:54:10 +03:00
Grail Finder
b8e7649e69 Enha (rag): one table to manage files and data loaded 2026-02-25 10:47:35 +03:00
Grail Finder
6664c1a0fc Dep (rag): better extractors 2026-02-25 07:51:24 +03:00
Grail Finder
e0c3fe554f Feat: rag text extractors 2026-02-25 06:51:02 +03:00
Grail Finder
40943ff4d3 Enha: spinner for tool calls 2026-02-24 21:47:57 +03:00
8 changed files with 319 additions and 37 deletions

16
bot.go
View File

@@ -830,11 +830,21 @@ func chatWatcher(ctx context.Context) {
func showSpinner() { func showSpinner() {
spinners := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} spinners := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
var i int var i int
for botRespMode { botPersona := cfg.AssistantRole
if cfg.WriteNextMsgAsCompletionAgent != "" {
botPersona = cfg.WriteNextMsgAsCompletionAgent
}
for botRespMode || toolRunningMode {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
spin := i % len(spinners) spin := i % len(spinners)
app.QueueUpdateDraw(func() { app.QueueUpdateDraw(func() {
textArea.SetTitle(spinners[spin] + " input") if toolRunningMode {
textArea.SetTitle(spinners[spin] + " tool")
} else if botRespMode {
textArea.SetTitle(spinners[spin] + " " + botPersona)
} else {
textArea.SetTitle(spinners[spin] + " input")
}
}) })
i++ i++
} }
@@ -1219,7 +1229,9 @@ func findCall(msg, toolCall string) bool {
} }
// Show tool call progress indicator before execution // Show tool call progress indicator before execution
fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name) fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name)
toolRunningMode = true
resp := callToolWithAgent(fc.Name, fc.Args) resp := callToolWithAgent(fc.Name, fc.Args)
toolRunningMode = false
toolMsg := string(resp) // Remove the "tool response: " prefix and %+v formatting toolMsg := string(resp) // Remove the "tool response: " prefix and %+v formatting
logger.Info("llm used a tool call", "tool_name", fc.Name, "too_args", fc.Args, "id", fc.ID, "tool_resp", toolMsg) logger.Info("llm used a tool call", "tool_name", fc.Name, "too_args", fc.Args, "id", fc.ID, "tool_resp", toolMsg)
fmt.Fprintf(textView, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n", fmt.Fprintf(textView, "%s[-:-:b](%d) <%s>: [-:-:-]\n%s\n",

4
go.mod
View File

@@ -6,17 +6,19 @@ require (
github.com/BurntSushi/toml v1.5.0 github.com/BurntSushi/toml v1.5.0
github.com/GrailFinder/google-translate-tts v0.1.3 github.com/GrailFinder/google-translate-tts v0.1.3
github.com/GrailFinder/searchagent v0.2.0 github.com/GrailFinder/searchagent v0.2.0
github.com/PuerkitoBio/goquery v1.11.0
github.com/gdamore/tcell/v2 v2.13.2 github.com/gdamore/tcell/v2 v2.13.2
github.com/glebarez/go-sqlite v1.22.0 github.com/glebarez/go-sqlite v1.22.0
github.com/gopxl/beep/v2 v2.1.1 github.com/gopxl/beep/v2 v2.1.1
github.com/gordonklaus/portaudio v0.0.0-20250206071425-98a94950218b github.com/gordonklaus/portaudio v0.0.0-20250206071425-98a94950218b
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728
github.com/neurosnap/sentences v1.1.2 github.com/neurosnap/sentences v1.1.2
github.com/rivo/tview v0.42.0 github.com/rivo/tview v0.42.0
github.com/yuin/goldmark v1.4.13
) )
require ( require (
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/oto/v3 v3.4.0 // indirect github.com/ebitengine/oto/v3 v3.4.0 // indirect

3
go.sum
View File

@@ -43,6 +43,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728 h1:QwWKgMY28TAXaDl+ExRDqGQltzXqN/xypdKP86niVn8=
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
@@ -67,6 +69,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

View File

@@ -7,6 +7,7 @@ import (
var ( var (
boolColors = map[bool]string{true: "green", false: "red"} boolColors = map[bool]string{true: "green", false: "red"}
botRespMode = false botRespMode = false
toolRunningMode = false
editMode = false editMode = false
roleEditMode = false roleEditMode = false
injectRole = true injectRole = true

184
rag/extractors.go Normal file
View File

@@ -0,0 +1,184 @@
package rag
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/ledongthuc/pdf"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
func ExtractText(fpath string) (string, error) {
ext := strings.ToLower(path.Ext(fpath))
switch ext {
case ".txt":
return extractTextFromFile(fpath)
case ".md", ".markdown":
return extractTextFromMarkdown(fpath)
case ".html", ".htm":
return extractTextFromHtmlFile(fpath)
case ".epub":
return extractTextFromEpub(fpath)
case ".pdf":
return extractTextFromPdf(fpath)
default:
return "", fmt.Errorf("unsupported file format: %s", ext)
}
}
func extractTextFromFile(fpath string) (string, error) {
data, err := os.ReadFile(fpath)
if err != nil {
return "", err
}
return string(data), nil
}
func extractTextFromHtmlFile(fpath string) (string, error) {
data, err := os.ReadFile(fpath)
if err != nil {
return "", err
}
return extractTextFromHtmlContent(data)
}
// non utf-8 encoding?
func extractTextFromHtmlContent(data []byte) (string, error) {
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data))
if err != nil {
return "", err
}
// Remove script and style tags
doc.Find("script, style, noscript").Each(func(i int, s *goquery.Selection) {
s.Remove()
})
// Get text and clean it
text := doc.Text()
// Collapse all whitespace (newlines, tabs, multiple spaces) into single spaces
cleaned := strings.Join(strings.Fields(text), " ")
return cleaned, nil
}
func extractTextFromMarkdown(fpath string) (string, error) {
data, err := os.ReadFile(fpath)
if err != nil {
return "", err
}
// Convert markdown to HTML
md := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(parser.WithAutoHeadingID()),
goldmark.WithRendererOptions(html.WithUnsafe()), // allow raw HTML if needed
)
var buf bytes.Buffer
if err := md.Convert(data, &buf); err != nil {
return "", err
}
// Now extract text from the resulting HTML (using goquery or similar)
return extractTextFromHtmlContent(buf.Bytes())
}
func extractTextFromEpub(fpath string) (string, error) {
r, err := zip.OpenReader(fpath)
if err != nil {
return "", fmt.Errorf("failed to open epub: %w", err)
}
defer r.Close()
var sb strings.Builder
for _, f := range r.File {
ext := strings.ToLower(path.Ext(f.Name))
if ext != ".xhtml" && ext != ".html" && ext != ".htm" && ext != ".xml" {
continue
}
// Skip manifest, toc, ncx files - they don't contain book content
nameLower := strings.ToLower(f.Name)
if strings.Contains(nameLower, "toc") || strings.Contains(nameLower, "nav") ||
strings.Contains(nameLower, "manifest") || strings.Contains(nameLower, ".opf") ||
strings.HasSuffix(nameLower, ".ncx") {
continue
}
rc, err := f.Open()
if err != nil {
continue
}
if sb.Len() > 0 {
sb.WriteString("\n\n")
}
sb.WriteString(f.Name)
sb.WriteString("\n")
buf, readErr := io.ReadAll(rc)
rc.Close()
if readErr == nil {
sb.WriteString(stripHTML(string(buf)))
}
}
if sb.Len() == 0 {
return "", errors.New("no content extracted from epub")
}
return sb.String(), nil
}
func stripHTML(html string) string {
var sb strings.Builder
inTag := false
for _, r := range html {
switch r {
case '<':
inTag = true
case '>':
inTag = false
default:
if !inTag {
sb.WriteRune(r)
}
}
}
return sb.String()
}
func extractTextFromPdf(fpath string) (string, error) {
_, err := exec.LookPath("pdftotext")
if err == nil {
out, err := exec.Command("pdftotext", "-layout", fpath, "-").Output()
if err == nil && len(out) > 0 {
return string(out), nil
}
}
return extractTextFromPdfPureGo(fpath)
}
func extractTextFromPdfPureGo(fpath string) (string, error) {
df, r, err := pdf.Open(fpath)
if err != nil {
return "", fmt.Errorf("failed to open pdf: %w", err)
}
defer df.Close()
textReader, err := r.GetPlainText()
if err != nil {
return "", fmt.Errorf("failed to extract text from pdf: %w", err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, textReader)
if err != nil {
return "", fmt.Errorf("failed to read pdf text: %w", err)
}
return buf.String(), nil
}

View File

@@ -7,7 +7,6 @@ import (
"gf-lt/models" "gf-lt/models"
"gf-lt/storage" "gf-lt/storage"
"log/slog" "log/slog"
"os"
"path" "path"
"regexp" "regexp"
"sort" "sort"
@@ -58,7 +57,7 @@ func wordCounter(sentence string) int {
func (r *RAG) LoadRAG(fpath string) error { func (r *RAG) LoadRAG(fpath string) error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
data, err := os.ReadFile(fpath) fileText, err := ExtractText(fpath)
if err != nil { if err != nil {
return err return err
} }
@@ -68,7 +67,6 @@ func (r *RAG) LoadRAG(fpath string) error {
default: default:
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", LoadedFileRAGStatus) r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", LoadedFileRAGStatus)
} }
fileText := string(data)
tokenizer, err := english.NewSentenceTokenizer(nil) tokenizer, err := english.NewSentenceTokenizer(nil)
if err != nil { if err != nil {
return err return err

125
tables.go
View File

@@ -247,9 +247,49 @@ func formatSize(size int64) string {
return fmt.Sprintf("%.1f%s", s, units[i]) return fmt.Sprintf("%.1f%s", s, units[i])
} }
func makeRAGTable(fileList []string) *tview.Flex { type ragFileInfo struct {
actions := []string{"load", "delete"} name string
rows, cols := len(fileList), len(actions)+2 inRAGDir bool
isLoaded bool
fullPath string
}
func makeRAGTable(fileList []string, loadedFiles []string) *tview.Flex {
// Build set of loaded files for quick lookup
loadedSet := make(map[string]bool)
for _, f := range loadedFiles {
loadedSet[f] = true
}
// Build merged list: files from ragdir + orphaned files from DB
ragFiles := make([]ragFileInfo, 0, len(fileList)+len(loadedFiles))
seen := make(map[string]bool)
// Add files from ragdir
for _, f := range fileList {
ragFiles = append(ragFiles, ragFileInfo{
name: f,
inRAGDir: true,
isLoaded: loadedSet[f],
fullPath: path.Join(cfg.RAGDir, f),
})
seen[f] = true
}
// Add orphaned files (in DB but not in ragdir)
for _, f := range loadedFiles {
if !seen[f] {
ragFiles = append(ragFiles, ragFileInfo{
name: f,
inRAGDir: false,
isLoaded: true,
fullPath: "",
})
}
}
rows := len(ragFiles)
cols := 4 // File Name | Preview | Action | Delete
fileTable := tview.NewTable(). fileTable := tview.NewTable().
SetBorders(true) SetBorders(true)
longStatusView := tview.NewTextView() longStatusView := tview.NewTextView()
@@ -273,7 +313,7 @@ func makeRAGTable(fileList []string) *tview.Flex {
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetSelectable(false)) SetSelectable(false))
fileTable.SetCell(0, 2, fileTable.SetCell(0, 2,
tview.NewTableCell("Load"). tview.NewTableCell("Load/Unload").
SetTextColor(tcell.ColorWhite). SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetSelectable(false)) SetSelectable(false))
@@ -284,18 +324,29 @@ func makeRAGTable(fileList []string) *tview.Flex {
SetSelectable(false)) SetSelectable(false))
// Add the file rows starting from row 1 // Add the file rows starting from row 1
for r := 0; r < rows; r++ { for r := 0; r < rows; r++ {
f := ragFiles[r]
for c := 0; c < cols; c++ { for c := 0; c < cols; c++ {
color := tcell.ColorWhite color := tcell.ColorWhite
switch { switch {
case c == 0: case c == 0:
displayName := f.name
if !f.inRAGDir {
displayName = f.name + " (orphaned)"
}
fileTable.SetCell(r+1, c, fileTable.SetCell(r+1, c,
tview.NewTableCell(fileList[r]). tview.NewTableCell(displayName).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetSelectable(false)) SetSelectable(false))
case c == 1: case c == 1:
fpath := path.Join(cfg.RAGDir, fileList[r]) if !f.inRAGDir {
if fi, err := os.Stat(fpath); err == nil { // Orphaned file - no preview available
fileTable.SetCell(r+1, c,
tview.NewTableCell("not in ragdir").
SetTextColor(tcell.ColorYellow).
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else if fi, err := os.Stat(f.fullPath); err == nil {
size := fi.Size() size := fi.Size()
modTime := fi.ModTime() modTime := fi.ModTime()
preview := fmt.Sprintf("%s | %s", formatSize(size), modTime.Format("2006-01-02 15:04")) preview := fmt.Sprintf("%s | %s", formatSize(size), modTime.Format("2006-01-02 15:04"))
@@ -312,15 +363,32 @@ func makeRAGTable(fileList []string) *tview.Flex {
SetSelectable(false)) SetSelectable(false))
} }
case c == 2: case c == 2:
actionText := "load"
if f.isLoaded {
actionText = "unload"
}
if !f.inRAGDir {
// Orphaned file - can only unload
actionText = "unload"
}
fileTable.SetCell(r+1, c, fileTable.SetCell(r+1, c,
tview.NewTableCell("load"). tview.NewTableCell(actionText).
SetTextColor(color).
SetAlign(tview.AlignCenter))
default:
fileTable.SetCell(r+1, c,
tview.NewTableCell("delete").
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter))
case c == 3:
if !f.inRAGDir {
// Orphaned file - cannot delete from ragdir (not there)
fileTable.SetCell(r+1, c,
tview.NewTableCell("-").
SetTextColor(tcell.ColorDarkGray).
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else {
fileTable.SetCell(r+1, c,
tview.NewTableCell("delete").
SetTextColor(color).
SetAlign(tview.AlignCenter))
}
} }
} }
} }
@@ -376,12 +444,16 @@ func makeRAGTable(fileList []string) *tview.Flex {
pages.RemovePage(RAGPage) pages.RemovePage(RAGPage)
return return
} }
// For file rows, get the filename (row index - 1 because of the exit row at index 0) // For file rows, get the file info (row index - 1 because of the exit row at index 0)
fpath := fileList[row-1] // -1 to account for the exit row at index 0 f := ragFiles[row-1]
// notification := fmt.Sprintf("chat: %s; action: %s", fpath, tc.Text) // Handle "-" case (orphaned file with no delete option)
if tc.Text == "-" {
pages.RemovePage(RAGPage)
return
}
switch tc.Text { switch tc.Text {
case "load": case "load":
fpath = path.Join(cfg.RAGDir, fpath) fpath := path.Join(cfg.RAGDir, f.name)
longStatusView.SetText("clicked load") longStatusView.SetText("clicked load")
go func() { go func() {
if err := ragger.LoadRAG(fpath); err != nil { if err := ragger.LoadRAG(fpath); err != nil {
@@ -398,8 +470,25 @@ func makeRAGTable(fileList []string) *tview.Flex {
}) })
}() }()
return return
case "unload":
longStatusView.SetText("clicked unload")
go func() {
if err := ragger.RemoveFile(f.name); err != nil {
logger.Error("failed to unload file from RAG", "filename", f.name, "error", err)
_ = notifyUser("RAG", "failed to unload file; error: "+err.Error())
app.QueueUpdate(func() {
pages.RemovePage(RAGPage)
})
return
}
_ = notifyUser("RAG", "file unloaded successfully")
app.QueueUpdate(func() {
pages.RemovePage(RAGPage)
})
}()
return
case "delete": case "delete":
fpath = path.Join(cfg.RAGDir, fpath) fpath := path.Join(cfg.RAGDir, f.name)
if err := os.Remove(fpath); err != nil { if err := os.Remove(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

19
tui.go
View File

@@ -913,6 +913,7 @@ func init() {
return nil return nil
} }
} }
// Get files from ragdir
fileList := []string{} fileList := []string{}
for _, f := range files { for _, f := range files {
if f.IsDir() { if f.IsDir() {
@@ -920,22 +921,14 @@ func init() {
} }
fileList = append(fileList, f.Name()) fileList = append(fileList, f.Name())
} }
chatRAGTable := makeRAGTable(fileList) // Get loaded files from vector DB
pages.AddPage(RAGPage, chatRAGTable, true, true) loadedFiles, err := ragger.ListLoaded()
return nil
}
if event.Key() == tcell.KeyCtrlY { // Use Ctrl+Y to list loaded RAG files
// List files already loaded into the RAG system
fileList, err := ragger.ListLoaded()
if err != nil { if err != nil {
logger.Error("failed to list loaded RAG files", "error", err) logger.Error("failed to list loaded RAG files", "error", err)
if notifyerr := notifyUser("failed to list RAG files", err.Error()); notifyerr != nil { loadedFiles = []string{} // Continue with empty list on error
logger.Error("failed to send notification", "error", notifyerr)
}
return nil
} }
chatLoadedRAGTable := makeLoadedRAGTable(fileList) chatRAGTable := makeRAGTable(fileList, loadedFiles)
pages.AddPage(RAGLoadedPage, chatLoadedRAGTable, true, true) pages.AddPage(RAGPage, chatRAGTable, true, true)
return nil return nil
} }
if event.Key() == tcell.KeyRune && event.Modifiers() == tcell.ModAlt && event.Rune() == '1' { if event.Key() == tcell.KeyRune && event.Modifiers() == tcell.ModAlt && event.Rune() == '1' {