First session of vibe coding the UI
This commit is contained in:
26
src/main/java/net/metaunix/openwebfx/App.java
Normal file
26
src/main/java/net/metaunix/openwebfx/App.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
17
src/main/java/net/metaunix/openwebfx/model/ChatModel.java
Normal file
17
src/main/java/net/metaunix/openwebfx/model/ChatModel.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
77
src/main/resources/net/metaunix/openwebfx/main-view.fxml
Normal file
77
src/main/resources/net/metaunix/openwebfx/main-view.fxml
Normal 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>
|
||||
20
src/main/resources/net/metaunix/openwebfx/style-dark.css
Normal file
20
src/main/resources/net/metaunix/openwebfx/style-dark.css
Normal 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;
|
||||
}
|
||||
20
src/main/resources/net/metaunix/openwebfx/style-light.css
Normal file
20
src/main/resources/net/metaunix/openwebfx/style-light.css
Normal 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;
|
||||
}
|
||||
20
src/main/resources/net/metaunix/openwebfx/style.css
Normal file
20
src/main/resources/net/metaunix/openwebfx/style.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user