mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-16 04:05:15 +00:00
* fix(web-shell): i18n for ~43 hardcoded English strings across 15 files Multi-round audit replaced hardcoded UI strings with t() calls and added en/zh-CN i18n keys for: - Session timeline labels, aria-labels, and kind labels (MessageList) - SubAgent tab labels, tools count, pending/running status - Auth protocol option labels, placeholders, API key masking - Voice dictation UI states (VoiceButton) - Copy tooltip, Runtime badge, N/A, Server/Agent fallbacks - Mermaid code block labels, image alt text, model switch summary * fix(web-shell): i18n for missed models placeholder in AuthMessage * fix(web-shell): invalidate timeline cache on locale switch Store the t function reference in sessionTimelineCache alongside the message signature. When the locale changes, t gets a new reference, triggering cache invalidation and re-generating entries with the new language labels.
124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
import {
|
|
memo,
|
|
useMemo,
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { isSafeImageSrc } from './Markdown';
|
|
import { useWebShellCustomization } from '../../customization';
|
|
import { useI18n } from '../../i18n';
|
|
import flashStyles from '../MessageLocateFlash.module.css';
|
|
import styles from './UserMessage.module.css';
|
|
|
|
interface UserMessageImage {
|
|
data: string;
|
|
mimeType: string;
|
|
}
|
|
|
|
interface UserMessageProps {
|
|
content: string;
|
|
images?: UserMessageImage[];
|
|
isLocateFlashing?: boolean;
|
|
}
|
|
|
|
export const UserMessage = memo(function UserMessage({
|
|
content,
|
|
images,
|
|
isLocateFlashing = false,
|
|
}: UserMessageProps) {
|
|
const { t } = useI18n();
|
|
const { renderUserMessageContent } = useWebShellCustomization();
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
const [expanded, setExpanded] = useState(false);
|
|
const [heightOverflowing, setHeightOverflowing] = useState(false);
|
|
const renderedContent = useMemo(
|
|
() => renderUserMessageContent?.({ content, images }) ?? content,
|
|
[content, images, renderUserMessageContent],
|
|
);
|
|
|
|
const measureOverflow = useCallback(() => {
|
|
const el = contentRef.current;
|
|
if (!el) return;
|
|
setHeightOverflowing(el.scrollHeight > 400);
|
|
}, []);
|
|
|
|
useLayoutEffect(() => {
|
|
setExpanded(false);
|
|
measureOverflow();
|
|
}, [content, images?.length, measureOverflow]);
|
|
|
|
useEffect(() => {
|
|
const el = contentRef.current;
|
|
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
const observer = new ResizeObserver(measureOverflow);
|
|
observer.observe(el);
|
|
return () => observer.disconnect();
|
|
}, [measureOverflow]);
|
|
|
|
return (
|
|
<div className={styles.chatMessageRow}>
|
|
<div
|
|
className={`${styles.chatBubble}${
|
|
isLocateFlashing ? ` ${flashStyles.flash}` : ''
|
|
}`}
|
|
>
|
|
<div
|
|
ref={contentRef}
|
|
className={`${styles.chatContent} ${
|
|
heightOverflowing && !expanded ? styles.chatContentCollapsed : ''
|
|
}`}
|
|
>
|
|
{images && images.length > 0 && (
|
|
<div className={styles.chatImages}>
|
|
{images.map((img, index) => {
|
|
const src = img.data.startsWith('data:')
|
|
? img.data
|
|
: `data:${img.mimeType};base64,${img.data}`;
|
|
if (!isSafeImageSrc(src)) return null;
|
|
return (
|
|
<img
|
|
key={index}
|
|
src={src}
|
|
alt={t('user.uploadedImage', { index: index + 1 })}
|
|
className={styles.chatImageThumb}
|
|
onLoad={measureOverflow}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
{renderedContent}
|
|
</div>
|
|
{heightOverflowing && (
|
|
<button
|
|
type="button"
|
|
className={styles.toggleButton}
|
|
onClick={() => setExpanded((value) => !value)}
|
|
>
|
|
<span>
|
|
{expanded ? t('userMessage.showLess') : t('userMessage.showMore')}
|
|
</span>
|
|
<svg
|
|
className={`${styles.toggleIcon} ${
|
|
expanded ? styles.toggleIconExpanded : ''
|
|
}`}
|
|
viewBox="0 0 16 16"
|
|
aria-hidden="true"
|
|
>
|
|
<path
|
|
d="m4 6 4 4 4-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|