diff --git a/packages/@react-spectrum/ai/exports/loader.ts b/packages/@react-spectrum/ai/exports/loader.ts new file mode 100644 index 00000000000..4c0c853ec7d --- /dev/null +++ b/packages/@react-spectrum/ai/exports/loader.ts @@ -0,0 +1 @@ +export * from '../src/loader/data'; diff --git a/packages/@react-spectrum/ai/intl/en-US.json b/packages/@react-spectrum/ai/intl/en-US.json index adc6cbede25..555ea77c69a 100644 --- a/packages/@react-spectrum/ai/intl/en-US.json +++ b/packages/@react-spectrum/ai/intl/en-US.json @@ -2,5 +2,6 @@ "chat.newMessage": "New message", "messagefeedback.thumbDown": "Bad response", "messagefeedback.thumbUp": "Good response", - "responsestatus.loading": "Loading" + "responsestatus.loading": "Loading", + "promptfield.placeholder": "Ready to get started? Ask a question, share an idea, or add a task." } diff --git a/packages/@react-spectrum/ai/src/HorizontalCard.tsx b/packages/@react-spectrum/ai/src/HorizontalCard.tsx index 920078102c1..a9f71686e8a 100644 --- a/packages/@react-spectrum/ai/src/HorizontalCard.tsx +++ b/packages/@react-spectrum/ai/src/HorizontalCard.tsx @@ -115,46 +115,11 @@ let card = style({ flexDirection: 'row', position: 'relative', borderRadius, - '--s2-container-bg': { - type: 'backgroundColor', - value: { - variant: { - primary: 'elevated', - secondary: 'layer-1', - tertiary: 'layer-2' - }, - isBasic: { - variant: { - primary: 'layer-2', - secondary: 'layer-1', - tertiary: 'layer-2', - quiet: 'layer-2' - } - }, - forcedColors: 'ButtonFace' - } - }, - backgroundColor: '--s2-container-bg', - // TODO: No box shadow for basic, secondary, dark - // also none for basic tertiary - boxShadow: { - default: 'emphasized', - isHovered: 'elevated', - isFocusVisible: 'elevated', - isSelected: 'elevated', - forcedColors: '[0 0 0 1px var(--hcm-buttonborder, ButtonBorder)]', - variant: { - tertiary: { - // Render border with box-shadow to avoid affecting layout. - default: `[0 0 0 2px ${color('gray-100')}]`, - isHovered: `[0 0 0 2px ${color('gray-200')}]`, - isFocusVisible: `[0 0 0 2px ${color('gray-200')}]`, - isSelected: 'none', - forcedColors: '[0 0 0 2px var(--hcm-buttonborder, ButtonBorder)]' - }, - quiet: 'none' - } + backgroundColor: { + default: lightDark('transparent-white-300', 'transparent-black-300'), + forcedColors: 'ButtonFace' }, + boxShadow: `[inset 0 0 0 1px light-dark(${color('transparent-black-300')}, ${color('transparent-white-300')})]`, forcedColorAdjust: 'none', transition: 'default', fontFamily: 'sans', diff --git a/packages/@react-spectrum/ai/src/PromptField.tsx b/packages/@react-spectrum/ai/src/PromptField.tsx index 7614bcae7d3..e18d031b5be 100644 --- a/packages/@react-spectrum/ai/src/PromptField.tsx +++ b/packages/@react-spectrum/ai/src/PromptField.tsx @@ -14,9 +14,16 @@ import {ActionButton} from '@react-spectrum/s2/ActionButton'; import Attach from '@react-spectrum/s2/icons/Attach'; import {Attachment, AttachmentList, AttachmentListProps} from './AttachmentList'; import {Autocomplete} from 'react-aria-components/Autocomplete'; -import {baseColor, css, style, StyleString} from '@react-spectrum/s2/style' with {type: 'macro'}; import {Button} from '@react-spectrum/s2/Button'; +import {Cell} from './loader/data'; import {CenterBaseline} from '@react-spectrum/s2/CenterBaseline'; +import { + color, + css, + iconStyle, + style, + StyleString +} from '@react-spectrum/s2/style' with {type: 'macro'}; import { createContext, createRef, @@ -27,7 +34,6 @@ import { useRef, useState } from 'react'; -// eslint-disable-next-line import { Direction, Position, @@ -35,22 +41,23 @@ import { TokenSegment, TokenSegmentList } from './TokenSegmentList'; -import {getEventTarget} from 'react-aria/private/utils/shadowdom/DOMFunctions'; -import {Group} from 'react-aria-components/Group'; -import {IconContext, mergeStyles} from '@react-spectrum/s2'; +import {IconContext} from '@react-spectrum/s2'; import {Image, Text} from '@react-spectrum/s2/Card'; +// @ts-ignore +import intlMessages from '../intl/*.json'; import {isFileDropItem, useDrop} from 'react-aria-components/useDrop'; -import {isFocusable} from 'react-aria/private/utils/isFocusable'; import {Link} from '@react-spectrum/s2/Link'; import {Menu, MenuItem, MenuItemProps, MenuTrigger} from '@react-spectrum/s2/Menu'; -// eslint-disable-next-line +import {PixelLoader} from './loader/react'; import Plus from '@react-spectrum/s2/icons/Add'; import {Popover, PopoverProps} from '@react-spectrum/s2/Popover'; import {positionToDOMRange, Token, TokenField, TokenProps} from './TokenField'; +import {PromptFieldContainer} from './PromptFieldContainer'; import {PromptFocusContext} from './Chat'; import Send from '@react-spectrum/s2/icons/ArrowUpSend'; import Stop from '@react-spectrum/s2/icons/StopProcessing'; import {useFocusWithin} from 'react-aria/useFocusWithin'; +import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; export interface PromptFieldAttachment { id: string; @@ -67,6 +74,7 @@ export interface PromptFieldProps { onAddAttachments?: (attachments: PromptFieldAttachment[]) => void; onRemoveAttachments?: (attachments: PromptFieldAttachment[]) => void; styles?: StyleString; + variant?: 'balanced' | 'prominent' | 'subtle'; } interface PromptFieldState { @@ -141,7 +149,8 @@ export function PromptField(props: PromptFieldProps) { onStop, styles, onAddAttachments, - onRemoveAttachments + onRemoveAttachments, + variant = 'balanced' } = props; let [prompt, setPrompt] = useState(new AutoLinkingSegmentList([])); let [attachments, setAttachments] = useState([]); @@ -202,47 +211,16 @@ export function PromptField(props: PromptFieldProps) { onRemoveAttachments }}>
- - mergeStyles( - style({ - display: 'flex', - flexDirection: 'column', - gap: 16, - padding: 16, - boxShadow: 'emphasized', - backgroundColor: { - default: 'elevated', - isDropTarget: 'blue-200' - }, - borderRadius: 'lg', - borderWidth: 2, - borderStyle: 'solid', - borderColor: { - default: 'transparent', - isDropTarget: 'blue-800' - }, - cursor: 'text' - })({...renderProps, isDropTarget}), - styles - ) - } - onPointerDown={e => { - // If not clicking on something focusable within the prompt field, focus the input. - let target = getEventTarget(e) as Element | null; - while (target && target !== e.currentTarget && !isFocusable(target)) { - target = target.parentElement; - } - - if (target === e.currentTarget) { - e.preventDefault(); - inputRef.current?.focus(); - } - }}> + variant={variant} + isGenerating={isGenerating ?? false} + isDropTarget={isDropTarget} + styles={styles} + inputRef={inputRef}> {children} - +

Responses are generated using AI, and may be inaccurate. Check before using.{' '} React.ReactNode[] | null | Promise; children?: (segment: TokenSegment) => React.ReactElement; + pixelLoader?: Cell[] | Cell[][]; + placeholder?: string; } export function PromptTokenField(props: PromptTokenFieldProps) { - let {completionTrigger, renderCompletions, children} = props; + let {completionTrigger, renderCompletions, children, pixelLoader, placeholder} = props; let { prompt, setPrompt, @@ -308,8 +288,10 @@ export function PromptTokenField(props: PromptTokenFieldProps) { setAttachments, onAddAttachments, inputRef, - onSubmit + onSubmit, + isGenerating } = useContext(PromptFieldContext); + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai'); let [isFocused, setFocused] = useState(false); let [filterAnchor, filterValue] = useMemo(() => { @@ -332,71 +314,90 @@ export function PromptTokenField(props: PromptTokenFieldProps) { }, [filterValue, renderCompletions]); return ( - - { - if (e.isTrusted) { - setFocused(true); - } - }} - onBlur={e => { - if (e.isTrusted) { - setFocused(false); - } - }} - onPaste={ - acceptedAttachmentTypes - ? e => { - let clipboardData = e.clipboardData as DataTransfer; - let attachments: PromptFieldAttachment[] = []; - for (let item of clipboardData.items) { - if (matchMimeType(item.type, acceptedAttachmentTypes)) { - let file = item.getAsFile()!; - attachments.push({ - id: crypto.randomUUID(), - file, - image: file.type.startsWith('image/') ? URL.createObjectURL(file) : '' - }); +

0})}> + + + + + { + if (e.isTrusted) { + setFocused(true); + } + }} + onBlur={e => { + if (e.isTrusted) { + setFocused(false); + } + }} + onPaste={ + acceptedAttachmentTypes + ? e => { + let clipboardData = e.clipboardData as DataTransfer; + let attachments: PromptFieldAttachment[] = []; + for (let item of clipboardData.items) { + if (matchMimeType(item.type, acceptedAttachmentTypes)) { + let file = item.getAsFile()!; + attachments.push({ + id: crypto.randomUUID(), + file, + image: file.type.startsWith('image/') ? URL.createObjectURL(file) : '' + }); + } + } + if (attachments.length > 0) { + onAddAttachments?.(attachments); + setAttachments(prev => [...prev, ...attachments]); } } - if (attachments.length > 0) { - onAddAttachments?.(attachments); - setAttachments(prev => [...prev, ...attachments]); + : undefined + } + onSubmit={onSubmit} + className={ + css('&:empty::before { content: attr(data-placeholder); }') + + style({ + flexGrow: 1, + font: 'body', + color: { + default: 'gray-800', + ':empty': { + default: 'transparent-overlay-600', + forcedColors: 'GrayText' } - } - : undefined - } - onSubmit={onSubmit} - className={renderProps => - css('&:empty::before { content: attr(data-placeholder); }') + - style({ - font: 'body', - color: { - default: baseColor('neutral'), - ':empty': { - default: 'gray-600', - forcedColors: 'GrayText' - } - }, - width: 'full', - outlineStyle: 'none', - cursor: 'text' - })(renderProps) - }> - {children || (segment => {segment.text})} - - - + }, + width: 'full', + outlineStyle: 'none', + cursor: 'text' + }) + }> + {children || (segment => {segment.text})} + + + +
); } @@ -462,18 +463,24 @@ export function PromptToken(props: PromptTokenProps) { - {icon}}}> + {icon} + }}> {props.children} @@ -519,6 +530,7 @@ export function PromptFieldSubmitButton(props: PromptFieldSubmitButtonProps) { return (