mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(provider): show image previews in model test dialog
This commit is contained in:
@@ -393,6 +393,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="resultImagePreviews.length > 0"
|
||||
class="rounded-lg border border-border/60 bg-muted/20 p-3 space-y-3"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
||||
<span>图片预览</span>
|
||||
<span>{{ resultImagePreviews.length }} 张</span>
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<button
|
||||
v-for="(preview, index) in resultImagePreviews"
|
||||
:key="`${preview.src}-${index}`"
|
||||
type="button"
|
||||
class="group overflow-hidden rounded-md border border-border/60 bg-background text-left transition-colors hover:border-primary/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/70"
|
||||
:title="preview.label"
|
||||
@click="openImagePreview(preview)"
|
||||
>
|
||||
<div class="aspect-square w-full overflow-hidden bg-muted/30">
|
||||
<img
|
||||
:src="preview.src"
|
||||
:alt="preview.label"
|
||||
class="h-full w-full object-contain"
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2 border-t border-border/60 px-2 py-1 text-[11px] text-muted-foreground">
|
||||
<span>{{ preview.label }}</span>
|
||||
<span>{{ preview.source === 'base64' ? 'base64' : 'URL' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="shouldCollapseAttempts"
|
||||
class="flex items-center justify-between gap-3 text-xs text-muted-foreground"
|
||||
@@ -935,11 +968,17 @@ const activeImagePreview = ref<ModelTestImagePreview | null>(null)
|
||||
|
||||
watch(() => props.result, () => {
|
||||
showAllAttempts.value = false
|
||||
inspectionTab.value = 'request-body'
|
||||
inspectionExpandDepth.value = 0
|
||||
inspectionCopiedStates.value = {}
|
||||
const defaultAttempt = inspectableAttempts.value[0] ?? resultAttempts.value[0] ?? null
|
||||
const defaultAttempt = resultImageAttempt.value
|
||||
?? resultAttempts.value.find(attempt => attempt.status === 'success')
|
||||
?? inspectableAttempts.value[0]
|
||||
?? resultAttempts.value[0]
|
||||
?? null
|
||||
selectedInspectionKey.value = defaultAttempt ? inspectionKey(defaultAttempt) : null
|
||||
inspectionTab.value = defaultAttempt && attemptImagePreviews(defaultAttempt).length > 0
|
||||
? 'response-body'
|
||||
: 'request-body'
|
||||
})
|
||||
|
||||
const shouldCollapseAttempts = computed(() => resultAttempts.value.length > 20)
|
||||
@@ -1251,6 +1290,18 @@ const selectedInspectionImagePreviews = computed(() => (
|
||||
: []
|
||||
))
|
||||
|
||||
const resultImageAttempt = computed(() => {
|
||||
return resultAttempts.value.find(attempt => attempt.status === 'success' && attemptImagePreviews(attempt).length > 0)
|
||||
?? resultAttempts.value.find(attempt => attemptImagePreviews(attempt).length > 0)
|
||||
?? null
|
||||
})
|
||||
|
||||
const resultImagePreviews = computed(() => (
|
||||
resultImageAttempt.value
|
||||
? extractModelTestImagePreviews(resultImageAttempt.value.response_body)
|
||||
: []
|
||||
))
|
||||
|
||||
const resultWinningTitle = computed(() => {
|
||||
const summary = resultSummary.value
|
||||
const keyName = summary.winning_key_name || summary.winning_key_id
|
||||
@@ -1432,7 +1483,7 @@ function inspectionKey(attempt: TestAttemptDetail): string {
|
||||
|
||||
function selectInspectionAttempt(attempt: TestAttemptDetail) {
|
||||
selectedInspectionKey.value = inspectionKey(attempt)
|
||||
inspectionTab.value = 'request-body'
|
||||
inspectionTab.value = attemptImagePreviews(attempt).length > 0 ? 'response-body' : 'request-body'
|
||||
}
|
||||
|
||||
function hasDebugData(attempt: TestAttemptDetail): boolean {
|
||||
|
||||
@@ -479,6 +479,23 @@ describe('extractModelTestResponsePreview', () => {
|
||||
})).toBe('图片:https://example.com/generated.png')
|
||||
})
|
||||
|
||||
it('extracts renderable URL image previews from OpenAI image responses', () => {
|
||||
expect(extractModelTestImagePreviews({
|
||||
data: [
|
||||
{
|
||||
url: 'https://example.com/generated.png',
|
||||
revised_prompt: 'A generated image',
|
||||
},
|
||||
],
|
||||
})).toEqual([
|
||||
{
|
||||
src: 'https://example.com/generated.png',
|
||||
label: '图片 1',
|
||||
source: 'url',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('summarizes base64 image responses without dumping the image payload', () => {
|
||||
expect(extractModelTestResponsePreview({
|
||||
data: [
|
||||
@@ -506,6 +523,55 @@ describe('extractModelTestResponsePreview', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('accepts root-relative URLs in OpenAI image responses', () => {
|
||||
expect(extractModelTestImagePreviews({
|
||||
data: [
|
||||
{
|
||||
url: '/v1/files/image?id=2af14311-a0cb-4bbf-ae20-d1fcf44e0479',
|
||||
},
|
||||
],
|
||||
})).toEqual([
|
||||
{
|
||||
src: '/v1/files/image?id=2af14311-a0cb-4bbf-ae20-d1fcf44e0479',
|
||||
label: '图片 1',
|
||||
source: 'url',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('parses stringified JSON image responses', () => {
|
||||
expect(extractModelTestImagePreviews(JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
url: 'https://example.com/generated.png',
|
||||
},
|
||||
],
|
||||
}))).toEqual([
|
||||
{
|
||||
src: 'https://example.com/generated.png',
|
||||
label: '图片 1',
|
||||
source: 'url',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('parses stringified JSON base64 image responses', () => {
|
||||
expect(extractModelTestImagePreviews(JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
b64_json: 'aGVsbG8=',
|
||||
mime_type: 'image/jpeg',
|
||||
},
|
||||
],
|
||||
}))).toEqual([
|
||||
{
|
||||
src: 'data:image/jpeg;base64,aGVsbG8=',
|
||||
label: '图片 1',
|
||||
source: 'base64',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('extracts image previews from nested response image urls', () => {
|
||||
expect(extractModelTestImagePreviews({
|
||||
output: [
|
||||
|
||||
@@ -10,16 +10,17 @@ export type ModelTestImagePreview = {
|
||||
}
|
||||
|
||||
export function extractModelTestResponsePreview(responseBody: unknown): string | null {
|
||||
const text = extractResponseText(responseBody)
|
||||
const normalizedBody = normalizeModelTestResponseBody(responseBody)
|
||||
const text = extractResponseText(normalizedBody)
|
||||
if (text) return text
|
||||
|
||||
const reasoning = extractResponseReasoning(responseBody)
|
||||
const reasoning = extractResponseReasoning(normalizedBody)
|
||||
if (reasoning) return `推理:${reasoning}`
|
||||
|
||||
const image = extractImagePreview(responseBody)
|
||||
const image = extractImagePreview(normalizedBody)
|
||||
if (image) return image
|
||||
|
||||
const summary = extractResponseSummary(responseBody)
|
||||
const summary = extractResponseSummary(normalizedBody)
|
||||
if (summary) return summary
|
||||
|
||||
return null
|
||||
@@ -27,10 +28,26 @@ export function extractModelTestResponsePreview(responseBody: unknown): string |
|
||||
|
||||
export function extractModelTestImagePreviews(responseBody: unknown): ModelTestImagePreview[] {
|
||||
const previews: ModelTestImagePreview[] = []
|
||||
collectImagePreviews(responseBody, previews, new Set(), 0)
|
||||
collectImagePreviews(normalizeModelTestResponseBody(responseBody), previews, new Set(), 0)
|
||||
return previews
|
||||
}
|
||||
|
||||
function normalizeModelTestResponseBody(value: unknown): unknown {
|
||||
if (typeof value !== 'string') return value
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return value
|
||||
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
try {
|
||||
return JSON.parse(trimmed)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function isJsonRecord(value: unknown): value is JsonRecord {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
@@ -146,6 +163,7 @@ function collectImagePreviews(
|
||||
value.output,
|
||||
value.images,
|
||||
value.content,
|
||||
value.item,
|
||||
]
|
||||
for (const nested of nestedValues) {
|
||||
collectImagePreviews(nested, previews, seen, depth + 1)
|
||||
@@ -203,6 +221,9 @@ function imageUrlToPreview(value: unknown, source: 'url'): ModelTestImagePreview
|
||||
if (url.startsWith('data:image/')) {
|
||||
return { src: url, label: 'base64', source: 'base64' }
|
||||
}
|
||||
if (url.startsWith('/')) {
|
||||
return { src: url, label: 'URL', source }
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
|
||||
Reference in New Issue
Block a user