diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 008487a7f..25c467488 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -79,7 +79,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1312,7 +1311,6 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" @@ -4027,7 +4025,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4189,7 +4188,6 @@ "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4205,7 +4203,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4216,7 +4213,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4288,7 +4284,6 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -4523,7 +4518,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4878,7 +4872,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5316,7 +5309,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/domexception": { "version": "4.0.0", @@ -5357,8 +5351,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-autoplay": { "version": "8.6.0", @@ -5559,7 +5552,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6664,7 +6656,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7819,6 +7810,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -8406,6 +8398,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -8421,6 +8414,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -8521,7 +8515,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8534,7 +8527,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -9143,7 +9135,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9285,7 +9276,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9372,7 +9362,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9501,7 +9490,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9595,7 +9583,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/src/components/Chat/ChatInputArea.styles.ts b/frontend/src/components/Chat/ChatInputArea.styles.ts index 75b67ff53..b147da5f7 100644 --- a/frontend/src/components/Chat/ChatInputArea.styles.ts +++ b/frontend/src/components/Chat/ChatInputArea.styles.ts @@ -79,6 +79,7 @@ export const useChatInputAreaStyles = makeStyles({ display: 'flex', gap: tokens.spacingHorizontalXS, marginRight: tokens.spacingHorizontalS, + alignItems: 'center', }, iconButtonsRight: { display: 'flex', @@ -131,4 +132,53 @@ export const useChatInputAreaStyles = makeStyles({ color: tokens.colorPaletteRedForeground1, fontWeight: tokens.fontWeightSemibold as unknown as string, }, + conversionBarBottom: { + display: 'flex', + alignItems: 'flex-start', + gap: tokens.spacingHorizontalXS, + padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalL}`, + paddingLeft: `calc(${tokens.spacingHorizontalL} + 32px + ${tokens.spacingHorizontalXS} + 32px + ${tokens.spacingHorizontalS})`, + borderTop: `1px solid ${tokens.colorNeutralStroke1}`, + backgroundColor: tokens.colorNeutralBackground4, + overflow: 'hidden', + }, + conversionLabel: { + display: 'flex', + alignItems: 'center', + gap: tokens.spacingHorizontalXXS, + flex: 1, + minWidth: 0, + overflow: 'hidden', + }, + conversionText: { + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + fontFamily: tokens.fontFamilyMonospace, + fontSize: tokens.fontSizeBase200, + minWidth: 0, + flex: 1, + maxHeight: '80px', + overflowY: 'auto', + }, + originalBadge: { + display: 'inline-block', + padding: `0 ${tokens.spacingHorizontalXS}`, + marginRight: tokens.spacingHorizontalXS, + borderRadius: tokens.borderRadiusSmall, + backgroundColor: tokens.colorPaletteBlueBackground2, + color: tokens.colorPaletteBlueForeground2, + fontSize: tokens.fontSizeBase100, + fontWeight: tokens.fontWeightSemibold as unknown as string, + flexShrink: 0, + }, + convertedBadge: { + display: 'inline-block', + padding: `0 ${tokens.spacingHorizontalXS}`, + borderRadius: tokens.borderRadiusSmall, + backgroundColor: tokens.colorPaletteGreenBackground2, + color: tokens.colorPaletteGreenForeground2, + fontSize: tokens.fontSizeBase100, + fontWeight: tokens.fontWeightSemibold as unknown as string, + flexShrink: 0, + }, }) diff --git a/frontend/src/components/Chat/ChatInputArea.test.tsx b/frontend/src/components/Chat/ChatInputArea.test.tsx index c2712acdd..777be2dd0 100644 --- a/frontend/src/components/Chat/ChatInputArea.test.tsx +++ b/frontend/src/components/Chat/ChatInputArea.test.tsx @@ -26,12 +26,31 @@ describe("ChatInputArea", () => { it("should render input area and send button", () => { render( - + ); expect(screen.getByRole("textbox")).toBeInTheDocument(); expect(getSendButton()).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /convert/i })).toBeInTheDocument(); + }); + + it("should call converter panel toggle handler when convert button is clicked", async () => { + const user = userEvent.setup(); + const onToggleConverterPanel = jest.fn(); + + render( + + + + ); + + await user.click(screen.getByRole("button", { name: /convert/i })); + + expect(onToggleConverterPanel).toHaveBeenCalledTimes(1); }); it("should call onSend with input value when send button clicked", async () => { diff --git a/frontend/src/components/Chat/ChatInputArea.tsx b/frontend/src/components/Chat/ChatInputArea.tsx index 12ef7b3d2..dd8850d92 100644 --- a/frontend/src/components/Chat/ChatInputArea.tsx +++ b/frontend/src/components/Chat/ChatInputArea.tsx @@ -1,12 +1,12 @@ import { useState, useEffect, useLayoutEffect, useRef, forwardRef, useImperativeHandle, KeyboardEvent } from 'react' import { Button, - tokens, Caption1, Tooltip, Text, + tokens, } from '@fluentui/react-components' -import { SendRegular, AttachRegular, DismissRegular, InfoRegular, AddRegular, CopyRegular, WarningRegular, SettingsRegular } from '@fluentui/react-icons' +import { SendRegular, AttachRegular, DismissRegular, InfoRegular, AddRegular, CopyRegular, WarningRegular, SettingsRegular, ArrowShuffleRegular } from '@fluentui/react-icons' import { MessageAttachment, TargetInstance } from '../../types' import { useChatInputAreaStyles } from './ChatInputArea.styles' @@ -68,9 +68,16 @@ interface ChatInputAreaProps { attackOperator?: string noTargetSelected?: boolean onConfigureTarget?: () => void + onToggleConverterPanel?: () => void + isConverterPanelOpen?: boolean + onInputChange?: (value: string) => void + onAttachmentsChange?: (types: string[]) => void + convertedValue?: string | null + originalValue?: string | null + onClearConversion?: () => void } -const ChatInputArea = forwardRef(function ChatInputArea({ onSend, disabled = false, activeTarget, singleTurnLimitReached = false, onNewConversation, operatorLocked = false, crossTargetLocked = false, onUseAsTemplate, attackOperator, noTargetSelected = false, onConfigureTarget }, ref) { +const ChatInputArea = forwardRef(function ChatInputArea({ onSend, disabled = false, activeTarget, singleTurnLimitReached = false, onNewConversation, operatorLocked = false, crossTargetLocked = false, onUseAsTemplate, attackOperator, noTargetSelected = false, onConfigureTarget, onToggleConverterPanel, isConverterPanelOpen = false, onInputChange, onAttachmentsChange, convertedValue, originalValue, onClearConversion }, ref) { const styles = useChatInputAreaStyles() const [input, setInput] = useState('') const [attachments, setAttachments] = useState([]) @@ -126,9 +133,10 @@ const ChatInputArea = forwardRef(functi const handleSend = () => { if ((input || attachments.length > 0) && !disabled) { - onSend(input, undefined, attachments) + onSend(input, convertedValue ?? undefined, attachments) setInput('') setAttachments([]) + onClearConversion?.() if (textareaRef.current) { textareaRef.current.style.height = 'auto' } @@ -156,7 +164,13 @@ const ChatInputArea = forwardRef(functi textareaRef.current.style.height = 'auto' textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 96) + 'px' } - }, [input]) + onInputChange?.(input) + }, [input, onInputChange]) + + useEffect(() => { + const types = [...new Set(attachments.map((a) => a.type))] + onAttachmentsChange?.(types) + }, [attachments, onAttachmentsChange]) const handleInput = (e: React.ChangeEvent) => { setInput(e.target.value) @@ -261,7 +275,19 @@ const ChatInputArea = forwardRef(functi disabled={disabled} title="Attach files" /> +