mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Improve Ask AI tab
Don't show in changelog
This commit is contained in:
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -9,8 +9,10 @@ a.conceptLink {
|
||||
}
|
||||
|
||||
a.noValidLink {
|
||||
color: inherit;
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
}
|
||||
|
||||
.conceptLink span {
|
||||
|
@@ -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"
|
||||
|
@@ -23,6 +23,7 @@ export const Toolbar = ({
|
||||
<ToolbarGroup firstChild>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="default"
|
||||
tooltip={t`View history`}
|
||||
onClick={onOpenHistory}
|
||||
>
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
|
@@ -3167,6 +3167,7 @@ const MainFrame = (props: Props) => {
|
||||
useOpenInitialDialog({
|
||||
openInAppTutorialDialog: selectInAppTutorial,
|
||||
openProfileDialog: onOpenProfileDialog,
|
||||
openAskAi,
|
||||
});
|
||||
|
||||
const onChangeProjectName = async (newName: string): Promise<void> => {
|
||||
|
@@ -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: "''",
|
||||
|
@@ -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) {
|
||||
|
@@ -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()
|
||||
),
|
||||
};
|
||||
};
|
||||
|
@@ -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,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user