-
Notifications
You must be signed in to change notification settings - Fork 625
refactor: standardize image block data structure #1082
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,17 +4,17 @@ | |
| <div class="flex flex-col space-y-2"> | ||
| <!-- 图片加载区域 --> | ||
| <div class="flex justify-center"> | ||
| <template v-if="block.image_data"> | ||
| <template v-if="resolvedImageData"> | ||
| <img | ||
| v-if="block.image_data.mimeType === 'deepchat/image-url'" | ||
| :src="`${block.image_data.data}`" | ||
| v-if="resolvedImageData.mimeType === 'deepchat/image-url'" | ||
| :src="`${resolvedImageData.data}`" | ||
| class="max-w-[400px] rounded-md cursor-pointer hover:shadow-md transition-shadow" | ||
| @click="openFullImage" | ||
| @error="handleImageError" | ||
| /> | ||
| <img | ||
| v-else | ||
| :src="`data:${block.image_data.mimeType};base64,${block.image_data.data}`" | ||
| :src="`data:${resolvedImageData.mimeType};base64,${resolvedImageData.data}`" | ||
| class="max-w-[400px] rounded-md cursor-pointer hover:shadow-md transition-shadow" | ||
| @click="openFullImage" | ||
| @error="handleImageError" | ||
|
|
@@ -41,15 +41,15 @@ | |
| </DialogTitle> | ||
| </DialogHeader> | ||
| <div class="flex items-center justify-center"> | ||
| <template v-if="block.image_data"> | ||
| <template v-if="resolvedImageData"> | ||
| <img | ||
| v-if="block.image_data.mimeType === 'deepchat/image-url'" | ||
| :src="block.image_data.data" | ||
| v-if="resolvedImageData.mimeType === 'deepchat/image-url'" | ||
| :src="resolvedImageData.data" | ||
| class="rounded-md max-h-[80vh] max-w-full object-contain" | ||
| /> | ||
| <img | ||
| v-else | ||
| :src="`data:${block.image_data.mimeType};base64,${block.image_data.data}`" | ||
| :src="`data:${resolvedImageData.mimeType};base64,${resolvedImageData.data}`" | ||
| class="rounded-md max-h-[80vh] max-w-full object-contain" | ||
| /> | ||
| </template> | ||
|
|
@@ -60,7 +60,7 @@ | |
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import { ref } from 'vue' | ||
| import { computed, ref } from 'vue' | ||
| import { Icon } from '@iconify/vue' | ||
| import { AssistantMessageBlock } from '@shared/chat' | ||
| import { useI18n } from 'vue-i18n' | ||
|
|
@@ -90,15 +90,144 @@ const props = defineProps<{ | |
| threadId?: string | ||
| }>() | ||
|
|
||
| type LegacyImageBlockContent = { | ||
| data?: string | ||
| mimeType?: string | ||
| } | ||
|
|
||
| const imageError = ref(false) | ||
| const showFullImage = ref(false) | ||
|
|
||
| const inferMimeType = (data: string, mimeType?: string): string => { | ||
| if (mimeType && mimeType.trim().length > 0) { | ||
| return mimeType | ||
| } | ||
|
|
||
| if (data.startsWith('imgcache://') || data.startsWith('http://') || data.startsWith('https://')) { | ||
| return 'deepchat/image-url' | ||
| } | ||
|
|
||
| if (data.startsWith('data:image/')) { | ||
| const match = data.match(/^data:([^;]+);base64,(.*)$/) | ||
| if (match?.[1]) { | ||
| return match[1] | ||
| } | ||
| } | ||
|
|
||
| return 'image/png' | ||
| } | ||
|
|
||
| const resolvedImageData = computed(() => { | ||
| // Handle new format with image_data field | ||
| if (props.block.image_data?.data) { | ||
| const rawData = props.block.image_data.data | ||
|
|
||
| // Handle URLs | ||
| if ( | ||
| rawData.startsWith('imgcache://') || | ||
| rawData.startsWith('http://') || | ||
| rawData.startsWith('https://') | ||
| ) { | ||
| return { | ||
| data: rawData, | ||
| mimeType: 'deepchat/image-url' | ||
| } | ||
| } | ||
|
|
||
| let normalizedData = rawData | ||
| let normalizedMimeType = inferMimeType(rawData, props.block.image_data.mimeType) | ||
|
|
||
| // Handle legacy data URIs that may still exist in persisted data | ||
| if (rawData.startsWith('data:image/')) { | ||
| const match = rawData.match(/^data:([^;]+);base64,(.*)$/) | ||
| if (match?.[1] && match?.[2]) { | ||
| normalizedMimeType = match[1] | ||
| normalizedData = match[2] | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| data: normalizedData, | ||
| mimeType: normalizedMimeType | ||
| } | ||
| } | ||
|
|
||
| // Handle legacy formats (for backward compatibility) | ||
| const content = props.block.content | ||
|
|
||
| if (content && typeof content === 'object' && 'data' in (content as LegacyImageBlockContent)) { | ||
| const legacyContent = content as LegacyImageBlockContent | ||
| if (legacyContent.data) { | ||
| const rawData = legacyContent.data | ||
|
|
||
| // Handle URLs | ||
| if ( | ||
| rawData.startsWith('imgcache://') || | ||
| rawData.startsWith('http://') || | ||
| rawData.startsWith('https://') | ||
| ) { | ||
| return { | ||
| data: rawData, | ||
| mimeType: 'deepchat/image-url' | ||
| } | ||
| } | ||
|
|
||
| let normalizedData = rawData | ||
| let normalizedMimeType = inferMimeType(rawData, legacyContent.mimeType) | ||
|
|
||
| // Handle data URIs | ||
| if (rawData.startsWith('data:image/')) { | ||
| const match = rawData.match(/^data:([^;]+);base64,(.*)$/) | ||
| if (match?.[1] && match?.[2]) { | ||
| normalizedMimeType = match[1] | ||
| normalizedData = match[2] | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| data: normalizedData, | ||
| mimeType: normalizedMimeType | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (typeof content === 'string' && content.length > 0) { | ||
| if (content.startsWith('data:image/')) { | ||
| const match = content.match(/^data:([^;]+);base64,(.*)$/) | ||
| if (match?.[1] && match?.[2]) { | ||
| return { | ||
| data: match[2], | ||
| mimeType: match[1] | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if ( | ||
| content.startsWith('imgcache://') || | ||
| content.startsWith('http://') || | ||
| content.startsWith('https://') | ||
| ) { | ||
| return { | ||
| data: content, | ||
| mimeType: 'deepchat/image-url' | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| data: content, | ||
| mimeType: inferMimeType(content) | ||
| } | ||
| } | ||
|
|
||
| return null | ||
|
Comment on lines
+101
to
+222
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize legacy data URIs before rendering. const inferMimeType = (data: string, mimeType?: string): string => {
if (mimeType && mimeType.trim().length > 0) {
return mimeType
}
if (data.startsWith('imgcache://') || data.startsWith('http://') || data.startsWith('https://')) {
return 'deepchat/image-url'
}
- if (data.startsWith('data:image/')) {
- const match = data.match(/^data:([^;]+);base64,(.*)$/)
- if (match?.[1]) {
- return match[1]
- }
- }
+ if (data.startsWith('data:image/')) {
+ const header = data.slice(5, data.indexOf(',')) ?? ''
+ const mime = header.split(';')[0]?.trim()
+ if (mime) {
+ return mime
+ }
+ }
return 'image/png'
}
…
- if (rawData.startsWith('data:image/')) {
- const match = rawData.match(/^data:([^;]+);base64,(.*)$/)
+ if (rawData.startsWith('data:image/')) {
+ const match = rawData.match(/^data:([^;,]+)(?:;[^,]+)*;base64,(.*)$/i)
if (match?.[1] && match?.[2]) {
normalizedMimeType = match[1]
normalizedData = match[2]
}
}Apply the same regex tweak in the legacy branches below so every data URI path uses the sanitized payload.
🤖 Prompt for AI Agents |
||
| }) | ||
|
|
||
| const handleImageError = () => { | ||
| imageError.value = true | ||
| } | ||
|
|
||
| const openFullImage = () => { | ||
| if (props.block.image_data) { | ||
| if (resolvedImageData.value) { | ||
| showFullImage.value = true | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix data URI normalization edge cases.
The regex only matches
data:mime;base64,...with no extra parameters. Providers often emitdata:image/png;charset=utf-8;base64,..., so we fail to strip the header, fall back toimage/png, and persist the entire data URI. The renderer then buildsdata:image/png;base64,data:image/png;charset=utf-8;base64,..., which ships a broken image. Please widen the match to tolerate additional;paramsegments (and keep the real mime type) before falling back to the default.That ensures any extra parameters (charset, name, etc.) are handled and the payload stays in the expected raw/base64 form.
🤖 Prompt for AI Agents