Просмотр исходного кода

feat(speaking): render AI image bubble for video scene mode

jimmylee 1 месяц назад
Родитель
Сommit
fd1db46599
1 измененных файлов с 87 добавлено и 68 удалено
  1. 87 68
      src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue

+ 87 - 68
src/views/Editor/EnglishSpeaking/preview/DialogueChatView.vue

@@ -65,79 +65,84 @@
           <div v-if="message.role === 'ai'" class="msg-row msg-ai fade-in">
           <div v-if="message.role === 'ai'" class="msg-row msg-ai fade-in">
             <div class="avatar-sm">{{ aiAvatar }}</div>
             <div class="avatar-sm">{{ aiAvatar }}</div>
             <div class="msg-col">
             <div class="msg-col">
-              <!-- 音频条 -->
-              <div v-if="message.content || message.status === 'done'" class="voice-bar voice-ai">
-                <button
-                  class="play-btn play-ai"
-                  :class="{ 'play-btn-error': player.errorId.value === message.id }"
-                  @click="togglePlay(message.id)"
-                >
-                  <svg
-                    v-if="player.loadingId.value === message.id"
-                    class="play-spinner"
-                    width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
-                    stroke-width="2" stroke-linecap="round"
-                  >
-                    <path d="M21 12a9 9 0 1 1-6.219-8.56" />
-                  </svg>
-                  <svg
-                    v-else-if="player.playingId.value === message.id"
-                    width="12" height="12" viewBox="0 0 24 24" fill="currentColor"
-                  >
-                    <rect x="6" y="4" width="4" height="16" /><rect x="14" y="4" width="4" height="16" />
-                  </svg>
-                  <svg
-                    v-else-if="player.errorId.value === message.id"
-                    width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
-                    stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
-                  >
-                    <path d="M12 9v4" />
-                    <path d="M12 17h.01" />
-                    <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
-                  </svg>
-                  <svg
-                    v-else
-                    width="12" height="12" viewBox="0 0 24 24" fill="currentColor"
+              <div v-if="message.imageUrl" class="ai-image-wrap">
+                <img :src="message.imageUrl" class="ai-image-bubble" alt="AI generated image" />
+              </div>
+              <template v-else>
+                <!-- 音频条 -->
+                <div v-if="message.content || message.status === 'done'" class="voice-bar voice-ai">
+                  <button
+                    class="play-btn play-ai"
+                    :class="{ 'play-btn-error': player.errorId.value === message.id }"
+                    @click="togglePlay(message.id)"
                   >
                   >
-                    <polygon points="5 3 19 12 5 21 5 3" />
-                  </svg>
-                </button>
-                <div class="wave-bar-group">
-                  <div
-                    v-for="i in 14"
-                    :key="i"
-                    class="wave-bar wave-ai"
-                    :style="{ height: `${Math.abs(Math.sin(i * 0.7)) * 8 + 3}px` }"
-                  />
+                    <svg
+                      v-if="player.loadingId.value === message.id"
+                      class="play-spinner"
+                      width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                      stroke-width="2" stroke-linecap="round"
+                    >
+                      <path d="M21 12a9 9 0 1 1-6.219-8.56" />
+                    </svg>
+                    <svg
+                      v-else-if="player.playingId.value === message.id"
+                      width="12" height="12" viewBox="0 0 24 24" fill="currentColor"
+                    >
+                      <rect x="6" y="4" width="4" height="16" /><rect x="14" y="4" width="4" height="16" />
+                    </svg>
+                    <svg
+                      v-else-if="player.errorId.value === message.id"
+                      width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                      stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
+                    >
+                      <path d="M12 9v4" />
+                      <path d="M12 17h.01" />
+                      <path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
+                    </svg>
+                    <svg
+                      v-else
+                      width="12" height="12" viewBox="0 0 24 24" fill="currentColor"
+                    >
+                      <polygon points="5 3 19 12 5 21 5 3" />
+                    </svg>
+                  </button>
+                  <div class="wave-bar-group">
+                    <div
+                      v-for="i in 14"
+                      :key="i"
+                      class="wave-bar wave-ai"
+                      :style="{ height: `${Math.abs(Math.sin(i * 0.7)) * 8 + 3}px` }"
+                    />
+                  </div>
+                  <span
+                    v-if="player.errorId.value === message.id"
+                    class="play-error-hint"
+                  >点击重试</span>
                 </div>
                 </div>
-                <span
-                  v-if="player.errorId.value === message.id"
-                  class="play-error-hint"
-                >点击重试</span>
-              </div>
 
 
-              <!-- 英文文本 -->
-              <div v-if="showEnglishText && message.content" class="bubble bubble-ai">
-                {{ message.content }}
-              </div>
+                <!-- 英文文本 -->
+                <div v-if="showEnglishText && message.content" class="bubble bubble-ai">
+                  {{ message.content }}
+                </div>
 
 
-              <!-- 流式加载中 -->
-              <div v-if="message.status === 'loading' && !message.content" class="typing-bubble">
-                <span class="typing-dot" style="animation-delay: 0ms" />
-                <span class="typing-dot" style="animation-delay: 150ms" />
-                <span class="typing-dot" style="animation-delay: 300ms" />
-              </div>
+                <!-- 流式加载中 -->
+                <div v-if="message.status === 'loading' && !message.content" class="typing-bubble">
+                  <span class="typing-dot" style="animation-delay: 0ms" />
+                  <span class="typing-dot" style="animation-delay: 150ms" />
+                  <span class="typing-dot" style="animation-delay: 300ms" />
+                </div>
 
 
-              <!-- AI 错误 -->
-              <div v-if="message.status === 'error'" class="error-card">
-                <span class="error-text">{{ message.error || '生成失败' }}</span>
-                <button
-                  v-if="hasRetryButton(message)"
-                  class="retry-btn"
-                  :disabled="engine.greetingInflight.value"
-                  @click="handleRetry(message)"
-                >{{ retryButtonLabel(message) }}</button>
-              </div>
+                <!-- AI 错误 -->
+                <div v-if="message.status === 'error'" class="error-card">
+                  <span class="error-text">{{ message.error || '生成失败' }}</span>
+                  <button
+                    v-if="hasRetryButton(message)"
+                    class="retry-btn"
+                    :disabled="engine.greetingInflight.value"
+                    @click="handleRetry(message)"
+                  >{{ retryButtonLabel(message) }}</button>
+                </div>
+              </template>
             </div>
             </div>
           </div>
           </div>
 
 
@@ -1323,6 +1328,20 @@ onUnmounted(() => {
   color: #374151;
   color: #374151;
   box-shadow: 0 1px 2px rgba(0,0,0,0.05);
   box-shadow: 0 1px 2px rgba(0,0,0,0.05);
 }
 }
+
+.ai-image-wrap {
+  margin-top: 4px;
+  max-width: 320px;
+}
+
+.ai-image-bubble {
+  width: 100%;
+  border-radius: 12px;
+  display: block;
+  background: #f3f4f6;
+  border: 1px solid #e5e7eb;
+}
+
 .bubble-student {
 .bubble-student {
   background: #fff7ed;
   background: #fff7ed;
   border: 1px solid #fed7aa;
   border: 1px solid #fed7aa;