diff --git a/frontend/src/components/Chat/ChatInputArea.styles.ts b/frontend/src/components/Chat/ChatInputArea.styles.ts index f99c2f46b5..06d27b9953 100644 --- a/frontend/src/components/Chat/ChatInputArea.styles.ts +++ b/frontend/src/components/Chat/ChatInputArea.styles.ts @@ -256,6 +256,24 @@ export const useChatInputAreaStyles = makeStyles({ fontSize: tokens.fontSizeBase200, color: tokens.colorNeutralForeground2, }, + openLink: { + display: 'inline-flex', + alignItems: 'center', + gap: tokens.spacingHorizontalXXS, + padding: `0 ${tokens.spacingHorizontalS}`, + borderRadius: tokens.borderRadiusSmall, + backgroundColor: tokens.colorBrandBackground, + color: tokens.colorNeutralForegroundOnBrand, + fontSize: tokens.fontSizeBase200, + fontWeight: tokens.fontWeightSemibold as unknown as string, + textDecoration: 'none', + flexShrink: 0, + height: '20px', + ':hover': { + backgroundColor: tokens.colorBrandBackgroundHover, + color: tokens.colorNeutralForegroundOnBrand, + }, + }, unsupportedWarning: { display: 'flex', alignItems: 'center', diff --git a/frontend/src/components/Chat/ChatInputArea.test.tsx b/frontend/src/components/Chat/ChatInputArea.test.tsx index c4c893cc92..8dbb77f0ee 100644 --- a/frontend/src/components/Chat/ChatInputArea.test.tsx +++ b/frontend/src/components/Chat/ChatInputArea.test.tsx @@ -558,7 +558,7 @@ describe("ChatInputArea", () => { ); @@ -586,7 +586,7 @@ describe("ChatInputArea", () => { @@ -663,6 +663,52 @@ describe("ChatInputArea", () => { expect(onClearConversion).toHaveBeenCalled(); }); + it("should render converted file chip with Open link for text→file conversion", async () => { + render( + + + + ); + + expect(screen.getByTestId("original-banner")).toBeInTheDocument(); + const chip = screen.getByTestId("converted-file-chip"); + expect(chip).toHaveTextContent("result.pdf"); + const openLink = screen.getByTestId("converted-file-open"); + expect(openLink).toHaveAttribute("href", "/api/media?path=%2Ftmp%2Fresult.pdf"); + expect(openLink).toHaveAttribute("target", "_blank"); + }); + + it("should call onClearConvertedFileChip when chip dismiss is clicked", async () => { + const onClearConvertedFileChip = jest.fn(); + const user = userEvent.setup(); + + render( + + + + ); + + await user.click(screen.getByTestId("clear-converted-file-chip")); + expect(onClearConvertedFileChip).toHaveBeenCalledTimes(1); + }); + // --------------------------------------------------------------------------- // Unsupported modality warnings // --------------------------------------------------------------------------- diff --git a/frontend/src/components/Chat/ChatInputArea.tsx b/frontend/src/components/Chat/ChatInputArea.tsx index 82c1817e6d..a6a7a244d9 100644 --- a/frontend/src/components/Chat/ChatInputArea.tsx +++ b/frontend/src/components/Chat/ChatInputArea.tsx @@ -7,7 +7,7 @@ import { tokens, mergeClasses, } from '@fluentui/react-components' -import { SendRegular, AttachRegular, DismissRegular, InfoRegular, AddRegular, CopyRegular, WarningRegular, SettingsRegular, ArrowShuffleRegular } from '@fluentui/react-icons' +import { SendRegular, AttachRegular, DismissRegular, InfoRegular, AddRegular, CopyRegular, WarningRegular, SettingsRegular, ArrowShuffleRegular, OpenRegular } from '@fluentui/react-icons' import { MessageAttachment, TargetInstance } from '../../types' import { useChatInputAreaStyles } from './ChatInputArea.styles' import { PIECE_TYPE_TO_DATA_TYPE } from './converterTypes' @@ -16,6 +16,12 @@ import { PIECE_TYPE_TO_DATA_TYPE } from './converterTypes' // Reusable status banner // --------------------------------------------------------------------------- +export interface ConvertedFileChip { + name: string + url: string + iconKind: 'image' | 'audio' | 'video' | 'file' +} + interface StatusBannerProps { icon: React.ReactElement text: string @@ -55,7 +61,7 @@ function StatusBanner({ icon, text, buttonText, buttonIcon, onButtonClick, testI interface AttachmentListProps { attachments: MessageAttachment[] - mediaConversions: Array<{ pieceType: string; convertedValue: string }> + mediaConversions: Array<{ pieceType: string; convertedValue: string; convertedDataType: string }> onRemove: (index: number) => void onClearMediaConversion: (pieceType: string) => void formatFileSize: (bytes: number) => string @@ -120,21 +126,24 @@ function AttachmentList({ attachments, mediaConversions, onRemove, onClearMediaC interface TextInputRowsProps { input: string convertedValue?: string | null + convertedFileChip?: ConvertedFileChip | null disabled: boolean textareaRef: Ref convertedRef: Ref onInput: (e: React.ChangeEvent) => void onKeyDown: (e: KeyboardEvent) => void onConvertedValueChange: (value: string) => void + onClearConvertedFileChip?: () => void styles: ReturnType textInputClassName: string } -function TextInputRows({ input, convertedValue, disabled, textareaRef, convertedRef, onInput, onKeyDown, onConvertedValueChange, styles, textInputClassName }: TextInputRowsProps) { +function TextInputRows({ input, convertedValue, convertedFileChip, disabled, textareaRef, convertedRef, onInput, onKeyDown, onConvertedValueChange, onClearConvertedFileChip, styles, textInputClassName }: TextInputRowsProps) { + const hasConversion = Boolean(convertedValue) || Boolean(convertedFileChip) return ( <>
- {convertedValue && ( + {hasConversion && ( Original )}