Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ starters/docs/registry
starters/tailwind/registry
starters/docs/yarn.lock
starters/tailwind/yarn.lock
.scout/
81 changes: 57 additions & 24 deletions packages/@react-spectrum/ai/src/AttachmentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ import {BasicHorizontalCard} from './HorizontalCard';
import {Button} from 'react-aria-components/Button';
import {CardProps} from '@react-spectrum/s2/Card';
import Cross from '../ui-icons/Cross';
import {forwardRef, ReactNode, useRef} from 'react';
import {forwardRef, ReactNode, useContext, useRef} from 'react';
import {IconContext} from '@react-spectrum/s2/Icon';
import {ImageContext} from '@react-spectrum/s2/Image';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {mergeStyles} from '@react-spectrum/s2/mergeStyles';
import {pressScale} from '@react-spectrum/s2/pressScale';
import {ProgressCircle} from '@react-spectrum/s2/ProgressCircle';
import {Provider} from 'react-aria-components/slots';
import {StyleString} from '@react-spectrum/s2/style' with {type: 'macro'};
import {
Tag,
Expand Down Expand Up @@ -217,6 +219,56 @@ const attachmentErrorStyles = style({
}
});

function AttachmentContextProvider({
children,
isUploading
}: {
children: ReactNode;
isUploading: boolean;
}) {
let imageCtx = useContext(ImageContext);
let iconCtx = useContext(IconContext);
const opacityStyles = style({
opacity: {default: 1, isUploading: 0.15},
transition: 'default'
})({isUploading});
const imageSlots = imageCtx && 'slots' in imageCtx ? imageCtx.slots : undefined;
const iconSlots = iconCtx && 'slots' in iconCtx ? iconCtx.slots : undefined;

return (
<Provider
values={[
[
ImageContext,
{
...imageCtx,
slots: {
...imageSlots,
thumbnail: {
...imageSlots?.thumbnail,
styles: mergeStyles(imageSlots?.thumbnail?.styles, opacityStyles)
}
}
}
],
[
IconContext,
{
slots: {
...iconSlots,
thumbnail: {
...iconSlots?.thumbnail,
styles: mergeStyles(iconSlots?.thumbnail?.styles, opacityStyles)
}
}
}
]
]}>
{children}
</Provider>
);
}

export const Attachment = forwardRef(function Attachment(
props: AttachmentProps,
ref: DOMRef<HTMLDivElement>
Expand Down Expand Up @@ -266,29 +318,10 @@ export const Attachment = forwardRef(function Attachment(
/>
</div>
)}
{/* Reduce opacity of the thumbnail if upload is in progress */}
<ImageContext.Consumer>
{ctx => (
<ImageContext.Provider
value={{
...ctx,
slots: {
thumbnail: {
...(ctx && 'slots' in ctx ? ctx.slots?.thumbnail : {}),
styles: mergeStyles(
ctx && 'slots' in ctx ? ctx.slots?.thumbnail?.styles : undefined,
style({
opacity: {default: 1, isUploading: 0.15},
transition: 'default'
})({isUploading: props.uploadProgress != null && props.uploadProgress < 100})
)
}
}
}}>
{typeof children === 'function' ? children({size}) : children}
</ImageContext.Provider>
)}
</ImageContext.Consumer>
<AttachmentContextProvider
isUploading={props.uploadProgress != null && props.uploadProgress < 100}>
{typeof children === 'function' ? children({size}) : children}
</AttachmentContextProvider>
{isInvalid && (
<div aria-hidden="true" className={attachmentErrorStyles}>
<AlertTriangleIcon size={size} />
Expand Down
42 changes: 35 additions & 7 deletions packages/@react-spectrum/ai/src/HorizontalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {DOMProps, DOMRef, GlobalDOMAttributes} from '@react-types/shared';
import {filterDOMProps} from 'react-aria/filterDOMProps';
import {FooterContext} from '@react-spectrum/s2/Footer';
import {GridListItem, GridListItemProps} from 'react-aria-components/GridList';
import {IconContext} from '@react-spectrum/s2/Icon';
import {ImageContext} from '@react-spectrum/s2/Image';
import {ImageCoordinator} from '@react-spectrum/s2/ImageCoordinator';
import {inertValue} from 'react-aria/private/utils/inertValue';
Expand Down Expand Up @@ -312,7 +313,7 @@ const image = style({
pointerEvents: 'none'
});

let title = style({
let title = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'; isBasic?: boolean}>({
font: 'title',
fontSize: {
size: {
Expand All @@ -323,11 +324,14 @@ let title = style({
XL: 'title-lg'
}
},
lineClamp: 3,
lineClamp: {
default: 3,
isBasic: 1
},
gridArea: 'title'
});

let description = style({
let description = style<{size: 'XS' | 'S' | 'M' | 'L' | 'XL'; isBasic?: boolean}>({
font: 'body',
fontSize: {
size: {
Expand All @@ -338,7 +342,10 @@ let description = style({
XL: 'body'
}
},
lineClamp: 3,
lineClamp: {
default: 3,
isBasic: 1
},
gridArea: 'description'
});

Expand All @@ -347,7 +354,7 @@ let content = style({
// By default, all elements are displayed in a stack.
// If an action menu is present, place it next to the title.
gridTemplateColumns: {
default: ['1fr'],
default: ['minmax(0, 1fr)'],
':has([data-slot=menu])': ['minmax(0, 1fr)', 'auto']
},
gridTemplateAreas: {
Expand All @@ -356,6 +363,7 @@ let content = style({
},
columnGap: 4,
flexGrow: 1,
minWidth: 0,
alignItems: 'baseline',
alignContent: 'start',
rowGap: {
Expand Down Expand Up @@ -456,8 +464,8 @@ const Card = forwardRef(function Card(
{
slots: {
[DEFAULT_SLOT]: {},
title: {styles: title({size})},
description: {styles: description({size})}
title: {styles: title({size, isBasic})},
description: {styles: description({size, isBasic})}
}
}
],
Expand Down Expand Up @@ -802,6 +810,15 @@ export const HorizontalCard = forwardRef(function HorizontalCard(
);
});

const iconThumbnailStyles = style({
position: 'relative',
alignSelf: 'center',
flexShrink: 0,
pointerEvents: 'none',
userSelect: 'none',
size: '--basic-thumb-size'
});

export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard(
props: BasicCardProps,
ref: DOMRef<HTMLDivElement>
Expand Down Expand Up @@ -840,6 +857,17 @@ export const BasicHorizontalCard = forwardRef(function BasicHorizontalCard(
}
}
],
[
IconContext,
{
slots: {
icon: {},
thumbnail: {
styles: iconThumbnailStyles
}
}
}
],
[
FooterContext,
{
Expand Down
36 changes: 25 additions & 11 deletions packages/@react-spectrum/ai/src/PromptField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ import {
useRef,
useState
} from 'react';
import {DOMRef} from '@react-types/shared';
import {FocusableRef} from '@react-types/shared';
import {IconContext} from '@react-spectrum/s2';
import {Image, Text} from '@react-spectrum/s2/Card';
import intlMessages from '../intl/*.json';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {isFileDropItem, useDrop} from 'react-aria-components/useDrop';
import {Link} from '@react-spectrum/s2/Link';
import {Menu, MenuItem, MenuItemProps, MenuTrigger} from '@react-spectrum/s2/Menu';
Expand Down Expand Up @@ -70,8 +70,8 @@ import {
} from 'react-aria-components/TokenField';
import {Tooltip, TooltipTrigger} from '@react-spectrum/s2/Tooltip';
import {useControlledState} from 'react-stately/useControlledState';
import {useDOMRef} from './useDOMRef';
import {useEffectEvent} from 'react-aria/private/utils/useEffectEvent';
import {useFocusableRef} from './useDOMRef';
import {useFocusWithin} from 'react-aria/useFocusWithin';
import {useKeyboard} from 'react-aria/useKeyboard';
import {useLocale} from 'react-aria/I18nProvider';
Expand Down Expand Up @@ -182,7 +182,7 @@ function matchMimeType(mimeType: string, acceptedMimeTypes: string[]): boolean {

export const PromptField = forwardRef(function PromptField(
props: PromptFieldProps,
ref: DOMRef<HTMLDivElement>
ref: FocusableRef<HTMLDivElement>
) {
let {
children,
Expand All @@ -195,7 +195,10 @@ export const PromptField = forwardRef(function PromptField(
variant = 'balanced',
brandColor
} = props;
let domRef = useDOMRef(ref);
// Not using RAC DropZone because it adds its own focusable button,
// and we want to avoid an extra tab. We support pasting files directly into the input.
let inputRef = useRef<HTMLDivElement>(null);
let domRef = useFocusableRef(ref, inputRef);
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/ai');
let [prompt, setPrompt] = useControlledState(
props.value,
Expand All @@ -207,10 +210,6 @@ export const PromptField = forwardRef(function PromptField(
props.defaultAttachments ?? [],
props.onAttachmentsChange
);

// Not using RAC DropZone because it adds its own focusable button,
// and we want to avoid an extra tab. We support pasting files directly into the input.
let inputRef = useRef<HTMLDivElement>(null);
let {dropProps, isDropTarget} = useDrop({
ref: inputRef,
hasDropButton: true,
Expand Down Expand Up @@ -342,6 +341,9 @@ export interface PromptTokenFieldProps {
pixelLoader?: Cell[] | Cell[][];
placeholder?: string;
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
// TODO: temp api for coworker so that the weird popover shrinking behavior
// doesn't appear when rendering near edge of page
menuWidth?: number;
}

export function PromptTokenField(props: PromptTokenFieldProps) {
Expand All @@ -351,6 +353,7 @@ export function PromptTokenField(props: PromptTokenFieldProps) {
children,
pixelLoader,
placeholder,
menuWidth,
onKeyDown: onKeyDownProp
} = props;
let {keyboardProps} = useKeyboard({onKeyDown: onKeyDownProp});
Expand Down Expand Up @@ -473,6 +476,7 @@ export function PromptTokenField(props: PromptTokenFieldProps) {
filterAnchor={filterAnchor}
items={useDeferredValue(items)}
isFocused={isFocused}
menuWidth={menuWidth}
/>
</Autocomplete>
</div>
Expand All @@ -483,10 +487,12 @@ export interface PromptTokenFieldPopoverProps extends Omit<PopoverProps, 'should
filterAnchor?: Position | null;
items?: React.ReactNode[] | null | Promise<React.ReactNode[] | null>;
isFocused?: boolean;
// TODO: temp for coworker see above comment
menuWidth?: number;
}

function PromptTokenFieldPopover(props: PromptTokenFieldPopoverProps) {
let {filterAnchor, items, isFocused} = props;
let {filterAnchor, items, isFocused, menuWidth} = props;
let {inputRef} = useContext(PromptFieldContext);

let resolvedItems = items instanceof Promise ? use(items) : items;
Expand All @@ -506,6 +512,7 @@ function PromptTokenFieldPopover(props: PromptTokenFieldPopoverProps) {
isNonModal
hideArrow
placement="bottom start"
UNSAFE_style={menuWidth != null ? {width: menuWidth} : undefined}
getTargetRect={target => {
return tokenFieldPositionToDOMRange(target, filterAnchor!).getBoundingClientRect();
}}>
Expand Down Expand Up @@ -611,10 +618,11 @@ export interface PromptFieldVoiceButtonProps {
lang?: string;
isDisabled?: boolean;
onError?: (code: VoiceInputErrorCode) => void;
onToggle?: (isListening: boolean) => void;
}

export function PromptFieldVoiceButton(props: PromptFieldVoiceButtonProps) {
let {lang: langProp, isDisabled: isDisabledProp, onError} = props;
let {lang: langProp, isDisabled: isDisabledProp, onError, onToggle} = props;
let {locale} = useLocale();
let lang = langProp ?? locale;
let {prompt, setPrompt, inputRef, setListening} = useContext(PromptFieldContext);
Expand Down Expand Up @@ -646,14 +654,20 @@ export function PromptFieldVoiceButton(props: PromptFieldVoiceButtonProps) {
setPrompt(finalPrompt);
});

let onToggleEvent = useEffectEvent((isListening: boolean) => {
onToggle?.(isListening);
});

let wasListeningRef = useRef(false);
useEffect(() => {
if (isVoiceListening) {
updateBasePrompt();
wasListeningRef.current = true;
onToggleEvent(true);
} else if (wasListeningRef.current) {
wasListeningRef.current = false;
restoreFocus();
onToggleEvent(false);
}
}, [isVoiceListening]);

Expand Down
Loading