Files
twenty/packages/twenty-front/src/modules/settings/admin-panel/ai/components/SettingsAdminAI.tsx
T
Félix Malfait 09806d7d8c Add admin panel workspace detail page with chat viewer (#19579)
## Overview
Adds comprehensive admin panel functionality for viewing workspace
details and AI chat threads.

## Changes

### Frontend
- **New Routes**: Added `AdminPanelWorkspaceDetail` and
`AdminPanelWorkspaceChatThread` pages with lazy loading
- **New Queries**: 
  - `getAdminWorkspaceChatThreads` - fetch chat threads for a workspace
  - `getAdminChatThreadMessages` - fetch messages for a specific thread
  - `workspaceLookupAdminPanel` - lookup workspace info and users
- **New Components**:
- `SettingsAdminWorkspaceDetail` - displays workspace info and chat
sessions tabs
- `SettingsAdminWorkspaceChatThread` - renders chat conversation with
message bubbles
- **Navigation**: Updated AI admin panel to link to workspace detail
pages
- **Settings Paths**: Added `AdminPanelWorkspaceDetail` and
`AdminPanelWorkspaceChatThread` paths

### Backend
- **New DTOs**:
  - `AdminWorkspaceChatThreadDTO` - workspace chat thread data
  - `AdminChatThreadMessagesDTO` - thread with messages
  - `AdminChatMessageDTO` - individual message with parts
- **New Resolvers**: Added three queries to `AdminPanelResolver`
- **New Service Methods**:
  - `workspaceLookup()` - fetch workspace info
  - `getWorkspaceChatThreads()` - list chat threads
  - `getChatThreadMessages()` - fetch thread messages with validation
- **Module Updates**: Added entity imports for workspace, user, AI chat,
and feature flag data

### Security
- Added `allowImpersonation` check before accessing chat data
- Validates workspace ownership and access permissions

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:47:34 +02:00

358 lines
13 KiB
TypeScript

import { useMemo, useState } from 'react';
import { useMutation, useQuery } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { Tag } from 'twenty-ui/components';
import { H2Title, IconBolt, IconLock, IconRobot } from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { billingState } from '@/client-config/states/billingState';
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
import { SettingsAiModelsTable } from '@/settings/ai/components/SettingsAiModelsTable';
import { SettingsAdminAiProviderListCard } from '@/settings/admin-panel/ai/components/SettingsAdminAiProviderListCard';
import { AI_PROVIDER_SOURCE } from '@/settings/admin-panel/ai/constants/AiProviderSource';
import { SET_ADMIN_AI_MODEL_RECOMMENDED } from '@/settings/admin-panel/ai/graphql/mutations/setAdminAiModelRecommended';
import { SET_ADMIN_AI_MODELS_RECOMMENDED } from '@/settings/admin-panel/ai/graphql/mutations/setAdminAiModelsRecommended';
import { SET_ADMIN_DEFAULT_AI_MODEL } from '@/settings/admin-panel/ai/graphql/mutations/setAdminDefaultAiModel';
import { GET_ADMIN_AI_MODELS } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiModels';
import { GET_ADMIN_AI_USAGE_BY_WORKSPACE } from '@/settings/admin-panel/ai/graphql/queries/getAdminAiUsageByWorkspace';
import { GET_AI_PROVIDERS } from '@/settings/admin-panel/ai/graphql/queries/getAiProviders';
import { type GetAiProvidersResult } from '@/settings/admin-panel/ai/types/GetAiProvidersResult';
import { parseProviderItems } from '@/settings/admin-panel/ai/utils/parseProviderItems';
import { getModelIcon } from '@/settings/ai/utils/getModelIcon';
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { useUsageValueFormatter } from '@/settings/usage/hooks/useUsageValueFormatter';
import { getPeriodDates } from '@/settings/usage/utils/getPeriodDates';
import { getPeriodOptions } from '@/settings/usage/utils/getPeriodOptions';
import { type PeriodPreset } from '@/settings/usage/utils/periodPreset';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { Select } from '@/ui/input/components/Select';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import {
AiModelRole,
type AdminAiModelConfig,
} from '~/generated-metadata/graphql';
const USAGE_TABLE_GRID_TEMPLATE_COLUMNS = '1fr 120px';
type UsageBreakdownItem = {
key: string;
label?: string | null;
creditsUsed: number;
};
export const SettingsAdminAI = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const { refetch: refetchClientConfig } = useClientConfig();
const { formatUsageValue } = useUsageValueFormatter();
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const billing = useAtomStateValue(billingState);
const isBillingEnabled = billing?.isBillingEnabled ?? false;
const hasEnterpriseAccess =
isBillingEnabled || currentWorkspace?.hasValidEnterpriseKey === true;
const [usagePeriod, setUsagePeriod] = useState<PeriodPreset>('30d');
const periodOptions = getPeriodOptions();
const usageDates = getPeriodDates(usagePeriod);
const {
data,
loading: isLoadingModels,
refetch: refetchModels,
} = useQuery<{
getAdminAiModels: {
defaultSmartModelId?: string | null;
defaultFastModelId?: string | null;
models: AdminAiModelConfig[];
};
}>(GET_ADMIN_AI_MODELS);
const [setModelRecommended] = useMutation(SET_ADMIN_AI_MODEL_RECOMMENDED);
const [setModelsRecommended] = useMutation(SET_ADMIN_AI_MODELS_RECOMMENDED);
const [setDefaultModel] = useMutation(SET_ADMIN_DEFAULT_AI_MODEL);
const { data: providersData, loading: isLoadingProviders } =
useQuery<GetAiProvidersResult>(GET_AI_PROVIDERS);
const { data: usageData, previousData: previousUsageData } = useQuery<{
getAdminAiUsageByWorkspace: UsageBreakdownItem[];
}>(GET_ADMIN_AI_USAGE_BY_WORKSPACE, {
variables: {
periodStart: usageDates.periodStart,
periodEnd: usageDates.periodEnd,
},
skip: !hasEnterpriseAccess,
});
const effectiveUsageData = usageData ?? previousUsageData;
const usageByWorkspace = effectiveUsageData?.getAdminAiUsageByWorkspace ?? [];
const models = data?.getAdminAiModels?.models ?? [];
const providerItems = useMemo(
() => parseProviderItems(providersData?.getAiProviders ?? {}),
[providersData],
);
const catalogProviders = useMemo(
() =>
providerItems
.filter((provider) => provider.source === AI_PROVIDER_SOURCE.CATALOG)
.sort((a, b) => (a.label ?? a.id).localeCompare(b.label ?? b.id)),
[providerItems],
);
const customProviders = providerItems.filter(
(provider) => provider.source === AI_PROVIDER_SOURCE.CUSTOM,
);
if (isLoadingProviders || isLoadingModels) {
return <SettingsAdminTabSkeletonLoader />;
}
const handleRecommendedToggle = async (
modelId: string,
isCurrentlyRecommended: boolean,
) => {
try {
await setModelRecommended({
variables: { modelId, recommended: !isCurrentlyRecommended },
refetchQueries: [{ query: GET_ADMIN_AI_MODELS }],
});
await refetchClientConfig();
} catch {
enqueueErrorSnackBar({
message: t`Failed to update model recommendation`,
});
}
};
const defaultSmartModelId = data?.getAdminAiModels?.defaultSmartModelId;
const defaultFastModelId = data?.getAdminAiModels?.defaultFastModelId;
const enabledModels = models.filter(
(model) => model.isAvailable && model.isAdminEnabled && !model.isDeprecated,
);
const availableModelOptions = enabledModels.map((model) => ({
value: model.modelId,
label: model.label,
Icon: getModelIcon(model.modelFamily, model.providerName),
}));
const handleDefaultModelChange = async (
role: AiModelRole,
modelId: string,
) => {
try {
await setDefaultModel({
variables: { role, modelId },
refetchQueries: [{ query: GET_ADMIN_AI_MODELS }],
});
await refetchClientConfig();
} catch {
enqueueErrorSnackBar({
message: t`Failed to update default model`,
});
}
};
return (
<>
<Section>
<H2Title
title={t`Providers`}
description={t`Built-in providers activated by API key. Click to manage models.`}
/>
<SettingsAdminAiProviderListCard
providers={catalogProviders}
showAddButton={false}
/>
</Section>
<Section>
<H2Title
title={t`Custom Providers`}
description={t`Add custom endpoints, private gateways, or additional regions.`}
adornment={
<Tag
text={t`Enterprise`}
color="transparent"
Icon={IconLock}
variant="border"
/>
}
/>
<SettingsAdminAiProviderListCard
providers={customProviders}
showAddButton
/>
</Section>
{availableModelOptions.length > 0 && (
<Section>
<H2Title
title={t`Default Models`}
description={t`Configure the default AI models for all workspaces`}
/>
<Card rounded>
<SettingsOptionCardContentSelect
Icon={IconRobot}
title={t`Smart Model`}
description={t`Default model for chats and complex reasoning`}
>
<Select
dropdownId="admin-smart-model-select"
value={defaultSmartModelId ?? undefined}
onChange={(value: string) =>
handleDefaultModelChange(AiModelRole.SMART, value)
}
options={availableModelOptions}
selectSizeVariant="small"
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
/>
</SettingsOptionCardContentSelect>
<SettingsOptionCardContentSelect
Icon={IconBolt}
title={t`Fast Model`}
description={t`Default model for lightweight tasks`}
>
<Select
dropdownId="admin-fast-model-select"
value={defaultFastModelId ?? undefined}
onChange={(value: string) =>
handleDefaultModelChange(AiModelRole.FAST, value)
}
options={availableModelOptions}
selectSizeVariant="small"
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
/>
</SettingsOptionCardContentSelect>
</Card>
</Section>
)}
{enabledModels.length > 0 && (
<Section>
<H2Title
title={t`Recommended Models`}
description={t`Select which models appear as recommended in the workspace model picker`}
/>
<SettingsAiModelsTable
models={enabledModels}
isChecked={(model) => model.isRecommended === true}
onToggle={handleRecommendedToggle}
onToggleAll={async (shouldCheckAll) => {
const modelIds = enabledModels
.filter(
(model) => (model.isRecommended === true) !== shouldCheckAll,
)
.map((model) => model.modelId);
if (modelIds.length === 0) return;
try {
await setModelsRecommended({
variables: {
modelIds,
recommended: shouldCheckAll,
},
});
} catch {
enqueueErrorSnackBar({
message: t`Failed to update model recommendations`,
});
} finally {
await refetchModels();
await refetchClientConfig();
}
}}
anchorPrefix="recommended-model-row"
/>
</Section>
)}
<Section>
<H2Title
title={t`AI Usage by Workspace`}
description={t`AI consumption across all workspaces.`}
adornment={
hasEnterpriseAccess ? (
<Select
dropdownId="admin-ai-usage-period"
value={usagePeriod}
options={periodOptions}
onChange={setUsagePeriod}
needIconCheck
selectSizeVariant="small"
/>
) : (
<Tag
text={t`Enterprise`}
color="transparent"
Icon={IconLock}
variant="border"
/>
)
}
/>
{hasEnterpriseAccess ? (
usageByWorkspace.length > 0 ? (
<Table>
<TableRow gridTemplateColumns={USAGE_TABLE_GRID_TEMPLATE_COLUMNS}>
<TableHeader>{t`Workspace`}</TableHeader>
<TableHeader align="right">{t`Usage`}</TableHeader>
</TableRow>
{usageByWorkspace.map((item) => (
<TableRow
key={item.key}
gridTemplateColumns={USAGE_TABLE_GRID_TEMPLATE_COLUMNS}
to={getSettingsPath(SettingsPath.AdminPanelWorkspaceDetail, {
workspaceId: item.key,
})}
>
<TableCell color={themeCssVariables.font.color.primary}>
{item.label ?? item.key}
</TableCell>
<TableCell align="right">
{formatUsageValue(item.creditsUsed)}
</TableCell>
</TableRow>
))}
</Table>
) : (
<Card rounded>
<TableRow gridTemplateColumns="1fr">
<TableCell
color={themeCssVariables.font.color.tertiary}
align="center"
>
{t`No AI usage data recorded yet.`}
</TableCell>
</TableRow>
</Card>
)
) : (
<SettingsEnterpriseFeatureGateCard
description={t`AI usage analytics across workspaces is available with an Enterprise key.`}
/>
)}
</Section>
</>
);
};