Improve Ask AI tab

Don't show in changelog
This commit is contained in:
Florian Rival
2025-03-31 18:46:48 +02:00
parent aa90621899
commit 416ef44ee1
12 changed files with 259 additions and 79 deletions

View File

@@ -25,6 +25,9 @@ import Like from '../../../UI/CustomSvgIcons/Like';
import Dislike from '../../../UI/CustomSvgIcons/Dislike';
import GDevelopThemeContext from '../../../UI/Theme/GDevelopThemeContext';
import { type MessageDescriptor } from '../../../Utils/i18n/MessageDescriptor.flow';
import Link from '../../../UI/Link';
import { getHelpLink } from '../../../Utils/HelpLink';
import Window from '../../../Utils/Window';
const TOO_MANY_MESSAGES_WARNING_COUNT = 9;
const TOO_MANY_MESSAGES_ERROR_COUNT = 14;
@@ -200,66 +203,80 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
An error happened when sending your request, please try again.
</Trans>
) : (
quotaOrCreditsExplanation
quotaOrCreditsExplanation || '\u00a0'
)}
</Text>
);
if (!aiRequest) {
const disclaimer = errorOrQuotaOrCreditsExplanation || (
<Text size="body2" color="secondary">
<Trans>
The AI will answer according to your game project. Always verify AI
answers before applying them.
</Trans>
</Text>
);
return (
<div className={classes.newChatContainer}>
<Line noMargin justifyContent="center">
<RobotIcon rotating size={40} />
</Line>
<Column noMargin alignItems="center">
<Text size="bold-title">
<Trans>What do you want to make?</Trans>
<ColumnStackLayout justifyContent="center" expand>
<Line noMargin justifyContent="center">
<RobotIcon rotating size={40} />
</Line>
<Column noMargin alignItems="center">
<Text size="bold-title">
<Trans>What do you want to make?</Trans>
</Text>
</Column>
<Column noMargin alignItems="stretch" justifyContent="stretch">
<CompactTextAreaField
maxLength={6000}
value={userRequestText}
disabled={isLaunchingAiRequest}
onChange={userRequestText =>
setUserRequestText(userRequestText)
}
placeholder={newChatPlaceholder}
rows={5}
/>
</Column>
<Line noMargin>
<ResponsiveLineStackLayout
noMargin
alignItems="flex-start"
justifyContent="space-between"
expand
>
{!isMobile && errorOrQuotaOrCreditsExplanation}
<Line noMargin justifyContent="flex-end">
<LeftLoader reserveSpace isLoading={isLaunchingAiRequest}>
<RaisedButton
color="primary"
label={<Trans>Send</Trans>}
style={{ flexShrink: 0 }}
disabled={isLaunchingAiRequest}
onClick={() => {
onSendUserRequest(userRequestText);
}}
/>
</LeftLoader>
</Line>
{isMobile && errorOrQuotaOrCreditsExplanation}
</ResponsiveLineStackLayout>
</Line>
{subscriptionBanner}
</ColumnStackLayout>
<Column justifyContent="center" noMargin>
<Text size="body-small" color="secondary" align="center" noMargin>
<Trans>
The AI is experimental and still being improved.{' '}
<Link
href={getHelpLink('/interface/ask-ai')}
color="secondary"
onClick={() =>
Window.openExternalURL(getHelpLink('/interface/ask-ai'))
}
>
It has access to your game objects but not events.
</Link>
</Trans>
</Text>
<Text size="body-small" color="secondary" align="center" noMargin>
<Trans>Answers may have mistakes: always verify them.</Trans>
</Text>
</Column>
<Column noMargin alignItems="stretch" justifyContent="stretch">
<CompactTextAreaField
maxLength={6000}
value={userRequestText}
disabled={isLaunchingAiRequest}
onChange={userRequestText => setUserRequestText(userRequestText)}
placeholder={newChatPlaceholder}
rows={5}
/>
</Column>
<Line noMargin>
<ResponsiveLineStackLayout
noMargin
alignItems="flex-start"
justifyContent="space-between"
expand
>
{!isMobile && disclaimer}
<Line noMargin justifyContent="flex-end">
<LeftLoader reserveSpace isLoading={isLaunchingAiRequest}>
<RaisedButton
color="primary"
label={<Trans>Send</Trans>}
style={{ flexShrink: 0 }}
disabled={isLaunchingAiRequest}
onClick={() => {
onSendUserRequest(userRequestText);
}}
/>
</LeftLoader>
</Line>
{isMobile && disclaimer}
</ResponsiveLineStackLayout>
</Line>
{subscriptionBanner}
</div>
);
}
@@ -381,8 +398,8 @@ export const AiRequestChat = React.forwardRef<Props, AiRequestChatInterface>(
<Line justifyContent="flex-start">
<AlertMessage kind="error">
<Trans>
The AI encountered an error while handling your request. Try
again later.
The AI encountered an error while handling your request - this
was request was not counted in your AI usage. Try again later.
</Trans>
</AlertMessage>
</Line>

View File

@@ -51,11 +51,7 @@
display: flex;
flex-direction: column;
flex: 1;
align-items: stretch;
justify-content: center;
gap: 8px;
margin-left: 8px;
margin-right: 8px;
animation: new-chat-appear 0.5s;
}

View File

@@ -9,8 +9,10 @@ a.conceptLink {
}
a.noValidLink {
color: inherit;
cursor: default;
text-decoration: none;
text-decoration: underline;
text-decoration-style: dotted;
}
.conceptLink span {

View File

@@ -151,10 +151,10 @@ export const ConceptLink = ({
return (
<Tooltip
title={[
<Text>
<Text color="inherit">
<b>{getConceptKindLabel(conceptMetadata.kind)}</b>
</Text>,
conceptParent ? <Text>{conceptParent}</Text> : null,
conceptParent ? <Text color="inherit">{conceptParent}</Text> : null,
<MarkdownText source={conceptMetadata.description} />,
]}
placement="bottom"

View File

@@ -23,6 +23,7 @@ export const Toolbar = ({
<ToolbarGroup firstChild>
<IconButton
size="small"
color="default"
tooltip={t`View history`}
onClick={onOpenHistory}
>

View File

@@ -257,10 +257,10 @@ export const buildMainMenuDeclarativeTemplate = ({
label: i18n._(t`Help`),
role: 'help',
submenu: [
// {
// label: i18n._(t`Ask AI (GDevelop chatbot)`),
// onClickSendEvent: 'main-menu-open-ask-ai',
// },
{
label: i18n._(t`Ask AI (GDevelop chatbot)`),
onClickSendEvent: 'main-menu-open-ask-ai',
},
{
label: i18n._(t`GDevelop website`),
onClickOpenLink: 'http://gdevelop.io',

View File

@@ -13,6 +13,11 @@ import { type EditorTab } from './EditorTabs/EditorTabsHandler';
import { getTabId } from './EditorTabs/DraggableEditorTabs';
import { useScreenType } from '../UI/Responsive/ScreenTypeMeasurer';
import TabsTitlebarTooltip from './TabsTitlebarTooltip';
import RobotIcon from '../ProjectCreation/RobotIcon';
import PreferencesContext from './Preferences/PreferencesContext';
import TextButton from '../UI/TextButton';
import { useInterval } from '../Utils/UseInterval';
import { useIsMounted } from '../Utils/UseIsMounted';
const WINDOW_DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part';
const WINDOW_NON_DRAGGABLE_PART_CLASS_NAME = 'title-bar-non-draggable-part';
@@ -34,7 +39,7 @@ const styles = {
},
askAiContainer: {
marginBottom: 4,
marginRight: 0,
marginRight: 1,
marginLeft: 2,
},
};
@@ -50,6 +55,46 @@ type TabsTitlebarProps = {|
onOpenAskAi: () => void,
|};
const useIsAskAiIconAnimated = (shouldDisplayAskAi: boolean) => {
const isMounted = useIsMounted();
const [isAskAiIconAnimated, setIsAskAiIconAnimated] = React.useState(true);
const animate = React.useCallback(
(animationDuration: number) => {
if (isMounted.current) {
setIsAskAiIconAnimated(true);
setTimeout(() => {
if (!isMounted.current) return;
setIsAskAiIconAnimated(false);
}, animationDuration);
}
},
[isMounted]
);
React.useEffect(
() => {
// Animate the icon for a long time at the beginning.
animate(9000);
},
[animate]
);
useInterval(
() => {
setIsAskAiIconAnimated(true);
setTimeout(() => {
setIsAskAiIconAnimated(false);
}, 8000);
},
// Animate the icon every 20 minutes.
shouldDisplayAskAi ? 20 * 60 * 1000 : null
);
return isAskAiIconAnimated;
};
/**
* The titlebar containing a menu, the tabs and giving space for window controls.
*/
@@ -63,6 +108,7 @@ export default function TabsTitlebar({
const isTouchscreen = useScreenType() === 'touch';
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const backgroundColor = gdevelopTheme.titlebar.backgroundColor;
const preferences = React.useContext(PreferencesContext);
const [tooltipData, setTooltipData] = React.useState<?{|
element: HTMLElement,
editorTab: EditorTab,
@@ -135,6 +181,10 @@ export default function TabsTitlebar({
[]
);
const shouldDisplayAskAi =
preferences.values.showAiAskButtonInTitleBar && !hasAskAiOpened;
const isAskAiIconAnimated = useIsAskAiIconAnimated(shouldDisplayAskAi);
return (
<div
style={{
@@ -160,17 +210,18 @@ export default function TabsTitlebar({
<MenuIcon />
</IconButton>
{renderTabs(onEditorTabHovered, onEditorTabClosing)}
{/* {!preferences.values.showAiAskButtonInTitleBar ||
hasAskAiOpened ? null : (
<div style={styles.askAiContainer}>
<RaisedButton
icon={<RobotIcon size={16} />}
color="primary"
{shouldDisplayAskAi ? (
<div
style={styles.askAiContainer}
className={WINDOW_NON_DRAGGABLE_PART_CLASS_NAME}
>
<TextButton
icon={<RobotIcon size={16} rotating={isAskAiIconAnimated} />}
label={'Ask AI'}
onClick={onOpenAskAi}
/>
</div>
)} */}
) : null}
<TitleBarRightSafeMargins />
{tooltipData && (
<TabsTitlebarTooltip

View File

@@ -3167,6 +3167,7 @@ const MainFrame = (props: Props) => {
useOpenInitialDialog({
openInAppTutorialDialog: selectInAppTutorial,
openProfileDialog: onOpenProfileDialog,
openAskAi,
});
const onChangeProjectName = async (newName: string): Promise<void> => {

View File

@@ -27,7 +27,8 @@ const useClasses = (rotating, size) =>
transform: 'translate(-50%, -50%)',
borderRadius: '100%',
zIndex: 0,
animation: rotating ? 'spin-background 3s linear infinite' : 'none',
animation: 'spin-background 3s linear infinite',
animationPlayState: rotating ? 'running' : 'paused',
},
'&::after': {
content: "''",

View File

@@ -10,26 +10,37 @@ type Props = {|
href: string,
onClick: () => void | Promise<void>,
disabled?: boolean,
color?: 'primary' | 'secondary',
|};
const useLinkStyles = (theme: GDevelopTheme, disabled: boolean) =>
const useLinkStyles = (
theme: GDevelopTheme,
color: 'primary' | 'secondary',
disabled: boolean
) =>
makeStyles({
root: {
color: theme.link.color.default,
color: color === 'primary' ? theme.link.color.default : undefined,
textDecoration: 'underline',
'&:hover': {
color: !disabled ? theme.link.color.hover : undefined,
color:
color === 'primary' && !disabled ? theme.link.color.hover : undefined,
cursor: !disabled ? 'pointer' : 'default',
},
'&:focus': {
color: !disabled ? theme.link.color.hover : undefined,
color:
color === 'primary' && !disabled ? theme.link.color.hover : undefined,
},
},
})();
const Link = (props: Props) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const linkStyles = useLinkStyles(gdevelopTheme, !!props.disabled);
const linkStyles = useLinkStyles(
gdevelopTheme,
props.color || 'primary',
!!props.disabled
);
const onClick = (event: MouseEvent) => {
event.preventDefault(); // Avoid triggering the href (avoids a warning on mobile in case of unsaved changes).
if (!props.disabled) {

View File

@@ -1,25 +1,38 @@
// @flow
import { mapFor } from './MapFor';
import { isCollectionVariable } from './VariablesUtils';
const gd: libGDevelop = global.gd;
type SimplifiedBehaviorJson = {|
behaviorName: string,
behaviorType: string,
|};
type SimplifiedVariableJson = {|
variableName: string,
type: string,
value?: string,
children: ?Array<SimplifiedVariableJson>,
|};
type SimplifiedObjectJson = {|
objectName: string,
objectType: string,
behaviors: Array<SimplifiedBehaviorJson>,
objectVariables: Array<SimplifiedVariableJson>,
|};
type SimplifiedSceneJson = {|
sceneName: string,
objects: Array<SimplifiedObjectJson>,
sceneVariables: Array<SimplifiedVariableJson>,
|};
type SimplifiedProjectJson = {|
globalObjects: Array<SimplifiedObjectJson>,
scenes: Array<SimplifiedSceneJson>,
globalVariables: Array<SimplifiedVariableJson>,
|};
const getSimplifiedBehaviorJson = (behavior: gdBehavior) => {
@@ -29,7 +42,83 @@ const getSimplifiedBehaviorJson = (behavior: gdBehavior) => {
};
};
const getSimplifiedObjectJson = (object: gdObject) => {
const getVariableType = (variable: gdVariable) => {
const type = variable.getType();
return type === gd.Variable.String
? 'String'
: type === gd.Variable.Number
? 'Number'
: type === gd.Variable.Boolean
? 'Boolean'
: type === gd.Variable.Structure
? 'Structure'
: type === gd.Variable.Array
? 'Array'
: 'unknown';
};
const getVariableValueAsString = (variable: gdVariable) => {
const type = variable.getType();
return type === gd.Variable.Structure || type === gd.Variable.Array
? variable.getChildrenCount() === 0
? `No children`
: variable.getChildrenCount() === 1
? `1 child`
: `${variable.getChildrenCount()} children`
: type === gd.Variable.String
? variable.getString()
: type === gd.Variable.Number
? variable.getValue().toString()
: type === gd.Variable.Boolean
? variable.getBool()
? `True`
: `False`
: 'unknown';
};
const getSimplifiedVariableJson = (
name: string,
variable: gdVariable,
depth = 0
): SimplifiedVariableJson => {
const isCollection = isCollectionVariable(variable);
// Don't diplay children of arrays, and only display the first level of children of structures.
const shouldDisplayChildren =
variable.getType() === gd.Variable.Structure && depth === 0;
return {
variableName: name,
type: getVariableType(variable),
value: !isCollection ? getVariableValueAsString(variable) : undefined,
children: shouldDisplayChildren
? variable
.getAllChildrenNames()
.toJSArray()
.map(childName => {
const childVariable = variable.getChild(childName);
return getSimplifiedVariableJson(
childName,
childVariable,
depth + 1
);
})
.filter(Boolean)
: undefined,
};
};
const getSimplifiedVariablesContainerJson = (
container: gdVariablesContainer
): Array<SimplifiedVariableJson> => {
return mapFor(0, Math.min(container.count(), 20), (index: number) => {
const name = container.getNameAt(index);
const variable = container.getAt(index);
return getSimplifiedVariableJson(name, variable);
}).filter(Boolean);
};
const getSimplifiedObjectJson = (object: gdObject): SimplifiedObjectJson => {
return {
objectName: object.getName(),
objectType: object.getType(),
@@ -42,6 +131,7 @@ const getSimplifiedObjectJson = (object: gdObject) => {
return getSimplifiedBehaviorJson(behavior);
})
.filter(Boolean),
objectVariables: getSimplifiedVariablesContainerJson(object.getVariables()),
};
};
@@ -58,6 +148,7 @@ const getSimplifiedSceneJson = (scene: gdLayout) => {
return {
sceneName: scene.getName(),
objects: getSimplifiedObjectsJson(scene.getObjects()),
sceneVariables: getSimplifiedVariablesContainerJson(scene.getVariables()),
};
};
@@ -73,5 +164,8 @@ export const getSimplifiedProjectJson = (
return {
globalObjects,
scenes,
globalVariables: getSimplifiedVariablesContainerJson(
project.getVariables()
),
};
};

View File

@@ -8,6 +8,7 @@ import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
type Props = {|
openInAppTutorialDialog: (tutorialId: string) => void,
openProfileDialog: () => void,
openAskAi: () => void,
|};
/**
@@ -17,6 +18,7 @@ type Props = {|
const useOpenInitialDialog = ({
openInAppTutorialDialog,
openProfileDialog,
openAskAi,
}: Props) => {
const { routeArguments, removeRouteArguments } = React.useContext(
RouterContext
@@ -72,6 +74,9 @@ const useOpenInitialDialog = ({
// Do nothing as it should open the games dashboard on the homepage
// in the manage tab. So the homepage handles the route arguments itself.
break;
case 'ask-ai':
openAskAi();
break;
default:
break;
}
@@ -85,6 +90,7 @@ const useOpenInitialDialog = ({
authenticated,
onOpenCreateAccountDialog,
onOpenLoginDialog,
openAskAi,
]
);
};