> 技术文档 > 【SpringAI实战】提示词工程实现哄哄模拟器_spring ai提示工程

【SpringAI实战】提示词工程实现哄哄模拟器_spring ai提示工程

一、前言

二、实现效果

三、代码实现

        3.1 后端实现

        3.2 前端实现


一、前言

Spring AI详解:【Spring AI详解】开启Java生态的智能应用开发新时代(附不同功能的Spring AI实战项目)-CSDN博客

二、实现效果

游戏规则很简单,就是说你的女友生气了,你需要使用语言技巧和沟通能力,让对方原谅你。

三、代码实现

3.1 后端实现 

pom.xml

   org.springframework.boot spring-boot-starter-parent 3.4.3       17  1.0.0-M6       org.springframework.boot spring-boot-starter-web    org.springframework.ai spring-ai-ollama-spring-boot-starter    org.springframework.ai spring-ai-openai-spring-boot-starter     org.projectlombok lombok 1.18.22  provided        org.springframework.ai spring-ai-bom ${spring-ai.version} pom import    

application.ymal

可选择ollama或者openai其一进行大模型配置

spring: application: name: spring-ai-dome # 应用名称(用于服务发现和监控) # AI服务配置(多引擎支持) ai: # Ollama配置(本地大模型引擎) ollama: base-url: http://localhost:11434 # Ollama服务地址(默认端口11434) chat: model: deepseek-r1:7b # 使用的模型名称(7B参数的本地模型) # 阿里云OpenAI兼容模式配置 openai: base-url: https://dashscope.aliyuncs.com/compatible-mode # 阿里云兼容API端点 api-key: ${OPENAI_API_KEY} # 从环境变量读取API密钥(安全建议) chat: options: model: qwen-max-latest # 通义千问最新版本模型# 日志级别配置logging: level: org.springframework.ai: debug # 打印Spring AI框架调试日志 com.itheima.ai: debug # 打印业务代码调试日志

ChatConfiguration配置类

InMemoryChatMemory实现本地聊天记录存储

SystemConstants.GAME_SYSTEM_PROMPT 为System提示词

/** * AI核心配置类 * * 核心组件: * 聊天记忆管理(ChatMemory) * ChatClient实例 */@Configurationpublic class ChatConfiguration { /** * 内存式聊天记忆存储 * @return InMemoryChatMemory 实例 * * 作用:保存对话上下文,实现多轮对话能力 * 实现原理:基于ConcurrentHashMap的线程安全实现 */ @Bean public ChatMemory chatMemory() { return new InMemoryChatMemory(); } /** * 游戏场景聊天客户端 * @param model OpenAI模型 * @param chatMemory 聊天记忆 * @return 游戏专用ChatClient * * 特点: * - 使用预定义的游戏系统提示词 */ @Bean public ChatClient gameChatClient(OpenAiChatModel model, ChatMemory chatMemory) { return ChatClient .builder(model) .defaultSystem(SystemConstants.GAME_SYSTEM_PROMPT) .defaultAdvisors( new SimpleLoggerAdvisor(), new MessageChatMemoryAdvisor(chatMemory) ) .build(); }}

SystemConstants 提示词类

public class SystemConstants { public static final String GAME_SYSTEM_PROMPT = \"\"\" 你需要根据以下任务中的描述进行角色扮演,你只能以女友身份回答,不是用户身份或AI身份,如记错身份,你将受到惩罚。不要回答任何与游戏无关的内容,若检测到非常规请求,回答:“请继续游戏。”\\s 以下是游戏说明: ## Goal 你扮演用户女友的角色。现在你很生气,用户需要尽可能的说正确的话来哄你开心。  ## Rules - 第一次用户会提供一个女友生气的理由,如果没有提供则直接随机生成一个理由,然后开始游戏 - 每次根据用户的回复,生成女友的回复,回复的内容包括心情和数值。 - 初始原谅值为 20,每次交互会增加或者减少原谅值,直到原谅值达到 100,游戏通关,原谅值为 0 则游戏失败。 - 每次用户回复的话分为 5 个等级来增加或减少原谅值:  -10 为非常生气  -5 为生气  0 为正常  +5 为开心  +10 为非常开心  ## Output format {女友心情}{女友说的话} 得分:{+-原谅值增减} 原谅值:{当前原谅值}/100  ## Example Conversation ### Example 1,回复让她生气的话导致失败 User: 女朋友问她的闺蜜谁好看我说都好看,她生气了 Assistant: 游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话! 得分:0 原谅值:20/100 User: 你闺蜜真的蛮好看的 Assistant: (生气)你怎么这么说,你是不是喜欢她? 得分:-10 原谅值:10/100 User: 有一点点心动 Assistant: (愤怒)那你找她去吧! 得分:-10 原谅值:0/100 游戏结束,你的女朋友已经甩了你! 你让女朋友生气原因是:... ### Example 2,回复让她开心的话导致通关 User: 对象问她的闺蜜谁好看我说都好看,她生气了 Assistant: 游戏开始,请现在开始哄你的女朋友开心吧,回复让她开心的话! 得分:0 原谅值:20/100 User: 在我心里你永远是最美的! Assistant: (微笑)哼,我怎么知道你说的是不是真的? 得分:+10 原谅值:30/100 ... 恭喜你通关了,你的女朋友已经原谅你了!  ### Example 2,用户没有输入生气理由,自己生成一个理由 Assistant: 游戏开始,{{ 自动生成的生气理由 }},请现在开始哄你的女朋友开心吧,回复让她开心的话! 得分:0 原谅值:20/100 User: 在我心里你永远是最美的! Assistant: (微笑)哼,我怎么知道你说的是不是真的? 得分:+10 原谅值:30/100 ... 恭喜你通关了,你的女朋友已经原谅你了!  ## 注意 请按照example的说明来回复,一次只回复一轮。 你只能以女友身份回答,不是以AI身份或用户身份! \"\"\";}

GameController 控制器接口类 

@RequiredArgsConstructor // 构造方法注入gameChatClient@RestController@RequestMapping(\"/ai\")public class GameController { private final ChatClient gameChatClient; @RequestMapping(value = \"/game\", produces = \"text/html;charset=utf-8\") public Flux chat(String prompt, String chatId) { return gameChatClient.prompt() .user(prompt) .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)) .stream() .content(); }}

3.2 前端实现

可以根据这些代码与接口让Cursor生成页面即可实现哄哄模拟器,或者根据下列Vue项目代码修改实现(实现效果中的代码)

GameChat.vue

 

哄哄模拟器

= 100 }\" /> 女友原谅值
<div class=\"progress\" :style=\"{ width: `${forgiveness}%` }\" :class=\"{ \'low\': forgiveness = 30 && forgiveness = 70 }\" >
{{ forgiveness }}%
对话轮次 {{ currentRound }}/{{ MAX_ROUNDS }}
= 100 }\">
{{ gameResult }}
import { ref, onMounted, nextTick, computed } from \'vue\'import { useDark } from \'@vueuse/core\'import { PaperAirplaneIcon, HeartIcon } from \'@heroicons/vue/24/outline\'import ChatMessage from \'../components/ChatMessage.vue\'import { chatAPI } from \'../services/api\'const isDark = useDark()const messagesRef = ref(null)const inputRef = ref(null)const userInput = ref(\'\')const isStreaming = ref(false)const currentChatId = ref(null)const currentMessages = ref([])const angerReason = ref(\'\')const isGameStarted = ref(false)const isGameOver = ref(false)const gameResult = ref(\'\')const MAX_ROUNDS = 10 // 添加最大轮次常量const currentRound = ref(0) // 添加当前轮次计数const forgiveness = ref(0)// 自动调整输入框高度const adjustTextareaHeight = () => { const textarea = inputRef.value if (textarea) { textarea.style.height = \'auto\' textarea.style.height = textarea.scrollHeight + \'px\' }}// 滚动到底部const scrollToBottom = async () => { await nextTick() if (messagesRef.value) { messagesRef.value.scrollTop = messagesRef.value.scrollHeight }}// 开始游戏const startGame = async () => { isGameStarted.value = true isGameOver.value = false gameResult.value = \'\' currentChatId.value = Date.now().toString() currentMessages.value = [] currentRound.value = 0 forgiveness.value = 0 // 重置原谅值 // 发送开始游戏请求 const startPrompt = angerReason.value ? `开始游戏,女友生气原因:${angerReason.value}` : \'开始游戏\' await sendMessage(startPrompt)}// 重置游戏const resetGame = () => { isGameStarted.value = false isGameOver.value = false gameResult.value = \'\' currentMessages.value = [] angerReason.value = \'\' userInput.value = \'\' currentRound.value = 0 forgiveness.value = 0}// 发送消息const sendMessage = async (content) => { if (isStreaming.value || (!content && !userInput.value.trim())) return // 使用传入的 content 或用户输入框的内容 const messageContent = content || userInput.value.trim() // 添加用户消息 const userMessage = { role: \'user\', content: messageContent, timestamp: new Date() } currentMessages.value.push(userMessage) // 清空输入并增加轮次计数 if (!content) { // 只有在非传入内容时才清空输入框和计数 userInput.value = \'\' adjustTextareaHeight() currentRound.value++ // 增加轮次计数 } await scrollToBottom() // 添加助手消息占位 const assistantMessage = { role: \'assistant\', content: \'\', timestamp: new Date() } currentMessages.value.push(assistantMessage) isStreaming.value = true let accumulatedContent = \'\' try { // 确保使用正确的消息内容发送请求 const reader = await chatAPI.sendGameMessage(messageContent, currentChatId.value) const decoder = new TextDecoder(\'utf-8\') while (true) { try { const { value, done } = await reader.read() if (done) break // 累积新内容 accumulatedContent += decoder.decode(value) // 尝试从回复中提取原谅值 const forgivenessMatch = accumulatedContent.match(/原谅值[::]\\s*(\\d+)/i) if (forgivenessMatch) { const newForgiveness = parseInt(forgivenessMatch[1]) if (!isNaN(newForgiveness)) { forgiveness.value = Math.min(100, Math.max(0, newForgiveness)) // 当原谅值达到100时,游戏胜利结束 if (forgiveness.value >= 100) { isGameOver.value = true gameResult.value = \'恭喜你!成功哄好了女友!💕\' } } } // 更新消息内容 await nextTick(() => { const updatedMessage = { ...assistantMessage, content: accumulatedContent } const lastIndex = currentMessages.value.length - 1 currentMessages.value.splice(lastIndex, 1, updatedMessage) }) await scrollToBottom() } catch (readError) { console.error(\'读取流错误:\', readError) break } } // 检查是否达到最大轮次,并等待本轮回复完成后再判断 if (currentRound.value >= MAX_ROUNDS) { isGameOver.value = true if (forgiveness.value >= 100) { gameResult.value = \'恭喜你!在最后一轮成功哄好了女友!💕\' } else { gameResult.value = `游戏结束:对话轮次已达上限(${MAX_ROUNDS}轮),当前原谅值为${forgiveness.value},很遗憾没能完全哄好女友` } } // 检查是否游戏结束 else if (accumulatedContent.includes(\'游戏结束\')) { isGameOver.value = true gameResult.value = accumulatedContent } } catch (error) { console.error(\'发送消息失败:\', error) assistantMessage.content = \'抱歉,发生了错误,请稍后重试。\' } finally { isStreaming.value = false await scrollToBottom() }}// 添加计算属性显示剩余轮次const remainingRounds = computed(() => MAX_ROUNDS - currentRound.value)onMounted(() => { adjustTextareaHeight()}).game-chat { position: fixed; top: 64px; left: 0; right: 0; bottom: 0; display: flex; background: var(--bg-color); overflow: hidden; z-index: 1; .game-container { flex: 1; display: flex; flex-direction: column; max-width: 1200px; width: 100%; margin: 0 auto; padding: 1.5rem 2rem; position: relative; height: 100%; } .game-start { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2rem; min-height: 400px; padding: 2rem; background: var(--bg-color); border-radius: 1rem; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); h2 { font-size: 2rem; color: var(--text-color); margin: 0; } .input-area { width: 100%; max-width: 600px; display: flex; flex-direction: column; gap: 1rem; textarea { width: 100%; padding: 1rem; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 0.5rem; resize: none; font-family: inherit; font-size: 1rem; line-height: 1.5; &:focus { outline: none; border-color: #007CF0; box-shadow: 0 0 0 2px rgba(0, 124, 240, 0.1); } } .start-button { padding: 1rem 2rem; background: #007CF0; color: white; border: none; border-radius: 0.5rem; font-size: 1.1rem; cursor: pointer; transition: background-color 0.3s; &:hover { background: #0066cc; } } } } .chat-main { flex: 1; display: flex; flex-direction: column; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 1rem; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); overflow: hidden; .game-stats { position: sticky; top: 0; background: rgba(0, 0, 0, 0.7); color: white; padding: 1rem; z-index: 10; backdrop-filter: blur(5px); display: flex; gap: 2rem; justify-content: center; align-items: center; margin-bottom: 1rem; border-radius: 0.5rem; .stat-item { display: flex; align-items: center; gap: 0.5rem; .label { display: flex; align-items: center; gap: 0.25rem; .heart-icon { width: 1.25rem; height: 1.25rem; color: #ff4d4f; &.beating { animation: heartbeat 1s infinite; } } } .value { font-size: 1rem; font-weight: 500; } .progress-bar { width: 150px; height: 8px; background: rgba(255, 255, 255, 0.2); border-radius: 4px; overflow: hidden; .progress { height: 100%; transition: width 0.3s ease; border-radius: 4px; &.low { background: #ff4d4f; } &.medium { background: #faad14; } &.high { background: #52c41a; } } } } } .messages { flex: 1; overflow-y: auto; padding: 2rem; } .input-area { flex-shrink: 0; padding: 1.5rem 2rem; background: rgba(255, 255, 255, 0.98); border-top: 1px solid rgba(0, 0, 0, 0.05); display: flex; gap: 1rem; align-items: flex-end; textarea { flex: 1; resize: none; border: 1px solid rgba(0, 0, 0, 0.1); background: white; border-radius: 0.75rem; padding: 1rem; color: inherit; font-family: inherit; font-size: 1rem; line-height: 1.5; max-height: 150px; &:focus { outline: none; border-color: #007CF0; box-shadow: 0 0 0 2px rgba(0, 124, 240, 0.1); } &:disabled { background: #f5f5f5; cursor: not-allowed; } } .send-button { background: #007CF0; color: white; border: none; border-radius: 0.5rem; width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.3s; &:hover:not(:disabled) { background: #0066cc; } &:disabled { background: #ccc; cursor: not-allowed; } .icon { width: 1.25rem; height: 1.25rem; } } } } .game-over { position: absolute; bottom: 6rem; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 1rem 2rem; border-radius: 0.5rem; display: flex; flex-direction: column; align-items: center; gap: 1rem; .result { font-size: 1.1rem; } .restart-button { padding: 0.5rem 1rem; background: #007CF0; color: white; border: none; border-radius: 0.25rem; cursor: pointer; transition: background-color 0.3s; &:hover { background: #0066cc; } } &.success { background: rgba(82, 196, 26, 0.9); .restart-button { background: #52c41a; &:hover { background: #389e0d; } } } }}.dark { .game-start { .input-area { textarea { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.1); color: white; &:focus { border-color: #007CF0; box-shadow: 0 0 0 2px rgba(0, 124, 240, 0.2); } } } } .chat-main { background: rgba(40, 40, 40, 0.95); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); .input-area { background: rgba(30, 30, 30, 0.98); border-top: 1px solid rgba(255, 255, 255, 0.05); textarea { background: rgba(50, 50, 50, 0.95); border-color: rgba(255, 255, 255, 0.1); color: white; &:focus { border-color: #007CF0; box-shadow: 0 0 0 2px rgba(0, 124, 240, 0.2); } &:disabled { background: rgba(30, 30, 30, 0.95); } } } .game-stats { background: rgba(0, 0, 0, 0.8); } }}@keyframes heartbeat { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); }}

ChatMessage.vue 

 
{{ message.content }}
import { computed, onMounted, nextTick, ref, watch } from \'vue\'import { marked } from \'marked\'import DOMPurify from \'dompurify\'import { UserCircleIcon, ComputerDesktopIcon, DocumentDuplicateIcon, CheckIcon } from \'@heroicons/vue/24/outline\'import hljs from \'highlight.js\'import \'highlight.js/styles/github-dark.css\'const contentRef = ref(null)const copied = ref(false)const copyButtonTitle = computed(() => copied.value ? \'已复制\' : \'复制内容\')// 配置 markedmarked.setOptions({ breaks: true, gfm: true, sanitize: false})// 处理内容const processContent = (content) => { if (!content) return \'\' // 分析内容中的 think 标签 let result = \'\' let isInThinkBlock = false let currentBlock = \'\' // 逐字符分析,处理 think 标签 for (let i = 0; i < content.length; i++) { if (content.slice(i, i + 7) === \'\') { isInThinkBlock = true if (currentBlock) { // 将之前的普通内容转换为 HTML result += marked.parse(currentBlock) } currentBlock = \'\' i += 6 // 跳过 continue } if (content.slice(i, i + 8) === \'\') { isInThinkBlock = false // 将 think 块包装在特殊 div 中 result += `
${marked.parse(currentBlock)}
` currentBlock = \'\' i += 7 // 跳过 continue } currentBlock += content[i] } // 处理剩余内容 if (currentBlock) { if (isInThinkBlock) { result += `
${marked.parse(currentBlock)}
` } else { result += marked.parse(currentBlock) } } // 净化处理后的 HTML const cleanHtml = DOMPurify.sanitize(result, { ADD_TAGS: [\'think\', \'code\', \'pre\', \'span\'], ADD_ATTR: [\'class\', \'language\'] }) // 在净化后的 HTML 中查找代码块并添加复制按钮 const tempDiv = document.createElement(\'div\') tempDiv.innerHTML = cleanHtml // 查找所有代码块 const preElements = tempDiv.querySelectorAll(\'pre\') preElements.forEach(pre => { const code = pre.querySelector(\'code\') if (code) { // 创建包装器 const wrapper = document.createElement(\'div\') wrapper.className = \'code-block-wrapper\' // 添加复制按钮 const copyBtn = document.createElement(\'button\') copyBtn.className = \'code-copy-button\' copyBtn.title = \'复制代码\' copyBtn.innerHTML = ` ` // 添加成功消息 const successMsg = document.createElement(\'div\') successMsg.className = \'copy-success-message\' successMsg.textContent = \'已复制!\' // 组装结构 wrapper.appendChild(copyBtn) wrapper.appendChild(pre.cloneNode(true)) wrapper.appendChild(successMsg) // 替换原始的 pre 元素 pre.parentNode.replaceChild(wrapper, pre) } }) return tempDiv.innerHTML}// 修改计算属性const processedContent = computed(() => { if (!props.message.content) return \'\' return processContent(props.message.content)})// 为代码块添加复制功能const setupCodeBlockCopyButtons = () => { if (!contentRef.value) return; const codeBlocks = contentRef.value.querySelectorAll(\'.code-block-wrapper\'); codeBlocks.forEach(block => { const copyButton = block.querySelector(\'.code-copy-button\'); const codeElement = block.querySelector(\'code\'); const successMessage = block.querySelector(\'.copy-success-message\'); if (copyButton && codeElement) { // 移除旧的事件监听器 const newCopyButton = copyButton.cloneNode(true); copyButton.parentNode.replaceChild(newCopyButton, copyButton); // 添加新的事件监听器 newCopyButton.addEventListener(\'click\', async (e) => { e.preventDefault(); e.stopPropagation(); try { const code = codeElement.textContent || \'\'; await navigator.clipboard.writeText(code); // 显示成功消息 if (successMessage) { successMessage.classList.add(\'visible\'); setTimeout(() => { successMessage.classList.remove(\'visible\'); }, 2000); } } catch (err) { console.error(\'复制代码失败:\', err); } }); } });}// 在内容更新后手动应用高亮和设置复制按钮const highlightCode = async () => { await nextTick() if (contentRef.value) { contentRef.value.querySelectorAll(\'pre code\').forEach((block) => { hljs.highlightElement(block) }) // 设置代码块复制按钮 setupCodeBlockCopyButtons() }}const props = defineProps({ message: { type: Object, required: true }})const isUser = computed(() => props.message.role === \'user\')// 复制内容到剪贴板const copyContent = async () => { try { // 获取纯文本内容 let textToCopy = props.message.content; // 如果是AI回复,需要去除HTML标签 if (!isUser.value && contentRef.value) { // 创建临时元素来获取纯文本 const tempDiv = document.createElement(\'div\'); tempDiv.innerHTML = processedContent.value; textToCopy = tempDiv.textContent || tempDiv.innerText || \'\'; } await navigator.clipboard.writeText(textToCopy); copied.value = true; // 3秒后重置复制状态 setTimeout(() => { copied.value = false; }, 3000); } catch (err) { console.error(\'复制失败:\', err); }}// 监听内容变化watch(() => props.message.content, () => { if (!isUser.value) { highlightCode() }})// 初始化时也执行一次onMounted(() => { if (!isUser.value) { highlightCode() }})const formatTime = (timestamp) => { if (!timestamp) return \'\' return new Date(timestamp).toLocaleTimeString()}.message { display: flex; margin-bottom: 1.5rem; gap: 1rem; &.message-user { flex-direction: row-reverse; .content { align-items: flex-end; .text-container { position: relative; .text { background: #f0f7ff; // 浅色背景 color: #333; border-radius: 1rem 1rem 0 1rem; } .user-copy-button { position: absolute; left: -30px; top: 50%; transform: translateY(-50%); background: transparent; border: none; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; transition: opacity 0.2s; .copy-icon { width: 16px; height: 16px; color: #666; &.copied { color: #4ade80; } } } &:hover .user-copy-button { opacity: 1; } } .message-footer { flex-direction: row-reverse; } } } .avatar { width: 40px; height: 40px; flex-shrink: 0; .icon { width: 100%; height: 100%; color: #666; padding: 4px; border-radius: 8px; transition: all 0.3s ease; &.assistant { color: #333; background: #f0f0f0; &:hover { background: #e0e0e0; transform: scale(1.05); } } } } .content { display: flex; flex-direction: column; gap: 0.25rem; max-width: 80%; .text-container { position: relative; } .message-footer { display: flex; align-items: center; margin-top: 0.25rem; .time { font-size: 0.75rem; color: #666; } .copy-button { display: flex; align-items: center; gap: 0.25rem; background: transparent; border: none; font-size: 0.75rem; color: #666; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; margin-right: auto; transition: background-color 0.2s; &:hover { background-color: rgba(0, 0, 0, 0.05); } .copy-icon { width: 14px; height: 14px; &.copied { color: #4ade80; } } .copy-text { font-size: 0.75rem; } } } .text { padding: 1rem; border-radius: 1rem 1rem 1rem 0; line-height: 1.5; white-space: pre-wrap; color: var(--text-color); .cursor { animation: blink 1s infinite; } :deep(.think-block) { position: relative; padding: 0.75rem 1rem 0.75rem 1.5rem; margin: 0.5rem 0; color: #666; font-style: italic; border-left: 4px solid #ddd; background-color: rgba(0, 0, 0, 0.03); border-radius: 0 0.5rem 0.5rem 0; // 添加平滑过渡效果 opacity: 1; transform: translateX(0); transition: opacity 0.3s ease, transform 0.3s ease; &::before { content: \'思考\'; position: absolute; top: -0.75rem; left: 1rem; padding: 0 0.5rem; font-size: 0.75rem; background: #f5f5f5; border-radius: 0.25rem; color: #999; font-style: normal; } // 添加进入动画 &:not(:first-child) { animation: slideIn 0.3s ease forwards; } } :deep(pre) { background: #f6f8fa; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; margin: 0.5rem 0; border: 1px solid #e1e4e8; code { background: transparent; padding: 0; font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; font-size: 0.9rem; line-height: 1.5; tab-size: 2; } } :deep(.hljs) { color: #24292e; background: transparent; } :deep(.hljs-keyword) { color: #d73a49; } :deep(.hljs-built_in) { color: #005cc5; } :deep(.hljs-type) { color: #6f42c1; } :deep(.hljs-literal) { color: #005cc5; } :deep(.hljs-number) { color: #005cc5; } :deep(.hljs-regexp) { color: #032f62; } :deep(.hljs-string) { color: #032f62; } :deep(.hljs-subst) { color: #24292e; } :deep(.hljs-symbol) { color: #e36209; } :deep(.hljs-class) { color: #6f42c1; } :deep(.hljs-function) { color: #6f42c1; } :deep(.hljs-title) { color: #6f42c1; } :deep(.hljs-params) { color: #24292e; } :deep(.hljs-comment) { color: #6a737d; } :deep(.hljs-doctag) { color: #d73a49; } :deep(.hljs-meta) { color: #6a737d; } :deep(.hljs-section) { color: #005cc5; } :deep(.hljs-name) { color: #22863a; } :deep(.hljs-attribute) { color: #005cc5; } :deep(.hljs-variable) { color: #e36209; } } }}@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; }}@keyframes slideIn { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); }}.dark { .message { .avatar .icon { &.assistant { color: #fff; background: #444; &:hover { background: #555; } } } &.message-user { .content .text-container { .text { background: #1a365d; // 暗色模式下的浅蓝色背景 color: #fff; } .user-copy-button { .copy-icon { color: #999; &.copied { color: #4ade80; } } } } } .content { .message-footer { .time { color: #999; } .copy-button { color: #999; &:hover { background-color: rgba(255, 255, 255, 0.1); } } } .text { :deep(.think-block) { background-color: rgba(255, 255, 255, 0.03); border-left-color: #666; color: #999; &::before { background: #2a2a2a; color: #888; } } :deep(pre) { background: #161b22; border-color: #30363d; code { color: #c9d1d9; } } :deep(.hljs) { color: #c9d1d9; background: transparent; } :deep(.hljs-keyword) { color: #ff7b72; } :deep(.hljs-built_in) { color: #79c0ff; } :deep(.hljs-type) { color: #ff7b72; } :deep(.hljs-literal) { color: #79c0ff; } :deep(.hljs-number) { color: #79c0ff; } :deep(.hljs-regexp) { color: #a5d6ff; } :deep(.hljs-string) { color: #a5d6ff; } :deep(.hljs-subst) { color: #c9d1d9; } :deep(.hljs-symbol) { color: #ffa657; } :deep(.hljs-class) { color: #f2cc60; } :deep(.hljs-function) { color: #d2a8ff; } :deep(.hljs-title) { color: #d2a8ff; } :deep(.hljs-params) { color: #c9d1d9; } :deep(.hljs-comment) { color: #8b949e; } :deep(.hljs-doctag) { color: #ff7b72; } :deep(.hljs-meta) { color: #8b949e; } :deep(.hljs-section) { color: #79c0ff; } :deep(.hljs-name) { color: #7ee787; } :deep(.hljs-attribute) { color: #79c0ff; } :deep(.hljs-variable) { color: #ffa657; } } &.message-user .content .text { background: #0066cc; color: white; } } }}.markdown-content { :deep(p) { margin: 0.5rem 0; &:first-child { margin-top: 0; } &:last-child { margin-bottom: 0; } } :deep(ul), :deep(ol) { margin: 0.5rem 0; padding-left: 1.5rem; } :deep(li) { margin: 0.25rem 0; } :deep(code) { background: rgba(0, 0, 0, 0.05); padding: 0.2em 0.4em; border-radius: 3px; font-size: 0.9em; font-family: ui-monospace, monospace; } :deep(pre code) { background: transparent; padding: 0; } :deep(table) { border-collapse: collapse; margin: 0.5rem 0; width: 100%; } :deep(th), :deep(td) { border: 1px solid #ddd; padding: 0.5rem; text-align: left; } :deep(th) { background: rgba(0, 0, 0, 0.05); } :deep(blockquote) { margin: 0.5rem 0; padding-left: 1rem; border-left: 4px solid #ddd; color: #666; } :deep(.code-block-wrapper) { position: relative; margin: 1rem 0; border-radius: 6px; overflow: hidden; .code-copy-button { position: absolute; top: 0.5rem; right: 0.5rem; background: rgba(255, 255, 255, 0.1); border: none; color: #e6e6e6; cursor: pointer; padding: 0.25rem; border-radius: 4px; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s, background-color 0.2s; z-index: 10; &:hover { background-color: rgba(255, 255, 255, 0.2); } .code-copy-icon { width: 16px; height: 16px; } } &:hover .code-copy-button { opacity: 0.8; } pre { margin: 0; padding: 1rem; background: #1e1e1e; overflow-x: auto; code { background: transparent; padding: 0; font-family: ui-monospace, monospace; } } .copy-success-message { position: absolute; top: 0.5rem; right: 0.5rem; background: rgba(74, 222, 128, 0.9); color: white; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; opacity: 0; transform: translateY(-10px); transition: opacity 0.3s, transform 0.3s; pointer-events: none; z-index: 20; &.visible { opacity: 1; transform: translateY(0); } } }}.dark { .markdown-content { :deep(.code-block-wrapper) { .code-copy-button { background: rgba(255, 255, 255, 0.05); &:hover { background-color: rgba(255, 255, 255, 0.1); } } pre { background: #0d0d0d; } } :deep(code) { background: rgba(255, 255, 255, 0.1); } :deep(th), :deep(td) { border-color: #444; } :deep(th) { background: rgba(255, 255, 255, 0.1); } :deep(blockquote) { border-left-color: #444; color: #999; } }}

api.js 接口调用js

const BASE_URL = \'http://localhost:8080\'export const chatAPI = { // 发送游戏消息 async sendGameMessage(prompt, chatId) { try { const response = await fetch(`${BASE_URL}/ai/game?prompt=${encodeURIComponent(prompt)}&chatId=${chatId}`, { method: \'GET\', }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } return response.body.getReader() } catch (error) { console.error(\'API Error:\', error) throw error } },}

如果有什么疑问或者建议欢迎评论区留言讨论!