fix(webchat): improve image paste UI layout and display
- Fix preview container width (use inline-flex + fit-content) - Fix flex layout conflict in components.css (grid -> flex column) - Change preview thumbnail to object-fit: contain (no cropping) - Add image rendering in sent message bubbles - Add CSS for chat-message-images display Improves upon #1900
This commit is contained in:
@@ -113,13 +113,16 @@
|
|||||||
|
|
||||||
/* Image attachments preview */
|
/* Image attachments preview */
|
||||||
.chat-attachments {
|
.chat-attachments {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
align-self: flex-start; /* Don't stretch in flex column parent */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-attachment {
|
.chat-attachment {
|
||||||
@@ -135,7 +138,7 @@
|
|||||||
.chat-attachment__img {
|
.chat-attachment__img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-attachment__remove {
|
.chat-attachment__remove {
|
||||||
@@ -189,6 +192,32 @@
|
|||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Message images (sent images displayed in chat) */
|
||||||
|
.chat-message-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-image {
|
||||||
|
max-width: 300px;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: contain;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-image:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User message images align right */
|
||||||
|
.chat-group.user .chat-message-images {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
/* Compose input row - horizontal layout */
|
/* Compose input row - horizontal layout */
|
||||||
.chat-compose__row {
|
.chat-compose__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1303,9 +1303,8 @@
|
|||||||
/* Chat compose */
|
/* Chat compose */
|
||||||
.chat-compose {
|
.chat-compose {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
flex-direction: column;
|
||||||
align-items: end;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,48 @@ import {
|
|||||||
} from "./message-extract";
|
} from "./message-extract";
|
||||||
import { extractToolCards, renderToolCardSidebar } from "./tool-cards";
|
import { extractToolCards, renderToolCardSidebar } from "./tool-cards";
|
||||||
|
|
||||||
|
type ImageBlock = {
|
||||||
|
url: string;
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function extractImages(message: unknown): ImageBlock[] {
|
||||||
|
const m = message as Record<string, unknown>;
|
||||||
|
const content = m.content;
|
||||||
|
const images: ImageBlock[] = [];
|
||||||
|
|
||||||
|
if (Array.isArray(content)) {
|
||||||
|
for (const block of content) {
|
||||||
|
if (typeof block !== "object" || block === null) continue;
|
||||||
|
const b = block as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (b.type === "image") {
|
||||||
|
// Handle source object format (from sendChatMessage)
|
||||||
|
const source = b.source as Record<string, unknown> | undefined;
|
||||||
|
if (source?.type === "base64" && typeof source.data === "string") {
|
||||||
|
const data = source.data as string;
|
||||||
|
const mediaType = (source.media_type as string) || "image/png";
|
||||||
|
// If data is already a data URL, use it directly
|
||||||
|
const url = data.startsWith("data:")
|
||||||
|
? data
|
||||||
|
: `data:${mediaType};base64,${data}`;
|
||||||
|
images.push({ url });
|
||||||
|
} else if (typeof b.url === "string") {
|
||||||
|
images.push({ url: b.url });
|
||||||
|
}
|
||||||
|
} else if (b.type === "image_url") {
|
||||||
|
// OpenAI format
|
||||||
|
const imageUrl = b.image_url as Record<string, unknown> | undefined;
|
||||||
|
if (typeof imageUrl?.url === "string") {
|
||||||
|
images.push({ url: imageUrl.url });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderReadingIndicatorGroup(assistant?: AssistantIdentity) {
|
export function renderReadingIndicatorGroup(assistant?: AssistantIdentity) {
|
||||||
return html`
|
return html`
|
||||||
<div class="chat-group assistant">
|
<div class="chat-group assistant">
|
||||||
@@ -163,6 +205,25 @@ function isAvatarUrl(value: string): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMessageImages(images: ImageBlock[]) {
|
||||||
|
if (images.length === 0) return nothing;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="chat-message-images">
|
||||||
|
${images.map(
|
||||||
|
(img) => html`
|
||||||
|
<img
|
||||||
|
src=${img.url}
|
||||||
|
alt=${img.alt ?? "Attached image"}
|
||||||
|
class="chat-message-image"
|
||||||
|
@click=${() => window.open(img.url, "_blank")}
|
||||||
|
/>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderGroupedMessage(
|
function renderGroupedMessage(
|
||||||
message: unknown,
|
message: unknown,
|
||||||
opts: { isStreaming: boolean; showReasoning: boolean },
|
opts: { isStreaming: boolean; showReasoning: boolean },
|
||||||
@@ -179,6 +240,8 @@ function renderGroupedMessage(
|
|||||||
|
|
||||||
const toolCards = extractToolCards(message);
|
const toolCards = extractToolCards(message);
|
||||||
const hasToolCards = toolCards.length > 0;
|
const hasToolCards = toolCards.length > 0;
|
||||||
|
const images = extractImages(message);
|
||||||
|
const hasImages = images.length > 0;
|
||||||
|
|
||||||
const extractedText = extractTextCached(message);
|
const extractedText = extractTextCached(message);
|
||||||
const extractedThinking =
|
const extractedThinking =
|
||||||
@@ -207,11 +270,12 @@ function renderGroupedMessage(
|
|||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!markdown && !hasToolCards) return nothing;
|
if (!markdown && !hasToolCards && !hasImages) return nothing;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="${bubbleClasses}">
|
<div class="${bubbleClasses}">
|
||||||
${canCopyMarkdown ? renderCopyAsMarkdownButton(markdown!) : nothing}
|
${canCopyMarkdown ? renderCopyAsMarkdownButton(markdown!) : nothing}
|
||||||
|
${renderMessageImages(images)}
|
||||||
${reasoningMarkdown
|
${reasoningMarkdown
|
||||||
? html`<div class="chat-thinking">${unsafeHTML(
|
? html`<div class="chat-thinking">${unsafeHTML(
|
||||||
toSanitizedMarkdownHtml(reasoningMarkdown),
|
toSanitizedMarkdownHtml(reasoningMarkdown),
|
||||||
|
|||||||
Reference in New Issue
Block a user