mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
1 Commits
d474c2a47e
...
ai-ux-impr
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d1b53485b8 |
@@ -6,6 +6,7 @@ import Text from '../../UI/Text';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import {
|
||||
type AiRequest,
|
||||
type AiRequestUserMessage,
|
||||
type AiRequestMessageAssistantFunctionCall,
|
||||
} from '../../Utils/GDevelopServices/Generation';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
getDefaultAiConfigurationPresetId,
|
||||
} from '../AiConfiguration';
|
||||
import { AiConfigurationPresetSelector } from './AiConfigurationPresetSelector';
|
||||
import { AiRequestContext } from '../AiRequestContext';
|
||||
|
||||
const TOO_MANY_USER_MESSAGES_WARNING_COUNT = 5;
|
||||
const TOO_MANY_USER_MESSAGES_ERROR_COUNT = 10;
|
||||
@@ -282,6 +284,9 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const { aiRequestStorage } = React.useContext(AiRequestContext);
|
||||
const { aiRequests } = aiRequestStorage;
|
||||
|
||||
// TODO: store the default mode in the user preferences?
|
||||
const [newAiRequestMode, setNewAiRequestMode] = React.useState<
|
||||
'chat' | 'agent'
|
||||
@@ -336,7 +341,11 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
userRequestTextPerAiRequestId,
|
||||
setUserRequestTextPerRequestId,
|
||||
] = React.useState<{ [string]: string }>({});
|
||||
const [historyIndex, setHistoryIndex] = React.useState<number>(-1);
|
||||
const [savedCurrentText, setSavedCurrentText] = React.useState<string>('');
|
||||
const scrollViewRef = React.useRef<ScrollViewInterface | null>(null);
|
||||
const textAreaRefForNewChat = React.useRef<any>(null);
|
||||
const textAreaRefForExistingChat = React.useRef<any>(null);
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = React.useState<boolean>(
|
||||
true
|
||||
);
|
||||
@@ -399,13 +408,26 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
[newAiRequestMode, hasOpenedProject]
|
||||
);
|
||||
|
||||
const onUserRequestTextChange = React.useCallback(
|
||||
(userRequestText: string, aiRequestIdToChange: string) => {
|
||||
setUserRequestTextPerRequestId(userRequestTextPerAiRequestId => ({
|
||||
...userRequestTextPerAiRequestId,
|
||||
[aiRequestIdToChange]: userRequestText,
|
||||
}));
|
||||
// Reset history navigation when field is cleared,
|
||||
// so that pressing up goes to the last message again.
|
||||
if (!userRequestText && historyIndex !== -1) {
|
||||
setHistoryIndex(-1);
|
||||
setSavedCurrentText('');
|
||||
}
|
||||
},
|
||||
[historyIndex]
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
resetUserInput: (aiRequestId: string | null) => {
|
||||
const aiRequestIdToReset: string = aiRequestId || '';
|
||||
setUserRequestTextPerRequestId(userRequestTextPerAiRequestId => ({
|
||||
...userRequestTextPerAiRequestId,
|
||||
[aiRequestIdToReset]: '',
|
||||
}));
|
||||
onUserRequestTextChange('', aiRequestIdToReset);
|
||||
|
||||
if (scrollViewRef.current) {
|
||||
scrollViewRef.current.scrollToBottom({
|
||||
@@ -417,6 +439,109 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
|
||||
// Build history from sent user messages across all aiRequests
|
||||
const requestsHistory = React.useMemo(
|
||||
() => {
|
||||
const history: Array<string> = [];
|
||||
|
||||
// Iterate through all aiRequests in reverse order (most recent first)
|
||||
Object.values(aiRequests)
|
||||
.reverse()
|
||||
.forEach(
|
||||
// $FlowFixMe - Object.values() loses the type of aiRequests.
|
||||
(request: AiRequest) => {
|
||||
const userMessages = request.output
|
||||
.filter(
|
||||
message =>
|
||||
message.type === 'message' && message.role === 'user'
|
||||
)
|
||||
.map(
|
||||
// $FlowFixMe - We filtered the type above.
|
||||
(message: AiRequestUserMessage) => {
|
||||
const userRequest = message.content.find(
|
||||
item => item.type === 'user_request'
|
||||
);
|
||||
return userRequest ? userRequest.text : '';
|
||||
}
|
||||
)
|
||||
.filter(text => text !== '');
|
||||
|
||||
history.push(...userMessages);
|
||||
}
|
||||
);
|
||||
|
||||
return history;
|
||||
},
|
||||
[aiRequests]
|
||||
);
|
||||
|
||||
// Reset history index when aiRequest changes,
|
||||
// ensuring pressing up and down doesn't depend on the previous aiRequest.
|
||||
React.useEffect(
|
||||
() => {
|
||||
setHistoryIndex(-1);
|
||||
setSavedCurrentText('');
|
||||
},
|
||||
[aiRequestId]
|
||||
);
|
||||
|
||||
const handleNavigateHistory = React.useCallback(
|
||||
(direction: 'up' | 'down') => {
|
||||
const currentText = userRequestTextPerAiRequestId[aiRequestId] || '';
|
||||
const textAreaRef = aiRequest
|
||||
? textAreaRefForExistingChat
|
||||
: textAreaRefForNewChat;
|
||||
|
||||
if (direction === 'up') {
|
||||
// Save current text when starting navigation,
|
||||
// so we can restore it if going back to current.
|
||||
if (historyIndex === -1) {
|
||||
setSavedCurrentText(currentText);
|
||||
}
|
||||
|
||||
const newIndex = historyIndex + 1;
|
||||
if (newIndex < requestsHistory.length) {
|
||||
setHistoryIndex(newIndex);
|
||||
const historicalText =
|
||||
requestsHistory[requestsHistory.length - 1 - newIndex];
|
||||
onUserRequestTextChange(historicalText, aiRequestId);
|
||||
|
||||
// Set cursor to start when navigating up,
|
||||
// otherwise it goes to the end of the text, making it harder
|
||||
// to navigate with one key press.
|
||||
if (textAreaRef.current) {
|
||||
// Use timeout so that the text is updated before setting the cursor position.
|
||||
setTimeout(() => {
|
||||
textAreaRef.current.setCursorPosition(0);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
} else if (direction === 'down') {
|
||||
const newIndex = historyIndex - 1;
|
||||
|
||||
if (newIndex === -1) {
|
||||
// We're at the end of the history. Restore the saved current text.
|
||||
setHistoryIndex(-1);
|
||||
onUserRequestTextChange(savedCurrentText, aiRequestId);
|
||||
} else if (newIndex >= 0) {
|
||||
setHistoryIndex(newIndex);
|
||||
const historicalText =
|
||||
requestsHistory[requestsHistory.length - 1 - newIndex];
|
||||
onUserRequestTextChange(historicalText, aiRequestId);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
aiRequestId,
|
||||
historyIndex,
|
||||
requestsHistory,
|
||||
userRequestTextPerAiRequestId,
|
||||
savedCurrentText,
|
||||
onUserRequestTextChange,
|
||||
aiRequest,
|
||||
]
|
||||
);
|
||||
|
||||
const priceText = (
|
||||
<Text size="body-small" color="secondary" noMargin>
|
||||
{getPriceText({
|
||||
@@ -551,20 +676,17 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
<Column noMargin alignItems="stretch" justifyContent="stretch">
|
||||
<Spacer />
|
||||
<CompactTextAreaFieldWithControls
|
||||
ref={textAreaRefForNewChat}
|
||||
maxLength={6000}
|
||||
value={userRequestTextPerAiRequestId[''] || ''}
|
||||
disabled={isSending}
|
||||
hasNeonCorner
|
||||
hasAnimatedNeonCorner={isSending}
|
||||
errored={!!lastSendError}
|
||||
onChange={userRequestText =>
|
||||
setUserRequestTextPerRequestId(
|
||||
userRequestTextPerAiRequestId => ({
|
||||
...userRequestTextPerAiRequestId,
|
||||
'': userRequestText,
|
||||
})
|
||||
)
|
||||
}
|
||||
onChange={userRequestText => {
|
||||
onUserRequestTextChange(userRequestText, '');
|
||||
}}
|
||||
onNavigateHistory={handleNavigateHistory}
|
||||
onSubmit={() => {
|
||||
onStartNewAiRequest({
|
||||
mode: newAiRequestMode,
|
||||
@@ -878,6 +1000,7 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
</Paper>
|
||||
) : null}
|
||||
<CompactTextAreaFieldWithControls
|
||||
ref={textAreaRefForExistingChat}
|
||||
maxLength={6000}
|
||||
value={userRequestTextPerAiRequestId[aiRequestId] || ''}
|
||||
disabled={isSending || isForAnotherProject}
|
||||
@@ -885,13 +1008,9 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
|
||||
hasNeonCorner
|
||||
hasAnimatedNeonCorner={isSending}
|
||||
onChange={userRequestText =>
|
||||
setUserRequestTextPerRequestId(
|
||||
userRequestTextPerAiRequestId => ({
|
||||
...userRequestTextPerAiRequestId,
|
||||
[aiRequestId]: userRequestText,
|
||||
})
|
||||
)
|
||||
onUserRequestTextChange(userRequestText, aiRequestId)
|
||||
}
|
||||
onNavigateHistory={handleNavigateHistory}
|
||||
placeholder={
|
||||
aiRequest.mode === 'agent'
|
||||
? isForAnotherProject
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
fetchAiSettings,
|
||||
type AiRequest,
|
||||
type AiSettings,
|
||||
getAiRequests,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { type EditorFunctionCallResult } from '../EditorFunctions/EditorFunctionCallRunner';
|
||||
@@ -82,6 +83,11 @@ const useEditorFunctionCallResultsStorage = (): EditorFunctionCallResultsStorage
|
||||
};
|
||||
|
||||
type AiRequestStorage = {|
|
||||
fetchAiRequests: () => Promise<void>,
|
||||
onLoadMoreAiRequests: () => Promise<void>,
|
||||
canLoadMore: boolean,
|
||||
error: ?Error,
|
||||
isLoading: boolean,
|
||||
aiRequests: { [string]: AiRequest },
|
||||
updateAiRequest: (aiRequestId: string, aiRequest: AiRequest) => void,
|
||||
refreshAiRequest: (aiRequestId: string) => Promise<void>,
|
||||
@@ -96,20 +102,105 @@ type AiRequestSendState = {|
|
||||
lastSendError: ?Error,
|
||||
|};
|
||||
|
||||
type PaginationState = {|
|
||||
aiRequests: { [string]: AiRequest },
|
||||
nextPageUri: ?Object,
|
||||
|};
|
||||
|
||||
const emptyPaginationState: PaginationState = {
|
||||
aiRequests: {},
|
||||
nextPageUri: null,
|
||||
};
|
||||
|
||||
export const useAiRequestsStorage = (): AiRequestStorage => {
|
||||
const { profile, getAuthorizationHeader } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
|
||||
const [aiRequests, setAiRequests] = React.useState<{ [string]: AiRequest }>(
|
||||
{}
|
||||
const [state, setState] = React.useState<PaginationState>(
|
||||
emptyPaginationState
|
||||
);
|
||||
const [error, setError] = React.useState<Error | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
|
||||
const fetchAiRequests = React.useCallback(
|
||||
async () => {
|
||||
if (!profile) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const history = await getAiRequests(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
forceUri: null, // Fetch the first page.
|
||||
});
|
||||
if (!history) return;
|
||||
const aiRequestsById = history.aiRequests.reduce(
|
||||
(accumulator, aiRequest) => {
|
||||
accumulator[aiRequest.id] = aiRequest;
|
||||
return accumulator;
|
||||
},
|
||||
{}
|
||||
);
|
||||
setState({
|
||||
aiRequests: aiRequestsById,
|
||||
nextPageUri: history.nextPageUri,
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
console.error('Error fetching AI requests:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[profile, getAuthorizationHeader]
|
||||
);
|
||||
|
||||
const onLoadMoreAiRequests = React.useCallback(
|
||||
async () => {
|
||||
if (!profile) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const history = await getAiRequests(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
forceUri: state.nextPageUri,
|
||||
});
|
||||
if (!history) return;
|
||||
const newRequests = history.aiRequests;
|
||||
const currentRequestsById = state.aiRequests;
|
||||
|
||||
newRequests.forEach(newRequest => {
|
||||
// Add new requests to the state.
|
||||
if (!currentRequestsById[newRequest.id]) {
|
||||
currentRequestsById[newRequest.id] = newRequest;
|
||||
}
|
||||
});
|
||||
setState({
|
||||
aiRequests: currentRequestsById,
|
||||
nextPageUri: history.nextPageUri,
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
console.error('Error fetching AI requests:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[profile, getAuthorizationHeader, state.nextPageUri, state.aiRequests]
|
||||
);
|
||||
|
||||
const updateAiRequest = React.useCallback(
|
||||
(aiRequestId: string, aiRequest: AiRequest) => {
|
||||
setAiRequests(aiRequests => ({
|
||||
...aiRequests,
|
||||
[aiRequestId]: aiRequest,
|
||||
setState(prevState => ({
|
||||
...prevState,
|
||||
aiRequests: {
|
||||
...(prevState.aiRequests || {}),
|
||||
[aiRequestId]: aiRequest,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[]
|
||||
@@ -179,7 +270,12 @@ export const useAiRequestsStorage = (): AiRequestStorage => {
|
||||
);
|
||||
|
||||
return {
|
||||
aiRequests,
|
||||
fetchAiRequests,
|
||||
onLoadMoreAiRequests,
|
||||
canLoadMore: !!state.nextPageUri,
|
||||
error,
|
||||
isLoading,
|
||||
aiRequests: state.aiRequests,
|
||||
updateAiRequest,
|
||||
refreshAiRequest,
|
||||
isSendingAiRequest,
|
||||
@@ -195,8 +291,13 @@ type AiRequestContextState = {|
|
||||
getAiSettings: () => AiSettings | null,
|
||||
|};
|
||||
|
||||
export const AiRequestContext = React.createContext<AiRequestContextState>({
|
||||
export const initialAiRequestContextState: AiRequestContextState = {
|
||||
aiRequestStorage: {
|
||||
fetchAiRequests: async () => {},
|
||||
onLoadMoreAiRequests: async () => {},
|
||||
canLoadMore: true,
|
||||
error: null,
|
||||
isLoading: false,
|
||||
aiRequests: {},
|
||||
updateAiRequest: () => {},
|
||||
refreshAiRequest: async () => {},
|
||||
@@ -211,7 +312,10 @@ export const AiRequestContext = React.createContext<AiRequestContextState>({
|
||||
clearEditorFunctionCallResults: () => {},
|
||||
},
|
||||
getAiSettings: () => null,
|
||||
});
|
||||
};
|
||||
export const AiRequestContext = React.createContext<AiRequestContextState>(
|
||||
initialAiRequestContextState
|
||||
);
|
||||
|
||||
type AiRequestProviderProps = {|
|
||||
children: React.Node,
|
||||
|
@@ -480,6 +480,9 @@ export const AskAiEditor = React.memo<Props>(
|
||||
[onOpenLayout, onCreateProject]
|
||||
);
|
||||
|
||||
const {
|
||||
aiRequestStorage: { fetchAiRequests },
|
||||
} = React.useContext(AiRequestContext);
|
||||
const {
|
||||
selectedAiRequest,
|
||||
selectedAiRequestId,
|
||||
@@ -511,6 +514,15 @@ export const AskAiEditor = React.memo<Props>(
|
||||
[initialMode]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
fetchAiRequests();
|
||||
},
|
||||
// Only fetch once on mount (we provide a way to refresh in the history).
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const canStartNewChat = !!selectedAiRequestId;
|
||||
const onStartOrOpenChat = React.useCallback(
|
||||
({
|
||||
|
@@ -3,14 +3,10 @@ import * as React from 'react';
|
||||
import Drawer from '@material-ui/core/Drawer';
|
||||
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||
import { Line, Column } from '../UI/Grid';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import { ColumnStackLayout, LineStackLayout } from '../UI/Layout';
|
||||
import Text from '../UI/Text';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import {
|
||||
getAiRequests,
|
||||
type AiRequest,
|
||||
} from '../Utils/GDevelopServices/Generation';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { type AiRequest } from '../Utils/GDevelopServices/Generation';
|
||||
import Paper from '../UI/Paper';
|
||||
import ScrollView from '../UI/ScrollView';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
@@ -21,6 +17,7 @@ import formatDate from 'date-fns/format';
|
||||
import DrawerTopBar from '../UI/DrawerTopBar';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import { textEllipsisStyle } from '../UI/TextEllipsis';
|
||||
import { AiRequestContext } from './AiRequestContext';
|
||||
|
||||
type Props = {|
|
||||
open: boolean,
|
||||
@@ -79,23 +76,27 @@ const getFirstUserRequestText = (aiRequest: AiRequest): string => {
|
||||
};
|
||||
|
||||
type AskAiHistoryContentProps = {|
|
||||
aiRequests: Array<AiRequest> | null,
|
||||
isLoading: boolean,
|
||||
error: ?Error,
|
||||
onSelectAiRequest: (aiRequest: AiRequest) => void,
|
||||
selectedAiRequestId: string | null,
|
||||
onFetchAiRequests: () => Promise<void>,
|
||||
|};
|
||||
|
||||
export const AskAiHistoryContent = ({
|
||||
aiRequests,
|
||||
isLoading,
|
||||
error,
|
||||
onSelectAiRequest,
|
||||
selectedAiRequestId,
|
||||
onFetchAiRequests,
|
||||
}: AskAiHistoryContentProps) => {
|
||||
if (!aiRequests && isLoading) {
|
||||
const {
|
||||
aiRequestStorage: {
|
||||
aiRequests,
|
||||
fetchAiRequests,
|
||||
onLoadMoreAiRequests,
|
||||
canLoadMore,
|
||||
isLoading,
|
||||
error,
|
||||
},
|
||||
} = React.useContext(AiRequestContext);
|
||||
// $FlowFixMe - Flow loses type with Object.values
|
||||
const aiRequestsArray: AiRequest[] = Object.values(aiRequests);
|
||||
if (!aiRequestsArray.length && isLoading) {
|
||||
return (
|
||||
<Column
|
||||
noMargin
|
||||
@@ -111,13 +112,13 @@ export const AskAiHistoryContent = ({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<PlaceholderError onRetry={onFetchAiRequests}>
|
||||
<PlaceholderError onRetry={fetchAiRequests}>
|
||||
<Trans>An error occurred while loading your AI requests.</Trans>
|
||||
</PlaceholderError>
|
||||
);
|
||||
}
|
||||
|
||||
if (!aiRequests || aiRequests.length === 0) {
|
||||
if (aiRequestsArray.length === 0) {
|
||||
return (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
@@ -130,7 +131,7 @@ export const AskAiHistoryContent = ({
|
||||
return (
|
||||
<ScrollView>
|
||||
<ColumnStackLayout expand>
|
||||
{aiRequests.map(aiRequest => {
|
||||
{aiRequestsArray.map(aiRequest => {
|
||||
const isSelected = selectedAiRequestId === aiRequest.id;
|
||||
const userRequestText = getFirstUserRequestText(aiRequest);
|
||||
const requestDate = new Date(aiRequest.createdAt);
|
||||
@@ -176,14 +177,20 @@ export const AskAiHistoryContent = ({
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
<Line justifyContent="center">
|
||||
<LineStackLayout justifyContent="center">
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>Refresh</Trans>}
|
||||
onClick={onFetchAiRequests}
|
||||
onClick={fetchAiRequests}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Line>
|
||||
<FlatButton
|
||||
primary
|
||||
label={<Trans>Load more</Trans>}
|
||||
onClick={onLoadMoreAiRequests}
|
||||
disabled={isLoading || !canLoadMore}
|
||||
/>
|
||||
</LineStackLayout>
|
||||
</ColumnStackLayout>
|
||||
</ScrollView>
|
||||
);
|
||||
@@ -196,46 +203,6 @@ export const AskAiHistory = ({
|
||||
selectedAiRequestId,
|
||||
}: Props) => {
|
||||
const { isMobile } = useResponsiveWindowSize();
|
||||
const [aiRequests, setAiRequests] = React.useState<Array<AiRequest> | null>(
|
||||
null
|
||||
);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<Error | null>(null);
|
||||
|
||||
const { profile, getAuthorizationHeader } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
|
||||
const fetchAiRequests = React.useCallback(
|
||||
async () => {
|
||||
if (!profile) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const requests = await getAiRequests(getAuthorizationHeader, {
|
||||
userId: profile.id,
|
||||
});
|
||||
setAiRequests(requests);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
console.error('Error fetching AI requests:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[profile, getAuthorizationHeader]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (open) {
|
||||
fetchAiRequests();
|
||||
}
|
||||
},
|
||||
[open, fetchAiRequests]
|
||||
);
|
||||
|
||||
const handleSelectAiRequest = (aiRequest: AiRequest) => {
|
||||
onSelectAiRequest(aiRequest);
|
||||
@@ -265,12 +232,8 @@ export const AskAiHistory = ({
|
||||
onClose={onClose}
|
||||
/>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={aiRequests}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
onSelectAiRequest={handleSelectAiRequest}
|
||||
selectedAiRequestId={selectedAiRequestId}
|
||||
onFetchAiRequests={fetchAiRequests}
|
||||
/>
|
||||
</ColumnStackLayout>
|
||||
</Drawer>
|
||||
|
@@ -12,6 +12,7 @@ export type CompactTextAreaFieldWithControlsProps = {|
|
||||
value: string,
|
||||
onChange: (newValue: string) => void,
|
||||
onSubmit?: () => void,
|
||||
onNavigateHistory?: (direction: 'up' | 'down') => void,
|
||||
id?: string,
|
||||
disabled?: boolean,
|
||||
errored?: boolean,
|
||||
@@ -23,60 +24,125 @@ export type CompactTextAreaFieldWithControlsProps = {|
|
||||
hasAnimatedNeonCorner?: boolean,
|
||||
|};
|
||||
|
||||
export const CompactTextAreaFieldWithControls = ({
|
||||
value,
|
||||
onChange,
|
||||
id,
|
||||
disabled,
|
||||
errored,
|
||||
placeholder,
|
||||
rows,
|
||||
maxLength,
|
||||
onSubmit,
|
||||
controls,
|
||||
hasNeonCorner,
|
||||
hasAnimatedNeonCorner,
|
||||
}: CompactTextAreaFieldWithControlsProps) => {
|
||||
const idToUse = React.useRef<string>(id || makeTimestampedId());
|
||||
export type CompactTextAreaFieldWithControlsInterface = {|
|
||||
setCursorPosition: (position: number) => void,
|
||||
|};
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<label
|
||||
className={classNames({
|
||||
[classes.container]: true,
|
||||
[classes.disabled]: disabled,
|
||||
[classes.errored]: errored,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
export const CompactTextAreaFieldWithControls = React.forwardRef<
|
||||
CompactTextAreaFieldWithControlsProps,
|
||||
CompactTextAreaFieldWithControlsInterface
|
||||
>(
|
||||
(
|
||||
{
|
||||
value,
|
||||
onChange,
|
||||
id,
|
||||
disabled,
|
||||
errored,
|
||||
placeholder,
|
||||
rows,
|
||||
maxLength,
|
||||
onSubmit,
|
||||
onNavigateHistory,
|
||||
controls,
|
||||
hasNeonCorner,
|
||||
hasAnimatedNeonCorner,
|
||||
}: CompactTextAreaFieldWithControlsProps,
|
||||
ref
|
||||
) => {
|
||||
const idToUse = React.useRef<string>(id || makeTimestampedId());
|
||||
const textareaRef = React.useRef<?HTMLTextAreaElement>(null);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
setCursorPosition: (position: number) => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.setSelectionRange(position, position);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
const handleKeyDown = React.useCallback(
|
||||
(e: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// Handle submit first
|
||||
if (onSubmit && shouldSubmit(e)) {
|
||||
onSubmit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!onNavigateHistory) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isArrowUp = e.key === 'ArrowUp';
|
||||
const isArrowDown = e.key === 'ArrowDown';
|
||||
|
||||
if (!isArrowUp && !isArrowDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
const textarea = e.currentTarget;
|
||||
const { selectionStart, value: textValue } = textarea;
|
||||
|
||||
// Calculate cursor position info
|
||||
const textBeforeCursor = textValue.substring(0, selectionStart);
|
||||
const lines = textValue.split('\n');
|
||||
const currentLineIndex = textBeforeCursor.split('\n').length - 1;
|
||||
const currentLineStart = textBeforeCursor.lastIndexOf('\n') + 1;
|
||||
const currentLine = lines[currentLineIndex];
|
||||
const positionInLine = selectionStart - currentLineStart;
|
||||
|
||||
// Check if we should navigate history
|
||||
const isAtFirstLineStart =
|
||||
currentLineIndex === 0 && positionInLine === 0;
|
||||
const isAtLastLineEnd =
|
||||
currentLineIndex === lines.length - 1 &&
|
||||
positionInLine === currentLine.length;
|
||||
|
||||
if (
|
||||
(isArrowUp && isAtFirstLineStart) ||
|
||||
(isArrowDown && isAtLastLineEnd)
|
||||
) {
|
||||
e.preventDefault();
|
||||
onNavigateHistory(isArrowUp ? 'up' : 'down');
|
||||
}
|
||||
},
|
||||
[onSubmit, onNavigateHistory]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<label
|
||||
className={classNames({
|
||||
[classes.compactTextAreaField]: true,
|
||||
[classes.neonCorner]: hasNeonCorner,
|
||||
[classes.animatedNeonCorner]:
|
||||
hasNeonCorner && hasAnimatedNeonCorner,
|
||||
[classes.container]: true,
|
||||
[classes.disabled]: disabled,
|
||||
[classes.errored]: errored,
|
||||
})}
|
||||
>
|
||||
<textarea
|
||||
id={idToUse.current}
|
||||
disabled={disabled}
|
||||
value={value === null ? '' : value}
|
||||
onChange={e => onChange(e.currentTarget.value)}
|
||||
placeholder={i18n._(placeholder)}
|
||||
onKeyDown={
|
||||
onSubmit
|
||||
? e => {
|
||||
if (shouldSubmit(e)) onSubmit();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
rows={rows || 3}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
{controls}
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
<div
|
||||
className={classNames({
|
||||
[classes.compactTextAreaField]: true,
|
||||
[classes.neonCorner]: hasNeonCorner,
|
||||
[classes.animatedNeonCorner]:
|
||||
hasNeonCorner && hasAnimatedNeonCorner,
|
||||
})}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
id={idToUse.current}
|
||||
disabled={disabled}
|
||||
value={value === null ? '' : value}
|
||||
onChange={e => onChange(e.currentTarget.value)}
|
||||
placeholder={i18n._(placeholder)}
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={rows || 3}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
{controls}
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@@ -3,6 +3,7 @@ import axios from 'axios';
|
||||
import { GDevelopAiCdn, GDevelopGenerationApi } from './ApiConfigs';
|
||||
import { type MessageByLocale } from '../i18n/MessageByLocale';
|
||||
import { getIDEVersionWithHash } from '../../Version';
|
||||
import { extractNextPageUriFromLinkHeader } from './Play';
|
||||
|
||||
export type Environment = 'staging' | 'live';
|
||||
|
||||
@@ -22,39 +23,43 @@ export type AiRequestFunctionCallOutput = {
|
||||
output: string,
|
||||
};
|
||||
|
||||
export type AiRequestMessage =
|
||||
| {
|
||||
type: 'message',
|
||||
status: 'completed',
|
||||
role: 'assistant',
|
||||
content: Array<
|
||||
| {
|
||||
type: 'reasoning',
|
||||
status: 'completed',
|
||||
summary: {
|
||||
text: string,
|
||||
type: 'summary_text',
|
||||
},
|
||||
}
|
||||
| {
|
||||
type: 'output_text',
|
||||
status: 'completed',
|
||||
text: string,
|
||||
annotations: Array<{}>,
|
||||
}
|
||||
| AiRequestMessageAssistantFunctionCall
|
||||
>,
|
||||
}
|
||||
| {
|
||||
type: 'message',
|
||||
status: 'completed',
|
||||
role: 'user',
|
||||
content: Array<{
|
||||
type: 'user_request',
|
||||
export type AiRequestAssistantMessage = {
|
||||
type: 'message',
|
||||
status: 'completed',
|
||||
role: 'assistant',
|
||||
content: Array<
|
||||
| {
|
||||
type: 'reasoning',
|
||||
status: 'completed',
|
||||
summary: {
|
||||
text: string,
|
||||
type: 'summary_text',
|
||||
},
|
||||
}
|
||||
| {
|
||||
type: 'output_text',
|
||||
status: 'completed',
|
||||
text: string,
|
||||
}>,
|
||||
}
|
||||
annotations: Array<{}>,
|
||||
}
|
||||
| AiRequestMessageAssistantFunctionCall
|
||||
>,
|
||||
};
|
||||
|
||||
export type AiRequestUserMessage = {
|
||||
type: 'message',
|
||||
status: 'completed',
|
||||
role: 'user',
|
||||
content: Array<{
|
||||
type: 'user_request',
|
||||
status: 'completed',
|
||||
text: string,
|
||||
}>,
|
||||
};
|
||||
|
||||
export type AiRequestMessage =
|
||||
| AiRequestAssistantMessage
|
||||
| AiRequestUserMessage
|
||||
| AiRequestFunctionCallOutput;
|
||||
|
||||
export type AiConfiguration = {
|
||||
@@ -166,6 +171,10 @@ export type AssetSearch = {
|
||||
}> | null,
|
||||
};
|
||||
|
||||
export const apiClient = axios.create({
|
||||
baseURL: GDevelopGenerationApi.baseUrl,
|
||||
});
|
||||
|
||||
export const getAiRequest = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
{
|
||||
@@ -195,23 +204,37 @@ export const getAiRequests = async (
|
||||
getAuthorizationHeader: () => Promise<string>,
|
||||
{
|
||||
userId,
|
||||
forceUri,
|
||||
}: {|
|
||||
userId: string,
|
||||
forceUri: ?string,
|
||||
|}
|
||||
): Promise<Array<AiRequest>> => {
|
||||
): Promise<{
|
||||
aiRequests: Array<AiRequest>,
|
||||
nextPageUri: ?string,
|
||||
}> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.get(
|
||||
`${GDevelopGenerationApi.baseUrl}/ai-request`,
|
||||
{
|
||||
params: {
|
||||
userId,
|
||||
},
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
const uri = forceUri || '/ai-request';
|
||||
|
||||
// $FlowFixMe
|
||||
const response = await apiClient.get(uri, {
|
||||
headers: {
|
||||
Authorization: authorizationHeader,
|
||||
},
|
||||
params: forceUri ? { userId } : { userId, perPage: 10 },
|
||||
});
|
||||
const nextPageUri = response.headers.link
|
||||
? extractNextPageUriFromLinkHeader(response.headers.link)
|
||||
: null;
|
||||
const aiRequests = response.data;
|
||||
if (!Array.isArray(aiRequests)) {
|
||||
throw new Error('Invalid response from Ai requests API.');
|
||||
}
|
||||
|
||||
return {
|
||||
aiRequests,
|
||||
nextPageUri,
|
||||
};
|
||||
};
|
||||
|
||||
export const createAiRequest = async (
|
||||
@@ -252,8 +275,8 @@ export const createAiRequest = async (
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.post(
|
||||
`${GDevelopGenerationApi.baseUrl}/ai-request`,
|
||||
const response = await apiClient.post(
|
||||
'/ai-request',
|
||||
{
|
||||
gdevelopVersionWithHash: getIDEVersionWithHash(),
|
||||
userRequest,
|
||||
@@ -306,10 +329,8 @@ export const addMessageToAiRequest = async (
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.post(
|
||||
`${
|
||||
GDevelopGenerationApi.baseUrl
|
||||
}/ai-request/${aiRequestId}/action/add-message`,
|
||||
const response = await apiClient.post(
|
||||
`/ai-request/${aiRequestId}/action/add-message`,
|
||||
{
|
||||
gdevelopVersionWithHash: getIDEVersionWithHash(),
|
||||
functionCallOutputs,
|
||||
@@ -351,10 +372,8 @@ export const sendAiRequestFeedback = async (
|
||||
|}
|
||||
): Promise<AiRequest> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.post(
|
||||
`${
|
||||
GDevelopGenerationApi.baseUrl
|
||||
}/ai-request/${aiRequestId}/action/set-feedback`,
|
||||
const response = await apiClient.post(
|
||||
`/ai-request/${aiRequestId}/action/set-feedback`,
|
||||
{
|
||||
gdevelopVersionWithHash: getIDEVersionWithHash(),
|
||||
messageIndex,
|
||||
@@ -415,8 +434,8 @@ export const createAiGeneratedEvent = async (
|
||||
|}
|
||||
): Promise<CreateAiGeneratedEventResult> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.post(
|
||||
`${GDevelopGenerationApi.baseUrl}/ai-generated-event`,
|
||||
const response = await apiClient.post(
|
||||
`/ai-generated-event`,
|
||||
{
|
||||
gdevelopVersionWithHash: getIDEVersionWithHash(),
|
||||
gameProjectJson,
|
||||
@@ -474,8 +493,8 @@ export const getAiGeneratedEvent = async (
|
||||
|}
|
||||
): Promise<AiGeneratedEvent> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.get(
|
||||
`${GDevelopGenerationApi.baseUrl}/ai-generated-event/${aiGeneratedEventId}`,
|
||||
const response = await apiClient.get(
|
||||
`/ai-generated-event/${aiGeneratedEventId}`,
|
||||
{
|
||||
params: {
|
||||
userId,
|
||||
@@ -505,8 +524,8 @@ export const createAssetSearch = async (
|
||||
|}
|
||||
): Promise<AssetSearch> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.post(
|
||||
`${GDevelopGenerationApi.baseUrl}/asset-search`,
|
||||
const response = await apiClient.post(
|
||||
`/asset-search`,
|
||||
{
|
||||
gdevelopVersionWithHash: getIDEVersionWithHash(),
|
||||
searchTerms,
|
||||
@@ -546,10 +565,8 @@ export const createAiUserContentPresignedUrls = async (
|
||||
|}
|
||||
): Promise<AiUserContentPresignedUrlsResult> => {
|
||||
const authorizationHeader = await getAuthorizationHeader();
|
||||
const response = await axios.post(
|
||||
`${
|
||||
GDevelopGenerationApi.baseUrl
|
||||
}/ai-user-content/action/create-presigned-urls`,
|
||||
const response = await apiClient.post(
|
||||
`/ai-user-content/action/create-presigned-urls`,
|
||||
{
|
||||
gdevelopVersionWithHash: getIDEVersionWithHash(),
|
||||
gameProjectJsonHash,
|
||||
|
@@ -3,6 +3,11 @@ import * as React from 'react';
|
||||
import paperDecorator from '../../PaperDecorator';
|
||||
import { AskAiHistoryContent } from '../../../AiGeneration/AskAiHistory';
|
||||
import FixedHeightFlexContainer from '../../FixedHeightFlexContainer';
|
||||
import {
|
||||
AiRequestContext,
|
||||
initialAiRequestContextState,
|
||||
} from '../../../AiGeneration/AiRequestContext';
|
||||
import { type AiRequest } from '../../../Utils/GDevelopServices/Generation';
|
||||
|
||||
// Re-use fake AI request data from AiRequestChat.stories.js
|
||||
const fakeOutputWithUserRequestOnly = [
|
||||
@@ -93,183 +98,190 @@ export default {
|
||||
decorators: [paperDecorator],
|
||||
};
|
||||
|
||||
export const Loading = () => (
|
||||
const AskAIHistoryContentStoryTemplate = ({
|
||||
error,
|
||||
isLoading,
|
||||
aiRequests,
|
||||
canLoadMore,
|
||||
selectedAiRequestId,
|
||||
}: {|
|
||||
error: ?Error,
|
||||
isLoading: boolean,
|
||||
aiRequests: { [string]: AiRequest },
|
||||
canLoadMore: boolean,
|
||||
selectedAiRequestId: string | null,
|
||||
|}) => (
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={null}
|
||||
isLoading={true}
|
||||
error={null}
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId={null}
|
||||
onFetchAiRequests={async () => {}}
|
||||
/>
|
||||
<AiRequestContext.Provider
|
||||
value={{
|
||||
...initialAiRequestContextState,
|
||||
aiRequestStorage: {
|
||||
...initialAiRequestContextState.aiRequestStorage,
|
||||
aiRequests,
|
||||
isLoading,
|
||||
error,
|
||||
canLoadMore,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AskAiHistoryContent
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId={selectedAiRequestId}
|
||||
/>
|
||||
</AiRequestContext.Provider>
|
||||
</FixedHeightFlexContainer>
|
||||
);
|
||||
|
||||
export const Loading = () => (
|
||||
<AskAIHistoryContentStoryTemplate
|
||||
aiRequests={{}}
|
||||
isLoading={true}
|
||||
error={null}
|
||||
selectedAiRequestId={null}
|
||||
canLoadMore={false}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Errored = () => (
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={null}
|
||||
isLoading={false}
|
||||
error={new Error('Failed to fetch AI requests')}
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId={null}
|
||||
onFetchAiRequests={async () => {}}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
<AskAIHistoryContentStoryTemplate
|
||||
aiRequests={{}}
|
||||
isLoading={false}
|
||||
error={new Error('Failed to fetch AI requests')}
|
||||
selectedAiRequestId={null}
|
||||
canLoadMore={false}
|
||||
/>
|
||||
);
|
||||
|
||||
export const Empty = () => (
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={[]}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId={null}
|
||||
onFetchAiRequests={async () => {}}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
<AskAIHistoryContentStoryTemplate
|
||||
aiRequests={{}}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
selectedAiRequestId={null}
|
||||
canLoadMore={false}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SingleAiRequest = () => (
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={[
|
||||
createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
createdAt: '2024-03-15T10:30:00Z',
|
||||
output: fakeOutputWithAiResponses,
|
||||
}),
|
||||
]}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId={null}
|
||||
onFetchAiRequests={async () => {}}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
<AskAIHistoryContentStoryTemplate
|
||||
aiRequests={{
|
||||
'request-1': createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
createdAt: '2024-03-15T10:30:00Z',
|
||||
output: fakeOutputWithAiResponses,
|
||||
}),
|
||||
}}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
selectedAiRequestId={null}
|
||||
canLoadMore={false}
|
||||
/>
|
||||
);
|
||||
|
||||
export const MultipleAiRequests = () => (
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={[
|
||||
createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
output: fakeOutputWithAiResponses,
|
||||
}),
|
||||
createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
output: fakeOutputWithDifferentUserRequest,
|
||||
}),
|
||||
createFakeAiRequest({
|
||||
id: 'request-3',
|
||||
createdAt: '2024-03-10T16:20:00Z',
|
||||
}),
|
||||
]}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId={null}
|
||||
onFetchAiRequests={async () => {}}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
<AskAIHistoryContentStoryTemplate
|
||||
aiRequests={{
|
||||
'request-1': createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
output: fakeOutputWithAiResponses,
|
||||
}),
|
||||
'request-2': createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
output: fakeOutputWithDifferentUserRequest,
|
||||
}),
|
||||
'request-3': createFakeAiRequest({
|
||||
id: 'request-3',
|
||||
createdAt: '2024-03-10T16:20:00Z',
|
||||
}),
|
||||
}}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
selectedAiRequestId={null}
|
||||
canLoadMore
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithSelectedRequest = () => (
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={[
|
||||
createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
output: fakeOutputWithAiResponses,
|
||||
}),
|
||||
createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
output: fakeOutputWithDifferentUserRequest,
|
||||
}),
|
||||
]}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId="request-2"
|
||||
onFetchAiRequests={async () => {}}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
<AskAIHistoryContentStoryTemplate
|
||||
aiRequests={{
|
||||
'request-1': createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
output: fakeOutputWithAiResponses,
|
||||
}),
|
||||
'request-2': createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
output: fakeOutputWithDifferentUserRequest,
|
||||
}),
|
||||
}}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
selectedAiRequestId="request-2"
|
||||
canLoadMore={false}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithWorkingRequest = () => (
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={[
|
||||
createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
status: 'working',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
}),
|
||||
createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
output: fakeOutputWithDifferentUserRequest,
|
||||
}),
|
||||
]}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId={null}
|
||||
onFetchAiRequests={async () => {}}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
<AskAIHistoryContentStoryTemplate
|
||||
aiRequests={{
|
||||
'request-1': createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
status: 'working',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
}),
|
||||
'request-2': createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
output: fakeOutputWithDifferentUserRequest,
|
||||
}),
|
||||
}}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
selectedAiRequestId={null}
|
||||
canLoadMore={false}
|
||||
/>
|
||||
);
|
||||
|
||||
export const WithErroredRequest = () => (
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={[
|
||||
createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
status: 'error',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
error: { code: 'internal-error', message: 'Some error happened' },
|
||||
}),
|
||||
createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
output: fakeOutputWithDifferentUserRequest,
|
||||
}),
|
||||
]}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId={null}
|
||||
onFetchAiRequests={async () => {}}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
<AskAIHistoryContentStoryTemplate
|
||||
aiRequests={{
|
||||
'request-1': createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
status: 'error',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
error: { code: 'internal-error', message: 'Some error happened' },
|
||||
}),
|
||||
'request-2': createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
output: fakeOutputWithDifferentUserRequest,
|
||||
}),
|
||||
}}
|
||||
isLoading={false}
|
||||
error={null}
|
||||
selectedAiRequestId={null}
|
||||
canLoadMore={false}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RefreshingRequests = () => (
|
||||
<FixedHeightFlexContainer height={500}>
|
||||
<AskAiHistoryContent
|
||||
aiRequests={[
|
||||
createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
}),
|
||||
createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
}),
|
||||
]}
|
||||
isLoading={true}
|
||||
error={null}
|
||||
onSelectAiRequest={() => {}}
|
||||
selectedAiRequestId={null}
|
||||
onFetchAiRequests={async () => {}}
|
||||
/>
|
||||
</FixedHeightFlexContainer>
|
||||
<AskAIHistoryContentStoryTemplate
|
||||
aiRequests={{
|
||||
'request-1': createFakeAiRequest({
|
||||
id: 'request-1',
|
||||
createdAt: '2024-03-15T14:30:00Z',
|
||||
}),
|
||||
'request-2': createFakeAiRequest({
|
||||
id: 'request-2',
|
||||
createdAt: '2024-03-14T09:45:00Z',
|
||||
}),
|
||||
}}
|
||||
isLoading={true}
|
||||
error={null}
|
||||
selectedAiRequestId={null}
|
||||
canLoadMore={false}
|
||||
/>
|
||||
);
|
||||
|
Reference in New Issue
Block a user