diff --git a/pkg/llm/assistant.go b/pkg/llm/assistant.go index 48cf7d0..1e7140b 100644 --- a/pkg/llm/assistant.go +++ b/pkg/llm/assistant.go @@ -223,3 +223,11 @@ Maintain maximum analytical depth while ensuring clarity and actionability in pr return response, nil } + +func (a *Assistant) StartSuggestionChat(suggestions []string) *SuggestionChat { + return NewSuggestionChat( + a, + a.context, + suggestions, + ) +} diff --git a/pkg/llm/chat.go b/pkg/llm/chat.go new file mode 100644 index 0000000..b5267a4 --- /dev/null +++ b/pkg/llm/chat.go @@ -0,0 +1,93 @@ +package llm + +import ( + "fmt" + "strings" +) + +type SuggestionChat struct { + assistant *Assistant + history []Message + context string + suggestions []string +} + +func NewSuggestionChat(assistant *Assistant, initialContext string, suggestions []string) *SuggestionChat { + return &SuggestionChat{ + assistant: assistant, + history: make([]Message, 0), + context: initialContext, + suggestions: suggestions, + } +} + +func (sc *SuggestionChat) Chat(userInput string) (string, error) { + // Add user message to history + sc.history = append(sc.history, Message{ + Role: "user", + Content: userInput, + }) + + systemPrompt := `You are an advanced task optimization assistant engaged in a discussion about specific task suggestions. Your core responsibilities: + +CONTEXT AWARENESS: +- Maintain strict relevance to the session context and current suggestions +- Detect and flag off-topic or digressing questions +- Guide users back to relevant discussion points + +SUGGESTION CLARIFICATION: +- Provide detailed, actionable explanations for suggestions +- Break down complex tasks into clear, achievable steps +- Highlight dependencies and prerequisites +- Explain the reasoning behind each suggestion +- Focus on practical implementation details + +RESPONSE GUIDELINES: +1. If question is relevant: + - Provide clear, structured response + - Include specific steps or clarifications + - Reference context when applicable + - Maintain focus on task completion + +2. If question seems off-topic: + - Politely flag the digression + - Explain why it seems unrelated + - Offer to hear user's perspective + - Guide back to relevant discussion + +3. For implementation queries: + - Break down into concrete steps + - Highlight potential challenges + - Suggest specific approaches + - Focus on actionability + +Current Session Context: +%s + +Current Suggestions Under Discussion: +%s` + + messages := []Message{ + { + Role: "system", + Content: fmt.Sprintf(systemPrompt, sc.context, strings.Join(sc.suggestions, "\n")), + }, + } + + // Add chat history + messages = append(messages, sc.history...) + + // Get response from llm + response, err := sc.assistant.perplexity.GetResponse(messages) + if err != nil { + return "", err + } + + // Add assistant's response to history + sc.history = append(sc.history, Message{ + Role: "assistant", + Content: response, + }) + + return response, nil +} diff --git a/pkg/pomodoro/pom.go b/pkg/pomodoro/pom.go index d123f76..fdf68f9 100644 --- a/pkg/pomodoro/pom.go +++ b/pkg/pomodoro/pom.go @@ -13,6 +13,7 @@ import ( "github.com/1x-eng/tomatick/pkg/ltm" "github.com/1x-eng/tomatick/pkg/llm" + "github.com/1x-eng/tomatick/pkg/markdown" "github.com/1x-eng/tomatick/config" @@ -21,6 +22,8 @@ import ( "github.com/chzyer/readline" "github.com/logrusorgru/aurora" + "bufio" + "github.com/1x-eng/tomatick/pkg/context" "github.com/1x-eng/tomatick/pkg/ui" tea "github.com/charmbracelet/bubbletea" @@ -37,6 +40,7 @@ var commandInstructions = []struct { {"edit N text", "Edit task number N with new text"}, {"remove N", "Remove task number N from the list"}, {"suggest", "Get AI-powered task suggestions"}, + {"discuss suggestions", "Start an interactive discussion about current suggestions"}, {"flush", "Clear any existing in-memory AI suggestions"}, {"help", "Show this help message"}, {"quit", "End the session and save progress"}, @@ -55,6 +59,7 @@ type TomatickMemento struct { currentSuggestions []string currentTasks []string lastAnalysis string + currentChat *llm.SuggestionChat } func NewTomatickMemento(cfg *config.Config) *TomatickMemento { @@ -235,7 +240,6 @@ func (p *TomatickMemento) captureTasks() []string { instructions := p.theme.Styles.SystemInstruction.Render(sb.String()) fmt.Println(p.theme.Styles.Subtitle.Render(header + "\n" + instructions)) - assistant := llm.NewAssistant(p.llmClient, p.sessionContext) var tasks []string rl, _ := readline.New(p.auroraInstance.BrightGreen("➤ ").String()) defer rl.Close() @@ -271,6 +275,7 @@ func (p *TomatickMemento) captureTasks() []string { } }() + assistant := llm.NewAssistant(p.llmClient, p.sessionContext) suggestions, err := assistant.GetTaskSuggestions(tasks, p.lastAnalysis) done <- true fmt.Print("\r") // Clear spinner line @@ -280,7 +285,10 @@ func (p *TomatickMemento) captureTasks() []string { continue } p.currentSuggestions = suggestions // Store suggestions + // Initialize chat session here + p.currentChat = assistant.StartSuggestionChat(suggestions) p.displaySuggestions(suggestions) + fmt.Println(p.theme.Styles.InfoText.Render("\nType 'discuss suggestions' to discuss these suggestions with your copilot")) case "flush": p.FlushSuggestions() case "quit": @@ -292,6 +300,12 @@ func (p *TomatickMemento) captureTasks() []string { continue case "": fmt.Println(p.auroraInstance.Red("❗ Task cannot be empty. Please try again.")) + case "discuss suggestions": + if p.currentChat == nil { + fmt.Println(p.auroraInstance.Red("❗ No active suggestion session. Use 'suggest' first.")) + continue + } + p.handleSuggestionChat() default: if strings.HasPrefix(input, "edit ") { p.editTask(&tasks, input) @@ -603,3 +617,84 @@ func (p *TomatickMemento) FlushSuggestions() { p.lastAnalysis = "" fmt.Println(p.auroraInstance.Green("✓ Copilot suggestions and analysis cache flushed successfully.")) } + +func (p *TomatickMemento) handleSuggestionChat() { + // Display chat session start + chatBorder := strings.Repeat(p.theme.Emoji.ChatDivider, 50) + fmt.Println(p.theme.Styles.ChatBorder.Render(chatBorder)) + fmt.Println(p.theme.Styles.ChatHeader.Render( + fmt.Sprintf("%s Interactive Suggestion Discussion %s", + p.theme.Emoji.ChatStart, + p.theme.Emoji.Brain))) + fmt.Println(p.theme.Styles.ChatBorder.Render(chatBorder)) + + // Display current suggestions for reference + fmt.Println(p.theme.Styles.InfoText.Render("\nCurrent Suggestions:")) + for i, suggestion := range p.currentSuggestions { + fmt.Printf("%s %s %s\n", + p.theme.Emoji.Bullet, + p.theme.Styles.TaskNumber.Render(fmt.Sprintf("%d.", i+1)), + p.theme.Styles.AIMessage.Render(suggestion)) + } + + fmt.Println(p.theme.Styles.SystemInstruction.Render("\nAsk questions or discuss these suggestions (type 'exit' to end chat)")) + + scanner := bufio.NewScanner(os.Stdin) + for { + fmt.Printf("%s", p.theme.Styles.ChatPrompt.PaddingTop(0).PaddingBottom(0).Render(p.theme.Emoji.UserInput+" ")) + + if !scanner.Scan() { + break + } + + input := strings.TrimSpace(scanner.Text()) + if input == "" { + continue + } + + if input == "exit" { + fmt.Println(p.theme.Styles.ChatBorder.Render(chatBorder)) + fmt.Println(p.theme.Styles.ChatHeader.Render( + fmt.Sprintf("%s Chat session ended %s", + p.theme.Emoji.ChatEnd, + p.theme.Emoji.Success))) + fmt.Println(p.theme.Styles.ChatBorder.Render(chatBorder)) + break + } + + // Show thinking spinner + spinner := ui.NewSpinner(p.theme.Styles.Spinner. + Foreground(lipgloss.Color("#818CF8")). + Bold(true)) + done := make(chan bool) + + go func() { + for { + select { + case <-done: + return + default: + fmt.Printf("\r%s Thinking...", spinner.Next()) + time.Sleep(100 * time.Millisecond) + } + } + }() + + response, err := p.currentChat.Chat(input) + done <- true + fmt.Print("\r\033[K") // Clear spinner line + + if err != nil { + fmt.Println(p.theme.Styles.ErrorText.Render( + fmt.Sprintf("%s Error: %v", p.theme.Emoji.Error, err))) + continue + } + + // Format and display response + fmt.Println(p.theme.Styles.ChatDivider.Render(strings.Repeat("─", 50))) + fmt.Printf("%s %s\n", + p.theme.Emoji.AIResponse, + p.theme.Styles.AIMessage.Render(response)) + fmt.Println(p.theme.Styles.ChatDivider.Render(strings.Repeat("─", 50))) + } +} diff --git a/pkg/ui/theme.go b/pkg/ui/theme.go index 5b1c5db..ba4598a 100644 --- a/pkg/ui/theme.go +++ b/pkg/ui/theme.go @@ -27,6 +27,12 @@ type ThemeStyles struct { Spinner lipgloss.Style AIMessage lipgloss.Style Break lipgloss.Style + ChatHeader lipgloss.Style + ChatBorder lipgloss.Style + UserMessage lipgloss.Style + ChatPrompt lipgloss.Style + ChatSession lipgloss.Style + ChatDivider lipgloss.Style } type ThemeEmoji struct { @@ -48,6 +54,11 @@ type ThemeEmoji struct { Brain string Bullet string Section string + ChatStart string + UserInput string + AIResponse string + ChatEnd string + ChatDivider string } func NewTheme() *Theme { @@ -111,6 +122,36 @@ func NewTheme() *Theme { Break: lipgloss.NewStyle(). Foreground(lipgloss.Color("#86EFAC")). Bold(true), + + ChatHeader: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#9D8CFF")). + Bold(true). + Padding(1, 0), + + ChatBorder: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#9D8CFF")). + Bold(true). + Padding(1, 0), + + UserMessage: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#9D8CFF")). + Bold(true). + Padding(1, 0), + + ChatPrompt: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#9D8CFF")). + Bold(true). + Padding(1, 0), + + ChatSession: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#9D8CFF")). + Bold(true). + Padding(1, 0), + + ChatDivider: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#9D8CFF")). + Bold(true). + Padding(1, 0), }, Emoji: ThemeEmoji{ TaskComplete: "✅", @@ -131,6 +172,11 @@ func NewTheme() *Theme { Brain: "🧠", Bullet: "•", Section: "📋", + ChatStart: "👋", + UserInput: "👤", + AIResponse: "🤖", + ChatEnd: "👋", + ChatDivider: "🔀", }, } }