feat(web): enlarge user message images on click

Wrap user-message image thumbnails in a button that opens the existing image preview panel, matching the tool-result image behavior. Adds the en/zh aria-label and resets the button chrome so the thumbnail looks unchanged.
This commit is contained in:
qer 2026-07-03 13:29:35 +08:00
parent 276407d2a4
commit 1da6446437
4 changed files with 64 additions and 16 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---
Click an image in a user message to open its full-size preview in the web chat.

View file

@ -8,6 +8,7 @@ import ToolGroup from './ToolGroup.vue';
import Markdown from './Markdown.vue';
import ThinkingBlock from './ThinkingBlock.vue';
import ActivityNotice from './ActivityNotice.vue';
import AuthMedia from './AuthMedia.vue';
import MoonSpinner from '../ui/MoonSpinner.vue';
import Spinner from '../ui/Spinner.vue';
import Icon from '../ui/Icon.vue';
@ -487,6 +488,12 @@ function copyUserMessage(turn: ChatTurn): void {
}).catch(() => {/* ignore */});
}
function userImageMedia(img: { url: string; alt?: string }): ToolMedia {
// User-uploaded images carry no path/mime metadata; the preview panel falls
// back to a generic label and sniffs the mime from the URL when needed.
return { kind: 'image', url: img.url, path: img.alt };
}
function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): boolean {
if (turn.id !== streamingTurnId.value) return false;
return block.sourceIndex === turnBlocks(turn).length - 1;
@ -537,21 +544,28 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }):
<!-- Image / video attachments -->
<div v-if="turn.images && turn.images.length > 0" class="u-imgs">
<template v-for="(img, ii) in turn.images" :key="ii">
<video
<AuthMedia
v-if="img.kind === 'video'"
class="u-img"
:src="img.url"
controls
playsinline
preload="metadata"
:url="img.url"
kind="video"
:file-id="img.fileId"
media-class="u-img"
/>
<img
<button
v-else
class="u-img"
:src="img.url"
:alt="img.alt || ''"
loading="lazy"
/>
type="button"
class="u-img-btn"
:aria-label="t('filePreview.enlargeImage')"
@click="emit('openMedia', userImageMedia(img))"
>
<AuthMedia
:url="img.url"
kind="image"
:alt="img.alt"
:file-id="img.fileId"
media-class="u-img"
/>
</button>
</template>
</div>
<!-- Skill activation card (replaces raw XML) -->
@ -716,10 +730,16 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }):
</span>
</button>
<div v-if="hasImages(item)" class="q-imgs">
<template v-for="(att, ai) in item.attachments" :key="ai">
<video v-if="att.kind === 'video'" class="q-img" :src="att.url" muted playsinline preload="metadata" />
<img v-else class="q-img" :src="att.url" alt="" loading="lazy" />
</template>
<AuthMedia
v-for="(att, ai) in item.attachments"
:key="ai"
:url="att.url"
:kind="att.kind"
:file-id="att.fileId"
media-class="q-img"
:controls="false"
muted
/>
</div>
<span v-if="qi === 0" class="q-tag q-tag-next">{{ t('composer.queueNext') }}</span>
<span v-else class="q-tag q-tag-idx">#{{ qi + 1 }}</span>
@ -1078,6 +1098,27 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }):
border-radius: 8px;
object-fit: cover;
}
/* Clickable image thumbnail reset button chrome so it looks like the plain
image it replaced, while still opening the preview on click. */
.u-img-btn {
display: block;
flex: none;
align-self: flex-start;
max-width: 100%;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
}
.u-img-btn .u-img {
display: block;
}
.u-img-btn:focus-visible {
outline: none;
box-shadow: var(--p-focus-ring);
}
/* NOTE: Chat/bubble styles live in src/style.css (global). Scoped `.u-bub`
rules here did NOT win the cascade, so they were moved to the global sheet. */

View file

@ -24,6 +24,7 @@ export default {
binaryNoPreview: 'Binary file · {mime} · {size} bytes · preview unavailable',
unknownType: 'unknown type',
copyCode: 'Copy code',
enlargeImage: 'Enlarge image',
errors: {
emptyPath: 'File path is empty',
unsupportedPath: 'URLs and remote paths cannot be previewed',

View file

@ -24,6 +24,7 @@ export default {
binaryNoPreview: '二进制文件 · {mime} · {size} 字节 · 暂不预览',
unknownType: '未知类型',
copyCode: '复制代码',
enlargeImage: '放大图片',
errors: {
emptyPath: '文件路径为空',
unsupportedPath: '不支持预览 URL 或远程路径',