diff --git a/data/net.sapples.LiveCaptions.gschema.xml b/data/net.sapples.LiveCaptions.gschema.xml index 04aad72..2b67a06 100644 --- a/data/net.sapples.LiveCaptions.gschema.xml +++ b/data/net.sapples.LiveCaptions.gschema.xml @@ -73,5 +73,20 @@ false Enable DBus API for external applications to use caption output + + + false + Auto-refresh history + + + + '' + OpenAI API Token + + + + '' + OpenAI API Token + diff --git a/src/history-symbolic.svg b/src/history-symbolic.svg new file mode 100644 index 0000000..49279c5 --- /dev/null +++ b/src/history-symbolic.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/livecaptions-application.c b/src/livecaptions-application.c index 2517f2c..98a5f8c 100644 --- a/src/livecaptions-application.c +++ b/src/livecaptions-application.c @@ -19,6 +19,7 @@ #include "livecaptions-application.h" #include "livecaptions-settings.h" #include "livecaptions-window.h" +#include "livecaptions-history-window.h" #include "livecaptions-welcome.h" #include "window-helper.h" #include "asrproc.h" @@ -251,6 +252,46 @@ livecaptions_application_show_preferences(G_GNUC_UNUSED GSimpleAction *action, } +static void history_window_destroy_cb(GtkWidget *widget, gpointer data) +{ + LiveCaptionsApplication *self = (LiveCaptionsApplication *)data; + self->history_window = NULL; +} + +static void +livecaptions_application_show_history(G_GNUC_UNUSED GSimpleAction *action, + G_GNUC_UNUSED GVariant *parameter, + gpointer user_data) +{ + LiveCaptionsApplication *self = LIVECAPTIONS_APPLICATION(user_data); + if (self->welcome != NULL) return; + + if (self->history_window != NULL) { + // If history window already exists, just present it + gtk_window_present(GTK_WINDOW(self->history_window)); + return; + } + + // Get the active window + GtkWindow *window = gtk_application_get_active_window(GTK_APPLICATION(self)); + if (!GTK_IS_WINDOW(window)) { + g_warning("No active window found or invalid active window."); + return; + } + + // Create a new history window with the active window as its transient parent + self->history_window = g_object_new(LIVECAPTIONS_TYPE_HISTORY_WINDOW, "transient-for", window, NULL); + if (!LIVECAPTIONS_IS_HISTORY_WINDOW(self->history_window)) { + g_warning("Failed to create LiveCaptionsHistoryWindow."); + self->history_window = NULL; + return; + } + + // Connect to the destroy signal to reset the reference when the window is closed + g_signal_connect(self->history_window, "destroy", G_CALLBACK(history_window_destroy_cb), self); + + gtk_window_present(GTK_WINDOW(self->history_window)); +} static void on_settings_change(G_GNUC_UNUSED GSettings *settings, char *key, @@ -328,6 +369,10 @@ static void livecaptions_application_init(LiveCaptionsApplication *self) { g_signal_connect(prefs_action, "activate", G_CALLBACK(livecaptions_application_show_preferences), self); g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(prefs_action)); + g_autoptr(GSimpleAction) history_action = g_simple_action_new("history", NULL); + g_signal_connect(history_action, "activate", G_CALLBACK(livecaptions_application_show_history), self); // Corrected line + g_action_map_add_action(G_ACTION_MAP(self), G_ACTION(history_action)); + gboolean use_microphone = g_settings_get_boolean(self->settings, "microphone"); g_autoptr(GSimpleAction) mic_action = g_simple_action_new_stateful("microphone", NULL, g_variant_new_boolean(use_microphone)); g_signal_connect(mic_action, "change-state", G_CALLBACK(livecaptions_application_toggle_microphone), self); diff --git a/src/livecaptions-application.h b/src/livecaptions-application.h index 610700a..c14f11f 100644 --- a/src/livecaptions-application.h +++ b/src/livecaptions-application.h @@ -21,6 +21,7 @@ #include #include "audiocap.h" #include "livecaptions-window.h" +#include "livecaptions-history-window.h" #include "dbus-interface.h" struct _LiveCaptionsApplication { @@ -32,6 +33,8 @@ struct _LiveCaptionsApplication { LiveCaptionsWindow *window; GtkWindow *welcome; + LiveCaptionsHistoryWindow *history_window; + asr_thread asr; audio_thread audio; diff --git a/src/livecaptions-history-window.c b/src/livecaptions-history-window.c index 3f4edbd..58a87a1 100644 --- a/src/livecaptions-history-window.c +++ b/src/livecaptions-history-window.c @@ -26,6 +26,7 @@ #include "common.h" #include "window-helper.h" #include "line-gen.h" +#include "openai.h" G_DEFINE_TYPE(LiveCaptionsHistoryWindow, livecaptions_history_window, GTK_TYPE_WINDOW) @@ -39,39 +40,6 @@ static gboolean close_self_window(gpointer userdata) { return G_SOURCE_REMOVE; } -static void message_cb(AdwMessageDialog *dialog, gchar *response, gpointer userdata){ - if(g_str_equal(response, "delete")){ - erase_all_history(); - - g_idle_add(close_self_window, userdata); - } -} - -static void warn_deletion_cb(LiveCaptionsHistoryWindow *self){ - GtkWindow *parent = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(self))); - GtkWidget *dialog; - - dialog = adw_message_dialog_new(parent, - _("Erase History?"), - _("Everything in history will be erased. You may wish to export your history before erasing!")); - - adw_message_dialog_add_responses(ADW_MESSAGE_DIALOG(dialog), - "cancel", _("_Cancel"), - "delete", _("_Erase Everything"), - NULL); - - adw_message_dialog_set_response_appearance(ADW_MESSAGE_DIALOG(dialog), "delete", ADW_RESPONSE_DESTRUCTIVE); - - adw_message_dialog_set_default_response(ADW_MESSAGE_DIALOG(dialog), "cancel"); - adw_message_dialog_set_close_response(ADW_MESSAGE_DIALOG(dialog), "cancel"); - - g_signal_connect(ADW_MESSAGE_DIALOG(dialog), "response", G_CALLBACK(message_cb), self); - - gtk_window_present(GTK_WINDOW(dialog)); -} - - - static gboolean force_bottom(gpointer userdata) { LiveCaptionsHistoryWindow *self = LIVECAPTIONS_HISTORY_WINDOW(userdata); @@ -302,6 +270,113 @@ static void refresh_cb(LiveCaptionsHistoryWindow *self) { g_idle_add(force_bottom, self); } +static void message_cb(AdwMessageDialog *dialog, gchar *response, gpointer userdata){ + if(g_str_equal(response, "delete")){ + erase_all_history(); + + LiveCaptionsHistoryWindow *self = LIVECAPTIONS_HISTORY_WINDOW(userdata); + refresh_cb(self); + //g_idle_add(close_self_window, userdata); + } +} + +static void warn_deletion_cb(LiveCaptionsHistoryWindow *self){ + GtkWindow *parent = GTK_WINDOW(gtk_widget_get_root(GTK_WIDGET(self))); + GtkWidget *dialog; + + dialog = adw_message_dialog_new(parent, + _("Erase History?"), + _("Everything in history will be erased. You may wish to export your history before erasing!")); + + adw_message_dialog_add_responses(ADW_MESSAGE_DIALOG(dialog), + "cancel", _("_Cancel"), + "delete", _("_Erase Everything"), + NULL); + + adw_message_dialog_set_response_appearance(ADW_MESSAGE_DIALOG(dialog), "delete", ADW_RESPONSE_DESTRUCTIVE); + + adw_message_dialog_set_default_response(ADW_MESSAGE_DIALOG(dialog), "cancel"); + adw_message_dialog_set_close_response(ADW_MESSAGE_DIALOG(dialog), "cancel"); + + g_signal_connect(ADW_MESSAGE_DIALOG(dialog), "response", G_CALLBACK(message_cb), self); + + gtk_window_present(GTK_WINDOW(dialog)); +} + +char* get_full_conversation_history(void) { + const struct history_session *session; + GString *full_history = g_string_new(NULL); + size_t idx = 0; + + // First, determine the total number of sessions + while (get_history_session(idx) != NULL) { + idx++; + } + + // Now iterate over the sessions in forward order + for (size_t i = idx; i > 0; i--) { + session = get_history_session(i - 1); + for (size_t j = 0; j < session->entries_count; j++) { + const struct history_entry *entry = &session->entries[j]; + for (size_t k = 0; k < entry->tokens_count; k++) { + g_string_append(full_history, entry->tokens[k].token); + } + g_string_append_c(full_history, '\n'); + } + } + + return g_string_free(full_history, FALSE); // FALSE means don't deallocate, return the data with a terminating null byte +} + + +static void send_message_cb(GtkButton *button, gpointer userdata) { + LiveCaptionsHistoryWindow *self = LIVECAPTIONS_HISTORY_WINDOW(userdata); + + GtkLabel *response_label = GTK_LABEL(self->ai_response_label); + gtk_label_set_text(GTK_LABEL(self->ai_response_label), "Processing..."); + + OpenAI_Config config; + config.api_url = g_settings_get_string(self->settings, "openai-url"); + config.api_key = g_settings_get_string(self->settings, "openai-key"); + + char *session_text = get_full_conversation_history(); + char *system_text = g_strdup_printf("The user will ask questions relative the converation history: %s", session_text); + + OpenAI_Message messages[] = { + {"system", system_text}, + {"user", gtk_editable_get_text(GTK_EDITABLE(self->chat_input_entry))}, + }; + + OpenAI_Response response; + if (openai_chat(&config, "gpt-4o-mini", messages, sizeof(messages) / sizeof(messages[0]), 0.7, &response)) { + gtk_label_set_text(GTK_LABEL(self->ai_response_label), response.message_content); + } + + free_openai_response(&response); + + gtk_editable_set_text(GTK_EDITABLE(self->chat_input_entry), ""); +} + +// This function will be called every second +static gboolean refresh_history_callback(gpointer userdata) { + LiveCaptionsHistoryWindow *self = LIVECAPTIONS_HISTORY_WINDOW(userdata); + + // Check if auto-refresh is enabled + if (g_settings_get_boolean(self->settings, "auto-refresh")) { + refresh_cb(self); + } + + // Returning TRUE so the function gets called again + return G_SOURCE_CONTINUE; +} + +// Callback function for when the checkbox is toggled +static void auto_refresh_toggled_cb(GtkCheckButton *button, gpointer userdata) { + LiveCaptionsHistoryWindow *self = LIVECAPTIONS_HISTORY_WINDOW(userdata); + gboolean active = gtk_check_button_get_active(button); + g_settings_set_boolean(self->settings, "auto-refresh", active); +} + static void livecaptions_history_window_class_init(LiveCaptionsHistoryWindowClass *klass) { GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); @@ -309,11 +384,16 @@ static void livecaptions_history_window_class_init(LiveCaptionsHistoryWindowClas gtk_widget_class_bind_template_child(widget_class, LiveCaptionsHistoryWindow, scroll); gtk_widget_class_bind_template_child(widget_class, LiveCaptionsHistoryWindow, main_box); + gtk_widget_class_bind_template_child(widget_class, LiveCaptionsHistoryWindow, ai_response_label); + gtk_widget_class_bind_template_child(widget_class, LiveCaptionsHistoryWindow, chat_input_entry); + gtk_widget_class_bind_template_child(widget_class, LiveCaptionsHistoryWindow, auto_refresh_checkbox); gtk_widget_class_bind_template_callback(widget_class, load_more_cb); gtk_widget_class_bind_template_callback(widget_class, export_cb); gtk_widget_class_bind_template_callback(widget_class, warn_deletion_cb); gtk_widget_class_bind_template_callback(widget_class, refresh_cb); + gtk_widget_class_bind_template_callback(widget_class, send_message_cb); + gtk_widget_class_bind_template_callback(widget_class, auto_refresh_toggled_cb); } // TODO: ctrl+f search @@ -322,11 +402,17 @@ static void livecaptions_history_window_init(LiveCaptionsHistoryWindow *self) { self->settings = g_settings_new("net.sapples.LiveCaptions"); - self->session_load = 0; load_to(self, ++self->session_load); g_idle_add(force_bottom, self); g_idle_add(deferred_update_keep_above, self); -} + // Bind the checkbox state to GSettings key + GtkCheckButton *check_button = GTK_CHECK_BUTTON(self->auto_refresh_checkbox); + gboolean active = g_settings_get_boolean(self->settings, "auto-refresh"); + gtk_check_button_set_active(check_button, active); + + // Add a timeout to call refresh_history_callback every second + g_timeout_add_seconds(1, refresh_history_callback, self); +} \ No newline at end of file diff --git a/src/livecaptions-history-window.h b/src/livecaptions-history-window.h index 4af4cd9..e9307ea 100644 --- a/src/livecaptions-history-window.h +++ b/src/livecaptions-history-window.h @@ -26,10 +26,16 @@ struct _LiveCaptionsHistoryWindow { GSettings *settings; + GtkCheckButton *auto_refresh_checkbox; + GtkBox *main_box; GtkScrolledWindow *scroll; + GtkLabel *ai_response_label; + + GtkEntry *chat_input_entry; + size_t session_load; }; diff --git a/src/livecaptions-history-window.ui b/src/livecaptions-history-window.ui index 8d4113f..1ea582c 100644 --- a/src/livecaptions-history-window.ui +++ b/src/livecaptions-history-window.ui @@ -10,6 +10,13 @@ + + + Auto-refresh + True + + + refresh-symbolic @@ -34,50 +41,97 @@ - - + + vertical True + True + - - vertical + True - end - - - - 8 - Load More - center - start - - - - - - - True + + vertical True - start end - 18 - 36 - 12 - 12 + + + 8 + Load More + center + start - vertical + + + + + + + True + True + start + end + 18 + 36 + 12 + 12 + + vertical + + + + + + + + + + AI Response + + + True + + + AI response will appear here... + True + True + fill + fill + True + + + + + + + + horizontal + True + + + True + Type your message here... + + + + + + Send + + + - + \ No newline at end of file diff --git a/src/livecaptions-settings.c b/src/livecaptions-settings.c index d23a8ab..46c6ce7 100644 --- a/src/livecaptions-settings.c +++ b/src/livecaptions-settings.c @@ -202,6 +202,9 @@ static void livecaptions_settings_class_init(LiveCaptionsSettingsClass *klass) { gtk_widget_class_bind_template_child (widget_class, LiveCaptionsSettings, models_list); gtk_widget_class_bind_template_child (widget_class, LiveCaptionsSettings, radio_button_1); gtk_widget_class_bind_template_child (widget_class, LiveCaptionsSettings, file_filter); + + gtk_widget_class_bind_template_child (widget_class, LiveCaptionsSettings, openai_url_entry); + gtk_widget_class_bind_template_child (widget_class, LiveCaptionsSettings, openai_key_entry); gtk_widget_class_bind_template_callback (widget_class, report_cb); gtk_widget_class_bind_template_callback (widget_class, about_cb); @@ -444,4 +447,7 @@ static void livecaptions_settings_init(LiveCaptionsSettings *self) { } init_models_page(self); + + g_settings_bind(self->settings, "openai-key", self->openai_key_entry, "text", G_SETTINGS_BIND_DEFAULT); + g_settings_bind(self->settings, "openai-url", self->openai_url_entry, "text", G_SETTINGS_BIND_DEFAULT); } diff --git a/src/livecaptions-settings.h b/src/livecaptions-settings.h index 0c68318..875e5e3 100644 --- a/src/livecaptions-settings.h +++ b/src/livecaptions-settings.h @@ -53,6 +53,9 @@ struct _LiveCaptionsSettings { GtkCheckButton *radio_button_1; GtkFileFilter *file_filter; + + GtkEntry *openai_url_entry; + GtkEntry *openai_key_entry; }; G_BEGIN_DECLS diff --git a/src/livecaptions-settings.ui b/src/livecaptions-settings.ui index 9f3ac91..e2b23ac 100644 --- a/src/livecaptions-settings.ui +++ b/src/livecaptions-settings.ui @@ -281,6 +281,37 @@ + + + + + openai_url_entry + OpenAI URL + + + center + OpenAI Endpoint URL + 50 + + + + + + + openai_key_entry + OpenAI Key + + + center + false + OpenAI Key + 50 + + + + + + diff --git a/src/livecaptions-window.ui b/src/livecaptions-window.ui index e050345..85b9d12 100644 --- a/src/livecaptions-window.ui +++ b/src/livecaptions-window.ui @@ -22,6 +22,17 @@ + + + + + <Control>H + action(app.history) + + + + + + + +