First session of vibe coding the UI

This commit is contained in:
2026-02-20 00:43:39 -05:00
parent 212581ac77
commit 06b97ad2f4
10 changed files with 437 additions and 0 deletions

View File

@@ -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();
}
}

View File

@@ -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);
});
}
});
}
}

View File

@@ -0,0 +1,17 @@
package net.metaunix.openwebfx.model;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class ChatModel {
private final ObservableList<String> messages = FXCollections.observableArrayList();
public ObservableList<String> getMessages() {
return messages;
}
public void addMessage(String message) {
messages.add(message);
}
}

View File

@@ -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<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
JsonNode root = mapper.readTree(response.body());
return root.get("choices")
.get(0)
.get("message")
.get("content")
.asText();
}
}

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane fx:id="root"
xmlns:fx="http://javafx.com/fxml"
fx:controller="net.metaunix.openwebfx.controller.MainController"
styleClass="root">
<!-- LEFT SIDEBAR -->
<left>
<VBox prefWidth="220" styleClass="sidebar">
<Label text="Conversations" styleClass="sidebar-title"/>
<ListView fx:id="conversationList"
VBox.vgrow="ALWAYS"/>
</VBox>
</left>
<!-- TOP BAR -->
<top>
<HBox alignment="CENTER_LEFT" spacing="10" styleClass="topbar">
<padding>
<Insets top="10" right="15" bottom="10" left="15"/>
</padding>
<Label text="OpenWebFX" styleClass="app-title"/>
<Region HBox.hgrow="ALWAYS"/>
<ComboBox fx:id="modelSelector"
prefWidth="180"/>
<!-- Dark/Light Mode Switch -->
<CheckBox fx:id="darkModeToggle" text="Dark Mode" selected="false"/>
</HBox>
</top>
<!-- CHAT AREA -->
<center>
<ScrollPane fx:id="chatScrollPane"
fitToWidth="true"
styleClass="chat-scroll">
<VBox fx:id="chatContainer"
spacing="15"
styleClass="chat-container">
<padding>
<Insets top="20" right="20" bottom="20" left="20"/>
</padding>
</VBox>
</ScrollPane>
</center>
<!-- INPUT AREA -->
<bottom>
<HBox fx:id="bottomControls"
spacing="10" styleClass="input-area">
<padding>
<Insets top="10" right="20" bottom="20" left="20"/>
</padding>
<HBox spacing="10" alignment="CENTER" HBox.hgrow="ALWAYS">
<TextArea fx:id="inputField"
prefRowCount="2"
wrapText="true"
HBox.hgrow="ALWAYS"
promptText="Send a message..."/>
<Button fx:id="sendButton" text="Send" HBox.hgrow="ALWAYS" maxWidth="Infinity" maxHeight="Infinity"/>
</HBox>
</HBox>
</bottom>
</BorderPane>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}