Skip to content

Commit edf39a0

Browse files
committed
add screenshot
Signed-off-by: Siri Chongasamethaworn <siri@omise.co>
1 parent 63096c3 commit edf39a0

8 files changed

Lines changed: 194 additions & 1 deletion

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ KubeAI Chatbot is built with safety as a priority:
114114
- **Immutable Secrets**: The bot is hard-coded to refuse any request involving `kubectl secrets`. This prevention happens at both the LLM prompt level and the tool execution validator.
115115
- **Confirmation Flow**: By default, `AUTOMATIC_MODIFY_RESOURCES` is set to `false`. The bot will generate resource-modifying commands but will not execute them, instead providing the command for you to run manually.
116116

117+
## 📸 Screenshots
118+
119+
| | |
120+
| :-------------------------: | :-------------------------: |
121+
| ![1](docs/screenshot_1.png) | ![2](docs/screenshot_2.png) |
122+
| ![3](docs/screenshot_3.png) | ![4](docs/screenshot_4.png) |
123+
| ![5](docs/screenshot_5.png) | |
124+
117125
## 📜 Credits & Licensing
118126

119127
This project is a derivative work based on [kubectl-ai](https://github.com/GoogleCloudPlatform/kubectl-ai), originally developed by Google LLC.

docs/screenshot_1.png

165 KB
Loading

docs/screenshot_2.png

176 KB
Loading

docs/screenshot_3.png

101 KB
Loading

docs/screenshot_4.png

120 KB
Loading

docs/screenshot_5.png

105 KB
Loading

pkg/ui/slack/slack.go

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,8 @@ func (s *SlackUI) generateBlocks(text string, includeContext bool) []slack.Block
434434
func (s *SlackUI) markdownToBlocks(text string) []slack.Block {
435435
var blocks []slack.Block
436436

437-
// First, normalize tables that might be on a single line
437+
// First, normalize inline headers and tables
438+
text = s.normalizeInlineHeaders(text)
438439
text = s.normalizeInlineTables(text)
439440

440441
lines := strings.Split(text, "\n")
@@ -597,6 +598,148 @@ func (s *SlackUI) parseMarkdownTable(lines []string) ([]string, [][]string) {
597598
return headers, rows
598599
}
599600

601+
// normalizeInlineHeaders adds line breaks after markdown headers that are followed by text without a newline.
602+
// Example: "### HeaderText here" becomes "### Header\nText here"
603+
func (s *SlackUI) normalizeInlineHeaders(text string) string {
604+
lines := strings.Split(text, "\n")
605+
var result []string
606+
607+
for _, line := range lines {
608+
trimmed := strings.TrimSpace(line)
609+
610+
// Check if line starts with markdown header
611+
if strings.HasPrefix(trimmed, "###") {
612+
// Find where the header text ends (after the header marker and any spaces/emojis)
613+
headerStart := strings.Index(line, "###") + 3
614+
restOfLine := line[headerStart:]
615+
616+
// Skip initial spaces
617+
restOfLine = strings.TrimLeft(restOfLine, " ")
618+
619+
// Check if there's text after the header that should be on a new line
620+
// Look for patterns like: "### 💡 HeaderTextWithoutSpace" or "### Header TextHere"
621+
// We want to split after the first "word" (which might include emojis)
622+
623+
// Find the first lowercase letter after uppercase/emoji sequence
624+
// This indicates where the header ends and content begins
625+
headerEnd := -1
626+
inHeaderText := false
627+
628+
for i, r := range restOfLine {
629+
// Skip emojis and spaces at the start
630+
if i < 10 && (r >= 0x1F300 || r == ' ') {
631+
continue
632+
}
633+
634+
// If we see an uppercase letter or start of word, we're in header
635+
if !inHeaderText && (r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z') {
636+
inHeaderText = true
637+
continue
638+
}
639+
640+
// If we're in header text and see a capital letter followed by lowercase
641+
// or see text that looks like start of a sentence, that's where to split
642+
if inHeaderText && i > 0 {
643+
// Check for patterns like "PodI" -> split before "I"
644+
prevRune := rune(restOfLine[i-1])
645+
if (prevRune >= 'a' && prevRune <= 'z') && (r >= 'A' && r <= 'Z') {
646+
// Lowercase followed by uppercase - likely start of new word
647+
headerEnd = i
648+
break
649+
}
650+
}
651+
}
652+
653+
if headerEnd > 0 {
654+
headerText := strings.TrimSpace(restOfLine[:headerEnd])
655+
contentText := strings.TrimSpace(restOfLine[headerEnd:])
656+
result = append(result, "### "+headerText)
657+
if contentText != "" {
658+
result = append(result, contentText)
659+
}
660+
} else {
661+
result = append(result, line)
662+
}
663+
} else if strings.HasPrefix(trimmed, "##") {
664+
// Similar logic for ## headers
665+
headerStart := strings.Index(line, "##") + 2
666+
restOfLine := line[headerStart:]
667+
restOfLine = strings.TrimLeft(restOfLine, " ")
668+
669+
headerEnd := -1
670+
inHeaderText := false
671+
672+
for i, r := range restOfLine {
673+
if i < 10 && (r >= 0x1F300 || r == ' ') {
674+
continue
675+
}
676+
if !inHeaderText && (r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z') {
677+
inHeaderText = true
678+
continue
679+
}
680+
if inHeaderText && i > 0 {
681+
prevRune := rune(restOfLine[i-1])
682+
if (prevRune >= 'a' && prevRune <= 'z') && (r >= 'A' && r <= 'Z') {
683+
headerEnd = i
684+
break
685+
}
686+
}
687+
}
688+
689+
if headerEnd > 0 {
690+
headerText := strings.TrimSpace(restOfLine[:headerEnd])
691+
contentText := strings.TrimSpace(restOfLine[headerEnd:])
692+
result = append(result, "## "+headerText)
693+
if contentText != "" {
694+
result = append(result, contentText)
695+
}
696+
} else {
697+
result = append(result, line)
698+
}
699+
} else if strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "##") {
700+
// Similar logic for # headers
701+
headerStart := strings.Index(line, "#") + 1
702+
restOfLine := line[headerStart:]
703+
restOfLine = strings.TrimLeft(restOfLine, " ")
704+
705+
headerEnd := -1
706+
inHeaderText := false
707+
708+
for i, r := range restOfLine {
709+
if i < 10 && (r >= 0x1F300 || r == ' ') {
710+
continue
711+
}
712+
if !inHeaderText && (r >= 'A' && r <= 'Z' || r >= 'a' && r <= 'z') {
713+
inHeaderText = true
714+
continue
715+
}
716+
if inHeaderText && i > 0 {
717+
prevRune := rune(restOfLine[i-1])
718+
if (prevRune >= 'a' && prevRune <= 'z') && (r >= 'A' && r <= 'Z') {
719+
headerEnd = i
720+
break
721+
}
722+
}
723+
}
724+
725+
if headerEnd > 0 {
726+
headerText := strings.TrimSpace(restOfLine[:headerEnd])
727+
contentText := strings.TrimSpace(restOfLine[headerEnd:])
728+
result = append(result, "# "+headerText)
729+
if contentText != "" {
730+
result = append(result, contentText)
731+
}
732+
} else {
733+
result = append(result, line)
734+
}
735+
} else {
736+
result = append(result, line)
737+
}
738+
}
739+
740+
return strings.Join(result, "\n")
741+
}
742+
600743
// normalizeInlineTables converts inline tables (tables without line breaks) to multi-line format
601744
func (s *SlackUI) normalizeInlineTables(text string) string {
602745
// Pattern: | col1 | col2 || :--- | :--- || row1 | row2 |

pkg/ui/slack/slack_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,3 +675,45 @@ func TestNewTableBlockLimits(t *testing.T) {
675675
})
676676
}
677677
}
678+
679+
// TestNormalizeInlineHeaders verifies that markdown headers without line breaks after them
680+
// are correctly normalized to have the content on a separate line.
681+
func TestNormalizeInlineHeaders(t *testing.T) {
682+
s := &SlackUI{}
683+
684+
tests := []struct {
685+
name string
686+
input string
687+
expected string
688+
}{
689+
{
690+
name: "header with emoji and inline text",
691+
input: "### 💡 Comparison with Working PodI noticed that your other pod",
692+
expected: "### 💡 Comparison with Working Pod\nI noticed that your other pod",
693+
},
694+
{
695+
name: "header with emoji and inline text (different pattern)",
696+
input: "### 🚀 RecommendationTo fix this, you should",
697+
expected: "### 🚀 Recommendation\nTo fix this, you should",
698+
},
699+
{
700+
name: "normal header with line break",
701+
input: "### Header Text\n\nSome content",
702+
expected: "### Header Text\n\nSome content",
703+
},
704+
{
705+
name: "header without inline text",
706+
input: "### Just a Header",
707+
expected: "### Just a Header",
708+
},
709+
}
710+
711+
for _, tt := range tests {
712+
t.Run(tt.name, func(t *testing.T) {
713+
result := s.normalizeInlineHeaders(tt.input)
714+
if result != tt.expected {
715+
t.Errorf("normalizeInlineHeaders() failed\nInput:\n%s\n\nExpected:\n%s\n\nGot:\n%s", tt.input, tt.expected, result)
716+
}
717+
})
718+
}
719+
}

0 commit comments

Comments
 (0)