/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/widgets/ChatInput/fileUpload/ChatUploadedFile.tsx
225 строк
7 KB
Nico Bellante
[feat] Add image thumbnails to `st.chat_input` file uploads (#13547)
12 янв 2026, 20:02
Не верифицирован
12 янв 2026, 20:02
377c595
Код
Авторство
О чём код?
/** * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { FC, memo, useCallback, useId } from "react" import { ErrorOutline } from "@emotion-icons/material-outlined" import { Cancel } from "@emotion-icons/material-rounded" import BaseButton, { BaseButtonKind } from "~lib/components/shared/BaseButton" import Icon, { DynamicIcon } from "~lib/components/shared/Icon" import Tooltip, { Placement } from "~lib/components/shared/Tooltip" import { UploadFileInfo } from "~lib/components/widgets/FileUploader/UploadFileInfo" import { assertNever } from "~lib/util/assertNever" import { FileSize, getSizeDisplay } from "~lib/util/FileHelper" import { getFileTypeIcon } from "./getFileTypeIcon" import { StyledChatUploadedFile, StyledChatUploadedFileDeleteButton, StyledChatUploadedFileIconContainer, StyledChatUploadedFileImagePreview, StyledChatUploadedFileInfo, StyledChatUploadedFileName, StyledChatUploadedFileSize, StyledVisuallyHidden, } from "./styled-components" import { truncateFilename } from "./truncateFilename" import { useImagePreview } from "./useImagePreview" export interface Props { fileInfo: UploadFileInfo onDelete: (id: number) => void onRetry?: (fileInfo: UploadFileInfo) => void } export interface ChatUploadedFileIconProps { fileInfo: UploadFileInfo imagePreviewUrl: string | null } export const ChatUploadedFileIcon: FC<ChatUploadedFileIconProps> = ({ fileInfo, imagePreviewUrl, }) => { const { type } = fileInfo.status switch (type) { case "uploading": return ( <DynamicIcon iconValue="spinner" testid="stChatInputFileIconSpinner" size="lg" /> ) case "error": return ( <Icon content={ErrorOutline} size="lg" testid="stChatInputFileIconError" /> ) case "uploaded": if (imagePreviewUrl) { return ( <StyledChatUploadedFileImagePreview src={imagePreviewUrl} alt={fileInfo.name} data-testid="stChatInputFileImagePreview" /> ) } return <Icon content={getFileTypeIcon(fileInfo.name)} size="lg" /> default: assertNever(type) return null } } const ChatUploadedFile = ({ fileInfo, onDelete, onRetry, }: Props): React.ReactElement => { const statusType = fileInfo.status.type const isError = statusType === "error" const isUploading = statusType === "uploading" const canRetry = isError && onRetry !== undefined && fileInfo.file !== undefined // Generate unique ID for aria-describedby linking const errorId = useId() // Create image preview URL for image files const imagePreviewUrl = useImagePreview(fileInfo.file, fileInfo.name) // Extract error message once to avoid duplication const errorMessage = fileInfo.status.type === "error" ? fileInfo.status.errorMessage : "Upload failed" const handleChipClick = useCallback(() => { if (canRetry) { onRetry(fileInfo) } }, [canRetry, onRetry, fileInfo]) const handleChipKeyDown = useCallback( (e: React.KeyboardEvent) => { if (canRetry && (e.key === "Enter" || e.key === " ")) { e.preventDefault() onRetry(fileInfo) } }, [canRetry, onRetry, fileInfo] ) const handleDeleteClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation() onDelete(fileInfo.id) }, [onDelete, fileInfo.id] ) // Determine aria-label for delete button based on state const deleteButtonAriaLabel = isUploading ? `Cancel upload of ${fileInfo.name}` : `Remove ${fileInfo.name}` // Determine aria-label for the file chip // Keep aria-label stable (name + size) regardless of error state to avoid // double-announcing errors - screen readers get error info via aria-invalid // and the visually hidden error message linked by aria-describedby const sizeDisplay = getSizeDisplay(fileInfo.size, FileSize.Byte) const chipAriaLabel = `${fileInfo.name}, ${sizeDisplay}` const fileChip = ( <StyledChatUploadedFile className="stChatInputFile" data-testid="stChatInputFile" isError={isError} isClickable={canRetry} onClick={canRetry ? handleChipClick : undefined} onKeyDown={canRetry ? handleChipKeyDown : undefined} title={canRetry ? "Click to retry upload" : undefined} role={canRetry ? "button" : undefined} tabIndex={canRetry ? 0 : undefined} aria-label={chipAriaLabel} aria-invalid={isError || undefined} aria-describedby={isError ? errorId : undefined} > <StyledChatUploadedFileIconContainer fileStatus={statusType}> <ChatUploadedFileIcon fileInfo={fileInfo} imagePreviewUrl={imagePreviewUrl} /> </StyledChatUploadedFileIconContainer> <StyledChatUploadedFileInfo> <StyledChatUploadedFileName className="stChatInputFileName" data-testid="stChatInputFileName" title={fileInfo.name} fileStatus={fileInfo.status} > {truncateFilename(fileInfo.name)} </StyledChatUploadedFileName> <StyledChatUploadedFileSize> {getSizeDisplay(fileInfo.size, FileSize.Byte)} </StyledChatUploadedFileSize> </StyledChatUploadedFileInfo> <StyledChatUploadedFileDeleteButton data-testid="stChatInputDeleteBtn" isError={isError} > <BaseButton onClick={handleDeleteClick} kind={BaseButtonKind.MINIMAL} aria-label={deleteButtonAriaLabel} > <Icon content={Cancel} size="md" /> </BaseButton> </StyledChatUploadedFileDeleteButton> {/* Accessibility: Error messages are shown in a tooltip on hover, but tooltips are portals rendered outside this component and use accessibilityType="tooltip", which assistive tech treats as supplementary info. To meet WCAG 3.3.1 (Error Identification), we include a visually hidden element with role="alert" that screen readers announce immediately, linked via aria-describedby above. */} {isError && ( <StyledVisuallyHidden id={errorId} role="alert"> Error: {errorMessage} </StyledVisuallyHidden> )} </StyledChatUploadedFile> ) if (isError) { return ( <Tooltip content={errorMessage} placement={Placement.TOP} error> {fileChip} </Tooltip> ) } return fileChip } export default memo(ChatUploadedFile)