Skip to content

Commit 9ea6a04

Browse files
feat: save chat history
1 parent 32ac527 commit 9ea6a04

5 files changed

Lines changed: 285 additions & 19 deletions

File tree

internal/constants/commands.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package constants
2+
3+
const (
4+
ExitCommand = "exit"
5+
ClearCommand = "clear"
6+
HistoryCommand = "/history"
7+
EmailCommand = "/email"
8+
)

internal/helpers/http.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package helpers
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
)
9+
10+
func SendChatHistoryEmail(email string, chatHistory interface{}) error {
11+
payload := map[string]interface{}{
12+
"email": email,
13+
"chatHistory": chatHistory,
14+
}
15+
16+
jsonData, err := json.Marshal(payload)
17+
if err != nil {
18+
return fmt.Errorf("error marshaling JSON: %w", err)
19+
}
20+
21+
resp, err := http.Post(
22+
"https://api.harshalranjhani.in/genie/send-chat-history",
23+
"application/json",
24+
bytes.NewBuffer(jsonData),
25+
)
26+
if err != nil {
27+
return fmt.Errorf("error making request: %w", err)
28+
}
29+
defer resp.Body.Close()
30+
31+
if resp.StatusCode != http.StatusOK {
32+
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
33+
}
34+
35+
return nil
36+
}

internal/helpers/llm/gemini.go

Lines changed: 119 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import (
2020
"github.com/chzyer/readline"
2121
"github.com/fatih/color"
2222
"github.com/google/generative-ai-go/genai"
23+
"github.com/harshalranjhani/genie/internal/constants"
2324
"github.com/harshalranjhani/genie/internal/helpers"
25+
"github.com/harshalranjhani/genie/internal/middleware"
2426
"github.com/harshalranjhani/genie/internal/structs"
2527
"github.com/harshalranjhani/genie/pkg/prompts"
2628
"github.com/joho/godotenv"
@@ -436,6 +438,8 @@ func startChatSession(ctx context.Context, cs *genai.ChatSession, scanner *bufio
436438
color.New(color.FgHiMagenta).Println("🧞 Chat session started!")
437439
fmt.Println(style.Render("Type your message and press Enter to send. Type 'exit' to end the session."))
438440
fmt.Println(style.Render("Type 'clear' to clear chat history."))
441+
fmt.Println(style.Render("Type '/history' to export chat history to markdown."))
442+
fmt.Println(style.Render("Type '/email' to email chat history."))
439443
fmt.Println(strings.Repeat("─", 50))
440444

441445
for {
@@ -446,34 +450,34 @@ func startChatSession(ctx context.Context, cs *genai.ChatSession, scanner *bufio
446450

447451
userInput = strings.TrimSpace(userInput)
448452

449-
if strings.ToLower(userInput) == "exit" {
453+
switch strings.ToLower(userInput) {
454+
case constants.ExitCommand:
450455
fmt.Println(style.Render("\n👋 Ending chat session. Goodbye!"))
451-
break
452-
}
453-
454-
// Update clear command to clear screen
455-
if strings.ToLower(userInput) == "clear" {
456-
cs.History = nil // Clear the chat history
456+
return
457+
case constants.ClearCommand:
457458
// Clear terminal screen
458459
fmt.Print("\033[H\033[2J")
459460
// Reprint welcome message
460461
color.New(color.FgHiMagenta).Println("🧞 Chat session started!")
461462
fmt.Println(style.Render("Type your message and press Enter to send. Type 'exit' to end the session."))
462463
fmt.Println(style.Render("Type 'clear' to clear chat history."))
464+
fmt.Println(style.Render("Type '/history' to export chat history to markdown."))
465+
fmt.Println(style.Render("Type '/email' to email chat history."))
463466
fmt.Println(strings.Repeat("─", 50))
464467
continue
468+
case constants.HistoryCommand:
469+
exportGeminiChatHistory(cs.History)
470+
continue
471+
case constants.EmailCommand:
472+
emailChatHistory(cs.History)
473+
continue
465474
}
466475

467476
handleChatMessage(ctx, cs, userInput)
468477
}
469478
}
470479

471480
func handleChatMessage(ctx context.Context, cs *genai.ChatSession, userInput string) {
472-
cs.History = append(cs.History, &genai.Content{
473-
Parts: []genai.Part{genai.Text(userInput)},
474-
Role: "user",
475-
})
476-
477481
s := spinner.New(spinner.CharSets[11], 80*time.Millisecond)
478482
s.Prefix = color.HiCyanString("🤔 Thinking: ")
479483
s.Suffix = " Please wait..."
@@ -499,3 +503,106 @@ func handleChatMessage(ctx context.Context, cs *genai.ChatSession, userInput str
499503
fmt.Println(strings.Repeat("─", 50))
500504
}
501505
}
506+
507+
func exportGeminiChatHistory(history []*genai.Content) {
508+
if len(history) == 0 {
509+
fmt.Printf("%s No chat history available to export.\n", color.RedString("❌"))
510+
return
511+
}
512+
513+
s := spinner.New(spinner.CharSets[35], 100*time.Millisecond)
514+
s.Prefix = color.HiCyanString("📝 Exporting chat history: ")
515+
s.Start()
516+
517+
timestamp := time.Now().Format("2006-01-02-15-04-05")
518+
filename := filepath.Join(".", fmt.Sprintf("chat-history-%s.md", timestamp))
519+
520+
var content strings.Builder
521+
content.WriteString("# Chat History\n\n")
522+
content.WriteString(fmt.Sprintf("Generated on: %s\n\n", time.Now().Format("January 2, 2006 15:04:05")))
523+
content.WriteString("---\n\n")
524+
525+
for _, msg := range history {
526+
switch msg.Role {
527+
case "user":
528+
content.WriteString(fmt.Sprintf("### 💭 You\n%v\n\n", msg.Parts[0]))
529+
case "model":
530+
content.WriteString(fmt.Sprintf("### 🤖 AI\n%v\n\n", msg.Parts[0]))
531+
}
532+
content.WriteString("---\n\n")
533+
}
534+
535+
err := os.WriteFile(filename, []byte(content.String()), 0644)
536+
s.Stop()
537+
538+
if err != nil {
539+
fmt.Printf("%s Failed to export chat history: %v\n", color.RedString("❌"), err)
540+
return
541+
}
542+
543+
successMsg := fmt.Sprintf("✨ Chat history exported to: %s", filename)
544+
fmt.Println(color.GreenString(successMsg))
545+
}
546+
547+
func emailChatHistory(history []*genai.Content) {
548+
if len(history) == 0 {
549+
fmt.Printf("%s No chat history available to email.\n", color.RedString("❌"))
550+
return
551+
}
552+
553+
// Create a divider for visual separation
554+
fmt.Println(strings.Repeat("─", 50))
555+
fmt.Println(color.HiMagentaString("📧 Emailing Chat History"))
556+
fmt.Println(strings.Repeat("─", 50))
557+
558+
// Get current model name
559+
modelName := "gemini-1.5-pro"
560+
if selectedModel, err := keyring.Get("genie", "modelName"); err == nil {
561+
modelName = selectedModel
562+
}
563+
564+
// Get user status to check for verified email
565+
status, err := middleware.LoadStatus()
566+
var email string
567+
if err != nil || status == nil || status.Email == "" {
568+
fmt.Print(color.YellowString("Please enter your email address: "))
569+
fmt.Scanln(&email)
570+
} else {
571+
email = status.Email
572+
}
573+
574+
s := spinner.New(spinner.CharSets[35], 100*time.Millisecond)
575+
s.Prefix = color.HiCyanString("📝 Sending to ") + color.CyanString(email) + color.HiCyanString(": ")
576+
s.Start()
577+
578+
var messages []map[string]string
579+
for _, msg := range history {
580+
messages = append(messages, map[string]string{
581+
"role": msg.Role,
582+
"content": fmt.Sprintf("%v", msg.Parts[0]),
583+
})
584+
}
585+
586+
payload := map[string]interface{}{
587+
"timestamp": time.Now().Format(time.RFC3339),
588+
"model": modelName,
589+
"messages": messages,
590+
"metadata": map[string]string{
591+
"sessionId": fmt.Sprintf("gemini-%d", time.Now().Unix()),
592+
"format": "markdown",
593+
},
594+
}
595+
596+
if err := helpers.SendChatHistoryEmail(email, payload); err != nil {
597+
s.Stop()
598+
fmt.Printf("\n%s Failed to send chat history: %v\n", color.RedString("❌"), err)
599+
fmt.Println(strings.Repeat("─", 50))
600+
return
601+
}
602+
603+
s.Stop()
604+
fmt.Printf("\n%s Chat history sent successfully to %s!\n",
605+
color.GreenString("✨"),
606+
color.CyanString(email))
607+
fmt.Println(strings.Repeat("─", 50))
608+
}

internal/helpers/llm/gpt.go

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import (
2020
"github.com/charmbracelet/lipgloss"
2121
"github.com/chzyer/readline"
2222
"github.com/fatih/color"
23+
"github.com/harshalranjhani/genie/internal/constants"
2324
"github.com/harshalranjhani/genie/internal/helpers"
25+
"github.com/harshalranjhani/genie/internal/middleware"
2426
"github.com/harshalranjhani/genie/pkg/prompts"
2527
"github.com/sashabaranov/go-openai"
2628
"github.com/zalando/go-keyring"
@@ -458,6 +460,9 @@ func StartGPTChat() {
458460

459461
color.New(color.FgHiMagenta).Println("🧞 Chat session started!")
460462
fmt.Println(style.Render("Type your message and press Enter to send. Type 'exit' to end the session."))
463+
fmt.Println(style.Render("Type 'clear' to clear chat history."))
464+
fmt.Println(style.Render("Type '/history' to export chat history to markdown."))
465+
fmt.Println(style.Render("Type '/email' to email chat history."))
461466
fmt.Println(strings.Repeat("─", 50))
462467

463468
messages := []openai.ChatCompletionMessage{
@@ -475,13 +480,11 @@ func StartGPTChat() {
475480

476481
userInput = strings.TrimSpace(userInput)
477482

478-
if strings.ToLower(userInput) == "exit" {
483+
switch strings.ToLower(userInput) {
484+
case constants.ExitCommand:
479485
fmt.Println(style.Render("\n👋 Ending chat session. Goodbye!"))
480-
break
481-
}
482-
483-
// Update clear command to clear screen
484-
if strings.ToLower(userInput) == "clear" {
486+
return
487+
case constants.ClearCommand:
485488
// Clear message history
486489
messages = []openai.ChatCompletionMessage{
487490
{
@@ -494,8 +497,17 @@ func StartGPTChat() {
494497
// Reprint welcome message
495498
color.New(color.FgHiMagenta).Println("🧞 Chat session started!")
496499
fmt.Println(style.Render("Type your message and press Enter to send. Type 'exit' to end the session."))
500+
fmt.Println(style.Render("Type 'clear' to clear chat history."))
501+
fmt.Println(style.Render("Type '/history' to export chat history to markdown."))
502+
fmt.Println(style.Render("Type '/email' to email chat history."))
497503
fmt.Println(strings.Repeat("─", 50))
498504
continue
505+
case constants.HistoryCommand:
506+
exportChatHistory(messages)
507+
continue
508+
case constants.EmailCommand:
509+
emailGPTChatHistory(messages)
510+
continue
499511
}
500512

501513
messages = append(messages, openai.ChatCompletionMessage{
@@ -554,3 +566,106 @@ func StartGPTChat() {
554566
fmt.Println("\n" + strings.Repeat("─", 50))
555567
}
556568
}
569+
570+
func exportChatHistory(messages []openai.ChatCompletionMessage) {
571+
if len(messages) <= 1 { // Check if there's only the system message or no messages
572+
fmt.Printf("%s No chat history available to export.\n", color.RedString("❌"))
573+
return
574+
}
575+
576+
s := spinner.New(spinner.CharSets[35], 100*time.Millisecond)
577+
s.Prefix = color.HiCyanString("📝 Exporting chat history: ")
578+
s.Start()
579+
580+
timestamp := time.Now().Format("2006-01-02-15-04-05")
581+
filename := filepath.Join(".", fmt.Sprintf("chat-history-%s.md", timestamp))
582+
583+
var content strings.Builder
584+
content.WriteString("# Chat History\n\n")
585+
content.WriteString(fmt.Sprintf("Generated on: %s\n\n", time.Now().Format("January 2, 2006 15:04:05")))
586+
content.WriteString("---\n\n")
587+
588+
for _, msg := range messages[1:] { // Skip the system message
589+
switch msg.Role {
590+
case openai.ChatMessageRoleUser:
591+
content.WriteString(fmt.Sprintf("### 💭 You\n%s\n\n", msg.Content))
592+
case openai.ChatMessageRoleAssistant:
593+
content.WriteString(fmt.Sprintf("### 🤖 AI\n%s\n\n", msg.Content))
594+
}
595+
content.WriteString("---\n\n")
596+
}
597+
598+
err := os.WriteFile(filename, []byte(content.String()), 0644)
599+
s.Stop()
600+
601+
if err != nil {
602+
fmt.Printf("%s Failed to export chat history: %v\n", color.RedString("❌"), err)
603+
return
604+
}
605+
606+
successMsg := fmt.Sprintf("✨ Chat history exported to: %s", filename)
607+
fmt.Println(color.GreenString(successMsg))
608+
}
609+
610+
func emailGPTChatHistory(messages []openai.ChatCompletionMessage) {
611+
if len(messages) <= 1 {
612+
fmt.Printf("%s No chat history available to email.\n", color.RedString("❌"))
613+
return
614+
}
615+
616+
// Create a divider for visual separation
617+
fmt.Println(strings.Repeat("─", 50))
618+
fmt.Println(color.HiMagentaString("📧 Emailing Chat History"))
619+
fmt.Println(strings.Repeat("─", 50))
620+
621+
// Get current model name
622+
modelName := "gpt-4"
623+
if selectedModel, err := keyring.Get("genie", "modelName"); err == nil {
624+
modelName = selectedModel
625+
}
626+
627+
// Get user status to check for verified email
628+
status, err := middleware.LoadStatus()
629+
var email string
630+
if err != nil || status == nil || status.Email == "" {
631+
fmt.Print(color.YellowString("Please enter your email address: "))
632+
fmt.Scanln(&email)
633+
} else {
634+
email = status.Email
635+
}
636+
637+
s := spinner.New(spinner.CharSets[35], 100*time.Millisecond)
638+
s.Prefix = color.HiCyanString("📝 Sending to ") + color.CyanString(email) + color.HiCyanString(": ")
639+
s.Start()
640+
641+
var chatMessages []map[string]string
642+
for _, msg := range messages[1:] {
643+
chatMessages = append(chatMessages, map[string]string{
644+
"role": string(msg.Role),
645+
"content": msg.Content,
646+
})
647+
}
648+
649+
payload := map[string]interface{}{
650+
"timestamp": time.Now().Format(time.RFC3339),
651+
"model": modelName,
652+
"messages": chatMessages,
653+
"metadata": map[string]string{
654+
"sessionId": fmt.Sprintf("gpt-%d", time.Now().Unix()),
655+
"format": "markdown",
656+
},
657+
}
658+
659+
if err := helpers.SendChatHistoryEmail(email, payload); err != nil {
660+
s.Stop()
661+
fmt.Printf("\n%s Failed to send chat history: %v\n", color.RedString("❌"), err)
662+
fmt.Println(strings.Repeat("─", 50))
663+
return
664+
}
665+
666+
s.Stop()
667+
fmt.Printf("\n%s Chat history sent successfully to %s!\n",
668+
color.GreenString("✨"),
669+
color.CyanString(email))
670+
fmt.Println(strings.Repeat("─", 50))
671+
}

pkg/cmd/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ var versionCmd = &cobra.Command{
2020
}
2121

2222
// Version represents the current version of genie
23-
const Version = "v2.8.1"
23+
const Version = "v2.8.2"

0 commit comments

Comments
 (0)