Browse Source

first commit

zhangwl 1 day ago
commit
1d468bfe17

+ 1 - 0
chat-ai-ui-main/.env

@@ -0,0 +1 @@
+VITE_API_URL=http://localhost:8000

+ 19 - 0
chat-ai-ui-main/README.md

@@ -0,0 +1,19 @@
+# Chat AI 用户界面
+
+这是 Chat AI 应用的前端部分。它是一个基于 **Vue.js 3** 的应用程序,通过自定义 API 与 [Stream Chat](https://getstream.io)、您的 [Neon](https://neon.tech) PostgreSQL 数据库和 [Open AI](https://platform.openai.com) 进行交互。
+
+<img src="./src/assets/screen.png" />
+
+该应用的 Express 后端 API 可在[此处](https://github.com/bradtraversy/chat-ai-api)找到。
+
+## 安装说明
+
+1. 克隆仓库
+2. 运行 `npm install`
+3. 在根目录创建 `.env` 文件并添加以下环境变量:
+
+```
+VITE_API_URL=http://localhost:8000
+```
+
+4. Run the server with `npm run dev` and open on `http://localhost:5173/`

BIN
chat-ai-ui-main/favicon.ico


+ 14 - 0
chat-ai-ui-main/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="icon" type="image/ico" href="/favicon.ico" />
+    <title>Chat AI</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

File diff suppressed because it is too large
+ 3321 - 0
chat-ai-ui-main/package-lock.json


+ 30 - 0
chat-ai-ui-main/package.json

@@ -0,0 +1,30 @@
+{
+  "name": "chat-ai-ui",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc -b && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "axios": "^1.8.4",
+    "highlight.js": "^11.11.1",
+    "marked": "^12.0.2",
+    "marked-highlight": "^2.2.2",
+    "pinia": "^3.0.1",
+    "pinia-plugin-persistedstate": "^4.2.0",
+    "vue": "^3.5.13",
+    "vue-router": "^4.5.0"
+  },
+  "devDependencies": {
+    "@tailwindcss/vite": "^4.0.17",
+    "@vitejs/plugin-vue": "^5.2.1",
+    "@vue/tsconfig": "^0.7.0",
+    "tailwindcss": "^4.0.17",
+    "typescript": "~5.7.2",
+    "vite": "^6.2.0",
+    "vue-tsc": "^2.2.4"
+  }
+}

File diff suppressed because it is too large
+ 1 - 0
chat-ai-ui-main/public/vite.svg


+ 7 - 0
chat-ai-ui-main/src/App.vue

@@ -0,0 +1,7 @@
+<script setup lang="ts"></script>
+
+<template>
+  <div>
+    <RouterView />
+  </div>
+</template>

BIN
chat-ai-ui-main/src/assets/robot.png


BIN
chat-ai-ui-main/src/assets/robot1.png


BIN
chat-ai-ui-main/src/assets/screen.png


+ 232 - 0
chat-ai-ui-main/src/components/ChatInput.vue

@@ -0,0 +1,232 @@
+<script setup lang="ts">
+import { ref } from 'vue';
+
+const message = ref('');
+const model =  ref("qwen-turbo-2025-04-28");
+const temperature = ref(0.7);
+const maxTokens = ref(2000);
+const stream = ref(false);
+const emit = defineEmits(['send']);
+
+// 可用的模型选项
+const models = [
+  { value: "qwen-turbo-2025-04-28", label: "Qwen Turbo" },
+  { value: "qwen-plus-2025-07-14", label: "Qwen Plus" },
+  { value: "qwen-flash", label: "Qwen Flash" }
+];
+
+/**
+ * 发送消息函数
+ * 
+ * 该函数用于发送用户输入的消息,并触发相应的事件
+ * 
+ * @param {string} message.value - 要发送的消息内容
+ * @param {string} model.value - 使用的模型参数
+ * @param {number} temperature.value - 温度参数,控制生成文本的随机性
+ * @param {number} maxTokens.value - 最大令牌数参数,限制生成文本的长度
+ * @param {boolean} stream.value - 是否启用流式传输
+ * 
+ * @returns {void}
+ */
+const sendMessage = () => {
+  // 检查消息是否为空,如果为空则直接返回
+  if (!message.value.trim()) return;
+  // 触发send事件,传递消息和相关参数
+  emit('send', message.value, model.value, temperature.value, maxTokens.value, stream.value);
+  // 清空消息输入框
+  message.value = '';
+};
+</script>
+<!-- bg-gray-800 -->
+<template>
+  <div class="p-4 space-y-4">
+    <!-- 第一行:流式输出、状态指示器、模型选择 -->
+    <div class="flex items-center space-x-6">
+      <!-- 流式输出开关 -->
+      <label class="flex items-center space-x-2 text-white cursor-pointer">
+        <input
+          v-model="stream"
+          type="checkbox"
+          class="rounded bg-gray-700 border-gray-600 text-blue-500 focus:ring-blue-500"
+        />
+        <span class="text-sm">流式输出</span>
+      </label>
+      
+      <!-- 状态指示器 -->
+      <div class="flex items-center space-x-1">
+        <div 
+          :class="[
+            'w-2 h-2 rounded-full',
+            stream ? 'bg-green-400' : 'bg-yellow-400'
+          ]"
+        ></div>
+        <span 
+          :class="[
+            'text-xs font-medium',
+            stream ? 'text-green-400' : 'text-yellow-400'
+          ]"
+        >
+          {{ stream ? '流式模式' : '完整模式' }}
+        </span>
+      </div>
+
+      <!-- 模型选择下拉框 -->
+      <div class="flex items-center space-x-2">
+        <label class="text-sm text-gray-300">模型:</label>
+        <select
+          v-model="model"
+          class="bg-gray-700 text-white text-sm rounded-lg border border-gray-600 px-3 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-[140px]"
+        >
+          <option 
+            v-for="model in models" 
+            :key="model.value" 
+            :value="model.value"
+            class="bg-gray-700"
+          >
+            {{ model.label }}
+          </option>
+        </select>
+      </div>
+
+      <!-- 模型显示 -->
+      <div class="flex items-center space-x-1 text-xs text-gray-400">
+        <span>当前:</span>
+        <span class="text-blue-400 font-medium">
+          {{ models.find(m => m.value === model)?.label }}
+        </span>
+      </div>
+    </div>
+
+    <!-- 第二行:温度和Token控制滑块 -->
+    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
+      <!-- 温度控制 -->
+      <div class="space-y-2">
+        <div class="flex items-center justify-between">
+          <label class="text-sm text-gray-300">温度 (Temperature)</label>
+          <span class="text-xs text-blue-400 font-mono bg-gray-700 px-2 py-1 rounded">
+            {{ temperature.toFixed(1) }}
+          </span>
+        </div>
+        <div class="relative">
+          <input
+            v-model.number="temperature"
+            type="range"
+            min="0"
+            max="2"
+            step="0.1"
+            class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer slider-thumb"
+          />
+          <div class="flex justify-between text-xs text-gray-500 mt-1">
+            <span>保守 (0.0)</span>
+            <span>平衡 (1.0)</span>
+            <span>创造 (2.0)</span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 最大Token控制 -->
+      <div class="space-y-2">
+        <div class="flex items-center justify-between">
+          <label class="text-sm text-gray-300">最大Tokens</label>
+          <span class="text-xs text-green-400 font-mono bg-gray-700 px-2 py-1 rounded">
+            {{ maxTokens.toLocaleString() }}
+          </span>
+        </div>
+        <div class="relative">
+          <input
+            v-model.number="maxTokens"
+            type="range"
+            min="100"
+            max="8000"
+            step="100"
+            class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer slider-thumb"
+          />
+          <div class="flex justify-between text-xs text-gray-500 mt-1">
+            <span>短 (100)</span>
+            <span>中 (4000)</span>
+            <span>长 (8000)</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 输入框和发送按钮 -->
+    <div class="flex space-x-2">
+      <input
+        v-model="message"
+        @keyup.enter="sendMessage"
+        placeholder="输入你的消息..."
+        type="text"
+        class="flex-1 p-3 rounded-lg bg-gray-700 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 border border-gray-600"
+      />
+      <button 
+        @click="sendMessage" 
+        :disabled="!message.trim()"
+        :class="[
+          'px-6 py-3 rounded-lg font-medium transition-all duration-200',
+          message.trim() 
+            ? 'bg-blue-500 hover:bg-blue-600 text-white cursor-pointer transform hover:scale-105' 
+            : 'bg-gray-600 text-gray-400 cursor-not-allowed'
+        ]"
+      >
+        <div class="flex items-center space-x-2">
+          <span>发送</span>
+          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
+          </svg>
+        </div>
+      </button>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* 自定义滑块样式 */
+.slider-thumb::-webkit-slider-thumb {
+  appearance: none;
+  height: 20px;
+  width: 20px;
+  border-radius: 50%;
+  background: #3b82f6;
+  cursor: pointer;
+  border: 2px solid #1f2937;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+  transition: all 0.2s ease;
+}
+
+.slider-thumb::-webkit-slider-thumb:hover {
+  background: #2563eb;
+  transform: scale(1.1);
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
+}
+
+.slider-thumb::-moz-range-thumb {
+  height: 20px;
+  width: 20px;
+  border-radius: 50%;
+  background: #3b82f6;
+  cursor: pointer;
+  border: 2px solid #1f2937;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+  transition: all 0.2s ease;
+}
+
+.slider-thumb::-moz-range-thumb:hover {
+  background: #2563eb;
+  transform: scale(1.1);
+  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
+}
+
+/* 滑块轨道样式 */
+.slider-thumb::-webkit-slider-runnable-track {
+  height: 8px;
+  background: linear-gradient(to right, #374151 0%, #3b82f6 50%, #374151 100%);
+  border-radius: 4px;
+}
+
+.slider-thumb::-moz-range-track {
+  height: 8px;
+  background: linear-gradient(to right, #374151 0%, #3b82f6 50%, #374151 100%);
+  border-radius: 4px;
+}
+</style>

+ 87 - 0
chat-ai-ui-main/src/components/Header.vue

@@ -0,0 +1,87 @@
+<script setup lang="ts">
+import { useUserStore } from '../stores/user';
+import { useRouter } from 'vue-router';
+import robotImage from '../assets/robot.png';
+import axios from 'axios';
+
+const userStore = useUserStore();
+const router = useRouter();
+
+const logout = async () => {
+
+  if (!userStore.userId) {
+    userStore.logout();
+    router.push('/');
+    return;
+  }
+
+  try {
+    // 修正axios参数顺序:post(url, data, config)
+    const response = await axios.post(
+      `${import.meta.env.VITE_API_URL}/users/logout`,
+      {}, // 空的请求体
+      {
+        headers: {
+          'Authorization': `Bearer ${userStore.userId}`,
+          'Content-Type': 'application/json'
+        }
+      }
+    );
+
+    console.log('退出接口响应:', response.data);
+
+    // 清除用户状态
+    userStore.logout();
+
+    // 跳转到首页或登录页
+    router.push('/');
+
+    console.log('退出登录成功');
+  } catch (error: any) {
+    console.error('退出登录失败:', error);
+    console.error('状态码:', error.response?.status);
+    console.error('错误详情:', error.response?.data);
+
+    // 即使后端调用失败,也要清除本地数据
+    userStore.logout();
+    router.push('/');
+  }
+};
+</script>
+
+<template>
+  <div
+    class="py-4 px-6 bg-gray-800 shadow-md flex justify-between items-center"
+  >
+    <div class="flex items-center space-x-3">
+      <img :src="robotImage" alt="Chat AI" class="w-8 h-8" />
+      <h1 class="text-lg font-semibold text-white">AI智能问答系统</h1>
+    </div>
+
+    <!-- 用户信息区域 -->
+    <div class="flex items-center space-x-4">
+      <!-- 用户头像或图标 -->
+      <div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
+        <span class="text-white text-sm font-medium">
+          {{ (userStore.name || 'U').charAt(0).toUpperCase() }}
+        </span>
+      </div>
+
+      <!-- 用户名 -->
+      <span class="text-gray-300 text-sm">
+        {{ userStore.name || '用户' }}
+      </span>
+
+      <!-- 退出按钮 -->
+      <button
+        @click="logout"
+        class="flex items-center space-x-1 text-gray-400 hover:text-red-400 transition-colors duration-200 px-3 py-1 rounded hover:bg-gray-700"
+      >
+        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
+        </svg>
+        <span>退出</span>
+      </button>
+    </div>
+  </div>
+</template>

+ 14 - 0
chat-ai-ui-main/src/main.ts

@@ -0,0 +1,14 @@
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+import piniaPluginPersistedState from 'pinia-plugin-persistedstate';
+import { router } from './router';
+import './style.css';
+import App from './App.vue';
+
+const pinia = createPinia();
+pinia.use(piniaPluginPersistedState);
+
+const app = createApp(App);
+app.use(router);
+app.use(pinia);
+app.mount('#app');

+ 13 - 0
chat-ai-ui-main/src/router/index.ts

@@ -0,0 +1,13 @@
+import { createRouter, createWebHistory } from 'vue-router';
+import HomeView from '../views/HomeView.vue';
+import ChatView from '../views/ChatView.vue';
+
+const routes = [
+  { path: '/', component: HomeView },
+  { path: '/chat', component: ChatView },
+];
+
+export const router = createRouter({
+  history: createWebHistory(),
+  routes,
+});

+ 180 - 0
chat-ai-ui-main/src/stores/chat.ts

@@ -0,0 +1,180 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import axios from 'axios';
+import { useUserStore } from './user';
+
+interface FormattedMessage {
+  role: 'user' | 'assistant';
+  content: string;
+}
+
+export const useChatStore = defineStore('chat', () => {
+  const messages = ref<{ role: string; content: string }[]>([]);
+  const isLoading = ref(false);
+
+  const userStore = useUserStore();
+
+  // 获取历史消息接口调用
+  const loadChatHistory = async () => {
+    if (!userStore.userId) return;
+
+    try {
+      const { data } = await axios.get(
+        `${import.meta.env.VITE_API_URL}/chat/history`,
+        {
+          headers: {
+          'Authorization': `Bearer ${userStore.userId}`, // 添加认证头
+          'Content-Type': 'application/json'
+          }
+        }
+      );
+
+      messages.value = data.filter((msg: FormattedMessage) => msg.content);
+    } catch (error) {
+      console.error('Error loading chat history: ', error);
+    }
+  };
+
+  // 对话接口调用
+  const sendMessage = async (message: string, model: string, temperature: number, max_tokens: number, stream: boolean) => {
+    console.log(123);
+    if (!message.trim() || !userStore.userId) return;
+
+    messages.value.push({ role: 'user', content: message });
+
+    isLoading.value = true;
+
+    try {
+      if (stream) {
+      // 流式处理
+      await handleStreamResponse(model, temperature, max_tokens);
+    } else {
+      // 非流式处理(保持原有逻辑)
+      const { data } = await axios.post(
+        `${import.meta.env.VITE_API_URL}/chat/chat`,
+        {
+          // 请求体数据
+          messages: messages.value,
+          model: model,
+          temperature: temperature,
+          max_tokens: max_tokens,
+          stream: false,
+        },
+        {
+          // axios 配置选项
+          headers: {
+            'Authorization': `Bearer ${userStore.userId}`, // 认证头
+            'Content-Type': 'application/json', // 可选,axios通常会自动设置
+          }
+        }
+      );
+      messages.value.push({ role: data.message.role, content: data.message.content });
+    }
+    } catch (error) {
+      console.error('Error sending message: ', error);
+      messages.value.push({
+        role: 'ai',
+        content: 'Error: unable to process request',
+      });
+    } finally {
+      isLoading.value = false;
+    }
+  };
+
+
+/**
+ * 处理流式响应
+ * @param model - AI模型名称
+ * @param temperature - 温度参数,控制回复的随机性
+ * @param max_tokens - 最大token数量限制
+ */
+const handleStreamResponse = async (model: string, temperature: number, max_tokens: number) => {
+  // 获取即将添加的AI消息在数组中的索引位置
+  const aiMessageIndex = messages.value.length;
+  // 预先添加一个空的AI消息占位,用于后续实时更新内容
+  messages.value.push({ role: 'assistant', content: '' });
+  try {
+    // 发起流式请求到后端API
+    const response = await fetch(`${import.meta.env.VITE_API_URL}/chat/chat`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': `Bearer ${userStore.userId}`, // 认证头
+      },
+      body: JSON.stringify({
+        messages: messages.value,           // 完整的对话历史
+        model: model,                       // AI模型
+        temperature: temperature,           // 温度参数
+        max_tokens: max_tokens,            // token限制
+        stream: true                       // 开启流式响应
+      }
+    )
+    });
+    // 检查HTTP响应状态
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
+    }
+     // 检查浏览器是否支持ReadableStream
+    if (!response.body) {
+      throw new Error('ReadableStream not supported');
+    }
+     // 获取流式数据读取器
+    const reader = response.body.getReader();
+    // 创建文本解码器,用于将字节流转换为文本
+    const decoder = new TextDecoder();
+    // 缓冲区,用于处理可能被分割的数据行
+    let buffer = '';
+    // 持续读取流式数据
+    while (true) {
+       // 读取数据块
+      const { done, value } = await reader.read();
+      // 如果流结束,跳出循环
+      if (done) break;
+       // 将字节数据解码为文本并添加到缓冲区
+      buffer += decoder.decode(value, { stream: true });
+      // 按换行符分割数据,处理SSE格式的数据行
+      const lines = buffer.split('\n');
+      // 保留最后一行(可能是不完整的),其余行进行处理
+      buffer = lines.pop() || '';
+
+      // 逐行处理SSE数据
+      for (const line of lines) {
+        const trimmedLine = line.trim();
+         // 检查是否是SSE数据行(以"data: "开头)
+        if (trimmedLine.startsWith('data: ')) {
+          // 提取JSON数据部分,移除"data: "前缀
+          const jsonStr = trimmedLine.slice(6);
+          
+          // 跳过空数据行或结束标记
+          if (jsonStr.trim() === '' || jsonStr.trim() === '[DONE]') continue;
+
+          try {
+             // 解析JSON数据
+            const data = JSON.parse(jsonStr);
+            // 如果包含内容数据,实时更新AI消息
+            if (data.content) {
+              // 将新内容追加到AI消息中,实现打字机效果
+              messages.value[aiMessageIndex].content += data.content;
+            }
+            // 如果收到结束信号,跳出数据处理循环
+            if (data.finished) {
+              break;
+            }
+          } catch (e) {
+            console.warn('Failed to parse JSON:', jsonStr, e);
+          }
+        }
+      }
+    }
+    // 释放读取器资源
+    reader.releaseLock();
+    
+  } catch (error) {
+    console.error('Stream error:', error);
+    messages.value.splice(aiMessageIndex, 1);
+    throw error;
+  }
+};
+
+  return { messages, isLoading, loadChatHistory, sendMessage };
+});

+ 19 - 0
chat-ai-ui-main/src/stores/user.ts

@@ -0,0 +1,19 @@
+import { defineStore } from 'pinia';
+
+export const useUserStore = defineStore('user', {
+  state: () => ({
+    userId: null as string | null,
+    name: null as string | null,
+  }),
+  actions: {
+    setUser(data: { userId: string; name: string }) {
+      this.userId = data.userId;
+      this.name = data.name;
+    },
+    logout() {
+      this.userId = null;
+      this.name = null;
+    },
+  },
+  persist: true, // Keep user logged in across page reloads
+});

+ 1 - 0
chat-ai-ui-main/src/style.css

@@ -0,0 +1 @@
+@import 'tailwindcss';

+ 330 - 0
chat-ai-ui-main/src/views/ChatView.vue

@@ -0,0 +1,330 @@
+<script setup lang="ts">
+import { onMounted, nextTick } from 'vue';
+import { useUserStore } from '../stores/user';
+import { useChatStore } from '../stores/chat';
+import { useRouter } from 'vue-router';
+import Header from '../components/Header.vue';
+import ChatInput from '../components/ChatInput.vue';
+import { watch } from 'vue';
+import { marked } from 'marked';
+import hljs from 'highlight.js';
+import { markedHighlight } from 'marked-highlight';
+
+const userStore = useUserStore();
+const chatStore = useChatStore();
+const router = useRouter();
+
+// 确保用户已登录
+if (!userStore.userId) {
+  router.push('/');
+}
+// 配置标记以获得更好的呈现
+
+marked.use(markedHighlight({
+  langPrefix: 'hljs language-',
+  highlight(code, lang) {
+    if (lang && hljs.getLanguage(lang)) {
+      try {
+        return hljs.highlight(code, { language: lang }).value;
+      } catch (err) {}
+    }
+    return hljs.highlightAuto(code).value;
+  }
+}));
+
+// 格式化AI信息以更好地显示
+const formatMessage = (text: string, role: string) => {
+  if (!text) return '';
+  
+  if (role === 'user') {
+    // 对于用户消息,只需转义HTML并保留换行符
+    return text
+      .replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/\n/g, '<br>');
+  } else {
+    // 对于AI消息,渲染为 markdown
+    return marked(text);
+  }
+};
+
+// 自动滚动到底部
+const scrollToBottom = () => {
+  nextTick(() => {
+    const chatContainer = document.getElementById('chat-container');
+    if (chatContainer) chatContainer.scrollTop = chatContainer.scrollHeight;
+  });
+};
+
+// 初始化页面后进行加载历史消息
+onMounted(() => {
+  chatStore.loadChatHistory().then(() => scrollToBottom());
+});
+
+// 使用响应式监听器 监听 messages 变化并滚动到底
+watch(
+  () => chatStore.messages,
+  () => {
+    scrollToBottom(); // 回调函数,每次 messages 变化就滚动到底
+  },
+  { deep: true }
+);
+</script>
+
+<template>
+  <div class="min-h-screen bg-gray-900 text-white">
+    <Header />
+
+    <!-- 具有居中聊天区域的主容器 -->
+    <div class="flex justify-center px-4 py-6">
+      <div class="w-full max-w-4xl flex flex-col h-[calc(100vh-120px)]">
+        
+        <!-- 聊天消息容器 -->
+        <div id="chat-container" class="flex-1 overflow-y-auto space-y-4 mb-4 px-4 py-4 rounded-lg shadow-lg">
+          <!-- 没有对话内容显示的内容 -->
+          <div v-if="chatStore.messages.length === 0" class="flex flex-col items-center justify-center h-full text-gray-400">
+            <div class="text-6xl mb-4">💬</div>
+            <h2 class="text-2xl font-semibold mb-2">开始对话</h2>
+            <p class="text-center">向我提问任何问题,我会尽力为你解答</p>
+          </div>
+
+          <!-- 聊天信息 -->
+          <div
+            v-for="(msg, index) in chatStore.messages"
+            :key="index"
+            class="flex items-start"
+            :class="msg.role === 'user' ? 'justify-end' : 'justify-start'"
+          >
+            <!-- 图标 -->
+            <div v-if="msg.role !== 'user'" class="flex-shrink-0 mr-3">
+              <div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-sm font-semibold">
+                AI
+              </div>
+            </div>
+
+            <!-- 消息内容 -->
+            <div
+              v-html="formatMessage(msg.content, msg.role)"
+              class="max-w-2xl px-4 py-3 rounded-lg shadow-sm prose prose-invert max-w-none"
+              :class="
+                msg.role === 'user'
+                  ? 'bg-blue-600 text-white rounded-br-sm prose-headings:text-white prose-p:text-white prose-strong:text-white prose-em:text-white'
+                  : 'bg-gray-700 text-white rounded-bl-sm prose-headings:text-gray-100 prose-p:text-gray-100 prose-strong:text-gray-100 prose-em:text-gray-100 prose-code:text-blue-300 prose-code:bg-gray-800 prose-pre:bg-gray-800 prose-blockquote:border-blue-500'
+              "
+            ></div>
+            <!-- 用户 -->
+            <div v-if="msg.role === 'user'" class="flex-shrink-0 ml-3">
+              <div class="w-8 h-8 bg-gray-600 rounded-full flex items-center justify-center text-sm font-semibold">
+                我
+              </div>
+            </div>
+          </div>
+
+          <!-- 等待回复过程 -->
+          <div v-if="chatStore.isLoading" class="flex justify-start">
+            <div class="flex-shrink-0 mr-3">
+              <div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-sm font-semibold">
+                AI
+              </div>
+            </div>
+            <div class="bg-gray-700 text-white px-4 py-3 rounded-lg rounded-bl-sm shadow-sm">
+              <div class="flex items-center space-x-2">
+                <div class="flex space-x-1">
+                  <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
+                  <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
+                  <div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
+                </div>
+                <span class="text-sm text-gray-400">正在思考中...</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- Chat input -->
+        <div class="flex-shrink-0">
+          <ChatInput @send="chatStore.sendMessage" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+
+<style scoped>
+/* 聊天容器的自定义滚动条 */
+#chat-container::-webkit-scrollbar {
+  width: 6px;
+}
+
+#chat-container::-webkit-scrollbar-track {
+  background: #374151;
+  border-radius: 3px;
+}
+
+#chat-container::-webkit-scrollbar-thumb {
+  background: #6b7280;
+  border-radius: 3px;
+}
+
+#chat-container::-webkit-scrollbar-thumb:hover {
+  background: #9ca3af;
+}
+
+/* 平滑移动 */
+#chat-container {
+  scroll-behavior: smooth;
+}
+
+/* 列出格式化消息的样式 */
+:deep(ul) {
+  margin: 0.5rem 0;
+  padding-left: 1.5rem;
+}
+
+:deep(li) {
+  margin: 0.25rem 0;
+}
+
+:deep(code) {
+  background-color: rgba(0, 0, 0, 0.3);
+  padding: 0.125rem 0.25rem;
+  border-radius: 0.25rem;
+  font-family: 'Courier New', monospace;
+  font-size: 0.875rem;
+}
+
+:deep(b) {
+  font-weight: 600;
+}
+
+:deep(i) {
+  font-style: italic;
+}
+
+:deep(.prose h1),
+:deep(.prose h2),
+:deep(.prose h3),
+:deep(.prose h4),
+:deep(.prose h5),
+:deep(.prose h6) {
+  margin-top: 1rem;
+  margin-bottom: 0.5rem;
+  font-weight: 600;
+}
+
+:deep(.prose h1) { font-size: 1.5rem; }
+:deep(.prose h2) { font-size: 1.375rem; }
+:deep(.prose h3) { font-size: 1.25rem; }
+
+:deep(.prose p) {
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+
+:deep(.prose ul),
+:deep(.prose ol) {
+  margin: 0.5rem 0;
+  padding-left: 1.5rem;
+}
+
+:deep(.prose li) {
+  margin: 0.25rem 0;
+}
+
+:deep(.prose code) {
+  background-color: rgba(0, 0, 0, 0.4);
+  padding: 0.125rem 0.375rem;
+  border-radius: 0.25rem;
+  font-family: 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
+  font-size: 0.875rem;
+}
+
+:deep(.prose pre) {
+  background-color: #1f2937;
+  padding: 1rem;
+  border-radius: 0.5rem;
+  overflow-x: auto;
+  margin: 1rem 0;
+  border: 1px solid #374151;
+}
+
+:deep(.prose pre code) {
+  background-color: transparent;
+  padding: 0;
+  border-radius: 0;
+  font-size: 0.875rem;
+}
+
+:deep(.prose blockquote) {
+  border-left: 4px solid #3b82f6;
+  padding-left: 1rem;
+  margin: 1rem 0;
+  font-style: italic;
+  background-color: rgba(59, 130, 246, 0.1);
+  border-radius: 0 0.25rem 0.25rem 0;
+}
+
+:deep(.prose table) {
+  width: 100%;
+  border-collapse: collapse;
+  margin: 1rem 0;
+}
+
+:deep(.prose th),
+:deep(.prose td) {
+  border: 1px solid #374151;
+  padding: 0.5rem;
+  text-align: left;
+}
+
+:deep(.prose th) {
+  background-color: #374151;
+  font-weight: 600;
+}
+
+:deep(.prose tr:nth-child(even)) {
+  background-color: rgba(75, 85, 99, 0.3);
+}
+
+/* Syntax highlighting for code blocks */
+:deep(.hljs) {
+  background: #1f2937 !important;
+  color: #e5e7eb;
+}
+
+:deep(.hljs-keyword),
+:deep(.hljs-selector-tag),
+:deep(.hljs-literal),
+:deep(.hljs-section),
+:deep(.hljs-link) {
+  color: #8b5cf6;
+}
+
+:deep(.hljs-string),
+:deep(.hljs-attr) {
+  color: #10b981;
+}
+
+:deep(.hljs-number),
+:deep(.hljs-regexp),
+:deep(.hljs-addition) {
+  color: #f59e0b;
+}
+
+:deep(.hljs-comment),
+:deep(.hljs-quote),
+:deep(.hljs-meta) {
+  color: #6b7280;
+}
+
+:deep(.hljs-name),
+:deep(.hljs-symbol),
+:deep(.hljs-bullet),
+:deep(.hljs-subst),
+:deep(.hljs-title),
+:deep(.hljs-class),
+:deep(.hljs-type) {
+  color: #ef4444;
+}
+</style>

+ 82 - 0
chat-ai-ui-main/src/views/HomeView.vue

@@ -0,0 +1,82 @@
+<script setup lang="ts">
+import { ref } from 'vue';
+import axios from 'axios';
+import { useUserStore } from '../stores/user';
+import { useRouter } from 'vue-router';
+import robotImage from '../assets/robot.png';
+
+const userStore = useUserStore();
+const router = useRouter();
+
+const name = ref('');
+const password = ref('');
+const loading = ref(false);
+const error = ref('');
+
+const loginUser = async () => {
+if (!name.value || !password.value) {
+  error.value = '用户名和秘密是必填项';
+  return;
+}
+
+loading.value = true;
+error.value = '';
+
+try {
+  const { data } = await axios.post(
+    `${import.meta.env.VITE_API_URL}/users/token`,
+    {
+      username: name.value,
+      password: password.value,
+    }
+  );
+
+  userStore.setUser({
+    userId: data.access_token,
+    name: data.username,
+  });
+
+  router.push('/chat');
+} catch (err: any) {
+  error.value = err.response.data.detail;
+} finally {
+  loading.value = false;
+}
+};
+</script>
+
+<template>
+<div class="h-screen flex items-center justify-center bg-gray-900 text-white">
+  <div class="p-8 bg-gray-800 rounded-lg shadow-lg w-full max-w-md">
+    <img :src="robotImage" alt="" class="mx-auto w-24 h-24 mb-4" />
+    <h1 class="text-2xl font-semibold mb-4 text-center">
+     欢迎使用AI
+    </h1>
+
+    <input
+      type="text"
+      class="w-full p-2 mb-2 bg-gray-700 text-white rounded-lg focus:outline-none"
+      placeholder="Name"
+      v-model="name"
+      @keyup.enter="loginUser"
+    />
+    <input
+      type="password"
+      class="w-full p-2 mb-2 bg-gray-700 text-white rounded-lg focus:outline-none"
+      placeholder="password"
+      v-model="password"
+      @keyup.enter="loginUser"
+    />
+
+    <button
+      @click="loginUser"
+      class="w-full p-2 bg-blue-500 rounded-lg"
+      :disabled="loading"
+    >
+      {{ loading ? 'Logging in...' : 'Start Chat' }}
+    </button>
+
+    <p v-if="error" class="text-red-400 text-center mt-2">{{ error }}</p>
+  </div>
+</div>
+</template>

+ 1 - 0
chat-ai-ui-main/src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 14 - 0
chat-ai-ui-main/tsconfig.app.json

@@ -0,0 +1,14 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}

+ 7 - 0
chat-ai-ui-main/tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}

+ 24 - 0
chat-ai-ui-main/tsconfig.node.json

@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "ES2022",
+    "lib": ["ES2023"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 8 - 0
chat-ai-ui-main/vite.config.ts

@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import tailwindcss from '@tailwindcss/vite';
+
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [vue(), tailwindcss()],
+});

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "chat-ai-ui-main",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}