GPT对话UI--通义千问API

GPT对话UI

项目介绍

一个基于 GPT 的智能对话界面,提供简洁优雅的用户体验。本项目使用纯前端技术栈实现,无需后端服务器即可运行。

功能特点

  • 实时对话:支持与 AI 进行实时对话交互
  • 主题切换:支持浅色/深色主题,自动跟随系统设置
  • Markdown 渲染:AI 回复支持 Markdown 格式,包含代码高亮
  • 本地存储:对话历史保存在本地,刷新页面不会丢失
  • 智能推荐:每次对话后自动推荐相关问题
  • ⌨️ 快捷操作:支持快捷键发送消息,Shift+Enter 换行
  • 代码优化:支持代码块语法高亮和一键复制
  • 可配置:支持配置 API 密钥
  • 响应式:适配不同屏幕尺寸
  • 字体调节:支持动态调整界面字体大小

快速开始

  1. 克隆项目到本地
git clone https://gitee.com/anxwefndu/gpt-chat-ui.git
  1. 打开项目目录
cd gpt-chat-ui
  1. 在浏览器中打开 index.html 文件即可使用

使用说明

  1. 首次使用需要配置 API 密钥

    • 点击左侧边栏的"配置密钥"按钮
    • 输入你的 API 密钥
    • 点击保存即可使用
  2. 基本操作

    • 在输入框输入问题后点击发送或按回车键发送
    • 使用 Shift + Enter 可以在输入框换行
    • 点击推荐问题可以快速发送相关提问
    • 可以通过左侧的滑块调整字体大小
    • 点击主题按钮切换深色/浅色模式
  3. 历史记录

    • 所有对话记录会自动保存在本地
    • 可以通过"清除历史记录"按钮清空所有对话
    • 刷新页面不会丢失历史记录

技术栈

  • HTML5
  • CSS3 (Tailwind CSS)
  • JavaScript
  • Marked.js (Markdown 渲染)
  • Highlight.js (代码高亮)
  • Font Awesome (图标)

注意事项

  • 本项目需要有效的 API 密钥才能正常使用
  • 建议使用现代浏览器访问以获得最佳体验
  • 所有数据均存储在本地,清除浏览器数据会导致历史记录丢失
  • 目前文件上传这一块还没来得及加,后续添加上该块内容

源码下载

GPT对话UI

演示截图

1.系统首页
GPT对话UI--通义千问API_第1张图片

2.使用说明
GPT对话UI--通义千问API_第2张图片

3.配色API-key
GPT对话UI--通义千问API_第3张图片

4.系统首页-暗色
GPT对话UI--通义千问API_第4张图片

核心代码

index.html

DOCTYPE html>
<html lang="zh">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>AI 智能助手title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Pacifico&display=swap" rel="stylesheet">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  <script src="https://cdn.tailwindcss.com">script>
  <script src="./resource/marked.min.js">script>
  <link rel="stylesheet" href="./resource/github.min.css">
  <script src="./resource/highlight.min.js">script>
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#4A90E2',
            secondary: '#E3F2FD',
            'primary-dark': '#2B4C7E',
            'text-dark': '#E2E8F0'  // 暗色主题下的主要文字颜色
          },
          borderRadius: {
            'none': '0px',
            'sm': '2px',
            DEFAULT: '4px',
            'md': '8px',
            'lg': '12px',
            'xl': '16px',
            '2xl': '20px',
            '3xl': '24px',
            'full': '9999px',
            'button': '4px'
          }
        }
      },
      darkMode: 'class' // 启用暗色模式
    }
script>
  <style>
    /* 保留原有样式 */
    .message-bubble {
      max-width: 80%;
      margin: 8px 0;
      padding: 12px 16px;
      border-radius: 12px;
      width: fit-content;
    }

    .user-message {
      background-color: #E3F2FD;
      margin-left: auto;
      border-top-right-radius: 4px;
    }

    .ai-message {
      background-color: #F5F5F5;
      margin-right: auto;
      border-top-left-radius: 4px;
    }

    /* 自定义滚动条样式 */
    .custom-scrollbar::-webkit-scrollbar {
      width: 8px;
    }

    .custom-scrollbar::-webkit-scrollbar-track {
      background: #f1f1f1;
      border-radius: 4px;
    }

    .custom-scrollbar::-webkit-scrollbar-thumb {
      background: #c1c1c1;
      border-radius: 4px;
    }

    .custom-scrollbar::-webkit-scrollbar-thumb:hover {
      background: #a8a8a8;
    }

    /* 暗色主题样式 */
    .dark .message-bubble.ai-message {
      background-color: #2D3748;
    }

    .dark .message-bubble.user-message {
      background-color: #2B4C7E;
    }

    .dark .custom-scrollbar::-webkit-scrollbar-track {
      background: #1A202C;
    }

    .dark .custom-scrollbar::-webkit-scrollbar-thumb {
      background: #4A5568;
    }

    .dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
      background: #718096;
    }

    input[type="range"] {
      appearance: none;
      width: 100%;
      height: 4px;
      background: #E5E7EB;
      border-radius: 2px;
    }

    input[type="range"]::-webkit-slider-thumb {
      appearance: none;
      width: 16px;
      height: 16px;
      background: #4A90E2;
      border-radius: 50%;
      cursor: pointer;
    }

    /* 添加 Markdown 样式 */
    .markdown-body {
      font-size: 1em;
      line-height: 1.6;
    }

    .markdown-body h1,
    .markdown-body h2,
    .markdown-body h3,
    .markdown-body h4,
    .markdown-body h5,
    .markdown-body h6 {
      margin-top: 1.5em;
      margin-bottom: 0.5em;
      font-weight: 600;
    }

    .markdown-body code {
      color: #0f172a;
      padding: 0.2em 0.4em;
      font-size: 0.9em;
      background-color: rgba(175, 184, 193, 0.2);
      border-radius: 6px;
    }

    .markdown-body pre {
      padding: 16px;
      overflow: auto;
      border-radius: 6px;
      background-color: #f6f8fa;
      margin: 1em 0;
    }

    .markdown-body pre code {
      padding: 0;
      background-color: #f6f8fa;
      white-space: pre;
      font-size: 0.95em;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    }

    .dark .markdown-body pre {
      background-color: #f6f8fa;
    }

    /* 代码块内的复制按钮样式 */
    .copy-button {
      position: absolute;
      top: 8px;
      right: 8px;
      padding: 4px 8px;
      font-size: 12px;
      color: #64748b;
      background-color: #f1f5f9;
      border: 1px solid #e2e8f0;
      border-radius: 4px;
      opacity: 0;
      transition: all 0.2s ease;
    }

    .copy-button:hover {
      color: #0f172a;
      background-color: #e2e8f0;
      border-color: #cbd5e1;
    }

    /* 暗色主题下的复制按钮 */
    .dark .copy-button {
      color: #94a3b8;
      background-color: #1e293b;
      border-color: #334155;
    }

    .dark .copy-button:hover {
      color: #f1f5f9;
      background-color: #334155;
      border-color: #475569;
    }

    /* 鼠标悬停在代码块上时显示复制按钮 */
    .markdown-body pre:hover .copy-button {
      opacity: 1;
    }

    /* 代码高亮主题颜色优化 */
    .markdown-body code {
      padding: 0.2em 0.4em;
      font-size: 0.95em;
      background-color: #f1f5f9;
      border-radius: 4px;
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
    }

    .markdown-body ul,
    .markdown-body ol {
      padding-left: 2em;
      margin: 1em 0;
    }

    .markdown-body li {
      margin: 0.5em 0;
    }

    .markdown-body p {
      margin: 1em 0;
    }

    .markdown-body blockquote {
      padding: 0 1em;
      color: #57606a;
      border-left: 0.25em solid #d0d7de;
      margin: 1em 0;
    }

    .dark .markdown-body blockquote {
      color: #8b949e;
      border-left-color: #3b434b;
    }

    .markdown-body table {
      border-collapse: collapse;
      margin: 1em 0;
      width: 100%;
    }

    .markdown-body table th,
    .markdown-body table td {
      padding: 6px 13px;
      border: 1px solid #d0d7de;
    }

    .dark .markdown-body table th,
    .dark .markdown-body table td {
      border-color: #3b434b;
    }

    .markdown-body table tr:nth-child(2n) {
      background-color: #f6f8fa;
    }

    .dark .markdown-body table tr:nth-child(2n) {
      background-color: #161b22;
    }
  style>
head>

<body class="bg-white dark:bg-gray-900 transition-colors">
  <div class="flex h-screen">
    
    <aside class="w-64 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-700 flex flex-col">

      <div class="p-6 border-b border-gray-200">
        <h1 class="text-2xl font-['Pacifico'] text-primary">logoh1>
      div>

      <nav class="flex-1 p-6 space-y-6">
        <div class="space-y-2">
          <label class="text-sm font-medium text-gray-700 dark:text-gray-300">主题模式label>
          <button
            onclick="toggleTheme()"
            class="flex items-center justify-between w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 rounded-button hover:bg-gray-100 dark:hover:bg-gray-600">
            <span id="themeText">浅色模式span>
            <i class="fas fa-sun dark:fa-moon text-primary">i>
          button>
        div>

        <div class="space-y-2">
          <label class="text-sm font-medium text-gray-700 dark:text-gray-300">字体大小label>
          <div class="flex items-center space-x-2">
            <span class="text-xs text-gray-500 dark:text-gray-400">span>
            <input type="range" min="1" max="3" value="2" class="w-full">
            <span class="text-xs text-gray-500 dark:text-gray-400">span>
          div>
        div>

        <button
          class="flex items-center w-full px-4 py-2 text-sm text-white bg-primary dark:bg-primary-dark rounded-button hover:bg-primary/90 dark:hover:bg-primary-dark/80 whitespace-nowrap">
          <i class="fas fa-trash-alt mr-2">i>
          清除历史记录
        button>

        <button onclick="showHelp()"
          class="flex items-center w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 rounded-button hover:bg-gray-100 dark:hover:bg-gray-700 whitespace-nowrap">
          <i class="fas fa-question-circle mr-2">i>
          使用帮助
        button>

        <button onclick="showConfig()"
                class="flex items-center w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 rounded-button hover:bg-gray-100 dark:hover:bg-gray-700 whitespace-nowrap">
          <i class="fas fa-cog mr-2">i>
          配置密钥
        button>
      nav>
    aside>

    <main class="flex-1 flex flex-col">
      <div class="flex-1 overflow-y-auto p-6 space-y-4 custom-scrollbar">div>

      <div class="border-t border-gray-200 dark:border-gray-700 p-6">
        <div class="relative">
          <textarea
            class="w-full h-24 px-4 py-3 text-gray-700 dark:text-text-dark border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 rounded-lg focus:outline-none focus:border-primary resize-none"
            placeholder="输入你的问题...">textarea>
          <div class="absolute bottom-3 right-3 flex space-x-2">
            <button class="p-2 text-gray-400 hover:text-primary dark:hover:text-primary">
              <i class="fas fa-paperclip">i>
            button>
            <button class="px-4 py-2 bg-primary dark:bg-primary-dark text-white rounded-button hover:bg-primary/90 dark:hover:bg-primary-dark/80 whitespace-nowrap">
              发送 <i class="fas fa-paper-plane ml-2">i>
            button>
          div>
        div>
        <div class="mt-2 flex flex-wrap gap-2">
          <span class="px-3 py-1 text-sm text-primary dark:text-gray-300 bg-secondary dark:bg-gray-800 rounded-full cursor-pointer hover:bg-primary hover:text-white dark:hover:bg-primary-dark">
            如何优化代码?
          span>
          <span class="px-3 py-1 text-sm text-primary bg-secondary dark:bg-gray-700 rounded-full cursor-pointer hover:bg-primary hover:text-white">
            写一篇营销文案
          span>
          <span class="px-3 py-1 text-sm text-primary bg-secondary dark:bg-gray-700 rounded-full cursor-pointer hover:bg-primary hover:text-white">
            数据分析方法
          span>
        div>
      div>
    main>
  div>

  <script>
    function toggleTheme() {
      const html = document.documentElement;
      const themeText = document.getElementById('themeText');
      const isDark = html.classList.toggle('dark');
      themeText.textContent = isDark ? '深色模式' : '浅色模式';
      localStorage.setItem('theme', isDark ? 'dark' : 'light');
    }

    // 初始化主题
    if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark');
      document.getElementById('themeText').textContent = '深色模式';
    }

    function callGpt(questionText, onProgress, onDone) {
      const sk = loadSK();

      // 获取历史消息记录
      const messages = loadMessages();
      // 构建消息历史
      const messageHistory = messages.map(msg => ({
        role: msg.role === 'user' ? 'user' : 'assistant',
        content: msg.content
      }));

      // 添加当前问题
      messageHistory.push({
        role: 'user',
        content: questionText
      });

      fetch("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", {
        method: "post",
        headers: {
          "Content-Type": "application/json",
          "Authorization": `Bearer ${sk}`
        },
        body: JSON.stringify({
          model: "qwen-plus",
          messages: messageHistory,  // 使用完整的消息历史
          stream: true
        }),
      }).then(response => {
        const reader = response.body.getReader();
        const decoder = new TextDecoder('utf-8');
        let fullText = "";

        function read() {
          reader.read().then(({done, value}) => {
            if (done) {
              onDone(fullText);
              return;
            }
            const text = decoder.decode(value, {stream: true});
            const chunks = text.replace("data: [DONE]", "").replace("data: ", "").split("data: ");
            for (let i = 0; i < chunks.length; i++) {
              try {
                const obj = JSON.parse(chunks[i]);
                const choices = obj.choices;
                const delta = choices[0].delta;
                fullText += delta.content;

                // 调用回调函数,实时更新内容
                onProgress(fullText);
              } catch (e) {
                console.log(chunks[i])
                read();
              }
            }

            read();
          }).catch(error => {
            console.log(error);
            alert("工具处理出错");
          });
        }

        read();
      }).catch(error => {
        console.log(error);
        alert("工具处理出错:" + error);
      });
    }

    // 消息记录管理
    const MESSAGE_STORAGE_KEY = 'chat_messages';

    function loadMessages() {
      const stored = localStorage.getItem(MESSAGE_STORAGE_KEY);
      return stored ? JSON.parse(stored) : [];
    }

    function saveMessages(messages) {
      localStorage.setItem(MESSAGE_STORAGE_KEY, JSON.stringify(messages));
    }

    function appendMessage(role, content) {
      const messages = loadMessages();
      messages.push({
        role,
        content,
        timestamp: new Date().toLocaleTimeString('zh', { hour: '2-digit', minute: '2-digit' })
      });
      saveMessages(messages);
      renderMessages();
    }

    function clearMessages() {
      localStorage.removeItem(MESSAGE_STORAGE_KEY);
      renderMessages();
    }

    // 配置 marked 选项
    marked.setOptions({
      highlight: function(code, lang) {
        if (lang && hljs.getLanguage(lang)) {
          return hljs.highlight(code, { language: lang }).value;
        }
        return code;
      },
      breaks: true,
      gfm: true
    });

    function renderMessages() {
      const messagesContainer = document.querySelector('.custom-scrollbar');
      const messages = loadMessages();

      messagesContainer.innerHTML = messages.map(msg => `
      
${msg.role === 'user' ? 'user-message' : 'ai-message'}">
${msg.role === 'user' ? msg.content : marked.parse(msg.content)}
${msg.timestamp}
`
).join(''); // 处理代码块的高亮和复制功能 const codeBlocks = messagesContainer.querySelectorAll('pre code'); codeBlocks.forEach(block => { // 应用代码高亮 hljs.highlightElement(block); // 添加复制按钮 const copyButton = document.createElement('button'); copyButton.className = 'copy-button'; copyButton.innerHTML = '复制'; copyButton.onclick = async () => { try { await navigator.clipboard.writeText(block.textContent); copyButton.innerHTML = '已复制'; setTimeout(() => { copyButton.innerHTML = '复制'; }, 2000); } catch (err) { console.error('复制失败:', err); } }; // 为代码块容器添加相对定位 const preBlock = block.parentElement; preBlock.style.position = 'relative'; preBlock.appendChild(copyButton); }); // 滚动到底部 messagesContainer.scrollTop = messagesContainer.scrollHeight; } // 发送消息 function sendMessage() { const textarea = document.querySelector('textarea'); const content = textarea.value.trim(); if (!content) return; const sk = loadSK(); if (!sk) { alert('请先配置 API 密钥'); showConfig(); return; } // 添加用户消息 appendMessage('user', content); textarea.value = ''; // 调用 GPT callGpt(content, (text) => { const messages = loadMessages(); if (messages[messages.length - 1].role === 'assistant') { messages[messages.length - 1].content = text; } else { messages.push({ role: 'assistant', content: text, timestamp: new Date().toLocaleTimeString('zh', {hour: '2-digit', minute: '2-digit'}) }); } saveMessages(messages); renderMessages(); }, (finalText) => { console.log('回复完成:', finalText); renderSuggestion(); }); } // 事件处理 document.addEventListener('DOMContentLoaded', () => { const textarea = document.querySelector('textarea'); const sendButton = document.querySelector('button:has(.fa-paper-plane)'); const clearButton = document.querySelector('button:has(.fa-trash-alt)'); const suggestionSpans = document.querySelectorAll('.mt-2.flex.flex-wrap.gap-2 span'); // 绑定事件 sendButton.addEventListener('click', sendMessage); textarea.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // 快捷提问 suggestionSpans.forEach(span => { span.addEventListener('click', () => { textarea.value = span.textContent.trim(); sendMessage(); }); }); // 初始化消息记录 renderMessages(); // 修改清除按钮事件 clearButton.addEventListener('click', confirmClear); renderSuggestion(); }); function showHelp() { const helpContent = `

使用说明

1. 在输入框中输入您的问题,点击发送按钮或按回车键发送

2. 支持快捷提问,点击下方预设的问题直接发送

3. 可以使用 Shift + Enter 在输入框中换行

4. 支持深色/浅色主题切换,可根据个人喜好选择

5. 支持调整字体大小,通过滑块可以设置小、中、大三种字号

6. 所有对话记录会保存在本地,刷新页面不会丢失

7. 清除历史记录会删除所有本地保存的对话

8. 需要配置 API 密钥才能使用对话功能

9. 每次对话完成后会自动推荐相关的后续问题

10. 支持实时显示 AI 回复内容

`
; const helpDiv = document.createElement('div'); helpDiv.id = 'helpModal'; helpDiv.className = 'fixed inset-0 flex items-center justify-center z-50'; helpDiv.innerHTML = helpContent; document.body.appendChild(helpDiv); } function closeHelp() { const helpModal = document.getElementById('helpModal'); if (helpModal) { helpModal.remove(); } } function confirmClear() { const confirmContent = `

确认清除

确定要清除所有对话记录吗?此操作不可恢复。

`
; const confirmDiv = document.createElement('div'); confirmDiv.id = 'confirmModal'; confirmDiv.className = 'fixed inset-0 flex items-center justify-center z-50'; confirmDiv.innerHTML = confirmContent; document.body.appendChild(confirmDiv); } function closeConfirm() { const confirmModal = document.getElementById('confirmModal'); if (confirmModal) { confirmModal.remove(); } } function clearAndClose() { clearMessages(); closeConfirm(); } function renderSuggestion() { const messages = loadMessages(); // 获取最近的对话内容 if (messages.length > 0) { const recentMessages = messages.slice(-3).map(msg => msg.content).join('\n'); getSuggestions(recentMessages, (suggestions) => { const suggestionContainer = document.querySelector('.mt-2.flex.flex-wrap.gap-2'); suggestionContainer.innerHTML = suggestions.map(suggestion => ` ${suggestion} `).join(''); // 重新绑定事件 const suggestionSpans = document.querySelectorAll('.mt-2.flex.flex-wrap.gap-2 span'); suggestionSpans.forEach(span => { span.addEventListener('click', () => { document.querySelector('textarea').value = span.textContent.trim(); sendMessage(); }); }); }); } } function getSuggestions(context, callback) { const sk = loadSK(); if (!sk) { callback(['如何继续优化这个方案?', '有什么相关的最佳实践?', '还有其他方面需要考虑吗?']); return; } fetch("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", { method: "post", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${sk}` }, body: JSON.stringify({ model: "qwen-plus", messages: [ { role: "user", content: `基于以下对话内容,推荐3个相关的后续提问,直接返回3个问题,用|||分隔:\n${context}` } ], stream: false }), }).then(response => response.json()).then(data => { const suggestions = data.choices[0].message.content.split('|||').map(q => q.trim()); callback(suggestions); }).catch(error => { console.error('获取建议问题失败:', error); // 发生错误时使用默认建议 callback(['如何继续优化这个方案?', '有什么相关的最佳实践?', '还有其他方面需要考虑吗?']); }); } const SK_STORAGE_KEY = 'chat_sk'; function loadSK() { return localStorage.getItem(SK_STORAGE_KEY) || ''; } function saveSK(sk) { localStorage.setItem(SK_STORAGE_KEY, sk); } function showConfig() { const currentSK = loadSK(); const configContent = `

配置 API 密钥

${currentSK}" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary focus:border-primary dark:bg-gray-700 dark:text-white" placeholder="请输入你的 API 密钥">
请妥善保管你的 API 密钥,不要泄露给他人。
`
; const configDiv = document.createElement('div'); configDiv.id = 'configModal'; configDiv.className = 'fixed inset-0 flex items-center justify-center z-50'; configDiv.innerHTML = configContent; document.body.appendChild(configDiv); } function closeConfig() { const configModal = document.getElementById('configModal'); if (configModal) { configModal.remove(); } } function saveConfig() { const skInput = document.getElementById('skInput'); const sk = skInput.value.trim(); if (!sk) { alert('请输入有效的 API 密钥'); return; } saveSK(sk); closeConfig(); } const FONT_SIZE_KEY = 'chat_font_size'; function loadFontSize() { return localStorage.getItem(FONT_SIZE_KEY) || '2'; } function saveFontSize(size) { localStorage.setItem(FONT_SIZE_KEY, size); } function updateFontSize(size) { const html = document.documentElement; switch(size) { case '1': html.style.fontSize = '14px'; break; case '2': html.style.fontSize = '16px'; break; case '3': html.style.fontSize = '18px'; break; } } document.addEventListener('DOMContentLoaded', () => { // 初始化字体大小 const fontSizeSlider = document.querySelector('input[type="range"]'); fontSizeSlider.value = loadFontSize(); updateFontSize(fontSizeSlider.value); // 监听字体大小变化 fontSizeSlider.addEventListener('change', (e) => { const size = e.target.value; saveFontSize(size); updateFontSize(size); }); });
script> body> html>

你可能感兴趣的:(gpt,ui)