summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAldrik Ramaekers <aldrikboy@gmail.com>2025-09-27 18:38:35 +0200
committerAldrik Ramaekers <aldrikboy@gmail.com>2025-09-27 18:38:35 +0200
commitd8c4d84dc75300c6d4d8b0adceafa33741960b92 (patch)
tree00e2dfcc5c836d62fccff76c862e6ec3b0a74db8 /src
parentfa088bb60692ba02d30d39affa9a31d9e2b688e2 (diff)
added http lib, working on AI invoice importing
Diffstat (limited to 'src')
-rw-r--r--src/ai_service.cpp142
-rw-r--r--src/log.cpp4
-rw-r--r--src/main.cpp11
-rw-r--r--src/ui/imgui_extensions.cpp9
-rw-r--r--src/ui/ui_expenses.cpp17
-rw-r--r--src/ui/ui_invoices.cpp2
-rw-r--r--src/ui/ui_log.cpp2
7 files changed, 174 insertions, 13 deletions
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 <aldrik.ramaekers@gmail.com>
+*
+* 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 <fstream>
+#include <iostream>
+#include <string>
+
+#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--)
{