瀏覽代碼

对接配置语音接口

yangg 5 月之前
父節點
當前提交
11b3bce604
共有 3 個文件被更改,包括 743 次插入19 次删除
  1. 140 3
      src/components/SearchResults/index.vue
  2. 233 10
      src/components/VoiceConfig/index.vue
  3. 370 6
      src/views/report.vue

+ 140 - 3
src/components/SearchResults/index.vue

@@ -1,5 +1,24 @@
 <template>
   <div class="search-results">
+    <div class="search-box">
+      <div class="input-wrapper">
+        <input
+          v-model="searchQuery"
+          type="text"
+          placeholder="Enter your search query"
+          @keyup.enter="handleSearch"
+        />
+        <span 
+          v-if="searchQuery" 
+          class="clear-icon" 
+          @click="clearSearch"
+        >×</span>
+      </div>
+      <button @click="handleSearch" :disabled="isSearching">
+        搜索一下
+      </button>
+    </div>
+
     <div class="results-header">
       <h2>Related Search Results</h2>
       <div class="result-stats">
@@ -42,7 +61,7 @@
 import { defineComponent, ref, watch } from 'vue';
 import { FileSearchOutlined } from '@ant-design/icons-vue';
 import dayjs from 'dayjs';
-
+import axios from 'axios';
 export default defineComponent({
   name: 'SearchResults',
   components: {
@@ -62,9 +81,11 @@ export default defineComponent({
       default: 0
     }
   },
-  setup(props) {
+  setup(props, { emit }) {
     const displayResults = ref([]);
     const hasMore = ref(false);
+    const searchQuery = ref('');
+    const isSearching = ref(false);
 
     // 监听 searchResults 变化
     watch(() => props.searchResults, (newResults) => {
@@ -77,12 +98,58 @@ export default defineComponent({
       return dayjs(date).format('YYYY-MM-DD');
     };
 
+    // 更新搜索处理函数
+    const handleSearch = async () => {
+      if (!searchQuery.value.trim() || isSearching.value) return;
+      
+      isSearching.value = true;
+      try {
+        const response = await axios.post(
+          `${import.meta.env.VITE_BASE_AI_API}/api/web-search-results/`,
+          {
+            query: searchQuery.value,
+            num_results: 20,
+            page: 1,
+            page_size: 20,
+            engine: "bing",
+          }
+        );
+
+        // 添加调试日志
+        console.log('API Response:', response.data);
+        
+        if (response.data && response.data.code === 200) {
+          displayResults.value = response.data.data.results || [];
+          // 添加调试日志
+          console.log('Total Results:', response.data.data.total_results);
+          emit('update:total', response.data.data.total_results || 0);
+        }
+      } catch (error) {
+        console.error('Web search failed:', error);
+        displayResults.value = [];
+        emit('update:total', 0);
+      } finally {
+        isSearching.value = false;
+      }
+    };
+
+    // 清空搜索函数
+    const clearSearch = () => {
+      searchQuery.value = '';
+      displayResults.value = [];
+      emit('update:total', 0);
+    };
+
     return {
       displayResults,
       loading: props.loading,
       total: props.total,
       hasMore,
-      formatDate
+      formatDate,
+      searchQuery,
+      isSearching,
+      handleSearch,
+      clearSearch,
     };
   }
 });
@@ -97,6 +164,76 @@ export default defineComponent({
   width: 100%;
   overflow: hidden;
 
+  .search-box {
+    padding: 16px 24px;
+    border-bottom: 1px solid #f0f0f0;
+    display: flex;
+    gap: 0;
+
+    .input-wrapper {
+      position: relative;
+      flex: 1;
+      display: flex;
+      align-items: center;
+
+      input {
+        width: 100%;
+        padding: 8px 30px 8px 12px;
+        height: 24px;
+        font-size: 16px;
+        border: 2px solid #4e6ef2;
+        border-right: none;
+        border-radius: 4px 0 0 4px;
+
+        &:focus {
+          outline: none;
+        }
+      }
+
+      .clear-icon {
+        position: absolute;
+        right: 8px;
+        top: 50%;
+        transform: translateY(-50%);
+        cursor: pointer;
+        color: #999;
+        font-size: 16px;
+        width: 16px;
+        height: 16px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-radius: 50%;
+
+        &:hover {
+          color: #666;
+          background-color: rgba(0, 0, 0, 0.05);
+        }
+      }
+    }
+
+    button {
+      padding: 0 20px;
+      height: 44px;
+      background: #4e6ef2;
+      border: none;
+      border-radius: 0 4px 4px 0;
+      color: white;
+      cursor: pointer;
+      font-size: 16px;
+      min-width: 108px;
+
+      &:disabled {
+        background: #7a96fa;
+        cursor: not-allowed;
+      }
+
+      &:hover:not(:disabled) {
+        background: #4662d9;
+      }
+    }
+  }
+
   .results-header {
     padding: 16px 24px;
     border-bottom: 1px solid #f0f0f0;

+ 233 - 10
src/components/VoiceConfig/index.vue

@@ -14,11 +14,15 @@
       <!-- 语音角色选择 -->
       <div class="config-item">
         <label>语音角色</label>
-        <select v-model="voiceConfig.role" @change="handleConfigChange">
-          <option value="female">女声</option>
-          <option value="male">男声</option>
-          <option value="child">童声</option>
-        </select>
+        <div class="voice-select-container">
+          <select v-model="voiceConfig.role" @change="handleConfigChange">
+            <option v-for="voice in voiceList" 
+                    :key="voice.id" 
+                    :value="voice.id">
+              {{ voice.name+'_'+voice.gender }} ({{ getLanguageName(voice.language) }})
+            </option>
+          </select>
+        </div>
       </div>
       
       <!-- 语速调节 -->
@@ -38,31 +42,101 @@
       </div>
       
       <!-- 语言选择 -->
-      <div class="config-item">
-        <label>语言</label>
+      <div class="config-item" style="display: flex;justify-content: space-around;">
+        <button 
+            class="preview-button" 
+            @click="handlePreviewVoice"
+            :disabled="isPreviewPlaying">
+            {{ isPreviewPlaying ? '播放中...' : '试听' }}
+          </button>
+        <button 
+            class="switch-button" 
+            @click="handleSwitchVoice"
+            :disabled="switchingVoice">
+            {{ switchingVoice ? '切换中...' : '切换语音' }}
+          </button>
+       <!--  <label>语言</label>
         <select v-model="voiceConfig.language" @change="handleConfigChange">
           <option value="zh">中文</option>
           <option value="en">英文</option>
           <option value="ja">日语</option>
-        </select>
+        </select> -->
       </div>
     </div>
   </div>
 </template>
 
 <script setup>
-import { ref, reactive } from 'vue';
+import { ref, reactive, onMounted, onUnmounted } from 'vue';
 import { useStore } from 'vuex';
+import axios from 'axios';
 
 const store = useStore();
 const isPanelOpen = ref(false);
+const voiceList = ref([]);
 
 const voiceConfig = reactive({
-  role: 'female',
+  role: '',
   speed: 1,
   language: 'zh'
 });
 
+const switchingVoice = ref(false);
+const isPreviewPlaying = ref(false);
+const audioElement = ref(null);
+
+// 获取当前语音配置
+const fetchCurrentVoiceConfig = async () => {
+  try {
+    const response = await axios.get(`${import.meta.env.VITE_BASE_AI_API}/chatbot/current_voice_config`);
+    const currentConfig = response.data.data;
+    console.log('Current config:', currentConfig); // 添加日志
+    if (currentConfig && currentConfig.voice_name && voiceList.value.length > 0) {
+      // 在 voiceList 中查找匹配的语音
+      const matchedVoice = voiceList.value.find(voice => voice.name === currentConfig.voice_name);
+      console.log('Matched voice:', matchedVoice); // 添加日志
+      if (matchedVoice) {
+        voiceConfig.role = matchedVoice.id;
+        voiceConfig.speed = currentConfig.speed || 1;
+        voiceConfig.language = matchedVoice.language;
+        // 触发配置更新
+        handleConfigChange();
+      }
+    }
+  } catch (error) {
+    console.error('获取当前语音配置失败:', error);
+  }
+};
+
+// 获取语音列表
+const fetchVoiceList = async () => {
+  try {
+    const response = await axios.get(`${import.meta.env.VITE_BASE_AI_API}/chatbot/list_voices`);
+    const voices = response.data.data.voices;
+    voiceList.value = Object.values(voices).flat();
+    console.log('Voice list:', voiceList.value); // 添加日志
+    // 获取语音列表后再获取当前配置
+    await fetchCurrentVoiceConfig();
+  } catch (error) {
+    console.error('获取语音列表失败:', error);
+  }
+};
+
+// 获取语言名称
+const getLanguageName = (language) => {
+  const languageMap = {
+    'zh-cn': '中文',
+    'en-us': '英文',
+    'ja-jp': '日语',
+    'ko-kr': '韩语'
+  };
+  return languageMap[language] || language;
+};
+
+onMounted(() => {
+  fetchVoiceList();
+});
+
 const togglePanel = () => {
   isPanelOpen.value = !isPanelOpen.value;
 };
@@ -70,6 +144,87 @@ const togglePanel = () => {
 const handleConfigChange = () => {
   store.commit('updateVoiceConfig', { ...voiceConfig });
 };
+
+// 添加切换语音的方法
+const handleSwitchVoice = async () => {
+  if (!voiceConfig.role) return;
+  
+  switchingVoice.value = true;
+  try {
+    const formData = new FormData();
+    formData.append('voice_id', voiceConfig.role);
+    
+    await axios.post(
+      `${import.meta.env.VITE_BASE_AI_API}/switch_voice_config`,
+      formData,
+      {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        }
+      }
+    );
+    
+    // 切换成功后可以添加提示
+    /* alert('语音切换成功'); */
+  } catch (error) {
+    console.error('切换语音失败:', error);
+    alert('语音切换失败');
+  } finally {
+    switchingVoice.value = false;
+  }
+};
+
+// 添加试听功能
+const handlePreviewVoice = async () => {
+  if (!voiceConfig.role || isPreviewPlaying.value) return;
+  
+  try {
+    isPreviewPlaying.value = true;
+    
+    // 构建预览URL
+    const previewUrl = `${import.meta.env.VITE_BASE_AI_API}/get_voice_preview?voice_id=${voiceConfig.role}&text=你好&style=chat`;
+    
+    // 如果已存在音频元素,先停止并移除
+    if (audioElement.value) {
+      audioElement.value.pause();
+      audioElement.value = null;
+    }
+    
+    // 创建新的音频元素
+    const audio = new Audio(previewUrl);
+    audioElement.value = audio;
+    
+    // 监听播放结束事件
+    audio.onended = () => {
+      isPreviewPlaying.value = false;
+      audioElement.value = null;
+    };
+    
+    // 监听错误事件
+    audio.onerror = () => {
+      console.error('音频播放失败');
+      isPreviewPlaying.value = false;
+      audioElement.value = null;
+      alert('试听失败');
+    };
+    
+    // 开始播放
+    await audio.play();
+    
+  } catch (error) {
+    console.error('试听失败:', error);
+    isPreviewPlaying.value = false;
+    alert('试听失败');
+  }
+};
+
+// 组件卸载时清理音频
+onUnmounted(() => {
+  if (audioElement.value) {
+    audioElement.value.pause();
+    audioElement.value = null;
+  }
+});
 </script>
 
 <style scoped>
@@ -162,4 +317,72 @@ input[type="range"]::-webkit-slider-thumb {
   color: rgba(0, 0, 0, 0.45);
   min-width: 36px;
 }
+
+.voice-select-container {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.switch-button {
+  padding: 4px 15px;
+  height: 32px;
+  border-radius: 4px;
+  border: 1px solid #d9d9d9;
+  background: #fff;
+  cursor: pointer;
+  transition: all 0.3s;
+  white-space: nowrap;
+}
+
+.switch-button:hover {
+  color: #1677ff;
+  border-color: #1677ff;
+}
+
+.switch-button:disabled {
+  color: rgba(0, 0, 0, 0.25);
+  border-color: #d9d9d9;
+  background: #f5f5f5;
+  cursor: not-allowed;
+}
+
+select {
+  flex: 1;
+  min-width: 0; /* 防止select溢出 */
+}
+
+.preview-button {
+  padding: 4px 15px;
+  height: 32px;
+  border-radius: 4px;
+  border: 1px solid #d9d9d9;
+  background: #fff;
+  cursor: pointer;
+  transition: all 0.3s;
+  white-space: nowrap;
+}
+
+.preview-button:hover {
+  color: #1677ff;
+  border-color: #1677ff;
+}
+
+.preview-button:disabled {
+  color: rgba(0, 0, 0, 0.25);
+  border-color: #d9d9d9;
+  background: #f5f5f5;
+  cursor: not-allowed;
+}
+
+.voice-select-container {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+select {
+  flex: 1;
+  min-width: 0;
+}
 </style> 

+ 370 - 6
src/views/report.vue

@@ -535,9 +535,53 @@ const enableWebSearch = ref(false);
 // 添加新的响应式变量
 const isMenuCollapsed = ref(false);
 
-// 添加切换菜单方法
+// 添加移动端检测
+const isMobile = ref(false);
+
+// 检测设备类型
+const checkMobile = () => {
+  isMobile.value = window.innerWidth <= 768;
+};
+
+// 监听窗口大小变化
+onMounted(() => {
+  checkMobile();
+  window.addEventListener('resize', checkMobile);
+});
+
+onUnmounted(() => {
+  window.removeEventListener('resize', checkMobile);
+});
+
+// 添加遮罩层点击处理
+const handleOverlayClick = () => {
+  if (isMobile.value) {
+    isMenuCollapsed.value = true;
+    isRightMenuCollapsed.value = true;
+    document.body.style.overflow = 'auto';
+  }
+};
+
+// 修改菜单切换逻辑
 const toggleMenu = () => {
   isMenuCollapsed.value = !isMenuCollapsed.value;
+  if (isMobile.value) {
+    document.body.style.overflow = isMenuCollapsed.value ? 'auto' : 'hidden';
+    if (!isMenuCollapsed.value) {
+      isRightMenuCollapsed.value = true; // 确保右侧菜单关闭
+    }
+  }
+};
+
+// 修改右侧菜单切换逻辑
+const toggleRightMenu = () => {
+  isRightMenuCollapsed.value = !isRightMenuCollapsed.value;
+  if (isMobile.value) {
+    document.body.style.overflow = isRightMenuCollapsed.value ? 'auto' : 'hidden';
+    if (!isRightMenuCollapsed.value) {
+      isMenuCollapsed.value = true; // 确保左侧菜单关闭
+    }
+  }
 };
 
 // 修改获取文档摘要的方法
@@ -576,7 +620,6 @@ const startRotation = () => {
     // 处理第一项数据(Hot Topics)
     try {
       const hotTopicsData = documentSummary.value[0];
-      console.log(hotTopicsData);
       if (hotTopicsData?.questions) {
         const parsedQaPairs =
           /* JSON.parse( */ hotTopicsData.questions; /* .replace(/'/g, '"')); */
@@ -1002,10 +1045,6 @@ const switchConversation = (conversationId) => {
   session_id.value = ""; // 重置 session_id
 };
 
-const toggleRightMenu = () => {
-  isRightMenuCollapsed.value = !isRightMenuCollapsed.value;
-};
-
 const handleFileDrop = async (event) => {
   if (isUploading.value) return;
 
@@ -2364,6 +2403,331 @@ const renderMarkdown = (content) => {
   width: 12px;
   height: 12px;
 }
+
+/* 添加响应式布局样式 */
+@media screen and (max-width: 768px) {
+  .layout {
+    /* 移动端布局调整 */
+    flex-direction: column;
+  }
+
+  .menu {
+    /* 移动端菜单调整 */
+    position: fixed;
+    width: 100%;
+    height: 100%;
+    z-index: 1000;
+    transform: translateX(-100%);
+  }
+
+  .menu.collapsed {
+    transform: translateX(-100%);
+  }
+
+  .collapse-left-button {
+    /* 移动端收缩按钮调整 */
+    display: none;
+  }
+
+  .chat {
+    /* 移动端聊天区域调整 */
+    width: 100%;
+    margin: 0;
+    padding: 16px;
+    left: 0 !important;
+    right: 0 !important;
+  }
+
+  .message-list {
+    /* 移动端消息列表调整 */
+    width: 100%;
+    padding: 10px;
+  }
+
+  .input-container {
+    /* 移动端输入区域调整 */
+    width: 100%;
+    padding: 10px;
+  }
+
+  .cards-container {
+    /* 移动端卡片容器调整 */
+    flex-direction: column;
+  }
+
+  .suggestion-card {
+    /* 移动端建议卡片调整 */
+    width: 100%;
+  }
+
+  .welcome-section {
+    /* 移动端欢迎区域调整 */
+    width: 100%;
+    padding: 16px;
+  }
+
+  .right_menu {
+    /* 移动端右侧菜单调整 */
+    position: fixed;
+    width: 100%;
+    height: 100%;
+    z-index: 1000;
+  }
+
+  .right_menu.collapsed {
+    transform: translateX(100%);
+  }
+
+  /* 移动端文件预览调整 */
+  .image-preview, 
+  .video-preview {
+    width: 100%;
+    max-width: 300px;
+  }
+
+  /* 移动端文件列表调整 */
+  .file-list {
+    justify-content: center;
+  }
+
+  .file-item {
+    width: 100%;
+    max-width: 300px;
+  }
+
+  /* 移动端标签栏调整 */
+  .tabs-wrapper {
+    flex-wrap: wrap;
+    gap: 4px;
+  }
+
+  .web-search-toggle {
+    width: 100%;
+    justify-content: flex-start;
+    margin-top: 8px;
+  }
+
+  /* 移动端消息气泡调整 */
+  .message-bubble {
+    max-width: 90%;
+  }
+}
+
+/* 添加移动端手势支持 */
+@media (hover: none) and (pointer: coarse) {
+  .menu {
+    /* 支持触摸滑动 */
+    touch-action: pan-y;
+  }
+
+  .message-list {
+    /* 支持触摸滚动 */
+    -webkit-overflow-scrolling: touch;
+  }
+}
+
+/* 优化移动端字体大小 */
+@media screen and (max-width: 480px) {
+  .message-content {
+    font-size: 14px;
+  }
+
+  .suggestion-item {
+    font-size: 13px;
+  }
+
+  .file-name {
+    font-size: 13px;
+  }
+}
+
+/* 修改现有的响应式样式 */
+@media screen and (max-width: 768px) {
+  .layout {
+    overflow-x: hidden;
+  }
+
+  .chat {
+    /* 调整聊天区域以保持一致的宽度 */
+    padding: 24px 16px;
+    margin: 0;
+    width: 100%;
+  }
+
+  .message-list {
+    /* 保持消息列表的宽度一致 */
+    width: 100%;
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 0 10px;
+  }
+
+  .input-container {
+    /* 保持输入区域的宽度一致 */
+    width: 100%;
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 0 10px;
+  }
+
+  .welcome-section {
+    /* 保持欢迎区域的宽度一致 */
+    width: 100%;
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 0 16px;
+  }
+
+  .cards-container {
+    /* 保持卡片布局 */
+    flex-direction: row;
+    flex-wrap: wrap;
+    gap: 16px;
+  }
+
+  .suggestion-card {
+    /* 调整卡片大小 */
+    flex: 1 1 300px;
+    min-width: 300px;
+  }
+
+  .message-bubble {
+    /* 保持消息气泡的样式 */
+    max-width: 80%;
+    padding: 12px 16px;
+  }
+
+  /* 调整文件预览区域 */
+  .image-preview,
+  .video-preview {
+    width: 200px;
+    margin: 0 auto;
+  }
+
+  /* 调整文件列表布局 */
+  .file-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    justify-content: flex-start;
+  }
+
+  .file-item {
+    width: calc(50% - 8px);
+    min-width: 200px;
+  }
+
+  /* 调整标签栏布局 */
+  .tabs-wrapper {
+    flex-wrap: nowrap;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+    padding-bottom: 8px;
+  }
+
+  /* 调整搜索开关位置 */
+  .web-search-toggle {
+    position: relative;
+    width: auto;
+    margin-left: auto;
+  }
+
+  /* 调整菜单样式 */
+  .menu {
+    position: fixed;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    width: 260px;
+    transform: translateX(-100%);
+    transition: transform 0.3s ease;
+    z-index: 1000;
+  }
+
+  .menu.collapsed {
+    transform: translateX(-100%);
+  }
+
+  .menu:not(.collapsed) {
+    transform: translateX(0);
+  }
+
+  /* 调整右侧菜单样式 */
+  .right_menu {
+    position: fixed;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    width: 300px;
+    transform: translateX(100%);
+    transition: transform 0.3s ease;
+    z-index: 1000;
+  }
+
+  .right_menu.collapsed {
+    transform: translateX(100%);
+  }
+
+  .right_menu:not(.collapsed) {
+    transform: translateX(0);
+  }
+
+  /* 添加遮罩层样式 */
+  .menu-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    z-index: 999;
+    display: none;
+  }
+
+  .menu:not(.collapsed) ~ .menu-overlay,
+  .right_menu:not(.collapsed) ~ .menu-overlay {
+    display: block;
+  }
+}
+
+/* 添加更细致的断点控制 */
+@media screen and (max-width: 480px) {
+  .message-content {
+    font-size: 14px;
+    line-height: 1.5;
+  }
+
+  .suggestion-item {
+    font-size: 14px;
+    padding: 10px;
+  }
+
+  .input-wrapper {
+    padding: 8px;
+  }
+
+  .message-input {
+    font-size: 14px;
+  }
+}
+
+/* 确保滚动流畅 */
+* {
+  -webkit-overflow-scrolling: touch;
+}
+
+/* 防止页面横向滚动 */
+body {
+  overflow-x: hidden;
+  width: 100%;
+}
+
+/* 优化触摸体验 */
+button,
+.suggestion-item,
+.conversation-item {
+  touch-action: manipulation;
+}
 </style>
 <style lang="less" scoped>
 ::v-deep .message-content {