First session of vibe coding the UI
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -24,3 +24,6 @@
|
|||||||
hs_err_pid*
|
hs_err_pid*
|
||||||
replay_pid*
|
replay_pid*
|
||||||
|
|
||||||
|
# ignore compiled files
|
||||||
|
target/
|
||||||
|
|
||||||
|
|||||||
53
pom.xml
Normal file
53
pom.xml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>net.metaunix</groupId>
|
||||||
|
<artifactId>openwebfx</artifactId>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<javafx.version>21.0.2</javafx.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- JavaFX -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-controls</artifactId>
|
||||||
|
<version>${javafx.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-fxml</artifactId>
|
||||||
|
<version>${javafx.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JSON -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.17.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-maven-plugin</artifactId>
|
||||||
|
<version>0.0.8</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>net.metaunix.openwebfx.App</mainClass>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
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