458 lines
12 KiB
C++
458 lines
12 KiB
C++
/*
|
||
* Copyright 2024, Santiago Lema <santiago@lema.org>
|
||
* 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 <Application.h>
|
||
#include <Bitmap.h>
|
||
#include <Button.h>
|
||
#include <Catalog.h>
|
||
#include <LayoutBuilder.h>
|
||
#include <Menu.h>
|
||
#include <MenuBar.h>
|
||
#include <MimeType.h>
|
||
#include <ScrollView.h>
|
||
#include <StringView.h>
|
||
#include <TranslationUtils.h>
|
||
|
||
#include <Alert.h>
|
||
#include <MessageRunner.h>
|
||
#include <View.h>
|
||
|
||
#include "Conversation.h"
|
||
|
||
#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));
|
||
|
||
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)
|
||
|
||
.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;
|
||
}
|