Compare commits
9 Commits
feat/resp-
...
34cd4ac141
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34cd4ac141 | ||
|
|
343366b12d | ||
|
|
978369eeaa | ||
|
|
c39e1c267d | ||
|
|
9af21895c6 | ||
|
|
e3bd6f219f | ||
|
|
ae62c2c8d8 | ||
|
|
04db7c2f01 | ||
|
|
3d889e70b5 |
8
Makefile
8
Makefile
@@ -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
43
bot.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
9
llm.go
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
20
popups.go
20
popups.go
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
103
rag/rag.go
103
rag/rag.go
@@ -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
144
tables.go
@@ -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
27
tui.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user