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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + +