[AI] Match ai chat composer to figma (#18874)
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=93653-368288&t=obTG32NRidXid4lN-0 closes https://discord.com/channels/1130383047699738754/1480990726442582086 --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@@ -1,67 +1,28 @@
|
||||
import { AIChatBanner } from '@/ai/components/AIChatBanner';
|
||||
import { useEndSubscriptionTrialPeriod } from '@/settings/billing/hooks/useEndSubscriptionTrialPeriod';
|
||||
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
|
||||
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
|
||||
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { IconSparkles } from 'twenty-ui/display';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
PermissionFlagType,
|
||||
SubscriptionStatus,
|
||||
BillingPortalSessionDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
export const AIChatCreditsExhaustedMessage = () => {
|
||||
const { redirect } = useRedirect();
|
||||
const navigateSettings = useNavigateSettings();
|
||||
const subscriptionStatus = useSubscriptionStatus();
|
||||
const { endTrialPeriod, isLoading: isEndingTrial } =
|
||||
useEndSubscriptionTrialPeriod();
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const isTrialing = subscriptionStatus === SubscriptionStatus.Trialing;
|
||||
|
||||
const { [PermissionFlagType.WORKSPACE]: hasPermissionToManageBilling } =
|
||||
usePermissionFlagMap();
|
||||
|
||||
const { data: billingPortalData, loading: isBillingPortalLoading } = useQuery(
|
||||
BillingPortalSessionDocument,
|
||||
{
|
||||
variables: {
|
||||
returnUrlPath: getSettingsPath(SettingsPath.Billing),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const openBillingPortal = () => {
|
||||
if (
|
||||
isDefined(billingPortalData) &&
|
||||
isDefined(billingPortalData.billingPortalSession.url)
|
||||
) {
|
||||
redirect(billingPortalData.billingPortalSession.url);
|
||||
}
|
||||
const handleUpgradeClick = () => {
|
||||
navigateSettings(SettingsPath.Billing);
|
||||
};
|
||||
|
||||
const handleUpgradeClick = async () => {
|
||||
if (!isTrialing) {
|
||||
openBillingPortal();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
const result = await endTrialPeriod();
|
||||
setIsProcessing(false);
|
||||
|
||||
if (!result.success) {
|
||||
openBillingPortal();
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = isEndingTrial || isBillingPortalLoading || isProcessing;
|
||||
|
||||
const message = hasPermissionToManageBilling
|
||||
? isTrialing
|
||||
? t`Free trial credits exhausted. Subscribe now to continue using AI features.`
|
||||
@@ -79,8 +40,6 @@ export const AIChatCreditsExhaustedMessage = () => {
|
||||
buttonOnClick={
|
||||
hasPermissionToManageBilling ? handleUpgradeClick : undefined
|
||||
}
|
||||
isButtonDisabled={isLoading}
|
||||
isButtonLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { EditorContent } from '@tiptap/react';
|
||||
import { LightButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { AIChatEmptyState } from '@/ai/components/AIChatEmptyState';
|
||||
@@ -12,10 +11,17 @@ import { AIChatEditorFocusEffect } from '@/ai/components/internal/AIChatEditorFo
|
||||
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
|
||||
import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
|
||||
import { useAIChatEditor } from '@/ai/hooks/useAIChatEditor';
|
||||
import { useAiModelLabel } from '@/ai/hooks/useAiModelOptions';
|
||||
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
|
||||
import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
|
||||
import { agentChatUserSelectedModelState } from '@/ai/states/agentChatUserSelectedModelState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||
import { getModelIcon } from '@/settings/admin-panel/ai/utils/getModelIcon';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
|
||||
const StyledInputArea = styled.div<{ isMobile: boolean }>`
|
||||
align-items: flex-end;
|
||||
@@ -102,24 +108,48 @@ const StyledRightButtonsContainer = styled.div`
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledReadOnlyModelButtonContainer = styled.div`
|
||||
> * {
|
||||
cursor: default;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AIChatEditorSection = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const smartModelLabel = useAiModelLabel(currentWorkspace?.smartModel, false);
|
||||
const aiModels = useAtomStateValue(aiModelsState);
|
||||
const { enabledModels } = useWorkspaceAiModelAvailability();
|
||||
const setAgentChatUserSelectedModel = useSetAtomState(
|
||||
agentChatUserSelectedModelState,
|
||||
);
|
||||
const { selectedModelId } = useAgentChatModelId();
|
||||
|
||||
const { editor, handleSendAndClear } = useAIChatEditor();
|
||||
|
||||
const workspaceSmartModel = aiModels.find(
|
||||
(model) => model.modelId === currentWorkspace?.smartModel,
|
||||
);
|
||||
|
||||
const resolvedDefaultModelId = enabledModels.find(
|
||||
(model) =>
|
||||
model.label === workspaceSmartModel?.label &&
|
||||
model.providerName === workspaceSmartModel?.providerName,
|
||||
)?.modelId;
|
||||
|
||||
const defaultPinnedOption = workspaceSmartModel
|
||||
? {
|
||||
value: null as string | null,
|
||||
label: workspaceSmartModel.label,
|
||||
Icon: getModelIcon(
|
||||
workspaceSmartModel.modelFamily,
|
||||
workspaceSmartModel.providerName,
|
||||
),
|
||||
contextualText: t`default`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const smartModelOptions = enabledModels
|
||||
.filter((model) => model.modelId !== resolvedDefaultModelId)
|
||||
.map((model) => ({
|
||||
value: model.modelId as string | null,
|
||||
label: model.label,
|
||||
Icon: getModelIcon(model.modelFamily, model.providerName),
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<AIChatEditorFocusEffect editor={editor} />
|
||||
@@ -139,9 +169,17 @@ export const AIChatEditorSection = () => {
|
||||
<AIChatContextUsageButton />
|
||||
</StyledLeftButtonsContainer>
|
||||
<StyledRightButtonsContainer>
|
||||
<StyledReadOnlyModelButtonContainer>
|
||||
<LightButton accent="tertiary" title={smartModelLabel} />
|
||||
</StyledReadOnlyModelButtonContainer>
|
||||
<Select
|
||||
dropdownId="ai-chat-smart-model-select"
|
||||
value={selectedModelId}
|
||||
onChange={setAgentChatUserSelectedModel}
|
||||
options={smartModelOptions}
|
||||
pinnedOption={defaultPinnedOption}
|
||||
selectSizeVariant="small"
|
||||
showContextualTextInControl={false}
|
||||
withSearchInput
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
/>
|
||||
<SendMessageButton onSend={handleSendAndClear} />
|
||||
</StyledRightButtonsContainer>
|
||||
</StyledButtonsContainer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AIChatStandaloneError } from '@/ai/components/AIChatStandaloneError';
|
||||
import { AIChatErrorRenderer } from '@/ai/components/AIChatErrorRenderer';
|
||||
import { AgentMessageRole } from '@/ai/constants/AgentMessageRole';
|
||||
import { agentChatErrorState } from '@/ai/states/agentChatErrorState';
|
||||
import { agentChatIsStreamingState } from '@/ai/states/agentChatIsStreamingState';
|
||||
@@ -7,6 +7,12 @@ import { agentChatMessageIdsComponentSelector } from '@/ai/states/agentChatMessa
|
||||
import { useAtomComponentFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilySelectorValue';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledErrorWrapper = styled.div`
|
||||
padding-top: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
export const AIChatErrorUnderMessageList = () => {
|
||||
const agentChatError = useAtomStateValue(agentChatErrorState);
|
||||
@@ -31,5 +37,9 @@ export const AIChatErrorUnderMessageList = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <AIChatStandaloneError />;
|
||||
return (
|
||||
<StyledErrorWrapper>
|
||||
<AIChatErrorRenderer error={agentChatError} />
|
||||
</StyledErrorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEnsureAgentChatThreadIdForSend } from '@/ai/hooks/useEnsureAgentChat
|
||||
import { agentChatErrorState } from '@/ai/states/agentChatErrorState';
|
||||
import { agentChatIsLoadingState } from '@/ai/states/agentChatIsLoadingState';
|
||||
import { agentChatIsStreamingState } from '@/ai/states/agentChatIsStreamingState';
|
||||
import { normalizeAiSdkError } from '@/ai/utils/normalizeAiSdkError';
|
||||
import { agentChatMessagesComponentFamilyState } from '@/ai/states/agentChatMessagesComponentFamilyState';
|
||||
import { agentChatMessagesLoadingState } from '@/ai/states/agentChatMessagesLoadingState';
|
||||
import { agentChatThreadsLoadingState } from '@/ai/states/agentChatThreadsLoadingState';
|
||||
@@ -119,7 +120,7 @@ export const AgentChatAiSdkStreamEffect = () => {
|
||||
const setAgentChatError = useSetAtomState(agentChatErrorState);
|
||||
|
||||
useEffect(() => {
|
||||
setAgentChatError(chatState.error);
|
||||
setAgentChatError(normalizeAiSdkError(chatState.error));
|
||||
}, [chatState.error, setAgentChatError]);
|
||||
|
||||
const setAgentChatIsStreaming = useSetAtomState(agentChatIsStreamingState);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const DEFAULT_FAST_MODEL = 'default-fast-model' as const;
|
||||
@@ -1 +0,0 @@
|
||||
export const DEFAULT_SMART_MODEL = 'default-smart-model' as const;
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
agentChatDraftsByThreadIdState,
|
||||
} from '@/ai/states/agentChatDraftsByThreadIdState';
|
||||
import { agentChatInputState } from '@/ai/states/agentChatInputState';
|
||||
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
|
||||
import { REST_API_BASE_URL } from '@/apollo/constant/rest-api-base-url';
|
||||
import { getTokenPair } from '@/apollo/utils/getTokenPair';
|
||||
import { renewToken } from '@/auth/services/AuthService';
|
||||
@@ -40,6 +41,7 @@ export const useAgentChat = (
|
||||
const setTokenPair = useSetAtomState(tokenPairState);
|
||||
const setAgentChatUsage = useSetAtomState(agentChatUsageState);
|
||||
|
||||
const { modelIdForRequest } = useAgentChatModelId();
|
||||
const { getBrowsingContext } = useGetBrowsingContext();
|
||||
const setCurrentAIChatThreadTitle = useSetAtomState(
|
||||
currentAIChatThreadTitleState,
|
||||
@@ -258,6 +260,9 @@ export const useAgentChat = (
|
||||
body: {
|
||||
threadId,
|
||||
browsingContext,
|
||||
...(isDefined(modelIdForRequest) && {
|
||||
modelId: modelIdForRequest,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -273,6 +278,7 @@ export const useAgentChat = (
|
||||
agentChatUploadedFiles,
|
||||
setAgentChatUploadedFiles,
|
||||
setAgentChatDraftsByThreadId,
|
||||
modelIdForRequest,
|
||||
]);
|
||||
|
||||
useListenToBrowserEvent({
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
|
||||
import { agentChatUserSelectedModelState } from '@/ai/states/agentChatUserSelectedModelState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useAgentChatModelId = () => {
|
||||
const { enabledModels } = useWorkspaceAiModelAvailability();
|
||||
const agentChatUserSelectedModel = useAtomStateValue(
|
||||
agentChatUserSelectedModelState,
|
||||
);
|
||||
|
||||
const isUserModelAvailable =
|
||||
!isDefined(agentChatUserSelectedModel) ||
|
||||
enabledModels.some((model) => model.modelId === agentChatUserSelectedModel);
|
||||
|
||||
const selectedModelId = isUserModelAvailable
|
||||
? agentChatUserSelectedModel
|
||||
: null;
|
||||
const modelIdForRequest = selectedModelId ?? undefined;
|
||||
|
||||
return { selectedModelId, modelIdForRequest };
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { isAutoSelectModelId } from 'twenty-shared/utils';
|
||||
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
|
||||
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -16,13 +15,11 @@ export const useAiModelOptions = (): SelectOption<string>[] => {
|
||||
)
|
||||
.map((model) => ({
|
||||
value: model.modelId,
|
||||
label:
|
||||
model.modelId === DEFAULT_FAST_MODEL ||
|
||||
model.modelId === DEFAULT_SMART_MODEL
|
||||
? model.label
|
||||
: model.modelFamilyLabel
|
||||
? `${model.label} (${model.modelFamilyLabel})`
|
||||
: model.label,
|
||||
label: isAutoSelectModelId(model.modelId)
|
||||
? model.label
|
||||
: model.modelFamilyLabel
|
||||
? `${model.label} (${model.modelFamilyLabel})`
|
||||
: model.label,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
};
|
||||
@@ -43,11 +40,7 @@ export const useAiModelLabel = (
|
||||
return modelId;
|
||||
}
|
||||
|
||||
if (
|
||||
model.modelId === DEFAULT_FAST_MODEL ||
|
||||
model.modelId === DEFAULT_SMART_MODEL ||
|
||||
!includeProvider
|
||||
) {
|
||||
if (isAutoSelectModelId(model.modelId) || !includeProvider) {
|
||||
return model.label;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import { isAutoSelectModelId } from 'twenty-shared/utils';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type ClientAiModelConfig } from '~/generated-metadata/graphql';
|
||||
|
||||
const VIRTUAL_MODEL_IDS: Set<string> = new Set([
|
||||
DEFAULT_SMART_MODEL,
|
||||
DEFAULT_FAST_MODEL,
|
||||
]);
|
||||
|
||||
const isVirtualModel = (modelId: string) => VIRTUAL_MODEL_IDS.has(modelId);
|
||||
|
||||
export const useWorkspaceAiModelAvailability = () => {
|
||||
const aiModels = useAtomStateValue(aiModelsState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
@@ -23,7 +16,7 @@ export const useWorkspaceAiModelAvailability = () => {
|
||||
modelId: string,
|
||||
model?: ClientAiModelConfig,
|
||||
): boolean => {
|
||||
if (isVirtualModel(modelId)) {
|
||||
if (isAutoSelectModelId(modelId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -35,7 +28,7 @@ export const useWorkspaceAiModelAvailability = () => {
|
||||
};
|
||||
|
||||
const realModels = aiModels.filter(
|
||||
(model) => !isVirtualModel(model.modelId) && !model.isDeprecated,
|
||||
(model) => !isAutoSelectModelId(model.modelId) && !model.isDeprecated,
|
||||
);
|
||||
|
||||
const enabledModels = realModels.filter((model) =>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const agentChatUserSelectedModelState = createAtomState<string | null>({
|
||||
key: 'ai/agentChatUserSelectedModel',
|
||||
defaultValue: null,
|
||||
useLocalStorage: true,
|
||||
localStorageOptions: { getOnInit: true },
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { normalizeAiSdkError } from '@/ai/utils/normalizeAiSdkError';
|
||||
|
||||
describe('normalizeAiSdkError', () => {
|
||||
it('should return undefined for undefined input', () => {
|
||||
expect(normalizeAiSdkError(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the error unchanged if it already has a code', () => {
|
||||
const error = new Error('test') as Error & { code: string };
|
||||
error.code = 'EXISTING_CODE';
|
||||
|
||||
const result = normalizeAiSdkError(error);
|
||||
|
||||
expect(result).toBe(error);
|
||||
expect((result as Error & { code: string }).code).toBe('EXISTING_CODE');
|
||||
});
|
||||
|
||||
it('should parse JSON message and attach code from response body', () => {
|
||||
const error = new Error(
|
||||
'{"statusCode":402,"error":"Error","messages":["Credits exhausted"],"code":"BILLING_CREDITS_EXHAUSTED"}',
|
||||
);
|
||||
|
||||
const result = normalizeAiSdkError(error);
|
||||
|
||||
expect(result).not.toBe(error);
|
||||
expect((result as Error & { code: string }).code).toBe(
|
||||
'BILLING_CREDITS_EXHAUSTED',
|
||||
);
|
||||
expect(result?.message).toBe(error.message);
|
||||
});
|
||||
|
||||
it('should return original error for non-JSON message', () => {
|
||||
const error = new Error('Something went wrong');
|
||||
|
||||
const result = normalizeAiSdkError(error);
|
||||
|
||||
expect(result).toBe(error);
|
||||
});
|
||||
|
||||
it('should return original error for JSON message without code', () => {
|
||||
const error = new Error(
|
||||
'{"statusCode":500,"error":"Internal Server Error"}',
|
||||
);
|
||||
|
||||
const result = normalizeAiSdkError(error);
|
||||
|
||||
expect(result).toBe(error);
|
||||
});
|
||||
|
||||
it('should preserve the original stack trace', () => {
|
||||
const error = new Error(
|
||||
'{"statusCode":402,"code":"BILLING_CREDITS_EXHAUSTED"}',
|
||||
);
|
||||
const originalStack = error.stack;
|
||||
|
||||
const result = normalizeAiSdkError(error);
|
||||
|
||||
expect(result?.stack).toBe(originalStack);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
// The Vercel AI SDK wraps non-200 responses into an Error whose message
|
||||
// is the raw JSON response body, losing any custom properties (like `code`).
|
||||
// This function parses that JSON and re-attaches the code so downstream
|
||||
// consumers (extractErrorCode) can find it without knowing about the SDK.
|
||||
export const normalizeAiSdkError = (
|
||||
error: Error | undefined,
|
||||
): Error | undefined => {
|
||||
if (!isDefined(error) || !(error instanceof Error)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (
|
||||
'code' in error &&
|
||||
typeof (error as Error & { code: string }).code === 'string'
|
||||
) {
|
||||
return error;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(error.message);
|
||||
|
||||
if (
|
||||
isDefined(parsed) &&
|
||||
typeof parsed === 'object' &&
|
||||
'code' in parsed &&
|
||||
typeof (parsed as { code: unknown }).code === 'string'
|
||||
) {
|
||||
const normalizedError = new Error(error.message) as Error & {
|
||||
code: string;
|
||||
};
|
||||
normalizedError.code = (parsed as { code: string }).code;
|
||||
normalizedError.stack = error.stack;
|
||||
|
||||
return normalizedError;
|
||||
}
|
||||
} catch {
|
||||
// message is not JSON — nothing to normalize
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
||||
@@ -2,8 +2,10 @@ import { gql, InMemoryCache } from '@apollo/client';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import fetchMock, { enableFetchMocks } from 'jest-fetch-mock';
|
||||
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
import { ApolloFactory, type Options } from '@/apollo/services/apollo.factory';
|
||||
import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant';
|
||||
import { WorkspaceActivationStatus } from '~/generated-metadata/graphql';
|
||||
@@ -71,8 +73,8 @@ const mockWorkspace = {
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
trashRetentionDays: 14,
|
||||
eventLogRetentionDays: 365 * 3,
|
||||
fastModel: DEFAULT_FAST_MODEL,
|
||||
smartModel: DEFAULT_SMART_MODEL,
|
||||
fastModel: AUTO_SELECT_FAST_MODEL_ID,
|
||||
smartModel: AUTO_SELECT_SMART_MODEL_ID,
|
||||
routerModel: 'auto',
|
||||
enabledAiModelIds: [],
|
||||
useRecommendedModels: true,
|
||||
|
||||
+6
-4
@@ -1,7 +1,9 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant';
|
||||
import { useColumnDefinitionsFromObjectMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromObjectMetadata';
|
||||
@@ -68,8 +70,8 @@ describe('useColumnDefinitionsFromObjectMetadata', () => {
|
||||
isTwoFactorAuthenticationEnforced: false,
|
||||
trashRetentionDays: 14,
|
||||
eventLogRetentionDays: 365 * 3,
|
||||
fastModel: DEFAULT_FAST_MODEL,
|
||||
smartModel: DEFAULT_SMART_MODEL,
|
||||
fastModel: AUTO_SELECT_FAST_MODEL_ID,
|
||||
smartModel: AUTO_SELECT_SMART_MODEL_ID,
|
||||
enabledAiModelIds: [],
|
||||
useRecommendedModels: true,
|
||||
});
|
||||
|
||||
+12
-12
@@ -34,38 +34,38 @@ export const DateTimeSettingsDateFormatSelect = ({
|
||||
return (
|
||||
<Select
|
||||
dropdownId="datetime-settings-date-format"
|
||||
dropdownWidth={218}
|
||||
dropdownWidth={320}
|
||||
label={t`Date format`}
|
||||
fullWidth
|
||||
dropdownWidthAuto
|
||||
value={value}
|
||||
pinnedOption={{
|
||||
label: t`System settings`,
|
||||
value: DateFormat.SYSTEM,
|
||||
contextualText: systemDateFormatLabel,
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: t`System settings - ${systemDateFormatLabel}`,
|
||||
value: DateFormat.SYSTEM,
|
||||
},
|
||||
{
|
||||
label: `${formatInTimeZone(
|
||||
label: formatInTimeZone(
|
||||
Date.now(),
|
||||
usedTimeZone,
|
||||
DateFormat.MONTH_FIRST,
|
||||
)}`,
|
||||
),
|
||||
value: DateFormat.MONTH_FIRST,
|
||||
},
|
||||
{
|
||||
label: `${formatInTimeZone(
|
||||
label: formatInTimeZone(
|
||||
Date.now(),
|
||||
usedTimeZone,
|
||||
DateFormat.DAY_FIRST,
|
||||
)}`,
|
||||
),
|
||||
value: DateFormat.DAY_FIRST,
|
||||
},
|
||||
{
|
||||
label: `${formatInTimeZone(
|
||||
label: formatInTimeZone(
|
||||
Date.now(),
|
||||
usedTimeZone,
|
||||
DateFormat.YEAR_FIRST,
|
||||
)}`,
|
||||
),
|
||||
value: DateFormat.YEAR_FIRST,
|
||||
},
|
||||
]}
|
||||
|
||||
+9
-6
@@ -50,18 +50,21 @@ export const DateTimeSettingsTimeFormatSelect = ({
|
||||
dropdownWidthAuto
|
||||
fullWidth
|
||||
value={value}
|
||||
pinnedOption={{
|
||||
label: t`System settings`,
|
||||
value: TimeFormat.SYSTEM,
|
||||
contextualText: systemTimeFormatLabel,
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: t`System Settings - ${systemTimeFormatLabel}`,
|
||||
value: TimeFormat.SYSTEM,
|
||||
},
|
||||
{
|
||||
label: t`24h - ${hour24Label}`,
|
||||
label: t`24h`,
|
||||
value: TimeFormat.HOUR_24,
|
||||
contextualText: hour24Label,
|
||||
},
|
||||
{
|
||||
label: t`12h - ${hour12Label}`,
|
||||
label: t`12h`,
|
||||
value: TimeFormat.HOUR_12,
|
||||
contextualText: hour12Label,
|
||||
},
|
||||
]}
|
||||
onChange={onChange}
|
||||
|
||||
+7
-11
@@ -3,7 +3,6 @@ import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableT
|
||||
import { AVAILABLE_TIMEZONE_OPTIONS } from '@/settings/experience/constants/AvailableTimezoneOptions';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
type DateTimeSettingsTimeZoneSelectProps = {
|
||||
@@ -23,18 +22,15 @@ export const DateTimeSettingsTimeZoneSelect = ({
|
||||
<Select
|
||||
dropdownId="datetime-settings-time-zone"
|
||||
label={t`Time zone`}
|
||||
dropdownWidthAuto
|
||||
dropdownWidth={480}
|
||||
fullWidth
|
||||
value={value}
|
||||
options={[
|
||||
{
|
||||
label: isDefined(systemTimeZoneOption)
|
||||
? t`System settings`.concat(` - ${systemTimeZoneOption.label}`)
|
||||
: t`System settings`,
|
||||
value: 'system',
|
||||
},
|
||||
...(AVAILABLE_TIMEZONE_OPTIONS as SelectOption<string>[]),
|
||||
]}
|
||||
pinnedOption={{
|
||||
label: t`System settings`,
|
||||
value: 'system',
|
||||
contextualText: systemTimeZoneOption?.label,
|
||||
}}
|
||||
options={AVAILABLE_TIMEZONE_OPTIONS as SelectOption<string>[]}
|
||||
onChange={onChange}
|
||||
withSearchInput
|
||||
/>
|
||||
|
||||
+13
-8
@@ -46,26 +46,31 @@ export const NumberFormatSelect = ({
|
||||
dropdownWidthAuto
|
||||
fullWidth
|
||||
value={value}
|
||||
pinnedOption={{
|
||||
label: t`System settings`,
|
||||
value: NumberFormat.SYSTEM,
|
||||
contextualText: systemNumberFormatLabel,
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: t`System Settings - ${systemNumberFormatLabel}`,
|
||||
value: NumberFormat.SYSTEM,
|
||||
},
|
||||
{
|
||||
label: t`Commas and dot - ${commasAndDotExample}`,
|
||||
label: t`Commas and dot`,
|
||||
value: NumberFormat.COMMAS_AND_DOT,
|
||||
contextualText: commasAndDotExample,
|
||||
},
|
||||
{
|
||||
label: t`Spaces and comma - ${spacesAndCommaExample}`,
|
||||
label: t`Spaces and comma`,
|
||||
value: NumberFormat.SPACES_AND_COMMA,
|
||||
contextualText: spacesAndCommaExample,
|
||||
},
|
||||
{
|
||||
label: t`Dots and comma - ${dotsAndCommaExample}`,
|
||||
label: t`Dots and comma`,
|
||||
value: NumberFormat.DOTS_AND_COMMA,
|
||||
contextualText: dotsAndCommaExample,
|
||||
},
|
||||
{
|
||||
label: t`Apostrophe and dot - ${apostropheAndDotExample}`,
|
||||
label: t`Apostrophe and dot`,
|
||||
value: NumberFormat.APOSTROPHE_AND_DOT,
|
||||
contextualText: apostropheAndDotExample,
|
||||
},
|
||||
]}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -49,9 +49,11 @@ export type SelectProps<Value extends SelectValue> = {
|
||||
value?: Value;
|
||||
withSearchInput?: boolean;
|
||||
needIconCheck?: boolean;
|
||||
pinnedOption?: SelectOption<Value>;
|
||||
callToActionButton?: CallToActionButton;
|
||||
dropdownOffset?: DropdownOffset;
|
||||
hasRightElement?: boolean;
|
||||
showContextualTextInControl?: boolean;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
|
||||
@@ -88,15 +90,21 @@ export const Select = <Value extends SelectValue>({
|
||||
value,
|
||||
withSearchInput,
|
||||
needIconCheck,
|
||||
pinnedOption,
|
||||
callToActionButton,
|
||||
dropdownOffset,
|
||||
hasRightElement,
|
||||
showContextualTextInControl = true,
|
||||
}: SelectProps<Value>) => {
|
||||
const selectContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [searchInputValue, setSearchInputValue] = useState('');
|
||||
|
||||
const selectedOption = useMemo(() => {
|
||||
if (isDefined(pinnedOption) && pinnedOption.value === value) {
|
||||
return pinnedOption;
|
||||
}
|
||||
|
||||
const fromMatchingOption = options.find(
|
||||
({ value: optionValue }) => optionValue === value,
|
||||
);
|
||||
@@ -114,7 +122,7 @@ export const Select = <Value extends SelectValue>({
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [emptyOption, options, value]);
|
||||
}, [emptyOption, options, pinnedOption, value]);
|
||||
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
@@ -129,6 +137,7 @@ export const Select = <Value extends SelectValue>({
|
||||
const isDisabled =
|
||||
disabledFromProps ||
|
||||
(options.length <= 1 &&
|
||||
!isDefined(pinnedOption) &&
|
||||
!isDefined(callToActionButton) &&
|
||||
(!isDefined(emptyOption) || selectedOption !== emptyOption));
|
||||
|
||||
@@ -148,13 +157,26 @@ export const Select = <Value extends SelectValue>({
|
||||
|
||||
const { setSelectedItemId } = useSelectableList(dropdownId);
|
||||
|
||||
const controlSelectedOption = useMemo(() => {
|
||||
if (!isDefined(selectedOption) || showContextualTextInControl) {
|
||||
return selectedOption;
|
||||
}
|
||||
|
||||
const { contextualText: _, ...rest } = selectedOption;
|
||||
|
||||
return rest;
|
||||
}, [selectedOption, showContextualTextInControl]);
|
||||
|
||||
const handleDropdownOpen = () => {
|
||||
if (isDefined(selectedOption) && !isNonEmptyString(searchInputValue)) {
|
||||
setSelectedItemId(selectedOption.label);
|
||||
if (
|
||||
isDefined(controlSelectedOption) &&
|
||||
!isNonEmptyString(searchInputValue)
|
||||
) {
|
||||
setSelectedItemId(controlSelectedOption.label);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isDefined(selectedOption)) {
|
||||
if (!isDefined(controlSelectedOption)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -169,7 +191,7 @@ export const Select = <Value extends SelectValue>({
|
||||
{isNonEmptyString(label) && <StyledLabel>{label}</StyledLabel>}
|
||||
{isDisabled ? (
|
||||
<SelectControl
|
||||
selectedOption={selectedOption}
|
||||
selectedOption={controlSelectedOption}
|
||||
isDisabled={isDisabled}
|
||||
selectSizeVariant={selectSizeVariant}
|
||||
hasRightElement={hasRightElement}
|
||||
@@ -182,7 +204,7 @@ export const Select = <Value extends SelectValue>({
|
||||
onOpen={handleDropdownOpen}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
selectedOption={selectedOption}
|
||||
selectedOption={controlSelectedOption}
|
||||
isDisabled={isDisabled}
|
||||
selectSizeVariant={selectSizeVariant}
|
||||
hasRightElement={hasRightElement}
|
||||
@@ -200,6 +222,27 @@ export const Select = <Value extends SelectValue>({
|
||||
{withSearchInput === true && isNonEmptyArray(filteredOptions) && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{isDefined(pinnedOption) && (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItemSelect
|
||||
LeftIcon={pinnedOption.Icon}
|
||||
text={pinnedOption.label}
|
||||
contextualText={pinnedOption.contextualText}
|
||||
selected={
|
||||
controlSelectedOption.value === pinnedOption.value
|
||||
}
|
||||
needIconCheck={needIconCheck}
|
||||
onClick={() => {
|
||||
onChange?.(pinnedOption.value);
|
||||
onBlur?.();
|
||||
closeDropdown(dropdownId);
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
{isDefined(pinnedOption) && isNonEmptyArray(filteredOptions) && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{isNonEmptyArray(filteredOptions) && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<SelectableList
|
||||
@@ -220,7 +263,10 @@ export const Select = <Value extends SelectValue>({
|
||||
<MenuItemSelect
|
||||
LeftIcon={option.Icon}
|
||||
text={option.label}
|
||||
selected={selectedOption.value === option.value}
|
||||
contextualText={option.contextualText}
|
||||
selected={
|
||||
controlSelectedOption.value === option.value
|
||||
}
|
||||
focused={selectedItemId === option.label}
|
||||
needIconCheck={needIconCheck}
|
||||
onClick={() => {
|
||||
|
||||
@@ -94,7 +94,13 @@ export const SelectControl = ({
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
) : null}
|
||||
<OverflowingTextWithTooltip text={selectedOption.label} />
|
||||
<OverflowingTextWithTooltip
|
||||
text={
|
||||
selectedOption.contextualText
|
||||
? `${selectedOption.label} · ${selectedOption.contextualText}`
|
||||
: selectedOption.label
|
||||
}
|
||||
/>
|
||||
<StyledIconChevronDownWrapper disabled={isDisabled}>
|
||||
<IconChevronDown size={theme.icon.size.md} />
|
||||
</StyledIconChevronDownWrapper>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
|
||||
import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { aiModelsState } from '@/client-config/states/aiModelsState';
|
||||
@@ -51,46 +54,44 @@ export const SettingsAIModelsTab = () => {
|
||||
const currentSmartModel = currentWorkspace?.smartModel;
|
||||
const currentFastModel = currentWorkspace?.fastModel;
|
||||
|
||||
const buildVirtualModelOption = (virtualModelId: string) => {
|
||||
const virtualModel = aiModels.find(
|
||||
(model) => model.modelId === virtualModelId,
|
||||
const buildPinnedOption = (autoSelectModelId: string) => {
|
||||
const autoSelectEntry = aiModels.find(
|
||||
(model) => model.modelId === autoSelectModelId,
|
||||
);
|
||||
|
||||
return virtualModel
|
||||
? {
|
||||
value: virtualModelId,
|
||||
label: virtualModel.label,
|
||||
Icon: IconTwentyStar,
|
||||
}
|
||||
: null;
|
||||
};
|
||||
|
||||
const smartAutoOption = buildVirtualModelOption(DEFAULT_SMART_MODEL);
|
||||
const fastAutoOption = buildVirtualModelOption(DEFAULT_FAST_MODEL);
|
||||
|
||||
const modelOptions = enabledModels.map((model) => {
|
||||
const residencyFlag = model.dataResidency
|
||||
? ` ${getDataResidencyDisplay(model.dataResidency)}`
|
||||
: '';
|
||||
if (!autoSelectEntry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
value: model.modelId,
|
||||
label: `${model.label}${residencyFlag}`,
|
||||
Icon: getModelIcon(model.modelFamily, model.providerName),
|
||||
value: autoSelectModelId,
|
||||
label: autoSelectEntry.label,
|
||||
Icon: getModelIcon(
|
||||
autoSelectEntry.modelFamily,
|
||||
autoSelectEntry.providerName,
|
||||
),
|
||||
contextualText: t`Best`,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const smartModelOptions = [...modelOptions];
|
||||
const smartPinnedOption = buildPinnedOption(AUTO_SELECT_SMART_MODEL_ID);
|
||||
const fastPinnedOption = buildPinnedOption(AUTO_SELECT_FAST_MODEL_ID);
|
||||
|
||||
if (smartAutoOption !== null) {
|
||||
smartModelOptions.unshift(smartAutoOption);
|
||||
}
|
||||
const buildModelOptions = () =>
|
||||
enabledModels.map((model) => {
|
||||
const residencyFlag = model.dataResidency
|
||||
? ` ${getDataResidencyDisplay(model.dataResidency)}`
|
||||
: '';
|
||||
|
||||
const fastModelOptions = [...modelOptions];
|
||||
return {
|
||||
value: model.modelId,
|
||||
label: `${model.label}${residencyFlag}`,
|
||||
Icon: getModelIcon(model.modelFamily, model.providerName),
|
||||
};
|
||||
});
|
||||
|
||||
if (fastAutoOption !== null) {
|
||||
fastModelOptions.unshift(fastAutoOption);
|
||||
}
|
||||
const smartModelOptions = buildModelOptions();
|
||||
const fastModelOptions = buildModelOptions();
|
||||
|
||||
const handleModelFieldChange = async (
|
||||
field: 'smartModel' | 'fastModel',
|
||||
@@ -241,6 +242,7 @@ export const SettingsAIModelsTab = () => {
|
||||
value={currentSmartModel}
|
||||
onChange={(value) => handleModelFieldChange('smartModel', value)}
|
||||
options={smartModelOptions}
|
||||
pinnedOption={smartPinnedOption}
|
||||
selectSizeVariant="small"
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
@@ -255,6 +257,7 @@ export const SettingsAIModelsTab = () => {
|
||||
value={currentFastModel}
|
||||
onChange={(value) => handleModelFieldChange('fastModel', value)}
|
||||
options={fastModelOptions}
|
||||
pinnedOption={fastPinnedOption}
|
||||
selectSizeVariant="small"
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
|
||||
+19
-23
@@ -2,7 +2,6 @@ import { useMemo } from 'react';
|
||||
|
||||
import { detectCalendarStartDay } from '@/localization/utils/detection/detectCalendarStartDay';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { type DayNameWithIndex } from '@/ui/input/components/internal/date/types/DayNameWithIndex';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { CalendarStartDay } from 'twenty-shared/constants';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
@@ -18,29 +17,21 @@ export const DateTimeSettingsCalendarStartDaySelect = ({
|
||||
}: DateTimeSettingsCalendarStartDaySelectProps) => {
|
||||
const systemCalendarStartDay = CalendarStartDay[detectCalendarStartDay()];
|
||||
|
||||
const options: SelectOption<CalendarStartDay>[] = useMemo(() => {
|
||||
const systemDayLabel =
|
||||
systemCalendarStartDay === CalendarStartDay.SUNDAY
|
||||
? t`System settings - Sunday`
|
||||
: systemCalendarStartDay === CalendarStartDay.MONDAY
|
||||
? t`System settings - Monday`
|
||||
: t`System settings - Saturday`;
|
||||
const systemDayContextualText =
|
||||
systemCalendarStartDay === CalendarStartDay.SUNDAY
|
||||
? t`Sunday`
|
||||
: systemCalendarStartDay === CalendarStartDay.MONDAY
|
||||
? t`Monday`
|
||||
: t`Saturday`;
|
||||
|
||||
const allowedDaysWeek: DayNameWithIndex[] = [
|
||||
{
|
||||
day: systemDayLabel,
|
||||
index: CalendarStartDay.SYSTEM,
|
||||
},
|
||||
{ day: t`Sunday`, index: CalendarStartDay.SUNDAY },
|
||||
{ day: t`Monday`, index: CalendarStartDay.MONDAY },
|
||||
{ day: t`Saturday`, index: CalendarStartDay.SATURDAY },
|
||||
];
|
||||
|
||||
return allowedDaysWeek.map(({ day, index }) => ({
|
||||
label: day,
|
||||
value: index as CalendarStartDay,
|
||||
}));
|
||||
}, [systemCalendarStartDay]);
|
||||
const options: SelectOption<CalendarStartDay>[] = useMemo(
|
||||
() => [
|
||||
{ label: t`Sunday`, value: CalendarStartDay.SUNDAY },
|
||||
{ label: t`Monday`, value: CalendarStartDay.MONDAY },
|
||||
{ label: t`Saturday`, value: CalendarStartDay.SATURDAY },
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
@@ -50,6 +41,11 @@ export const DateTimeSettingsCalendarStartDaySelect = ({
|
||||
fullWidth
|
||||
dropdownWidthAuto
|
||||
value={value}
|
||||
pinnedOption={{
|
||||
label: t`System settings`,
|
||||
value: CalendarStartDay.SYSTEM,
|
||||
contextualText: systemDayContextualText,
|
||||
}}
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel';
|
||||
import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
import { type CurrentUserWorkspace } from '@/auth/states/currentUserWorkspaceState';
|
||||
import { CUSTOM_WORKSPACE_APPLICATION_MOCK } from '@/object-metadata/hooks/__tests__/constants/CustomWorkspaceApplicationMock.test.constant';
|
||||
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
@@ -86,8 +88,8 @@ export const mockCurrentWorkspace = {
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
metadataVersion: 1,
|
||||
trashRetentionDays: 14,
|
||||
fastModel: DEFAULT_FAST_MODEL,
|
||||
smartModel: DEFAULT_SMART_MODEL,
|
||||
fastModel: AUTO_SELECT_FAST_MODEL_ID,
|
||||
smartModel: AUTO_SELECT_SMART_MODEL_ID,
|
||||
routerModel: 'auto',
|
||||
enabledAiModelIds: [],
|
||||
useRecommendedModels: true,
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ export class MigrateModelIdsToCompositeFormat1773900000000
|
||||
name = 'MigrateModelIdsToCompositeFormat1773900000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Reset workspace model columns to sentinel defaults.
|
||||
// Reset workspace model columns to auto-select placeholders.
|
||||
// The runtime resolves these dynamically from admin preferences.
|
||||
await queryRunner.query(
|
||||
`UPDATE "core"."workspace"
|
||||
@@ -25,7 +25,7 @@ export class MigrateModelIdsToCompositeFormat1773900000000
|
||||
OR array_length("enabledAiModelIds", 1) > 0`,
|
||||
);
|
||||
|
||||
// Reset agent model IDs to the sentinel so they fall back to workspace defaults
|
||||
// Reset agent model IDs to the auto-select placeholder so they fall back to workspace defaults
|
||||
await queryRunner.query(
|
||||
`UPDATE "core"."agent"
|
||||
SET "modelId" = 'default-smart-model'
|
||||
@@ -34,6 +34,6 @@ export class MigrateModelIdsToCompositeFormat1773900000000
|
||||
}
|
||||
|
||||
public async down(_queryRunner: QueryRunner): Promise<void> {
|
||||
// No reversal needed — sentinel defaults are safe to leave in place.
|
||||
// No reversal needed — auto-select placeholders are safe to leave in place.
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { type AgentManifest } from 'twenty-shared/application';
|
||||
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/types/model-id.type';
|
||||
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
|
||||
|
||||
@@ -21,7 +21,7 @@ export const fromAgentManifestToUniversalFlatAgent = ({
|
||||
icon: agentManifest.icon ?? null,
|
||||
description: agentManifest.description ?? null,
|
||||
prompt: agentManifest.prompt,
|
||||
modelId: (agentManifest.modelId as ModelId) ?? DEFAULT_SMART_MODEL,
|
||||
modelId: (agentManifest.modelId as ModelId) ?? AUTO_SELECT_SMART_MODEL_ID,
|
||||
responseFormat: { type: 'text' },
|
||||
modelConfiguration: null,
|
||||
evaluationInputs: [],
|
||||
|
||||
+20
-16
@@ -20,8 +20,10 @@ import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain
|
||||
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { convertDollarsToBillingCredits } from 'src/engine/metadata-modules/ai/ai-billing/utils/convert-dollars-to-billing-credits.util';
|
||||
import { DEFAULT_FAST_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-fast-model.const';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
import { MODEL_FAMILY_LABELS } from 'src/engine/metadata-modules/ai/ai-models/constants/model-family-labels.const';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
|
||||
@@ -107,10 +109,6 @@ export class ClientConfigService {
|
||||
this.aiModelRegistryService.getDefaultSpeedModel();
|
||||
const defaultSpeedModelConfig =
|
||||
this.aiModelRegistryService.getModelConfig(defaultSpeedModel?.modelId);
|
||||
const defaultSpeedModelLabel =
|
||||
defaultSpeedModelConfig?.label ||
|
||||
defaultSpeedModel?.modelId ||
|
||||
'Default';
|
||||
|
||||
const defaultPerformanceModel =
|
||||
this.aiModelRegistryService.getDefaultPerformanceModel();
|
||||
@@ -118,23 +116,29 @@ export class ClientConfigService {
|
||||
this.aiModelRegistryService.getModelConfig(
|
||||
defaultPerformanceModel?.modelId,
|
||||
);
|
||||
const defaultPerformanceModelLabel =
|
||||
defaultPerformanceModelConfig?.label ||
|
||||
defaultPerformanceModel?.modelId ||
|
||||
'Default';
|
||||
|
||||
aiModels.unshift(
|
||||
{
|
||||
modelId: DEFAULT_SMART_MODEL,
|
||||
label: `Best (${defaultPerformanceModelLabel})`,
|
||||
sdkPackage: null,
|
||||
modelId: AUTO_SELECT_SMART_MODEL_ID,
|
||||
label:
|
||||
defaultPerformanceModelConfig?.label ||
|
||||
defaultPerformanceModel?.modelId ||
|
||||
'Default',
|
||||
modelFamily: defaultPerformanceModelConfig?.modelFamily,
|
||||
providerName: defaultPerformanceModel?.providerName,
|
||||
sdkPackage: defaultPerformanceModel?.sdkPackage ?? null,
|
||||
inputCostPerMillionTokensInCredits: 0,
|
||||
outputCostPerMillionTokensInCredits: 0,
|
||||
},
|
||||
{
|
||||
modelId: DEFAULT_FAST_MODEL,
|
||||
label: `Best (${defaultSpeedModelLabel})`,
|
||||
sdkPackage: null,
|
||||
modelId: AUTO_SELECT_FAST_MODEL_ID,
|
||||
label:
|
||||
defaultSpeedModelConfig?.label ||
|
||||
defaultSpeedModel?.modelId ||
|
||||
'Default',
|
||||
modelFamily: defaultSpeedModelConfig?.modelFamily,
|
||||
providerName: defaultSpeedModel?.providerName,
|
||||
sdkPackage: defaultSpeedModel?.sdkPackage ?? null,
|
||||
inputCostPerMillionTokensInCredits: 0,
|
||||
outputCostPerMillionTokensInCredits: 0,
|
||||
},
|
||||
|
||||
@@ -34,8 +34,10 @@ import { PublicDomainEntity } from 'src/engine/core-modules/public-domain/public
|
||||
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { DEFAULT_FAST_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-fast-model.const';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/types/model-id.type';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
@@ -301,11 +303,19 @@ export class WorkspaceEntity {
|
||||
version: string | null;
|
||||
|
||||
@Field(() => String, { nullable: false })
|
||||
@Column({ type: 'varchar', nullable: false, default: DEFAULT_FAST_MODEL })
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
nullable: false,
|
||||
default: AUTO_SELECT_FAST_MODEL_ID,
|
||||
})
|
||||
fastModel: ModelId;
|
||||
|
||||
@Field(() => String, { nullable: false })
|
||||
@Column({ type: 'varchar', nullable: false, default: DEFAULT_SMART_MODEL })
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
nullable: false,
|
||||
default: AUTO_SELECT_SMART_MODEL_ID,
|
||||
})
|
||||
smartModel: ModelId;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
|
||||
+6
-2
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
import { AgentResponseFormat } from 'src/engine/metadata-modules/ai/ai-agent/types/agent-response-format.type';
|
||||
import { ModelConfiguration } from 'src/engine/metadata-modules/ai/ai-agent/types/modelConfiguration';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/types/model-id.type';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
@@ -43,7 +43,11 @@ export class AgentEntity
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
prompt: string;
|
||||
|
||||
@Column({ nullable: false, type: 'varchar', default: DEFAULT_SMART_MODEL })
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'varchar',
|
||||
default: AUTO_SELECT_SMART_MODEL_ID,
|
||||
})
|
||||
modelId: ModelId;
|
||||
|
||||
// Should not be nullable
|
||||
|
||||
+4
-2
@@ -39,9 +39,9 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
|
||||
@Controller('rest/agent-chat')
|
||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||
@UseFilters(
|
||||
RestApiExceptionFilter,
|
||||
AgentRestApiExceptionFilter,
|
||||
BillingRestApiExceptionFilter,
|
||||
RestApiExceptionFilter,
|
||||
)
|
||||
export class AgentChatController {
|
||||
constructor(
|
||||
@@ -59,6 +59,7 @@ export class AgentChatController {
|
||||
threadId: string;
|
||||
messages: ExtendedUIMessage[];
|
||||
browsingContext?: BrowsingContextType | null;
|
||||
modelId?: string;
|
||||
},
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -71,7 +72,7 @@ export class AgentChatController {
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedModelId = workspace.smartModel;
|
||||
const resolvedModelId = body.modelId ?? workspace.smartModel;
|
||||
|
||||
this.aiModelRegistryService.validateModelAvailability(
|
||||
resolvedModelId,
|
||||
@@ -96,6 +97,7 @@ export class AgentChatController {
|
||||
threadId: body.threadId,
|
||||
messages: body.messages,
|
||||
browsingContext: body.browsingContext ?? null,
|
||||
modelId: body.modelId,
|
||||
userWorkspaceId,
|
||||
workspace,
|
||||
response,
|
||||
|
||||
+3
@@ -33,6 +33,7 @@ export type StreamAgentChatOptions = {
|
||||
response: Response;
|
||||
messages: ExtendedUIMessage[];
|
||||
browsingContext: BrowsingContextType | null;
|
||||
modelId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -51,6 +52,7 @@ export class AgentChatStreamingService {
|
||||
messages,
|
||||
browsingContext,
|
||||
response,
|
||||
modelId,
|
||||
}: StreamAgentChatOptions) {
|
||||
const thread = await this.threadRepository.findOne({
|
||||
where: {
|
||||
@@ -114,6 +116,7 @@ export class AgentChatStreamingService {
|
||||
messages,
|
||||
browsingContext,
|
||||
onCodeExecutionUpdate,
|
||||
modelId,
|
||||
});
|
||||
|
||||
let streamUsage = {
|
||||
|
||||
+8
-3
@@ -60,6 +60,7 @@ export type ChatExecutionOptions = {
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
browsingContext: BrowsingContextType | null;
|
||||
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
|
||||
modelId?: string;
|
||||
};
|
||||
|
||||
export type ChatExecutionResult = {
|
||||
@@ -89,6 +90,7 @@ export class ChatExecutionService {
|
||||
messages,
|
||||
browsingContext,
|
||||
onCodeExecutionUpdate,
|
||||
modelId,
|
||||
}: ChatExecutionOptions): Promise<ChatExecutionResult> {
|
||||
const { actorContext, roleId, userId, userContext } =
|
||||
await this.agentActorContextService.buildUserAndAgentActorContext(
|
||||
@@ -128,13 +130,16 @@ export class ChatExecutionService {
|
||||
toolContext,
|
||||
);
|
||||
|
||||
const modelId = workspace.smartModel;
|
||||
const resolvedModelId = modelId ?? workspace.smartModel;
|
||||
|
||||
this.aiModelRegistryService.validateModelAvailability(modelId, workspace);
|
||||
this.aiModelRegistryService.validateModelAvailability(
|
||||
resolvedModelId,
|
||||
workspace,
|
||||
);
|
||||
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent({
|
||||
modelId,
|
||||
modelId: resolvedModelId,
|
||||
});
|
||||
|
||||
const modelConfig = this.aiModelRegistryService.getEffectiveModelConfig(
|
||||
|
||||
+135
-135
File diff suppressed because it is too large
Load Diff
+9
-7
@@ -8,7 +8,7 @@ import { SdkProviderFactoryService } from 'src/engine/metadata-modules/ai/ai-mod
|
||||
import { buildCompositeModelId } from 'src/engine/metadata-modules/ai/ai-models/utils/composite-model-id.util';
|
||||
import { loadDefaultAiProviders } from 'src/engine/metadata-modules/ai/ai-models/utils/load-default-ai-providers.util';
|
||||
import { type AiProvidersConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-providers-config.type';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
|
||||
const DEFAULT_PROVIDERS: AiProvidersConfig = loadDefaultAiProviders();
|
||||
|
||||
@@ -125,13 +125,15 @@ describe('AiModelRegistryService', () => {
|
||||
service = module.get<AiModelRegistryService>(AiModelRegistryService);
|
||||
});
|
||||
|
||||
it('should throw when no models are available for DEFAULT_SMART_MODEL', () => {
|
||||
expect(() => service.getEffectiveModelConfig(DEFAULT_SMART_MODEL)).toThrow(
|
||||
it('should throw when no models are available for AUTO_SELECT_SMART_MODEL_ID', () => {
|
||||
expect(() =>
|
||||
service.getEffectiveModelConfig(AUTO_SELECT_SMART_MODEL_ID),
|
||||
).toThrow(
|
||||
'No AI models are available. Configure at least one AI provider.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return effective model config for DEFAULT_SMART_MODEL when models are available', () => {
|
||||
it('should return effective model config for AUTO_SELECT_SMART_MODEL_ID when models are available', () => {
|
||||
jest.spyOn(service, 'getAvailableModels').mockReturnValue([
|
||||
{
|
||||
modelId: 'openai/gpt-5.2',
|
||||
@@ -146,14 +148,14 @@ describe('AiModelRegistryService', () => {
|
||||
model: {} as any,
|
||||
});
|
||||
|
||||
const result = service.getEffectiveModelConfig(DEFAULT_SMART_MODEL);
|
||||
const result = service.getEffectiveModelConfig(AUTO_SELECT_SMART_MODEL_ID);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.modelId).toBe('openai/gpt-5.2');
|
||||
expect(result.sdkPackage).toBe('@ai-sdk/openai');
|
||||
});
|
||||
|
||||
it('should return effective model config for DEFAULT_SMART_MODEL with custom model', () => {
|
||||
it('should return effective model config for AUTO_SELECT_SMART_MODEL_ID with custom model', () => {
|
||||
jest.spyOn(service, 'getAvailableModels').mockReturnValue([
|
||||
{
|
||||
modelId: 'custom/mistral',
|
||||
@@ -168,7 +170,7 @@ describe('AiModelRegistryService', () => {
|
||||
model: {} as any,
|
||||
});
|
||||
|
||||
const result = service.getEffectiveModelConfig(DEFAULT_SMART_MODEL);
|
||||
const result = service.getEffectiveModelConfig(AUTO_SELECT_SMART_MODEL_ID);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.modelId).toBe('custom/mistral');
|
||||
|
||||
+10
-7
@@ -17,12 +17,15 @@ import { type AiProviderConfig } from 'src/engine/metadata-modules/ai/ai-models/
|
||||
import { type AiProviderModelConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-provider-model-config.type';
|
||||
import { type AiProvidersConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-providers-config.type';
|
||||
import { DEFAULT_CONTEXT_WINDOW_TOKENS } from 'src/engine/metadata-modules/ai/ai-models/types/default-context-window-tokens.const';
|
||||
import { DEFAULT_FAST_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-fast-model.const';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
import { isAutoSelectModelId } from 'twenty-shared/utils';
|
||||
|
||||
import { DEFAULT_MAX_OUTPUT_TOKENS } from 'src/engine/metadata-modules/ai/ai-models/types/default-max-output-tokens.const';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { buildCompositeModelId } from 'src/engine/metadata-modules/ai/ai-models/utils/composite-model-id.util';
|
||||
import { inferModelFamily } from 'src/engine/metadata-modules/ai/ai-models/utils/infer-model-family.util';
|
||||
import { isDefaultModelSentinel } from 'src/engine/metadata-modules/ai/ai-models/utils/is-default-model-sentinel.util';
|
||||
import {
|
||||
isModelAllowedByWorkspace,
|
||||
type WorkspaceModelAvailabilitySettings,
|
||||
@@ -203,9 +206,9 @@ export class AiModelRegistryService {
|
||||
}
|
||||
|
||||
getEffectiveModelConfig(modelId: string): AIModelConfig {
|
||||
if (isDefaultModelSentinel(modelId)) {
|
||||
if (isAutoSelectModelId(modelId)) {
|
||||
const defaultModel =
|
||||
modelId === DEFAULT_FAST_MODEL
|
||||
modelId === AUTO_SELECT_FAST_MODEL_ID
|
||||
? this.getDefaultSpeedModel()
|
||||
: this.getDefaultPerformanceModel();
|
||||
|
||||
@@ -253,7 +256,7 @@ export class AiModelRegistryService {
|
||||
}
|
||||
|
||||
isModelAdminAllowed(modelId: string): boolean {
|
||||
if (isDefaultModelSentinel(modelId)) {
|
||||
if (isAutoSelectModelId(modelId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -360,7 +363,7 @@ export class AiModelRegistryService {
|
||||
|
||||
resolveModelForAgent(agent: { modelId: string } | null): RegisteredAIModel {
|
||||
const aiModel = this.getEffectiveModelConfig(
|
||||
agent?.modelId ?? DEFAULT_SMART_MODEL,
|
||||
agent?.modelId ?? AUTO_SELECT_SMART_MODEL_ID,
|
||||
);
|
||||
|
||||
const registeredModel = this.getModel(aiModel.modelId);
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const DEFAULT_FAST_MODEL = 'default-fast-model' as const;
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const DEFAULT_SMART_MODEL = 'default-smart-model' as const;
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
export enum ModelFamily {
|
||||
GPT = 'gpt',
|
||||
CLAUDE = 'claude',
|
||||
GEMINI = 'gemini',
|
||||
MISTRAL = 'mistral',
|
||||
GROK = 'grok',
|
||||
GPT = 'GPT',
|
||||
CLAUDE = 'CLAUDE',
|
||||
GEMINI = 'GEMINI',
|
||||
MISTRAL = 'MISTRAL',
|
||||
GROK = 'GROK',
|
||||
}
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { DEFAULT_FAST_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-fast-model.const';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
|
||||
export const isDefaultModelSentinel = (modelId: string): boolean =>
|
||||
modelId === DEFAULT_FAST_MODEL || modelId === DEFAULT_SMART_MODEL;
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { isDefaultModelSentinel } from 'src/engine/metadata-modules/ai/ai-models/utils/is-default-model-sentinel.util';
|
||||
import { isAutoSelectModelId } from 'twenty-shared/utils';
|
||||
|
||||
export type WorkspaceModelAvailabilitySettings = {
|
||||
useRecommendedModels: boolean;
|
||||
@@ -10,7 +10,7 @@ export const isModelAllowedByWorkspace = (
|
||||
workspace: WorkspaceModelAvailabilitySettings,
|
||||
recommendedModelIds?: Set<string>,
|
||||
): boolean => {
|
||||
if (isDefaultModelSentinel(modelId)) {
|
||||
if (isAutoSelectModelId(modelId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { type AllStandardAgentName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-agent-name.type';
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ Response format:
|
||||
- Use markdown for readability
|
||||
|
||||
Always base answers on official Twenty documentation. Be patient and helpful.`,
|
||||
modelId: DEFAULT_SMART_MODEL,
|
||||
modelId: AUTO_SELECT_SMART_MODEL_ID,
|
||||
responseFormat: { type: 'text' },
|
||||
isCustom: false,
|
||||
modelConfiguration: {},
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/work
|
||||
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position.input';
|
||||
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
|
||||
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
|
||||
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
|
||||
@@ -446,7 +446,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
description: '',
|
||||
prompt:
|
||||
'You are a helpful AI assistant. Complete the task based on the workflow context.',
|
||||
modelId: DEFAULT_SMART_MODEL,
|
||||
modelId: AUTO_SELECT_SMART_MODEL_ID,
|
||||
responseFormat: { type: 'text' },
|
||||
isCustom: true,
|
||||
},
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/inte
|
||||
import { AgentAsyncExecutorService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { AiBillingService } from 'src/engine/metadata-modules/ai/ai-billing/services/ai-billing.service';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
@@ -90,7 +90,7 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
});
|
||||
|
||||
await this.aiBillingService.calculateAndBillUsage(
|
||||
agent?.modelId ?? DEFAULT_SMART_MODEL,
|
||||
agent?.modelId ?? AUTO_SELECT_SMART_MODEL_ID,
|
||||
{ usage, cacheCreationTokens },
|
||||
workspaceId,
|
||||
agent?.id || null,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const AUTO_SELECT_FAST_MODEL_ID = 'default-fast-model' as const;
|
||||
@@ -0,0 +1 @@
|
||||
export const AUTO_SELECT_SMART_MODEL_ID = 'default-smart-model' as const;
|
||||
@@ -7,6 +7,8 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export { AUTO_SELECT_FAST_MODEL_ID } from './AutoSelectFastModelId';
|
||||
export { AUTO_SELECT_SMART_MODEL_ID } from './AutoSelectSmartModelId';
|
||||
export { BACKEND_BATCH_REQUEST_MAX_COUNT } from './BackendBatchRequestMaxCount';
|
||||
export { CalendarStartDay } from './CalendarStartDay';
|
||||
export { COMPOSITE_FIELD_TYPE_SUB_FIELDS_NAMES } from './CompositeFieldTypeSubFieldsNames';
|
||||
|
||||
@@ -143,6 +143,7 @@ export {
|
||||
getLogoUrlFromDomainName,
|
||||
} from './image/getLogoUrlFromDomainName';
|
||||
export { getUniqueConstraintsFields } from './indexMetadata/getUniqueConstraintsFields';
|
||||
export { isAutoSelectModelId } from './isAutoSelectModelId';
|
||||
export { fastDeepEqual } from './json/fast-deep-equal';
|
||||
export { getAppPath } from './navigation/getAppPath';
|
||||
export { getSettingsPath } from './navigation/getSettingsPath';
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { AUTO_SELECT_FAST_MODEL_ID } from '../constants/AutoSelectFastModelId';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from '../constants/AutoSelectSmartModelId';
|
||||
|
||||
export const isAutoSelectModelId = (modelId: string): boolean =>
|
||||
modelId === AUTO_SELECT_FAST_MODEL_ID ||
|
||||
modelId === AUTO_SELECT_SMART_MODEL_ID;
|
||||
@@ -23,6 +23,10 @@ const StyledIconButton = styled.button<{
|
||||
color 0.1s ease-in-out,
|
||||
background 0.1s ease-in-out;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: ${themeCssVariables.color.blue10};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: ${themeCssVariables.background.quaternary};
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
|
||||
@@ -10,4 +10,5 @@ export type SelectOption<
|
||||
value: Value;
|
||||
disabled?: boolean;
|
||||
color?: ThemeColor | 'transparent';
|
||||
contextualText?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user