diff --git a/pom.xml b/pom.xml index 5c1cef2..6c6c0af 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ eu.m724 chatapi - 1.0 + 1.1.1 diff --git a/src/main/java/eu/m724/chaqt/ChatWindowViewModel.java b/src/main/java/eu/m724/chaqt/ChatWindowViewModel.java deleted file mode 100644 index a217c5c..0000000 --- a/src/main/java/eu/m724/chaqt/ChatWindowViewModel.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.m724.chaqt; - -import eu.m724.chatapi.chat.Chat; - -public class ChatWindowViewModel { - private ChatWindowWidget widget; - - private Chat chat; - - public ChatWindowViewModel(ChatWindowWidget widget, Chat chat) { - this.widget = widget; - this.chat = chat; - } -} diff --git a/src/main/java/eu/m724/chaqt/ChatWindowWidget.java b/src/main/java/eu/m724/chaqt/ChatWindowWidget.java deleted file mode 100644 index 9178950..0000000 --- a/src/main/java/eu/m724/chaqt/ChatWindowWidget.java +++ /dev/null @@ -1,62 +0,0 @@ -package eu.m724.chaqt; - -import eu.m724.chatapi.chat.Chat; -import io.qt.core.Qt; -import io.qt.widgets.*; - -public class ChatWindowWidget extends QWidget { - private ChatWindowViewModel viewModel; - - private QVBoxLayout layout; - private QTextEdit textEdit; - - public ChatWindowWidget(Chat chat) { - this.viewModel = new ChatWindowViewModel(this, chat); - - // - - this.layout = new QVBoxLayout(); - layout.setAlignment(Qt.AlignmentFlag.AlignBottom); // or top? - - QWidget widget = new QWidget(); - widget.setLayout(layout); - - QScrollArea scrollArea = new QScrollArea(); - scrollArea.setWidgetResizable(true); - scrollArea.setWidget(widget); - - // - - this.textEdit = new QTextEdit(); - textEdit.setPlaceholderText("Write a message..."); - textEdit.textChanged.connect(this, "handleComposerType()"); - - QPushButton sendButton = new QPushButton("Send"); - - QHBoxLayout composerLayout = new QHBoxLayout(); - composerLayout.addWidget(textEdit); - composerLayout.addWidget(sendButton, 0, Qt.AlignmentFlag.AlignBottom); - - QWidget composerWidget = new QWidget(); - composerWidget.setLayout(composerLayout); - - handleComposerType(); // TODO make it resizable and fix it - - // - - QVBoxLayout mainLayout = new QVBoxLayout(); - mainLayout.addWidget(scrollArea, 1); - mainLayout.addWidget(composerWidget); - setLayout(mainLayout); - - for (int i=0; i<10; i++) { - QPushButton pushButton = new QPushButton("hello"); - layout.addWidget(pushButton); - } - } - - public void handleComposerType() { - int height = (int) textEdit.document().size().height(); - textEdit.setFixedHeight(height + textEdit.contentsMargins().top() + textEdit.contentsMargins().bottom()); - } -} diff --git a/src/main/java/eu/m724/chaqt/Main.java b/src/main/java/eu/m724/chaqt/Main.java index 6d9f28e..796f4bf 100644 --- a/src/main/java/eu/m724/chaqt/Main.java +++ b/src/main/java/eu/m724/chaqt/Main.java @@ -1,8 +1,8 @@ package eu.m724.chaqt; +import eu.m724.chaqt.chatwindow.ChatWindowWidget; import eu.m724.chatapi.chat.Chat; import io.qt.widgets.QApplication; -import io.qt.widgets.QMessageBox; public class Main { @@ -11,9 +11,10 @@ public class Main { Chat chat = new Chat(); - ChatWindowWidget widget = new ChatWindowWidget(chat); + ChatWindowWidget widget = new ChatWindowWidget(); widget.show(); QApplication.exec(); + QApplication.shutdown(); } } \ No newline at end of file diff --git a/src/main/java/eu/m724/chaqt/ObservableChat.java b/src/main/java/eu/m724/chaqt/ObservableChat.java new file mode 100644 index 0000000..d760ad0 --- /dev/null +++ b/src/main/java/eu/m724/chaqt/ObservableChat.java @@ -0,0 +1,22 @@ +package eu.m724.chaqt; + +import eu.m724.chatapi.chat.Chat; +import eu.m724.chatapi.chat.ChatMessage; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +public class ObservableChat extends Chat { + private Set> watchers = new HashSet<>(); + + @Override + public void addMessage(ChatMessage message) { + super.addMessage(message); + watchers.forEach(c -> c.accept(message)); + } + + public void watch(Consumer callable) { + this.watchers.add(callable); + } +} diff --git a/src/main/java/eu/m724/chaqt/QMessage.java b/src/main/java/eu/m724/chaqt/QMessage.java new file mode 100644 index 0000000..4ddcdaa --- /dev/null +++ b/src/main/java/eu/m724/chaqt/QMessage.java @@ -0,0 +1,30 @@ +package eu.m724.chaqt; + +import io.qt.core.QMargins; +import io.qt.gui.QPalette; +import io.qt.widgets.QLabel; +import io.qt.widgets.QVBoxLayout; +import io.qt.widgets.QWidget; + +// TODO I think we could put Qtextdocument here +public class QMessage extends QWidget { + private final QLabel label; + + public QMessage(boolean assistant, String text) { + this.label = new QLabel(text); + label.setWordWrap(true); + + QVBoxLayout layout = new QVBoxLayout(); + layout.addWidget(label); + + if (assistant) { + layout.setContentsMargins(5,0,0, 0); + } + + setLayout(layout); + } + + public void addText(String text) { + label.setText(label.text() + text); + } +} diff --git a/src/main/java/eu/m724/chaqt/chatwindow/ChatWindowViewModel.java b/src/main/java/eu/m724/chaqt/chatwindow/ChatWindowViewModel.java new file mode 100644 index 0000000..690b7be --- /dev/null +++ b/src/main/java/eu/m724/chaqt/chatwindow/ChatWindowViewModel.java @@ -0,0 +1,69 @@ +package eu.m724.chaqt.chatwindow; + +import eu.m724.chatapi.chat.Chat; +import eu.m724.chatapi.chat.ChatEvent; +import eu.m724.chatapi.chat.ChatMessage; +import eu.m724.chatapi.example.ExampleSource; +import eu.m724.chatapi.source.ChatResponse; +import eu.m724.chatapi.source.ChatSource; +import io.qt.core.QObject.Signal1; +import io.qt.core.QThread; + +public class ChatWindowViewModel { + private final ChatWindowWidget widget; + private final Signal1 chatMessageSignal; + private final Signal1 chatEventSignal; + + private Chat chat = new Chat(); + private ChatSource source = new ExampleSource(); // TODO + + + public ChatWindowViewModel(ChatWindowWidget widget, Signal1 chatEventSignal, Signal1 chatMessageSignal) { + this.widget = widget; + this.chatEventSignal = chatEventSignal; + this.chatMessageSignal = chatMessageSignal; + } + + + void send(String text) { + ChatMessage promptMessage = new ChatMessage(false, text); + chatMessageSignal.emit(promptMessage); + chat.addMessage(new ChatMessage(false, text)); + + ChatResponse response = source.ask(chat); + ChatMessage message = response.message(); + + chatMessageSignal.emit(message); + message.addEventConsumer(chatEventSignal::emit, true); + + /*QThread thread = new QThread() { + @Override + protected void run() { + ChatEvent token; + do { + try { + token = response.eventQueue().take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); // TODO + } + + chatEventSignal.emit(token); + + if (token.finishReason() != null) { + chatMessageSignal.emit(response.message().join()); // TODO ??? + } + /*if (!"error".equals(token.finishReason())) { + if (token.text() != null) { + chatEventSignal.emit(token.text()); + } + } else { + System.out.print("Error: " + token.error().toString()); + }/ + } while (token.finishReason() == null); + // QApplication.invokeLater(() -> resultField.setText(result)); + } + }; + thread.start();*/ + // + } +} diff --git a/src/main/java/eu/m724/chaqt/chatwindow/ChatWindowWidget.java b/src/main/java/eu/m724/chaqt/chatwindow/ChatWindowWidget.java new file mode 100644 index 0000000..d28f467 --- /dev/null +++ b/src/main/java/eu/m724/chaqt/chatwindow/ChatWindowWidget.java @@ -0,0 +1,133 @@ +package eu.m724.chaqt.chatwindow; + +import eu.m724.chaqt.QMessage; +import eu.m724.chatapi.chat.ChatEvent; +import eu.m724.chatapi.chat.ChatMessage; +import io.qt.core.Qt; +import io.qt.widgets.*; + +public class ChatWindowWidget extends QWidget { + private ChatWindowViewModel viewModel; + + private QVBoxLayout layout; + private QTextEdit textEdit; + + // TODO are both really needed + private final Signal1 chatMessageSignal = new Signal1<>(); + private final Signal1 chatEventSignal = new Signal1<>(); + + private QMessage latestMessage; + + public ChatWindowWidget() { + this.viewModel = new ChatWindowViewModel(this, chatEventSignal, chatMessageSignal); + + // + + chatEventSignal.connect(this::handleChatEvent); + chatMessageSignal.connect(this::handleChatMessage); + + // + + this.layout = new QVBoxLayout(); + layout.setAlignment(Qt.AlignmentFlag.AlignBottom); // or top? + + QWidget widget = new QWidget(); + widget.setLayout(layout); + + QScrollArea scrollArea = new QScrollArea(); + scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff); + scrollArea.setWidgetResizable(true); + scrollArea.setWidget(widget); + + // + + this.textEdit = new QTextEdit(); + textEdit.setPlaceholderText("Write a message..."); + textEdit.textChanged.connect(this, "handleComposerType()"); + + QPushButton sendButton = new QPushButton("Send"); + sendButton.clicked.connect(this::onSend); + + QHBoxLayout composerLayout = new QHBoxLayout(); + composerLayout.addWidget(textEdit); + composerLayout.addWidget(sendButton, 0, Qt.AlignmentFlag.AlignBottom); + + QWidget composerWidget = new QWidget(); + composerWidget.setLayout(composerLayout); + + handleComposerType(); // TODO make it resizable and fix it + + // + + QVBoxLayout mainLayout = new QVBoxLayout(); + mainLayout.addWidget(scrollArea, 1); + mainLayout.addWidget(composerWidget); + setLayout(mainLayout); + + /*for (int i=0; i<10; i++) { + QPushButton pushButton = new QPushButton("hello"); + layout.addWidget(pushButton); + }*/ + + textEdit.setFocus(); + } + + public void handleComposerType() { + int height = (int) textEdit.document().size().height(); + textEdit.setFixedHeight(height + textEdit.contentsMargins().top() + textEdit.contentsMargins().bottom()); + } + + public void onSend() { + String prompt = textEdit.getPlainText(); + if (!prompt.isBlank()) { + textEdit.setText(""); + viewModel.send(prompt); + + textEdit.setEnabled(false); + textEdit.setPlaceholderText("Asking..."); + } + } + + private void handleChatMessage(ChatMessage message) { + System.out.printf("Widget received a message. Is response: %b\n", message.response()); + + QMessage qMessage = new QMessage(false, message.content()); + layout.addWidget(qMessage); + + // if the message is by assistant, disable composer. + // I don't know if this is good, but it works + if (message.response()) { + textEdit.setEnabled(false); + textEdit.setPlaceholderText("Thinking..."); + } + } + + private void handleChatEvent(ChatEvent chatEvent) { + String token = chatEvent.text(); + + // usually when it's the first token + if (latestMessage == null) { + latestMessage = new QMessage(true, ""); + layout.addWidget(latestMessage); + + textEdit.setPlaceholderText("Responding..."); + } + + if (token != null) { + latestMessage.addText(token); + } + + // when finished + if (chatEvent.finishReason() != null) { + // to make sure we won't be streaming to the same message + latestMessage = null; + + // enable composer + textEdit.setEnabled(true); + textEdit.setPlaceholderText("Write a message..."); + textEdit.setFocus(); + } + + System.out.printf("Event: %s %s\n", chatEvent.finishReason(), chatEvent.text()); + } // TODO that's messy so do something about that +}