haiku-dumber/MainWindow.cpp

472 lines
12 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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 <Resources.h>
#include <ScrollView.h>
#include <StringView.h>
#include <TranslationUtils.h>
#include <Alert.h>
#include <MessageRunner.h>
#include <View.h>
#include <Path.h>
#include "Conversation.h"
#include <FindDirectory.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));
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 (0ms)
);
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;
}