艾莉丝努力练剑头像
【AI大模型接入SDK】前端页面提示词封面图

【AI大模型接入SDK】前端页面提示词


头像


🎬 个人主页:艾莉丝努力练剑

❄专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平

🎬 艾莉丝的简介:

在这里插入图片描述



在这里插入图片描述


1 ~> AI 聊天助手前端工程需求与实现

1.1 项目背景

  • 后端服务地址:192.168.100.238:8080,C++ httplib 实现聊天服务
  • 静态资源挂载:服务将./www目录挂载为/,访问ip:port / ip:port/index.html加载前端页面
  • 技术选型:HTML + CSS + JavaScript;引入 marked 解析 Markdown,highlight.js 实现代码语法高亮
  • 代码分离原则:HTML、CSS、JS 拆分独立文件,降低局部修改带来的牵一发而动全身的副作用,便于接口调试、功能迭代维护
  • 页面原型参考:DeepSeek 网页版聊天界面

1.2 后端接口完整定义

1.2.1 获取会话列表 GET /api/sessions

  • HTTP 状态码:200 OK
  • 响应 JSON 结构
{
  "success": true,
  "message": "string",
  "data": [
    {
      "id": "string",
      "model": "string",
      "created_at": "int64_t",
      "updated_at": "int64_t",
      "message_count": "int",
      "first_user_message": "string"
    }
  ]
}

1.2.2 获取可用模型 GET /api/models

  • HTTP 状态码:200 OK
  • 响应 JSON 结构
{
  "success": true,
  "message": "string",
  "data": [
    {
      "name": "string",
      "desc": "string"
    }
  ]
}

1.2.3 创建新会话 POST /api/session

  • 请求体
{
  "model": "string"
}
  • HTTP 状态码:200 OK
  • 响应 JSON 结构
{
  "success": true,
  "message": "string",
  "data": {
    "sessionid": "string",
    "model": "string"
  }
}

1.2.4 获取会话历史消息 GET /api/session/${session_id}/history

  • HTTP 状态码:200 OK
  • 响应 JSON 结构
{
  "success": true,
  "message": "string",
  "data": [
    {
      "id": "string",
      "role": "string",
      "content": "string",
      "timestamp": 0
    }
  ]
}

1.2.5 发送消息 - 流式 SSE 响应 POST /api/message/async

  • 请求体
{
  "session_id": "string",
  "message": "string"
}
  • HTTP 状态码:200 OK
  • SSE 流式返回格式:
data: 文本数据\n\n
data: 文本数据\n\n
data: [DONE]\n\n
  • [DONE]标记流式输出结束

1.2.6 删除会话 DELETE /api/session/${session_id}

  • HTTP 状态码:200 OK
  • 响应 JSON 结构
{
  "success": true,
  "message": "string"
}

1.3 页面功能需求规约

1.3.1 页面布局

  1. 页面左上角:AI Chat 图标 + AI聊天助手文字 +【新建对话】按钮
  2. 左侧侧边栏:历史会话列表
    • 每条会话展示:会话更新时间、会话第一条消息、模型名称、删除按钮(按钮文本…)
    • 点击删除按钮调用后端接口删除服务器对应 session 会话
  3. 右侧主区域默认欢迎页面
    • 展示图标 + 提示文本:选择一个历史对话继续,或创建新的对话,开启您的AI之旅
    • 附带【新建对话】按钮
  4. 会话创建成功后右侧主区域
    • 85% 高度为聊天消息显示区域;模型消息居左侧展示,用户消息居右侧展示;每条消息附带收发时间
    • 底部输入编辑框:占位提示请输入您的消息... (Enter发送,Shift+Enter换行),最大输入 2000 字符;右侧发送按钮;下方展示当前字数/2000字数统计
  5. 模型选择弹窗
    • 点击新建对话触发弹窗,网格布局渲染全部可用模型,单选选择模型,展示模型名称 + 描述
    • 【确认创建对话】:关闭弹窗,向后端发起创建会话请求
    • 【取消】:直接关闭弹窗,不创建会话
    • 同一时间仅允许选中一个模型

1.3.2 页面生命周期行为

  1. 浏览器加载index.html完成后,自动请求后端获取会话列表,渲染左侧会话栏
  2. 两处新建对话按钮(左上角、欢迎页):先请求后端获取模型列表,用户选定模型确认后创建会话,会话加入左侧会话列表,进入聊天会话界面
  3. SSE 流式消息处理
    • 流式返回内容做 Markdown 解析渲染
    • 代码块启用语法高亮,代码块支持一键复制
    • 处理大量流式分片,防止页面 DOM 变形乱码

1.3.3 快捷键

  • Enter:发送消息
  • Shift + Enter:输入框内换行

2 ~> 完整前端实现代码

文件目录:www/目录下存放三个文件:index.html、style.css、app.js;后端 httplib 将./www挂载为 web 根路径

2.1 index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI聊天助手</title>
    <link rel="stylesheet" href="style.css">
    <!-- Markdown解析库 marked -->
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <!-- highlight.js 代码高亮 -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github.min.css">
    <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/core.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/javascript.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/python.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/cpp.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/css.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/bash.min.js"></script>
</head>
<body>
<div id="app">
    <!-- 左侧会话侧边栏 -->
    <aside class="sidebar">
        <div class="sidebar-header">
            <div class="logo">
                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
                </svg>
            </div>
            <span class="title">AI聊天助手</span>
            <button id="newChatBtn" class="new-chat-btn" title="新建对话">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                    <line x1="12" y1="5" x2="12" y2="19"></line>
                    <line x1="5" y1="12" x2="19" y2="12"></line>
                </svg>
                新建对话
            </button>
        </div>
        <div id="sessionList" class="session-list">
            <div class="loading">加载中...</div>
        </div>
    </aside>

    <!-- 主内容区域 -->
    <main class="main-content">
        <!-- 欢迎页面 -->
        <div id="welcomeSection" class="welcome-section">
            <div class="welcome-icon">
                <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
                    <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
                </svg>
            </div>
            <p class="welcome-text">选择一个历史对话继续,或创建新的对话,开启您的AI之旅</p>
            <button id="welcomeNewChatBtn" class="primary-btn">新建对话</button>
        </div>

        <!-- 聊天会话区域 -->
        <div id="chatSection" class="chat-section" style="display: none;">
            <div id="messagesContainer" class="messages-container"></div>
            <div class="input-area">
                <div class="input-wrapper">
                    <textarea id="messageInput" class="message-input"
                              placeholder="请输入您的消息...(Enter发送,Shift+Enter换行)"
                              maxlength="2000" rows="1"></textarea>
                    <button id="sendBtn" class="send-btn" disabled>
                        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <line x1="22" y1="2" x2="11" y2="13"></line>
                            <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
                        </svg>
                    </button>
                </div>
                <div class="char-count">
                    <span id="charCount">0</span>/2000
                </div>
            </div>
        </div>
    </main>
</div>

<!-- 模型选择弹窗 -->
<div id="modelModal" class="modal-overlay" style="display: none;">
    <div class="modal-content">
        <h3>选择模型</h3>
        <div id="modelGrid" class="model-grid">
            <div class="loading">加载中...</div>
        </div>
        <div class="modal-actions">
            <button id="cancelBtn" class="secondary-btn">取消</button>
            <button id="confirmBtn" class="primary-btn" disabled>确认创建对话</button>
        </div>
    </div>
</div>

<script src="app.js"></script>
</body>
</html>

2.2 style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    font-size: 14px;
    color: #333;
    background-color: #f7f7f8;
    height: 100vh;
    overflow: hidden;
}

#app {
    display: flex;
    height: 100vh;
}

/* ========== 侧边栏样式 ========== */
.sidebar {
    width: 260px;
    background-color: #202123;
    color: #fff;
    display: flex;
    flex-direction: column;
    border-right: 1px solid #2d2d2d;
}

.sidebar-header {
    padding: 16px;
    display: flex;
    align-items: center;
    gap: 10px;
    border-bottom: 1px solid #2d2d2d;
}

.logo {
    color: #10a37f;
    display: flex;
    align-items: center;
}

.title {
    font-size: 16px;
    font-weight: 600;
    flex: 1;
}

.new-chat-btn {
    background: transparent;
    border: 1px solid #4d4d4f;
    color: #fff;
    padding: 6px 10px;
    border-radius: 6px;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: 13px;
    transition: background-color 0.2s;
}

.new-chat-btn:hover {
    background-color: #2d2d2d;
}

.session-list {
    flex: 1;
    overflow-y: auto;
    padding: 8px;
}

.session-item {
    padding: 12px;
    border-radius: 8px;
    cursor: pointer;
    margin-bottom: 4px;
    position: relative;
    transition: background-color 0.2s;
}

.session-item:hover {
    background-color: #2d2d2d;
}

.session-item.active {
    background-color: #343541;
}

.session-first-msg {
    font-size: 14px;
    color: #ececf1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-bottom: 6px;
}

.session-meta {
    font-size: 12px;
    color: #8e8ea0;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.session-model {
    background-color: #343541;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 11px;
}

.delete-btn {
    background: none;
    border: none;
    color: #8e8ea0;
    cursor: pointer;
    font-size: 16px;
    padding: 0 4px;
    opacity: 0;
    transition: opacity 0.2s;
}

.session-item:hover .delete-btn {
    opacity: 1;
}

.delete-btn:hover {
    color: #fff;
}

.loading {
    text-align: center;
    color: #8e8ea0;
    padding: 20px;
}

/* ========== 主区域 ========== */
.main-content {
    flex: 1;
    display: flex;
    flex-direction: column;
    background-color: #ffffff;
    position: relative;
}

.welcome-section {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 20px;
    color: #6e6e80;
}

.welcome-icon {
    color: #10a37f;
}

.welcome-text {
    font-size: 16px;
    text-align: center;
    max-width: 400px;
    line-height: 1.6;
}

.primary-btn {
    background-color: #10a37f;
    color: #fff;
    border: none;
    padding: 10px 24px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 500;
    transition: background-color 0.2s;
}

.primary-btn:hover:not(:disabled) {
    background-color: #0d8a6c;
}

.primary-btn:disabled {
    background-color: #a8d8cd;
    cursor: not-allowed;
}

.secondary-btn {
    background-color: #f0f0f0;
    color: #333;
    border: none;
    padding: 10px 24px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 14px;
    transition: background-color 0.2s;
}

.secondary-btn:hover {
    background-color: #e5e5e5;
}

.chat-section {
    flex: 1;
    display: flex;
    flex-direction: column;
    height: 100%;
}

.messages-container {
    flex: 1;
    overflow-y: auto;
    padding: 20px 0;
    height: calc(85vh - 60px);
}

.message {
    max-width: 800px;
    margin: 0 auto 24px;
    padding: 0 20px;
    display: flex;
    gap: 16px;
}

.message.user {
    flex-direction: row-reverse;
}

.message-avatar {
    width: 36px;
    height: 36px;
    border-radius: 6px;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 600;
    font-size: 14px;
}

.message.assistant .message-avatar {
    background-color: #10a37f;
    color: #fff;
}

.message.user .message-avatar {
    background-color: #e5e5e5;
    color: #333;
}

.message-content {
    flex: 1;
    min-width: 0;
}

.message-text {
    line-height: 1.6;
    word-wrap: break-word;
}

.message-text p {
    margin-bottom: 12px;
}

.message-text p:last-child {
    margin-bottom: 0;
}

.message-text pre {
    background-color: #f6f8fa;
    border-radius: 6px;
    padding: 12px;
    overflow-x: auto;
    margin: 12px 0;
    position: relative;
}

.message-text code {
    font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
    font-size: 13px;
}

.message-text pre code {
    background: none;
    padding: 0;
}

.message-text :not(pre) > code {
    background-color: #f0f2f5;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 13px;
}

.copy-btn {
    position: absolute;
    top: 8px;
    right: 8px;
    background: #fff;
    border: 1px solid #d0d7de;
    border-radius: 4px;
    padding: 4px 8px;
    cursor: pointer;
    font-size: 12px;
    opacity: 0;
    transition: opacity 0.2s;
}

.message-text pre:hover .copy-btn {
    opacity: 1;
}

.message-time {
    font-size: 12px;
    color: #999;
    margin-top: 6px;
}

.message.user .message-time {
    text-align: right;
}

/* ========== 输入区域 ========== */
.input-area {
    border-top: 1px solid #e5e5e5;
    padding: 16px 20px;
    background-color: #fff;
}

.input-wrapper {
    max-width: 800px;
    margin: 0 auto;
    position: relative;
    border: 1px solid #d0d0d5;
    border-radius: 12px;
    background-color: #fff;
    transition: border-color 0.2s;
}

.input-wrapper:focus-within {
    border-color: #10a37f;
    box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.1);
}

.message-input {
    width: 100%;
    border: none;
    outline: none;
    resize: none;
    padding: 12px 50px 12px 16px;
    font-size: 14px;
    font-family: inherit;
    line-height: 1.5;
    max-height: 200px;
    border-radius: 12px;
    background: transparent;
}

.send-btn {
    position: absolute;
    right: 8px;
    bottom: 8px;
    width: 32px;
    height: 32px;
    border-radius: 6px;
    border: none;
    background-color: #10a37f;
    color: #fff;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background-color 0.2s;
}

.send-btn:hover:not(:disabled) {
    background-color: #0d8a6c;
}

.send-btn:disabled {
    background-color: #d0d0d5;
    cursor: not-allowed;
}

.char-count {
    max-width: 800px;
    margin: 6px auto 0;
    text-align: right;
    font-size: 12px;
    color: #999;
}

/* ========== 模型弹窗 ========== */
.modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
}

.modal-content {
    background-color: #fff;
    border-radius: 12px;
    width: 600px;
    max-height: 80vh;
    display: flex;
    flex-direction: column;
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}

.modal-content h3 {
    padding: 20px 24px;
    border-bottom: 1px solid #e5e5e5;
    font-size: 18px;
    font-weight: 600;
}

.model-grid {
    flex: 1;
    overflow-y: auto;
    padding: 20px 24px;
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
}

.model-card {
    border: 1px solid #d0d0d5;
    border-radius: 8px;
    padding: 16px;
    cursor: pointer;
    transition: all 0.2s;
    position: relative;
}

.model-card:hover {
    border-color: #10a37f;
}

.model-card.selected {
    border-color: #10a37f;
    background-color: rgba(16, 163, 127, 0.05);
}

.model-card input[type="radio"] {
    position: absolute;
    top: 16px;
    right: 16px;
    accent-color: #10a37f;
}

.model-name {
    font-weight: 600;
    margin-bottom: 8px;
    padding-right: 24px;
}

.model-desc {
    font-size: 13px;
    color: #6e6e80;
    line-height: 1.5;
}

.modal-actions {
    padding: 16px 24px;
    border-top: 1px solid #e5e5e5;
    display: flex;
    justify-content: flex-end;
    gap: 12px;
}

/* 滚动条美化 */
::-webkit-scrollbar {
    width: 6px;
}
::-webkit-scrollbar-track {
    background: transparent;
}
::-webkit-scrollbar-thumb {
    background-color: #c1c1c1;
    border-radius: 3px;
}
.sidebar ::-webkit-scrollbar-thumb {
    background-color: #565869;
}

2.3 app.js

// ========== 后端服务基础配置 ==========
const BASE_URL = 'http://192.168.100.238:8080';
const MAX_CHARS = 2000;

// ========== 全局状态变量 ==========
let currentSessionId = null;
let selectedModel = null;
let isSending = false;

// ========== DOM节点缓存 ==========
const elements = {
    sessionList: document.getElementById('sessionList'),
    newChatBtn: document.getElementById('newChatBtn'),
    welcomeNewChatBtn: document.getElementById('welcomeNewChatBtn'),
    welcomeSection: document.getElementById('welcomeSection'),
    chatSection: document.getElementById('chatSection'),
    messagesContainer: document.getElementById('messagesContainer'),
    messageInput: document.getElementById('messageInput'),
    sendBtn: document.getElementById('sendBtn'),
    charCount: document.getElementById('charCount'),
    modelModal: document.getElementById('modelModal'),
    modelGrid: document.getElementById('modelGrid'),
    confirmBtn: document.getElementById('confirmBtn'),
    cancelBtn: document.getElementById('cancelBtn')
};

// ========== 页面初始化入口 ==========
document.addEventListener('DOMContentLoaded', init);

function init() {
    // marked Markdown解析配置,对接highlight.js做代码高亮
    marked.setOptions({
        highlight: function (code, lang) {
            if (lang && hljs.getLanguage(lang)) {
                try {
                    return hljs.highlight(code, {language: lang}).value;
                } catch (e) {
                }
            }
            return code;
        },
        breaks: true
    });
    loadSessions();
    bindEvents();
}

// ========== 事件绑定 ==========
function bindEvents() {
    elements.newChatBtn.addEventListener('click', openModelModal);
    elements.welcomeNewChatBtn.addEventListener('click', openModelModal);

    elements.cancelBtn.addEventListener('click', closeModelModal);
    elements.confirmBtn.addEventListener('click', handleCreateSession);

    elements.sendBtn.addEventListener('click', handleSendMessage);
    elements.messageInput.addEventListener('keydown', handleInputKeydown);
    elements.messageInput.addEventListener('input', handleInputChange);

    // 点击遮罩层关闭弹窗
    elements.modelModal.addEventListener('click', (e) => {
        if (e.target === elements.modelModal) {
            closeModelModal();
        }
    });
}

// ========== API请求封装 ==========
/**
 * 获取全部会话列表
 */
async function fetchSessions() {
    const res = await fetch(`${BASE_URL}/api/sessions`);
    const data = await res.json();
    if (data.success) {
        return data.data;
    }
    throw new Error(data.message || '获取会话列表失败');
}

/**
 * 获取后端支持模型列表
 */
async function fetchModels() {
    const res = await fetch(`${BASE_URL}/api/models`);
    const data = await res.json();
    if (data.success) {
        return data.data;
    }
    throw new Error(data.message || '获取模型列表失败');
}

/**
 * 创建会话
 * @param {string} model 模型名称
 */
async function createSession(model) {
    const res = await fetch(`${BASE_URL}/api/session`, {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({model})
    });
    const data = await res.json();
    if (data.success) {
        return data.data;
    }
    throw new Error(data.message || '创建会话失败');
}

/**
 * 删除指定会话
 * @param {string} sessionId
 */
async function deleteSession(sessionId) {
    const res = await fetch(`${BASE_URL}/api/session/${sessionId}`, {
        method: 'DELETE'
    });
    const data = await res.json();
    if (!data.success) {
        throw new Error(data.message || '删除会话失败');
    }
}

/**
 * 获取会话历史消息
 * @param {string} sessionId
 */
async function fetchSessionHistory(sessionId) {
    const res = await fetch(`${BASE_URL}/api/session/${sessionId}/history`);
    const data = await res.json();
    if (data.success) {
        return data.data;
    }
    throw new Error(data.message || '获取会话历史失败');
}

/**
 * SSE流式发送消息,XMLHttpRequest实现SSE分片解析
 * @param {string} sessionId
 * @param {string} message 用户输入消息
 * @param {Function} onMessage 分片回调
 * @param {Function} onDone 结束/异常回调
 */
function sendMessageStream(sessionId, message, onMessage, onDone) {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', `${BASE_URL}/api/message/async`);
    xhr.setRequestHeader('Content-Type', 'application/json');

    let received = '';
    let lastPos = 0;

    xhr.onprogress = function () {
        received = xhr.responseText;
        const newData = received.slice(lastPos);
        lastPos = received.length;
        const lines = newData.split('\n\n');
        for (const line of lines) {
            if (line.startsWith('data: ')) {
                const content = line.slice(6);
                if (content === '[DONE]') {
                    onDone && onDone(null);
                    return;
                }
                onMessage && onMessage(content);
            }
        }
    };

    xhr.onload = function () {
        if (xhr.status !== 200) {
            onDone && onDone(new Error(`HTTP ${xhr.status} 请求失败`));
        }
    };

    xhr.onerror = function () {
        onDone && onDone(new Error('网络请求异常'));
    };

    xhr.send(JSON.stringify({session_id: sessionId, message}));
}

// ========== 业务逻辑函数 ==========
/**
 * 加载会话列表并渲染侧边栏
 */
async function loadSessions() {
    try {
        const sessions = await fetchSessions();
        renderSessions(sessions);
    } catch (e) {
        elements.sessionList.innerHTML = '<div class="loading">加载失败</div>';
        console.error(e);
    }
}

/**
 * 渲染会话侧边栏
 * @param {Array} sessions
 */
function renderSessions(sessions) {
    if (sessions.length === 0) {
        elements.sessionList.innerHTML = '<div class="loading">暂无会话</div>';
        return;
    }
    // 更新时间倒序排序
    sessions.sort((a, b) => b.updated_at - a.updated_at);

    elements.sessionList.innerHTML = sessions.map(session => `
        <div class="session-item ${session.id === currentSessionId ? 'active' : ''}" data-id="${escapeHtml(session.id)}">
            <div class="session-first-msg">${escapeHtml(session.first_user_message || '新对话')}</div>
            <div class="session-meta">
                <span class="session-model">${escapeHtml(session.model)}</span>
                <span>${formatTime(session.updated_at)}</span>
                <button class="delete-btn" data-id="${escapeHtml(session.id)}" title="删除">…</button>
            </div>
        </div>
    `).join('');

    // 会话点击切换事件
    document.querySelectorAll('.session-item').forEach(item => {
        item.addEventListener('click', (e) => {
            if (e.target.classList.contains('delete-btn')) return;
            const id = item.dataset.id;
            switchToSession(id);
        });
    });

    // 删除会话按钮事件
    document.querySelectorAll('.delete-btn').forEach(btn => {
        btn.addEventListener('click', async (e) => {
            e.stopPropagation();
            const id = btn.dataset.id;
            if (confirm('确定要删除这个会话吗?')) {
                try {
                    await deleteSession(id);
                    if (currentSessionId === id) {
                        switchToWelcome();
                    }
                    loadSessions();
                } catch (err) {
                    alert('删除失败:' + err.message);
                }
            }
        });
    });
}

/**
 * 打开模型选择弹窗,加载模型列表
 */
async function openModelModal() {
    elements.modelModal.style.display = 'flex';
    elements.modelGrid.innerHTML = '<div class="loading">加载中...</div>';
    elements.confirmBtn.disabled = true;
    selectedModel = null;
    try {
        const models = await fetchModels();
        renderModels(models);
    } catch (e) {
        elements.modelGrid.innerHTML = '<div class="loading">加载失败</div>';
        console.error(e);
    }
}

/**
 * 渲染模型卡片网格
 * @param {Array} models
 */
function renderModels(models) {
    elements.modelGrid.innerHTML = models.map(model => `
        <div class="model-card" data-name="${escapeHtml(model.name)}">
            <input type="radio" name="model" value="${escapeHtml(model.name)}">
            <div class="model-name">${escapeHtml(model.name)}</div>
            <div class="model-desc">${escapeHtml(model.desc)}</div>
        </div>
    `).join('');

    document.querySelectorAll('.model-card').forEach(card => {
        card.addEventListener('click', () => {
            document.querySelectorAll('.model-card').forEach(c => c.classList.remove('selected'));
            card.classList.add('selected');
            card.querySelector('input').checked = true;
            selectedModel = card.dataset.name;
            elements.confirmBtn.disabled = false;
        });
    });
}

/**
 * 关闭模型弹窗
 */
function closeModelModal() {
    elements.modelModal.style.display = 'none';
}

/**
 * 确认创建会话
 */
async function handleCreateSession() {
    if (!selectedModel) return;
    elements.confirmBtn.disabled = true;
    elements.confirmBtn.textContent = '创建中...';
    try {
        const result = await createSession(selectedModel);
        currentSessionId = result.sessionid;
        closeModelModal();
        switchToChat();
        loadSessions();
    } catch (e) {
        alert('创建会话失败:' + e.message);
    } finally {
        elements.confirmBtn.disabled = false;
        elements.confirmBtn.textContent = '确认创建对话';
    }
}

/**
 * 切换到指定会话,加载历史消息
 * @param {string} sessionId
 */
async function switchToSession(sessionId) {
    currentSessionId = sessionId;
    switchToChat();
    loadSessions();
    elements.messagesContainer.innerHTML = '<div class="loading">加载中...</div>';
    try {
        const history = await fetchSessionHistory(sessionId);
        renderHistory(history);
    } catch (e) {
        elements.messagesContainer.innerHTML = '<div class="loading">加载历史消息失败</div>';
        console.error(e);
    }
}

/**
 * 渲染会话全部历史消息
 * @param {Array} messages
 */
function renderHistory(messages) {
    elements.messagesContainer.innerHTML = '';
    for (const msg of messages) {
        addMessage(msg.role, msg.content, msg.timestamp);
    }
    scrollToBottom();
}

/**
 * 切换聊天界面
 */
function switchToChat() {
    elements.welcomeSection.style.display = 'none';
    elements.chatSection.style.display = 'flex';
    elements.messageInput.focus();
}

/**
 * 切回欢迎首页
 */
function switchToWelcome() {
    currentSessionId = null;
    elements.welcomeSection.style.display = 'flex';
    elements.chatSection.style.display = 'none';
    loadSessions();
}

/**
 * 发送消息主逻辑
 */
async function handleSendMessage() {
    if (isSending) return;
    const message = elements.messageInput.value.trim();
    if (!message || !currentSessionId) return;

    isSending = true;
    elements.sendBtn.disabled = true;
    elements.messageInput.disabled = true;

    // 渲染用户消息
    addMessage('user', message, Date.now());
    elements.messageInput.value = '';
    updateCharCount();

    // AI消息占位容器
    const aiMsgElement = addMessage('assistant', '', Date.now());
    const msgContentEl = aiMsgElement.querySelector('.message-text');
    let fullContent = '';

    sendMessageStream(
        currentSessionId,
        message,
        (chunk) => {
            fullContent += chunk;
            msgContentEl.innerHTML = renderMarkdown(fullContent);
            addCopyButtons(msgContentEl);
            scrollToBottom();
        },
        (err) => {
            isSending = false;
            elements.sendBtn.disabled = false;
            elements.messageInput.disabled = false;
            elements.messageInput.focus();
            if (err) {
                msgContentEl.innerHTML += `<p style="color: red;">出错了:${escapeHtml(err.message)}</p>`;
            }
            const timeEl = aiMsgElement.querySelector('.message-time');
            if (timeEl) {
                timeEl.textContent = formatTime(Date.now());
            }
            loadSessions();
        }
    );
    scrollToBottom();
}

/**
 * 添加单条消息DOM
 * @param {string} role user / assistant
 * @param {string} content
 * @param {number} timestamp
 * @returns {HTMLElement}
 */
function addMessage(role, content, timestamp) {
    const msgDiv = document.createElement('div');
    msgDiv.className = `message ${role}`;
    const avatar = role === 'assistant' ? 'AI' : '我';
    msgDiv.innerHTML = `
        <div class="message-avatar">${avatar}</div>
        <div class="message-content">
            <div class="message-text">${renderMarkdown(content)}</div>
            <div class="message-time">${formatTime(timestamp)}</div>
        </div>
    `;
    elements.messagesContainer.appendChild(msgDiv);
    addCopyButtons(msgDiv);
    scrollToBottom();
    return msgDiv;
}

/**
 * Markdown渲染,异常降级转义HTML
 * @param {string} text
 * @returns {string}
 */
function renderMarkdown(text) {
    if (!text) return '';
    try {
        return marked.parse(text);
    } catch (e) {
        return escapeHtml(text);
    }
}

/**
 * 给pre代码块附加复制按钮
 * @param {HTMLElement} container
 */
function addCopyButtons(container) {
    const pres = container.querySelectorAll('pre');
    pres.forEach(pre => {
        if (pre.querySelector('.copy-btn')) return;
        const btn = document.createElement('button');
        btn.className = 'copy-btn';
        btn.textContent = '复制';
        btn.addEventListener('click', () => {
            const code = pre.querySelector('code');
            if (code) {
                navigator.clipboard.writeText(code.innerText).then(() => {
                    btn.textContent = '已复制';
                    setTimeout(() => btn.textContent = '复制', 2000);
                });
            }
        });
        pre.appendChild(btn);
    });
}

/**
 * 输入框键盘事件 Enter发送 Shift+Enter换行
 * @param {KeyboardEvent} e
 */
function handleInputKeydown(e) {
    if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        handleSendMessage();
    }
}

/**
 * 输入框内容变化,更新字数、自动高度
 */
function handleInputChange() {
    updateCharCount();
    elements.sendBtn.disabled = !elements.messageInput.value.trim() || isSending;
    elements.messageInput.style.height = 'auto';
    elements.messageInput.style.height = Math.min(elements.messageInput.scrollHeight, 200) + 'px';
}

/**
 * 更新输入框字数统计
 */
function updateCharCount() {
    const len = elements.messageInput.value.length;
    elements.charCount.textContent = len;
}

/**
 * 消息容器滚动到底部
 */
function scrollToBottom() {
    elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight;
}

// ========== 通用工具函数 ==========
/**
 * 时间格式化,毫秒时间戳转友好中文时间
 * @param {number} timestamp
 * @returns {string}
 */
function formatTime(timestamp) {
    const date = new Date(timestamp);
    const now = new Date();
    // 今天
    if (date.toDateString() === now.toDateString()) {
        return date.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
    }
    // 昨天
    const yesterday = new Date(now);
    yesterday.setDate(yesterday.getDate() - 1);
    if (date.toDateString() === yesterday.toDateString()) {
        return '昨天 ' + date.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
    }
    // 本年度
    if (date.getFullYear() === now.getFullYear()) {
        return date.toLocaleDateString('zh-CN', {month: '2-digit', day: '2-digit'}) + ' ' +
            date.toLocaleTimeString('zh-CN', {hour: '2-digit', minute: '2-digit'});
    }
    return date.toLocaleDateString('zh-CN');
}

/**
 * HTML特殊字符转义,防止XSS
 * @param {string} str
 * @returns {string}
 */
function escapeHtml(str) {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
}

3 ~> Trae AI 生成前端页面提示词工程

3.1 提示词编写原则

  • 将完整接口定义、页面交互需求、样式约束全部写入文本文件Chat前端页面需求说明.txt
  • 在 Trae Builder 对话窗口,使用#引用 File 类型的需求说明 txt 文件,选择 DeepSeek 模型,Auto 模式生成代码
  • 优势:避免对话框输入字数限制,完整传递全部需求,便于修改维护需求文档,无需重复粘贴大段提示词

3.2 后端 C++ httplib 静态资源挂载关键代码片段

// 设置http路由
sethttpRoutes();
// 挂载www目录为web静态资源根目录
// 访问 http://ip:port 自动映射 index.html
_chaterver->set_mount_point("/", "./www");

// 后台子线程运行http服务,不阻塞主线程
std::thread serverThread([this](){
    _chaterver->listen(_config.host, _config.port);
    INFO("ChatServer start on {}:{}",_config.host,_config.port);
});
serverThread.detach();
_isRunning.store(true);
INFO("ChatServer start success!!!");
  • 前端三个文件全部放置服务器./www目录
  • 访问路径:http://192.168.100.238:8080/index.html,简写http://192.168.100.238:8080等价访问 index.html

4 ~> 部署调试注意事项

4.1 跨域问题

  • 前端直接浏览器本地打开 html 文件会触发 CORS 跨域;优先通过后端 httplib 服务访问页面
  • 后端服务必须配置 CORS 响应头,允许前端页面发起 fetch / XMLHttpRequest 请求

4.2 SSE 流式解析

  • 使用XMLHttpRequest处理 SSE 分片,按\n\n分割事件块,识别data: 前缀,遇到[DONE]结束流式会话
  • 不要一次性等待 response 全部返回,依靠onprogress事件逐块处理输出

4.3 XSS 安全防护

  • 所有后端返回的原始文本,渲染 DOM 前使用escapeHtml()做 HTML 转义;Markdown 渲染由 marked 库处理。

4.4 第三方 CDN 依赖

  • marked、highlight.js 使用公共 CDN;离线部署场景需要将 js/css 下载放到 www 目录,修改 html 中引用路径。

4.5 输入校验

  • 前端限制最大输入字符 2000;后端接口同样要做字符长度校验,不能只依赖前端校验。

结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

艾莉丝努力练剑

C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主


👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。
❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。
⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。
💬 【评论】 分享你的经验或疑问,评论区一起交流避坑!

不要忘记给博主“一键四连”哦!

“今日练剑达成!”
文章配图

“技术之路难免有困惑,但同行的人会让前进更有方向。”

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主“一键四连”哦!

往期回顾:

【AI大模型接入SDK】ChatServer接口测试

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡
૮₍ ˶ ˊ ᴥ ˋ˶₎ა

在这里插入图片描述

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_89899187/article/details/169268084

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--