2025年4月6日14:39:48
parent
5c86ff4c32
commit
5fcb78176e
|
|
@ -5,6 +5,7 @@ import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextAlignment;
|
||||||
import javafx.scene.text.TextFlow;
|
import javafx.scene.text.TextFlow;
|
||||||
import com.aiclient.service.ChatService;
|
import com.aiclient.service.ChatService;
|
||||||
import com.aiclient.service.MarkdownService;
|
import com.aiclient.service.MarkdownService;
|
||||||
|
|
@ -25,6 +26,9 @@ import javafx.scene.control.ScrollPane;
|
||||||
|
|
||||||
public class MainController {
|
public class MainController {
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public ScrollPane chatScrollPane;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ListView<Chat> chatListView;
|
private ListView<Chat> chatListView;
|
||||||
|
|
||||||
|
|
@ -58,7 +62,7 @@ public class MainController {
|
||||||
// 初始化聊天列表
|
// 初始化聊天列表
|
||||||
chatListView.setCellFactory(param -> new ChatListCell());
|
chatListView.setCellFactory(param -> new ChatListCell());
|
||||||
chatListView.getSelectionModel().selectedItemProperty().addListener(
|
chatListView.getSelectionModel().selectedItemProperty().addListener(
|
||||||
(observable, oldValue, newValue) -> loadChat(newValue)
|
(observable, oldValue, newValue) -> loadChat(newValue)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 创建初始会话
|
// 创建初始会话
|
||||||
|
|
@ -66,12 +70,18 @@ public class MainController {
|
||||||
|
|
||||||
// 配置快捷键
|
// 配置快捷键
|
||||||
setupShortcuts();
|
setupShortcuts();
|
||||||
|
|
||||||
|
chatContainer.heightProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (chatScrollPane.getVmax() > 0) {
|
||||||
|
chatScrollPane.setVvalue(1.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupShortcuts() {
|
private void setupShortcuts() {
|
||||||
// 发送消息 (Ctrl/Cmd + Enter)
|
// 发送消息 (Ctrl/Cmd + Enter)
|
||||||
KeyCombination sendCombination = new KeyCodeCombination(KeyCode.ENTER,
|
KeyCombination sendCombination = new KeyCodeCombination(KeyCode.ENTER,
|
||||||
KeyCombination.SHORTCUT_DOWN);
|
KeyCombination.SHORTCUT_DOWN);
|
||||||
inputArea.setOnKeyPressed(event -> {
|
inputArea.setOnKeyPressed(event -> {
|
||||||
if (sendCombination.match(event)) {
|
if (sendCombination.match(event)) {
|
||||||
event.consume();
|
event.consume();
|
||||||
|
|
@ -81,26 +91,26 @@ public class MainController {
|
||||||
|
|
||||||
// 新建会话 (Ctrl/Cmd + N)
|
// 新建会话 (Ctrl/Cmd + N)
|
||||||
shortcutService.addShortcut(
|
shortcutService.addShortcut(
|
||||||
new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN),
|
new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN),
|
||||||
this::createNewChat
|
this::createNewChat
|
||||||
);
|
);
|
||||||
|
|
||||||
// 切换主题 (Ctrl/Cmd + T)
|
// 切换主题 (Ctrl/Cmd + T)
|
||||||
shortcutService.addShortcut(
|
shortcutService.addShortcut(
|
||||||
new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN),
|
new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN),
|
||||||
this::toggleTheme
|
this::toggleTheme
|
||||||
);
|
);
|
||||||
|
|
||||||
// 清空当前会话 (Ctrl/Cmd + L)
|
// 清空当前会话 (Ctrl/Cmd + L)
|
||||||
shortcutService.addShortcut(
|
shortcutService.addShortcut(
|
||||||
new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN),
|
new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN),
|
||||||
this::clearCurrentChat
|
this::clearCurrentChat
|
||||||
);
|
);
|
||||||
|
|
||||||
// 复制选中文本 (Ctrl/Cmd + C)
|
// 复制选中文本 (Ctrl/Cmd + C)
|
||||||
shortcutService.addShortcut(
|
shortcutService.addShortcut(
|
||||||
new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN),
|
new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN),
|
||||||
this::copySelectedText
|
this::copySelectedText
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,79 +141,80 @@ public class MainController {
|
||||||
Message assistantMessage = new Message("assistant", "");
|
Message assistantMessage = new Message("assistant", "");
|
||||||
currentChat.addMessage(assistantMessage);
|
currentChat.addMessage(assistantMessage);
|
||||||
HBox messageBox = addMessageToChat(assistantMessage);
|
HBox messageBox = addMessageToChat(assistantMessage);
|
||||||
TextArea textArea = ((TextArea) ((VBox) messageBox.getChildren().get(0)).getChildren().get(0));
|
TextFlow textFlow = (TextFlow) messageBox.getChildren().get(0);
|
||||||
|
|
||||||
StringBuilder contentBuilder = new StringBuilder();
|
StringBuilder contentBuilder = new StringBuilder();
|
||||||
|
|
||||||
// 发送到服务器并获取流式响应
|
// 发送到服务器并获取流式响应
|
||||||
chatService.sendMessageStream(
|
chatService.sendMessageStream(
|
||||||
messageText,
|
messageText,
|
||||||
modelSelector.getValue(),
|
modelSelector.getValue(),
|
||||||
chunk -> {
|
chunk -> {
|
||||||
// 在JavaFX线程中更新UI
|
// 在JavaFX线程中更新UI
|
||||||
contentBuilder.append(chunk);
|
contentBuilder.append(chunk);
|
||||||
javafx.application.Platform.runLater(() -> {
|
javafx.application.Platform.runLater(() -> {
|
||||||
assistantMessage.setContent(contentBuilder.toString());
|
assistantMessage.setContent(contentBuilder.toString());
|
||||||
updateMessage(textArea, contentBuilder.toString() );
|
updateMessage(textFlow, chunk);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
() -> {
|
() -> {
|
||||||
// 完成后在JavaFX线程中更新UI
|
// 完成后在JavaFX线程中更新UI
|
||||||
javafx.application.Platform.runLater(() -> {
|
javafx.application.Platform.runLater(() -> {
|
||||||
inputArea.setDisable(false);
|
inputArea.setDisable(false);
|
||||||
inputArea.requestFocus();
|
inputArea.requestFocus();
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
currentChat
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HBox addMessageToChat(Message message) {
|
private HBox addMessageToChat(Message message) {
|
||||||
|
|
||||||
TextFlow textFlow = new TextFlow();
|
TextFlow textFlow = new TextFlow();
|
||||||
return addMessageToChat(message, textFlow);
|
textFlow.prefWidthProperty().bind(chatContainer.widthProperty());
|
||||||
}
|
|
||||||
|
|
||||||
private HBox addMessageToChat(Message message, TextFlow textFlow) {
|
textFlow.setLineSpacing(15);
|
||||||
// 创建一个VBox来包含消息内容
|
|
||||||
VBox messageContent = new VBox(5); // 5是内部间距
|
|
||||||
messageContent.setMaxWidth(800); // 限制最大宽度
|
|
||||||
|
|
||||||
// 创建文本显示区域
|
Text text = new Text(message.getContent());
|
||||||
TextArea textArea = new TextArea(message.getContent());
|
textFlow.getChildren().add(text);
|
||||||
textArea.setWrapText(true);
|
|
||||||
textArea.setEditable(false);
|
|
||||||
textArea.setPrefRowCount(1); // 初始显示1行
|
|
||||||
textArea.setStyle(
|
|
||||||
"-fx-background-color: transparent;" +
|
|
||||||
"-fx-text-fill: " + (themeService.isDarkTheme() ? "white" : "black") + ";" +
|
|
||||||
"-fx-focus-color: transparent;" +
|
|
||||||
"-fx-faint-focus-color: transparent;"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 自动调整高度
|
|
||||||
textArea.textProperty().addListener((obs, old, newText) -> {
|
|
||||||
int lineCount = (int) newText.lines().count();
|
|
||||||
textArea.setPrefRowCount(Math.max(1, lineCount));
|
|
||||||
});
|
|
||||||
|
|
||||||
messageContent.getChildren().add(textArea);
|
|
||||||
|
|
||||||
// 设置消息样式
|
// 设置消息样式
|
||||||
HBox messageBox = new HBox(messageContent);
|
HBox messageBox = new HBox(textFlow);
|
||||||
|
messageBox.prefWidthProperty().bind(chatContainer.widthProperty());
|
||||||
messageBox.getStyleClass().add(message.getRole() + "-message");
|
messageBox.getStyleClass().add(message.getRole() + "-message");
|
||||||
messageBox.setPadding(new javafx.geometry.Insets(10));
|
messageBox.setPadding(new javafx.geometry.Insets(10));
|
||||||
|
|
||||||
if ("user".equals(message.getRole())) {
|
if ("user".equals(message.getRole())) {
|
||||||
|
textFlow.setTextAlignment(TextAlignment.RIGHT);
|
||||||
messageBox.setAlignment(Pos.CENTER_RIGHT);
|
messageBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
} else {
|
} else {
|
||||||
messageBox.setAlignment(Pos.CENTER_LEFT);
|
messageBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
textFlow.setTextAlignment(TextAlignment.LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
chatContainer.getChildren().add(messageBox);
|
chatContainer.getChildren().add(messageBox);
|
||||||
return messageBox;
|
return messageBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMessage(TextArea textArea, String content) {
|
private void updateMessage(TextFlow textFlow, String content) {
|
||||||
textArea.setText(content);
|
|
||||||
|
if (content.contains("<think>")) {
|
||||||
|
content = content.split("</think>")[1];
|
||||||
|
if (content.equals("")){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.contains("</think>")) {
|
||||||
|
content = content.split("</think>")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Text text = new Text(content);
|
||||||
|
Text spacer = new Text("\r\n");
|
||||||
|
textFlow.getChildren().add(text);
|
||||||
|
textFlow.getChildren().add(spacer);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadChat(Chat chat) {
|
private void loadChat(Chat chat) {
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,11 @@ public class ChatService {
|
||||||
return new Chat();
|
return new Chat();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendMessageStream(String message, String model, Consumer<String> onChunk, Runnable onComplete) {
|
public void sendMessageStream(String message, String model, Consumer<String> onChunk, Runnable onComplete,Chat currentChat) {
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
String encodedMessage = URLEncoder.encode(message, StandardCharsets.UTF_8);
|
String encodedMessage = URLEncoder.encode(message, StandardCharsets.UTF_8);
|
||||||
String url = String.format("%s?userId=user&input=%s&stream=true", BASE_URL, encodedMessage);
|
String url = String.format("%s?input=%s&userId=%s", BASE_URL, encodedMessage,currentChat.getId());
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(url))
|
.uri(URI.create(url))
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,6 @@
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-message, .assistant-message {
|
|
||||||
-fx-background-radius: 5;
|
|
||||||
-fx-max-width: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-message {
|
.user-message {
|
||||||
-fx-background-color: #007AFF22;
|
-fx-background-color: #007AFF22;
|
||||||
}
|
}
|
||||||
|
|
@ -72,14 +67,7 @@
|
||||||
-fx-line-spacing: 5;
|
-fx-line-spacing: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动条样式 */
|
|
||||||
.scroll-bar:vertical {
|
|
||||||
-fx-pref-width: 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-bar:horizontal {
|
|
||||||
-fx-pref-height: 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-bar > .thumb {
|
.scroll-bar > .thumb {
|
||||||
-fx-background-radius: 6;
|
-fx-background-radius: 6;
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
</HBox>
|
</HBox>
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
|
|
||||||
<ScrollPane VBox.vgrow="ALWAYS" fitToWidth="true" styleClass="chat-scroll-pane">
|
<ScrollPane fx:id="chatScrollPane" VBox.vgrow="ALWAYS" fitToWidth="true" styleClass="chat-scroll-pane">
|
||||||
<VBox fx:id="chatContainer" spacing="10"/>
|
<VBox fx:id="chatContainer" spacing="10"/>
|
||||||
</ScrollPane>
|
</ScrollPane>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue