Skip to content

Commit d8cfa7c

Browse files
authored
[OPIK-3487] [FE] Resolve user feedback for dashboard before first public beta release (#4463)
1 parent e3c6eab commit d8cfa7c

File tree

10 files changed

+389
-174
lines changed

10 files changed

+389
-174
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import React, { useCallback, useMemo, useState } from "react";
2+
import { keepPreviousData } from "@tanstack/react-query";
3+
4+
import LoadableSelectBox from "@/components/shared/LoadableSelectBox/LoadableSelectBox";
5+
import useFeedbackDefinitionsList from "@/api/feedback-definitions/useFeedbackDefinitionsList";
6+
import useTracesFeedbackScoresNames from "@/api/traces/useTracesFeedbackScoresNames";
7+
import useThreadsFeedbackScoresNames from "@/api/traces/useThreadsFeedbackScoresNames";
8+
import useExperimentsFeedbackScoresNames from "@/api/datasets/useExperimentsFeedbackScoresNames";
9+
import useSpansFeedbackScoresNames from "@/api/traces/useSpansFeedbackScoresNames";
10+
import { DropdownOption } from "@/types/shared";
11+
import useAppStore from "@/store/AppStore";
12+
13+
const DEFAULT_LOADED_FEEDBACK_DEFINITION_ITEMS = 1000;
14+
15+
export enum ScoreSource {
16+
TRACES = "traces",
17+
THREADS = "threads",
18+
EXPERIMENTS = "experiments",
19+
SPANS = "spans",
20+
}
21+
22+
export type ScoreSourceType = ScoreSource;
23+
24+
interface BaseFeedbackDefinitionsAndScoresSelectBoxProps {
25+
className?: string;
26+
disabled?: boolean;
27+
scoreSource: ScoreSourceType;
28+
entityIds?: string[];
29+
placeholder?: string;
30+
}
31+
32+
interface SingleSelectProps
33+
extends BaseFeedbackDefinitionsAndScoresSelectBoxProps {
34+
value: string;
35+
onChange: (value: string) => void;
36+
multiselect?: false;
37+
}
38+
39+
interface MultiSelectProps
40+
extends BaseFeedbackDefinitionsAndScoresSelectBoxProps {
41+
value: string[];
42+
onChange: (value: string[]) => void;
43+
multiselect: true;
44+
showSelectAll?: boolean;
45+
}
46+
47+
export type FeedbackDefinitionsAndScoresSelectBoxProps =
48+
| SingleSelectProps
49+
| MultiSelectProps;
50+
51+
const FeedbackDefinitionsAndScoresSelectBox: React.FC<
52+
FeedbackDefinitionsAndScoresSelectBoxProps
53+
> = (props) => {
54+
const { className, disabled, scoreSource, entityIds, placeholder } = props;
55+
const workspaceName = useAppStore((state) => state.activeWorkspaceName);
56+
const [isLoadedMore, setIsLoadedMore] = useState(false);
57+
58+
// Extract IDs based on score source
59+
const projectId = entityIds?.[0]; // For traces/threads/spans (single project)
60+
const experimentsIds = entityIds; // For experiments (array)
61+
62+
// Fetch feedback definitions
63+
const { data: feedbackDefinitionsData, isLoading: isLoadingDefinitions } =
64+
useFeedbackDefinitionsList(
65+
{
66+
workspaceName,
67+
page: 1,
68+
size: isLoadedMore ? 10000 : DEFAULT_LOADED_FEEDBACK_DEFINITION_ITEMS,
69+
},
70+
{
71+
placeholderData: keepPreviousData,
72+
},
73+
);
74+
75+
const { data: traceScoresData, isLoading: isLoadingTraceScores } =
76+
useTracesFeedbackScoresNames(
77+
{ projectId },
78+
{
79+
enabled: scoreSource === ScoreSource.TRACES,
80+
placeholderData: keepPreviousData,
81+
},
82+
);
83+
84+
const { data: threadScoresData, isLoading: isLoadingThreadScores } =
85+
useThreadsFeedbackScoresNames(
86+
{ projectId },
87+
{
88+
enabled: scoreSource === ScoreSource.THREADS,
89+
placeholderData: keepPreviousData,
90+
},
91+
);
92+
93+
const { data: experimentScoresData, isLoading: isLoadingExperimentScores } =
94+
useExperimentsFeedbackScoresNames(
95+
{ experimentsIds },
96+
{
97+
enabled: scoreSource === ScoreSource.EXPERIMENTS,
98+
placeholderData: keepPreviousData,
99+
},
100+
);
101+
102+
const { data: spanScoresData, isLoading: isLoadingSpanScores } =
103+
useSpansFeedbackScoresNames(
104+
{ projectId },
105+
{
106+
enabled: scoreSource === ScoreSource.SPANS,
107+
placeholderData: keepPreviousData,
108+
},
109+
);
110+
111+
const total = feedbackDefinitionsData?.total ?? 0;
112+
113+
const loadMoreHandler = useCallback(() => setIsLoadedMore(true), []);
114+
115+
const options: DropdownOption<string>[] = useMemo(() => {
116+
const definitionNames = new Map<
117+
string,
118+
{ name: string; description?: string }
119+
>();
120+
121+
(feedbackDefinitionsData?.content || []).forEach((def) => {
122+
definitionNames.set(def.name, {
123+
name: def.name,
124+
description: def.description,
125+
});
126+
});
127+
128+
const scoreDataBySource = {
129+
[ScoreSource.TRACES]: traceScoresData?.scores,
130+
[ScoreSource.THREADS]: threadScoresData?.scores,
131+
[ScoreSource.EXPERIMENTS]: experimentScoresData?.scores,
132+
[ScoreSource.SPANS]: spanScoresData?.scores,
133+
};
134+
135+
const scoreNames = scoreDataBySource[scoreSource] || [];
136+
137+
scoreNames.forEach((score) => {
138+
if (!definitionNames.has(score.name)) {
139+
definitionNames.set(score.name, { name: score.name });
140+
}
141+
});
142+
143+
return Array.from(definitionNames.values())
144+
.sort((a, b) => a.name.localeCompare(b.name))
145+
.map((item) => ({
146+
value: item.name,
147+
label: item.name,
148+
description: item.description,
149+
}));
150+
}, [
151+
feedbackDefinitionsData?.content,
152+
traceScoresData?.scores,
153+
threadScoresData?.scores,
154+
experimentScoresData?.scores,
155+
spanScoresData?.scores,
156+
scoreSource,
157+
]);
158+
159+
const loadingStatesBySource = {
160+
[ScoreSource.TRACES]: isLoadingTraceScores,
161+
[ScoreSource.THREADS]: isLoadingThreadScores,
162+
[ScoreSource.EXPERIMENTS]: isLoadingExperimentScores,
163+
[ScoreSource.SPANS]: isLoadingSpanScores,
164+
};
165+
166+
const isLoading = isLoadingDefinitions || loadingStatesBySource[scoreSource];
167+
168+
const loadableSelectBoxProps = props.multiselect
169+
? {
170+
options,
171+
value: props.value,
172+
placeholder: placeholder || "Select scores",
173+
onChange: props.onChange,
174+
multiselect: true as const,
175+
showSelectAll: props.showSelectAll,
176+
}
177+
: {
178+
options,
179+
value: props.value,
180+
placeholder: placeholder || "Select score",
181+
onChange: props.onChange,
182+
multiselect: false as const,
183+
};
184+
185+
return (
186+
<LoadableSelectBox
187+
{...loadableSelectBoxProps}
188+
onLoadMore={
189+
total > DEFAULT_LOADED_FEEDBACK_DEFINITION_ITEMS && !isLoadedMore
190+
? loadMoreHandler
191+
: undefined
192+
}
193+
buttonClassName={className}
194+
disabled={disabled}
195+
isLoading={isLoading}
196+
optionsCount={DEFAULT_LOADED_FEEDBACK_DEFINITION_ITEMS}
197+
showTooltip
198+
minWidth={280}
199+
align="start"
200+
/>
201+
);
202+
};
203+
204+
export default FeedbackDefinitionsAndScoresSelectBox;

apps/opik-frontend/src/components/pages/AlertsPage/AddEditAlertPage/FeedbackScoreConditions.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import { FormControl, FormField, FormItem } from "@/components/ui/form";
88
import { Button } from "@/components/ui/button";
99
import { Input } from "@/components/ui/input";
1010
import SelectBox from "@/components/shared/SelectBox/SelectBox";
11-
import AlertFeedbackScoresSelect from "@/components/shared/AlertFeedbackScoresSelect/AlertFeedbackScoresSelect";
11+
import FeedbackDefinitionsAndScoresSelectBox, {
12+
ScoreSource,
13+
} from "@/components/pages-shared/experiments/FeedbackDefinitionsAndScoresSelectBox/FeedbackDefinitionsAndScoresSelectBox";
1214
import { DropdownOption } from "@/types/shared";
1315
import { AlertFormType, FeedbackScoreConditionType } from "./schema";
1416
import { ALERT_EVENT_TYPE } from "@/types/alerts";
@@ -55,6 +57,13 @@ const FeedbackScoreConditions: React.FC<FeedbackScoreConditionsProps> = ({
5557
name: `triggers.${triggerIndex}.conditions` as "triggers.0.conditions",
5658
});
5759

60+
const scoreSource =
61+
eventType === ALERT_EVENT_TYPE.trace_feedback_score
62+
? ScoreSource.TRACES
63+
: eventType === ALERT_EVENT_TYPE.trace_thread_feedback_score
64+
? ScoreSource.THREADS
65+
: ScoreSource.TRACES;
66+
5867
const addCondition = () => {
5968
conditionsFieldArray.append(DEFAULT_FEEDBACK_SCORE_CONDITION);
6069
};
@@ -128,10 +137,11 @@ const FeedbackScoreConditions: React.FC<FeedbackScoreConditionsProps> = ({
128137
</Label>
129138
)}
130139
<FormControl>
131-
<AlertFeedbackScoresSelect
140+
<FeedbackDefinitionsAndScoresSelectBox
132141
value={field.value as string}
133142
onChange={field.onChange}
134-
eventType={eventType}
143+
scoreSource={scoreSource}
144+
multiselect={false}
135145
className={cn("h-8 rounded-r-none", {
136146
"border-destructive": Boolean(
137147
validationErrors?.message,

apps/opik-frontend/src/components/shared/AlertFeedbackScoresSelect/AlertFeedbackScoresSelect.tsx

Lines changed: 0 additions & 134 deletions
This file was deleted.

apps/opik-frontend/src/components/shared/Dashboard/DashboardWidget/DashboardWidgetHeader.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ const DashboardWidgetHeader: React.FunctionComponent<
5656
<div className="flex items-start gap-2">
5757
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
5858
<div className="flex items-center gap-1.5">
59-
<div className="truncate text-xs font-medium text-foreground">
60-
{title}
61-
</div>
6259
{warningMessage && (
6360
<TooltipWrapper content={warningMessage}>
6461
<TriangleAlert className="size-3 shrink-0 text-amber-500" />
6562
</TooltipWrapper>
6663
)}
64+
<div className="truncate text-xs font-medium text-foreground">
65+
{title}
66+
</div>
6767
{infoMessage && (
6868
<TooltipWrapper content={infoMessage}>
6969
<Info className="size-3 shrink-0 text-light-slate" />

0 commit comments

Comments
 (0)