9 Commits

Author SHA1 Message Date
Grail Finder
34cd4ac141 Fix: ragflow 2026-02-24 14:24:57 +03:00
Grail Finder
343366b12d Fix: tag tables 2026-02-24 12:28:17 +03:00
Grail Finder
978369eeaa Enha: default rag dir 2026-02-24 11:37:44 +03:00
Grail Finder
c39e1c267d Enha: loaded model on top 2026-02-24 10:31:01 +03:00
Grail Finder
9af21895c6 Chore: remove cfg.ThinkUse
move cleaning image attachment to the end of chatRound
fmt cleanup
2026-02-24 08:59:34 +03:00
Grail Finder
e3bd6f219f Fix: whitespace adjestment 2026-02-23 14:15:17 +03:00
Grail Finder
ae62c2c8d8 Enha: tool use indicator 2026-02-23 13:34:43 +03:00
Grail Finder
04db7c2f01 Enha: not allow popups outside of main page 2026-02-23 12:46:28 +03:00
Grail Finder
3d889e70b5 Fix (config): hftoken 2026-02-23 10:47:16 +03:00
12 changed files with 230 additions and 139 deletions

View File

@@ -1,4 +1,4 @@
.PHONY: setconfig run lint setup-whisper build-whisper download-whisper-model docker-up docker-down docker-logs noextra-run .PHONY: setconfig run lint setup-whisper build-whisper download-whisper-model docker-up docker-down docker-logs noextra-run installdelve checkdelve
run: setconfig run: setconfig
go build -tags extra -o gf-lt && ./gf-lt go build -tags extra -o gf-lt && ./gf-lt
@@ -15,6 +15,12 @@ noextra-run: setconfig
setconfig: setconfig:
find config.toml &>/dev/null || cp config.example.toml config.toml find config.toml &>/dev/null || cp config.example.toml config.toml
installdelve:
go install github.com/go-delve/delve/cmd/dlv@latest
checkdelve:
which dlv &>/dev/null || installdelve
lint: ## Run linters. Use make install-linters first. lint: ## Run linters. Use make install-linters first.
golangci-lint run -c .golangci.yml ./... golangci-lint run -c .golangci.yml ./...

43
bot.go
View File

@@ -411,14 +411,21 @@ func fetchLCPModelsWithLoadStatus() ([]string, error) {
return nil, err return nil, err
} }
result := make([]string, 0, len(models.Data)) result := make([]string, 0, len(models.Data))
for _, m := range models.Data { li := 0 // loaded index
for i, m := range models.Data {
modelName := m.ID modelName := m.ID
if m.Status.Value == "loaded" { if m.Status.Value == "loaded" {
modelName = "(loaded) " + modelName modelName = "(loaded) " + modelName
li = i
} }
result = append(result, modelName) result = append(result, modelName)
} }
return result, nil if li == 0 {
return result, nil // no loaded models
}
loadedModel := result[li]
result = append(result[:li], result[li+1:]...)
return slices.Concat([]string{loadedModel}, result), nil
} }
// fetchLCPModelsWithStatus returns the full LCPModels struct including status information. // fetchLCPModelsWithStatus returns the full LCPModels struct including status information.
@@ -569,7 +576,6 @@ func sendMsgToLLM(body io.Reader) {
streamDone <- true streamDone <- true
return return
} }
// Check if the initial response is an error before starting to stream // Check if the initial response is an error before starting to stream
if resp.StatusCode >= 400 { if resp.StatusCode >= 400 {
// Read the response body to get detailed error information // Read the response body to get detailed error information
@@ -584,7 +590,6 @@ func sendMsgToLLM(body io.Reader) {
streamDone <- true streamDone <- true
return return
} }
// Parse the error response for detailed information // Parse the error response for detailed information
detailedError := extractDetailedErrorFromBytes(bodyBytes, resp.StatusCode) detailedError := extractDetailedErrorFromBytes(bodyBytes, resp.StatusCode)
logger.Error("API returned error status", "status_code", resp.StatusCode, "detailed_error", detailedError) logger.Error("API returned error status", "status_code", resp.StatusCode, "detailed_error", detailedError)
@@ -710,7 +715,6 @@ func sendMsgToLLM(body io.Reader) {
tokenCount++ tokenCount++
} }
} }
// When we get content and have been streaming reasoning, close the thinking block // When we get content and have been streaming reasoning, close the thinking block
if chunk.Chunk != "" && hasReasoning && !reasoningSent { if chunk.Chunk != "" && hasReasoning && !reasoningSent {
// Close the thinking block before sending actual content // Close the thinking block before sending actual content
@@ -718,7 +722,6 @@ func sendMsgToLLM(body io.Reader) {
tokenCount++ tokenCount++
reasoningSent = true reasoningSent = true
} }
// bot sends way too many \n // bot sends way too many \n
answerText = strings.ReplaceAll(chunk.Chunk, "\n\n", "\n") answerText = strings.ReplaceAll(chunk.Chunk, "\n\n", "\n")
// Accumulate text to check for stop strings that might span across chunks // Accumulate text to check for stop strings that might span across chunks
@@ -764,12 +767,10 @@ func chatRagUse(qText string) (string, error) {
questions[i] = q.Text questions[i] = q.Text
logger.Debug("RAG question extracted", "index", i, "question", q.Text) logger.Debug("RAG question extracted", "index", i, "question", q.Text)
} }
if len(questions) == 0 { if len(questions) == 0 {
logger.Warn("No questions extracted from query text", "query", qText) logger.Warn("No questions extracted from query text", "query", qText)
return "No related results from RAG vector storage.", nil return "No related results from RAG vector storage.", nil
} }
respVecs := []models.VectorRow{} respVecs := []models.VectorRow{}
for i, q := range questions { for i, q := range questions {
logger.Debug("Processing RAG question", "index", i, "question", q) logger.Debug("Processing RAG question", "index", i, "question", q)
@@ -779,7 +780,6 @@ func chatRagUse(qText string) (string, error) {
continue continue
} }
logger.Debug("Got embeddings for question", "index", i, "question_len", len(q), "embedding_len", len(emb)) logger.Debug("Got embeddings for question", "index", i, "question_len", len(q), "embedding_len", len(emb))
// Create EmbeddingResp struct for the search // Create EmbeddingResp struct for the search
embeddingResp := &models.EmbeddingResp{ embeddingResp := &models.EmbeddingResp{
Embedding: emb, Embedding: emb,
@@ -793,7 +793,6 @@ func chatRagUse(qText string) (string, error) {
logger.Debug("RAG search returned vectors", "index", i, "question", q, "vector_count", len(vecs)) logger.Debug("RAG search returned vectors", "index", i, "question", q, "vector_count", len(vecs))
respVecs = append(respVecs, vecs...) respVecs = append(respVecs, vecs...)
} }
// get raw text // get raw text
resps := []string{} resps := []string{}
logger.Debug("RAG query final results", "total_vecs_found", len(respVecs)) logger.Debug("RAG query final results", "total_vecs_found", len(respVecs))
@@ -801,12 +800,10 @@ func chatRagUse(qText string) (string, error) {
resps = append(resps, rv.RawText) resps = append(resps, rv.RawText)
logger.Debug("RAG result", "slug", rv.Slug, "filename", rv.FileName, "raw_text_len", len(rv.RawText)) logger.Debug("RAG result", "slug", rv.Slug, "filename", rv.FileName, "raw_text_len", len(rv.RawText))
} }
if len(resps) == 0 { if len(resps) == 0 {
logger.Info("No RAG results found for query", "original_query", qText, "question_count", len(questions)) logger.Info("No RAG results found for query", "original_query", qText, "question_count", len(questions))
return "No related results from RAG vector storage.", nil return "No related results from RAG vector storage.", nil
} }
result := strings.Join(resps, "\n") result := strings.Join(resps, "\n")
logger.Debug("RAG query completed", "result_len", len(result), "response_count", len(resps)) logger.Debug("RAG query completed", "result_len", len(result), "response_count", len(resps))
return result, nil return result, nil
@@ -836,7 +833,10 @@ func chatRound(r *models.ChatRoundReq) error {
if cfg.WriteNextMsgAsCompletionAgent != "" { if cfg.WriteNextMsgAsCompletionAgent != "" {
botPersona = cfg.WriteNextMsgAsCompletionAgent botPersona = cfg.WriteNextMsgAsCompletionAgent
} }
defer func() { botRespMode = false }() defer func() {
botRespMode = false
ClearImageAttachment()
}()
// check that there is a model set to use if is not local // check that there is a model set to use if is not local
choseChunkParser() choseChunkParser()
reader, err := chunkParser.FormMsg(r.UserMsg, r.Role, r.Resume) reader, err := chunkParser.FormMsg(r.UserMsg, r.Role, r.Resume)
@@ -855,13 +855,14 @@ func chatRound(r *models.ChatRoundReq) error {
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{ chatBody.Messages = append(chatBody.Messages, models.RoleMsg{
Role: botPersona, Content: "", Role: botPersona, Content: "",
}) })
fmt.Fprintf(textView, "\n[-:-:b](%d) ", msgIdx) nl := "\n\n"
fmt.Fprint(textView, roleToIcon(botPersona)) prevText := textView.GetText(true)
fmt.Fprint(textView, "[-:-:-]\n") if strings.HasSuffix(prevText, nl) {
if cfg.ThinkUse && !strings.Contains(cfg.CurrentAPI, "v1") { nl = ""
// fmt.Fprint(textView, "<think>") } else if strings.HasSuffix(prevText, "\n") {
chunkChan <- "<think>" nl = "\n"
} }
fmt.Fprintf(textView, "%s[-:-:b](%d) %s[-:-:-]\n", nl, msgIdx, roleToIcon(botPersona))
} else { } else {
msgIdx = len(chatBody.Messages) - 1 msgIdx = len(chatBody.Messages) - 1
} }
@@ -1198,6 +1199,8 @@ func findCall(msg, toolCall string) bool {
chatRoundChan <- crr chatRoundChan <- crr
return true return true
} }
// Show tool call progress indicator before execution
fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name)
resp := callToolWithAgent(fc.Name, fc.Args) resp := callToolWithAgent(fc.Name, fc.Args)
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)
@@ -1237,7 +1240,6 @@ func chatToTextSlice(messages []models.RoleMsg, showSys bool) []string {
func chatToText(messages []models.RoleMsg, showSys bool) string { func chatToText(messages []models.RoleMsg, showSys bool) string {
s := chatToTextSlice(messages, showSys) s := chatToTextSlice(messages, showSys)
text := strings.Join(s, "\n") text := strings.Join(s, "\n")
// Collapse thinking blocks if enabled // Collapse thinking blocks if enabled
if thinkingCollapsed { if thinkingCollapsed {
text = thinkRE.ReplaceAllStringFunc(text, func(match string) string { text = thinkRE.ReplaceAllStringFunc(text, func(match string) string {
@@ -1261,7 +1263,6 @@ func chatToText(messages []models.RoleMsg, showSys bool) string {
} }
} }
} }
return text return text
} }

View File

@@ -12,7 +12,7 @@ OpenRouterChatAPI = "https://openrouter.ai/api/v1/chat/completions"
# OpenRouterToken = "" # OpenRouterToken = ""
# embeddings # embeddings
EmbedURL = "http://localhost:8082/v1/embeddings" EmbedURL = "http://localhost:8082/v1/embeddings"
HFToken = false HFToken = ""
# #
ShowSys = true ShowSys = true
LogFile = "log.txt" LogFile = "log.txt"

View File

@@ -18,7 +18,6 @@ type Config struct {
UserRole string `toml:"UserRole"` UserRole string `toml:"UserRole"`
ToolRole string `toml:"ToolRole"` ToolRole string `toml:"ToolRole"`
ToolUse bool `toml:"ToolUse"` ToolUse bool `toml:"ToolUse"`
ThinkUse bool `toml:"ThinkUse"`
StripThinkingFromAPI bool `toml:"StripThinkingFromAPI"` StripThinkingFromAPI bool `toml:"StripThinkingFromAPI"`
ReasoningEffort string `toml:"ReasoningEffort"` ReasoningEffort string `toml:"ReasoningEffort"`
AssistantRole string `toml:"AssistantRole"` AssistantRole string `toml:"AssistantRole"`
@@ -125,6 +124,9 @@ func LoadConfig(fn string) (*Config, error) {
if config.CompletionAPI != "" { if config.CompletionAPI != "" {
config.ApiLinks = append(config.ApiLinks, config.CompletionAPI) config.ApiLinks = append(config.ApiLinks, config.CompletionAPI)
} }
if config.RAGDir == "" {
config.RAGDir = "ragimport"
}
// if any value is empty fill with default // if any value is empty fill with default
return config, nil return config, nil
} }

View File

@@ -165,9 +165,6 @@ Those could be switched in program, but also bould be setup in config.
#### ToolUse #### ToolUse
- Enable or disable explanation of tools to llm, so it could use them. - Enable or disable explanation of tools to llm, so it could use them.
#### ThinkUse
- Enable or disable insertion of JsonSerializerToken at the beggining of llm resp.
### StripThinkingFromAPI (`true`) ### StripThinkingFromAPI (`true`)
- Strip thinking blocks from messages before sending to LLM. Keeps them in chat history for local viewing but reduces token usage in API calls. - Strip thinking blocks from messages before sending to LLM. Keeps them in chat history for local viewing but reduces token usage in API calls.

9
llm.go
View File

@@ -184,9 +184,6 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro
botMsgStart := "\n" + botPersona + ":\n" botMsgStart := "\n" + botPersona + ":\n"
prompt += botMsgStart prompt += botMsgStart
} }
if cfg.ThinkUse && !cfg.ToolUse {
prompt += "<think>"
}
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse, logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
"msg", msg, "resume", resume, "prompt", prompt, "multimodal_data_count", len(multimodalData)) "msg", msg, "resume", resume, "prompt", prompt, "multimodal_data_count", len(multimodalData))
payload := models.NewLCPReq(prompt, chatBody.Model, multimodalData, payload := models.NewLCPReq(prompt, chatBody.Model, multimodalData,
@@ -423,9 +420,6 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader
botMsgStart := "\n" + botPersona + ":\n" botMsgStart := "\n" + botPersona + ":\n"
prompt += botMsgStart prompt += botMsgStart
} }
if cfg.ThinkUse && !cfg.ToolUse {
prompt += "<think>"
}
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse, logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
"msg", msg, "resume", resume, "prompt", prompt) "msg", msg, "resume", resume, "prompt", prompt)
payload := models.NewDSCompletionReq(prompt, chatBody.Model, payload := models.NewDSCompletionReq(prompt, chatBody.Model,
@@ -589,9 +583,6 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader
botMsgStart := "\n" + botPersona + ":\n" botMsgStart := "\n" + botPersona + ":\n"
prompt += botMsgStart prompt += botMsgStart
} }
if cfg.ThinkUse && !cfg.ToolUse {
prompt += "<think>"
}
stopSlice := chatBody.MakeStopSliceExcluding("", listChatRoles()) stopSlice := chatBody.MakeStopSliceExcluding("", listChatRoles())
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse, logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
"msg", msg, "resume", resume, "prompt", prompt, "stop_strings", stopSlice) "msg", msg, "resume", resume, "prompt", prompt, "stop_strings", stopSlice)

View File

@@ -241,8 +241,7 @@ func (m *RoleMsg) ToText(i int) string {
} }
finalContent.WriteString(contentStr) finalContent.WriteString(contentStr)
if m.Stats != nil { if m.Stats != nil {
finalContent.WriteString(fmt.Sprintf("\n[gray::i][%d tok, %.1fs, %.1f t/s][-:-:-]", fmt.Fprintf(&finalContent, "\n[gray::i][%d tok, %.1fs, %.1f t/s][-:-:-]", m.Stats.Tokens, m.Stats.Duration, m.Stats.TokensPerSec)
m.Stats.Tokens, m.Stats.Duration, m.Stats.TokensPerSec))
} }
textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, finalContent.String()) textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, finalContent.String())
return strings.ReplaceAll(textMsg, "\n\n", "\n") return strings.ReplaceAll(textMsg, "\n\n", "\n")

View File

@@ -51,7 +51,7 @@ func showModelSelectionPopup() {
// Find the current model index to set as selected // Find the current model index to set as selected
currentModelIndex := -1 currentModelIndex := -1
for i, model := range modelList { for i, model := range modelList {
if model == chatBody.Model { if strings.TrimPrefix(model, "(loaded) ") == chatBody.Model {
currentModelIndex = i currentModelIndex = i
} }
modelListWidget.AddItem(model, "", 0, nil) modelListWidget.AddItem(model, "", 0, nil)
@@ -65,16 +65,19 @@ func showModelSelectionPopup() {
chatBody.Model = modelName chatBody.Model = modelName
cfg.CurrentModel = chatBody.Model cfg.CurrentModel = chatBody.Model
pages.RemovePage("modelSelectionPopup") pages.RemovePage("modelSelectionPopup")
app.SetFocus(textArea)
updateCachedModelColor() updateCachedModelColor()
updateStatusLine() updateStatusLine()
}) })
modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape { if event.Key() == tcell.KeyEscape {
pages.RemovePage("modelSelectionPopup") pages.RemovePage("modelSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
if event.Key() == tcell.KeyRune && event.Rune() == 'x' { if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
pages.RemovePage("modelSelectionPopup") pages.RemovePage("modelSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
return event return event
@@ -160,6 +163,7 @@ func showAPILinkSelectionPopup() {
cfg.CurrentModel = chatBody.Model cfg.CurrentModel = chatBody.Model
} }
pages.RemovePage("apiLinkSelectionPopup") pages.RemovePage("apiLinkSelectionPopup")
app.SetFocus(textArea)
choseChunkParser() choseChunkParser()
updateCachedModelColor() updateCachedModelColor()
updateStatusLine() updateStatusLine()
@@ -167,10 +171,12 @@ func showAPILinkSelectionPopup() {
apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape { if event.Key() == tcell.KeyEscape {
pages.RemovePage("apiLinkSelectionPopup") pages.RemovePage("apiLinkSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
if event.Key() == tcell.KeyRune && event.Rune() == 'x' { if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
pages.RemovePage("apiLinkSelectionPopup") pages.RemovePage("apiLinkSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
return event return event
@@ -230,6 +236,7 @@ func showUserRoleSelectionPopup() {
textView.SetText(chatToText(filtered, cfg.ShowSys)) textView.SetText(chatToText(filtered, cfg.ShowSys))
// Remove the popup page // Remove the popup page
pages.RemovePage("userRoleSelectionPopup") pages.RemovePage("userRoleSelectionPopup")
app.SetFocus(textArea)
// Update the status line to reflect the change // Update the status line to reflect the change
updateStatusLine() updateStatusLine()
colorText() colorText()
@@ -237,10 +244,12 @@ func showUserRoleSelectionPopup() {
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape { if event.Key() == tcell.KeyEscape {
pages.RemovePage("userRoleSelectionPopup") pages.RemovePage("userRoleSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
if event.Key() == tcell.KeyRune && event.Rune() == 'x' { if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
pages.RemovePage("userRoleSelectionPopup") pages.RemovePage("userRoleSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
return event return event
@@ -303,16 +312,19 @@ func showBotRoleSelectionPopup() {
cfg.WriteNextMsgAsCompletionAgent = mainText cfg.WriteNextMsgAsCompletionAgent = mainText
// Remove the popup page // Remove the popup page
pages.RemovePage("botRoleSelectionPopup") pages.RemovePage("botRoleSelectionPopup")
app.SetFocus(textArea)
// Update the status line to reflect the change // Update the status line to reflect the change
updateStatusLine() updateStatusLine()
}) })
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape { if event.Key() == tcell.KeyEscape {
pages.RemovePage("botRoleSelectionPopup") pages.RemovePage("botRoleSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
if event.Key() == tcell.KeyRune && event.Rune() == 'x' { if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
pages.RemovePage("botRoleSelectionPopup") pages.RemovePage("botRoleSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
return event return event
@@ -364,14 +376,17 @@ func showFileCompletionPopup(filter string) {
textArea.SetText(before+mainText, true) textArea.SetText(before+mainText, true)
} }
pages.RemovePage("fileCompletionPopup") pages.RemovePage("fileCompletionPopup")
app.SetFocus(textArea)
}) })
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape { if event.Key() == tcell.KeyEscape {
pages.RemovePage("fileCompletionPopup") pages.RemovePage("fileCompletionPopup")
app.SetFocus(textArea)
return nil return nil
} }
if event.Key() == tcell.KeyRune && event.Rune() == 'x' { if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
pages.RemovePage("fileCompletionPopup") pages.RemovePage("fileCompletionPopup")
app.SetFocus(textArea)
return nil return nil
} }
return event return event
@@ -484,14 +499,17 @@ func showColorschemeSelectionPopup() {
} }
// Remove the popup page // Remove the popup page
pages.RemovePage("colorschemeSelectionPopup") pages.RemovePage("colorschemeSelectionPopup")
app.SetFocus(textArea)
}) })
schemeListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { schemeListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape { if event.Key() == tcell.KeyEscape {
pages.RemovePage("colorschemeSelectionPopup") pages.RemovePage("colorschemeSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
if event.Key() == tcell.KeyRune && event.Rune() == 'x' { if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
pages.RemovePage("colorschemeSelectionPopup") pages.RemovePage("colorschemeSelectionPopup")
app.SetFocus(textArea)
return nil return nil
} }
return event return event

View File

@@ -115,9 +115,6 @@ func makePropsTable(props map[string]float32) *tview.Table {
row++ row++
} }
// Add checkboxes // Add checkboxes
addCheckboxRow("Insert <think> tag (/completion only)", cfg.ThinkUse, func(checked bool) {
cfg.ThinkUse = checked
})
addCheckboxRow("RAG use", cfg.RAGEnabled, func(checked bool) { addCheckboxRow("RAG use", cfg.RAGEnabled, func(checked bool) {
cfg.RAGEnabled = checked cfg.RAGEnabled = checked
}) })

View File

@@ -23,7 +23,6 @@ var (
ErrRAGStatus = "some error occurred; failed to transfer data to vector db" ErrRAGStatus = "some error occurred; failed to transfer data to vector db"
) )
type RAG struct { type RAG struct {
logger *slog.Logger logger *slog.Logger
store storage.FullRepo store storage.FullRepo
@@ -122,10 +121,11 @@ func (r *RAG) LoadRAG(fpath string) error {
batchCh = make(chan map[int][]string, maxChSize) batchCh = make(chan map[int][]string, maxChSize)
vectorCh = make(chan []models.VectorRow, maxChSize) vectorCh = make(chan []models.VectorRow, maxChSize)
errCh = make(chan error, 1) errCh = make(chan error, 1)
doneCh = make(chan struct{})
wg = new(sync.WaitGroup) wg = new(sync.WaitGroup)
lock = new(sync.Mutex)
) )
defer close(doneCh)
defer close(errCh) defer close(errCh)
defer close(batchCh) defer close(batchCh)
@@ -156,18 +156,20 @@ func (r *RAG) LoadRAG(fpath string) error {
for w := 0; w < int(r.cfg.RAGWorkers); w++ { for w := 0; w < int(r.cfg.RAGWorkers); w++ {
go func(workerID int) { go func(workerID int) {
defer wg.Done() defer wg.Done()
r.batchToVectorAsync(lock, workerID, batchCh, vectorCh, errCh, path.Base(fpath)) r.batchToVectorAsync(workerID, batchCh, vectorCh, errCh, doneCh, path.Base(fpath))
}(w) }(w)
} }
// Use a goroutine to close the batchCh when all batches are sent // Close batchCh to signal workers no more data is coming
close(batchCh)
// Wait for all workers to finish, then close vectorCh
go func() { go func() {
wg.Wait() wg.Wait()
close(vectorCh) // Close vectorCh when all workers are done close(vectorCh)
}() }()
// Check for errors from workers // Check for errors from workers - this will block until an error occurs or all workers finish
// Use a non-blocking check for errors
select { select {
case err := <-errCh: case err := <-errCh:
if err != nil { if err != nil {
@@ -179,12 +181,28 @@ func (r *RAG) LoadRAG(fpath string) error {
} }
// Write vectors to storage - this will block until vectorCh is closed // Write vectors to storage - this will block until vectorCh is closed
return r.writeVectors(vectorCh) return r.writeVectors(vectorCh, errCh)
} }
func (r *RAG) writeVectors(vectorCh chan []models.VectorRow) error { func (r *RAG) writeVectors(vectorCh chan []models.VectorRow, errCh chan error) error {
// Use a select to handle both vectorCh and errCh
for { for {
for batch := range vectorCh { select {
case err := <-errCh:
if err != nil {
r.logger.Error("error during RAG processing in writeVectors", "error", err)
return err
}
case batch, ok := <-vectorCh:
if !ok {
r.logger.Debug("vector channel closed, finished writing vectors")
select {
case LongJobStatusCh <- FinishedRAGStatus:
default:
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", FinishedRAGStatus)
}
return nil
}
for _, vector := range batch { for _, vector := range batch {
if err := r.storage.WriteVector(&vector); err != nil { if err := r.storage.WriteVector(&vector); err != nil {
r.logger.Error("failed to write vector to DB", "error", err, "slug", vector.Slug) r.logger.Error("failed to write vector to DB", "error", err, "slug", vector.Slug)
@@ -192,74 +210,57 @@ func (r *RAG) writeVectors(vectorCh chan []models.VectorRow) error {
case LongJobStatusCh <- ErrRAGStatus: case LongJobStatusCh <- ErrRAGStatus:
default: default:
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", ErrRAGStatus) r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", ErrRAGStatus)
// Channel is full or closed, ignore the message to prevent panic
} }
return err // Stop the entire RAG operation on DB error return err
} }
} }
r.logger.Debug("wrote batch to db", "size", len(batch), "vector_chan_len", len(vectorCh)) r.logger.Debug("wrote batch to db", "size", len(batch))
if len(vectorCh) == 0 {
r.logger.Debug("finished writing vectors")
select {
case LongJobStatusCh <- FinishedRAGStatus:
default:
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", FinishedRAGStatus)
// Channel is full or closed, ignore the message to prevent panic
}
return nil
}
} }
} }
} }
func (r *RAG) batchToVectorAsync(lock *sync.Mutex, id int, inputCh <-chan map[int][]string, func (r *RAG) batchToVectorAsync(id int, inputCh <-chan map[int][]string,
vectorCh chan<- []models.VectorRow, errCh chan error, filename string) { vectorCh chan<- []models.VectorRow, errCh chan error, doneCh <-chan struct{}, filename string) {
var err error var err error
defer func() { defer func() {
// For errCh, make sure we only send if there's actually an error and the channel can accept it
if err != nil { if err != nil {
select { select {
case errCh <- err: case errCh <- err:
default: default:
// errCh might be full or closed, log but don't panic
r.logger.Warn("errCh channel full or closed, skipping error propagation", "worker", id, "error", err) r.logger.Warn("errCh channel full or closed, skipping error propagation", "worker", id, "error", err)
} }
} }
}() }()
for { for {
lock.Lock()
if len(inputCh) == 0 {
lock.Unlock()
return
}
select { select {
case linesMap := <-inputCh: case <-doneCh:
r.logger.Debug("worker received done signal", "worker", id)
return
case linesMap, ok := <-inputCh:
if !ok {
r.logger.Debug("input channel closed, worker exiting", "worker", id)
return
}
for leftI, lines := range linesMap { for leftI, lines := range linesMap {
select {
case <-doneCh:
return
default:
}
if err := r.fetchEmb(lines, errCh, vectorCh, fmt.Sprintf("%s_%d", filename, leftI), filename); err != nil { if err := r.fetchEmb(lines, errCh, vectorCh, fmt.Sprintf("%s_%d", filename, leftI), filename); err != nil {
r.logger.Error("error fetching embeddings", "error", err, "worker", id) r.logger.Error("error fetching embeddings", "error", err, "worker", id)
lock.Unlock()
return return
} }
} }
lock.Unlock() r.logger.Debug("processed batch", "worker#", id)
case err = <-errCh: statusMsg := fmt.Sprintf("converted to vector; worker#: %d", id)
r.logger.Error("got an error from error channel", "error", err) select {
lock.Unlock() case LongJobStatusCh <- statusMsg:
return default:
default: r.logger.Warn("LongJobStatusCh channel full or closed, dropping status message", "message", statusMsg)
lock.Unlock() }
}
r.logger.Debug("processed batch", "batches#", len(inputCh), "worker#", id)
statusMsg := fmt.Sprintf("converted to vector; batches: %d, worker#: %d", len(inputCh), id)
select {
case LongJobStatusCh <- statusMsg:
default:
r.logger.Warn("LongJobStatusCh channel full or closed, dropping status message", "message", statusMsg)
// Channel is full or closed, ignore the message to prevent panic
} }
} }
} }

144
tables.go
View File

@@ -236,9 +236,20 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
} }
// nolint:unused // nolint:unused
func formatSize(size int64) string {
units := []string{"B", "KB", "MB", "GB", "TB"}
i := 0
s := float64(size)
for s >= 1024 && i < len(units)-1 {
s /= 1024
i++
}
return fmt.Sprintf("%.1f%s", s, units[i])
}
func makeRAGTable(fileList []string) *tview.Flex { func makeRAGTable(fileList []string) *tview.Flex {
actions := []string{"load", "delete"} actions := []string{"load", "delete"}
rows, cols := len(fileList), len(actions)+1 rows, cols := len(fileList), len(actions)+2
fileTable := tview.NewTable(). fileTable := tview.NewTable().
SetBorders(true) SetBorders(true)
longStatusView := tview.NewTextView() longStatusView := tview.NewTextView()
@@ -252,39 +263,62 @@ func makeRAGTable(fileList []string) *tview.Flex {
AddItem(fileTable, 0, 60, true) AddItem(fileTable, 0, 60, true)
// Add the exit option as the first row (row 0) // Add the exit option as the first row (row 0)
fileTable.SetCell(0, 0, fileTable.SetCell(0, 0,
tview.NewTableCell("Exit RAG manager"). tview.NewTableCell("File Name").
SetTextColor(tcell.ColorWhite). SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetSelectable(false)) SetSelectable(false))
fileTable.SetCell(0, 1, fileTable.SetCell(0, 1,
tview.NewTableCell("(Close without action)"). tview.NewTableCell("Preview").
SetTextColor(tcell.ColorGray). SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetSelectable(false)) SetSelectable(false))
fileTable.SetCell(0, 2, fileTable.SetCell(0, 2,
tview.NewTableCell("exit"). tview.NewTableCell("Load").
SetTextColor(tcell.ColorGray). SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 3,
tview.NewTableCell("Delete").
SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter).
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++ {
for c := 0; c < cols; c++ { for c := 0; c < cols; c++ {
color := tcell.ColorWhite color := tcell.ColorWhite
switch { switch {
case c < 1: case c == 0:
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 fileTable.SetCell(r+1, c,
tview.NewTableCell(fileList[r]). tview.NewTableCell(fileList[r]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetSelectable(false)) SetSelectable(false))
case c == 1: // Action description column - not selectable case c == 1:
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 fpath := path.Join(cfg.RAGDir, fileList[r])
tview.NewTableCell("(Action)"). if fi, err := os.Stat(fpath); err == nil {
size := fi.Size()
modTime := fi.ModTime()
preview := fmt.Sprintf("%s | %s", formatSize(size), modTime.Format("2006-01-02 15:04"))
fileTable.SetCell(r+1, c,
tview.NewTableCell(preview).
SetTextColor(color).
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else {
fileTable.SetCell(r+1, c,
tview.NewTableCell("error").
SetTextColor(color).
SetAlign(tview.AlignCenter).
SetSelectable(false))
}
case c == 2:
fileTable.SetCell(r+1, c,
tview.NewTableCell("load").
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter))
SetSelectable(false)) default:
default: // Action button column - selectable fileTable.SetCell(r+1, c,
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 tview.NewTableCell("delete").
tview.NewTableCell(actions[c-1]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter))
} }
@@ -318,7 +352,7 @@ func makeRAGTable(fileList []string) *tview.Flex {
}() }()
fileTable.Select(0, 0). fileTable.Select(0, 0).
SetFixed(1, 1). SetFixed(1, 1).
SetSelectable(true, false). SetSelectable(true, true).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)). SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) { SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
@@ -335,6 +369,8 @@ func makeRAGTable(fileList []string) *tview.Flex {
} }
// defer pages.RemovePage(RAGPage) // defer pages.RemovePage(RAGPage)
tc := fileTable.GetCell(row, column) tc := fileTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
fileTable.SetSelectable(false, false)
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues // Check if the selected row is the exit row (row 0) - do this first to avoid index issues
if row == 0 { if row == 0 {
pages.RemovePage(RAGPage) pages.RemovePage(RAGPage)
@@ -385,7 +421,7 @@ func makeRAGTable(fileList []string) *tview.Flex {
func makeLoadedRAGTable(fileList []string) *tview.Flex { func makeLoadedRAGTable(fileList []string) *tview.Flex {
actions := []string{"delete"} actions := []string{"delete"}
rows, cols := len(fileList), len(actions)+1 rows, cols := len(fileList), len(actions)+2
// Add 1 extra row for the "exit" option at the top // Add 1 extra row for the "exit" option at the top
fileTable := tview.NewTable(). fileTable := tview.NewTable().
SetBorders(true) SetBorders(true)
@@ -400,39 +436,61 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
AddItem(fileTable, 0, 60, true) AddItem(fileTable, 0, 60, true)
// Add the exit option as the first row (row 0) // Add the exit option as the first row (row 0)
fileTable.SetCell(0, 0, fileTable.SetCell(0, 0,
tview.NewTableCell("Exit Loaded Files manager"). tview.NewTableCell("File Name").
SetTextColor(tcell.ColorWhite). SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetSelectable(false)) SetSelectable(false))
fileTable.SetCell(0, 1, fileTable.SetCell(0, 1,
tview.NewTableCell("(Close without action)"). tview.NewTableCell("Preview").
SetTextColor(tcell.ColorGray). SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetSelectable(false)) SetSelectable(false))
fileTable.SetCell(0, 2, fileTable.SetCell(0, 2,
tview.NewTableCell("exit"). tview.NewTableCell("Load").
SetTextColor(tcell.ColorGray). SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter).
SetSelectable(false))
fileTable.SetCell(0, 3,
tview.NewTableCell("Delete").
SetTextColor(tcell.ColorWhite).
SetAlign(tview.AlignCenter).
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++ {
for c := 0; c < cols; c++ { for c := 0; c < cols; c++ {
color := tcell.ColorWhite color := tcell.ColorWhite
switch { switch {
case c < 1: case c == 0:
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 fileTable.SetCell(r+1, c,
tview.NewTableCell(fileList[r]). tview.NewTableCell(fileList[r]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter).
SetSelectable(false)) SetSelectable(false))
case c == 1: // Action description column - not selectable case c == 1:
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 if fi, err := os.Stat(fileList[r]); err == nil {
tview.NewTableCell("(Action)"). size := fi.Size()
modTime := fi.ModTime()
preview := fmt.Sprintf("%s | %s", formatSize(size), modTime.Format("2006-01-02 15:04"))
fileTable.SetCell(r+1, c,
tview.NewTableCell(preview).
SetTextColor(color).
SetAlign(tview.AlignCenter).
SetSelectable(false))
} else {
fileTable.SetCell(r+1, c,
tview.NewTableCell("error").
SetTextColor(color).
SetAlign(tview.AlignCenter).
SetSelectable(false))
}
case c == 2:
fileTable.SetCell(r+1, c,
tview.NewTableCell("load").
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter). SetAlign(tview.AlignCenter))
SetSelectable(false)) default:
default: // Action button column - selectable fileTable.SetCell(r+1, c,
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0 tview.NewTableCell("delete").
tview.NewTableCell(actions[c-1]).
SetTextColor(color). SetTextColor(color).
SetAlign(tview.AlignCenter)) SetAlign(tview.AlignCenter))
} }
@@ -440,7 +498,7 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
} }
fileTable.Select(0, 0). fileTable.Select(0, 0).
SetFixed(1, 1). SetFixed(1, 1).
SetSelectable(true, false). SetSelectable(true, true).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)). SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) { SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
@@ -456,6 +514,8 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
return return
} }
tc := fileTable.GetCell(row, column) tc := fileTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
fileTable.SetSelectable(false, false)
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues // Check if the selected row is the exit row (row 0) - do this first to avoid index issues
if row == 0 { if row == 0 {
pages.RemovePage(RAGLoadedPage) pages.RemovePage(RAGLoadedPage)
@@ -533,7 +593,7 @@ func makeAgentTable(agentList []string) *tview.Table {
} }
chatActTable.Select(0, 0). chatActTable.Select(0, 0).
SetFixed(1, 1). SetFixed(1, 1).
SetSelectable(true, false). SetSelectable(true, true).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)). SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) { SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
@@ -549,6 +609,8 @@ func makeAgentTable(agentList []string) *tview.Table {
return return
} }
tc := chatActTable.GetCell(row, column) tc := chatActTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
chatActTable.SetSelectable(false, false)
selected := agentList[row] selected := agentList[row]
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
switch tc.Text { switch tc.Text {
@@ -630,7 +692,7 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table {
} }
table.Select(0, 0). table.Select(0, 0).
SetFixed(1, 1). SetFixed(1, 1).
SetSelectable(true, false). SetSelectable(true, true).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)). SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) { SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
@@ -646,6 +708,8 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table {
return return
} }
tc := table.GetCell(row, column) tc := table.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
table.SetSelectable(false, false)
selected := codeBlocks[row] selected := codeBlocks[row]
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
switch tc.Text { switch tc.Text {
@@ -702,7 +766,7 @@ func makeImportChatTable(filenames []string) *tview.Table {
} }
chatActTable.Select(0, 0). chatActTable.Select(0, 0).
SetFixed(1, 1). SetFixed(1, 1).
SetSelectable(true, false). SetSelectable(true, true).
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)). SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
SetDoneFunc(func(key tcell.Key) { SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') { if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
@@ -718,6 +782,8 @@ func makeImportChatTable(filenames []string) *tview.Table {
return return
} }
tc := chatActTable.GetCell(row, column) tc := chatActTable.GetCell(row, column)
tc.SetTextColor(tcell.ColorRed)
chatActTable.SetSelectable(false, false)
selected := filenames[row] selected := filenames[row]
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text) // notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
switch tc.Text { switch tc.Text {

27
tui.go
View File

@@ -15,6 +15,11 @@ import (
"github.com/rivo/tview" "github.com/rivo/tview"
) )
func isFullScreenPageActive() bool {
name, _ := pages.GetFrontPage()
return name != "main"
}
var ( var (
app *tview.Application app *tview.Application
pages *tview.Pages pages *tview.Pages
@@ -525,6 +530,9 @@ func init() {
return nil return nil
} }
if event.Key() == tcell.KeyRune && event.Rune() == 'i' && event.Modifiers()&tcell.ModAlt != 0 { if event.Key() == tcell.KeyRune && event.Rune() == 'i' && event.Modifiers()&tcell.ModAlt != 0 {
if isFullScreenPageActive() {
return event
}
showColorschemeSelectionPopup() showColorschemeSelectionPopup()
return nil return nil
} }
@@ -731,6 +739,9 @@ func init() {
return nil return nil
} }
if event.Key() == tcell.KeyCtrlL { if event.Key() == tcell.KeyCtrlL {
if isFullScreenPageActive() {
return event
}
// Show model selection popup instead of rotating models // Show model selection popup instead of rotating models
showModelSelectionPopup() showModelSelectionPopup()
return nil return nil
@@ -744,6 +755,9 @@ func init() {
return nil return nil
} }
if event.Key() == tcell.KeyCtrlV { if event.Key() == tcell.KeyCtrlV {
if isFullScreenPageActive() {
return event
}
// Show API link selection popup instead of rotating APIs // Show API link selection popup instead of rotating APIs
showAPILinkSelectionPopup() showAPILinkSelectionPopup()
return nil return nil
@@ -850,11 +864,17 @@ func init() {
return nil return nil
} }
if event.Key() == tcell.KeyCtrlQ { if event.Key() == tcell.KeyCtrlQ {
if isFullScreenPageActive() {
return event
}
// Show user role selection popup instead of cycling through roles // Show user role selection popup instead of cycling through roles
showUserRoleSelectionPopup() showUserRoleSelectionPopup()
return nil return nil
} }
if event.Key() == tcell.KeyCtrlX { if event.Key() == tcell.KeyCtrlX {
if isFullScreenPageActive() {
return event
}
// Show bot role selection popup instead of cycling through roles // Show bot role selection popup instead of cycling through roles
showBotRoleSelectionPopup() showBotRoleSelectionPopup()
return nil return nil
@@ -975,13 +995,6 @@ func init() {
} }
// go chatRound(msgText, persona, textView, false, false) // go chatRound(msgText, persona, textView, false, false)
chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText} chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText}
// Also clear any image attachment after sending the message
go func() {
// Wait a short moment for the message to be processed, then clear the image attachment
// This allows the image to be sent with the current message if it was attached
// But clears it for the next message
ClearImageAttachment()
}()
} }
return nil return nil
} }