/* * Copyright 2024, Santiago Lema * All rights reserved. Distributed under the terms of the MIT license. */ #include "MainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "include/json.hpp" using json = nlohmann::json; using namespace BPrivate::Network; #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "Window" static const uint32 kMsgNewFile = 'fnew'; static const uint32 kMsgOpenFile = 'fopn'; static const uint32 kMsgSaveFile = 'fsav'; static const uint32 kSendPrompt = 'kspt'; static const uint32 kQuestionChanged = 'kqch'; MainWindow::MainWindow() : BWindow(BRect(100, 100, 600, 400), B_TRANSLATE("BeDumb"), B_TITLED_WINDOW, B_ASYNCHRONOUS_CONTROLS | B_QUIT_ON_WINDOW_CLOSE) { BMenuBar *menuBar = _BuildMenu(); BLayoutBuilder::Group<>(this, B_VERTICAL, 0).Add(menuBar).AddGlue().End(); _inputField = new BTextControl("", "What is the matrix ?", new BMessage(kQuestionChanged)); _progress = new BStatusBar("prog"); _progress->SetMaxValue(100); _progress->SetTo(0); _progress->SetViewColor(ui_color(B_PANEL_BACKGROUND_COLOR)); BStringView *header = new BStringView("biglabel", "Let's Be Dumber!"); BFont font; header->GetFont(&font); font.SetSize(20); header->SetFont(&font); // 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); float lineHeight = _infoView->LineHeight(0); _infoView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight)); _infoView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, lineHeight)); float askH = lineHeight * 2; _inputField->SetExplicitMinSize(BSize(B_SIZE_UNSET, askH)); _inputField->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, askH * 6)); _apiKey = ReadOpenAIKey(); printf("key is: %s", _apiKey.String()); BButton *sendButton = new BButton("send", B_TRANSLATE("Send"), new BMessage(kSendPrompt), B_WILL_DRAW | B_NAVIGABLE); _answerView = new BTextView("answer", B_WILL_DRAW | B_FOLLOW_ALL); _answerView->MakeEditable(false); // Disable editing _answerView->MakeSelectable(true); // Enable text selection _answerView->SetWordWrap(true); BScrollView *scrollView = new BScrollView("scroll_view", _answerView, B_FOLLOW_ALL | B_WILL_DRAW, 0, false, true); // horizontal and vertical scrollbars BLayoutBuilder::Group<>(this, B_VERTICAL, 0) .AddGlue(0.1) .Add(header) .AddGlue(0.1) .AddGroup(B_HORIZONTAL, 0, 1) .Add(_inputField) .AddGroup(B_HORIZONTAL, 0, 1) .Add(sendButton) .End() .End() .AddGlue(0.1) .Add(scrollView) .Add(_progress) .Add(_infoView) .SetInsets(5, 5, 5, 5) .End(); } MainWindow::~MainWindow() {} void MainWindow::MessageReceived(BMessage *message) { switch (message->what) { // 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 kQuestionChanged: { printf("Question Changed\n"); _progress->SetTo(2); // sendQuery(); } break; case kSendPrompt: { printf("Button Pressed\n"); sendQuery(); } break; case UrlEvent::HostNameResolved: { printf("Host name resolved\n"); auto name = message->GetString(UrlEventData::HostName); message->PrintToStream(); _infoView->SetText("Hostname resolve..."); _infoView->SetText(name); _progress->SetTo(5); } break; case UrlEvent::ConnectionOpened: { printf("ConnectionOpened\n"); _progress->SetTo(10); _infoView->SetText("connection opened..."); } break; case UrlEvent::ResponseStarted: { printf("ResponseStarted\n"); _progress->SetTo(14); _infoView->SetText("ResponseStarted..."); } break; case UrlEvent::HttpRedirect: { printf("HttpRedirect\n"); _progress->SetTo(16); _infoView->SetText("HttpRedirect..."); } break; case UrlEvent::RequestCompleted: { printf("RequestCompleted\n"); auto identifier = message->GetInt32(UrlEventData::Id, -1); if (_lastResult->Identity() == identifier) { // The following call will not block, because we have been notified // that the request is done. BHttpBody body = _lastResult->Body(); if (body.text.has_value()) { json parsed = json::parse(body.text.value().String()); std::string content = parsed["choices"][0]["message"]["content"]; _answerView->SetText(content.c_str()); // _answerView->SetText(body.text.value()); } else _answerView->SetText("nuthin'"); } _infoView->SetText("Completed"); _progress->SetMaxValue(100); _progress->SetTo(100); } break; case UrlEvent::HttpStatus: { printf("HttpStatus\n"); _infoView->SetText("HttpStatus received"); _progress->SetTo(20); } break; case UrlEvent::BytesWritten: { _infoView->SetText("Some bytes written.."); auto identifier = message->GetInt32(UrlEventData::Id, -1); if (_lastResult->Identity() == identifier) { off_t numBytes = message->GetInt64(UrlEventData::NumBytes, 0); off_t totalBytes = message->GetInt64(UrlEventData::TotalBytes, 0); _progress->SetTo(numBytes); _progress->SetMaxValue(totalBytes); } } break; case UrlEvent::DownloadProgress: { auto identifier = message->GetInt32(UrlEventData::Id, -1); if (_lastResult->Identity() == identifier) { off_t nn = message->GetInt64(UrlEventData::NumBytes, 0); off_t totalBytes = message->GetInt64(UrlEventData::TotalBytes, 0); _progress->SetTo(nn); _progress->SetMaxValue(totalBytes); _infoView->SetText("Download Progress.."); } } break; default: { // message->PrintToStream(); BWindow::MessageReceived( message); // call the parent handler for other messages // _infoView->SetText(message->FindMessage()); break; } } // end switch } // end function void MainWindow::sendQuery() { if (_lastResult) _sharedSession.Cancel(_lastResult->Identity()); _progress->SetMaxValue(100); _progress->SetTo(0); auto url = BUrl("https://api.openai.com/v1/chat/completions"); BHttpRequest request = BHttpRequest(url); request.SetMethod(BHttpMethod::Post); // if the API key file contains a new line bhttpfields will crash with invalid // content .end() requires include algorithm std::string key = _apiKey.String(); key.erase(std::remove(key.begin(), key.end(), '\n'), key.end()); key.erase(std::remove(key.begin(), key.end(), '\r'), key.end()); std::string bearer = std::string("Bearer ") + std::string(key); BHttpFields fields = BHttpFields(); fields.AddField("Authorization", bearer); // fields.AddField("Content-Type", "application/json"); //NO, this will // crash, we set it in request request.SetFields(fields); json bodyJson = { {"model", "gpt-3.5-turbo"}, {"messages", {{{"role", "user"}, {"content", _inputField->Text()}}}}}; std::string body = bodyJson.dump(); BString mime = BString("application/json"); // request.SetRequestBody(body.c_str(),mime); auto memoryIO = std::make_unique(body.c_str(), body.size()); request.SetRequestBody(std::move(memoryIO), "application/json", body.size()); printf("Sending Prompt to server: %s\n", url.UrlString().String()); _lastResult = _sharedSession.Execute(std::move(request), nullptr, this); if (_lastResult) { printf("Result has identity: %d\n", _lastResult->Identity()); } } 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); return menuBar; } BString MainWindow::ReadOpenAIKey() { // /boot/home/config/openai_key // or ~/.config/openai_key on linux BPath configPath; if (find_directory(B_USER_SETTINGS_DIRECTORY, &configPath) != B_OK) return "error: couldn't find config directory"; configPath.Append("openai_key"); BFile file(configPath.Path(), B_READ_ONLY); printf("full path:%s\n", configPath.Path()); if (file.InitCheck() != B_OK) return "error: couldn't open key file "; off_t size; file.GetSize(&size); char *buffer = new char[size + 1]; file.Read(buffer, size); buffer[size] = '\0'; // null-terminate BString result(buffer); delete[] buffer; return result; }