Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
bd1b3bd
feat: add AppSetting model for secure configuration storage
snehmatic May 12, 2026
9b99554
feat: add crypto utilities for secure token storage
snehmatic May 12, 2026
de525ae
feat: implement AI service and prompt for journal generation
snehmatic May 12, 2026
7d955d2
feat: add CLI support for generating and saving AI journals
snehmatic May 12, 2026
3e299a3
feat: add API handlers for AI settings and journal generation
snehmatic May 12, 2026
5a17fbb
feat: add UI for AI configuration and journal generation
snehmatic May 12, 2026
05f4b6f
fix: use Find instead of First to suppress gorm record not found logs
snehmatic May 12, 2026
e30dfdb
fix: resolve all golangci-lint errors
snehmatic May 12, 2026
3e77716
fix: adjust z-index to prevent dropdown from hiding behind entries
snehmatic May 12, 2026
a49952d
fix: expose backend error messages in UI for AI generation
snehmatic May 12, 2026
604dbb4
fix: trim whitespace from api token and strip model prefix
snehmatic May 12, 2026
a2628c9
feat: add dynamic model discovery and dropdown selection
snehmatic May 12, 2026
1f3f429
feat: add AI connection test button in UI and test endpoint
snehmatic May 12, 2026
5afe28b
fix: test connection uses active UI fields instead of db settings
snehmatic May 13, 2026
f9f7af6
feat: improve AI prompt and enhance UI with modals and summary integr…
snehmatic May 13, 2026
e5889c5
feat: replace AI dropdown with period selection modal
snehmatic May 13, 2026
3eeb4f7
fix: add missing spin animation keyframes for loading indicator
snehmatic May 13, 2026
5c2b71d
fix: increase global header z-index to avoid clipping dropdowns
snehmatic May 13, 2026
d1bc3f4
cleanup
snehmatic May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*.dylib
mindloop
mindloop-server
server

# Test binary, built with `go test -c`
*.test
Expand Down
118 changes: 118 additions & 0 deletions api/v1/handlers_ai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package v1

import (
"encoding/json"
"net/http"

"github.com/snehmatic/mindloop/internal/core/ai"
"github.com/snehmatic/mindloop/internal/core/summary"
"github.com/snehmatic/mindloop/internal/utils"
)

type AISettingsRequest struct {
Provider string `json:"provider"`
Model string `json:"model"`
Token string `json:"token"`
}

func (mlh *MindloopHandler) HandleGetAISettings(w http.ResponseWriter, r *http.Request) {
// Re-initialize AI service with current DB
aiService := ai.NewService(mlh.journal.DB) // hack: accessing DB via a service that has it
provider, model, token, _ := aiService.GetSettings()

hasToken := token != ""

w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"provider": provider,
"model": model,
"hasToken": hasToken,
})
}

func (mlh *MindloopHandler) HandleSaveAISettings(w http.ResponseWriter, r *http.Request) {
var req AISettingsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}

aiService := ai.NewService(mlh.journal.DB)
if err := aiService.SaveSettings(req.Provider, req.Model, req.Token); err != nil {
http.Error(w, "Failed to save settings: "+err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Settings saved successfully"})
}

func (mlh *MindloopHandler) HandleListAIModels(w http.ResponseWriter, r *http.Request) {
aiService := ai.NewService(mlh.journal.DB)
models, err := aiService.ListModels()
if err != nil {
http.Error(w, "Failed to list models: "+err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"models": models,
})
}

func (mlh *MindloopHandler) HandleTestAIConnection(w http.ResponseWriter, r *http.Request) {
var req AISettingsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request payload", http.StatusBadRequest)
return
}

aiService := ai.NewService(mlh.journal.DB)

// If token is empty in the request, fallback to the saved token
if req.Token == "" {
_, _, savedToken, _ := aiService.GetSettings()
req.Token = savedToken
}

if err := aiService.TestConnection(req.Provider, req.Model, req.Token); err != nil {
http.Error(w, "Connection test failed: "+err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"message": "Connection successful!"})
}

func (mlh *MindloopHandler) HandleGenerateAIJournal(w http.ResponseWriter, r *http.Request) {
period := r.URL.Query().Get("period")
if period == "" {
period = "daily"
}

start, end := utils.GetDateRange(period)
summaryService := summary.NewService(mlh.journal.DB)
report, err := summaryService.GenerateSummary(start, end)
if err != nil {
http.Error(w, "Failed to generate summary data", http.StatusInternalServerError)
return
}

aiService := ai.NewService(mlh.journal.DB)
generatedText, err := aiService.GenerateJournal(report)
if err != nil {
http.Error(w, "AI Generation failed: "+err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{
"content": generatedText,
"title": "AI Summary: " + report.DateRange,
})
}
82 changes: 82 additions & 0 deletions cmd/cli/journal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package cli

import (
"fmt"
"time"

cfg "github.com/snehmatic/mindloop/internal/config"
"github.com/snehmatic/mindloop/internal/core/ai"
"github.com/snehmatic/mindloop/internal/core/journal"
"github.com/snehmatic/mindloop/internal/core/summary"
"github.com/snehmatic/mindloop/internal/utils"
"github.com/snehmatic/mindloop/models"
"github.com/spf13/cobra"
Expand All @@ -15,6 +18,79 @@ var (
journalService *journal.Service
)

func getJournalTimeRange(period string) (time.Time, time.Time) {
now := time.Now()
switch period {
case "yearly":
return time.Date(now.Year()-1, now.Month(), now.Day(), 0, 0, 0, 0, now.Location()), now
case "weekly":
end := time.Now()
start := end.AddDate(0, 0, -7)
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, start.Location())
return start, end
case "daily":
return now.Add(-24 * time.Hour), now
default:
return now.Add(-24 * time.Hour), now
}
}

var generateCmd = &cobra.Command{
Use: "generate",
Short: "Auto-generate a journal entry using AI",
Example: `mindloop journal generate -w`,
Run: func(cmd *cobra.Command, args []string) {
weekly, _ := cmd.Flags().GetBool("weekly")
yearly, _ := cmd.Flags().GetBool("yearly")

period := "daily"
if weekly {
period = "weekly"
} else if yearly {
period = "yearly"
}

start, end := getJournalTimeRange(period)

summarySvc := summary.NewService(gdb)
report, err := summarySvc.GenerateSummary(start, end)
if err != nil {
utils.PrintErrorln("Failed to generate summary report:", err)
return
}

utils.PrintLoadingln("✨ Generating AI journal entry...")
aiService := ai.NewService(gdb)
generatedText, err := aiService.GenerateJournal(report)
if err != nil {
utils.PrintErrorln("Failed to generate journal:", err)
return
}

fmt.Println("\n" + generatedText + "\n")

fmt.Print("Would you like to save this into the journal? (y/N): ")
var response string
_, _ = fmt.Scanln(&response)
if response == "y" || response == "Y" {
title := fmt.Sprintf("AI Summary: %s", report.DateRange)
// Assuming journal points default to 5 if config isn't read fully
uc := cfg.UserConfig{}
_ = uc.ReadFromYAML()
pts := uc.PointsConfig.Journal
if pts == 0 {
pts = 5
}
_, err = journalService.CreateEntry(title, generatedText, "reflective", pts)
if err != nil {
utils.PrintErrorln("Failed to save journal:", err)
} else {
utils.PrintSuccessln("Saved successfully!")
}
}
},
}

var journalCmd = &cobra.Command{
Use: "journal",
Short: "Journal your thoughts and progress",
Expand Down Expand Up @@ -159,6 +235,12 @@ func init() {
journalCmd.AddCommand(journalListCmd)
journalCmd.AddCommand(journalViewCmd)
journalCmd.AddCommand(journalDeleteCmd)

generateCmd.Flags().BoolP("daily", "d", false, "Generate daily summary")
generateCmd.Flags().BoolP("weekly", "w", false, "Generate weekly summary")
generateCmd.Flags().BoolP("yearly", "y", false, "Generate yearly summary")
journalCmd.AddCommand(generateCmd)

rootCmd.AddCommand(journalCmd)

mood = journalNewCmd.Flags().StringP("mood", "m", "neutral", "Set journal mood")
Expand Down
1 change: 1 addition & 0 deletions cmd/cli/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var summaryCmd = &cobra.Command{
ac.Logger.Info().Msgf("Generated summary from %s to %s", start.Format("02-Jan-2006"), end.Format("02-Jan-2006"))
utils.PrintSuccessln("Summary generated successfully!")

utils.PrintInfoln("\n💡 Tip: For an AI-generated overview, use `mindloop journal generate -d` (or -w, -y)")
utils.PrintInfoln("You can also use -d, -w, -m, or -y flags to specify the time range for these summaries. (-d is default)")
},
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ func CreateRouter(mlh *v1.MindloopHandler) *mux.Router {
r.HandleFunc("/settings", mlh.HandleSettings).Methods("GET")
r.HandleFunc("/settings/update", mlh.HandleSettingsUpdate).Methods("POST")

// AI Routes
r.HandleFunc("/api/v1/ai/settings", mlh.HandleGetAISettings).Methods("GET")
r.HandleFunc("/api/v1/ai/settings", mlh.HandleSaveAISettings).Methods("POST")
r.HandleFunc("/api/v1/ai/models", mlh.HandleListAIModels).Methods("GET")
r.HandleFunc("/api/v1/ai/test", mlh.HandleTestAIConnection).Methods("POST")
r.HandleFunc("/api/v1/ai/generate", mlh.HandleGenerateAIJournal).Methods("GET")

// Backup Routes
r.HandleFunc("/backup/export", mlh.HandleBackupExport).Methods("GET")
r.HandleFunc("/backup/import", mlh.HandleBackupImport).Methods("POST")
Expand Down
1 change: 1 addition & 0 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func MigrateDB(db *gorm.DB) error {
&models.Routine{},
&models.Task{},
&models.SubTask{},
&models.AppSetting{},
)
if err != nil {
logger.Error().Err(err).Msg("Failed to migrate DB")
Expand Down
Loading