/* * Copyright 2024, Santiago Lema * All rights reserved. Distributed under the terms of the MIT license. */ #include "MainWindow.h" static int progressAnim = 0; static int progressColor = 0; static bool progressColorUp = false; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Conversation.h" #include #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "Window" MainWindow::MainWindow() : BWindow(BRect(50, 50, 600, 400), B_TRANSLATE("DumBer"), B_TITLED_WINDOW, B_ASYNCHRONOUS_CONTROLS | B_QUIT_ON_WINDOW_CLOSE) { // Without this conversation would never get bmessages from HttpRequest LockLooper(); AddHandler(_conversation); UnlockLooper(); BMenuBar *menuBar = _BuildMenu(); BLayoutBuilder::Group<>(this, B_VERTICAL, 0).Add(menuBar).AddGlue().End(); _inputField = new BTextView("input_view", B_WILL_DRAW); _inputField->SetText("What is the matrix... printer, Neo ?"); _inputField->MakeEditable(true); _inputField->MakeSelectable(true); _inputField->SetWordWrap(true); _modelMenu = new BPopUpMenu("Models"); _modelField = new BMenuField("model_field", NULL, _modelMenu); _modelField->SetEnabled(false); _progress = new BStatusBar("prog"); _progress->SetMaxValue(100); _progress->SetTo(0); _progress->SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR)); // Info view, only one line high _infoView = new BTextView("info"); _infoView->SetText("..."); _infoView->SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR)); _infoView->MakeEditable(false); _infoView->MakeSelectable(false); _infoView->SetWordWrap(false); _infoConversation = new BTextView("convers"); _infoConversation->SetText("(No history)"); _infoConversation->SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR)); _infoConversation->MakeEditable(false); _infoConversation->MakeSelectable(false); _infoConversation->SetWordWrap(false); float lineHeight = _infoView->LineHeight(0); _infoView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight)); _infoView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, lineHeight)); _infoConversation->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight)); _infoConversation->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, lineHeight)); float askH = lineHeight * 2; _inputField->SetExplicitMinSize(BSize(B_SIZE_UNSET, askH)); _sendButton = new BButton("send", B_TRANSLATE("Send"), new BMessage(kSendPrompt), B_WILL_DRAW | B_NAVIGABLE); _sendButton->MakeDefault(true); _answerView = new BTextView("answer", B_WILL_DRAW); _answerView->MakeEditable(false); // Disable editing _answerView->MakeSelectable(true); // Enable text selection _answerView->SetWordWrap(true); _answerView->SetExplicitMinSize(BSize(B_SIZE_UNSET, askH * 2)); _answerScrollView = new BScrollView("scroll_view", _answerView, B_FRAME_EVENTS | B_WILL_DRAW, 0, false, true, B_FANCY_BORDER); // horizontal and vertical scrollbars // Enable correct resizing behavior, otherwise we get no correct scrollbar after resizing _answerView->SetFlags(_answerView->Flags() | B_FULL_UPDATE_ON_RESIZE); _answerScrollView->SetFlags(_answerView->Flags() | B_FULL_UPDATE_ON_RESIZE); /* BView *imageView = new BView("icon_view", B_WILL_DRAW | B_FOLLOW_NONE); imageView->SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR)); BBitmap* oneImage = BTranslationUtils::GetBitmap('RAWT', 77, NULL); imageView->SetViewColor(ui_color(B_CONTROL_BORDER_COLOR)); if (oneImage) { imageView->SetViewBitmap(oneImage,B_FOLLOW_LEFT_TOP, B_TILE_BITMAP); printf("Image loaded!\n"); } else { printf("Image NOT loaded!\n"); } */ BStringView *headerQuestion = new BStringView("questionLabel", "Your question: "); BStringView *headerAnswer = new BStringView("questionAnswer", "Answer: "); rgb_color colorQuestion = {100, 100,150, 255}; //rgb_color colorAnswer = {100, 100,150, 255}; //high color = txt headerQuestion->SetHighColor(colorQuestion); headerAnswer->SetHighColor(colorQuestion); BLayoutBuilder::Group<>(this, B_VERTICAL, 0) .AddGlue(0.1) .Add(headerQuestion) .AddGroup(B_HORIZONTAL, 0, 1) .Add(_inputField) .AddGlue(0.01) // .Add(imageView) .AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING, 0) .AddGroup(B_HORIZONTAL, 0) .Add(_infoConversation) .AddGlue(0.1) .End() .AddGroup(B_HORIZONTAL, 0) .Add(_modelField) .AddGlue(0.1) .End() .AddGroup(B_HORIZONTAL, 0) .Add(_sendButton) .AddGlue(0.1) .End() .End() .End() .AddGlue(0.1) .Add(headerAnswer) .AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING, 1.0) .Add(_answerScrollView, 1) .Add(_progress, 0.1) .Add(_infoView, 0.1) .End() .SetInsets(6, 6, 6, 6) .End(); // Loop Just to animate progress in Bar BMessageRunner *runner = new BMessageRunner(this, // target BHandler new BMessage(kPulse), 100000 // interval in μs (0 ms) ); updateHistoryInfo(); PostMessage(kCheckKey); } void MainWindow::checkValidKey() { if (!_conversation->validKey) { _infoView->SetText("MISSING API KEY"); ShowMissingKeyAlertAndQuit(); return; } else { _infoView->SetText("API Key loaded."); waitMode = true; progressColor = 0; progressAnim = 1; _conversation->loadModels(); _infoView->SetText("Requesting model lists..."); } } void MainWindow::ShowMissingKeyAlertAndQuit() { BAlert *alert = new BAlert( "Missing key file!", "Create a file named 'openai_key' containing a valid OpenAI Token on one " "line in \n\n/boot/home/config/settings/openai_key .\n\nThen relaunch " "the app.\n\nBe aware that this is not a safe storage so don't use " "valuable keys.", "Oh, no", "Sigh", "Just give up", B_WIDTH_AS_USUAL, B_WARNING_ALERT); alert->SetType(B_INFO_ALERT); uint32 result = alert->Go(); PostMessage(B_QUIT_REQUESTED); } MainWindow::~MainWindow() {} void MainWindow::SelectModelByName(const char *targetLabel) { BMenu *menu = _modelField->Menu(); if (!menu) return; for (int32 i = 0; i < menu->CountItems(); ++i) { BMenuItem *item = menu->ItemAt(i); // printf("comparing %s\n", item->Label()); if (item && strcmp(item->Label(), targetLabel) == 0) { printf("FOUND %s\n", item->Label()); item->SetMarked(true); PostMessage(item->Message()); break; } } } void MainWindow::updateHistoryInfo() { _infoConversation->SetText(_conversation->buildHistoryInfoLine().c_str()); } void MainWindow::MessageReceived(BMessage *message) { switch (message->what) { case kCheckKey: { checkValidKey(); } break; case kClearHistory: { printf("will clear history"); _infoView->SetText("Cleared conversation history. Starting new context"); _inputField->SetText(""); _answerView->SetText(""); _conversation->ClearHistory(); updateHistoryInfo(); } break; case kModelSelected: { printf("model selected"); const char *model; if (message->FindString("model", &model) == B_OK) { _infoView->SetText(BString("Model selected: ") << model); printf("model selected: %s\n", model); _conversation->setModel(model); } } break; case kModelsReceived: { waitMode = false; progressAnim = 100; _modelMenu->RemoveItems(0, _modelMenu->CountItems(), true); _infoView->SetText("Models list received."); _modelMenu->SetTargetForItems(this); const char *model; for (int32 i = 0; message->FindString("model", i, &model) == B_OK; ++i) { BMessage *modelMsg = new BMessage(kModelSelected); modelMsg->AddString("model", model); BMenuItem *item = new BMenuItem(model, modelMsg); item->SetTarget(this); _modelMenu->AddItem(item); } _modelField->SetEnabled(true); _sendButton->SetEnabled(true); SelectModelByName("gpt-4o-mini"); } break; case kPulse: { uint8 r = (uint8)((20 * progressColor) / 255); uint8 g = (uint8)((128 * progressColor) / 255); uint8 b = (uint8)((255 * progressColor) / 255); rgb_color color = {r, g, b, 255}; _progress->SetBarColor(color); if (waitMode) { int step = 8; if (progressColorUp) progressColor += step; else progressColor -= step; if (progressColor >= 255) { progressColorUp = false; progressColor = 255; } else if (progressColor <= 0) { progressColorUp = true; progressColor = 0; } if (progressAnim >= 1 && progressAnim <= 99) { _progress->SetTo(progressAnim); progressAnim++; } } else _progress->SetTo(progressAnim); } break; // case kMsgNewFile: { // fSaveMenuItem->SetEnabled(false); // printf("New\n"); // } break; // case kMsgOpenFile: { // fSaveMenuItem->SetEnabled(true); // printf("Open\n"); // } break; // case kMsgSaveFile: { // printf("Save\n"); // } break; case kSendPrompt: { _progress->SetMaxValue(100); _progress->SetTo(0); _answerView->SetText("..."); progressAnim = 1; // will trigger animation _sendButton->SetEnabled(false); waitMode = true; progressColor = 255; printf("Button Pressed\n"); _infoView->SetText("Asking..."); _conversation->ask(std::string(_inputField->Text())); } break; case kSendReply: { _sendButton->SetEnabled(true); waitMode = false; progressColor = 255; printf("Conversation returned!\n"); _infoView->SetText("Answer Received"); progressAnim = 100; const char *text; if (message->FindString("text", &text) == B_OK) { // printf("Received text: %s\n", text); // Do something with text (e.g., set it to a BTextView) _answerView->SetText(text); } else { printf("No text found in message.\n"); _answerView->SetText("NO TEXT IN REPLY"); } _progress->SetMaxValue(100); _progress->SetTo(100); updateHistoryInfo(); } break; default: { // message->PrintToStream(); BHandler::MessageReceived( message); // call the parent handler for other messages // _infoView->SetText(message->FindMessage()); break; } } // end switch } // end function BMenuBar *MainWindow::_BuildMenu() { BMenuBar *menuBar = new BMenuBar("menubar"); BMenu *menu; BMenuItem *item; // menu 'File' menu = new BMenu(B_TRANSLATE("File")); // item = new BMenuItem(B_TRANSLATE("New"), new BMessage(kMsgNewFile), 'N'); // menu->AddItem(item); // item = new BMenuItem(B_TRANSLATE("Open" B_UTF8_ELLIPSIS), // new BMessage(kMsgOpenFile), 'O'); // menu->AddItem(item); // fSaveMenuItem = // new BMenuItem(B_TRANSLATE("Save"), new BMessage(kMsgSaveFile), 'S'); // fSaveMenuItem->SetEnabled(false); // menu->AddItem(fSaveMenuItem); // menu->AddSeparatorItem(); item = new BMenuItem(B_TRANSLATE("About" B_UTF8_ELLIPSIS), new BMessage(B_ABOUT_REQUESTED)); item->SetTarget(be_app); menu->AddItem(item); item = new BMenuItem(B_TRANSLATE("Quit"), new BMessage(B_QUIT_REQUESTED), 'Q'); menu->AddItem(item); menuBar->AddItem(menu); //------------------------- menu = new BMenu(B_TRANSLATE("Conversation")); item = new BMenuItem(B_TRANSLATE("Send Prompt" B_UTF8_ELLIPSIS), new BMessage(kSendPrompt)); item->SetShortcut('S', B_COMMAND_KEY); item->SetTarget(this); menu->AddItem(item); item = new BMenuItem(B_TRANSLATE("Clear History" B_UTF8_ELLIPSIS), new BMessage(kClearHistory)); item->SetTarget(this); item->SetShortcut('K', B_COMMAND_KEY | B_SHIFT_KEY); menu->AddItem(item); menuBar->AddItem(menu); return menuBar; }