diff --git a/.gitignore b/.gitignore
index 9154f4c..ec60d2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@
hs_err_pid*
replay_pid*
+# ignore compiled files
+target/
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..4c96624
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,53 @@
+
+
+ 4.0.0
+
+ net.metaunix
+ openwebfx
+ 0.1.0
+
+
+ 21
+ 21
+ 21.0.2
+
+
+
+
+
+ org.openjfx
+ javafx-controls
+ ${javafx.version}
+
+
+
+ org.openjfx
+ javafx-fxml
+ ${javafx.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.17.0
+
+
+
+
+
+
+ org.openjfx
+ javafx-maven-plugin
+ 0.0.8
+
+ net.metaunix.openwebfx.App
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/net/metaunix/openwebfx/App.java b/src/main/java/net/metaunix/openwebfx/App.java
new file mode 100644
index 0000000..f141418
--- /dev/null
+++ b/src/main/java/net/metaunix/openwebfx/App.java
@@ -0,0 +1,26 @@
+package net.metaunix.openwebfx;
+
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+public class App extends Application {
+
+ @Override
+ public void start(Stage stage) throws Exception {
+ FXMLLoader loader = new FXMLLoader(
+ getClass().getResource("/net/metaunix/openwebfx/main-view.fxml")
+ );
+
+ Scene scene = new Scene(loader.load(), 1000, 700);
+ stage.setTitle("Open-WebUI Client");
+ stage.setScene(scene);
+ stage.show();
+ }
+
+ public static void main(String[] args) {
+ launch();
+ }
+
+}
diff --git a/src/main/java/net/metaunix/openwebfx/controller/MainController.java b/src/main/java/net/metaunix/openwebfx/controller/MainController.java
new file mode 100644
index 0000000..320590f
--- /dev/null
+++ b/src/main/java/net/metaunix/openwebfx/controller/MainController.java
@@ -0,0 +1,150 @@
+package net.metaunix.openwebfx.controller;
+
+import net.metaunix.openwebfx.model.ChatModel;
+import net.metaunix.openwebfx.service.OpenWebUIService;
+import javafx.application.Platform;
+import javafx.fxml.FXML;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.VBox;
+import javafx.scene.layout.HBox;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class MainController {
+
+ @FXML private TextArea inputField;
+ @FXML private Button sendButton;
+ @FXML private VBox chatContainer;
+ @FXML private ScrollPane chatScrollPane;
+ @FXML private CheckBox darkModeToggle;
+
+ @FXML private BorderPane root;
+
+ private final ChatModel model = new ChatModel();
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ // CHANGE THESE
+ private final OpenWebUIService service =
+ new OpenWebUIService("http://localhost:3000", "YOUR_API_KEY");
+
+ @FXML
+ private void initialize() {
+
+ // Apply dark/light mode once scene is ready
+ root.sceneProperty().addListener((obs, oldScene, newScene) -> {
+ if (newScene != null) {
+ applyDarkMode(darkModeToggle.isSelected());
+ }
+ });
+
+ // Toggle listener for user changes
+ darkModeToggle.selectedProperty().addListener((obs, oldVal, newVal) -> {
+ applyDarkMode(newVal);
+ });
+
+ // Key handler for sending messages
+ inputField.setOnKeyPressed(event -> {
+ if (event.getCode() == KeyCode.ENTER && !event.isShiftDown()) {
+ event.consume();
+ handleSend();
+ }
+ });
+ }
+
+ // Method to apply dark or light mode
+ private void applyDarkMode(boolean dark) {
+ Scene scene = root.getScene();
+ if (scene == null) return;
+
+ scene.getStylesheets().clear();
+
+ // Always add base
+ scene.getStylesheets().add(getClass()
+ .getResource("/net/metaunix/openwebfx/style.css")
+ .toExternalForm());
+
+ // Then theme
+ if (dark) {
+ scene.getStylesheets().add(getClass()
+ .getResource("/net/metaunix/openwebfx/style-dark.css")
+ .toExternalForm());
+ } else {
+ scene.getStylesheets().add(getClass()
+ .getResource("/net/metaunix/openwebfx/style-light.css")
+ .toExternalForm());
+ }
+ }
+
+ private void addMessage(String text, boolean isUser) {
+ Label label = new Label(text);
+ label.setWrapText(true);
+ label.setMaxWidth(500);
+
+ VBox bubble = new VBox(label);
+ bubble.setPadding(new Insets(10));
+ bubble.setStyle(
+ isUser
+ ? "-fx-background-color: #2563eb; -fx-background-radius: 15;"
+ : "-fx-background-color: #2d2d2d; -fx-background-radius: 15;"
+ );
+
+ label.setStyle("-fx-text-fill: white;");
+
+ HBox wrapper = new HBox(bubble);
+ wrapper.setPadding(new Insets(5));
+
+ if (isUser) {
+ wrapper.setAlignment(Pos.CENTER_RIGHT);
+ } else {
+ wrapper.setAlignment(Pos.CENTER_LEFT);
+ }
+
+ chatContainer.getChildren().add(wrapper);
+
+ Platform.runLater(() ->
+ chatScrollPane.setVvalue(1.0)
+ );
+ }
+
+ @FXML
+ private void handleSend() {
+
+ String userMessage = inputField.getText().trim();
+ if (userMessage.isEmpty()) {
+ return;
+ }
+
+ // Add user bubble
+ addMessage(userMessage, true);
+
+ // Clear input + disable send while processing
+ inputField.clear();
+ sendButton.setDisable(true);
+
+ executor.submit(() -> {
+ try {
+
+ String response = service.sendMessage(userMessage);
+
+ Platform.runLater(() -> {
+ addMessage(response, false);
+ sendButton.setDisable(false);
+ inputField.requestFocus();
+ });
+
+ } catch (Exception e) {
+
+ Platform.runLater(() -> {
+ addMessage("⚠ Error: " + e.getMessage(), false);
+ sendButton.setDisable(false);
+ });
+ }
+ });
+ }
+}
diff --git a/src/main/java/net/metaunix/openwebfx/model/ChatModel.java b/src/main/java/net/metaunix/openwebfx/model/ChatModel.java
new file mode 100644
index 0000000..ecb023d
--- /dev/null
+++ b/src/main/java/net/metaunix/openwebfx/model/ChatModel.java
@@ -0,0 +1,17 @@
+package net.metaunix.openwebfx.model;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+public class ChatModel {
+
+ private final ObservableList messages = FXCollections.observableArrayList();
+
+ public ObservableList getMessages() {
+ return messages;
+ }
+
+ public void addMessage(String message) {
+ messages.add(message);
+ }
+}
diff --git a/src/main/java/net/metaunix/openwebfx/service/OpenWebUIService.java b/src/main/java/net/metaunix/openwebfx/service/OpenWebUIService.java
new file mode 100644
index 0000000..445c549
--- /dev/null
+++ b/src/main/java/net/metaunix/openwebfx/service/OpenWebUIService.java
@@ -0,0 +1,51 @@
+package net.metaunix.openwebfx.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+public class OpenWebUIService {
+
+ private final String baseUrl;
+ private final String apiKey;
+ private final HttpClient client = HttpClient.newHttpClient();
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public OpenWebUIService(String baseUrl, String apiKey) {
+ this.baseUrl = baseUrl;
+ this.apiKey = apiKey;
+ }
+
+ public String sendMessage(String message) throws Exception {
+
+ String requestBody = """
+ {
+ "model": "gpt-4o-mini",
+ "messages": [
+ {"role": "user", "content": "%s"}
+ ]
+ }
+ """.formatted(message.replace("\"", "\\\""));
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + "/v1/chat/completions"))
+ .header("Content-Type", "application/json")
+ .header("Authorization", "Bearer " + apiKey)
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ HttpResponse response =
+ client.send(request, HttpResponse.BodyHandlers.ofString());
+
+ JsonNode root = mapper.readTree(response.body());
+ return root.get("choices")
+ .get(0)
+ .get("message")
+ .get("content")
+ .asText();
+ }
+}
diff --git a/src/main/resources/net/metaunix/openwebfx/main-view.fxml b/src/main/resources/net/metaunix/openwebfx/main-view.fxml
new file mode 100644
index 0000000..41b2614
--- /dev/null
+++ b/src/main/resources/net/metaunix/openwebfx/main-view.fxml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/net/metaunix/openwebfx/style-dark.css b/src/main/resources/net/metaunix/openwebfx/style-dark.css
new file mode 100644
index 0000000..3a2385b
--- /dev/null
+++ b/src/main/resources/net/metaunix/openwebfx/style-dark.css
@@ -0,0 +1,20 @@
+/* Backgrounds */
+.root, #chatContainer, #conversationListContainer, #bottomControls {
+ -fx-background-color: #1e1e1e;
+}
+
+.text-area, #chatListView, #conversationList {
+ -fx-background-color: #2b2b2b;
+ -fx-text-fill: white;
+}
+
+/* Title color */
+.app-title {
+ -fx-text-fill: white;
+}
+
+/* Send button - green */
+.send-button {
+ -fx-background-color: #10b981; /* same green as light mode */
+ -fx-text-fill: white;
+}
\ No newline at end of file
diff --git a/src/main/resources/net/metaunix/openwebfx/style-light.css b/src/main/resources/net/metaunix/openwebfx/style-light.css
new file mode 100644
index 0000000..afaee96
--- /dev/null
+++ b/src/main/resources/net/metaunix/openwebfx/style-light.css
@@ -0,0 +1,20 @@
+/* Backgrounds */
+.root, #chatContainer, #conversationListContainer, #bottomControls {
+ -fx-background-color: #f5f5f5;
+}
+
+.text-area, #chatListView, #conversationList {
+ -fx-background-color: #ffffff;
+ -fx-text-fill: black;
+}
+
+/* Title color */
+.app-title {
+ -fx-text-fill: black;
+}
+
+/* Send button - green */
+.send-button {
+ -fx-background-color: #10b981; /* nice green */
+ -fx-text-fill: white;
+}
\ No newline at end of file
diff --git a/src/main/resources/net/metaunix/openwebfx/style.css b/src/main/resources/net/metaunix/openwebfx/style.css
new file mode 100644
index 0000000..afaee96
--- /dev/null
+++ b/src/main/resources/net/metaunix/openwebfx/style.css
@@ -0,0 +1,20 @@
+/* Backgrounds */
+.root, #chatContainer, #conversationListContainer, #bottomControls {
+ -fx-background-color: #f5f5f5;
+}
+
+.text-area, #chatListView, #conversationList {
+ -fx-background-color: #ffffff;
+ -fx-text-fill: black;
+}
+
+/* Title color */
+.app-title {
+ -fx-text-fill: black;
+}
+
+/* Send button - green */
+.send-button {
+ -fx-background-color: #10b981; /* nice green */
+ -fx-text-fill: white;
+}
\ No newline at end of file