diff --git a/newIDE/app/src/AiGeneration/AiConfiguration.js b/newIDE/app/src/AiGeneration/AiConfiguration.js index bc22fbae2382..5055fa3f9633 100644 --- a/newIDE/app/src/AiGeneration/AiConfiguration.js +++ b/newIDE/app/src/AiGeneration/AiConfiguration.js @@ -9,6 +9,7 @@ export type AiConfigurationPresetWithAvailability = {| ...AiConfigurationPreset, disabled: boolean, enableWith: 'higher-tier-plan' | null, + enabledWithPlans: Array, |}; export const getAiConfigurationPresetsWithAvailability = ({ @@ -27,6 +28,7 @@ export const getAiConfigurationPresetsWithAvailability = ({ return aiSettings.aiRequest.presets.map(preset => ({ ...preset, enableWith: null, + enabledWithPlans: [], disabled: preset.isDefault ? false : true, })); } @@ -45,6 +47,8 @@ export const getAiConfigurationPresetsWithAvailability = ({ ? presetAvailability.disabled : preset.disabled, enableWith: (presetAvailability && presetAvailability.enableWith) || null, + enabledWithPlans: + (presetAvailability && presetAvailability.enabledWithPlans) || [], }; }); }; diff --git a/newIDE/app/src/AiGeneration/AiRequestChat/AutoEditButton.js b/newIDE/app/src/AiGeneration/AiRequestChat/AutoEditButton.js new file mode 100644 index 000000000000..7e5d462c46a5 --- /dev/null +++ b/newIDE/app/src/AiGeneration/AiRequestChat/AutoEditButton.js @@ -0,0 +1,53 @@ +// @flow +import * as React from 'react'; +import { Trans } from '@lingui/macro'; +import ButtonBase from '@material-ui/core/ButtonBase'; +import Paper from '../../UI/Paper'; +import classes from './AutoEditButton.module.css'; + +type Props = {| + isAutoEditEnabled: boolean, + onToggle: () => void, + disabled?: boolean, +|}; + +const styles = { + paper: { + borderRadius: 8, + display: 'flex', + }, + button: { + padding: '6px 12px', + fontSize: 12, + fontFamily: 'var(--gdevelop-modern-font-family)', + color: 'inherit', + whiteSpace: 'nowrap', + display: 'flex', + alignItems: 'center', + gap: 4, + }, +}; + +const AutoEditButton = ({ + isAutoEditEnabled, + onToggle, + disabled, +}: Props): React.Node => ( + + + Auto edit + + {isAutoEditEnabled ? On : Off} + + + +); + +export default AutoEditButton; diff --git a/newIDE/app/src/AiGeneration/AiRequestChat/AutoEditButton.module.css b/newIDE/app/src/AiGeneration/AiRequestChat/AutoEditButton.module.css new file mode 100644 index 000000000000..1da4ec56f158 --- /dev/null +++ b/newIDE/app/src/AiGeneration/AiRequestChat/AutoEditButton.module.css @@ -0,0 +1,17 @@ +.statusPill { + border-radius: 6px; + padding: 2px 5px; + margin-left: 2px; + min-width: 1.4em; + text-align: center; +} + +.statusPillOn { + background-color: var(--theme-auto-edit-on-background-color); + color: var(--theme-auto-edit-on-text-color); +} + +.statusPillOff { + background-color: var(--theme-auto-edit-off-background-color); + color: var(--theme-auto-edit-off-text-color); +} diff --git a/newIDE/app/src/AiGeneration/AiRequestChat/ReasoningLevelSelector.js b/newIDE/app/src/AiGeneration/AiRequestChat/ReasoningLevelSelector.js new file mode 100644 index 000000000000..942b00957d82 --- /dev/null +++ b/newIDE/app/src/AiGeneration/AiRequestChat/ReasoningLevelSelector.js @@ -0,0 +1,199 @@ +// @flow +import * as React from 'react'; +import { Trans } from '@lingui/macro'; +import { I18n } from '@lingui/react'; +import ButtonBase from '@material-ui/core/ButtonBase'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import ListSubheader from '@material-ui/core/ListSubheader'; +import Tooltip from '@material-ui/core/Tooltip'; +import Paper from '../../UI/Paper'; +import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext'; +import NetworkLow from '../../UI/CustomSvgIcons/NetworkLow'; +import NetworkMedium from '../../UI/CustomSvgIcons/NetworkMedium'; +import NetworkHigh from '../../UI/CustomSvgIcons/NetworkHigh'; +import NetworkMaximum from '../../UI/CustomSvgIcons/NetworkMaximum'; +import ChevronArrowBottom from '../../UI/CustomSvgIcons/ChevronArrowBottom'; +import Silver from '../../Profile/Subscription/Icons/Silver'; +import GoldCompact from '../../Profile/Subscription/Icons/GoldCompact'; +import Startup from '../../Profile/Subscription/Icons/Startup'; +import { type AiConfigurationPresetWithAvailability } from '../AiConfiguration'; +import { selectMessageByLocale } from '../../Utils/i18n/MessageByLocale'; +import { tooltipEnterDelay } from '../../UI/Tooltip'; + +type Props = {| + chosenOrDefaultAiConfigurationPresetId: string, + setAiConfigurationPresetId: string => void, + aiConfigurationPresetsWithAvailability: Array, + disabled?: boolean, +|}; + +const networkIcons = [NetworkLow, NetworkMedium, NetworkHigh, NetworkMaximum]; + +const styles = { + paper: { + borderRadius: 8, + display: 'flex', + }, + button: { + padding: '4px 4px 4px 8px', + display: 'flex', + alignItems: 'center', + }, + menuItemContent: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', + gap: 24, + }, + freeBadge: { + fontSize: 12, + opacity: 0.7, + }, + networkIcon: { + fontSize: 20, + }, + chevronIcon: { + fontSize: 20, + }, + subscriptionIcon: { + width: 20, + height: 20, + }, +}; + +const getSubscriptionBadge = (enabledWithPlans: Array): React.Node => { + if (enabledWithPlans.length === 0) { + return ( + + Free + + ); + } + if ( + enabledWithPlans.some( + p => p === 'gdevelop_silver' || p === 'gdevelop_indie' + ) + ) { + return ; + } + if (enabledWithPlans.some(p => p === 'gdevelop_gold')) { + return ; + } + return ; +}; + +export const ReasoningLevelSelector = ({ + chosenOrDefaultAiConfigurationPresetId, + setAiConfigurationPresetId, + aiConfigurationPresetsWithAvailability, + disabled, +}: Props): React.Node => { + const gdevelopTheme = React.useContext(GDevelopThemeContext); + const networkIconColor = + gdevelopTheme.palette.type === 'light' ? '#7046EC' : '#9979F1'; + const [anchorEl, setAnchorEl] = React.useState(null); + const isMenuOpen = Boolean(anchorEl); + + const orchestratorPresets = aiConfigurationPresetsWithAvailability.filter( + preset => preset.mode === 'orchestrator' + ); + + // Deselect the current preset if it becomes disabled + React.useEffect( + () => { + const currentPreset = orchestratorPresets.find( + preset => preset.id === chosenOrDefaultAiConfigurationPresetId + ); + + if (currentPreset && currentPreset.disabled) { + const firstEnabledPreset = orchestratorPresets.find( + preset => !preset.disabled + ); + + if (firstEnabledPreset) { + setAiConfigurationPresetId(firstEnabledPreset.id); + } + } + }, + [ + chosenOrDefaultAiConfigurationPresetId, + orchestratorPresets, + setAiConfigurationPresetId, + ] + ); + + if (orchestratorPresets.length === 0) return null; + + const currentPreset = orchestratorPresets.find( + preset => preset.id === chosenOrDefaultAiConfigurationPresetId + ); + const NetworkIcon = + networkIcons[ + currentPreset != null ? currentPreset.reasoningLevel ?? 0 : 0 + ] || NetworkLow; + + return ( + + {({ i18n }) => ( + <> + + Reasoning level} + enterDelay={tooltipEnterDelay} + > + + setAnchorEl(e.currentTarget)} + disabled={disabled} + style={styles.button} + > + + + + + + + setAnchorEl(null)} + > + + Reasoning level: + + {orchestratorPresets.map(preset => ( + { + setAiConfigurationPresetId(preset.id); + setAnchorEl(null); + }} + > +
+ + {preset.reasoningLevelByLocale + ? selectMessageByLocale( + i18n, + preset.reasoningLevelByLocale + ) + : selectMessageByLocale(i18n, preset.nameByLocale)} + + {getSubscriptionBadge(preset.enabledWithPlans)} +
+
+ ))} +
+ + )} +
+ ); +}; diff --git a/newIDE/app/src/AiGeneration/AiRequestChat/index.js b/newIDE/app/src/AiGeneration/AiRequestChat/index.js index 4a0d26309050..95ee13570f36 100644 --- a/newIDE/app/src/AiGeneration/AiRequestChat/index.js +++ b/newIDE/app/src/AiGeneration/AiRequestChat/index.js @@ -19,7 +19,6 @@ import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView'; import AlertMessage from '../../UI/AlertMessage'; import classes from './AiRequestChat.module.css'; import RobotIcon from '../../ProjectCreation/RobotIcon'; -import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer'; import { type Quota, type UsagePrice, @@ -34,8 +33,6 @@ import { getFunctionCallOutputsFromEditorFunctionCallResults, getFunctionCallsToProcess, } from '../AiRequestUtils'; -import HelpQuestion from '../../UI/CustomSvgIcons/HelpQuestion'; -import Hammer from '../../UI/CustomSvgIcons/Hammer'; import { ChatMessages } from './ChatMessages'; import Send from '../../UI/CustomSvgIcons/Send'; import classNames from 'classnames'; @@ -43,22 +40,24 @@ import { type AiConfigurationPresetWithAvailability, getDefaultAiConfigurationPresetId, } from '../AiConfiguration'; -import { AiConfigurationPresetSelector } from './AiConfigurationPresetSelector'; +import { ReasoningLevelSelector } from './ReasoningLevelSelector'; import { AiRequestContext } from '../AiRequestContext'; import PreferencesContext from '../../MainFrame/Preferences/PreferencesContext'; import { useStickyVisibility } from './UseStickyVisibility'; +import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext'; import CircledInfo from '../../UI/CustomSvgIcons/CircledInfo'; import Coin from '../../Credits/Icons/Coin'; +import LinearProgress from '../../UI/LinearProgress'; import FlatButton from '../../UI/FlatButton'; import GoldCompact from '../../Profile/Subscription/Icons/GoldCompact'; import { SubscriptionContext } from '../../Profile/Subscription/SubscriptionContext'; import { CreditsPackageStoreContext } from '../../AssetStore/CreditsPackages/CreditsPackageStoreContext'; import Paper from '../../UI/Paper'; -import SelectOption from '../../UI/SelectOption'; -import CompactSelectField from '../../UI/CompactSelectField'; import useAlertDialog from '../../UI/Alert/useAlertDialog'; import { type FileMetadata } from '../../ProjectsStorage'; import Stop from '../../UI/CustomSvgIcons/Stop'; +import AutoEditButton from './AutoEditButton'; +import { textEllipsisStyle } from '../../UI/TextEllipsis'; const TOO_MANY_USER_MESSAGES_WARNING_COUNT = 15; const TOO_MANY_USER_MESSAGES_ERROR_COUNT = 20; @@ -80,6 +79,47 @@ const styles = { display: 'flex', alignItems: 'center', }, + quotaContainer: { + display: 'flex', + alignItems: 'center', + overflow: 'hidden', + gap: 4, + width: '100%', + }, + quotaPlaceholderContainer: { + display: 'flex', + alignItems: 'center', + overflow: 'hidden', + }, + quotaPlaceholderTextContainer: { + flex: 1, + minWidth: 0, + textAlign: 'right', + ...textEllipsisStyle, + }, + quotaInfoIconSpan: { + flexShrink: 0, + display: 'inline-flex', + alignItems: 'center', + }, + quotaInfoIcon: { + fontSize: 18, + }, + quotaProgressBarWrapper: { + width: 30, + }, + quotaProgressBar: { + height: 4, + borderRadius: 2, + }, + quotaCoinSpan: { + verticalAlign: 'middle', + display: 'inline-block', + marginRight: 4, + }, + quotaPlaceholder: { + height: 29, + }, }; const getRowsAndHeight = ({ @@ -97,93 +137,89 @@ const getPriceAndRequestsTextAndTooltip = ({ quota, price, availableCredits, - selectedMode, automaticallyUseCreditsForAiRequests, isRefreshingLimits, + progressBarColor, + progressTrackColor, }: {| quota: Quota | null, price: UsagePrice | null, availableCredits: number, - selectedMode: 'chat' | 'agent' | 'orchestrator', automaticallyUseCreditsForAiRequests: boolean, isRefreshingLimits?: boolean, + progressBarColor: string, + progressTrackColor: string, |}): React.Node => { if (!quota || !price) { if (isRefreshingLimits) { // Placeholder to avoid layout shift, while showing the (i) icon. return ( - - Calculating... - - +
+
+ + Calculating... + +
+ + - +
); } // Placeholder to avoid layout shift. - return
; + return
; } const aiCreditsAvailable = Math.max(0, quota.max - quota.current); - - const currentQuotaText = ( - {aiCreditsAvailable} AI credits available - ); - const creditsText = ( - {Math.max(0, availableCredits)} credits available - ); + const percentage = + quota.max > 0 ? Math.round((aiCreditsAvailable / quota.max) * 100) : 0; const timeForReset = quota.resetsAt ? new Date(quota.resetsAt) : null; const now = new Date(); - let summarySentence = - quota.period === '7days' ? ( - Your credits reset every week. - ) : quota.period === '30days' ? ( - Your credits reset every month. - ) : ( - Your credits reset every day. - ); - if (timeForReset) { - const timeDiff = timeForReset.getTime() - now.getTime(); - // Date to look like 'Nov 30th' - const dateString = timeForReset.toLocaleDateString(undefined, { + + let dateString = ''; + let timeString = ''; + if (timeForReset && timeForReset.getTime() - now.getTime() > 0) { + dateString = timeForReset.toLocaleDateString(undefined, { month: 'short', day: 'numeric', }); - // Time to look like '14:05' - const timeString = timeForReset.toLocaleTimeString(undefined, { + timeString = timeForReset.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false, }); - if (timeDiff <= 0) { - summarySentence = Your credits will reset soon.; - } else { - summarySentence = ( - - You need to wait until {dateString} at {timeString} to reset to - {quota.max} AI credits. - - ); - } } + const hasTimeForReset = !!dateString; + + const tooltipSentence = hasTimeForReset ? ( + quota.period === '7days' ? ( + + You still have {percentage}% left on this week's AI usage. It resets on{' '} + {dateString} at {timeString}. + + ) : quota.period === '30days' ? ( + + You still have {percentage}% left on this month's AI usage. It resets on{' '} + {dateString} at {timeString}. + + ) : ( + + You still have {percentage}% left on today's AI usage. It resets on{' '} + {dateString} at {timeString}. + + ) + ) : quota.period === '7days' ? ( + You still have {percentage}% left on this week's AI usage. + ) : quota.period === '30days' ? ( + You still have {percentage}% left on this month's AI usage. + ) : ( + You still have {percentage}% left on today's AI usage. + ); const tooltipText = ( - {summarySentence && {summarySentence}} + {tooltipSentence} - {!isRefreshingLimits && shouldShowCredits && ( - - - - )} - {isRefreshingLimits ? ( - Calculating... - ) : shouldShowCredits ? ( - creditsText - ) : ( - currentQuotaText +
+ + {isRefreshingLimits ? ( + Calculating... + ) : shouldShowCredits ? ( + <> + + + + {Math.max(0, availableCredits)} credits available + + ) : ( + {percentage}% left + )} + + {!isRefreshingLimits && !shouldShowCredits && ( +
+ +
)} - + - + - +
); }; -const getSendButtonLabelAndIcon = ({ - aiRequest, - selectedMode, - isWorking, - isMobile, - hasOpenedProject, - standAloneForm, -}: {| - aiRequest: AiRequest | null, - selectedMode?: 'chat' | 'agent' | 'orchestrator', - isWorking: boolean, - isMobile: boolean, - hasOpenedProject: boolean, - standAloneForm?: boolean, -|}): { label: React.Node, icon: React.Node } => { - if (aiRequest && !standAloneForm) { - // We're in a running chat, that is not standalone, - // hide label. - return { label: null, icon: }; - } - - return selectedMode === 'agent' || selectedMode === 'orchestrator' - ? isWorking - ? { label: Building..., icon: } - : isMobile - ? { label: Build, icon: } - : hasOpenedProject && !standAloneForm - ? { - label: Build this on my game, - icon: , - } - : { - label: Start building the game, - icon: , - } - : isWorking - ? { label: Sending..., icon: } - : { label: Send, icon: }; -}; - -const actionsOnExistingProject = [ - t`Add solid rocks that falls from the sky at a random position around the player every 0.5 seconds`, - t`Add a score and display it on the screen`, - t`Create a 3D explosion when the player is hit`, -]; +const getSendButtonIcon = (): React.Node => ; const actionsToCreateAProject = [ t`Start a simple platformer with a player that can move and jump`, @@ -295,17 +288,17 @@ const actionsToCreateAProject = [ t`Create a simple flying game with obstacles to avoid`, ]; -const generalQuestions = [ - t`How to add a leaderboard?`, - t`How to display the health of my player?`, - t`How to add an explosion when an enemy is destroyed?`, - t`How to create a main menu for my game?`, -]; - -const questionsOnExistingProject = [ +const actionsOnExistingProject = [ t`What would you add to my game?`, t`How to make my game more fun?`, t`What is a good GDevelop feature I could use in my game?`, + t`I want to add a leaderboard`, + t`I want to display the health of my player`, + t`I want to add an explosion when an enemy is destroyed`, + t`I want to create a main menu for my game`, + t`Add solid rocks that falls from the sky at a random position around the player every 0.5 seconds`, + t`Add a score and display it on the screen`, + t`Create a 3D explosion when the player is hit`, ]; type Props = {| @@ -320,10 +313,12 @@ type Props = {| mode: 'chat' | 'agent' | 'orchestrator', userRequest: string, aiConfigurationPresetId: string, + autoEdit: boolean, |}) => void, onSendUserMessage: ({| userMessage: string, mode: 'chat' | 'agent' | 'orchestrator', + autoEdit: boolean, |}) => Promise, onSendFeedback: ( aiRequestId: string, @@ -413,16 +408,19 @@ export const AiRequestChat: React.ComponentType<{ aiRequestHistory: { handleNavigateHistory, resetNavigation }, activeSubAgents, } = React.useContext(AiRequestContext); - const [selectedMode, setSelectedMode] = React.useState< - 'chat' | 'agent' | 'orchestrator' - >( - (aiRequest && aiRequest.mode) || - (hasOpenedProject ? 'chat' : 'orchestrator') + const selectedMode = 'orchestrator'; + const [isAutoEditEnabled, setIsAutoEditEnabled] = React.useState( + !hasOpenedProject || !!standAloneForm ); const { values: { automaticallyUseCreditsForAiRequests }, setAutomaticallyUseCreditsForAiRequests, } = React.useContext(PreferencesContext); + const gdevelopTheme = React.useContext(GDevelopThemeContext); + const progressBarColor = + gdevelopTheme.palette.type === 'light' ? '#7046EC' : '#9979F1'; + const progressTrackColor = + gdevelopTheme.palette.type === 'light' ? '#D9D9DE' : '#32323B'; const { openSubscriptionDialog } = React.useContext(SubscriptionContext); const { openCreditsPackageDialog } = React.useContext( CreditsPackageStoreContext @@ -466,11 +464,7 @@ export const AiRequestChat: React.ComponentType<{ ); setAiConfigurationPresetId(null); }, - [ - selectedMode, - aiConfigurationPresetsWithAvailability, - aiConfigurationPresetId, - ] + [aiConfigurationPresetsWithAvailability, aiConfigurationPresetId] ); const aiRequestId: string = aiRequest ? aiRequest.id : ''; @@ -517,19 +511,15 @@ export const AiRequestChat: React.ComponentType<{ const newChatPlaceholder = React.useMemo( () => { const newChatPlaceholders: Array = - selectedMode === 'agent' || selectedMode === 'orchestrator' - ? hasOpenedProject && !standAloneForm - ? actionsOnExistingProject - : actionsToCreateAProject - : hasOpenedProject && !standAloneForm - ? [...questionsOnExistingProject, ...generalQuestions] - : generalQuestions; + !hasOpenedProject || standAloneForm + ? actionsToCreateAProject + : actionsOnExistingProject; return newChatPlaceholders[ Math.floor(Math.random() * newChatPlaceholders.length) ]; }, - [selectedMode, hasOpenedProject, standAloneForm] + [hasOpenedProject, standAloneForm] ); const onUserRequestTextChange = React.useCallback( @@ -556,6 +546,17 @@ export const AiRequestChat: React.ComponentType<{ [resetNavigation, aiRequestId] ); + // Mirror the autoEdit flag of the opened request. + React.useEffect( + () => { + if (aiRequest) { + setIsAutoEditEnabled(!!aiRequest.autoEdit); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [aiRequestId] + ); + React.useImperativeHandle(ref, () => ({ resetUserInput: (aiRequestId: string | null) => { const aiRequestIdToReset: string = aiRequestId || ''; @@ -565,8 +566,6 @@ export const AiRequestChat: React.ComponentType<{ }, })); - const { isMobile } = useResponsiveWindowSize(); - const errorText = lastSendError ? ( @@ -585,9 +584,10 @@ export const AiRequestChat: React.ComponentType<{ quota, price, availableCredits, - selectedMode, automaticallyUseCreditsForAiRequests, isRefreshingLimits: isRefreshingLimitsStable, + progressBarColor, + progressTrackColor, }); const chosenOrDefaultAiConfigurationPresetId = @@ -657,17 +657,7 @@ export const AiRequestChat: React.ComponentType<{ // If a request is ongoing, the ChatMessages.js will show the prompt instead. !aiRequest; - const { - label: sendButtonLabel, - icon: sendButtonIcon, - } = getSendButtonLabelAndIcon({ - aiRequest, - selectedMode, - isWorking, - isMobile, - hasOpenedProject, - standAloneForm, - }); + const sendButtonIcon = getSendButtonIcon(); const onSubmitForNewChat = React.useCallback( async () => { @@ -692,6 +682,7 @@ export const AiRequestChat: React.ComponentType<{ userRequest: userRequestTextPerAiRequestId[''], aiConfigurationPresetId: chosenOrDefaultAiConfigurationPresetId, mode: selectedMode, + autoEdit: isAutoEditEnabled, }); }, [ @@ -702,7 +693,7 @@ export const AiRequestChat: React.ComponentType<{ cannotContinue, hasOpenedProject, showConfirmation, - selectedMode, + isAutoEditEnabled, standAloneForm, ] ); @@ -717,6 +708,7 @@ export const AiRequestChat: React.ComponentType<{ return onSendUserMessage({ userMessage: userRequestTextPerAiRequestId[aiRequestId] || '', mode: selectedMode, + autoEdit: isAutoEditEnabled, }); }, [ @@ -725,7 +717,7 @@ export const AiRequestChat: React.ComponentType<{ userRequestTextPerAiRequestId, scrollToBottom, cannotContinue, - selectedMode, + isAutoEditEnabled, ] ); @@ -822,25 +814,11 @@ export const AiRequestChat: React.ComponentType<{ - - - {!standAloneForm && ( - { - if ( - value !== 'chat' && - value !== 'agent' && - value !== 'orchestrator' - ) { - return; - } - setSelectedMode(value); - }} - renderOptionIcon={className => - selectedMode === 'chat' ? ( - - ) : ( - - ) - } - rounded - > - - - + {hasOpenedProject && ( + setIsAutoEditEnabled(v => !v)} + disabled={isWorking} /> - - )} + )} + + + )} + + {errorText || priceAndRequestsText} - {errorText || priceAndRequestsText}
@@ -1163,7 +1130,7 @@ export const AiRequestChat: React.ComponentType<{ sendButtonIcon ) } - label={canRequestBeStopped ? null : sendButtonLabel} + label={null} onClick={onClickExistingChatButton} /> @@ -1177,43 +1144,26 @@ export const AiRequestChat: React.ComponentType<{ alignItems="center" justifyContent="space-between" > - - { - if ( - value !== 'chat' && - value !== 'agent' && - value !== 'orchestrator' - ) { - return; - } - setSelectedMode(value); - }} - renderOptionIcon={className => - selectedMode === 'chat' ? ( - - ) : ( - - ) - } - rounded - > - - - + {hasOpenedProject && ( + setIsAutoEditEnabled(v => !v)} + disabled={isWorking} /> - - - + )} + + + {isForAnotherProjectText || errorText || priceAndRequestsText} diff --git a/newIDE/app/src/AiGeneration/AskAiEditorContainer.js b/newIDE/app/src/AiGeneration/AskAiEditorContainer.js index 2a1a68911841..2809e4c3affe 100644 --- a/newIDE/app/src/AiGeneration/AskAiEditorContainer.js +++ b/newIDE/app/src/AiGeneration/AskAiEditorContainer.js @@ -477,6 +477,7 @@ export const AskAiEditor: React.ComponentType = React.memo( mode, userRequest, aiConfigurationPresetId, + autoEdit, } = newAiRequestOptions; startNewAiRequest(null); @@ -539,6 +540,7 @@ export const AskAiEditor: React.ComponentType = React.memo( fileMetadata, storageProviderName, mode, + autoEdit, toolsVersion: getToolsVersionForAiRequestMode(mode), aiConfiguration: { presetId: aiConfigurationPresetId, @@ -619,6 +621,7 @@ export const AskAiEditor: React.ComponentType = React.memo( createdProject, editorFunctionCallResults, newMode, + autoEdit, }: {| aiRequestId: string, userMessage: string, @@ -626,6 +629,7 @@ export const AskAiEditor: React.ComponentType = React.memo( createdProject?: ?gdProject, editorFunctionCallResults: Array, newMode?: 'chat' | 'agent' | 'orchestrator', + autoEdit?: boolean, |}) => { if (!profile) return; @@ -766,6 +770,7 @@ export const AskAiEditor: React.ComponentType = React.memo( userMessage, paused: hasJustInitializedProject && modeForThisMessage === 'agent', + autoEdit, // These are defined only if there is a mode change: mode: newMode, toolsVersion: newMode @@ -1411,15 +1416,18 @@ export const AskAiEditor: React.ComponentType = React.memo( onSendUserMessage={async ({ userMessage, mode, + autoEdit, }: {| userMessage: string, mode: 'chat' | 'agent' | 'orchestrator', + autoEdit: boolean, |}) => { if (!selectedAiRequestId) return; await onSendMessage({ aiRequestId: selectedAiRequestId, userMessage, newMode: mode, + autoEdit, editorFunctionCallResults: selectedAiRequest ? getEditorFunctionCallResults(selectedAiRequest.id) || [] : [], diff --git a/newIDE/app/src/AiGeneration/AskAiStandAloneForm.js b/newIDE/app/src/AiGeneration/AskAiStandAloneForm.js index 4de221c4405f..700a995d01e0 100644 --- a/newIDE/app/src/AiGeneration/AskAiStandAloneForm.js +++ b/newIDE/app/src/AiGeneration/AskAiStandAloneForm.js @@ -42,6 +42,7 @@ import { useProcessFunctionCalls, useRefreshLimits, type NewAiRequestOptions, + type OpenAskAiOptions, AI_ORCHESTRATOR_TOOLS_VERSION, } from './Utils'; import { ColumnStackLayout, LineStackLayout } from '../UI/Layout'; @@ -81,6 +82,8 @@ type Props = {| onWillInstallExtension: (extensionNames: Array) => void, onExtensionInstalled: (extensionNames: Array) => void, onCloseAskAi: () => void, + onOpenAskAi?: (?OpenAskAiOptions) => void, + closeProject?: () => Promise, dismissableIdentifier?: string, |}; @@ -94,6 +97,8 @@ export const AskAiStandAloneForm = ({ onCreateEmptyProject, onOpenLayout, onCloseAskAi, + onOpenAskAi, + closeProject, dismissableIdentifier, onWillInstallExtension, onExtensionInstalled, @@ -243,13 +248,20 @@ export const AskAiStandAloneForm = ({ return; } + // Read the options and reset them immediately to prevent the effect from firing + // again if dependencies change during the async operations below (e.g. when + // closeProject causes project to become null). + const { userRequest, aiConfigurationPresetId } = newAiRequestOptions; + startNewAiRequest(null); + // Ensure the Ask AI pane is closed, to avoid multiple requests being sent // at the same time from the editor and the standalone form. onCloseAskAi(); - // Read the options and reset them (to avoid launching the same request twice). - const { userRequest, aiConfigurationPresetId } = newAiRequestOptions; - startNewAiRequest(null); + // Close any open project since the AI will create a new one. + if (project && closeProject) { + await closeProject(); + } // Ensure the user has enough credits to pay for the request, or ask them // to buy some more. @@ -299,6 +311,7 @@ export const AskAiStandAloneForm = ({ fileMetadata: null, // No file metadata when starting from the standalone form. storageProviderName, mode: aiRequestModeForForm, + autoEdit: true, toolsVersion: AI_ORCHESTRATOR_TOOLS_VERSION, aiConfiguration: { presetId: aiConfigurationPresetId, @@ -313,12 +326,26 @@ export const AskAiStandAloneForm = ({ // Select the new AI request just created - unless the user switched to another one // in the meantime. if (!upToDateSelectedAiRequestId.current) { - setAiRequestIdForForm(aiRequest.id); - // Also set the global selected AI request state, - // so that the editor is in sync, when we'll open it. + // Set the global selected AI request state so the editor tab + // can find the right request when it opens. setSelectedAiRequestId(aiRequest.id); } + // Open the Ask AI tab right away. Always use 'center' pane since + // the project has been closed (or was never open) at this point. + if (onOpenAskAi) { + onOpenAskAi({ + continueProcessingFunctionCallsOnMount: true, + paneIdentifier: 'center', + }); + } + + // Reset the form so it's ready for a new request (the tab now owns the request). + setAiRequestIdForForm(null); + if (aiRequestChatRef.current) { + aiRequestChatRef.current.resetUserInput(''); + } + sendAiRequestStarted({ simplifiedProjectJsonLength: 0, projectSpecificExtensionsSummaryJsonLength: 0, @@ -363,6 +390,8 @@ export const AskAiStandAloneForm = ({ openSubscriptionDialog, onCloseAskAi, automaticallyUseCreditsForAiRequests, + onOpenAskAi, + closeProject, ] ); @@ -377,12 +406,14 @@ export const AskAiStandAloneForm = ({ createdSceneNames, createdProject, editorFunctionCallResults, + autoEdit, }: {| aiRequestId: string, userMessage: string, createdSceneNames?: Array, createdProject?: ?gdProject, editorFunctionCallResults: Array, + autoEdit?: boolean, |}) => { if (!profile) return; @@ -464,6 +495,7 @@ export const AskAiStandAloneForm = ({ // If we switch back to agent mode for the standalone form in the future, // check if it has just initialized the project to mark it as paused. paused: false, + autoEdit, mode: aiRequestModeForForm, toolsVersion: AI_ORCHESTRATOR_TOOLS_VERSION, }) @@ -610,14 +642,17 @@ export const AskAiStandAloneForm = ({ onSendUserMessage={async ({ userMessage, mode, + autoEdit, }: {| userMessage: string, mode: 'chat' | 'agent' | 'orchestrator', + autoEdit: boolean, |}) => { if (!aiRequestIdForForm) return; await onSendMessage({ aiRequestId: aiRequestIdForForm, userMessage, + autoEdit, // mode, Mode is forced to agent in standalone form, no need to pass it here. editorFunctionCallResults: aiRequestForForm ? getEditorFunctionCallResults(aiRequestForForm.id) || [] diff --git a/newIDE/app/src/AiGeneration/Utils.js b/newIDE/app/src/AiGeneration/Utils.js index 281f3dd2ffab..105a0d7523eb 100644 --- a/newIDE/app/src/AiGeneration/Utils.js +++ b/newIDE/app/src/AiGeneration/Utils.js @@ -1048,4 +1048,5 @@ export type NewAiRequestOptions = {| mode: 'chat' | 'agent' | 'orchestrator', userRequest: string, aiConfigurationPresetId: string, + autoEdit: boolean, |}; diff --git a/newIDE/app/src/EditorFunctions/EditorFunctionCallRunner.js b/newIDE/app/src/EditorFunctions/EditorFunctionCallRunner.js index edd4a6864ace..a4330194831d 100644 --- a/newIDE/app/src/EditorFunctions/EditorFunctionCallRunner.js +++ b/newIDE/app/src/EditorFunctions/EditorFunctionCallRunner.js @@ -104,6 +104,18 @@ export const processEditorFunctionCalls = async ({ }); continue; } + if (project && name === 'initialize_project') { + results.push({ + status: 'finished', + call_id, + success: false, + output: { + message: + 'A project is already open — initialize_project cannot be called. If starting from a new project is the right approach, suggest the user close the current project and start a new AI request.', + }, + }); + continue; + } let args; try { try { diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/index.js index 5f732c010c94..c2b4ab6f0bb7 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/CreateSection/index.js @@ -55,6 +55,7 @@ import { deleteCloudProject } from '../../../../Utils/GDevelopServices/Project'; import { getDefaultRegisterGameProperties } from '../../../../Utils/UseGameAndBuildsManager'; import { type CreateProjectResult } from '../../../../Utils/UseCreateProject'; import { AskAiStandAloneForm } from '../../../../AiGeneration/AskAiStandAloneForm'; +import { type OpenAskAiOptions } from '../../../../AiGeneration/Utils'; import { AiRequestContext } from '../../../../AiGeneration/AiRequestContext'; const getExampleItemsColumns = ( @@ -100,6 +101,7 @@ type Props = {| onWillInstallExtension: (extensionNames: Array) => void, onExtensionInstalled: (extensionNames: Array) => void, onCloseAskAi: () => void, + onOpenAskAi: (?OpenAskAiOptions) => void, closeProject: () => Promise, canOpen: boolean, onOpenProfile: () => void, @@ -138,6 +140,7 @@ const CreateSection = ({ onWillInstallExtension, onExtensionInstalled, onCloseAskAi, + onOpenAskAi, closeProject, canOpen, onOpenProfile, @@ -509,6 +512,8 @@ const CreateSection = ({ onWillInstallExtension={onWillInstallExtension} onExtensionInstalled={onExtensionInstalled} onCloseAskAi={onCloseAskAi} + onOpenAskAi={onOpenAskAi} + closeProject={closeProject} dismissableIdentifier="home-page-create-section" /> diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js index 61daf41b026c..14dd5d1e38e0 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js @@ -616,6 +616,7 @@ export const HomePage: React.ComponentType = React.memo( onWillInstallExtension={onWillInstallExtension} onExtensionInstalled={onExtensionInstalled} onCloseAskAi={onCloseAskAi} + onOpenAskAi={onOpenAskAi} closeProject={closeProject} games={games} onRefreshGames={fetchGames} diff --git a/newIDE/app/src/MainFrame/ElectronMainMenu.js b/newIDE/app/src/MainFrame/ElectronMainMenu.js index 6b9ea4bf143c..be020eec6f5a 100644 --- a/newIDE/app/src/MainFrame/ElectronMainMenu.js +++ b/newIDE/app/src/MainFrame/ElectronMainMenu.js @@ -231,7 +231,17 @@ const ElectronMainMenu = ({ }); useIPCEventListener({ ipcEvent: 'main-menu-select-all', - callback: callbacks.onSelectAll, + callback: () => { + const active = document.activeElement; + if ( + active instanceof HTMLInputElement || + active instanceof HTMLTextAreaElement + ) { + active.select(); + } else { + callbacks.onSelectAll(); + } + }, shouldApply: isFocusedOnMainWindow, }); useIPCEventListener({ diff --git a/newIDE/app/src/MainFrame/UseNewProjectDialog.js b/newIDE/app/src/MainFrame/UseNewProjectDialog.js index e87c1bbe22ca..8c9273b1fd3b 100644 --- a/newIDE/app/src/MainFrame/UseNewProjectDialog.js +++ b/newIDE/app/src/MainFrame/UseNewProjectDialog.js @@ -17,6 +17,7 @@ import { type FileMetadata, type StorageProvider } from '../ProjectsStorage'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import RouterContext from './RouterContext'; import { type CreateProjectResult } from '../Utils/UseCreateProject'; +import { type OpenAskAiOptions } from '../AiGeneration/Utils'; type Props = {| project: ?gdProject, @@ -34,6 +35,8 @@ type Props = {| newProjectSetup: NewProjectSetup ) => Promise, closeAskAi: () => void, + openAskAi: (?OpenAskAiOptions) => void, + closeProject: () => Promise, storageProviders: Array, storageProvider: ?StorageProvider, onOpenLayout: ( @@ -81,6 +84,8 @@ const useNewProjectDialog = ({ createProjectFromExample, createProjectFromPrivateGameTemplate, closeAskAi, + openAskAi, + closeProject, storageProviders, storageProvider, onOpenLayout, @@ -236,6 +241,8 @@ const useNewProjectDialog = ({ createProjectFromPrivateGameTemplate } onCloseAskAi={closeAskAi} + onOpenAskAi={openAskAi} + closeProject={closeProject} storageProviders={storageProviders} storageProvider={storageProvider} selectedExampleShortHeader={selectedExampleShortHeader} diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 90f638395126..9f91dccc3d23 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -4268,8 +4268,6 @@ const MainFrame = (props: Props): React.MixedElement => { skipNewVersionWarning: !!checkedOutVersionStatus || (options && options.skipNewVersionWarning), - canonicalEventSerialization: - preferences.values.canonicalEventSerialization, }; if (cloudProjectRecoveryOpenedVersionId) { saveOptions.previousVersion = cloudProjectRecoveryOpenedVersionId; @@ -5033,6 +5031,8 @@ const MainFrame = (props: Props): React.MixedElement => { createProjectFromExample, createProjectFromPrivateGameTemplate, closeAskAi, + openAskAi, + closeProject, storageProviders: props.storageProviders, storageProvider: getStorageProvider(), resourceManagementProps, diff --git a/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js b/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js index 7de1691726e9..9ddd0d1192bb 100644 --- a/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js +++ b/newIDE/app/src/ProjectCreation/NewProjectSetupDialog.js @@ -62,6 +62,7 @@ import { BundleStoreContext } from '../AssetStore/Bundles/BundleStoreContext'; import { type CreateProjectResult } from '../Utils/UseCreateProject'; import { isNativeMobileApp } from '../Utils/Platform'; import { AskAiStandAloneForm } from '../AiGeneration/AskAiStandAloneForm'; +import { type OpenAskAiOptions } from '../AiGeneration/Utils'; import { AiRequestContext } from '../AiGeneration/AiRequestContext'; const electron = optionalRequire('electron'); @@ -125,6 +126,8 @@ type Props = {| i18n: I18nType ) => Promise, onCloseAskAi: () => void, + onOpenAskAi?: (?OpenAskAiOptions) => void, + closeProject?: () => Promise, selectedExampleShortHeader: ?ExampleShortHeader, onSelectExampleShortHeader: (exampleShortHeader: ?ExampleShortHeader) => void, selectedPrivateGameTemplateListingData: ?PrivateGameTemplateListingData, @@ -161,6 +164,8 @@ const NewProjectSetupDialog = ({ onCreateFromExample, onCreateProjectFromPrivateGameTemplate, onCloseAskAi, + onOpenAskAi, + closeProject, selectedExampleShortHeader, onSelectExampleShortHeader, selectedPrivateGameTemplateListingData, @@ -677,6 +682,8 @@ const NewProjectSetupDialog = ({ onWillInstallExtension={onWillInstallExtension} onExtensionInstalled={onExtensionInstalled} onCloseAskAi={onCloseAskAi} + onOpenAskAi={onOpenAskAi} + closeProject={closeProject} /> { diff --git a/newIDE/app/src/UI/CompactTextAreaFieldWithControls/index.js b/newIDE/app/src/UI/CompactTextAreaFieldWithControls/index.js index e2c7d8eebb59..e81832f974a7 100644 --- a/newIDE/app/src/UI/CompactTextAreaFieldWithControls/index.js +++ b/newIDE/app/src/UI/CompactTextAreaFieldWithControls/index.js @@ -117,6 +117,13 @@ export const CompactTextAreaFieldWithControls: React.ComponentType<{ return; } + // Stop propagation for all other Cmd/Ctrl combos (Cmd+A, Cmd+C, Cmd+Z, etc.) + // so parent keyboard shortcut handlers don't intercept standard text editing. + if (e.metaKey || e.ctrlKey) { + e.stopPropagation(); + return; + } + if (!onNavigateHistory) { return; } diff --git a/newIDE/app/src/UI/CustomSvgIcons/NetworkHigh.js b/newIDE/app/src/UI/CustomSvgIcons/NetworkHigh.js new file mode 100644 index 000000000000..18afad503a91 --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/NetworkHigh.js @@ -0,0 +1,30 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + + + + +)); diff --git a/newIDE/app/src/UI/CustomSvgIcons/NetworkLow.js b/newIDE/app/src/UI/CustomSvgIcons/NetworkLow.js new file mode 100644 index 000000000000..51e4aaeb490e --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/NetworkLow.js @@ -0,0 +1,30 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + + + + +)); diff --git a/newIDE/app/src/UI/CustomSvgIcons/NetworkMaximum.js b/newIDE/app/src/UI/CustomSvgIcons/NetworkMaximum.js new file mode 100644 index 000000000000..68a42fe4b949 --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/NetworkMaximum.js @@ -0,0 +1,30 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + + + + +)); diff --git a/newIDE/app/src/UI/CustomSvgIcons/NetworkMedium.js b/newIDE/app/src/UI/CustomSvgIcons/NetworkMedium.js new file mode 100644 index 000000000000..9461e9a650cd --- /dev/null +++ b/newIDE/app/src/UI/CustomSvgIcons/NetworkMedium.js @@ -0,0 +1,30 @@ +import React from 'react'; +import SvgIcon from '@material-ui/core/SvgIcon'; + +export default React.memo(props => ( + + + + + + +)); diff --git a/newIDE/app/src/UI/LinearProgress.js b/newIDE/app/src/UI/LinearProgress.js index fc46da0a9a69..0de1a297cfa0 100644 --- a/newIDE/app/src/UI/LinearProgress.js +++ b/newIDE/app/src/UI/LinearProgress.js @@ -6,29 +6,42 @@ import GDevelopThemeContext from './Theme/GDevelopThemeContext'; import type { GDevelopTheme } from './Theme'; import { makeStyles } from '@material-ui/core/styles'; -const useStyles = (color?: 'success', gdevelopTheme: GDevelopTheme) => +const useStyles = ( + color?: 'success', + barColor?: string, + trackColor?: string, + gdevelopTheme: GDevelopTheme +) => makeStyles({ colorSecondary: { - backgroundColor: gdevelopTheme.paper.backgroundColor.light, + backgroundColor: trackColor || gdevelopTheme.paper.backgroundColor.light, }, barColorSecondary: { backgroundColor: - color === 'success' + barColor || + (color === 'success' ? gdevelopTheme.statusIndicator.success - : gdevelopTheme.palette.secondary, + : gdevelopTheme.palette.secondary), }, })(); type Props = {| variant?: 'indeterminate' | 'determinate', color?: 'success', + barColor?: string, + trackColor?: string, value?: ?number, style?: {| height?: number, borderRadius?: number, width?: number |}, |}; function LinearProgress(props: Props): React.Node { const gdevelopTheme = React.useContext(GDevelopThemeContext); - const classes = useStyles(props.color, gdevelopTheme); + const classes = useStyles( + props.color, + props.barColor, + props.trackColor, + gdevelopTheme + ); return ( => { @@ -500,6 +506,7 @@ export const addMessageToAiRequest = async ( projectSpecificExtensionsSummaryJsonUserRelativeKey, paused, mode, + autoEdit, toolsVersion, }, { @@ -937,6 +944,8 @@ export type AiConfigurationPreset = {| mode: 'chat' | 'agent' | 'orchestrator', id: string, nameByLocale: MessageByLocale, + reasoningLevelByLocale?: MessageByLocale, + reasoningLevel?: number, disabled: boolean, isDefault?: boolean, |}; diff --git a/newIDE/app/src/Utils/GDevelopServices/Usage.js b/newIDE/app/src/Utils/GDevelopServices/Usage.js index 594c3c8fdfa6..df554224e7ed 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Usage.js +++ b/newIDE/app/src/Utils/GDevelopServices/Usage.js @@ -68,6 +68,7 @@ type AiCapability = { id: string, disabled?: boolean, enableWith?: 'higher-tier-plan', + enabledWithPlans?: Array, }>, versionHistory?: { retentionDays: number }, }; diff --git a/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Agent.stories.js b/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Agent.stories.js index ee269c467ef8..e75bf4937c64 100644 --- a/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Agent.stories.js +++ b/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Agent.stories.js @@ -40,6 +40,7 @@ export const commonProps = { mode: 'chat', disabled: false, enableWith: null, + enabledWithPlans: ([]: Array), }, { id: 'expert-mode', @@ -47,6 +48,7 @@ export const commonProps = { mode: 'chat', disabled: false, enableWith: null, + enabledWithPlans: ([]: Array), }, { id: 'default', @@ -54,6 +56,7 @@ export const commonProps = { mode: 'agent', disabled: false, enableWith: null, + enabledWithPlans: ([]: Array), }, { id: 'extended-thinking', @@ -61,6 +64,7 @@ export const commonProps = { mode: 'agent', disabled: false, enableWith: null, + enabledWithPlans: ([]: Array), }, { id: 'max-mode', @@ -68,6 +72,11 @@ export const commonProps = { mode: 'agent', disabled: true, enableWith: 'higher-tier-plan', + enabledWithPlans: [ + 'gdevelop_gold', + 'gdevelop_startup', + 'gdevelop_education', + ], }, ], editorCallbacks: { diff --git a/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Chat.stories.js b/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Chat.stories.js index 7445be9eff45..d6ac16995fd3 100644 --- a/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Chat.stories.js +++ b/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Chat.stories.js @@ -36,6 +36,7 @@ const commonProps = { mode: 'chat', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'expert-mode', @@ -43,6 +44,7 @@ const commonProps = { mode: 'chat', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'default', @@ -50,6 +52,7 @@ const commonProps = { mode: 'agent', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'extended-thinking', @@ -57,6 +60,7 @@ const commonProps = { mode: 'agent', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'max-mode', @@ -64,6 +68,11 @@ const commonProps = { mode: 'agent', disabled: true, enableWith: 'higher-tier-plan', + enabledWithPlans: [ + 'gdevelop_gold', + 'gdevelop_startup', + 'gdevelop_education', + ], }, ], editorCallbacks: { diff --git a/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Form.stories.js b/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Form.stories.js index 0e18dfcfee07..6f79efd7b5f4 100644 --- a/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Form.stories.js +++ b/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/Form.stories.js @@ -35,6 +35,7 @@ const commonProps = { mode: 'chat', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'expert-mode', @@ -42,6 +43,7 @@ const commonProps = { mode: 'chat', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'default', @@ -49,6 +51,7 @@ const commonProps = { mode: 'agent', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'extended-thinking', @@ -56,6 +59,7 @@ const commonProps = { mode: 'agent', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'max-mode', @@ -63,6 +67,11 @@ const commonProps = { mode: 'agent', disabled: true, enableWith: 'higher-tier-plan', + enabledWithPlans: [ + 'gdevelop_gold', + 'gdevelop_startup', + 'gdevelop_education', + ], }, ], editorCallbacks: { diff --git a/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/StandAloneForm.stories.js b/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/StandAloneForm.stories.js index 037ab088a387..4dca519af647 100644 --- a/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/StandAloneForm.stories.js +++ b/newIDE/app/src/stories/componentStories/AiGeneration/AiRequestChat/StandAloneForm.stories.js @@ -35,6 +35,7 @@ const commonProps = { mode: 'chat', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'expert-mode', @@ -42,6 +43,7 @@ const commonProps = { mode: 'chat', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'default', @@ -49,6 +51,7 @@ const commonProps = { mode: 'agent', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'extended-thinking', @@ -56,6 +59,7 @@ const commonProps = { mode: 'agent', disabled: false, enableWith: null, + enabledWithPlans: [], }, { id: 'max-mode', @@ -63,6 +67,11 @@ const commonProps = { mode: 'agent', disabled: true, enableWith: 'higher-tier-plan', + enabledWithPlans: [ + 'gdevelop_gold', + 'gdevelop_startup', + 'gdevelop_education', + ], }, ], editorCallbacks: { diff --git a/newIDE/app/src/stories/componentStories/HomePage/CreateSection/CreateSection.stories.js b/newIDE/app/src/stories/componentStories/HomePage/CreateSection/CreateSection.stories.js index 7b44531de23c..17f6585ffc4b 100644 --- a/newIDE/app/src/stories/componentStories/HomePage/CreateSection/CreateSection.stories.js +++ b/newIDE/app/src/stories/componentStories/HomePage/CreateSection/CreateSection.stories.js @@ -96,6 +96,7 @@ const WrappedCreateSection = ({ )} onExtensionInstalled={action('onExtensionInstalled')} onCloseAskAi={() => action('onCloseAskAi')()} + onOpenAskAi={() => action('onOpenAskAi')()} closeProject={async () => {}} canOpen={true} onOpenProfile={() => action('open profile')()}