2025年4月3日18:13:53

master
qmstyle 2025-04-03 18:14:12 +08:00
commit 61ca638f92
16 changed files with 1169 additions and 0 deletions

107
pom.xml Normal file
View File

@ -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>

99
readme.md Normal file
View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
});
}
}

View File

@ -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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.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);
}
}

View File

@ -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)
);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>