2025年4月3日18:13:53
commit
61ca638f92
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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>com.aiclient</groupId>
|
||||||
|
<artifactId>ai-chat-client</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<javafx.version>17.0.2</javafx.version>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- JavaFX -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-controls</artifactId>
|
||||||
|
<version>${javafx.version}</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-fxml</artifactId>
|
||||||
|
<version>${javafx.version}</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-web</artifactId>
|
||||||
|
<version>${javafx.version}</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-base</artifactId>
|
||||||
|
<version>${javafx.version}</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-graphics</artifactId>
|
||||||
|
<version>${javafx.version}</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JSON Processing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.15.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- SQLite for local storage -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xerial</groupId>
|
||||||
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
<version>3.42.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${maven.compiler.source}</source>
|
||||||
|
<target>${maven.compiler.target}</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-maven-plugin</artifactId>
|
||||||
|
<version>0.0.8</version>
|
||||||
|
<configuration>
|
||||||
|
<mainClass>com.aiclient.Main</mainClass>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<transformers>
|
||||||
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>com.aiclient.Main</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
# AI 聊天客户端 (类 Deepseek)
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
这是一个基于 JavaFX 开发的 AI 聊天客户端,通过 Ollama API 实现与多个 AI 模型的对话功能。本项目旨在提供一个简洁、易用、功能完善的 AI 聊天体验。
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
### 1. 对话管理
|
||||||
|
- 支持多轮对话,保持上下文连贯性
|
||||||
|
- 会话历史记录的保存与加载
|
||||||
|
- 支持创建多个独立会话
|
||||||
|
- 支持会话分组管理
|
||||||
|
|
||||||
|
### 2. AI 模型支持
|
||||||
|
- 选用ollama + spring ai,
|
||||||
|
- 显示模型响应状态和加载进度
|
||||||
|
|
||||||
|
### 3. 用户界面
|
||||||
|
- 现代化的界面设计,支持亮色/暗色主题
|
||||||
|
- 支持代码高亮显示
|
||||||
|
- Markdown 格式渲染
|
||||||
|
- 消息复制
|
||||||
|
- 快捷键支持
|
||||||
|
- 系统提示词(System Prompt)配置
|
||||||
|
|
||||||
|
## 快捷键
|
||||||
|
|
||||||
|
### 对话操作
|
||||||
|
- `Ctrl/Cmd + Enter`: 发送消息
|
||||||
|
- `Ctrl/Cmd + N`: 新建会话
|
||||||
|
- `Ctrl/Cmd + L`: 清空当前会话
|
||||||
|
- `Ctrl/Cmd + C`: 复制选中文本
|
||||||
|
|
||||||
|
### 界面操作
|
||||||
|
- `Ctrl/Cmd + T`: 切换主题(亮色/暗色)
|
||||||
|
|
||||||
|
注:在 macOS 上,`Ctrl` 键对应 `Command(⌘)` 键
|
||||||
|
|
||||||
|
## 技术架构
|
||||||
|
|
||||||
|
### 前端(JavaFX)
|
||||||
|
- 使用 JavaFX 17+ 构建用户界面
|
||||||
|
- 采用 MVVM 架构模式
|
||||||
|
- 使用 CSS 实现主题定制
|
||||||
|
- 使用 WebView 支持 Markdown 渲染
|
||||||
|
|
||||||
|
### 后端集成
|
||||||
|
- 集成 Ollama REST API
|
||||||
|
- 实现异步通信,避免界面卡顿
|
||||||
|
- 支持流式响应,实现打字机效果
|
||||||
|
- 本地数据持久化存储
|
||||||
|
|
||||||
|
## 开发计划
|
||||||
|
|
||||||
|
### 第一阶段(基础功能)
|
||||||
|
1. 搭建基础项目框架
|
||||||
|
2. 实现基本的对话界面
|
||||||
|
3. 完成 Ollama API 集成
|
||||||
|
4. 实现会话管理功能
|
||||||
|
|
||||||
|
### 第二阶段(功能完善)
|
||||||
|
1. 添加主题支持
|
||||||
|
2. 实现代码高亮
|
||||||
|
3. 添加多语言支持
|
||||||
|
4. 完善会话管理功能
|
||||||
|
|
||||||
|
|
||||||
|
## 使用指南
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- Java 17 或更高版本
|
||||||
|
- Ollama 服务已安装并运行
|
||||||
|
- 系统内存建议 8GB 以上
|
||||||
|
|
||||||
|
### 安装步骤
|
||||||
|
1. 下载最新版本安装包
|
||||||
|
2. 运行安装程序
|
||||||
|
3. 配置 Ollama 服务地址
|
||||||
|
4. 开始使用
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
1. 创建新会话
|
||||||
|
2. 选择 AI 模型
|
||||||
|
3. 输入问题并发送
|
||||||
|
4. 查看 AI 响应
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
- 请确保 Ollama 服务正常运行
|
||||||
|
- 建议定期备份重要对话
|
||||||
|
- 注意网络连接状态
|
||||||
|
|
||||||
|
## 反馈与支持
|
||||||
|
- 问题反馈请提交 Issue
|
||||||
|
- 功能建议欢迎提交 PR
|
||||||
|
- 邮件支持:[待添加]
|
||||||
|
|
||||||
|
## 开源协议
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.aiclient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author zm
|
||||||
|
* @date 2025/4/3 15:48
|
||||||
|
* @version: 1.0
|
||||||
|
*/
|
||||||
|
public class Application {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
javafx.application.Application.launch(Main.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.aiclient;
|
||||||
|
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import com.aiclient.controller.MainController;
|
||||||
|
|
||||||
|
public class Main extends Application {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Stage primaryStage) throws Exception {
|
||||||
|
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"));
|
||||||
|
Parent root = loader.load();
|
||||||
|
Scene scene = new Scene(root, 1200, 800);
|
||||||
|
|
||||||
|
// 设置控制器
|
||||||
|
MainController controller = loader.getController();
|
||||||
|
controller.setScene(scene);
|
||||||
|
|
||||||
|
primaryStage.setTitle("AI Chat Client");
|
||||||
|
primaryStage.setScene(scene);
|
||||||
|
primaryStage.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
launch(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.aiclient.controller;
|
||||||
|
|
||||||
|
import com.aiclient.model.Chat;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
|
||||||
|
public class ChatListCell extends ListCell<Chat> {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(Chat chat, boolean empty) {
|
||||||
|
super.updateItem(chat, empty);
|
||||||
|
|
||||||
|
if (empty || chat == null) {
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
setText(chat.getTitle());
|
||||||
|
getStyleClass().add("chat-list-cell");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,287 @@
|
||||||
|
package com.aiclient.controller;
|
||||||
|
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextFlow;
|
||||||
|
import com.aiclient.service.ChatService;
|
||||||
|
import com.aiclient.service.MarkdownService;
|
||||||
|
import com.aiclient.service.ThemeService;
|
||||||
|
import com.aiclient.model.Chat;
|
||||||
|
import com.aiclient.model.Message;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyCodeCombination;
|
||||||
|
import javafx.scene.input.KeyCombination;
|
||||||
|
import com.aiclient.service.ShortcutService;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
|
||||||
|
public class MainController {
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ListView<Chat> chatListView;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> modelSelector;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private VBox chatContainer;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextArea inputArea;
|
||||||
|
|
||||||
|
private ChatService chatService;
|
||||||
|
private MarkdownService markdownService;
|
||||||
|
private ThemeService themeService;
|
||||||
|
private Chat currentChat;
|
||||||
|
private ShortcutService shortcutService;
|
||||||
|
private Scene scene;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
chatService = new ChatService();
|
||||||
|
markdownService = new MarkdownService();
|
||||||
|
themeService = new ThemeService();
|
||||||
|
shortcutService = new ShortcutService();
|
||||||
|
|
||||||
|
// 初始化模型选择器
|
||||||
|
modelSelector.getItems().addAll("deepseek-coder:latest");
|
||||||
|
modelSelector.setValue("deepseek-coder:latest");
|
||||||
|
|
||||||
|
// 初始化聊天列表
|
||||||
|
chatListView.setCellFactory(param -> new ChatListCell());
|
||||||
|
chatListView.getSelectionModel().selectedItemProperty().addListener(
|
||||||
|
(observable, oldValue, newValue) -> loadChat(newValue)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建初始会话
|
||||||
|
createNewChat();
|
||||||
|
|
||||||
|
// 配置快捷键
|
||||||
|
setupShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupShortcuts() {
|
||||||
|
// 发送消息 (Ctrl/Cmd + Enter)
|
||||||
|
KeyCombination sendCombination = new KeyCodeCombination(KeyCode.ENTER,
|
||||||
|
KeyCombination.SHORTCUT_DOWN);
|
||||||
|
inputArea.setOnKeyPressed(event -> {
|
||||||
|
if (sendCombination.match(event)) {
|
||||||
|
event.consume();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 新建会话 (Ctrl/Cmd + N)
|
||||||
|
shortcutService.addShortcut(
|
||||||
|
new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN),
|
||||||
|
this::createNewChat
|
||||||
|
);
|
||||||
|
|
||||||
|
// 切换主题 (Ctrl/Cmd + T)
|
||||||
|
shortcutService.addShortcut(
|
||||||
|
new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN),
|
||||||
|
this::toggleTheme
|
||||||
|
);
|
||||||
|
|
||||||
|
// 清空当前会话 (Ctrl/Cmd + L)
|
||||||
|
shortcutService.addShortcut(
|
||||||
|
new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN),
|
||||||
|
this::clearCurrentChat
|
||||||
|
);
|
||||||
|
|
||||||
|
// 复制选中文本 (Ctrl/Cmd + C)
|
||||||
|
shortcutService.addShortcut(
|
||||||
|
new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN),
|
||||||
|
this::copySelectedText
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void createNewChat() {
|
||||||
|
Chat newChat = chatService.createNewChat();
|
||||||
|
chatListView.getItems().add(newChat);
|
||||||
|
chatListView.getSelectionModel().select(newChat);
|
||||||
|
currentChat = newChat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void sendMessage() {
|
||||||
|
String messageText = inputArea.getText();
|
||||||
|
if (messageText.trim().isEmpty()) return;
|
||||||
|
|
||||||
|
// 禁用输入框和发送按钮
|
||||||
|
inputArea.setDisable(true);
|
||||||
|
|
||||||
|
Message userMessage = new Message("user", messageText);
|
||||||
|
currentChat.addMessage(userMessage);
|
||||||
|
addMessageToChat(userMessage);
|
||||||
|
|
||||||
|
// 清空输入框
|
||||||
|
inputArea.clear();
|
||||||
|
|
||||||
|
// 创建助手消息
|
||||||
|
Message assistantMessage = new Message("assistant", "");
|
||||||
|
currentChat.addMessage(assistantMessage);
|
||||||
|
HBox messageBox = addMessageToChat(assistantMessage);
|
||||||
|
TextArea textArea = ((TextArea) ((VBox) messageBox.getChildren().get(0)).getChildren().get(0));
|
||||||
|
|
||||||
|
StringBuilder contentBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
// 发送到服务器并获取流式响应
|
||||||
|
chatService.sendMessageStream(
|
||||||
|
messageText,
|
||||||
|
modelSelector.getValue(),
|
||||||
|
chunk -> {
|
||||||
|
// 在JavaFX线程中更新UI
|
||||||
|
// contentBuilder.append(chunk);
|
||||||
|
javafx.application.Platform.runLater(() -> {
|
||||||
|
assistantMessage.setContent(contentBuilder.toString());
|
||||||
|
updateMessage(textArea, chunk);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() -> {
|
||||||
|
// 完成后在JavaFX线程中更新UI
|
||||||
|
javafx.application.Platform.runLater(() -> {
|
||||||
|
inputArea.setDisable(false);
|
||||||
|
inputArea.requestFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HBox addMessageToChat(Message message) {
|
||||||
|
TextFlow textFlow = new TextFlow();
|
||||||
|
return addMessageToChat(message, textFlow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HBox addMessageToChat(Message message, TextFlow textFlow) {
|
||||||
|
// 创建一个VBox来包含消息内容
|
||||||
|
VBox messageContent = new VBox(5); // 5是内部间距
|
||||||
|
messageContent.setMaxWidth(800); // 限制最大宽度
|
||||||
|
|
||||||
|
// 创建文本显示区域
|
||||||
|
TextArea textArea = new TextArea(message.getContent());
|
||||||
|
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);
|
||||||
|
messageBox.getStyleClass().add(message.getRole() + "-message");
|
||||||
|
messageBox.setPadding(new javafx.geometry.Insets(10));
|
||||||
|
|
||||||
|
if ("user".equals(message.getRole())) {
|
||||||
|
messageBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
} else {
|
||||||
|
messageBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
chatContainer.getChildren().add(messageBox);
|
||||||
|
return messageBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMessage(TextArea textArea, String content) {
|
||||||
|
if (!textArea.getText().isEmpty()){
|
||||||
|
textArea.appendText(content);
|
||||||
|
}else {
|
||||||
|
textArea.setText(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadChat(Chat chat) {
|
||||||
|
if (chat == null) return;
|
||||||
|
currentChat = chat;
|
||||||
|
chatContainer.getChildren().clear();
|
||||||
|
chat.getMessages().forEach(this::addMessageToChat);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void toggleTheme() {
|
||||||
|
if (themeService == null) return;
|
||||||
|
themeService.toggleTheme();
|
||||||
|
markdownService.setDarkTheme(themeService.isDarkTheme());
|
||||||
|
reloadMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadMessages() {
|
||||||
|
VBox oldContainer = chatContainer;
|
||||||
|
chatContainer = new VBox(10);
|
||||||
|
oldContainer.getChildren().clear();
|
||||||
|
loadChat(currentChat);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void clearCurrentChat() {
|
||||||
|
if (currentChat != null) {
|
||||||
|
currentChat.getMessages().clear();
|
||||||
|
chatContainer.getChildren().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copySelectedText() {
|
||||||
|
if (scene == null) return;
|
||||||
|
|
||||||
|
// 获取当前焦点节点
|
||||||
|
Node focusedNode = scene.getFocusOwner();
|
||||||
|
String selectedText = null;
|
||||||
|
|
||||||
|
// 根据不同的节点类型获取选中的文本
|
||||||
|
if (focusedNode instanceof TextArea) {
|
||||||
|
TextArea textArea = (TextArea) focusedNode;
|
||||||
|
selectedText = textArea.getSelectedText();
|
||||||
|
} else if (focusedNode instanceof ScrollPane) {
|
||||||
|
// 如果是消息容器,获取其中的文本内容
|
||||||
|
ScrollPane scrollPane = (ScrollPane) focusedNode;
|
||||||
|
Node content = scrollPane.getContent();
|
||||||
|
if (content instanceof TextFlow) {
|
||||||
|
TextFlow textFlow = (TextFlow) content;
|
||||||
|
// 获取文本内容
|
||||||
|
for (Node node : textFlow.getChildren()) {
|
||||||
|
if (node instanceof Text) {
|
||||||
|
Text text = (Text) node;
|
||||||
|
selectedText = text.getText();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有文本,复制到剪贴板
|
||||||
|
if (selectedText != null && !selectedText.isEmpty()) {
|
||||||
|
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||||
|
final ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(selectedText);
|
||||||
|
clipboard.setContent(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScene(Scene scene) {
|
||||||
|
if (scene == null) return;
|
||||||
|
this.scene = scene;
|
||||||
|
themeService.setScene(scene);
|
||||||
|
shortcutService.setScene(scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.aiclient.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Chat {
|
||||||
|
private String id;
|
||||||
|
private String title;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private List<Message> messages = new ArrayList<>();
|
||||||
|
|
||||||
|
public Chat() {
|
||||||
|
this.id = UUID.randomUUID().toString();
|
||||||
|
this.createdAt = LocalDateTime.now();
|
||||||
|
this.title = "新会话";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Message> getMessages() {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessages(List<Message> messages) {
|
||||||
|
this.messages = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addMessage(Message message) {
|
||||||
|
messages.add(message);
|
||||||
|
if (messages.size() == 1 && message.getRole().equals("user")) {
|
||||||
|
title = message.getContent().length() > 20
|
||||||
|
? message.getContent().substring(0, 20) + "..."
|
||||||
|
: message.getContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package com.aiclient.model;
|
||||||
|
|
||||||
|
public class Message {
|
||||||
|
private final String role;
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
public Message(String role, String content) {
|
||||||
|
this.role = role;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.aiclient.service;
|
||||||
|
|
||||||
|
import com.aiclient.model.Chat;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class ChatService {
|
||||||
|
private final HttpClient client;
|
||||||
|
private final ObjectMapper mapper;
|
||||||
|
private final String BASE_URL = "http://localhost:8083/ai/v1/ollama/redis/chat";
|
||||||
|
|
||||||
|
public ChatService() {
|
||||||
|
this.client = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(30))
|
||||||
|
.build();
|
||||||
|
this.mapper = new ObjectMapper();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Chat createNewChat() {
|
||||||
|
return new Chat();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMessageStream(String message, String model, Consumer<String> onChunk, Runnable onComplete) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
String encodedMessage = URLEncoder.encode(message, StandardCharsets.UTF_8);
|
||||||
|
String url = String.format("%s?userId=user&input=%s&stream=true", BASE_URL, encodedMessage);
|
||||||
|
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(url))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.GET()
|
||||||
|
.timeout(Duration.ofMinutes(2))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StringBuilder responseBuilder = new StringBuilder();
|
||||||
|
client.send(request, HttpResponse.BodyHandlers.ofLines())
|
||||||
|
.body()
|
||||||
|
.forEach(line -> {
|
||||||
|
try {
|
||||||
|
// JsonNode jsonNode = mapper.readTree(line);
|
||||||
|
String chunk = line;
|
||||||
|
|
||||||
|
// if (jsonNode.has("data")) {
|
||||||
|
// chunk = jsonNode.get("data").asText();
|
||||||
|
// } else if (jsonNode.has("message")) {
|
||||||
|
// chunk = jsonNode.get("message").asText();
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (chunk != null && !chunk.isEmpty()) {
|
||||||
|
responseBuilder.append(chunk);
|
||||||
|
onChunk.accept(chunk);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onComplete.run();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
onChunk.accept("发送消息失败: " + e.getMessage());
|
||||||
|
onComplete.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
package com.aiclient.service;
|
||||||
|
|
||||||
|
public class MarkdownService {
|
||||||
|
private boolean isDarkTheme = false;
|
||||||
|
|
||||||
|
public void setDarkTheme(boolean dark) {
|
||||||
|
this.isDarkTheme = dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDarkTheme() {
|
||||||
|
return isDarkTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String convertToHtml(String markdown) {
|
||||||
|
if (markdown == null || markdown.trim().isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的 Markdown 转换
|
||||||
|
String html = markdown
|
||||||
|
.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("\n", "<br>")
|
||||||
|
.replaceAll("\\*\\*(.+?)\\*\\*", "<strong>$1</strong>") // 粗体
|
||||||
|
.replaceAll("\\*(.+?)\\*", "<em>$1</em>") // 斜体
|
||||||
|
.replaceAll("`([^`]+)`", "<code>$1</code>") // 行内代码
|
||||||
|
.replaceAll("```([\\s\\S]*?)```", "<pre><code>$1</code></pre>"); // 代码块
|
||||||
|
|
||||||
|
return wrapWithStyles(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String wrapWithStyles(String html) {
|
||||||
|
String backgroundColor = isDarkTheme ? "#2d2d2d" : "#ffffff";
|
||||||
|
String textColor = isDarkTheme ? "#ffffff" : "#000000";
|
||||||
|
String codeBackground = isDarkTheme ? "#363636" : "#f6f8fa";
|
||||||
|
String borderColor = isDarkTheme ? "#505050" : "#dfe2e5";
|
||||||
|
|
||||||
|
return String.format("""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
background-color: %s;
|
||||||
|
color: %s;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: %s;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||||
|
background-color: %s;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%%;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid %s;
|
||||||
|
padding: 6px 13px;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: %s;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%%;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0096c9;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid %s;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 16px;
|
||||||
|
color: %s;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid %s;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
%s
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""",
|
||||||
|
backgroundColor, textColor,
|
||||||
|
codeBackground, codeBackground,
|
||||||
|
borderColor, codeBackground,
|
||||||
|
borderColor, isDarkTheme ? "#cccccc" : "#666666",
|
||||||
|
borderColor, html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.aiclient.service;
|
||||||
|
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.input.KeyCombination;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ShortcutService {
|
||||||
|
private final Map<KeyCombination, Runnable> shortcuts = new HashMap<>();
|
||||||
|
private Scene scene;
|
||||||
|
|
||||||
|
public void setScene(Scene scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
registerShortcuts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addShortcut(KeyCombination combination, Runnable action) {
|
||||||
|
shortcuts.put(combination, action);
|
||||||
|
if (scene != null) {
|
||||||
|
scene.getAccelerators().put(combination, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerShortcuts() {
|
||||||
|
if (scene == null) return;
|
||||||
|
|
||||||
|
// 清除现有快捷键
|
||||||
|
scene.getAccelerators().clear();
|
||||||
|
|
||||||
|
// 重新注册所有快捷键
|
||||||
|
shortcuts.forEach((combination, action) ->
|
||||||
|
scene.getAccelerators().put(combination, action)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.aiclient.service;
|
||||||
|
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class ThemeService {
|
||||||
|
private static final String LIGHT_THEME = "/css/light-theme.css";
|
||||||
|
private static final String DARK_THEME = "/css/dark-theme.css";
|
||||||
|
private static final String BASE_STYLE = "/css/style.css";
|
||||||
|
|
||||||
|
private Scene scene;
|
||||||
|
public boolean isDarkTheme = false;
|
||||||
|
|
||||||
|
public void setScene(Scene scene) {
|
||||||
|
if (scene == null) return;
|
||||||
|
this.scene = scene;
|
||||||
|
applyBaseStyle();
|
||||||
|
applyTheme(isDarkTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBaseStyle() {
|
||||||
|
if (scene == null) return;
|
||||||
|
String baseStyle = Objects.requireNonNull(getClass().getResource(BASE_STYLE)).toExternalForm();
|
||||||
|
if (!scene.getStylesheets().contains(baseStyle)) {
|
||||||
|
scene.getStylesheets().add(baseStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleTheme() {
|
||||||
|
if (scene == null) return;
|
||||||
|
isDarkTheme = !isDarkTheme;
|
||||||
|
applyTheme(isDarkTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyTheme(boolean dark) {
|
||||||
|
if (scene == null) return;
|
||||||
|
|
||||||
|
// 移除现有主题
|
||||||
|
scene.getStylesheets().removeIf(style ->
|
||||||
|
style.endsWith("light-theme.css") || style.endsWith("dark-theme.css")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 应用新主题
|
||||||
|
String theme = dark ? DARK_THEME : LIGHT_THEME;
|
||||||
|
String themeStyle = Objects.requireNonNull(getClass().getResource(theme)).toExternalForm();
|
||||||
|
scene.getStylesheets().add(themeStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDarkTheme() {
|
||||||
|
return isDarkTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
.root {
|
||||||
|
-fx-background-color: #1e1e1e;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
-fx-background-color: #3c3c3c;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
-fx-background-color: #505050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area {
|
||||||
|
-fx-background-color: #2d2d2d;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-control-inner-background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view {
|
||||||
|
-fx-background-color: #2d2d2d;
|
||||||
|
-fx-control-inner-background: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-cell {
|
||||||
|
-fx-background-color: #2d2d2d;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-cell:selected {
|
||||||
|
-fx-background-color: #0096c9;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message {
|
||||||
|
-fx-background-color: #264f73;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message {
|
||||||
|
-fx-background-color: #363636;
|
||||||
|
}
|
||||||
|
|
||||||
|
.combo-box {
|
||||||
|
-fx-background-color: #2d2d2d;
|
||||||
|
-fx-border-color: #505050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane {
|
||||||
|
-fx-background-color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-view {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
.root {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-text-fill: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
-fx-background-color: #e3e3e3;
|
||||||
|
-fx-text-fill: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
-fx-background-color: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-text-fill: #000000;
|
||||||
|
-fx-control-inner-background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-view {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-control-inner-background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-cell {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-text-fill: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-cell:selected {
|
||||||
|
-fx-background-color: #0096c9;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message {
|
||||||
|
-fx-background-color: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message {
|
||||||
|
-fx-background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.combo-box {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
-fx-border-color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane {
|
||||||
|
-fx-background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
/* 基础样式 */
|
||||||
|
.root {
|
||||||
|
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
-fx-padding: 8 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area {
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
-fx-background-radius: 0;
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
-fx-font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area .content {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area:focused {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area .scroll-pane {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area .scroll-pane .viewport {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area .scroll-pane .content {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message, .assistant-message {
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
-fx-max-width: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message {
|
||||||
|
-fx-background-color: #007AFF22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message {
|
||||||
|
-fx-background-color: #36363622;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-bar {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-area .prompt-text {
|
||||||
|
-fx-text-fill: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-pane > .viewport {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-flow {
|
||||||
|
-fx-line-spacing: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.scroll-bar:vertical {
|
||||||
|
-fx-pref-width: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-bar:horizontal {
|
||||||
|
-fx-pref-height: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-bar > .thumb {
|
||||||
|
-fx-background-radius: 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 组合框样式 */
|
||||||
|
.combo-box {
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表视图样式 */
|
||||||
|
.list-view {
|
||||||
|
-fx-background-radius: 5;
|
||||||
|
-fx-padding: 5;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
|
||||||
|
<BorderPane xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="com.aiclient.controller.MainController"
|
||||||
|
prefHeight="800.0" prefWidth="1200.0"
|
||||||
|
stylesheets="@../css/style.css">
|
||||||
|
|
||||||
|
<left>
|
||||||
|
<VBox spacing="10" style="-fx-padding: 10;">
|
||||||
|
<ListView fx:id="chatListView" VBox.vgrow="ALWAYS"/>
|
||||||
|
</VBox>
|
||||||
|
</left>
|
||||||
|
|
||||||
|
<center>
|
||||||
|
<VBox spacing="10">
|
||||||
|
<padding>
|
||||||
|
<Insets top="10" right="10" bottom="10" left="10"/>
|
||||||
|
</padding>
|
||||||
|
|
||||||
|
<ToolBar>
|
||||||
|
<HBox spacing="10" alignment="CENTER_LEFT" HBox.hgrow="ALWAYS">
|
||||||
|
<Label text="选择模型:"/>
|
||||||
|
<ComboBox fx:id="modelSelector"/>
|
||||||
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
<Button text="新建会话 (Ctrl+N)" onAction="#createNewChat"/>
|
||||||
|
<Button text="切换主题 (Ctrl+T)" onAction="#toggleTheme"/>
|
||||||
|
<Button text="清空会话 (Ctrl+L)" onAction="#clearCurrentChat"/>
|
||||||
|
</HBox>
|
||||||
|
</ToolBar>
|
||||||
|
|
||||||
|
<ScrollPane VBox.vgrow="ALWAYS" fitToWidth="true" styleClass="chat-scroll-pane">
|
||||||
|
<VBox fx:id="chatContainer" spacing="10"/>
|
||||||
|
</ScrollPane>
|
||||||
|
|
||||||
|
<HBox spacing="10">
|
||||||
|
<TextArea fx:id="inputArea" HBox.hgrow="ALWAYS" prefRowCount="3"
|
||||||
|
promptText="输入消息,按 Ctrl+Enter 发送"/>
|
||||||
|
<Button text="发送" onAction="#sendMessage"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
|
||||||
|
</BorderPane>
|
||||||
Loading…
Reference in New Issue