Fix streaming response UI issues in OutputPanel
- Fix loading indicator visibility during streaming - Fix copy button not showing due to hideTitle attribute - Prevent token count reset after stream completion - Add resultTitle prop for custom panel titles
This commit is contained in:
1 parent
3b8b59941c
commit
fd44067c86
4 files changed
+47
-38
No files matched your search
@@ -1,14 +1,17 @@
|
||||
<!-- 输出面板组件 -->
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div v-if="!hideTitle" class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold theme-text">{{ t('output.title') }}</h3>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 v-if="resultTitle" class="text-lg font-semibold theme-text">{{ resultTitle }}</h3>
|
||||
<div v-else-if="!hideTitle" class="text-lg font-semibold theme-text">{{ t('output.title') }}</div>
|
||||
<div v-else></div>
|
||||
|
||||
<button
|
||||
v-if="result"
|
||||
v-if="contentTokens.length > 0 && !isStreaming"
|
||||
@click="copySelectedText"
|
||||
class="px-3 py-1.5 theme-button-secondary flex items-center space-x-2"
|
||||
>
|
||||
<span>{{ t('common.copy') }}</span>
|
||||
<span>{{ t('prompt.copy') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -32,11 +35,11 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="loading && !isStreaming"
|
||||
v-if="(loading || (isStreaming && contentTokens.length === 0))"
|
||||
class="absolute top-2 right-2 theme-loading px-3 py-1.5 rounded-lg flex items-center space-x-2"
|
||||
>
|
||||
<span class="animate-spin">⏳</span>
|
||||
<span>{{ t('common.processing') }}</span>
|
||||
<span>{{ t('output.processing') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,6 +84,7 @@ interface Props {
|
||||
error?: string;
|
||||
result?: string;
|
||||
hideTitle?: boolean;
|
||||
resultTitle?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -92,7 +96,7 @@ const emit = defineEmits<{
|
||||
|
||||
// 监听 result 变化
|
||||
watch(() => props.result, (newVal) => {
|
||||
if (!isStreaming.value && newVal) {
|
||||
if (!selfUpdate && !isStreaming.value && newVal) {
|
||||
contentTokens.value = [newVal]
|
||||
}
|
||||
})
|
||||
@@ -109,7 +113,7 @@ interface StreamHandlers {
|
||||
onComplete: () => void;
|
||||
onError: (error: Error) => void;
|
||||
}
|
||||
|
||||
let selfUpdate = false;
|
||||
// 处理流式响应
|
||||
const handleStream = (): StreamHandlers => {
|
||||
console.log('Starting stream handling, container exists:', !!resultContainer.value);
|
||||
@@ -130,8 +134,16 @@ const handleStream = (): StreamHandlers => {
|
||||
},
|
||||
onComplete: () => {
|
||||
console.log('Stream completed, total tokens:', contentTokens.value.length);
|
||||
const finalContent = contentTokens.value.join(''); // 保存一份最终内容
|
||||
isStreaming.value = false;
|
||||
emit('update:result', displayContent.value);
|
||||
|
||||
// 设置标记,表示这是自身触发的更新
|
||||
selfUpdate = true;
|
||||
emit('update:result', finalContent);
|
||||
// 给标记一个恢复的延时
|
||||
setTimeout(() => {
|
||||
selfUpdate = false;
|
||||
}, 100);
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error('Stream error:', error);
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
@click="copyPrompt"
|
||||
class="px-3 py-1.5 theme-button-secondary flex items-center space-x-2"
|
||||
>
|
||||
<span>{{ t('common.copy') }}</span>
|
||||
<span>{{ t('prompt.copy') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,39 +42,35 @@
|
||||
<div class="flex-1 min-h-0 overflow-y-auto mt-5">
|
||||
<div class="relative h-full">
|
||||
<!-- Original Prompt Test Result -->
|
||||
<div
|
||||
v-show="isCompareMode"
|
||||
<div
|
||||
v-show="isCompareMode"
|
||||
class="absolute inset-0 flex flex-col md:w-[calc(50%-6px)] md:mr-3"
|
||||
>
|
||||
<h3 class="text-lg font-semibold theme-text mb-2">{{ t('test.originalResult') }}</h3>
|
||||
<OutputPanelUI
|
||||
ref="originalOutputPanelRef"
|
||||
:loading="isTestingOriginal"
|
||||
:error="originalTestError"
|
||||
:result="originalTestResult"
|
||||
v-model:result="originalTestResult"
|
||||
class="flex-1"
|
||||
hide-title
|
||||
:resultTitle="t('test.originalResult')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Optimized Prompt Test Result -->
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col"
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col"
|
||||
:class="{
|
||||
'md:w-[calc(50%-6px)] md:left-[calc(50%+6px)] transition-[width,left] duration-300': isCompareMode,
|
||||
'w-full': !isCompareMode
|
||||
'md:w-[calc(50%-6px)] md:left-[calc(50%+6px)] transition-[width,left] duration-300': isCompareMode,
|
||||
'w-full': !isCompareMode
|
||||
}"
|
||||
>
|
||||
<h3 class="text-lg font-semibold theme-text mb-2 truncate">
|
||||
{{ isCompareMode ? t('test.optimizedResult') : t('test.testResult') }}
|
||||
</h3>
|
||||
<OutputPanelUI
|
||||
ref="optimizedOutputPanelRef"
|
||||
:loading="isTestingOptimized"
|
||||
:error="optimizedTestError"
|
||||
:result="optimizedTestResult"
|
||||
v-model:result="optimizedTestResult"
|
||||
class="flex-1"
|
||||
hide-title
|
||||
:resultTitle="isCompareMode ? t('test.optimizedResult') : t('test.testResult')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -162,18 +158,18 @@ const ensureString = (value) => {
|
||||
// Test original prompt
|
||||
const testOriginalPrompt = async () => {
|
||||
if (!props.originalPrompt || !testContent.value) return
|
||||
|
||||
|
||||
isTestingOriginal.value = true
|
||||
originalTestError.value = ''
|
||||
|
||||
|
||||
try {
|
||||
if (originalOutputPanelRef.value) {
|
||||
const streamHandler = originalOutputPanelRef.value.handleStream()
|
||||
|
||||
|
||||
// Ensure prompt is a string
|
||||
const promptStr = ensureString(props.originalPrompt)
|
||||
console.log('[TestPanel] Original prompt type:', typeof promptStr, promptStr.substring(0, 30))
|
||||
|
||||
|
||||
await props.promptService.testPromptStream(
|
||||
promptStr,
|
||||
testContent.value,
|
||||
@@ -197,20 +193,20 @@ const testOriginalPrompt = async () => {
|
||||
// Test optimized prompt
|
||||
const testOptimizedPrompt = async () => {
|
||||
if (!props.optimizedPrompt || !testContent.value) return
|
||||
|
||||
|
||||
isTestingOptimized.value = true
|
||||
optimizedTestError.value = ''
|
||||
|
||||
|
||||
try {
|
||||
const outputPanel = optimizedOutputPanelRef.value
|
||||
|
||||
|
||||
if (outputPanel) {
|
||||
const streamHandler = outputPanel.handleStream()
|
||||
|
||||
|
||||
// Ensure prompt is a string
|
||||
const promptStr = ensureString(props.optimizedPrompt)
|
||||
console.log('[TestPanel] Optimized prompt type:', typeof promptStr, promptStr.substring(0, 30))
|
||||
|
||||
|
||||
await props.promptService.testPromptStream(
|
||||
promptStr,
|
||||
testContent.value,
|
||||
@@ -239,7 +235,7 @@ const handleTest = async () => {
|
||||
optimizedTestError.value = errorMsg
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (isCompareMode.value) {
|
||||
// Compare test mode: test both original and optimized prompts
|
||||
try {
|
||||
@@ -264,6 +260,7 @@ const handleTest = async () => {
|
||||
|
||||
// Component mounted, if there is a default model, select it automatically
|
||||
onMounted(() => {
|
||||
console.log("hideTitle value:", originalOutputPanelRef.value?.hideTitle);
|
||||
if (props.modelValue) {
|
||||
selectedTestModel.value = props.modelValue
|
||||
}
|
||||
@@ -288,4 +285,4 @@ defineExpose({
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -622,7 +622,7 @@ html * {
|
||||
/* ===== 状态组件 ===== */
|
||||
/* 主题加载中 */
|
||||
.theme-loading {
|
||||
@apply text-teal-700;
|
||||
@apply text-teal-300;
|
||||
}
|
||||
/* 主题错误 */
|
||||
.theme-error {
|
||||
@@ -1100,7 +1100,7 @@ html * {
|
||||
/* ===== 状态组件 ===== */
|
||||
/* 主题加载中 */
|
||||
.theme-loading {
|
||||
@apply text-purple-400 animate-pulse;
|
||||
@apply text-purple-400;
|
||||
}
|
||||
|
||||
/* 主题错误 */
|
||||
|
||||
Reference in new issue
Block a user