From d8c4d84dc75300c6d4d8b0adceafa33741960b92 Mon Sep 17 00:00:00 2001 From: Aldrik Ramaekers Date: Sat, 27 Sep 2025 18:38:35 +0200 Subject: added http lib, working on AI invoice importing --- src/ai_service.cpp | 142 ++++++++++++++++++++++++++++++++++++++++++++ src/log.cpp | 4 +- src/main.cpp | 11 +++- src/ui/imgui_extensions.cpp | 9 ++- src/ui/ui_expenses.cpp | 17 +++++- src/ui/ui_invoices.cpp | 2 +- src/ui/ui_log.cpp | 2 +- 7 files changed, 174 insertions(+), 13 deletions(-) create mode 100644 src/ai_service.cpp (limited to 'src') diff --git a/src/ai_service.cpp b/src/ai_service.cpp new file mode 100644 index 0000000..2553f61 --- /dev/null +++ b/src/ai_service.cpp @@ -0,0 +1,142 @@ +/* +* Copyright (c) 2025 Aldrik Ramaekers +* +* Permission to use, copy, modify, and/or distribute this software for any +* purpose with or without fee is hereby granted, provided that the above +* copyright notice and this permission notice appear in all copies. +* +* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include +#include +#include + +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include "log.hpp" +#include "ai_service.hpp" + + +// ---- Utility: simple JSON value extractor (very naive) ---- +char *extract_json_value(const char *json, const char *key, char *out, size_t out_size) { + char pattern[128]; + snprintf(pattern, sizeof(pattern), "\"%s\"", key); + const char *pos = strstr(json, pattern); + if (!pos) return NULL; + pos = strchr(pos, ':'); + if (!pos) return NULL; + pos++; + + // Skip whitespace and quotes + while (*pos == ' ' || *pos == '\"') pos++; + + size_t i = 0; + while (*pos && *pos != '\"' && *pos != ',' && *pos != '}' && i < out_size - 1) { + out[i++] = *pos++; + } + out[i] = '\0'; + return out; +} + +// ---- Read file chunk ---- +size_t read_chunk(FILE *fp, char *buffer, size_t chunk_size) { + return fread(buffer, 1, chunk_size, fp); +} + +const char* get_filename(const char* path) { + const char* filename = strrchr(path, '/'); // for Unix-style paths + if (filename) return filename + 1; // skip the '/' + filename = strrchr(path, '\\'); // for Windows-style paths + if (filename) return filename + 1; + return path; // no slashes found, path itself is filename +} + +ai_request* ai_document_to_invoice(char* file_path) +{ + const char *api_key = administration_get_ai_service().api_key_public; + const char *filename = get_filename(file_path); + + FILE* orig_file = fopen(file_path, "rb"); + if (orig_file == NULL) { + log_error("ERROR: file to upload could not be opened."); + return 0; + } + + fseek(orig_file, 0L, SEEK_END); + long sz = ftell(orig_file); + fseek(orig_file, 0, SEEK_SET); + + httplib::SSLClient cli("api.openai.com", 443); + cli.enable_server_certificate_verification(false); + + char body[512]; + snprintf(body, sizeof(body), "{\"filename\":\"%s\",\"purpose\":\"user_data\", \"bytes\": %d, \"mime_type\": \"application/pdf\", \"expires_after\": { \"anchor\": \"created_at\", \"seconds\": 3600 } }", filename, sz); + + httplib::Headers headers; + headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + + httplib::Result res = cli.Post("/v1/uploads", headers, body, "application/json"); + if (!res || res->status != 200) { + log_error("ERROR Failed to create upload."); + fclose(orig_file); + return 0; + } + + char upload_id[128]; + extract_json_value(res->body.c_str(), "id", upload_id, sizeof(upload_id)); + size_t part_size = 64000000; // 64mb + log_info("Created upload %s with part size %zu.", upload_id, part_size); + + char *buffer = (char*)malloc(part_size); + + int part_number = 0; + while (1) { + size_t read_bytes = read_chunk(orig_file, buffer, part_size); + if (read_bytes == 0) break; + + httplib::Headers part_headers; + part_headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + part_headers.insert(std::make_pair("Content-Type", "multipart/form-data")); + + std::string chunk(buffer, read_bytes); + + httplib::UploadFormDataItems items = { + {"data", chunk, filename, "application/pdf"} + }; + + char path[256]; + snprintf(path, sizeof(path), "/v1/uploads/%s/parts?part_number=%d", upload_id, part_number); + + httplib::Result part_res = cli.Post(path, part_headers, items); + + if (!part_res || part_res->status != 200) { + log_error("Failed to upload part %d.", part_number); + free(buffer); + fclose(orig_file); + return 0; + } + + log_info("Uploaded part %d\n", part_number); + part_number++; + } + + free(buffer); + fclose(orig_file); + + // ---------- Step 3: Complete upload ---------- + httplib::Result complete_res = cli.Post((std::string("/v1/uploads/") + upload_id + "/complete").c_str(), + headers, "", "application/json"); + if (!complete_res || complete_res->status != 200) { + log_error("ERROR Failed to complete upload."); + return 0; + } + + return 0; +} \ No newline at end of file diff --git a/src/log.cpp b/src/log.cpp index 5180705..094bcab 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -20,9 +20,9 @@ #include "timer.h" #include "log.hpp" -log g_log = {0}; +program_log g_log = {0}; -log* get_log() +program_log* get_log() { return &g_log; } diff --git a/src/main.cpp b/src/main.cpp index 2e3d215..bdddd98 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ #include "ui.hpp" #include "administration.hpp" #include "administration_writer.hpp" +#include "administration_reader.hpp" // Data static HWND hwnd; @@ -58,7 +59,7 @@ void platorm_maximize_window() // Main code //int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) -int main() +int main(int argc, char** argv) { int start_width = 1280; int start_height = 800; @@ -139,7 +140,13 @@ int main() timer_lib_initialize(); administration_writer_create(); - administration_create_default(""); + + if (argc < 2) { + administration_create_default(""); + } + else { + administration_reader_open_existing(argv[1]); + } // Main loop bool done = false; diff --git a/src/ui/imgui_extensions.cpp b/src/ui/imgui_extensions.cpp index 4dc1fb9..4a0117b 100644 --- a/src/ui/imgui_extensions.cpp +++ b/src/ui/imgui_extensions.cpp @@ -43,20 +43,19 @@ namespace ImGui } } - bool FormFileSelector(char* buffer) + bool FormInvoiceFileSelector(char* text, char* buffer) { bool result = false; float widthAvailable = ImGui::GetContentRegionAvail().x; ImGui::SetNextItemWidth(widthAvailable*0.5f); - if (ImGui::Button("Select file...")) // @localize + if (ImGui::Button(text)) { - // You can adjust filters, title, default path - const char *filterPatterns[] = { "*.png", "*.jpg", "*.pdf", "*" }; + const char *filterPatterns[] = { "*.pdf" }; const char *file = tinyfd_openFileDialog( "Choose a file", // dialog title // @localize NULL, // default path - 4, // number of filter patterns + 1, // number of filter patterns filterPatterns, // filter patterns array NULL, // single filter description (can be NULL) 0); // allowMultiple (0 = single) diff --git a/src/ui/ui_expenses.cpp b/src/ui/ui_expenses.cpp index 92f9c7c..790394a 100644 --- a/src/ui/ui_expenses.cpp +++ b/src/ui/ui_expenses.cpp @@ -27,6 +27,7 @@ #include "administration.hpp" #include "administration_writer.hpp" #include "locales.hpp" +#include "ai_service.hpp" static view_state current_view_state = view_state::LIST; static invoice active_invoice = {0}; @@ -85,7 +86,7 @@ static void draw_expense_form(invoice* buffer, bool viewing_only = false) ImGui::Separator(); - if (ImGui::FormFileSelector(buffer->document.original_path)) { + if (ImGui::FormInvoiceFileSelector("Select file...", buffer->document.original_path)) { // @localize buffer->document.copy_path[0] = 0; } @@ -162,7 +163,7 @@ static void ui_draw_expenses_list() s32 max_page = (total_invoice_count + items_per_page - 1) / items_per_page; if (max_page == 0) max_page = 1; - // Table header controls: create button and pagination. + // Table header controls: create, import, and pagination. if (ImGui::Button(localize("form.create"))) { current_view_state = view_state::CREATE; @@ -172,6 +173,18 @@ static void ui_draw_expenses_list() active_invoice.status = invoice_status::INVOICE_RECEIVED; } + char import_file_path[MAX_LEN_PATH] = {0}; + ImGui::SameLine(); + if (ImGui::FormInvoiceFileSelector("+ Import", import_file_path)) { // @localize + current_view_state = view_state::CREATE; + active_invoice = administration_invoice_create_empty(); // @leak + active_invoice.customer = administration_company_info_get(); + active_invoice.is_outgoing = 0; + active_invoice.status = invoice_status::INVOICE_RECEIVED; + + ai_document_to_invoice(import_file_path); + } + if (current_page >= max_page-1) current_page = max_page-1; if (current_page < 0) current_page = 0; diff --git a/src/ui/ui_invoices.cpp b/src/ui/ui_invoices.cpp index fdeafc6..1be0c82 100644 --- a/src/ui/ui_invoices.cpp +++ b/src/ui/ui_invoices.cpp @@ -206,7 +206,7 @@ static void draw_invoice_form(invoice* buffer, bool viewing_only = false) ImGui::Separator(); - if (ImGui::FormFileSelector(buffer->document.original_path)) { + if (ImGui::FormInvoiceFileSelector("Select file...", buffer->document.original_path)) { // @localize buffer->document.copy_path[0] = 0; } diff --git a/src/ui/ui_log.cpp b/src/ui/ui_log.cpp index 5b380a3..fa56f4a 100644 --- a/src/ui/ui_log.cpp +++ b/src/ui/ui_log.cpp @@ -23,7 +23,7 @@ void ui_draw_log() { - log* l = get_log(); + program_log* l = get_log(); for (int i = (int)l->history_length-1; i >= 0; i--) { -- cgit v1.2.3-70-g09d2