diff --git a/internal/app/command.go b/internal/app/command.go index db5e114..ee2439a 100644 --- a/internal/app/command.go +++ b/internal/app/command.go @@ -6,16 +6,16 @@ type Command string const ( // User commands - Start Command = "/start" // Start the bot - Why Command = "/why" // Tells the purpose of the bot - Note Command = "/note" // Add a new note - Last Command = "/last" // Get the last note - Analysis Command = "/analysis" // Get the analysis of the last notes - TherapySession Command = "/therapy_session" // Start a real-time therapy session - Language Command = "/language" // Change the language - Settings Command = "/settings" // Show the settings - Help Command = "/help" // Show the help - Version Command = "/version" // Show the version + Start Command = "/start" // Start the bot + Why Command = "/why" // Tells the purpose of the bot + Note Command = "/note" // Add a new note + Last Command = "/last" // Get the last note + Analysis Command = "/analysis" // Get the analysis of the last notes + Therapy Command = "/therapy" // Start a real-time therapy session + Language Command = "/language" // Change the language + Settings Command = "/settings" // Show the settings + Help Command = "/help" // Show the help + Version Command = "/version" // Show the version // Hidden user commands MissingNote Command = "/missing_note" // Ask to put a note from the previous text diff --git a/internal/app/session.go b/internal/app/session.go index 70935a0..ee40486 100644 --- a/internal/app/session.go +++ b/internal/app/session.go @@ -59,8 +59,8 @@ func handleSession(session *Session) { command := session.Job.Command commandStr := string(command) // Keep therapy session as the effective last command during active window - if command == None && session.User.TherapySessionEndAt != nil && now.Before(*session.User.TherapySessionEndAt) && session.Job.LastCommand == TherapySession { - commandStr = string(TherapySession) + if command == None && session.User.TherapySessionEndAt != nil && now.Before(*session.User.TherapySessionEndAt) && session.Job.LastCommand == Therapy { + commandStr = string(Therapy) } session.User.LastCommand = &commandStr session.User.Timestamp = &now @@ -71,7 +71,7 @@ func handleSession(session *Session) { } // If another command is called while therapy is active, end therapy - if command != None && command != TherapySession && session.User.TherapySessionEndAt != nil { + if command != None && command != Therapy && session.User.TherapySessionEndAt != nil { endTherapySession(session) } @@ -87,7 +87,7 @@ func handleSession(session *Session) { handleWhy(session) case Note: startNote(session) - case TherapySession: + case Therapy: startTherapySession(session) case MissingNote: handleMissingNote(session, noteStorage) @@ -161,7 +161,7 @@ func handleSession(session *Session) { switch session.Job.LastCommand { case Note: finishNote(*session.Job.Input, session, noteStorage) - case TherapySession: + case Therapy: relayTherapyMessage(*session.Job.Input, session) case Support: finishFeedback(session, feedbackStorage) @@ -176,7 +176,7 @@ func handleSession(session *Session) { // If the user is not in typing mode but therapy session is active, keep the session alive and forward if session.User.TherapySessionEndAt != nil && now.Before(*session.User.TherapySessionEndAt) { session.User.IsTyping = true - session.Job.LastCommand = TherapySession + session.Job.LastCommand = Therapy relayTherapyMessage(*session.Job.Input, session) } else { handleInputWithoutCommand(session) diff --git a/internal/app/therapy.go b/internal/app/therapy.go index 904742d..647209c 100644 --- a/internal/app/therapy.go +++ b/internal/app/therapy.go @@ -61,7 +61,11 @@ func callTherapySessionEndpoint(text string, session *Session) *string { // Relay a user message to the therapy session backend and append the reply func relayTherapyMessage(text string, session *Session) { //coverage:ignore - // Send immediate typing acknowledgement is already enabled via IsTyping + // Send immediate feedback message to let user know we're processing + thinkingMessageKey := getRandomThinkingMessage(session) + setOutputText(thinkingMessageKey, session) + + // Send the actual request to the therapy endpoint reply := callTherapySessionEndpoint(text, session) if reply != nil && *reply != "" { setOutputRawText(*reply, session) @@ -250,6 +254,29 @@ func parseRunResponse(respStr string) *string { return &respStr } +// Get a random thinking message to show immediate feedback +func getRandomThinkingMessage(session *Session) string { + thinkingMessages := []string{ + "therapy_thinking_1", + "therapy_thinking_2", + "therapy_thinking_3", + "therapy_thinking_4", + "therapy_thinking_5", + } + + // Use a simple hash of the user ID to get consistent randomness per user + userIDHash := 0 + for _, char := range session.User.ID { + userIDHash += int(char) + } + + // Add current time to make it more random + userIDHash += int(time.Now().Unix()) + + selectedIndex := userIDHash % len(thinkingMessages) + return thinkingMessages[selectedIndex] +} + // End the therapy session and notify the user func endTherapySession(session *Session) { session.User.IsTyping = false diff --git a/internal/app/therapy_test.go b/internal/app/therapy_test.go index 795729c..ce912d5 100644 --- a/internal/app/therapy_test.go +++ b/internal/app/therapy_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "os" "testing" "time" @@ -15,7 +16,7 @@ import ( func TestStartTherapySession(t *testing.T) { ctx := context.Background() user := &database.User{IsTyping: false} - session := createSession(&Job{Command: TherapySession}, user, nil, &ctx) + session := createSession(&Job{Command: Therapy}, user, nil, &ctx) startTherapySession(session) @@ -37,7 +38,7 @@ func TestEndTherapySession(t *testing.T) { ctx := context.Background() endAt := time.Now().Add(10 * time.Minute) user := &database.User{IsTyping: true, TherapySessionEndAt: &endAt} - session := createSession(&Job{Command: TherapySession}, user, nil, &ctx) + session := createSession(&Job{Command: Therapy}, user, nil, &ctx) endTherapySession(session) @@ -89,11 +90,35 @@ func TestRelayTherapyMessage(t *testing.T) { relayTherapyMessage("hi", session) - if len(session.Job.Output) == 0 { - t.Fatalf("expected at least one output") + if len(session.Job.Output) < 2 { + t.Fatalf("expected at least two outputs (immediate feedback + response)") + } + + // First output should be a thinking message + firstOutput := session.Job.Output[0] + validThinkingKeys := []string{ + "therapy_thinking_1", + "therapy_thinking_2", + "therapy_thinking_3", + "therapy_thinking_4", + "therapy_thinking_5", + } + + found := false + for _, validKey := range validThinkingKeys { + if firstOutput.TextID == validKey { + found = true + break + } + } + + if !found { + t.Fatalf("expected first output to be a thinking message, got %s", firstOutput.TextID) } - if session.Job.Output[0].TextID != "Hello, I'm here for you." { - t.Fatalf("unexpected relay text: %s", session.Job.Output[0].TextID) + + // Second output should be the actual therapy response + if session.Job.Output[1].TextID != "Hello, I'm here for you." { + t.Fatalf("unexpected relay text: %s", session.Job.Output[1].TextID) } } @@ -143,13 +168,40 @@ func TestHandleSession_ForwardDuringActive(t *testing.T) { locale := "en" user := &database.User{ID: "u1", TherapySessionEndAt: &future, IsTyping: true, Locale: &locale} input := "some text" - job := &Job{Command: None, LastCommand: TherapySession, Input: &input} + job := &Job{Command: None, LastCommand: Therapy, Input: &input} session := createSession(job, user, nil, &ctx) handleSession(session) - if len(session.Job.Output) == 0 || session.Job.Output[0].TextID != "Therapist reply" { - t.Fatalf("expected Therapist reply, got %v", session.Job.Output) + if len(session.Job.Output) < 2 { + t.Fatalf("expected at least two outputs (immediate feedback + response), got %v", session.Job.Output) + } + + // First output should be a thinking message + firstOutput := session.Job.Output[0] + validThinkingKeys := []string{ + "therapy_thinking_1", + "therapy_thinking_2", + "therapy_thinking_3", + "therapy_thinking_4", + "therapy_thinking_5", + } + + found := false + for _, validKey := range validThinkingKeys { + if firstOutput.TextID == validKey { + found = true + break + } + } + + if !found { + t.Fatalf("expected first output to be a thinking message, got %s", firstOutput.TextID) + } + + // Second output should be the actual therapy response + if session.Job.Output[1].TextID != "Therapist reply" { + t.Fatalf("expected Therapist reply in second output, got %v", session.Job.Output) } } @@ -187,11 +239,35 @@ func TestRelayTherapyMessage_ExistingSessionContinues(t *testing.T) { relayTherapyMessage("hi", session) - if len(session.Job.Output) == 0 { - t.Fatalf("expected at least one output") + if len(session.Job.Output) < 2 { + t.Fatalf("expected at least two outputs (immediate feedback + response)") } - if session.Job.Output[0].TextID != "Hello again" { - t.Fatalf("unexpected relay text: %s", session.Job.Output[0].TextID) + + // First output should be a thinking message + firstOutput := session.Job.Output[0] + validThinkingKeys := []string{ + "therapy_thinking_1", + "therapy_thinking_2", + "therapy_thinking_3", + "therapy_thinking_4", + "therapy_thinking_5", + } + + found := false + for _, validKey := range validThinkingKeys { + if firstOutput.TextID == validKey { + found = true + break + } + } + + if !found { + t.Fatalf("expected first output to be a thinking message, got %s", firstOutput.TextID) + } + + // Second output should be the actual therapy response + if session.Job.Output[1].TextID != "Hello again" { + t.Fatalf("unexpected relay text: %s", session.Job.Output[1].TextID) } } @@ -215,3 +291,113 @@ func TestHandleSession_EndOnOtherCommand(t *testing.T) { t.Fatalf("expected commands_hint second, got %s", session.Job.Output[1].TextID) } } + +func TestGetRandomThinkingMessage(t *testing.T) { + ctx := context.Background() + user := &database.User{ID: "test_user_123"} + session := createSession(&Job{Command: Therapy}, user, nil, &ctx) + + // Test that we get a valid thinking message key + messageKey := getRandomThinkingMessage(session) + + validKeys := []string{ + "therapy_thinking_1", + "therapy_thinking_2", + "therapy_thinking_3", + "therapy_thinking_4", + "therapy_thinking_5", + } + + found := false + for _, validKey := range validKeys { + if messageKey == validKey { + found = true + break + } + } + + if !found { + t.Fatalf("expected one of %v, got %s", validKeys, messageKey) + } +} + +func TestRelayTherapyMessageWithImmediateFeedback(t *testing.T) { + // Create a mock server that simulates the therapy endpoint + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate a successful therapy response + response := `{"content": {"parts": [{"text": "I understand your concerns. Let's work through this together."}]}}` + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(response)) + })) + defer server.Close() + + // Set environment variables for testing + originalURL := os.Getenv("CAPY_THERAPY_SESSION_URL") + originalCloud := os.Getenv("CLOUD") + originalToken := os.Getenv("CAPY_AGENT_TOKEN") + + os.Setenv("CAPY_THERAPY_SESSION_URL", server.URL) + os.Setenv("CLOUD", "false") + os.Setenv("CAPY_AGENT_TOKEN", "test_token") + + defer func() { + os.Setenv("CAPY_THERAPY_SESSION_URL", originalURL) + os.Setenv("CLOUD", originalCloud) + os.Setenv("CAPY_AGENT_TOKEN", originalToken) + }() + + ctx := context.Background() + user := &database.User{ + ID: "test_user", + TherapySessionId: stringPtr("test_session_id"), + TherapySessionEndAt: timePtr(time.Now().Add(30 * time.Minute)), + IsTyping: true, + } + session := createSession(&Job{Command: Therapy}, user, nil, &ctx) + + // Call relayTherapyMessage + relayTherapyMessage("I'm feeling anxious about work", session) + + // Check that we got at least 2 outputs: immediate feedback + actual response + if len(session.Job.Output) < 2 { + t.Fatalf("expected at least 2 outputs, got %d", len(session.Job.Output)) + } + + // Check that the first output is a thinking message + firstOutput := session.Job.Output[0] + validThinkingKeys := []string{ + "therapy_thinking_1", + "therapy_thinking_2", + "therapy_thinking_3", + "therapy_thinking_4", + "therapy_thinking_5", + } + + found := false + for _, validKey := range validThinkingKeys { + if firstOutput.TextID == validKey { + found = true + break + } + } + + if !found { + t.Fatalf("expected first output to be a thinking message, got %s", firstOutput.TextID) + } + + // Check that the second output is the actual therapy response + secondOutput := session.Job.Output[1] + if !strings.Contains(secondOutput.TextID, "I understand your concerns") { + t.Fatalf("expected therapy response in second output TextID, got: %s", secondOutput.TextID) + } +} + +// Helper functions for creating test data +func stringPtr(s string) *string { + return &s +} + +func timePtr(t time.Time) *time.Time { + return &t +} diff --git a/internal/translator/translations.go b/internal/translator/translations.go index 886079a..e354bfa 100644 --- a/internal/translator/translations.go +++ b/internal/translator/translations.go @@ -9,9 +9,9 @@ const translationsJSON = `{ "start_note" : "Share your thoughts and feelings by entering them in the text field and sending them my way. Your personal reflections will be securely saved in your journal 👇", "finish_note" : "Your thoughts have been successfully saved. Thank you for trusting CapyMind. Remember, each note is a step forward on your journey to better mental well-being 🙂", "start_therapy_session": "Your therapist is here and ready to listen. Share what’s on your mind — I’m with you.", - "therapy_session_ended": "Your therapy session has ended. You can start a new one anytime with /therapy_session.", + "therapy_session_ended": "Your therapy session has ended. You can start a new one anytime with /therapy.", "start_therapy_session": "Your therapist is here and ready to listen. Share what’s on your mind — I’m with you.", - "therapy_session_ended": "Your therapy session has ended. You can start a new one anytime with /therapy_session.", + "therapy_session_ended": "Your therapy session has ended. You can start a new one anytime with /therapy.", "your_last_note": "Here’s your most recent note 👇\n\n", "no_notes": "You haven’t added any entries yet. Start by sharing your thoughts and feelings with CapyMind.", "commands_hint": "Here are the commands you can use to interact with CapyMind 👇\n\n/start Begin using the bot\n/note Make a journal entry\n/last View your most recent entry\n/analysis Receive an analysis of your journal\n/settings Settings\n/language Set your language preference\n/timezone Set your time zone\n/feedback Give feedback \n/help Get assistance with using CapyMind\n", @@ -89,7 +89,12 @@ const translationsJSON = `{ "timezone_not_found": "The time zone for the city you entered could not be found. Please set up your time zone manually.", "is_this_your_time": "Is this your current time? 🕒\n", "yes": "Yes", - "no": "No" + "no": "No", + "therapy_thinking_1": "I hear you. Let me think about this...", + "therapy_thinking_2": "Thank you for sharing that with me. I'm processing your thoughts...", + "therapy_thinking_3": "I understand. Give me a moment to reflect on what you've said...", + "therapy_thinking_4": "That's important. I'm taking time to consider your words carefully...", + "therapy_thinking_5": "I'm listening and thinking about your message..." }, "uk": { "welcome": "Ласкаво просимо до CapyMind 👋 Ваш особистий журнал для записів про психічне здоров'я тут, щоб допомогти вам на вашому шляху. Рефлексуйте над своїми думками та емоціями, використовуйте нагадування, щоб залишатися на шляху, та досліджуйте інсайти терапії, щоб поглибити свою самосвідомість.", @@ -97,9 +102,9 @@ const translationsJSON = `{ "start_note": "Поділіться своїми думками та почуттями, введіть їх у текстове поле та надішліть. Ваші особисті роздуми будуть безпечно збережені у вашому журналі 👇", "finish_note": "Ваші думки успішно збережені. Дякуємо вам за довіру CapyMind. Пам'ятайте, кожен запис - це крок вперед на вашому шляху до кращого психічного самопочуття 🙂", "start_therapy_session": "Терапевт поруч і готовий слухати. Поділіться тим, що на душі — я з вами.", - "therapy_session_ended": "Сесію терапії завершено. Ви можете розпочати нову командою /therapy_session.", + "therapy_session_ended": "Сесію терапії завершено. Ви можете розпочати нову командою /therapy.", "start_therapy_session": "Терапевт поруч і готовий слухати. Поділіться тим, що на душі — я з вами.", - "therapy_session_ended": "Сесію терапії завершено. Ви можете розпочати нову командою /therapy_session.", + "therapy_session_ended": "Сесію терапії завершено. Ви можете розпочати нову командою /therapy.", "your_last_note": "Ось ваш останній запис 👇\n\n", "no_notes": "Ви ще не додали жодних записів. Почніть, поділившись своїми думками та почуттями з CapyMind.", "commands_hint": "Ось команди, які ви можете використовувати для взаємодії з CapyMind 👇\n\n/start Почати використання бота\n/note Зробити запис у журнал\n/last Переглянути ваш останній запис\n/analysis Отримати аналіз вашого журналу\n/settings Налаштування\n/language Встановити мову\n/timezone Встановити ваш часовий пояс\n/feedback Залишити відгук\n/help Отримати допомогу з використання CapyMind\n", @@ -177,6 +182,11 @@ const translationsJSON = `{ "timezone_not_found": "Часовий пояс для введеного вами міста не вдалося знайти. Будь ласка, встановіть свій часовий пояс вручну.", "is_this_your_time": "Це ваш поточний час? 🕒\n", "yes": "Так", - "no": "Ні" + "no": "Ні", + "therapy_thinking_1": "Я чую вас. Дозвольте мені подумати про це...", + "therapy_thinking_2": "Дякую, що поділилися цим зі мною. Я обробляю ваші думки...", + "therapy_thinking_3": "Я розумію. Дайте мені хвилинку, щоб обдумати те, що ви сказали...", + "therapy_thinking_4": "Це важливо. Я беру час, щоб ретельно обдумати ваші слова...", + "therapy_thinking_5": "Я слухаю та думаю про ваше повідомлення..." } }` diff --git a/scripts/localize_telegram_bot.sh b/scripts/localize_telegram_bot.sh index 9cb0182..1829138 100755 --- a/scripts/localize_telegram_bot.sh +++ b/scripts/localize_telegram_bot.sh @@ -75,6 +75,7 @@ COMMANDS_EN='[ { "command": "note", "description": "Make a note" }, { "command": "last", "description": "Get last note" }, { "command": "analysis", "description": "Get an analysis" }, + { "command": "therapy", "description": "Start therapy session" }, { "command": "settings", "description": "Settings" }, { "command": "support", "description": "Send feedback" }, { "command": "help", "description": "Help" } @@ -98,6 +99,7 @@ COMMANDS_UK='[ { "command": "note", "description": "Зробити запис" }, { "command": "last", "description": "Останній запис" }, { "command": "analysis", "description": "Зробити аналіз" }, + { "command": "therapy", "description": "Розпочати сесію терапії" }, { "command": "settings", "description": "Налаштування" }, { "command": "support", "description": "Надіслати відгук" }, { "command": "help", "description": "Допомога" }