Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions front/components/workspace/analytics/UsageFilterPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getModelEffortTier } from "@app/components/model_picker/modelPickerUtils";
import type {
UsageFilter,
UsageFilterAgentOption,
Expand All @@ -16,6 +17,7 @@ import {
USAGE_FILTER_CATEGORIES,
USAGE_FILTER_CATEGORY_LABEL,
USAGE_MODEL_TIERS,
usageModelTierFromModelsTierName,
} from "@app/components/workspace/analytics/usageFilter";
import { UsageFilterAgentScopeControls } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterAgentScopeControls";
import { UsageFilterCategoryNav } from "@app/components/workspace/analytics/usageFilterPanel/UsageFilterCategoryNav";
Expand All @@ -29,8 +31,11 @@ import { useToggleSelectionList } from "@app/hooks/useToggleSelectionList";
import { useAgentConfigurations } from "@app/lib/swr/assistants";
import { useGroups } from "@app/lib/swr/groups";
import { useSearchMembers } from "@app/lib/swr/memberships";
import { useModels } from "@app/lib/swr/models";
import type { AgentConfigurationScope } from "@app/types/assistant/agent";
import { AGENT_CONFIGURATION_SCOPES } from "@app/types/assistant/agent";
import { isModelStreamId } from "@app/types/assistant/models/auto";
import { getModelMaker } from "@app/types/assistant/models/providers";
import { MANAGEABLE_GROUP_KINDS } from "@app/types/groups";
import { assertNever } from "@app/types/shared/utils/assert_never";
import type { LightWorkspaceType } from "@app/types/user";
Expand All @@ -56,14 +61,13 @@ interface UsageFilterPaginationState {

interface UsageFilterPanelProps {
owner: LightWorkspaceType;
// Models/tools/skills/sources are still mock data (see
// usageFilterMockData.ts — sources are fake connectors standing in for a
// real db call); agents come from the same workspace-wide listing the rest
// of the app uses (useAgentConfigurations), members and teams via the
// generic member search and group listing endpoints (useSearchMembers,
// useGroups).
// Tools/skills/sources are still mock data (see usageFilterMockData.ts —
// sources are fake connectors standing in for a real db call); agents come
// from useAgentConfigurations, members from useSearchMembers, teams from
// useGroups, and models from the workspace's full model catalog
// (useModels) — the same endpoint that backs the model picker elsewhere in
// the app.
categoryOptions: {
model: UsageFilterModelOption[];
tool: UsageFilterToolOption[];
skill: UsageFilterSkillOption[];
source: UsageFilterSourceOption[];
Expand Down Expand Up @@ -106,6 +110,7 @@ export function UsageFilterPanel({
const isMemberCategoryActive = isOpen && activeCategory === "member";
const isTeamCategoryActive = isOpen && activeCategory === "team";
const isAgentCategoryActive = isOpen && activeCategory === "agent";
const isModelCategoryActive = isOpen && activeCategory === "model";

// Every category picker supports scroll-to-load-more:
const [memberPageIndex, setMemberPageIndex] = useState(0);
Expand Down Expand Up @@ -185,6 +190,15 @@ export function UsageFilterPanel({
disabled: !isAgentCategoryActive,
});

// The workspace's full, period-independent model catalog — the same
// endpoint backing the model picker elsewhere in the app — rather than a
// period-scoped top-N, so every enabled model is listable and searchable
// regardless of the selected period.
const { models: modelCatalog } = useModels({
owner,
disabled: !isModelCategoryActive,
});

const groups = useMemo<UsageFilterGroup[]>(
() =>
workspaceGroups.map((group) => ({
Expand Down Expand Up @@ -217,6 +231,27 @@ export function UsageFilterPanel({
[agentConfigurations]
);

// Every enabled model in the workspace, regardless of the selected period.
// Excludes the auto/meta stream ids (Fast/Standard/Complex are exposed as
// the quick-filter tier buttons, not as catalog entries). Search is
// applied client-side below; tier is derived from the same static table
// ModelsTierResource.getTierForModel resolves server-side.
const modelCatalogOptions = useMemo<UsageFilterModelOption[]>(
() =>
modelCatalog
.filter((model) => !isModelStreamId(model.modelId))
.map((model) => ({
id: model.modelId,
name: model.displayName,
kind: "model" as const,
lab: getModelMaker(model),
tier: usageModelTierFromModelsTierName(
getModelEffortTier(model.modelId, model.defaultReasoningEffort)
),
})),
[modelCatalog]
);

const resolvedCategoryOptions = useMemo<{
[C in UsageFilterCategory]: UsageFilterOptionForCategory<C>[];
}>(
Expand All @@ -225,8 +260,15 @@ export function UsageFilterPanel({
member: accumulatedMemberOptions,
team: teamOptions,
agent: agentOptions,
model: modelCatalogOptions,
}),
[categoryOptions, accumulatedMemberOptions, teamOptions, agentOptions]
[
categoryOptions,
accumulatedMemberOptions,
teamOptions,
agentOptions,
modelCatalogOptions,
]
);

const activeOptions = resolvedCategoryOptions[activeCategory];
Expand Down Expand Up @@ -431,7 +473,7 @@ export function UsageFilterPanel({
)}
{activeCategory === "model" && (
<UsageFilterModelComplexityControls
models={categoryOptions.model}
moreModelsCatalog={modelCatalogOptions}
selectedModelIds={selectedIdsForActiveCategory}
onToggleModel={(model) => toggleOption("model", model)}
activeTier={activeTier}
Expand Down
41 changes: 38 additions & 3 deletions front/components/workspace/analytics/usageFilter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { ConsumptionScopeFilter } from "@app/lib/api/analytics/consumption/scope";
import type { ModelsTierName } from "@app/lib/api/assistant/token_pricing/tiers";
import type { AgentConfigurationScope } from "@app/types/assistant/agent";
import type { ModelMakerIdType } from "@app/types/assistant/models/types";
import type { ConnectorProvider } from "@app/types/data_source";
import { assertNeverAndIgnore } from "@app/types/shared/utils/assert_never";

export const USAGE_FILTER_CATEGORIES = [
"agent",
Expand Down Expand Up @@ -71,7 +73,10 @@ export interface UsageFilterSourceOption extends UsageFilterOptionBase {
export interface UsageFilterModelOption extends UsageFilterOptionBase {
kind: "model";
lab: ModelMakerIdType;
tier: UsageModelTier;
// Undefined for a model outside the static tier table — it doesn't match
// any Fast/Standard/Complex quick filter, so it's absent from the main
// checklist but still reachable through the "More models" browse dropdown.
tier: UsageModelTier | undefined;
}

export interface UsageFilterToolOption extends UsageFilterOptionBase {
Expand Down Expand Up @@ -146,8 +151,9 @@ export function selectAllUsageFilterOptions<C extends UsageFilterCategory>(
return { ...filter, [category]: [...current, ...additions] };
}

// Members, teams, and agents are wired to real consumption scope dimensions.
// The other categories stay mock data and are not sent as query filters yet.
// Members, teams, agents, and models are wired to real consumption scope
// dimensions. The other categories stay mock data and are not sent as query
// filters yet.
export function toConsumptionScopeFilter(
filter: UsageFilter
): ConsumptionScopeFilter {
Expand All @@ -168,5 +174,34 @@ export function toConsumptionScopeFilter(
scopeFilter.agents = agentIds;
}

const modelIds = filter.model?.map((entity) => entity.id);
if (modelIds && modelIds.length > 0) {
scopeFilter.models = modelIds;
}

return scopeFilter;
}

// Maps the backend's reasoning-effort-aware pricing tier onto the filter
// panel's simpler Fast/Standard/Complex bucket. Null propagates (a model
// outside the static tier table, or a raw catalog entry with no config match)
// as "no bucket", so it's excluded from every quick-filter tier rather than
// landing in one arbitrarily.
export function usageModelTierFromModelsTierName(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be discussed @flvndvd @ClementAupiais.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

material: #29299

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably needs some alignment with what we're doing with the model picker, we should not introduce a new wording users are not familiar with here

tier: ModelsTierName | null | undefined
): UsageModelTier | undefined {
if (tier === null || tier === undefined) {
return undefined;
}
switch (tier) {
case "cost_efficient":
return "fast";
case "balanced":
return "standard";
case "premium":
return "complex";
default:
Comment thread
avervaet marked this conversation as resolved.
assertNeverAndIgnore(tier);
return undefined;
}
}
49 changes: 3 additions & 46 deletions front/components/workspace/analytics/usageFilterMockData.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,15 @@
import type {
UsageFilterModelOption,
UsageFilterSkillOption,
UsageFilterSourceOption,
UsageFilterToolOption,
} from "@app/components/workspace/analytics/usageFilter";
import { USAGE_MODEL_TIERS } from "@app/components/workspace/analytics/usageFilter";
import type { ModelMakerIdType } from "@app/types/assistant/models/types";
import type { ConnectorProvider } from "@app/types/data_source";

const MOCK_MODEL_LAB: Record<string, ModelMakerIdType> = {
"Claude Sonnet 5": "anthropic",
"Claude Opus 5": "anthropic",
"Claude Haiku 4.5": "anthropic",
"Claude Fable 5": "anthropic",
"GPT-5": "openai",
"GPT-5 mini": "openai",
"Gemini 3 Pro": "google_ai_studio",
"Gemini 3 Flash": "google_ai_studio",
"Llama 4 Maverick": "fireworks",
"Mistral Large 3": "mistral",
"Grok 4": "xai",
"DeepSeek V4": "deepseek",
};

// Placeholder data for categories not yet wired to a real backend endpoint.
// Agents are fetched live in UsageFilterPanel (useConsumptionTop); members
// and groups via useSearchMembers and useGroups. Lists are long enough to
// exercise scrolling in the preview.
// Agents are fetched live in UsageFilterPanel (useAgentConfigurations);
// members via useSearchMembers; groups via useGroups; models via useModels.
// Lists are long enough to exercise scrolling in the preview.
const MOCK_ENTITY_NAMES = {
model: [
"Claude Sonnet 5",
"Claude Opus 5",
"Claude Haiku 4.5",
"Claude Fable 5",
"GPT-5",
"GPT-5 mini",
"Gemini 3 Pro",
"Gemini 3 Flash",
"Llama 4 Maverick",
"Mistral Large 3",
"Grok 4",
"DeepSeek V4",
],
tool: [
"web_search",
"file_search",
Expand Down Expand Up @@ -100,16 +68,6 @@ const MOCK_SOURCE_CONNECTORS: Array<{
{ name: "Uploaded files — Legal templates", connectorProvider: undefined },
];

function buildModelOptions(names: string[]): UsageFilterModelOption[] {
return names.map((name, index) => ({
id: `model_${index + 1}`,
name,
kind: "model",
lab: MOCK_MODEL_LAB[name],
tier: USAGE_MODEL_TIERS[index % USAGE_MODEL_TIERS.length],
}));
}

function buildToolOptions(names: string[]): UsageFilterToolOption[] {
return names.map((name, index) => ({
id: `tool_${index + 1}`,
Expand All @@ -127,7 +85,6 @@ function buildSkillOptions(names: string[]): UsageFilterSkillOption[] {
}

export const USAGE_FILTER_MOCK_OPTIONS = {
model: buildModelOptions(MOCK_ENTITY_NAMES.model),
tool: buildToolOptions(MOCK_ENTITY_NAMES.tool),
skill: buildSkillOptions(MOCK_ENTITY_NAMES.skill),
source: MOCK_SOURCE_CONNECTORS.map<UsageFilterSourceOption>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ const MODEL_TIER_ICON: Record<UsageModelTier, ComponentType> = {
};

interface UsageFilterModelComplexityControlsProps {
models: UsageFilterModelOption[];
moreModelsCatalog: UsageFilterModelOption[];
selectedModelIds: Set<string>;
onToggleModel: (model: UsageFilterModelOption) => void;
activeTier: UsageModelTier;
onTierChange: (tier: UsageModelTier) => void;
}

export function UsageFilterModelComplexityControls({
models,
moreModelsCatalog,
selectedModelIds,
onToggleModel,
activeTier,
Expand Down Expand Up @@ -75,13 +75,13 @@ export function UsageFilterModelComplexityControls({
const isSearchingMoreModels = moreModelsQuery !== "";

const moreModelsSearchResults = isSearchingMoreModels
? models.filter((model) =>
? moreModelsCatalog.filter((model) =>
model.name.toLowerCase().includes(moreModelsQuery)
)
: [];

const moreModelsGroups = MODEL_MAKER_IDS.flatMap((lab) => {
const labModels = models.filter((model) => model.lab === lab);
const labModels = moreModelsCatalog.filter((model) => model.lab === lab);
return labModels.length > 0 ? [{ lab, models: labModels }] : [];
});

Expand Down
2 changes: 2 additions & 0 deletions front/hooks/useConsumptionTop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ function toRows(data: ConsumptionTopResponse): ConsumptionTopRow[] {
id: row.teamId,
name: row.name,
pictureUrl: null,
modelMaker: null,
tier: null,
credits: row.credits,
avgCredits: row.avgCreditsPerMessage,
}));
Expand Down