|
@@ -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;
|